diff --git a/src/Utils/AbiBase.php b/src/Utils/AbiBase.php index 558240d..71cf80d 100644 --- a/src/Utils/AbiBase.php +++ b/src/Utils/AbiBase.php @@ -13,11 +13,24 @@ abstract class AbiBase public function __construct(ContractAbiType $type = ContractAbiType::CONSENSUS, ?string $path = null) { - $abiFilePath = $this->contractAbiPath($type, $path); + $abiFilePath = self::contractAbiPath($type, $path); + $decodedAbi = self::loadAbiJson($abiFilePath); - $abiJson = file_get_contents($abiFilePath); + $this->abi = $decodedAbi['abi']; + } + + public static function methodIdentifiers( + ContractAbiType $type = ContractAbiType::CONSENSUS, + ?string $path = null + ): array { + $abiFilePath = self::contractAbiPath($type, $path); + $decodedAbi = self::loadAbiJson($abiFilePath); + + if (! isset($decodedAbi['methodIdentifiers']) || ! is_array($decodedAbi['methodIdentifiers'])) { + throw new \RuntimeException("ABI JSON does not contain methodIdentifiers: {$abiFilePath}"); + } - $this->abi = json_decode($abiJson, true)['abi']; + return $decodedAbi['methodIdentifiers']; } protected static function getArrayComponents(string $type): ?array @@ -75,27 +88,38 @@ protected function toFunctionSelector(array $abiItem): string return $selector; } - private function contractAbiPath(ContractAbiType $type, ?string $path = null): ?string + protected static function contractAbiPath(ContractAbiType $type, ?string $path = null): string { - switch ($type) { - case ContractAbiType::CONSENSUS: - return __DIR__.'/Abi/json/Abi.Consensus.json'; - case ContractAbiType::MULTIPAYMENT: - return __DIR__.'/Abi/json/Abi.Multipayment.json'; - case ContractAbiType::USERNAMES: - return __DIR__.'/Abi/json/Abi.Usernames.json'; - case ContractAbiType::ERC20BATCH_TRANSFER: - return __DIR__.'/Abi/json/Abi.ERC20BatchTransfer.json'; - case ContractAbiType::TOKEN: - return __DIR__.'/Abi/json/Abi.Token.json'; - case ContractAbiType::CUSTOM: + return match ($type) { + ContractAbiType::CONSENSUS => __DIR__.'/Abi/json/Abi.Consensus.json', + ContractAbiType::MULTIPAYMENT => __DIR__.'/Abi/json/Abi.Multipayment.json', + ContractAbiType::USERNAMES => __DIR__.'/Abi/json/Abi.Usernames.json', + ContractAbiType::ERC20BATCH_TRANSFER => __DIR__.'/Abi/json/Abi.ERC20BatchTransfer.json', + ContractAbiType::TOKEN => __DIR__.'/Abi/json/Abi.Token.json', + ContractAbiType::CUSTOM => (function () use ($path): string { if ($path === null || $path === '') { throw new \InvalidArgumentException('A non-empty $path must be provided when using ContractAbiType::CUSTOM.'); } return $path; - default: - throw new \InvalidArgumentException('Unhandled ContractAbiType: '.$type->name); + })(), + }; + } + + private static function loadAbiJson(string $path): array + { + $rawJson = file_get_contents($path); + + if ($rawJson === false) { + throw new \RuntimeException("Unable to load ABI JSON: {$path}"); + } + + $decoded = json_decode($rawJson, true); + + if (! is_array($decoded) || ! isset($decoded['abi']) || ! is_array($decoded['abi'])) { + throw new \RuntimeException("ABI JSON does not contain a valid abi array: {$path}"); } + + return $decoded; } } diff --git a/src/Utils/TransactionTypeIdentifier.php b/src/Utils/TransactionTypeIdentifier.php new file mode 100644 index 0000000..5e59f1b --- /dev/null +++ b/src/Utils/TransactionTypeIdentifier.php @@ -0,0 +1,113 @@ + $multipaymentMethods['pay(address[],uint256[])'], + 'registerUsername' => $usernamesMethods['registerUsername(string)'], + 'resignUsername' => $usernamesMethods['resignUsername()'], + 'registerValidator' => $consensusMethods['registerValidator(bytes)'], + 'resignValidator' => $consensusMethods['resignValidator()'], + 'vote' => $consensusMethods['vote(address)'], + 'unvote' => $consensusMethods['unvote()'], + 'updateValidator' => $consensusMethods['updateValidator(bytes)'], + 'transfer' => 'transfer', + ]; + + return self::$signatures; + } + + private static function decodeTokenFunction(string $data): ?array + { + try { + $decodedData = (new AbiDecoder(ContractAbiType::TOKEN))->decodeFunctionData($data); + + return ['functionName' => $decodedData['functionName'], 'args' => $decodedData['args']]; + } catch (\Exception $e) { + // Different abi type. Ignore. + } + + return null; + } +} diff --git a/tests/Unit/Enums/AbiFunctionTest.php b/tests/Unit/Enums/AbiFunctionTest.php index a6e73ee..d7cee8f 100644 --- a/tests/Unit/Enums/AbiFunctionTest.php +++ b/tests/Unit/Enums/AbiFunctionTest.php @@ -3,6 +3,7 @@ declare(strict_types=1); use ArkEcosystem\Crypto\Enums\AbiFunction; +use ArkEcosystem\Crypto\Transactions\Types\EvmCall; use ArkEcosystem\Crypto\Transactions\Types\Multipayment; use ArkEcosystem\Crypto\Transactions\Types\Unvote; use ArkEcosystem\Crypto\Transactions\Types\UsernameRegistration; @@ -21,4 +22,6 @@ 'UsernameRegistration' => ['USERNAME_REGISTRATION', UsernameRegistration::class], 'UsernameResignation' => ['USERNAME_RESIGNATION', UsernameResignation::class], 'Multipayment' => ['MULTIPAYMENT', Multipayment::class], + 'Transfer' => ['TRANSFER', EvmCall::class], + 'Approve' => ['APPROVE', EvmCall::class], ]); diff --git a/tests/Unit/Utils/AbiEncoderTest.php b/tests/Unit/Utils/AbiEncoderTest.php index fcc715c..14a5189 100644 --- a/tests/Unit/Utils/AbiEncoderTest.php +++ b/tests/Unit/Utils/AbiEncoderTest.php @@ -20,6 +20,32 @@ function testPrivateMethod(string $methodName, &$object): ReflectionMethod $this->encoder = new AbiEncoder(); }); +it('should require a non-empty custom ABI path', function () { + new AbiEncoder(ContractAbiType::CUSTOM); +})->throws( + InvalidArgumentException::class, + 'A non-empty $path must be provided when using ContractAbiType::CUSTOM.' +); + +it('should fail when custom ABI file cannot be read', function () { + $previousErrorReporting = error_reporting(); + error_reporting($previousErrorReporting & ~E_WARNING); + + try { + new AbiEncoder(ContractAbiType::CUSTOM, dirname(__DIR__, 3).'/tests/fixtures/does-not-exist.json'); + } finally { + error_reporting($previousErrorReporting); + } +})->throws(RuntimeException::class, 'Unable to load ABI JSON'); + +it('should fail when custom ABI JSON is missing abi array', function () { + new AbiEncoder(ContractAbiType::CUSTOM, dirname(__DIR__, 3).'/tests/fixtures/message-sign.json'); +})->throws(RuntimeException::class, 'ABI JSON does not contain a valid abi array'); + +it('should fail when custom ABI JSON is missing method identifiers', function () { + AbiEncoder::methodIdentifiers(ContractAbiType::CUSTOM, dirname(__DIR__, 3).'/tests/fixtures/mock-abi.json'); +})->throws(RuntimeException::class, 'ABI JSON does not contain methodIdentifiers'); + it('should encode vote function call', function () { $functionName = 'vote'; $args = ['0x512F366D524157BcF734546eB29a6d687B762255']; diff --git a/tests/Unit/Utils/TransactionTypeIdentifierTest.php b/tests/Unit/Utils/TransactionTypeIdentifierTest.php new file mode 100644 index 0000000..ec8737c --- /dev/null +++ b/tests/Unit/Utils/TransactionTypeIdentifierTest.php @@ -0,0 +1,101 @@ +not->toBeFalse(); + + $decoded = json_decode($json, true); + + expect($decoded)->toBeArray(); + expect($decoded)->toHaveKey('methodIdentifiers'); + + return $decoded['methodIdentifiers']; +} + +beforeEach(function () { + $this->consensusMethods = loadMethodIdentifiers(dirname(__DIR__, 3).'/src/Utils/Abi/json/Abi.Consensus.json'); + $this->multipaymentMethods = loadMethodIdentifiers(dirname(__DIR__, 3).'/src/Utils/Abi/json/Abi.Multipayment.json'); + $this->usernamesMethods = loadMethodIdentifiers(dirname(__DIR__, 3).'/src/Utils/Abi/json/Abi.Usernames.json'); +}); + +it('identifies transfer by empty payload', function () { + expect(TransactionTypeIdentifier::isTransfer(''))->toBeTrue(); + expect(TransactionTypeIdentifier::isTransfer('0x'))->toBeFalse(); + expect(TransactionTypeIdentifier::isTransfer('12345678'))->toBeFalse(); +}); + +it('identifies vote signature', function () { + $signature = $this->consensusMethods['vote(address)']; + + expect(TransactionTypeIdentifier::isVote($signature))->toBeTrue(); + expect(TransactionTypeIdentifier::isVote('0x'.$signature))->toBeTrue(); + expect(TransactionTypeIdentifier::isVote('1234567'))->toBeFalse(); +}); + +it('identifies unvote signature', function () { + $signature = $this->consensusMethods['unvote()']; + + expect(TransactionTypeIdentifier::isUnvote($signature))->toBeTrue(); + expect(TransactionTypeIdentifier::isUnvote('0x'.$signature))->toBeTrue(); + expect(TransactionTypeIdentifier::isUnvote('1234567'))->toBeFalse(); +}); + +it('identifies multipayment signature', function () { + $signature = $this->multipaymentMethods['pay(address[],uint256[])']; + + expect(TransactionTypeIdentifier::isMultiPayment($signature))->toBeTrue(); + expect(TransactionTypeIdentifier::isMultiPayment('0x'.$signature))->toBeTrue(); + expect(TransactionTypeIdentifier::isMultiPayment('1234567'))->toBeFalse(); +}); + +it('identifies username registration signature', function () { + $signature = $this->usernamesMethods['registerUsername(string)']; + + expect(TransactionTypeIdentifier::isUsernameRegistration($signature))->toBeTrue(); + expect(TransactionTypeIdentifier::isUsernameRegistration('0x'.$signature))->toBeTrue(); + expect(TransactionTypeIdentifier::isUsernameRegistration('1234567'))->toBeFalse(); +}); + +it('identifies username resignation signature', function () { + $signature = $this->usernamesMethods['resignUsername()']; + + expect(TransactionTypeIdentifier::isUsernameResignation($signature))->toBeTrue(); + expect(TransactionTypeIdentifier::isUsernameResignation('0x'.$signature))->toBeTrue(); + expect(TransactionTypeIdentifier::isUsernameResignation('1234567'))->toBeFalse(); +}); + +it('identifies validator registration signature', function () { + $signature = $this->consensusMethods['registerValidator(bytes)']; + + expect(TransactionTypeIdentifier::isValidatorRegistration($signature))->toBeTrue(); + expect(TransactionTypeIdentifier::isValidatorRegistration('0x'.$signature))->toBeTrue(); + expect(TransactionTypeIdentifier::isValidatorRegistration('1234567'))->toBeFalse(); +}); + +it('identifies validator resignation signature', function () { + $signature = $this->consensusMethods['resignValidator()']; + + expect(TransactionTypeIdentifier::isValidatorResignation($signature))->toBeTrue(); + expect(TransactionTypeIdentifier::isValidatorResignation('0x'.$signature))->toBeTrue(); + expect(TransactionTypeIdentifier::isValidatorResignation('1234567'))->toBeFalse(); +}); + +it('identifies update validator signature', function () { + $signature = $this->consensusMethods['updateValidator(bytes)']; + + expect(TransactionTypeIdentifier::isUpdateValidator($signature))->toBeTrue(); + expect(TransactionTypeIdentifier::isUpdateValidator('0x'.$signature))->toBeTrue(); + expect(TransactionTypeIdentifier::isUpdateValidator('1234567'))->toBeFalse(); +}); + +it('identifies token transfer payloads', function () { + expect(TransactionTypeIdentifier::isTokenTransfer('0xa9059cbb000000000000000000000000a5cc0bfeb09742c5e4c610f2ebaab82eb142ca10000000000000000000000000000000000000009bd2ffdd71438a49e803314000'))->toBeTrue(); + expect(TransactionTypeIdentifier::isTokenTransfer('0x'.str_repeat('0', 64)))->toBeFalse(); + expect(TransactionTypeIdentifier::isTokenTransfer('1234567'))->toBeFalse(); +});