Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: multipayment support #144

Merged
merged 9 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
}
Loading