Skip to content

Commit ea01ea3

Browse files
authored
feat: transaction type identity implementation (#191)
1 parent ba96dfd commit ea01ea3

File tree

5 files changed

+285
-18
lines changed

5 files changed

+285
-18
lines changed

src/Utils/AbiBase.php

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,24 @@ abstract class AbiBase
1313

1414
public function __construct(ContractAbiType $type = ContractAbiType::CONSENSUS, ?string $path = null)
1515
{
16-
$abiFilePath = $this->contractAbiPath($type, $path);
16+
$abiFilePath = self::contractAbiPath($type, $path);
17+
$decodedAbi = self::loadAbiJson($abiFilePath);
1718

18-
$abiJson = file_get_contents($abiFilePath);
19+
$this->abi = $decodedAbi['abi'];
20+
}
21+
22+
public static function methodIdentifiers(
23+
ContractAbiType $type = ContractAbiType::CONSENSUS,
24+
?string $path = null
25+
): array {
26+
$abiFilePath = self::contractAbiPath($type, $path);
27+
$decodedAbi = self::loadAbiJson($abiFilePath);
28+
29+
if (! isset($decodedAbi['methodIdentifiers']) || ! is_array($decodedAbi['methodIdentifiers'])) {
30+
throw new \RuntimeException("ABI JSON does not contain methodIdentifiers: {$abiFilePath}");
31+
}
1932

20-
$this->abi = json_decode($abiJson, true)['abi'];
33+
return $decodedAbi['methodIdentifiers'];
2134
}
2235

2336
protected static function getArrayComponents(string $type): ?array
@@ -75,27 +88,38 @@ protected function toFunctionSelector(array $abiItem): string
7588
return $selector;
7689
}
7790

78-
private function contractAbiPath(ContractAbiType $type, ?string $path = null): ?string
91+
protected static function contractAbiPath(ContractAbiType $type, ?string $path = null): string
7992
{
80-
switch ($type) {
81-
case ContractAbiType::CONSENSUS:
82-
return __DIR__.'/Abi/json/Abi.Consensus.json';
83-
case ContractAbiType::MULTIPAYMENT:
84-
return __DIR__.'/Abi/json/Abi.Multipayment.json';
85-
case ContractAbiType::USERNAMES:
86-
return __DIR__.'/Abi/json/Abi.Usernames.json';
87-
case ContractAbiType::ERC20BATCH_TRANSFER:
88-
return __DIR__.'/Abi/json/Abi.ERC20BatchTransfer.json';
89-
case ContractAbiType::TOKEN:
90-
return __DIR__.'/Abi/json/Abi.Token.json';
91-
case ContractAbiType::CUSTOM:
93+
return match ($type) {
94+
ContractAbiType::CONSENSUS => __DIR__.'/Abi/json/Abi.Consensus.json',
95+
ContractAbiType::MULTIPAYMENT => __DIR__.'/Abi/json/Abi.Multipayment.json',
96+
ContractAbiType::USERNAMES => __DIR__.'/Abi/json/Abi.Usernames.json',
97+
ContractAbiType::ERC20BATCH_TRANSFER => __DIR__.'/Abi/json/Abi.ERC20BatchTransfer.json',
98+
ContractAbiType::TOKEN => __DIR__.'/Abi/json/Abi.Token.json',
99+
ContractAbiType::CUSTOM => (function () use ($path): string {
92100
if ($path === null || $path === '') {
93101
throw new \InvalidArgumentException('A non-empty $path must be provided when using ContractAbiType::CUSTOM.');
94102
}
95103

96104
return $path;
97-
default:
98-
throw new \InvalidArgumentException('Unhandled ContractAbiType: '.$type->name);
105+
})(),
106+
};
107+
}
108+
109+
private static function loadAbiJson(string $path): array
110+
{
111+
$rawJson = file_get_contents($path);
112+
113+
if ($rawJson === false) {
114+
throw new \RuntimeException("Unable to load ABI JSON: {$path}");
115+
}
116+
117+
$decoded = json_decode($rawJson, true);
118+
119+
if (! is_array($decoded) || ! isset($decoded['abi']) || ! is_array($decoded['abi'])) {
120+
throw new \RuntimeException("ABI JSON does not contain a valid abi array: {$path}");
99121
}
122+
123+
return $decoded;
100124
}
101125
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ArkEcosystem\Crypto\Utils;
6+
7+
use ArkEcosystem\Crypto\Enums\ContractAbiType;
8+
use ArkEcosystem\Crypto\Helpers;
9+
10+
class TransactionTypeIdentifier
11+
{
12+
private const TRANSFER_SIGNATURE = '';
13+
14+
private static ?array $signatures = null;
15+
16+
public static function isTransfer(string $data): bool
17+
{
18+
return $data === self::TRANSFER_SIGNATURE;
19+
}
20+
21+
public static function isVote(string $data): bool
22+
{
23+
return self::startsWithSignature($data, self::signatures()['vote']);
24+
}
25+
26+
public static function isUnvote(string $data): bool
27+
{
28+
return self::startsWithSignature($data, self::signatures()['unvote']);
29+
}
30+
31+
public static function isMultiPayment(string $data): bool
32+
{
33+
return self::startsWithSignature($data, self::signatures()['multiPayment']);
34+
}
35+
36+
public static function isUsernameRegistration(string $data): bool
37+
{
38+
return self::startsWithSignature($data, self::signatures()['registerUsername']);
39+
}
40+
41+
public static function isUsernameResignation(string $data): bool
42+
{
43+
return self::startsWithSignature($data, self::signatures()['resignUsername']);
44+
}
45+
46+
public static function isValidatorRegistration(string $data): bool
47+
{
48+
return self::startsWithSignature($data, self::signatures()['registerValidator']);
49+
}
50+
51+
public static function isValidatorResignation(string $data): bool
52+
{
53+
return self::startsWithSignature($data, self::signatures()['resignValidator']);
54+
}
55+
56+
public static function isUpdateValidator(string $data): bool
57+
{
58+
return self::startsWithSignature($data, self::signatures()['updateValidator']);
59+
}
60+
61+
public static function isTokenTransfer(string $data): bool
62+
{
63+
$decodedData = static::decodeTokenFunction($data);
64+
65+
return $decodedData ? $decodedData['functionName'] === 'transfer' : false;
66+
}
67+
68+
private static function startsWithSignature(string $data, string $signature): bool
69+
{
70+
return str_starts_with(
71+
strtolower(Helpers::removeLeadingHexZero($data)),
72+
strtolower($signature)
73+
);
74+
}
75+
76+
private static function signatures(): array
77+
{
78+
if (self::$signatures !== null) {
79+
return self::$signatures;
80+
}
81+
82+
$consensusMethods = AbiBase::methodIdentifiers(ContractAbiType::CONSENSUS);
83+
$multipaymentMethods = AbiBase::methodIdentifiers(ContractAbiType::MULTIPAYMENT);
84+
$usernamesMethods = AbiBase::methodIdentifiers(ContractAbiType::USERNAMES);
85+
86+
self::$signatures = [
87+
'multiPayment' => $multipaymentMethods['pay(address[],uint256[])'],
88+
'registerUsername' => $usernamesMethods['registerUsername(string)'],
89+
'resignUsername' => $usernamesMethods['resignUsername()'],
90+
'registerValidator' => $consensusMethods['registerValidator(bytes)'],
91+
'resignValidator' => $consensusMethods['resignValidator()'],
92+
'vote' => $consensusMethods['vote(address)'],
93+
'unvote' => $consensusMethods['unvote()'],
94+
'updateValidator' => $consensusMethods['updateValidator(bytes)'],
95+
'transfer' => 'transfer',
96+
];
97+
98+
return self::$signatures;
99+
}
100+
101+
private static function decodeTokenFunction(string $data): ?array
102+
{
103+
try {
104+
$decodedData = (new AbiDecoder(ContractAbiType::TOKEN))->decodeFunctionData($data);
105+
106+
return ['functionName' => $decodedData['functionName'], 'args' => $decodedData['args']];
107+
} catch (\Exception $e) {
108+
// Different abi type. Ignore.
109+
}
110+
111+
return null;
112+
}
113+
}

tests/Unit/Enums/AbiFunctionTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
declare(strict_types=1);
44

55
use ArkEcosystem\Crypto\Enums\AbiFunction;
6+
use ArkEcosystem\Crypto\Transactions\Types\EvmCall;
67
use ArkEcosystem\Crypto\Transactions\Types\Multipayment;
78
use ArkEcosystem\Crypto\Transactions\Types\Unvote;
89
use ArkEcosystem\Crypto\Transactions\Types\UsernameRegistration;
@@ -21,4 +22,6 @@
2122
'UsernameRegistration' => ['USERNAME_REGISTRATION', UsernameRegistration::class],
2223
'UsernameResignation' => ['USERNAME_RESIGNATION', UsernameResignation::class],
2324
'Multipayment' => ['MULTIPAYMENT', Multipayment::class],
25+
'Transfer' => ['TRANSFER', EvmCall::class],
26+
'Approve' => ['APPROVE', EvmCall::class],
2427
]);

tests/Unit/Utils/AbiEncoderTest.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,32 @@ function testPrivateMethod(string $methodName, &$object): ReflectionMethod
2020
$this->encoder = new AbiEncoder();
2121
});
2222

23+
it('should require a non-empty custom ABI path', function () {
24+
new AbiEncoder(ContractAbiType::CUSTOM);
25+
})->throws(
26+
InvalidArgumentException::class,
27+
'A non-empty $path must be provided when using ContractAbiType::CUSTOM.'
28+
);
29+
30+
it('should fail when custom ABI file cannot be read', function () {
31+
$previousErrorReporting = error_reporting();
32+
error_reporting($previousErrorReporting & ~E_WARNING);
33+
34+
try {
35+
new AbiEncoder(ContractAbiType::CUSTOM, dirname(__DIR__, 3).'/tests/fixtures/does-not-exist.json');
36+
} finally {
37+
error_reporting($previousErrorReporting);
38+
}
39+
})->throws(RuntimeException::class, 'Unable to load ABI JSON');
40+
41+
it('should fail when custom ABI JSON is missing abi array', function () {
42+
new AbiEncoder(ContractAbiType::CUSTOM, dirname(__DIR__, 3).'/tests/fixtures/message-sign.json');
43+
})->throws(RuntimeException::class, 'ABI JSON does not contain a valid abi array');
44+
45+
it('should fail when custom ABI JSON is missing method identifiers', function () {
46+
AbiEncoder::methodIdentifiers(ContractAbiType::CUSTOM, dirname(__DIR__, 3).'/tests/fixtures/mock-abi.json');
47+
})->throws(RuntimeException::class, 'ABI JSON does not contain methodIdentifiers');
48+
2349
it('should encode vote function call', function () {
2450
$functionName = 'vote';
2551
$args = ['0x512F366D524157BcF734546eB29a6d687B762255'];
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use ArkEcosystem\Crypto\Utils\TransactionTypeIdentifier;
6+
7+
function loadMethodIdentifiers(string $path): array
8+
{
9+
$json = file_get_contents($path);
10+
11+
expect($json)->not->toBeFalse();
12+
13+
$decoded = json_decode($json, true);
14+
15+
expect($decoded)->toBeArray();
16+
expect($decoded)->toHaveKey('methodIdentifiers');
17+
18+
return $decoded['methodIdentifiers'];
19+
}
20+
21+
beforeEach(function () {
22+
$this->consensusMethods = loadMethodIdentifiers(dirname(__DIR__, 3).'/src/Utils/Abi/json/Abi.Consensus.json');
23+
$this->multipaymentMethods = loadMethodIdentifiers(dirname(__DIR__, 3).'/src/Utils/Abi/json/Abi.Multipayment.json');
24+
$this->usernamesMethods = loadMethodIdentifiers(dirname(__DIR__, 3).'/src/Utils/Abi/json/Abi.Usernames.json');
25+
});
26+
27+
it('identifies transfer by empty payload', function () {
28+
expect(TransactionTypeIdentifier::isTransfer(''))->toBeTrue();
29+
expect(TransactionTypeIdentifier::isTransfer('0x'))->toBeFalse();
30+
expect(TransactionTypeIdentifier::isTransfer('12345678'))->toBeFalse();
31+
});
32+
33+
it('identifies vote signature', function () {
34+
$signature = $this->consensusMethods['vote(address)'];
35+
36+
expect(TransactionTypeIdentifier::isVote($signature))->toBeTrue();
37+
expect(TransactionTypeIdentifier::isVote('0x'.$signature))->toBeTrue();
38+
expect(TransactionTypeIdentifier::isVote('1234567'))->toBeFalse();
39+
});
40+
41+
it('identifies unvote signature', function () {
42+
$signature = $this->consensusMethods['unvote()'];
43+
44+
expect(TransactionTypeIdentifier::isUnvote($signature))->toBeTrue();
45+
expect(TransactionTypeIdentifier::isUnvote('0x'.$signature))->toBeTrue();
46+
expect(TransactionTypeIdentifier::isUnvote('1234567'))->toBeFalse();
47+
});
48+
49+
it('identifies multipayment signature', function () {
50+
$signature = $this->multipaymentMethods['pay(address[],uint256[])'];
51+
52+
expect(TransactionTypeIdentifier::isMultiPayment($signature))->toBeTrue();
53+
expect(TransactionTypeIdentifier::isMultiPayment('0x'.$signature))->toBeTrue();
54+
expect(TransactionTypeIdentifier::isMultiPayment('1234567'))->toBeFalse();
55+
});
56+
57+
it('identifies username registration signature', function () {
58+
$signature = $this->usernamesMethods['registerUsername(string)'];
59+
60+
expect(TransactionTypeIdentifier::isUsernameRegistration($signature))->toBeTrue();
61+
expect(TransactionTypeIdentifier::isUsernameRegistration('0x'.$signature))->toBeTrue();
62+
expect(TransactionTypeIdentifier::isUsernameRegistration('1234567'))->toBeFalse();
63+
});
64+
65+
it('identifies username resignation signature', function () {
66+
$signature = $this->usernamesMethods['resignUsername()'];
67+
68+
expect(TransactionTypeIdentifier::isUsernameResignation($signature))->toBeTrue();
69+
expect(TransactionTypeIdentifier::isUsernameResignation('0x'.$signature))->toBeTrue();
70+
expect(TransactionTypeIdentifier::isUsernameResignation('1234567'))->toBeFalse();
71+
});
72+
73+
it('identifies validator registration signature', function () {
74+
$signature = $this->consensusMethods['registerValidator(bytes)'];
75+
76+
expect(TransactionTypeIdentifier::isValidatorRegistration($signature))->toBeTrue();
77+
expect(TransactionTypeIdentifier::isValidatorRegistration('0x'.$signature))->toBeTrue();
78+
expect(TransactionTypeIdentifier::isValidatorRegistration('1234567'))->toBeFalse();
79+
});
80+
81+
it('identifies validator resignation signature', function () {
82+
$signature = $this->consensusMethods['resignValidator()'];
83+
84+
expect(TransactionTypeIdentifier::isValidatorResignation($signature))->toBeTrue();
85+
expect(TransactionTypeIdentifier::isValidatorResignation('0x'.$signature))->toBeTrue();
86+
expect(TransactionTypeIdentifier::isValidatorResignation('1234567'))->toBeFalse();
87+
});
88+
89+
it('identifies update validator signature', function () {
90+
$signature = $this->consensusMethods['updateValidator(bytes)'];
91+
92+
expect(TransactionTypeIdentifier::isUpdateValidator($signature))->toBeTrue();
93+
expect(TransactionTypeIdentifier::isUpdateValidator('0x'.$signature))->toBeTrue();
94+
expect(TransactionTypeIdentifier::isUpdateValidator('1234567'))->toBeFalse();
95+
});
96+
97+
it('identifies token transfer payloads', function () {
98+
expect(TransactionTypeIdentifier::isTokenTransfer('0xa9059cbb000000000000000000000000a5cc0bfeb09742c5e4c610f2ebaab82eb142ca10000000000000000000000000000000000000009bd2ffdd71438a49e803314000'))->toBeTrue();
99+
expect(TransactionTypeIdentifier::isTokenTransfer('0x'.str_repeat('0', 64)))->toBeFalse();
100+
expect(TransactionTypeIdentifier::isTokenTransfer('1234567'))->toBeFalse();
101+
});

0 commit comments

Comments
 (0)