Skip to content

Commit

Permalink
feat: multipayment support (#144)
Browse files Browse the repository at this point in the history
  • Loading branch information
ItsANameToo authored Dec 19, 2024
1 parent 20b4118 commit 74a1e52
Show file tree
Hide file tree
Showing 13 changed files with 241 additions and 6 deletions.
3 changes: 3 additions & 0 deletions src/Enums/AbiFunction.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace ArkEcosystem\Crypto\Enums;

use ArkEcosystem\Crypto\Transactions\Types\Multipayment;
use ArkEcosystem\Crypto\Transactions\Types\Unvote;
use ArkEcosystem\Crypto\Transactions\Types\UsernameRegistration;
use ArkEcosystem\Crypto\Transactions\Types\UsernameResignation;
Expand All @@ -19,6 +20,7 @@ enum AbiFunction: string
case VALIDATOR_RESIGNATION = 'resignValidator';
case USERNAME_REGISTRATION = 'registerUsername';
case USERNAME_RESIGNATION = 'resignUsername';
case MULTIPAYMENT = 'pay';

public function transactionClass(): string
{
Expand All @@ -29,6 +31,7 @@ public function transactionClass(): string
self::VALIDATOR_RESIGNATION => ValidatorResignation::class,
self::USERNAME_REGISTRATION => UsernameRegistration::class,
self::USERNAME_RESIGNATION => UsernameResignation::class,
self::MULTIPAYMENT => Multipayment::class,
};
}
}
5 changes: 5 additions & 0 deletions src/Helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,9 @@ public static function isValidUsername(string $username): bool

return true;
}

public static function removeLeadingHexZero(string $hex): string
{
return preg_replace('/^0x/', '', $hex); // using ltrim($hex, '0x') also removes leading 0s which is not desired, e.g. 0x0123 -> 123
}
}
39 changes: 39 additions & 0 deletions src/Transactions/Builder/MultipaymentBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace ArkEcosystem\Crypto\Transactions\Builder;

use ArkEcosystem\Crypto\Enums\ContractAddresses;
use ArkEcosystem\Crypto\Transactions\Types\AbstractTransaction;
use ArkEcosystem\Crypto\Transactions\Types\Multipayment;

class MultipaymentBuilder extends AbstractTransactionBuilder
{
public function __construct(?array $data = null)
{
parent::__construct($data);

$this->recipientAddress(ContractAddresses::MULTIPAYMENT->value);

$this->transaction->data['pay'] = [[], []];
$this->transaction->refreshPayloadData();
}

public function pay(string $address, string $amount): self
{
$this->transaction->data['pay'][0][] = $address;
$this->transaction->data['pay'][1][] = $amount;

$this->transaction->refreshPayloadData();

$this->transaction->data['value'] += $amount;

return $this;
}

protected function getTransactionInstance(?array $data = []): AbstractTransaction
{
return new Multipayment($data);
}
}
8 changes: 5 additions & 3 deletions src/Transactions/Types/AbstractTransaction.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
namespace ArkEcosystem\Crypto\Transactions\Types;

use ArkEcosystem\Crypto\Configuration\Network;
use ArkEcosystem\Crypto\Enums\ContractAbiType;
use ArkEcosystem\Crypto\Helpers;
use ArkEcosystem\Crypto\Identities\Address;
use ArkEcosystem\Crypto\Transactions\Serializer;
use ArkEcosystem\Crypto\Utils\AbiDecoder;
Expand Down Expand Up @@ -33,7 +35,7 @@ abstract public function getPayload(): string;

public function refreshPayloadData(): static
{
$this->data['data'] = ltrim($this->getPayload(), '0x');
$this->data['data'] = Helpers::removeLeadingHexZero($this->getPayload());

return $this;
}
Expand Down Expand Up @@ -170,7 +172,7 @@ public function hash(bool $skipSignature): BufferInterface
return TransactionHasher::toHash($hashData, $skipSignature);
}

protected function decodePayload(array $data): ?array
protected function decodePayload(array $data, ContractAbiType $type = ContractAbiType::CONSENSUS): ?array
{
if (! isset($data['data'])) {
return null;
Expand All @@ -182,7 +184,7 @@ protected function decodePayload(array $data): ?array
return null;
}

return (new AbiDecoder())->decodeFunctionData($payload);
return (new AbiDecoder($type))->decodeFunctionData($payload);
}

private function getSignature(): CompactSignatureInterface
Expand Down
32 changes: 32 additions & 0 deletions src/Transactions/Types/Multipayment.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace ArkEcosystem\Crypto\Transactions\Types;

use ArkEcosystem\Crypto\Enums\AbiFunction;
use ArkEcosystem\Crypto\Enums\ContractAbiType;
use ArkEcosystem\Crypto\Utils\AbiEncoder;

class Multipayment extends AbstractTransaction
{
public function __construct(?array $data = [])
{
$payload = $this->decodePayload($data, ContractAbiType::MULTIPAYMENT);

if ($payload !== null) {
$data['pay'] = $payload['args'];
}

parent::__construct($data);
}

public function getPayload(): string
{
if (! array_key_exists('pay', $this->data)) {
return '';
}

return (new AbiEncoder(ContractAbiType::MULTIPAYMENT))->encodeFunctionCall(AbiFunction::MULTIPAYMENT->value, $this->data['pay']);
}
}
3 changes: 2 additions & 1 deletion src/Transactions/Types/ValidatorRegistration.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace ArkEcosystem\Crypto\Transactions\Types;

use ArkEcosystem\Crypto\Enums\AbiFunction;
use ArkEcosystem\Crypto\Helpers;
use ArkEcosystem\Crypto\Utils\AbiEncoder;

class ValidatorRegistration extends AbstractTransaction
Expand All @@ -14,7 +15,7 @@ public function __construct(?array $data = [])
$payload = $this->decodePayload($data);

if ($payload !== null) {
$data['validatorPublicKey'] = ltrim($payload['args'][0], '0x');
$data['validatorPublicKey'] = Helpers::removeLeadingHexZero($payload['args'][0]);
}

parent::__construct($data);
Expand Down
2 changes: 1 addition & 1 deletion src/Utils/AbiBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ private function contractAbiPath(ContractAbiType $type, string $path = null): ?s
case ContractAbiType::CONSENSUS:
return __DIR__.'/Abi/json/Abi.Consensus.json';
case ContractAbiType::MULTIPAYMENT:
return __DIR__.'/Abi/json/Abi.MultiPayment.json';
return __DIR__.'/Abi/json/Abi.Multipayment.json';
case ContractAbiType::USERNAMES:
return __DIR__.'/Abi/json/Abi.Usernames.json';
case ContractAbiType::CUSTOM:
Expand Down
3 changes: 2 additions & 1 deletion src/Utils/TransactionHasher.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace ArkEcosystem\Crypto\Utils;

use ArkEcosystem\Crypto\Helpers;
use BitWasp\Bitcoin\Crypto\Hash;
use BitWasp\Buffertools\Buffer;
use BitWasp\Buffertools\BufferInterface;
Expand Down Expand Up @@ -31,7 +32,7 @@ public static function toHash(array $transaction, bool $skipSignature = false):
self::toBeArray($transaction['gasLimit']),
$recipientAddress,
self::toBeArray($transaction['value']),
isset($transaction['data']) ? hex2bin(ltrim($transaction['data'], '0x')) : '',
isset($transaction['data']) ? hex2bin(Helpers::removeLeadingHexZero($transaction['data'])) : '',
[], // accessList is unused
];

Expand Down
9 changes: 9 additions & 0 deletions tests/Unit/HelpersTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@
*/
class HelpersTest extends TestCase
{
/** @test */
public function it_should_trim_hex_values_properly(): void
{
$this->assertSame('0123', Helpers::removeLeadingHexZero('0x0123'));
$this->assertSame('0123', Helpers::removeLeadingHexZero('0123'));
$this->assertSame('1234', Helpers::removeLeadingHexZero('0x1234'));
$this->assertSame('0000', Helpers::removeLeadingHexZero('0x0000'));
}

/**
* @test
* @dataProvider validUsernamesProvider
Expand Down
95 changes: 95 additions & 0 deletions tests/Unit/Transactions/Builder/MultipaymentBuilderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

declare(strict_types=1);

namespace ArkEcosystem\Tests\Crypto\Unit\Transactions\Builder;

use ArkEcosystem\Crypto\Enums\ContractAbiType;
use ArkEcosystem\Crypto\Identities\PrivateKey;
use ArkEcosystem\Crypto\Transactions\Builder\MultipaymentBuilder;
use ArkEcosystem\Crypto\Transactions\Types\Multipayment;
use ArkEcosystem\Crypto\Utils\AbiEncoder;
use ArkEcosystem\Tests\Crypto\TestCase;

/**
* @covers \ArkEcosystem\Crypto\Transactions\Builder\MultipaymentBuilder
*/
class MultipaymentBuilderTest extends TestCase
{
/** @test */
public function it_should_sign_it_with_a_passphrase()
{
$fixture = $this->getTransactionFixture('evm_call', 'multipayment');

$builder = MultipaymentBuilder::new()
->gasPrice($fixture['data']['gasPrice'])
->nonce($fixture['data']['nonce'])
->network($fixture['data']['network'])
->gasLimit($fixture['data']['gasLimit'])
->pay('0x8233F6Df6449D7655f4643D2E752DC8D2283fAd5', '1000000000000000000')
->pay('0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22', '2000000000000000000')
->sign($this->passphrase);

$this->assertSame($fixture['serialized'], $builder->transaction->serialize()->getHex());

$this->assertSame($fixture['data']['id'], $builder->transaction->data['id']);

$this->assertTrue($builder->verify());
}

/** @test */
public function it_should_handle_single_recipient()
{
$fixture = $this->getTransactionFixture('evm_call', 'multipayment-1');

$builder = MultipaymentBuilder::new()
->gasPrice($fixture['data']['gasPrice'])
->nonce($fixture['data']['nonce'])
->network($fixture['data']['network'])
->gasLimit($fixture['data']['gasLimit'])
->pay('0x8233F6Df6449D7655f4643D2E752DC8D2283fAd5', '1000000000000000000')
->sign($this->passphrase);

$this->assertSame($fixture['serialized'], $builder->transaction->serialize()->getHex());

$this->assertSame($fixture['data']['id'], $builder->transaction->data['id']);

$this->assertTrue($builder->verify());
}

/** @test */
public function it_should_handle_empty_payment()
{
$fixture = $this->getTransactionFixture('evm_call', 'multipayment-0');

$builder = MultipaymentBuilder::new()
->gasPrice($fixture['data']['gasPrice'])
->nonce($fixture['data']['nonce'])
->network($fixture['data']['network'])
->gasLimit($fixture['data']['gasLimit'])
->sign($this->passphrase);

$this->assertSame($fixture['serialized'], $builder->transaction->serialize()->getHex());

$this->assertSame($fixture['data']['id'], $builder->transaction->data['id']);

$this->assertTrue($builder->verify());
}

// TODO: fix decoder issue first
// /** @test */
// public function it_should_be_possible_to_create_manual_multipayment()
// {
// $fixture = $this->getTransactionFixture('evm_call', 'multipayment-1');

// $payload = (new AbiEncoder(ContractAbiType::MULTIPAYMENT))->encodeFunctionCall('pay', [['0x8233F6Df6449D7655f4643D2E752DC8D2283fAd5'], ['1000000000000000000']]);
// $tx = (new Multipayment(['data' => $payload]));
// $tx->data['nonce'] = $fixture['data']['nonce'];
// $tx->data['network'] = $fixture['data']['network'];
// $tx->data['gasLimit'] = $fixture['data']['gasLimit'];
// $tx->data['gasPrice'] = $fixture['data']['gasPrice'];
// $tx->sign(PrivateKey::fromPassphrase($this->passphrase));

// $this->assertTrue($tx->verify());
// }
}
16 changes: 16 additions & 0 deletions tests/fixtures/transactions/evm_call/multipayment-0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"data": {
"network": 30,
"nonce": "1",
"gasPrice": 5,
"gasLimit": 1000000,
"value": "0",
"recipientAddress": "0x83769BeEB7e5405ef0B7dc3C66C43E3a51A6d27f",
"data": "084ce7080000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"signature": "97e06c7c8c2d7d9409d0330113784126ff87e873f777864295c28403e2c8fc1d2bf455c94b8b72e70d6ac414aadbcc5c9a3c3cca82686587345346ec5554d09500",
"senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3",
"senderAddress": "0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22",
"id": "8cc750a6260fcf0a56e88a5c0e8e1b8f6edda3747b9517533e3081fa78735290"
},
"serialized": "1e01000000000000000500000040420f0000000000000000000000000000000000000000000000000000000000000000000183769beeb7e5405ef0b7dc3c66c43e3a51a6d27f84000000084ce708000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000097e06c7c8c2d7d9409d0330113784126ff87e873f777864295c28403e2c8fc1d2bf455c94b8b72e70d6ac414aadbcc5c9a3c3cca82686587345346ec5554d09500"
}
16 changes: 16 additions & 0 deletions tests/fixtures/transactions/evm_call/multipayment-1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"data": {
"network": 30,
"nonce": "19",
"gasPrice": 5,
"gasLimit": 1000000,
"value": "1000000000000000000",
"recipientAddress": "0x83769BeEB7e5405ef0B7dc3C66C43E3a51A6d27f",
"data": "084ce7080000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000008233f6df6449d7655f4643d2e752dc8d2283fad500000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000de0b6b3a7640000",
"signature": "fe5f074906a6afebb80bbcd77b932093667c74bbbec5b9cd32c2653ee7ac593e50c4279ea762dcd4096f90bf95a9aa5574cfb4c278e364c13b281d90356632c400",
"senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3",
"senderAddress": "0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22",
"id": "b86d867f1385c9f3c8e6a4b665618c911a28d2cd3d961ee5044491253d64fc5d"
},
"serialized": "1e13000000000000000500000040420f000000000000000000000000000000000000000000000000000de0b6b3a76400000183769beeb7e5405ef0b7dc3c66c43e3a51a6d27fc4000000084ce7080000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000008233f6df6449d7655f4643d2e752dc8d2283fad500000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000de0b6b3a7640000fe5f074906a6afebb80bbcd77b932093667c74bbbec5b9cd32c2653ee7ac593e50c4279ea762dcd4096f90bf95a9aa5574cfb4c278e364c13b281d90356632c400"
}
16 changes: 16 additions & 0 deletions tests/fixtures/transactions/evm_call/multipayment.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"data": {
"network": 30,
"nonce": "18",
"gasPrice": 5,
"gasLimit": 1000000,
"value": "3000000000000000000",
"recipientAddress": "0x83769BeEB7e5405ef0B7dc3C66C43E3a51A6d27f",
"data": "084ce708000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000020000000000000000000000008233f6df6449d7655f4643d2e752dc8d2283fad50000000000000000000000006f0182a0cc707b055322ccf6d4cb6a5aff1aeb2200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000001bc16d674ec80000",
"signature": "505f18f42724f6654895b230e1b5454000d2bea35140932ec8ffd2c8ee0a09da1ca1f516767b2b0873fcba39f663250ab4d2b1cad2637e657aacfe3ac28896b000",
"senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3",
"senderAddress": "0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22",
"id": "a275211280ee214a5e86a70c65b8f0963fcaa93d9bbcd889e7a4cfc0a96ae14b"
},
"serialized": "1e12000000000000000500000040420f0000000000000000000000000000000000000000000000000029a2241af62c00000183769beeb7e5405ef0b7dc3c66c43e3a51a6d27f04010000084ce708000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000020000000000000000000000008233f6df6449d7655f4643d2e752dc8d2283fad50000000000000000000000006f0182a0cc707b055322ccf6d4cb6a5aff1aeb2200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000001bc16d674ec80000505f18f42724f6654895b230e1b5454000d2bea35140932ec8ffd2c8ee0a09da1ca1f516767b2b0873fcba39f663250ab4d2b1cad2637e657aacfe3ac28896b000"
}

0 comments on commit 74a1e52

Please sign in to comment.