diff --git a/src/Utils/AbiBase.php b/src/Utils/AbiBase.php index 71cf80d..cd360f4 100644 --- a/src/Utils/AbiBase.php +++ b/src/Utils/AbiBase.php @@ -11,12 +11,27 @@ abstract class AbiBase { protected array $abi; + protected array $functionSelectorMap = []; + + protected array $errorSelectorMap = []; + public function __construct(ContractAbiType $type = ContractAbiType::CONSENSUS, ?string $path = null) { $abiFilePath = self::contractAbiPath($type, $path); $decodedAbi = self::loadAbiJson($abiFilePath); $this->abi = $decodedAbi['abi']; + + foreach ($this->abi as $item) { + $signature = $this->getFunctionSignature($item); + $selector = substr($this->keccak256($signature), 2, 8); + + if ($item['type'] === 'function') { + $this->functionSelectorMap[$selector] = $item; + } elseif ($item['type'] === 'error') { + $this->errorSelectorMap[$selector] = $item; + } + } } public static function methodIdentifiers( diff --git a/src/Utils/AbiDecoder.php b/src/Utils/AbiDecoder.php index b8a97f3..5747065 100644 --- a/src/Utils/AbiDecoder.php +++ b/src/Utils/AbiDecoder.php @@ -165,36 +165,12 @@ public static function decodeFunctionWithAbi(string $functionSignature, string $ private function findFunctionBySelector(string $selector): ?array { - foreach ($this->abi as $item) { - if ($item['type'] !== 'function') { - continue; - } - - $functionSignature = $this->getFunctionSignature($item); - $functionSelector = substr($this->keccak256($functionSignature), 2, 8); - if ($functionSelector === $selector) { - return $item; - } - } - - return null; + return $this->functionSelectorMap[$selector] ?? null; } private function findErrorBySelector(string $selector): ?array { - foreach ($this->abi as $item) { - if ($item['type'] !== 'error') { - continue; - } - - $errorSignature = $this->getFunctionSignature($item); - $errorSelector = substr($this->keccak256($errorSignature), 2, 8); - if ($errorSelector === $selector) { - return $item; - } - } - - return null; + return $this->errorSelectorMap[$selector] ?? null; } private function decodeAbiParameters(array $params, string $data): array diff --git a/src/Utils/Address.php b/src/Utils/Address.php index 77bd7ac..e15feb4 100644 --- a/src/Utils/Address.php +++ b/src/Utils/Address.php @@ -10,6 +10,8 @@ class Address { + private static array $cache = []; + /** * Validate the given address. * @@ -32,18 +34,24 @@ public static function validate(string $address): bool */ public static function toChecksumAddress(string $address): string { - $address = strtolower(substr($address, 2)); - $hash = Keccak::hash($address, 256); + if (isset(self::$cache[$address])) { + return self::$cache[$address]; + } + + $rawAddress = strtolower(substr($address, 2)); + $hash = Keccak::hash($rawAddress, 256); $checksumAddress = '0x'; for ($i = 0; $i < 40; $i++) { if (intval($hash[$i], 16) >= 8) { - $checksumAddress .= strtoupper($address[$i]); + $checksumAddress .= strtoupper($rawAddress[$i]); } else { - $checksumAddress .= $address[$i]; + $checksumAddress .= $rawAddress[$i]; } } + self::$cache[$address] = $checksumAddress; + return $checksumAddress; } diff --git a/tests/Unit/Utils/AbiDecoderTest.php b/tests/Unit/Utils/AbiDecoderTest.php index 2868dc3..da40af8 100644 --- a/tests/Unit/Utils/AbiDecoderTest.php +++ b/tests/Unit/Utils/AbiDecoderTest.php @@ -4,6 +4,7 @@ use ArkEcosystem\Crypto\Enums\ContractAbiType; use ArkEcosystem\Crypto\Utils\AbiDecoder; +use kornrunner\Keccak; it('should decode vote payload', function () { $decoder = new AbiDecoder(); @@ -373,3 +374,43 @@ $decoder->decodeError('123456'); })->throws(Exception::class, 'Function selector not found in ABI: 123456'); + +test('should precompute selector maps for custom abi items', function () { + $decoder = new AbiDecoder(ContractAbiType::CUSTOM, dirname(__DIR__, 2).'/fixtures/mock-abi-selectors.json'); + + $reflector = new ReflectionObject($decoder); + $functions = $reflector->getProperty('functionSelectorMap'); + $errors = $reflector->getProperty('errorSelectorMap'); + $functions->setAccessible(true); + $errors->setAccessible(true); + + $functionSelector = substr(Keccak::hash('transfer(address,uint256)', 256), 0, 8); + $errorSelector = substr(Keccak::hash('InsufficientBalance(uint256,uint256)', 256), 0, 8); + + $functionMap = $functions->getValue($decoder); + $errorMap = $errors->getValue($decoder); + + expect($functionMap)->toHaveKey($functionSelector); + expect($functionMap[$functionSelector]['name'])->toBe('transfer'); + expect($errorMap)->toHaveKey($errorSelector); + expect($errorMap[$errorSelector]['name'])->toBe('InsufficientBalance'); +}); + +test('should decode function and error payloads using custom selector maps', function () { + $decoder = new AbiDecoder(ContractAbiType::CUSTOM, dirname(__DIR__, 2).'/fixtures/mock-abi-selectors.json'); + + $functionSelector = substr(Keccak::hash('transfer(address,uint256)', 256), 0, 8); + $errorSelector = substr(Keccak::hash('InsufficientBalance(uint256,uint256)', 256), 0, 8); + + $to = 'b693449adda7efc015d87944eae8b7c37eb1690a'; + $amount = str_pad(dechex(7), 64, '0', STR_PAD_LEFT); + $data = '0x'.$functionSelector.str_pad($to, 64, '0', STR_PAD_LEFT).$amount; + $decoded = $decoder->decodeFunctionData($data); + $abiError = $decoder->decodeError('0x'.$errorSelector); + + expect($decoded)->toBe([ + 'functionName' => 'transfer', + 'args' => ['0xb693449AdDa7EFc015D87944EAE8b7C37EB1690A', '7'], + ]); + expect($abiError)->toBe('InsufficientBalance'); +}); diff --git a/tests/Unit/Utils/AddressTest.php b/tests/Unit/Utils/AddressTest.php index b70e8da..cfa6b2e 100644 --- a/tests/Unit/Utils/AddressTest.php +++ b/tests/Unit/Utils/AddressTest.php @@ -34,3 +34,24 @@ expect($actual)->toBe($fixture['data']['address']); }); + +it('should convert to checksum address and cache the result', function () { + $address = '0xb693449adda7efc015d87944eae8b7c37eb1690a'; + $expected = '0xb693449AdDa7EFc015D87944EAE8b7C37EB1690A'; + + $reflector = new ReflectionClass(TestClass::class); + $cache = $reflector->getProperty('cache'); + $cache->setAccessible(true); + $cache->setValue(null, []); + + $actual = TestClass::toChecksumAddress($address); + + expect($actual)->toBe($expected); + expect($cache->getValue())->toHaveCount(1); + expect($cache->getValue())->toHaveKey($address); + + $cached = TestClass::toChecksumAddress($address); + + expect($cached)->toBe($expected); + expect($cache->getValue())->toHaveCount(1); +}); diff --git a/tests/fixtures/mock-abi-selectors.json b/tests/fixtures/mock-abi-selectors.json new file mode 100644 index 0000000..c4c173d --- /dev/null +++ b/tests/fixtures/mock-abi-selectors.json @@ -0,0 +1,38 @@ +{ + "abi": [ + { + "type": "function", + "name": "transfer", + "inputs": [ + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "error", + "name": "InsufficientBalance", + "inputs": [ + { + "name": "available", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "required", + "type": "uint256", + "internalType": "uint256" + } + ] + } + ] +}