diff --git a/src/Http/Entities/Account.php b/src/Http/Entities/Account.php index 6aa64ed..874e5a5 100644 --- a/src/Http/Entities/Account.php +++ b/src/Http/Entities/Account.php @@ -14,8 +14,8 @@ public function __construct( public Address $address, public int $nonce, public BigInteger $balance, - public int $txCount, public int $shard, + public ?int $txCount = null, public ?string $username = null, public ?string $rootHash = null, ) { diff --git a/src/Transaction.php b/src/Transaction.php index 0c6dd34..c20723d 100644 --- a/src/Transaction.php +++ b/src/Transaction.php @@ -12,6 +12,8 @@ class Transaction implements ISignable const OPTIONS_DEFAULT = 0; public ?Signature $signature = null; + public ?Signature $guardianSignature = null; + public ?Signature $relayerSignature = null; public function __construct( public int $nonce, @@ -24,6 +26,10 @@ public function __construct( public string $chainID = '1', public int $version = self::VERSION_DEFAULT, public int $options = self::OPTIONS_DEFAULT, + public string $senderUsername = '', + public string $receiverUsername = '', + public ?Address $guardian = null, + public ?Address $relayer = null, ) { } @@ -33,7 +39,7 @@ public function serializeForSigning(): string ->reject(fn ($field) => $field === null) ->toArray(); - unset($plain['signature']); + unset($plain['signature'], $plain['guardianSignature'], $plain['relayerSignature']); return bin2hex(json_encode($plain)); } @@ -45,13 +51,19 @@ public function toArray(): array 'value' => (string) $this->value, 'receiver' => $this->receiver->bech32(), 'sender' => $this->sender->bech32(), + 'senderUsername' => $this->senderUsername ? base64_encode($this->senderUsername) : null, + 'receiverUsername' => $this->receiverUsername ? base64_encode($this->receiverUsername) : null, 'gasPrice' => $this->gasPrice, 'gasLimit' => $this->gasLimit, 'data' => $this->data?->toBase64(), 'chainID' => $this->chainID, 'version' => $this->version, 'options' => $this->options === 0 ? null : $this->options, + 'guardian' => $this->guardian?->bech32(), + 'relayer' => $this->relayer?->bech32(), 'signature' => $this->signature?->hex(), + 'guardianSignature' => $this->guardianSignature?->hex(), + 'relayerSignature' => $this->relayerSignature?->hex(), ]; } @@ -60,8 +72,90 @@ public function applySignature(Signature $signature): void $this->signature = $signature; } + public function applyGuardianSignature(Signature $signature): void + { + $this->guardianSignature = $signature; + } + + public function applyRelayerSignature(Signature $signature): void + { + $this->relayerSignature = $signature; + } + public function toSendable(): array { return $this->toArray(); } + + public function isGuardedTransaction(): bool + { + $hasGuardian = $this->guardian !== null && !$this->guardian->isZero(); + $hasGuardianSignature = $this->guardianSignature !== null; + + return $this->hasOptionsSetForGuardedTransaction() && $hasGuardian && $hasGuardianSignature; + } + + private function hasOptionsSetForGuardedTransaction(): bool + { + return ($this->options & 0b0001) !== 0; + } + + public static function fromArray(array $data): self + { + $senderUsername = ''; + if (!empty($data['senderUsername'])) { + $senderUsername = base64_decode($data['senderUsername']); + } + + $receiverUsername = ''; + if (!empty($data['receiverUsername'])) { + $receiverUsername = base64_decode($data['receiverUsername']); + } + + $guardian = null; + if (!empty($data['guardian'])) { + $guardian = Address::newFromBech32($data['guardian']); + } + + $relayer = null; + if (!empty($data['relayer'])) { + $relayer = Address::newFromBech32($data['relayer']); + } + + $dataPayload = null; + if (!empty($data['data'])) { + $dataPayload = new TransactionPayload(base64_decode($data['data'])); + } + + $transaction = new self( + nonce: $data['nonce'], + value: BigInteger::of($data['value'] ?? '0'), + sender: Address::newFromBech32($data['sender']), + receiver: Address::newFromBech32($data['receiver']), + gasPrice: $data['gasPrice'] ?? self::MIN_GAS_PRICE, + gasLimit: $data['gasLimit'], + data: $dataPayload, + chainID: $data['chainID'], + version: $data['version'] ?? self::VERSION_DEFAULT, + options: $data['options'] ?? self::OPTIONS_DEFAULT, + senderUsername: $senderUsername, + receiverUsername: $receiverUsername, + guardian: $guardian, + relayer: $relayer, + ); + + if (!empty($data['signature'])) { + $transaction->signature = new Signature($data['signature']); + } + + if (!empty($data['guardianSignature'])) { + $transaction->guardianSignature = new Signature($data['guardianSignature']); + } + + if (!empty($data['relayerSignature'])) { + $transaction->relayerSignature = new Signature($data['relayerSignature']); + } + + return $transaction; + } } diff --git a/tests/Http/Api/responses/accounts/account.json b/tests/Http/Api/responses/accounts/account.json index 8bbc2dc..37917b9 100644 --- a/tests/Http/Api/responses/accounts/account.json +++ b/tests/Http/Api/responses/accounts/account.json @@ -1,9 +1,8 @@ { - "address": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l", - "nonce": 404, - "balance": "46931489907311278839", - "rootHash": "lLXmK8ipiC9pEQaaaaaaaaaabbbbbbbbbbcccccccccc", - "txCount": 470, - "username": "aabbcc.elrond", - "shard": 1 + "address": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l", + "nonce": 404, + "balance": "46931489907311278839", + "rootHash": "lLXmK8ipiC9pEQaaaaaaaaaabbbbbbbbbbcccccccccc", + "username": "aabbcc.elrond", + "shard": 1 } diff --git a/tests/Http/__snapshots__/AccountsTest____pest_evaluable_it_getByAddress___gets_an_account_by_address__1.json b/tests/Http/__snapshots__/AccountsTest____pest_evaluable_it_getByAddress___gets_an_account_by_address__1.json index b2df386..d2eb20b 100644 --- a/tests/Http/__snapshots__/AccountsTest____pest_evaluable_it_getByAddress___gets_an_account_by_address__1.json +++ b/tests/Http/__snapshots__/AccountsTest____pest_evaluable_it_getByAddress___gets_an_account_by_address__1.json @@ -4,8 +4,8 @@ }, "nonce": 404, "balance": "46931489907311278839", - "txCount": 470, "shard": 1, + "txCount": null, "username": "aabbcc.elrond", "rootHash": "lLXmK8ipiC9pEQaaaaaaaaaabbbbbbbbbbcccccccccc", "rawResponse": { @@ -13,7 +13,6 @@ "nonce": 404, "balance": "46931489907311278839", "rootHash": "lLXmK8ipiC9pEQaaaaaaaaaabbbbbbbbbbcccccccccc", - "txCount": 470, "username": "aabbcc.elrond", "shard": 1 } diff --git a/tests/Unit/TransactionTest.php b/tests/Unit/TransactionTest.php index 8d2f3c8..b8aaf21 100644 --- a/tests/Unit/TransactionTest.php +++ b/tests/Unit/TransactionTest.php @@ -1,13 +1,15 @@ toBeNull(); expect($actual['signature'])->toBeNull(); }); + +it('should support guardian and relayer functionality', function () { + $guardian = Address::newFromBech32(EVE_ADDRESS); + $guardianSig = new Signature('guardian_sig'); + + $tx = new Transaction( + nonce: 90, + value: BigInteger::zero(), + sender: Address::newFromBech32(ALICE_ADDRESS), + receiver: Address::newFromBech32(BOB_ADDRESS), + gasPrice: MIN_GAS_PRICE, + gasLimit: MIN_GAS_LIMIT, + chainID: 'local-testnet', + options: 1, + guardian: $guardian + ); + + $tx->applyGuardianSignature($guardianSig); + + expect($tx->isGuardedTransaction())->toBeTrue(); + expect($tx->toArray()['guardian'])->toBe(EVE_ADDRESS); +}); + +it('should create transaction from array', function () { + $data = [ + 'nonce' => 90, + 'value' => '123456789000000000000000000000', + 'sender' => ALICE_ADDRESS, + 'receiver' => BOB_ADDRESS, + 'gasPrice' => MIN_GAS_PRICE, + 'gasLimit' => 80000, + 'data' => base64_encode('hello'), + 'chainID' => 'local-testnet', + 'senderUsername' => base64_encode('alice'), + 'guardian' => 'erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l', + 'signature' => 'test_signature' + ]; + + $tx = Transaction::fromArray($data); + + expect($tx->nonce)->toBe(90); + expect((string) $tx->value)->toBe('123456789000000000000000000000'); + expect($tx->sender->bech32())->toBe(ALICE_ADDRESS); + expect($tx->receiver->bech32())->toBe(BOB_ADDRESS); + expect($tx->data->data)->toBe('hello'); + expect($tx->senderUsername)->toBe('alice'); + expect($tx->guardian->bech32())->toBe('erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l'); + expect($tx->signature->hex())->toBe('test_signature'); +});