Skip to content

Commit 74a1e52

Browse files
authored
feat: multipayment support (#144)
1 parent 20b4118 commit 74a1e52

File tree

13 files changed

+241
-6
lines changed

13 files changed

+241
-6
lines changed

src/Enums/AbiFunction.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace ArkEcosystem\Crypto\Enums;
66

7+
use ArkEcosystem\Crypto\Transactions\Types\Multipayment;
78
use ArkEcosystem\Crypto\Transactions\Types\Unvote;
89
use ArkEcosystem\Crypto\Transactions\Types\UsernameRegistration;
910
use ArkEcosystem\Crypto\Transactions\Types\UsernameResignation;
@@ -19,6 +20,7 @@ enum AbiFunction: string
1920
case VALIDATOR_RESIGNATION = 'resignValidator';
2021
case USERNAME_REGISTRATION = 'registerUsername';
2122
case USERNAME_RESIGNATION = 'resignUsername';
23+
case MULTIPAYMENT = 'pay';
2224

2325
public function transactionClass(): string
2426
{
@@ -29,6 +31,7 @@ public function transactionClass(): string
2931
self::VALIDATOR_RESIGNATION => ValidatorResignation::class,
3032
self::USERNAME_REGISTRATION => UsernameRegistration::class,
3133
self::USERNAME_RESIGNATION => UsernameResignation::class,
34+
self::MULTIPAYMENT => Multipayment::class,
3235
};
3336
}
3437
}

src/Helpers.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,9 @@ public static function isValidUsername(string $username): bool
5656

5757
return true;
5858
}
59+
60+
public static function removeLeadingHexZero(string $hex): string
61+
{
62+
return preg_replace('/^0x/', '', $hex); // using ltrim($hex, '0x') also removes leading 0s which is not desired, e.g. 0x0123 -> 123
63+
}
5964
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ArkEcosystem\Crypto\Transactions\Builder;
6+
7+
use ArkEcosystem\Crypto\Enums\ContractAddresses;
8+
use ArkEcosystem\Crypto\Transactions\Types\AbstractTransaction;
9+
use ArkEcosystem\Crypto\Transactions\Types\Multipayment;
10+
11+
class MultipaymentBuilder extends AbstractTransactionBuilder
12+
{
13+
public function __construct(?array $data = null)
14+
{
15+
parent::__construct($data);
16+
17+
$this->recipientAddress(ContractAddresses::MULTIPAYMENT->value);
18+
19+
$this->transaction->data['pay'] = [[], []];
20+
$this->transaction->refreshPayloadData();
21+
}
22+
23+
public function pay(string $address, string $amount): self
24+
{
25+
$this->transaction->data['pay'][0][] = $address;
26+
$this->transaction->data['pay'][1][] = $amount;
27+
28+
$this->transaction->refreshPayloadData();
29+
30+
$this->transaction->data['value'] += $amount;
31+
32+
return $this;
33+
}
34+
35+
protected function getTransactionInstance(?array $data = []): AbstractTransaction
36+
{
37+
return new Multipayment($data);
38+
}
39+
}

src/Transactions/Types/AbstractTransaction.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
namespace ArkEcosystem\Crypto\Transactions\Types;
66

77
use ArkEcosystem\Crypto\Configuration\Network;
8+
use ArkEcosystem\Crypto\Enums\ContractAbiType;
9+
use ArkEcosystem\Crypto\Helpers;
810
use ArkEcosystem\Crypto\Identities\Address;
911
use ArkEcosystem\Crypto\Transactions\Serializer;
1012
use ArkEcosystem\Crypto\Utils\AbiDecoder;
@@ -33,7 +35,7 @@ abstract public function getPayload(): string;
3335

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

3840
return $this;
3941
}
@@ -170,7 +172,7 @@ public function hash(bool $skipSignature): BufferInterface
170172
return TransactionHasher::toHash($hashData, $skipSignature);
171173
}
172174

173-
protected function decodePayload(array $data): ?array
175+
protected function decodePayload(array $data, ContractAbiType $type = ContractAbiType::CONSENSUS): ?array
174176
{
175177
if (! isset($data['data'])) {
176178
return null;
@@ -182,7 +184,7 @@ protected function decodePayload(array $data): ?array
182184
return null;
183185
}
184186

185-
return (new AbiDecoder())->decodeFunctionData($payload);
187+
return (new AbiDecoder($type))->decodeFunctionData($payload);
186188
}
187189

188190
private function getSignature(): CompactSignatureInterface
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ArkEcosystem\Crypto\Transactions\Types;
6+
7+
use ArkEcosystem\Crypto\Enums\AbiFunction;
8+
use ArkEcosystem\Crypto\Enums\ContractAbiType;
9+
use ArkEcosystem\Crypto\Utils\AbiEncoder;
10+
11+
class Multipayment extends AbstractTransaction
12+
{
13+
public function __construct(?array $data = [])
14+
{
15+
$payload = $this->decodePayload($data, ContractAbiType::MULTIPAYMENT);
16+
17+
if ($payload !== null) {
18+
$data['pay'] = $payload['args'];
19+
}
20+
21+
parent::__construct($data);
22+
}
23+
24+
public function getPayload(): string
25+
{
26+
if (! array_key_exists('pay', $this->data)) {
27+
return '';
28+
}
29+
30+
return (new AbiEncoder(ContractAbiType::MULTIPAYMENT))->encodeFunctionCall(AbiFunction::MULTIPAYMENT->value, $this->data['pay']);
31+
}
32+
}

src/Transactions/Types/ValidatorRegistration.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace ArkEcosystem\Crypto\Transactions\Types;
66

77
use ArkEcosystem\Crypto\Enums\AbiFunction;
8+
use ArkEcosystem\Crypto\Helpers;
89
use ArkEcosystem\Crypto\Utils\AbiEncoder;
910

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

1617
if ($payload !== null) {
17-
$data['validatorPublicKey'] = ltrim($payload['args'][0], '0x');
18+
$data['validatorPublicKey'] = Helpers::removeLeadingHexZero($payload['args'][0]);
1819
}
1920

2021
parent::__construct($data);

src/Utils/AbiBase.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ private function contractAbiPath(ContractAbiType $type, string $path = null): ?s
8181
case ContractAbiType::CONSENSUS:
8282
return __DIR__.'/Abi/json/Abi.Consensus.json';
8383
case ContractAbiType::MULTIPAYMENT:
84-
return __DIR__.'/Abi/json/Abi.MultiPayment.json';
84+
return __DIR__.'/Abi/json/Abi.Multipayment.json';
8585
case ContractAbiType::USERNAMES:
8686
return __DIR__.'/Abi/json/Abi.Usernames.json';
8787
case ContractAbiType::CUSTOM:

src/Utils/TransactionHasher.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace ArkEcosystem\Crypto\Utils;
66

7+
use ArkEcosystem\Crypto\Helpers;
78
use BitWasp\Bitcoin\Crypto\Hash;
89
use BitWasp\Buffertools\Buffer;
910
use BitWasp\Buffertools\BufferInterface;
@@ -31,7 +32,7 @@ public static function toHash(array $transaction, bool $skipSignature = false):
3132
self::toBeArray($transaction['gasLimit']),
3233
$recipientAddress,
3334
self::toBeArray($transaction['value']),
34-
isset($transaction['data']) ? hex2bin(ltrim($transaction['data'], '0x')) : '',
35+
isset($transaction['data']) ? hex2bin(Helpers::removeLeadingHexZero($transaction['data'])) : '',
3536
[], // accessList is unused
3637
];
3738

tests/Unit/HelpersTest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@
1313
*/
1414
class HelpersTest extends TestCase
1515
{
16+
/** @test */
17+
public function it_should_trim_hex_values_properly(): void
18+
{
19+
$this->assertSame('0123', Helpers::removeLeadingHexZero('0x0123'));
20+
$this->assertSame('0123', Helpers::removeLeadingHexZero('0123'));
21+
$this->assertSame('1234', Helpers::removeLeadingHexZero('0x1234'));
22+
$this->assertSame('0000', Helpers::removeLeadingHexZero('0x0000'));
23+
}
24+
1625
/**
1726
* @test
1827
* @dataProvider validUsernamesProvider
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ArkEcosystem\Tests\Crypto\Unit\Transactions\Builder;
6+
7+
use ArkEcosystem\Crypto\Enums\ContractAbiType;
8+
use ArkEcosystem\Crypto\Identities\PrivateKey;
9+
use ArkEcosystem\Crypto\Transactions\Builder\MultipaymentBuilder;
10+
use ArkEcosystem\Crypto\Transactions\Types\Multipayment;
11+
use ArkEcosystem\Crypto\Utils\AbiEncoder;
12+
use ArkEcosystem\Tests\Crypto\TestCase;
13+
14+
/**
15+
* @covers \ArkEcosystem\Crypto\Transactions\Builder\MultipaymentBuilder
16+
*/
17+
class MultipaymentBuilderTest extends TestCase
18+
{
19+
/** @test */
20+
public function it_should_sign_it_with_a_passphrase()
21+
{
22+
$fixture = $this->getTransactionFixture('evm_call', 'multipayment');
23+
24+
$builder = MultipaymentBuilder::new()
25+
->gasPrice($fixture['data']['gasPrice'])
26+
->nonce($fixture['data']['nonce'])
27+
->network($fixture['data']['network'])
28+
->gasLimit($fixture['data']['gasLimit'])
29+
->pay('0x8233F6Df6449D7655f4643D2E752DC8D2283fAd5', '1000000000000000000')
30+
->pay('0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22', '2000000000000000000')
31+
->sign($this->passphrase);
32+
33+
$this->assertSame($fixture['serialized'], $builder->transaction->serialize()->getHex());
34+
35+
$this->assertSame($fixture['data']['id'], $builder->transaction->data['id']);
36+
37+
$this->assertTrue($builder->verify());
38+
}
39+
40+
/** @test */
41+
public function it_should_handle_single_recipient()
42+
{
43+
$fixture = $this->getTransactionFixture('evm_call', 'multipayment-1');
44+
45+
$builder = MultipaymentBuilder::new()
46+
->gasPrice($fixture['data']['gasPrice'])
47+
->nonce($fixture['data']['nonce'])
48+
->network($fixture['data']['network'])
49+
->gasLimit($fixture['data']['gasLimit'])
50+
->pay('0x8233F6Df6449D7655f4643D2E752DC8D2283fAd5', '1000000000000000000')
51+
->sign($this->passphrase);
52+
53+
$this->assertSame($fixture['serialized'], $builder->transaction->serialize()->getHex());
54+
55+
$this->assertSame($fixture['data']['id'], $builder->transaction->data['id']);
56+
57+
$this->assertTrue($builder->verify());
58+
}
59+
60+
/** @test */
61+
public function it_should_handle_empty_payment()
62+
{
63+
$fixture = $this->getTransactionFixture('evm_call', 'multipayment-0');
64+
65+
$builder = MultipaymentBuilder::new()
66+
->gasPrice($fixture['data']['gasPrice'])
67+
->nonce($fixture['data']['nonce'])
68+
->network($fixture['data']['network'])
69+
->gasLimit($fixture['data']['gasLimit'])
70+
->sign($this->passphrase);
71+
72+
$this->assertSame($fixture['serialized'], $builder->transaction->serialize()->getHex());
73+
74+
$this->assertSame($fixture['data']['id'], $builder->transaction->data['id']);
75+
76+
$this->assertTrue($builder->verify());
77+
}
78+
79+
// TODO: fix decoder issue first
80+
// /** @test */
81+
// public function it_should_be_possible_to_create_manual_multipayment()
82+
// {
83+
// $fixture = $this->getTransactionFixture('evm_call', 'multipayment-1');
84+
85+
// $payload = (new AbiEncoder(ContractAbiType::MULTIPAYMENT))->encodeFunctionCall('pay', [['0x8233F6Df6449D7655f4643D2E752DC8D2283fAd5'], ['1000000000000000000']]);
86+
// $tx = (new Multipayment(['data' => $payload]));
87+
// $tx->data['nonce'] = $fixture['data']['nonce'];
88+
// $tx->data['network'] = $fixture['data']['network'];
89+
// $tx->data['gasLimit'] = $fixture['data']['gasLimit'];
90+
// $tx->data['gasPrice'] = $fixture['data']['gasPrice'];
91+
// $tx->sign(PrivateKey::fromPassphrase($this->passphrase));
92+
93+
// $this->assertTrue($tx->verify());
94+
// }
95+
}

0 commit comments

Comments
 (0)