Skip to content

Commit c098ca5

Browse files
authored
refactor: abi & keccak caching (#193)
1 parent ea01ea3 commit c098ca5

6 files changed

Lines changed: 129 additions & 30 deletions

File tree

src/Utils/AbiBase.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,27 @@ abstract class AbiBase
1111
{
1212
protected array $abi;
1313

14+
protected array $functionSelectorMap = [];
15+
16+
protected array $errorSelectorMap = [];
17+
1418
public function __construct(ContractAbiType $type = ContractAbiType::CONSENSUS, ?string $path = null)
1519
{
1620
$abiFilePath = self::contractAbiPath($type, $path);
1721
$decodedAbi = self::loadAbiJson($abiFilePath);
1822

1923
$this->abi = $decodedAbi['abi'];
24+
25+
foreach ($this->abi as $item) {
26+
$signature = $this->getFunctionSignature($item);
27+
$selector = substr($this->keccak256($signature), 2, 8);
28+
29+
if ($item['type'] === 'function') {
30+
$this->functionSelectorMap[$selector] = $item;
31+
} elseif ($item['type'] === 'error') {
32+
$this->errorSelectorMap[$selector] = $item;
33+
}
34+
}
2035
}
2136

2237
public static function methodIdentifiers(

src/Utils/AbiDecoder.php

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -165,36 +165,12 @@ public static function decodeFunctionWithAbi(string $functionSignature, string $
165165

166166
private function findFunctionBySelector(string $selector): ?array
167167
{
168-
foreach ($this->abi as $item) {
169-
if ($item['type'] !== 'function') {
170-
continue;
171-
}
172-
173-
$functionSignature = $this->getFunctionSignature($item);
174-
$functionSelector = substr($this->keccak256($functionSignature), 2, 8);
175-
if ($functionSelector === $selector) {
176-
return $item;
177-
}
178-
}
179-
180-
return null;
168+
return $this->functionSelectorMap[$selector] ?? null;
181169
}
182170

183171
private function findErrorBySelector(string $selector): ?array
184172
{
185-
foreach ($this->abi as $item) {
186-
if ($item['type'] !== 'error') {
187-
continue;
188-
}
189-
190-
$errorSignature = $this->getFunctionSignature($item);
191-
$errorSelector = substr($this->keccak256($errorSignature), 2, 8);
192-
if ($errorSelector === $selector) {
193-
return $item;
194-
}
195-
}
196-
197-
return null;
173+
return $this->errorSelectorMap[$selector] ?? null;
198174
}
199175

200176
private function decodeAbiParameters(array $params, string $data): array

src/Utils/Address.php

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
class Address
1212
{
13+
private static array $cache = [];
14+
1315
/**
1416
* Validate the given address.
1517
*
@@ -32,18 +34,24 @@ public static function validate(string $address): bool
3234
*/
3335
public static function toChecksumAddress(string $address): string
3436
{
35-
$address = strtolower(substr($address, 2));
36-
$hash = Keccak::hash($address, 256);
37+
if (isset(self::$cache[$address])) {
38+
return self::$cache[$address];
39+
}
40+
41+
$rawAddress = strtolower(substr($address, 2));
42+
$hash = Keccak::hash($rawAddress, 256);
3743
$checksumAddress = '0x';
3844

3945
for ($i = 0; $i < 40; $i++) {
4046
if (intval($hash[$i], 16) >= 8) {
41-
$checksumAddress .= strtoupper($address[$i]);
47+
$checksumAddress .= strtoupper($rawAddress[$i]);
4248
} else {
43-
$checksumAddress .= $address[$i];
49+
$checksumAddress .= $rawAddress[$i];
4450
}
4551
}
4652

53+
self::$cache[$address] = $checksumAddress;
54+
4755
return $checksumAddress;
4856
}
4957

tests/Unit/Utils/AbiDecoderTest.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use ArkEcosystem\Crypto\Enums\ContractAbiType;
66
use ArkEcosystem\Crypto\Utils\AbiDecoder;
7+
use kornrunner\Keccak;
78

89
it('should decode vote payload', function () {
910
$decoder = new AbiDecoder();
@@ -373,3 +374,43 @@
373374

374375
$decoder->decodeError('123456');
375376
})->throws(Exception::class, 'Function selector not found in ABI: 123456');
377+
378+
test('should precompute selector maps for custom abi items', function () {
379+
$decoder = new AbiDecoder(ContractAbiType::CUSTOM, dirname(__DIR__, 2).'/fixtures/mock-abi-selectors.json');
380+
381+
$reflector = new ReflectionObject($decoder);
382+
$functions = $reflector->getProperty('functionSelectorMap');
383+
$errors = $reflector->getProperty('errorSelectorMap');
384+
$functions->setAccessible(true);
385+
$errors->setAccessible(true);
386+
387+
$functionSelector = substr(Keccak::hash('transfer(address,uint256)', 256), 0, 8);
388+
$errorSelector = substr(Keccak::hash('InsufficientBalance(uint256,uint256)', 256), 0, 8);
389+
390+
$functionMap = $functions->getValue($decoder);
391+
$errorMap = $errors->getValue($decoder);
392+
393+
expect($functionMap)->toHaveKey($functionSelector);
394+
expect($functionMap[$functionSelector]['name'])->toBe('transfer');
395+
expect($errorMap)->toHaveKey($errorSelector);
396+
expect($errorMap[$errorSelector]['name'])->toBe('InsufficientBalance');
397+
});
398+
399+
test('should decode function and error payloads using custom selector maps', function () {
400+
$decoder = new AbiDecoder(ContractAbiType::CUSTOM, dirname(__DIR__, 2).'/fixtures/mock-abi-selectors.json');
401+
402+
$functionSelector = substr(Keccak::hash('transfer(address,uint256)', 256), 0, 8);
403+
$errorSelector = substr(Keccak::hash('InsufficientBalance(uint256,uint256)', 256), 0, 8);
404+
405+
$to = 'b693449adda7efc015d87944eae8b7c37eb1690a';
406+
$amount = str_pad(dechex(7), 64, '0', STR_PAD_LEFT);
407+
$data = '0x'.$functionSelector.str_pad($to, 64, '0', STR_PAD_LEFT).$amount;
408+
$decoded = $decoder->decodeFunctionData($data);
409+
$abiError = $decoder->decodeError('0x'.$errorSelector);
410+
411+
expect($decoded)->toBe([
412+
'functionName' => 'transfer',
413+
'args' => ['0xb693449AdDa7EFc015D87944EAE8b7C37EB1690A', '7'],
414+
]);
415+
expect($abiError)->toBe('InsufficientBalance');
416+
});

tests/Unit/Utils/AddressTest.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,24 @@
3434

3535
expect($actual)->toBe($fixture['data']['address']);
3636
});
37+
38+
it('should convert to checksum address and cache the result', function () {
39+
$address = '0xb693449adda7efc015d87944eae8b7c37eb1690a';
40+
$expected = '0xb693449AdDa7EFc015D87944EAE8b7C37EB1690A';
41+
42+
$reflector = new ReflectionClass(TestClass::class);
43+
$cache = $reflector->getProperty('cache');
44+
$cache->setAccessible(true);
45+
$cache->setValue(null, []);
46+
47+
$actual = TestClass::toChecksumAddress($address);
48+
49+
expect($actual)->toBe($expected);
50+
expect($cache->getValue())->toHaveCount(1);
51+
expect($cache->getValue())->toHaveKey($address);
52+
53+
$cached = TestClass::toChecksumAddress($address);
54+
55+
expect($cached)->toBe($expected);
56+
expect($cache->getValue())->toHaveCount(1);
57+
});
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"abi": [
3+
{
4+
"type": "function",
5+
"name": "transfer",
6+
"inputs": [
7+
{
8+
"name": "to",
9+
"type": "address",
10+
"internalType": "address"
11+
},
12+
{
13+
"name": "amount",
14+
"type": "uint256",
15+
"internalType": "uint256"
16+
}
17+
],
18+
"outputs": [],
19+
"stateMutability": "nonpayable"
20+
},
21+
{
22+
"type": "error",
23+
"name": "InsufficientBalance",
24+
"inputs": [
25+
{
26+
"name": "available",
27+
"type": "uint256",
28+
"internalType": "uint256"
29+
},
30+
{
31+
"name": "required",
32+
"type": "uint256",
33+
"internalType": "uint256"
34+
}
35+
]
36+
}
37+
]
38+
}

0 commit comments

Comments
 (0)