From f4101986f743535e935dd4e797a93873bbacaae6 Mon Sep 17 00:00:00 2001 From: Alex Barnsley <8069294+alexbarnsley@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:14:43 +0000 Subject: [PATCH 1/7] feat: token implementation --- src/Enums/AbiFunction.php | 3 + src/Enums/ContractAbiType.php | 1 + .../Builder/TokenTransferBuilder.php | 40 ++ src/Utils/Abi/json/Abi.Token.json | 345 ++++++++++++++++++ src/Utils/AbiBase.php | 2 + .../Builder/TokenTransferBuilderTest.php | 37 ++ 6 files changed, 428 insertions(+) create mode 100644 src/Transactions/Builder/TokenTransferBuilder.php create mode 100644 src/Utils/Abi/json/Abi.Token.json create mode 100644 tests/Unit/Transactions/Builder/TokenTransferBuilderTest.php diff --git a/src/Enums/AbiFunction.php b/src/Enums/AbiFunction.php index 0b1ed35c..2085133b 100644 --- a/src/Enums/AbiFunction.php +++ b/src/Enums/AbiFunction.php @@ -4,6 +4,7 @@ namespace ArkEcosystem\Crypto\Enums; +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,6 +22,7 @@ enum AbiFunction: string case USERNAME_REGISTRATION = 'registerUsername'; case USERNAME_RESIGNATION = 'resignUsername'; case MULTIPAYMENT = 'pay'; + case TRANSFER = 'transfer'; public function transactionClass(): string { @@ -32,6 +34,7 @@ public function transactionClass(): string self::USERNAME_REGISTRATION => UsernameRegistration::class, self::USERNAME_RESIGNATION => UsernameResignation::class, self::MULTIPAYMENT => Multipayment::class, + self::TRANSFER => EvmCall::class, }; } } diff --git a/src/Enums/ContractAbiType.php b/src/Enums/ContractAbiType.php index f38b7093..f19c686a 100644 --- a/src/Enums/ContractAbiType.php +++ b/src/Enums/ContractAbiType.php @@ -10,4 +10,5 @@ enum ContractAbiType: string case CONSENSUS = 'consensus'; case MULTIPAYMENT = 'multipayment'; case USERNAMES = 'usernames'; + case TOKEN = 'token'; } diff --git a/src/Transactions/Builder/TokenTransferBuilder.php b/src/Transactions/Builder/TokenTransferBuilder.php new file mode 100644 index 00000000..85006259 --- /dev/null +++ b/src/Transactions/Builder/TokenTransferBuilder.php @@ -0,0 +1,40 @@ +transaction->data['to'] = $address; + + return $this; + } + + public function recipient(string $address, BigDecimal $amount): self + { + $payload = (new AbiEncoder(ContractAbiType::TOKEN))->encodeFunctionCall( + AbiFunction::TRANSFER->value, + [$address, $amount] + ); + + $this->transaction->data['data'] = Helpers::removeLeadingHexZero($payload); + + return $this; + } + + protected function getTransactionInstance(array $data): AbstractTransaction + { + return new EvmCall($data); + } +} diff --git a/src/Utils/Abi/json/Abi.Token.json b/src/Utils/Abi/json/Abi.Token.json new file mode 100644 index 00000000..854420b0 --- /dev/null +++ b/src/Utils/Abi/json/Abi.Token.json @@ -0,0 +1,345 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "TOKEN", + "sourceName": "*/TOKEN.sol", + "abi": [ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "allowance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "name": "ERC20InsufficientAllowance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "name": "ERC20InsufficientBalance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "approver", + "type": "address" + } + ], + "name": "ERC20InvalidApprover", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "name": "ERC20InvalidReceiver", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "ERC20InvalidSender", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "ERC20InvalidSpender", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "recipients", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "name": "batchTransfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x608060405234801561001057600080fd5b506040518060400160405280600681526020017f4441524b323000000000000000000000000000000000000000000000000000008152506040518060400160405280600681526020017f4441524b32300000000000000000000000000000000000000000000000000000815250816003908161008c91906105bc565b50806004908161009c91906105bc565b5050506100ba336a52b7d2dcc80cd2e40000006100bf60201b60201c565b6107ae565b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036101315760006040517fec442f0500000000000000000000000000000000000000000000000000000000815260040161012891906106cf565b60405180910390fd5b6101436000838361014760201b60201c565b5050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff160361019957806002600082825461018d9190610719565b9250508190555061026c565b60008060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905081811015610225578381836040517fe450d38c00000000000000000000000000000000000000000000000000000000815260040161021c9392919061075c565b60405180910390fd5b8181036000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550505b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036102b55780600260008282540392505081905550610302565b806000808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055505b8173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8360405161035f9190610793565b60405180910390a3505050565b600081519050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b600060028204905060018216806103ed57607f821691505b602082108103610400576103ff6103a6565b5b50919050565b60008190508160005260206000209050919050565b60006020601f8301049050919050565b600082821b905092915050565b6000600883026104687fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8261042b565b610472868361042b565b95508019841693508086168417925050509392505050565b6000819050919050565b6000819050919050565b60006104b96104b46104af8461048a565b610494565b61048a565b9050919050565b6000819050919050565b6104d38361049e565b6104e76104df826104c0565b848454610438565b825550505050565b600090565b6104fc6104ef565b6105078184846104ca565b505050565b5b8181101561052b576105206000826104f4565b60018101905061050d565b5050565b601f8211156105705761054181610406565b61054a8461041b565b81016020851015610559578190505b61056d6105658561041b565b83018261050c565b50505b505050565b600082821c905092915050565b600061059360001984600802610575565b1980831691505092915050565b60006105ac8383610582565b9150826002028217905092915050565b6105c58261036c565b67ffffffffffffffff8111156105de576105dd610377565b5b6105e882546103d5565b6105f382828561052f565b600060209050601f8311600181146106265760008415610614578287015190505b61061e85826105a0565b865550610686565b601f19841661063486610406565b60005b8281101561065c57848901518255600182019150602085019450602081019050610637565b868310156106795784890151610675601f891682610582565b8355505b6001600288020188555050505b505050505050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006106b98261068e565b9050919050565b6106c9816106ae565b82525050565b60006020820190506106e460008301846106c0565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006107248261048a565b915061072f8361048a565b9250828201905080821115610747576107466106ea565b5b92915050565b6107568161048a565b82525050565b600060608201905061077160008301866106c0565b61077e602083018561074d565b61078b604083018461074d565b949350505050565b60006020820190506107a8600083018461074d565b92915050565b611156806107bd6000396000f3fe608060405234801561001057600080fd5b506004361061009e5760003560e01c806370a082311161006657806370a082311461015d57806388d695b21461018d57806395d89b41146101bd578063a9059cbb146101db578063dd62ed3e1461020b5761009e565b806306fdde03146100a3578063095ea7b3146100c157806318160ddd146100f157806323b872dd1461010f578063313ce5671461013f575b600080fd5b6100ab61023b565b6040516100b89190610ba8565b60405180910390f35b6100db60048036038101906100d69190610c68565b6102cd565b6040516100e89190610cc3565b60405180910390f35b6100f96102f0565b6040516101069190610ced565b60405180910390f35b61012960048036038101906101249190610d08565b6102fa565b6040516101369190610cc3565b60405180910390f35b610147610329565b6040516101549190610d77565b60405180910390f35b61017760048036038101906101729190610d92565b610332565b6040516101849190610ced565b60405180910390f35b6101a760048036038101906101a29190610e7a565b61037a565b6040516101b49190610cc3565b60405180910390f35b6101c561043e565b6040516101d29190610ba8565b60405180910390f35b6101f560048036038101906101f09190610c68565b6104d0565b6040516102029190610cc3565b60405180910390f35b61022560048036038101906102209190610efb565b6104f3565b6040516102329190610ced565b60405180910390f35b60606003805461024a90610f6a565b80601f016020809104026020016040519081016040528092919081815260200182805461027690610f6a565b80156102c35780601f10610298576101008083540402835291602001916102c3565b820191906000526020600020905b8154815290600101906020018083116102a657829003601f168201915b5050505050905090565b6000806102d861057a565b90506102e5818585610582565b600191505092915050565b6000600254905090565b60008061030561057a565b9050610312858285610594565b61031d858585610628565b60019150509392505050565b60006012905090565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b60008282905085859050146103c4576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016103bb9061100d565b60405180910390fd5b60005b85859050811015610431576104246103dd61057a565b8787848181106103f0576103ef61102d565b5b90506020020160208101906104059190610d92565b8686858181106104185761041761102d565b5b90506020020135610628565b80806001019150506103c7565b5060019050949350505050565b60606004805461044d90610f6a565b80601f016020809104026020016040519081016040528092919081815260200182805461047990610f6a565b80156104c65780601f1061049b576101008083540402835291602001916104c6565b820191906000526020600020905b8154815290600101906020018083116104a957829003601f168201915b5050505050905090565b6000806104db61057a565b90506104e8818585610628565b600191505092915050565b6000600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905092915050565b600033905090565b61058f838383600161071c565b505050565b60006105a084846104f3565b90507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81146106225781811015610612578281836040517ffb8f41b20000000000000000000000000000000000000000000000000000000081526004016106099392919061106b565b60405180910390fd5b6106218484848403600061071c565b5b50505050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff160361069a5760006040517f96c6fd1e00000000000000000000000000000000000000000000000000000000815260040161069191906110a2565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff160361070c5760006040517fec442f0500000000000000000000000000000000000000000000000000000000815260040161070391906110a2565b60405180910390fd5b6107178383836108f3565b505050565b600073ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff160361078e5760006040517fe602df0500000000000000000000000000000000000000000000000000000000815260040161078591906110a2565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16036108005760006040517f94280d620000000000000000000000000000000000000000000000000000000081526004016107f791906110a2565b60405180910390fd5b81600160008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000208190555080156108ed578273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040516108e49190610ced565b60405180910390a35b50505050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff160361094557806002600082825461093991906110ec565b92505081905550610a18565b60008060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050818110156109d1578381836040517fe450d38c0000000000000000000000000000000000000000000000000000000081526004016109c89392919061106b565b60405180910390fd5b8181036000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550505b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603610a615780600260008282540392505081905550610aae565b806000808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055505b8173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef83604051610b0b9190610ced565b60405180910390a3505050565b600081519050919050565b600082825260208201905092915050565b60005b83811015610b52578082015181840152602081019050610b37565b60008484015250505050565b6000601f19601f8301169050919050565b6000610b7a82610b18565b610b848185610b23565b9350610b94818560208601610b34565b610b9d81610b5e565b840191505092915050565b60006020820190508181036000830152610bc28184610b6f565b905092915050565b600080fd5b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000610bff82610bd4565b9050919050565b610c0f81610bf4565b8114610c1a57600080fd5b50565b600081359050610c2c81610c06565b92915050565b6000819050919050565b610c4581610c32565b8114610c5057600080fd5b50565b600081359050610c6281610c3c565b92915050565b60008060408385031215610c7f57610c7e610bca565b5b6000610c8d85828601610c1d565b9250506020610c9e85828601610c53565b9150509250929050565b60008115159050919050565b610cbd81610ca8565b82525050565b6000602082019050610cd86000830184610cb4565b92915050565b610ce781610c32565b82525050565b6000602082019050610d026000830184610cde565b92915050565b600080600060608486031215610d2157610d20610bca565b5b6000610d2f86828701610c1d565b9350506020610d4086828701610c1d565b9250506040610d5186828701610c53565b9150509250925092565b600060ff82169050919050565b610d7181610d5b565b82525050565b6000602082019050610d8c6000830184610d68565b92915050565b600060208284031215610da857610da7610bca565b5b6000610db684828501610c1d565b91505092915050565b600080fd5b600080fd5b600080fd5b60008083601f840112610de457610de3610dbf565b5b8235905067ffffffffffffffff811115610e0157610e00610dc4565b5b602083019150836020820283011115610e1d57610e1c610dc9565b5b9250929050565b60008083601f840112610e3a57610e39610dbf565b5b8235905067ffffffffffffffff811115610e5757610e56610dc4565b5b602083019150836020820283011115610e7357610e72610dc9565b5b9250929050565b60008060008060408587031215610e9457610e93610bca565b5b600085013567ffffffffffffffff811115610eb257610eb1610bcf565b5b610ebe87828801610dce565b9450945050602085013567ffffffffffffffff811115610ee157610ee0610bcf565b5b610eed87828801610e24565b925092505092959194509250565b60008060408385031215610f1257610f11610bca565b5b6000610f2085828601610c1d565b9250506020610f3185828601610c1d565b9150509250929050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b60006002820490506001821680610f8257607f821691505b602082108103610f9557610f94610f3b565b5b50919050565b7f726563697069656e747320616e6420616d6f756e7473206c656e677468206d6960008201527f736d617463680000000000000000000000000000000000000000000000000000602082015250565b6000610ff7602683610b23565b915061100282610f9b565b604082019050919050565b6000602082019050818103600083015261102681610fea565b9050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b61106581610bf4565b82525050565b6000606082019050611080600083018661105c565b61108d6020830185610cde565b61109a6040830184610cde565b949350505050565b60006020820190506110b7600083018461105c565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006110f782610c32565b915061110283610c32565b925082820190508082111561111a576111196110bd565b5b9291505056fea2646970667358221220fdbfd2ff5d7f60018cd1ba9f99f9038e758419b539d4a76b8c9148f3b4c10b6b64736f6c634300081a0033" +} diff --git a/src/Utils/AbiBase.php b/src/Utils/AbiBase.php index 40824197..4c1e92ec 100644 --- a/src/Utils/AbiBase.php +++ b/src/Utils/AbiBase.php @@ -84,6 +84,8 @@ private function contractAbiPath(ContractAbiType $type, ?string $path = null): ? return __DIR__.'/Abi/json/Abi.Multipayment.json'; case ContractAbiType::USERNAMES: return __DIR__.'/Abi/json/Abi.Usernames.json'; + case ContractAbiType::TOKEN: + return __DIR__.'/Abi/json/Abi.Token.json'; case ContractAbiType::CUSTOM: return $path; } diff --git a/tests/Unit/Transactions/Builder/TokenTransferBuilderTest.php b/tests/Unit/Transactions/Builder/TokenTransferBuilderTest.php new file mode 100644 index 00000000..fc6dc9ca --- /dev/null +++ b/tests/Unit/Transactions/Builder/TokenTransferBuilderTest.php @@ -0,0 +1,37 @@ +contractAddress($contractAddress) + ->recipient($recipient, $amount); + + expect($builder->transaction->data['to'])->toBe($contractAddress); + expect($builder->transaction->data['value']->isZero())->toBeTrue(); + expect($builder->transaction->data['data'])->toBe($expectedPayload); +}); + +it('should sign and verify a token transfer', function () { + $builder = TokenTransferBuilder::new() + ->contractAddress('0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22') + ->recipient('0xA5cc0BfEB09742C5e4C610f2EBaaB82Eb142Ca10', BigDecimal::of('1000000000000')) + ->gasPrice(UnitConverter::parseUnits('5000000000', 'wei')) + ->gasLimit(UnitConverter::parseUnits('21000', 'wei')) + ->nonce('1') + ->sign($this->passphrase); + + expect($builder->verify())->toBeTrue(); + expect($builder->transaction->data['data'])->toStartWith('a9059cbb'); + expect($builder->transaction->data['to'])->toBe('0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22'); + expect($builder->transaction->data['value']->isZero())->toBeTrue(); +}); From 02bb99b6628472025f32b31587def3829cc58634 Mon Sep 17 00:00:00 2001 From: Alex Barnsley <8069294+alexbarnsley@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:21:04 +0000 Subject: [PATCH 2/7] add transaction encoder --- src/Utils/TransactionEncoder.php | 81 +++++++++++++++++++++ tests/Unit/Utils/TransactionEncoderTest.php | 80 ++++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 src/Utils/TransactionEncoder.php create mode 100644 tests/Unit/Utils/TransactionEncoderTest.php diff --git a/src/Utils/TransactionEncoder.php b/src/Utils/TransactionEncoder.php new file mode 100644 index 00000000..905613ad --- /dev/null +++ b/src/Utils/TransactionEncoder.php @@ -0,0 +1,81 @@ +encodeFunctionCall( + AbiFunction::MULTIPAYMENT->value, + [$recipients, $amounts] + ); + } + + public static function tokenTransfer(string $recipientAddress, $amount): string + { + return (new AbiEncoder(ContractAbiType::TOKEN))->encodeFunctionCall( + AbiFunction::TRANSFER->value, + [$recipientAddress, $amount] + ); + } + + public static function usernameRegistration(string $username): string + { + return (new AbiEncoder(ContractAbiType::USERNAMES))->encodeFunctionCall( + AbiFunction::USERNAME_REGISTRATION->value, + [$username] + ); + } + + public static function usernameResignation(): string + { + return (new AbiEncoder(ContractAbiType::USERNAMES))->encodeFunctionCall( + AbiFunction::USERNAME_RESIGNATION->value + ); + } + + public static function validatorRegistration(string $validatorPublicKey): string + { + return (new AbiEncoder(ContractAbiType::CONSENSUS))->encodeFunctionCall( + AbiFunction::VALIDATOR_REGISTRATION->value, + [self::addHexPrefix($validatorPublicKey)] + ); + } + + public static function validatorResignation(): string + { + return (new AbiEncoder(ContractAbiType::CONSENSUS))->encodeFunctionCall( + AbiFunction::VALIDATOR_RESIGNATION->value + ); + } + + public static function vote(string $voteAddress): string + { + return (new AbiEncoder(ContractAbiType::CONSENSUS))->encodeFunctionCall( + AbiFunction::VOTE->value, + [$voteAddress] + ); + } + + public static function unvote(): string + { + return (new AbiEncoder(ContractAbiType::CONSENSUS))->encodeFunctionCall( + AbiFunction::UNVOTE->value + ); + } + + private static function addHexPrefix(string $value): string + { + if (str_starts_with($value, '0x')) { + return $value; + } + + return '0x'.$value; + } +} diff --git a/tests/Unit/Utils/TransactionEncoderTest.php b/tests/Unit/Utils/TransactionEncoderTest.php new file mode 100644 index 00000000..34eb9337 --- /dev/null +++ b/tests/Unit/Utils/TransactionEncoderTest.php @@ -0,0 +1,80 @@ +toBe( + '0x084ce7080000000000000000000000000000000000000000000000000000000000000040' + .'0000000000000000000000000000000000000000000000000000000000000080' + .'0000000000000000000000000000000000000000000000000000000000000001' + .'000000000000000000000000b693449adda7efc015d87944eae8b7c37eb1690a' + .'0000000000000000000000000000000000000000000000000000000000000001' + .'00000000000000000000000000000000000000000000000000000000000003e8' + ); +}); + +it('should encode a token transfer payload', function () { + $encoded = TransactionEncoder::tokenTransfer( + '0xA5cc0BfEB09742C5e4C610f2EBaaB82Eb142Ca10', + '1000000000000' + ); + + expect($encoded)->toBe( + '0xa9059cbb000000000000000000000000a5cc0bfeb09742c5e4c610f2ebaab82eb142ca10' + .'000000000000000000000000000000000000000000000000000000e8d4a51000' + ); +}); + +it('should reject invalid address format in token transfer', function () { + TransactionEncoder::tokenTransfer('', '1000'); +})->throws(Exception::class); + +it('should reject invalid address format in multipayment', function () { + TransactionEncoder::multiPayment(['0x123'], ['1000']); +})->throws(Exception::class); + +it('should encode validator payloads', function () { + $validatorPublicKey = '30954f46d6097a1d314e900e66e11e0dad0a57cd03e04ec99f0dedd1c765dcb11e6d7fa02e22cf40f9ee23d9cc1c0624'; + + expect(TransactionEncoder::validatorRegistration($validatorPublicKey))->toBe( + '0x602a9eee0000000000000000000000000000000000000000000000000000000000000020' + .'0000000000000000000000000000000000000000000000000000000000000030' + .'30954f46d6097a1d314e900e66e11e0dad0a57cd03e04ec99f0dedd1c765dcb1' + .'1e6d7fa02e22cf40f9ee23d9cc1c062400000000000000000000000000000000' + ); + expect(TransactionEncoder::validatorResignation())->toBe('0xb85f5da2'); +}); + +it('should encode validator registration with a prefixed public key', function () { + $validatorPublicKey = '0x30954f46d6097a1d314e900e66e11e0dad0a57cd03e04ec99f0dedd1c765dcb11e6d7fa02e22cf40f9ee23d9cc1c0624'; + + expect(TransactionEncoder::validatorRegistration($validatorPublicKey))->toBe( + '0x602a9eee0000000000000000000000000000000000000000000000000000000000000020' + .'0000000000000000000000000000000000000000000000000000000000000030' + .'30954f46d6097a1d314e900e66e11e0dad0a57cd03e04ec99f0dedd1c765dcb1' + .'1e6d7fa02e22cf40f9ee23d9cc1c062400000000000000000000000000000000' + ); +}); + +it('should encode username payloads', function () { + expect(TransactionEncoder::usernameRegistration('fixture'))->toBe( + '0x36a941340000000000000000000000000000000000000000000000000000000000000020' + .'0000000000000000000000000000000000000000000000000000000000000007' + .'6669787475726500000000000000000000000000000000000000000000000000' + ); + expect(TransactionEncoder::usernameResignation())->toBe('0xebed6dab'); +}); + +it('should encode consensus vote payloads', function () { + expect(TransactionEncoder::vote('0x512F366D524157BcF734546eB29a6d687B762255'))->toBe( + '0x6dd7d8ea000000000000000000000000512f366d524157bcf734546eb29a6d687b762255' + ); + expect(TransactionEncoder::unvote())->toBe('0x3174b689'); +}); From e172124ac6848fb0417f10cee62e6644d4305084 Mon Sep 17 00:00:00 2001 From: Alex Barnsley <8069294+alexbarnsley@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:30:05 +0000 Subject: [PATCH 3/7] coding style adjustments --- .../Builder/TokenTransferBuilder.php | 4 +- src/Utils/Abi/json/Abi.Token.json | 686 +++++++++--------- src/Utils/TransactionEncoder.php | 11 +- 3 files changed, 354 insertions(+), 347 deletions(-) diff --git a/src/Transactions/Builder/TokenTransferBuilder.php b/src/Transactions/Builder/TokenTransferBuilder.php index 85006259..53a6205d 100644 --- a/src/Transactions/Builder/TokenTransferBuilder.php +++ b/src/Transactions/Builder/TokenTransferBuilder.php @@ -16,9 +16,7 @@ class TokenTransferBuilder extends AbstractTransactionBuilder { public function contractAddress(string $address): self { - $this->transaction->data['to'] = $address; - - return $this; + return $this->to($address); } public function recipient(string $address, BigDecimal $amount): self diff --git a/src/Utils/Abi/json/Abi.Token.json b/src/Utils/Abi/json/Abi.Token.json index 854420b0..bcfc52f2 100644 --- a/src/Utils/Abi/json/Abi.Token.json +++ b/src/Utils/Abi/json/Abi.Token.json @@ -1,345 +1,345 @@ { - "_format": "hh-sol-artifact-1", - "contractName": "TOKEN", - "sourceName": "*/TOKEN.sol", - "abi": [ - { - "inputs": [], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "spender", - "type": "address" - }, - { - "internalType": "uint256", - "name": "allowance", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "needed", - "type": "uint256" - } - ], - "name": "ERC20InsufficientAllowance", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "sender", - "type": "address" - }, - { - "internalType": "uint256", - "name": "balance", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "needed", - "type": "uint256" - } - ], - "name": "ERC20InsufficientBalance", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "approver", - "type": "address" - } - ], - "name": "ERC20InvalidApprover", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "receiver", - "type": "address" - } - ], - "name": "ERC20InvalidReceiver", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "sender", - "type": "address" - } - ], - "name": "ERC20InvalidSender", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "spender", - "type": "address" - } - ], - "name": "ERC20InvalidSpender", - "type": "error" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "owner", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "spender", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "name": "Approval", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "name": "Transfer", - "type": "event" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "owner", - "type": "address" - }, - { - "internalType": "address", - "name": "spender", - "type": "address" - } - ], - "name": "allowance", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "spender", - "type": "address" - }, - { - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "name": "approve", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "balanceOf", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address[]", - "name": "recipients", - "type": "address[]" - }, - { - "internalType": "uint256[]", - "name": "amounts", - "type": "uint256[]" - } - ], - "name": "batchTransfer", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "decimals", - "outputs": [ - { - "internalType": "uint8", - "name": "", - "type": "uint8" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "name", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "symbol", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "totalSupply", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "name": "transfer", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "name": "transferFrom", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - } - ], - "bytecode": "0x608060405234801561001057600080fd5b506040518060400160405280600681526020017f4441524b323000000000000000000000000000000000000000000000000000008152506040518060400160405280600681526020017f4441524b32300000000000000000000000000000000000000000000000000000815250816003908161008c91906105bc565b50806004908161009c91906105bc565b5050506100ba336a52b7d2dcc80cd2e40000006100bf60201b60201c565b6107ae565b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036101315760006040517fec442f0500000000000000000000000000000000000000000000000000000000815260040161012891906106cf565b60405180910390fd5b6101436000838361014760201b60201c565b5050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff160361019957806002600082825461018d9190610719565b9250508190555061026c565b60008060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905081811015610225578381836040517fe450d38c00000000000000000000000000000000000000000000000000000000815260040161021c9392919061075c565b60405180910390fd5b8181036000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550505b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036102b55780600260008282540392505081905550610302565b806000808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055505b8173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8360405161035f9190610793565b60405180910390a3505050565b600081519050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b600060028204905060018216806103ed57607f821691505b602082108103610400576103ff6103a6565b5b50919050565b60008190508160005260206000209050919050565b60006020601f8301049050919050565b600082821b905092915050565b6000600883026104687fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8261042b565b610472868361042b565b95508019841693508086168417925050509392505050565b6000819050919050565b6000819050919050565b60006104b96104b46104af8461048a565b610494565b61048a565b9050919050565b6000819050919050565b6104d38361049e565b6104e76104df826104c0565b848454610438565b825550505050565b600090565b6104fc6104ef565b6105078184846104ca565b505050565b5b8181101561052b576105206000826104f4565b60018101905061050d565b5050565b601f8211156105705761054181610406565b61054a8461041b565b81016020851015610559578190505b61056d6105658561041b565b83018261050c565b50505b505050565b600082821c905092915050565b600061059360001984600802610575565b1980831691505092915050565b60006105ac8383610582565b9150826002028217905092915050565b6105c58261036c565b67ffffffffffffffff8111156105de576105dd610377565b5b6105e882546103d5565b6105f382828561052f565b600060209050601f8311600181146106265760008415610614578287015190505b61061e85826105a0565b865550610686565b601f19841661063486610406565b60005b8281101561065c57848901518255600182019150602085019450602081019050610637565b868310156106795784890151610675601f891682610582565b8355505b6001600288020188555050505b505050505050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006106b98261068e565b9050919050565b6106c9816106ae565b82525050565b60006020820190506106e460008301846106c0565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006107248261048a565b915061072f8361048a565b9250828201905080821115610747576107466106ea565b5b92915050565b6107568161048a565b82525050565b600060608201905061077160008301866106c0565b61077e602083018561074d565b61078b604083018461074d565b949350505050565b60006020820190506107a8600083018461074d565b92915050565b611156806107bd6000396000f3fe608060405234801561001057600080fd5b506004361061009e5760003560e01c806370a082311161006657806370a082311461015d57806388d695b21461018d57806395d89b41146101bd578063a9059cbb146101db578063dd62ed3e1461020b5761009e565b806306fdde03146100a3578063095ea7b3146100c157806318160ddd146100f157806323b872dd1461010f578063313ce5671461013f575b600080fd5b6100ab61023b565b6040516100b89190610ba8565b60405180910390f35b6100db60048036038101906100d69190610c68565b6102cd565b6040516100e89190610cc3565b60405180910390f35b6100f96102f0565b6040516101069190610ced565b60405180910390f35b61012960048036038101906101249190610d08565b6102fa565b6040516101369190610cc3565b60405180910390f35b610147610329565b6040516101549190610d77565b60405180910390f35b61017760048036038101906101729190610d92565b610332565b6040516101849190610ced565b60405180910390f35b6101a760048036038101906101a29190610e7a565b61037a565b6040516101b49190610cc3565b60405180910390f35b6101c561043e565b6040516101d29190610ba8565b60405180910390f35b6101f560048036038101906101f09190610c68565b6104d0565b6040516102029190610cc3565b60405180910390f35b61022560048036038101906102209190610efb565b6104f3565b6040516102329190610ced565b60405180910390f35b60606003805461024a90610f6a565b80601f016020809104026020016040519081016040528092919081815260200182805461027690610f6a565b80156102c35780601f10610298576101008083540402835291602001916102c3565b820191906000526020600020905b8154815290600101906020018083116102a657829003601f168201915b5050505050905090565b6000806102d861057a565b90506102e5818585610582565b600191505092915050565b6000600254905090565b60008061030561057a565b9050610312858285610594565b61031d858585610628565b60019150509392505050565b60006012905090565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b60008282905085859050146103c4576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016103bb9061100d565b60405180910390fd5b60005b85859050811015610431576104246103dd61057a565b8787848181106103f0576103ef61102d565b5b90506020020160208101906104059190610d92565b8686858181106104185761041761102d565b5b90506020020135610628565b80806001019150506103c7565b5060019050949350505050565b60606004805461044d90610f6a565b80601f016020809104026020016040519081016040528092919081815260200182805461047990610f6a565b80156104c65780601f1061049b576101008083540402835291602001916104c6565b820191906000526020600020905b8154815290600101906020018083116104a957829003601f168201915b5050505050905090565b6000806104db61057a565b90506104e8818585610628565b600191505092915050565b6000600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905092915050565b600033905090565b61058f838383600161071c565b505050565b60006105a084846104f3565b90507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81146106225781811015610612578281836040517ffb8f41b20000000000000000000000000000000000000000000000000000000081526004016106099392919061106b565b60405180910390fd5b6106218484848403600061071c565b5b50505050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff160361069a5760006040517f96c6fd1e00000000000000000000000000000000000000000000000000000000815260040161069191906110a2565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff160361070c5760006040517fec442f0500000000000000000000000000000000000000000000000000000000815260040161070391906110a2565b60405180910390fd5b6107178383836108f3565b505050565b600073ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff160361078e5760006040517fe602df0500000000000000000000000000000000000000000000000000000000815260040161078591906110a2565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16036108005760006040517f94280d620000000000000000000000000000000000000000000000000000000081526004016107f791906110a2565b60405180910390fd5b81600160008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000208190555080156108ed578273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040516108e49190610ced565b60405180910390a35b50505050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff160361094557806002600082825461093991906110ec565b92505081905550610a18565b60008060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050818110156109d1578381836040517fe450d38c0000000000000000000000000000000000000000000000000000000081526004016109c89392919061106b565b60405180910390fd5b8181036000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550505b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603610a615780600260008282540392505081905550610aae565b806000808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055505b8173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef83604051610b0b9190610ced565b60405180910390a3505050565b600081519050919050565b600082825260208201905092915050565b60005b83811015610b52578082015181840152602081019050610b37565b60008484015250505050565b6000601f19601f8301169050919050565b6000610b7a82610b18565b610b848185610b23565b9350610b94818560208601610b34565b610b9d81610b5e565b840191505092915050565b60006020820190508181036000830152610bc28184610b6f565b905092915050565b600080fd5b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000610bff82610bd4565b9050919050565b610c0f81610bf4565b8114610c1a57600080fd5b50565b600081359050610c2c81610c06565b92915050565b6000819050919050565b610c4581610c32565b8114610c5057600080fd5b50565b600081359050610c6281610c3c565b92915050565b60008060408385031215610c7f57610c7e610bca565b5b6000610c8d85828601610c1d565b9250506020610c9e85828601610c53565b9150509250929050565b60008115159050919050565b610cbd81610ca8565b82525050565b6000602082019050610cd86000830184610cb4565b92915050565b610ce781610c32565b82525050565b6000602082019050610d026000830184610cde565b92915050565b600080600060608486031215610d2157610d20610bca565b5b6000610d2f86828701610c1d565b9350506020610d4086828701610c1d565b9250506040610d5186828701610c53565b9150509250925092565b600060ff82169050919050565b610d7181610d5b565b82525050565b6000602082019050610d8c6000830184610d68565b92915050565b600060208284031215610da857610da7610bca565b5b6000610db684828501610c1d565b91505092915050565b600080fd5b600080fd5b600080fd5b60008083601f840112610de457610de3610dbf565b5b8235905067ffffffffffffffff811115610e0157610e00610dc4565b5b602083019150836020820283011115610e1d57610e1c610dc9565b5b9250929050565b60008083601f840112610e3a57610e39610dbf565b5b8235905067ffffffffffffffff811115610e5757610e56610dc4565b5b602083019150836020820283011115610e7357610e72610dc9565b5b9250929050565b60008060008060408587031215610e9457610e93610bca565b5b600085013567ffffffffffffffff811115610eb257610eb1610bcf565b5b610ebe87828801610dce565b9450945050602085013567ffffffffffffffff811115610ee157610ee0610bcf565b5b610eed87828801610e24565b925092505092959194509250565b60008060408385031215610f1257610f11610bca565b5b6000610f2085828601610c1d565b9250506020610f3185828601610c1d565b9150509250929050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b60006002820490506001821680610f8257607f821691505b602082108103610f9557610f94610f3b565b5b50919050565b7f726563697069656e747320616e6420616d6f756e7473206c656e677468206d6960008201527f736d617463680000000000000000000000000000000000000000000000000000602082015250565b6000610ff7602683610b23565b915061100282610f9b565b604082019050919050565b6000602082019050818103600083015261102681610fea565b9050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b61106581610bf4565b82525050565b6000606082019050611080600083018661105c565b61108d6020830185610cde565b61109a6040830184610cde565b949350505050565b60006020820190506110b7600083018461105c565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006110f782610c32565b915061110283610c32565b925082820190508082111561111a576111196110bd565b5b9291505056fea2646970667358221220fdbfd2ff5d7f60018cd1ba9f99f9038e758419b539d4a76b8c9148f3b4c10b6b64736f6c634300081a0033" + "_format": "hh-sol-artifact-1", + "contractName": "TOKEN", + "sourceName": "*/TOKEN.sol", + "abi": [ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "allowance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "name": "ERC20InsufficientAllowance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "name": "ERC20InsufficientBalance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "approver", + "type": "address" + } + ], + "name": "ERC20InvalidApprover", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "name": "ERC20InvalidReceiver", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "ERC20InvalidSender", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "ERC20InvalidSpender", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "recipients", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "name": "batchTransfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x608060405234801561001057600080fd5b506040518060400160405280600681526020017f4441524b323000000000000000000000000000000000000000000000000000008152506040518060400160405280600681526020017f4441524b32300000000000000000000000000000000000000000000000000000815250816003908161008c91906105bc565b50806004908161009c91906105bc565b5050506100ba336a52b7d2dcc80cd2e40000006100bf60201b60201c565b6107ae565b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036101315760006040517fec442f0500000000000000000000000000000000000000000000000000000000815260040161012891906106cf565b60405180910390fd5b6101436000838361014760201b60201c565b5050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff160361019957806002600082825461018d9190610719565b9250508190555061026c565b60008060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905081811015610225578381836040517fe450d38c00000000000000000000000000000000000000000000000000000000815260040161021c9392919061075c565b60405180910390fd5b8181036000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550505b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036102b55780600260008282540392505081905550610302565b806000808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055505b8173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8360405161035f9190610793565b60405180910390a3505050565b600081519050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b600060028204905060018216806103ed57607f821691505b602082108103610400576103ff6103a6565b5b50919050565b60008190508160005260206000209050919050565b60006020601f8301049050919050565b600082821b905092915050565b6000600883026104687fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8261042b565b610472868361042b565b95508019841693508086168417925050509392505050565b6000819050919050565b6000819050919050565b60006104b96104b46104af8461048a565b610494565b61048a565b9050919050565b6000819050919050565b6104d38361049e565b6104e76104df826104c0565b848454610438565b825550505050565b600090565b6104fc6104ef565b6105078184846104ca565b505050565b5b8181101561052b576105206000826104f4565b60018101905061050d565b5050565b601f8211156105705761054181610406565b61054a8461041b565b81016020851015610559578190505b61056d6105658561041b565b83018261050c565b50505b505050565b600082821c905092915050565b600061059360001984600802610575565b1980831691505092915050565b60006105ac8383610582565b9150826002028217905092915050565b6105c58261036c565b67ffffffffffffffff8111156105de576105dd610377565b5b6105e882546103d5565b6105f382828561052f565b600060209050601f8311600181146106265760008415610614578287015190505b61061e85826105a0565b865550610686565b601f19841661063486610406565b60005b8281101561065c57848901518255600182019150602085019450602081019050610637565b868310156106795784890151610675601f891682610582565b8355505b6001600288020188555050505b505050505050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006106b98261068e565b9050919050565b6106c9816106ae565b82525050565b60006020820190506106e460008301846106c0565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006107248261048a565b915061072f8361048a565b9250828201905080821115610747576107466106ea565b5b92915050565b6107568161048a565b82525050565b600060608201905061077160008301866106c0565b61077e602083018561074d565b61078b604083018461074d565b949350505050565b60006020820190506107a8600083018461074d565b92915050565b611156806107bd6000396000f3fe608060405234801561001057600080fd5b506004361061009e5760003560e01c806370a082311161006657806370a082311461015d57806388d695b21461018d57806395d89b41146101bd578063a9059cbb146101db578063dd62ed3e1461020b5761009e565b806306fdde03146100a3578063095ea7b3146100c157806318160ddd146100f157806323b872dd1461010f578063313ce5671461013f575b600080fd5b6100ab61023b565b6040516100b89190610ba8565b60405180910390f35b6100db60048036038101906100d69190610c68565b6102cd565b6040516100e89190610cc3565b60405180910390f35b6100f96102f0565b6040516101069190610ced565b60405180910390f35b61012960048036038101906101249190610d08565b6102fa565b6040516101369190610cc3565b60405180910390f35b610147610329565b6040516101549190610d77565b60405180910390f35b61017760048036038101906101729190610d92565b610332565b6040516101849190610ced565b60405180910390f35b6101a760048036038101906101a29190610e7a565b61037a565b6040516101b49190610cc3565b60405180910390f35b6101c561043e565b6040516101d29190610ba8565b60405180910390f35b6101f560048036038101906101f09190610c68565b6104d0565b6040516102029190610cc3565b60405180910390f35b61022560048036038101906102209190610efb565b6104f3565b6040516102329190610ced565b60405180910390f35b60606003805461024a90610f6a565b80601f016020809104026020016040519081016040528092919081815260200182805461027690610f6a565b80156102c35780601f10610298576101008083540402835291602001916102c3565b820191906000526020600020905b8154815290600101906020018083116102a657829003601f168201915b5050505050905090565b6000806102d861057a565b90506102e5818585610582565b600191505092915050565b6000600254905090565b60008061030561057a565b9050610312858285610594565b61031d858585610628565b60019150509392505050565b60006012905090565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b60008282905085859050146103c4576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016103bb9061100d565b60405180910390fd5b60005b85859050811015610431576104246103dd61057a565b8787848181106103f0576103ef61102d565b5b90506020020160208101906104059190610d92565b8686858181106104185761041761102d565b5b90506020020135610628565b80806001019150506103c7565b5060019050949350505050565b60606004805461044d90610f6a565b80601f016020809104026020016040519081016040528092919081815260200182805461047990610f6a565b80156104c65780601f1061049b576101008083540402835291602001916104c6565b820191906000526020600020905b8154815290600101906020018083116104a957829003601f168201915b5050505050905090565b6000806104db61057a565b90506104e8818585610628565b600191505092915050565b6000600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905092915050565b600033905090565b61058f838383600161071c565b505050565b60006105a084846104f3565b90507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81146106225781811015610612578281836040517ffb8f41b20000000000000000000000000000000000000000000000000000000081526004016106099392919061106b565b60405180910390fd5b6106218484848403600061071c565b5b50505050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff160361069a5760006040517f96c6fd1e00000000000000000000000000000000000000000000000000000000815260040161069191906110a2565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff160361070c5760006040517fec442f0500000000000000000000000000000000000000000000000000000000815260040161070391906110a2565b60405180910390fd5b6107178383836108f3565b505050565b600073ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff160361078e5760006040517fe602df0500000000000000000000000000000000000000000000000000000000815260040161078591906110a2565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16036108005760006040517f94280d620000000000000000000000000000000000000000000000000000000081526004016107f791906110a2565b60405180910390fd5b81600160008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000208190555080156108ed578273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040516108e49190610ced565b60405180910390a35b50505050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff160361094557806002600082825461093991906110ec565b92505081905550610a18565b60008060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050818110156109d1578381836040517fe450d38c0000000000000000000000000000000000000000000000000000000081526004016109c89392919061106b565b60405180910390fd5b8181036000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550505b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603610a615780600260008282540392505081905550610aae565b806000808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055505b8173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef83604051610b0b9190610ced565b60405180910390a3505050565b600081519050919050565b600082825260208201905092915050565b60005b83811015610b52578082015181840152602081019050610b37565b60008484015250505050565b6000601f19601f8301169050919050565b6000610b7a82610b18565b610b848185610b23565b9350610b94818560208601610b34565b610b9d81610b5e565b840191505092915050565b60006020820190508181036000830152610bc28184610b6f565b905092915050565b600080fd5b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000610bff82610bd4565b9050919050565b610c0f81610bf4565b8114610c1a57600080fd5b50565b600081359050610c2c81610c06565b92915050565b6000819050919050565b610c4581610c32565b8114610c5057600080fd5b50565b600081359050610c6281610c3c565b92915050565b60008060408385031215610c7f57610c7e610bca565b5b6000610c8d85828601610c1d565b9250506020610c9e85828601610c53565b9150509250929050565b60008115159050919050565b610cbd81610ca8565b82525050565b6000602082019050610cd86000830184610cb4565b92915050565b610ce781610c32565b82525050565b6000602082019050610d026000830184610cde565b92915050565b600080600060608486031215610d2157610d20610bca565b5b6000610d2f86828701610c1d565b9350506020610d4086828701610c1d565b9250506040610d5186828701610c53565b9150509250925092565b600060ff82169050919050565b610d7181610d5b565b82525050565b6000602082019050610d8c6000830184610d68565b92915050565b600060208284031215610da857610da7610bca565b5b6000610db684828501610c1d565b91505092915050565b600080fd5b600080fd5b600080fd5b60008083601f840112610de457610de3610dbf565b5b8235905067ffffffffffffffff811115610e0157610e00610dc4565b5b602083019150836020820283011115610e1d57610e1c610dc9565b5b9250929050565b60008083601f840112610e3a57610e39610dbf565b5b8235905067ffffffffffffffff811115610e5757610e56610dc4565b5b602083019150836020820283011115610e7357610e72610dc9565b5b9250929050565b60008060008060408587031215610e9457610e93610bca565b5b600085013567ffffffffffffffff811115610eb257610eb1610bcf565b5b610ebe87828801610dce565b9450945050602085013567ffffffffffffffff811115610ee157610ee0610bcf565b5b610eed87828801610e24565b925092505092959194509250565b60008060408385031215610f1257610f11610bca565b5b6000610f2085828601610c1d565b9250506020610f3185828601610c1d565b9150509250929050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b60006002820490506001821680610f8257607f821691505b602082108103610f9557610f94610f3b565b5b50919050565b7f726563697069656e747320616e6420616d6f756e7473206c656e677468206d6960008201527f736d617463680000000000000000000000000000000000000000000000000000602082015250565b6000610ff7602683610b23565b915061100282610f9b565b604082019050919050565b6000602082019050818103600083015261102681610fea565b9050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b61106581610bf4565b82525050565b6000606082019050611080600083018661105c565b61108d6020830185610cde565b61109a6040830184610cde565b949350505050565b60006020820190506110b7600083018461105c565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006110f782610c32565b915061110283610c32565b925082820190508082111561111a576111196110bd565b5b9291505056fea2646970667358221220fdbfd2ff5d7f60018cd1ba9f99f9038e758419b539d4a76b8c9148f3b4c10b6b64736f6c634300081a0033" } diff --git a/src/Utils/TransactionEncoder.php b/src/Utils/TransactionEncoder.php index 905613ad..063a7697 100644 --- a/src/Utils/TransactionEncoder.php +++ b/src/Utils/TransactionEncoder.php @@ -6,18 +6,27 @@ use ArkEcosystem\Crypto\Enums\AbiFunction; use ArkEcosystem\Crypto\Enums\ContractAbiType; +use Brick\Math\BigDecimal; class TransactionEncoder { + /** + * @param string[] $recipients + * @param BigDecimal[] $amounts + */ public static function multiPayment(array $recipients, array $amounts): string { + if (count($recipients) !== count($amounts)) { + throw new \InvalidArgumentException('The number of recipients must match the number of amounts.'); + } + return (new AbiEncoder(ContractAbiType::MULTIPAYMENT))->encodeFunctionCall( AbiFunction::MULTIPAYMENT->value, [$recipients, $amounts] ); } - public static function tokenTransfer(string $recipientAddress, $amount): string + public static function tokenTransfer(string $recipientAddress, BigDecimal $amount): string { return (new AbiEncoder(ContractAbiType::TOKEN))->encodeFunctionCall( AbiFunction::TRANSFER->value, From 5f45f1f5267eba20da3bd13fab266e22eb720507 Mon Sep 17 00:00:00 2001 From: Alex Barnsley <8069294+alexbarnsley@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:39:30 +0000 Subject: [PATCH 4/7] remove check for multipayment param count --- src/Utils/TransactionEncoder.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Utils/TransactionEncoder.php b/src/Utils/TransactionEncoder.php index 063a7697..8170c2ca 100644 --- a/src/Utils/TransactionEncoder.php +++ b/src/Utils/TransactionEncoder.php @@ -13,13 +13,11 @@ class TransactionEncoder /** * @param string[] $recipients * @param BigDecimal[] $amounts + * + * @return string */ public static function multiPayment(array $recipients, array $amounts): string { - if (count($recipients) !== count($amounts)) { - throw new \InvalidArgumentException('The number of recipients must match the number of amounts.'); - } - return (new AbiEncoder(ContractAbiType::MULTIPAYMENT))->encodeFunctionCall( AbiFunction::MULTIPAYMENT->value, [$recipients, $amounts] From b47655e344caaa60cac4643a6dec4e19b886cf14 Mon Sep 17 00:00:00 2001 From: Alex Barnsley <8069294+alexbarnsley@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:39:42 +0000 Subject: [PATCH 5/7] test token transfer with bigdecimal --- tests/Unit/Utils/TransactionEncoderTest.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/Unit/Utils/TransactionEncoderTest.php b/tests/Unit/Utils/TransactionEncoderTest.php index 34eb9337..c0addd0c 100644 --- a/tests/Unit/Utils/TransactionEncoderTest.php +++ b/tests/Unit/Utils/TransactionEncoderTest.php @@ -3,6 +3,7 @@ declare(strict_types=1); use ArkEcosystem\Crypto\Utils\TransactionEncoder; +use ArkEcosystem\Crypto\Utils\UnitConverter; it('should encode a multipayment payload', function () { $encoded = TransactionEncoder::multiPayment( @@ -23,7 +24,7 @@ it('should encode a token transfer payload', function () { $encoded = TransactionEncoder::tokenTransfer( '0xA5cc0BfEB09742C5e4C610f2EBaaB82Eb142Ca10', - '1000000000000' + UnitConverter::parseUnits('1000000000000', 'wei') ); expect($encoded)->toBe( @@ -33,7 +34,7 @@ }); it('should reject invalid address format in token transfer', function () { - TransactionEncoder::tokenTransfer('', '1000'); + TransactionEncoder::tokenTransfer('', UnitConverter::parseUnits('1000', 'wei')); })->throws(Exception::class); it('should reject invalid address format in multipayment', function () { From 49d3a5555f0e49ced655dd63c06296c50fc041ea Mon Sep 17 00:00:00 2001 From: Alex Barnsley <8069294+alexbarnsley@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:43:54 +0000 Subject: [PATCH 6/7] feat: transaction type identity implementation --- src/Utils/AbiBase.php | 42 ++++++- src/Utils/TransactionTypeIdentifier.php | 112 ++++++++++++++++++ .../Utils/TransactionTypeIdentifierTest.php | 101 ++++++++++++++++ 3 files changed, 251 insertions(+), 4 deletions(-) create mode 100644 src/Utils/TransactionTypeIdentifier.php create mode 100644 tests/Unit/Utils/TransactionTypeIdentifierTest.php diff --git a/src/Utils/AbiBase.php b/src/Utils/AbiBase.php index 4c1e92ec..70cef70a 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,7 +88,7 @@ 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: @@ -87,7 +100,28 @@ private function contractAbiPath(ContractAbiType $type, ?string $path = null): ? case ContractAbiType::TOKEN: return __DIR__.'/Abi/json/Abi.Token.json'; case ContractAbiType::CUSTOM: + if ($path === null || $path === '') { + throw new \RuntimeException('Custom ABI type requires a valid path.'); + } + return $path; } } + + 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 00000000..608b4f2d --- /dev/null +++ b/src/Utils/TransactionTypeIdentifier.php @@ -0,0 +1,112 @@ + $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/Utils/TransactionTypeIdentifierTest.php b/tests/Unit/Utils/TransactionTypeIdentifierTest.php new file mode 100644 index 00000000..5c1f70f7 --- /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(); +}); From 17c94f4d4c9399699b10652aef7366f6fd5524d5 Mon Sep 17 00:00:00 2001 From: alexbarnsley Date: Thu, 5 Mar 2026 17:51:05 +0000 Subject: [PATCH 7/7] style: resolve style guide violations --- src/Utils/TransactionTypeIdentifier.php | 23 ++++++++++--------- .../Utils/TransactionTypeIdentifierTest.php | 4 ++-- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Utils/TransactionTypeIdentifier.php b/src/Utils/TransactionTypeIdentifier.php index 608b4f2d..5e59f1be 100644 --- a/src/Utils/TransactionTypeIdentifier.php +++ b/src/Utils/TransactionTypeIdentifier.php @@ -60,9 +60,9 @@ public static function isUpdateValidator(string $data): bool public static function isTokenTransfer(string $data): bool { - $decodedData = static::decodeTokenFunction($data); + $decodedData = static::decodeTokenFunction($data); - return $decodedData ? $decodedData['functionName'] === "transfer" : false; + return $decodedData ? $decodedData['functionName'] === 'transfer' : false; } private static function startsWithSignature(string $data, string $signature): bool @@ -98,15 +98,16 @@ private static function signatures(): array return self::$signatures; } - private static function decodeTokenFunction(string $data): ?array { - try { - $decodedData = (new AbiDecoder(ContractAbiType::TOKEN))->decodeFunctionData($data); + 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 ['functionName' => $decodedData['functionName'], 'args' => $decodedData['args']]; + } catch (\Exception $e) { + // Different abi type. Ignore. + } - return null; - } + return null; + } } diff --git a/tests/Unit/Utils/TransactionTypeIdentifierTest.php b/tests/Unit/Utils/TransactionTypeIdentifierTest.php index 5c1f70f7..ec8737c0 100644 --- a/tests/Unit/Utils/TransactionTypeIdentifierTest.php +++ b/tests/Unit/Utils/TransactionTypeIdentifierTest.php @@ -19,9 +19,9 @@ function loadMethodIdentifiers(string $path): array } beforeEach(function () { - $this->consensusMethods = loadMethodIdentifiers(dirname(__DIR__, 3).'/src/Utils/Abi/json/Abi.Consensus.json'); + $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'); + $this->usernamesMethods = loadMethodIdentifiers(dirname(__DIR__, 3).'/src/Utils/Abi/json/Abi.Usernames.json'); }); it('identifies transfer by empty payload', function () {