From 690a0fc7cbf79ec0aa38e29b6d4036f7ad86782d Mon Sep 17 00:00:00 2001 From: Edouard Courty Date: Wed, 8 Jan 2025 13:06:53 +0100 Subject: [PATCH] feat(transactions): add TransactionClient to support transaction methods --- src/Client/JsonRpcClient.php | 8 +- src/Client/SubClient/AbstractClient.php | 17 +++ src/Client/SubClient/AccountClient.php | 29 +--- src/Client/SubClient/LedgerClient.php | 29 +--- src/Client/SubClient/PaymentChannelClient.php | 10 +- src/Client/SubClient/ServerInfoClient.php | 10 +- src/Client/SubClient/TransactionClient.php | 129 ++++++++++++++++++ src/Client/SubClient/UtilityClient.php | 10 +- src/Client/XRPLClient.php | 7 +- src/Model/Account/AccountTransactions.php | 7 +- .../Account/Nested/AccountTransaction.php | 16 +++ src/Model/Account/NoRippleCheck.php | 3 - src/Model/Ledger/Nested/Ledger.php | 1 - .../MultiSignedSubmittedTransaction.php | 16 +++ .../Transaction/SubmittedTransaction.php | 25 ++++ src/Model/Transaction/TransactionEntry.php | 21 +++ src/Model/Transaction/TransactionMetadata.php | 19 +++ .../Denormalizer/TransactionDenormalizer.php | 49 +++---- src/Service/Serializer.php | 2 + 19 files changed, 285 insertions(+), 123 deletions(-) create mode 100644 src/Client/SubClient/AbstractClient.php create mode 100644 src/Client/SubClient/TransactionClient.php create mode 100644 src/Model/Account/Nested/AccountTransaction.php create mode 100644 src/Model/Transaction/MultiSignedSubmittedTransaction.php create mode 100644 src/Model/Transaction/SubmittedTransaction.php create mode 100644 src/Model/Transaction/TransactionEntry.php create mode 100644 src/Model/Transaction/TransactionMetadata.php diff --git a/src/Client/JsonRpcClient.php b/src/Client/JsonRpcClient.php index 97bd94c..4bd9485 100644 --- a/src/Client/JsonRpcClient.php +++ b/src/Client/JsonRpcClient.php @@ -47,13 +47,6 @@ public function getResult(string $method, array $params = [], int $timeout = sel return $response->result; } - public function getJson(string $method, array $params = [], int $timeout = self::DEFAULT_TIMEOUT): array - { - $response = $this->getResult($method, $params, $timeout); - - return json_decode(json_encode($response), true); - } - public function execute(JsonRpcRequest $request, int $timeout): RippledResponse { $response = $this->httpClient->request('POST', '/', [ @@ -71,6 +64,7 @@ public function execute(JsonRpcRequest $request, int $timeout): RippledResponse if (isset($content['error'])) { $error = $content['error']; + throw new JsonRpcException("RPC Error {$error['code']}: {$error['message']}", $response); } diff --git a/src/Client/SubClient/AbstractClient.php b/src/Client/SubClient/AbstractClient.php new file mode 100644 index 0000000..f8eda3c --- /dev/null +++ b/src/Client/SubClient/AbstractClient.php @@ -0,0 +1,17 @@ +transactionDenormalizer = new TransactionDenormalizer($this->serializer); - } - public function getAccountChannels( string $address, ?string $destinationAccount = null, @@ -186,8 +172,7 @@ public function getAccountTransactions( bool $forward = false, ?int $limit = null, mixed $marker = null, - ): AccountTransactions - { + ): AccountTransactions { $providedParams = array_filter([ 'ledger_index_min' => $ledgerIndexMin, 'ledger_index_max' => $ledgerIndexMax, @@ -222,10 +207,7 @@ public function getAccountTransactions( $response = $this->jsonRpcClient->getResult('account_tx', $params); - $accountTransaction = $this->serializer->deserialize(json_encode($response), AccountTransactions::class, 'json'); - $accountTransaction->transactions = $this->transactionDenormalizer->deserializeListForAccount($response->transactions); - - return $accountTransaction; + return $this->serializer->deserialize(json_encode($response), AccountTransactions::class, 'json'); } public function getGatewayBalances( @@ -271,9 +253,6 @@ public function getNoRippleCheck( $response = $this->jsonRpcClient->getResult('noripple_check', $params); - $noRippleCheck = $this->serializer->deserialize(json_encode($response), NoRippleCheck::class, 'json'); - $noRippleCheck->transactions = $this->transactionDenormalizer->deserializeList($response->transactions ?? []); - - return $noRippleCheck; + return $this->serializer->deserialize(json_encode($response), NoRippleCheck::class, 'json'); } } diff --git a/src/Client/SubClient/LedgerClient.php b/src/Client/SubClient/LedgerClient.php index 437694f..73634ea 100644 --- a/src/Client/SubClient/LedgerClient.php +++ b/src/Client/SubClient/LedgerClient.php @@ -4,27 +4,15 @@ namespace XRPL\Client\SubClient; -use XRPL\Client\JsonRpcClient; use XRPL\Enum\LedgerEntryEnum; use XRPL\Model\Ledger\LedgerClosed; use XRPL\Model\Ledger\LedgerCurrent; use XRPL\Model\Ledger\LedgerData; use XRPL\Model\Ledger\LedgerEntry; use XRPL\Model\Ledger\LedgerResult; -use XRPL\Service\Denormalizer\TransactionDenormalizer; -use XRPL\Service\Serializer; -readonly class LedgerClient +readonly class LedgerClient extends AbstractClient { - private TransactionDenormalizer $transactionDeserializer; - - public function __construct( - private Serializer $serializer, - private JsonRpcClient $jsonRpcClient, - ) { - $this->transactionDeserializer = new TransactionDenormalizer($this->serializer); - } - /** * @note If `transactions` is set to true but `expand` is set to false, only the `transactionIds` field will be populated. */ @@ -45,20 +33,17 @@ public function getLedger( 'ledger_index' => $ledgerIndex, ]; - $result = $this->jsonRpcClient->getResult('ledger', $payload); + $response = $this->jsonRpcClient->getResult('ledger', $payload); - /** @var LedgerResult $ledgerResponse */ - $ledgerResponse = $this->serializer->deserialize(json_encode($result), LedgerResult::class, 'json'); + /** @var LedgerResult $ledgerResult */ + $ledgerResult = $this->serializer->deserialize(json_encode($response), LedgerResult::class, 'json'); if ($transactions === true && $expand === false) { - $ledgerResponse->ledger->transactionIds = $result->ledger->transactions; - } - - if ($transactions === true && $expand === true) { - $ledgerResponse->ledger->transactions = $this->transactionDeserializer->deserializeList($result->ledger->transactions); + $ledgerResult->ledger->transactionIds = $response['ledger']['transactions']; + $ledgerResult->ledger->transactions = []; } - return $ledgerResponse; + return $ledgerResult; } public function getLedgerClosed(): LedgerClosed diff --git a/src/Client/SubClient/PaymentChannelClient.php b/src/Client/SubClient/PaymentChannelClient.php index 39612bf..67d7645 100644 --- a/src/Client/SubClient/PaymentChannelClient.php +++ b/src/Client/SubClient/PaymentChannelClient.php @@ -4,19 +4,11 @@ namespace XRPL\Client\SubClient; -use XRPL\Client\JsonRpcClient; use XRPL\Model\PaymentChannel\ChannelAuthorize; use XRPL\Model\PaymentChannel\ChannelVerify; -use XRPL\Service\Serializer; -readonly class PaymentChannelClient +readonly class PaymentChannelClient extends AbstractClient { - public function __construct( - private Serializer $serializer, - private JsonRpcClient $jsonRpcClient, - ) { - } - public function authorizeChannel( string $channelId, string $amount, diff --git a/src/Client/SubClient/ServerInfoClient.php b/src/Client/SubClient/ServerInfoClient.php index fde6e07..7ce5446 100644 --- a/src/Client/SubClient/ServerInfoClient.php +++ b/src/Client/SubClient/ServerInfoClient.php @@ -4,22 +4,14 @@ namespace XRPL\Client\SubClient; -use XRPL\Client\JsonRpcClient; use XRPL\Model\ServerInfo\Fee; use XRPL\Model\ServerInfo\Manifest; use XRPL\Model\ServerInfo\ServerDefinitions; use XRPL\Model\ServerInfo\ServerState; use XRPL\Model\ServerInfo\VersionResults; -use XRPL\Service\Serializer; -readonly class ServerInfoClient +readonly class ServerInfoClient extends AbstractClient { - public function __construct( - private Serializer $serializer, - private JsonRpcClient $jsonRpcClient, - ) { - } - public function getFee(): Fee { $response = $this->jsonRpcClient->getResult('fee'); diff --git a/src/Client/SubClient/TransactionClient.php b/src/Client/SubClient/TransactionClient.php new file mode 100644 index 0000000..703fa79 --- /dev/null +++ b/src/Client/SubClient/TransactionClient.php @@ -0,0 +1,129 @@ + $transactionBlob, + 'fail_hard' => $failHard, + ]; + + $response = $this->jsonRpcClient->getResult('submit', $params); + + return $this->serializer->deserialize(json_encode($response), SubmittedTransaction::class, 'json'); + } + + public function signAndSubmit( + array $txJson, + ?string $secret = null, + ?string $seed = null, + ?string $seedHex = null, + ?string $passphrase = null, + ?string $keyType = null, + bool $failHard = false, + bool $offline = false, + ?bool $buildPath = null, + ?int $feeMultMax = null, + ?int $feeDivMax = null, + ): SubmittedTransaction { + $params = [ + 'tx_json' => $txJson, + 'secret' => $secret, + 'seed' => $seed, + 'seed_hex' => $seedHex, + 'passphrase' => $passphrase, + 'key_type' => $keyType, + 'fail_hard' => $failHard, + 'offline' => $offline, + 'build_path' => $buildPath, + 'fee_mult_max' => $feeMultMax, + 'fee_div_max' => $feeDivMax, + ]; + + $response = $this->jsonRpcClient->getResult('submit', $params); + + return $this->serializer->deserialize(json_encode($response), SubmittedTransaction::class, 'json'); + } + + public function submitMultiSigned( + array $transaction, + bool $failHard = false, + ): MultiSignedSubmittedTransaction { + $params = [ + 'tx_json' => $transaction, + 'fail_hard' => $failHard, + ]; + + $response = $this->jsonRpcClient->getResult('submit_multisigned', $params); + + return $this->serializer->deserialize(json_encode($response), MultiSignedSubmittedTransaction::class, 'json'); + } + + public function getTransactionAtLedger( + string $transactionHash, + ?string $ledgerHash = null, + string|int|null $ledgerIndex = null, + ): TransactionEntry { + if ($ledgerHash === null && $ledgerIndex === null) { + throw new \InvalidArgumentException('Either ledgerHash or ledgerIndex must be provided.'); + } + + if ($ledgerHash !== null && $ledgerIndex !== null) { + throw new \InvalidArgumentException('Only one of ledgerHash or ledgerIndex can be provided.'); + } + + $params = [ + 'tx_hash' => $transactionHash, + 'ledger_hash' => $ledgerHash, + 'ledger_index' => $ledgerIndex, + ]; + + $response = $this->jsonRpcClient->getResult('transaction_entry', $params); + + $transactionEntry = $this->serializer->deserialize(json_encode($response), TransactionEntry::class, 'json'); + + if ($transactionEntry->transaction === null) { + throw new \RuntimeException('Transaction not found.'); + } + + return $transactionEntry; + } + + public function getTransaction( + ?string $transactionHash = null, + ?string $compactTransactionId = null, + ?int $minLedger = null, + ?int $maxLedger = null, + ): AbstractTransaction { + if (null === $transactionHash && null === $compactTransactionId) { + throw new \InvalidArgumentException('Either transactionHash or compactTransactionId must be provided.'); + } + + if (null !== $transactionHash && null !== $compactTransactionId) { + throw new \InvalidArgumentException('Only one of transactionHash or compactTransactionId can be provided.'); + } + + $params = [ + 'transaction' => $transactionHash, + 'ctid' => $compactTransactionId, + 'min_ledger' => $minLedger, + 'max_ledger' => $maxLedger, + ]; + + $response = $this->jsonRpcClient->getResult('tx', $params); + + return $this->serializer->deserialize(json_encode($response), AbstractTransaction::class, 'json'); + } +} diff --git a/src/Client/SubClient/UtilityClient.php b/src/Client/SubClient/UtilityClient.php index 625c500..fd234fb 100644 --- a/src/Client/SubClient/UtilityClient.php +++ b/src/Client/SubClient/UtilityClient.php @@ -4,19 +4,11 @@ namespace XRPL\Client\SubClient; -use XRPL\Client\JsonRpcClient; use XRPL\Model\Utility\Ping; use XRPL\Model\Utility\Random; -use XRPL\Service\Serializer; -readonly class UtilityClient +readonly class UtilityClient extends AbstractClient { - public function __construct( - private Serializer $serializer, - private JsonRpcClient $jsonRpcClient, - ) { - } - public function ping(): Ping { $response = $this->jsonRpcClient->getResult('ping'); diff --git a/src/Client/XRPLClient.php b/src/Client/XRPLClient.php index bfb2fc2..e14598e 100644 --- a/src/Client/XRPLClient.php +++ b/src/Client/XRPLClient.php @@ -8,6 +8,7 @@ use XRPL\Client\SubClient\LedgerClient; use XRPL\Client\SubClient\PaymentChannelClient; use XRPL\Client\SubClient\ServerInfoClient; +use XRPL\Client\SubClient\TransactionClient; use XRPL\Client\SubClient\UtilityClient; use XRPL\Service\Serializer; @@ -21,7 +22,8 @@ public AccountClient $account; public LedgerClient $ledger; - public PaymentChannelClient $paymentChannelClient; + public TransactionClient $transaction; + public PaymentChannelClient $paymentChannels; public ServerInfoClient $serverInfo; public UtilityClient $utility; @@ -33,7 +35,8 @@ public function __construct( $this->account = new AccountClient($this->serializer, $this->jsonRpcClient); $this->ledger = new LedgerClient($this->serializer, $this->jsonRpcClient); - $this->paymentChannelClient = new PaymentChannelClient($this->serializer, $this->jsonRpcClient); + $this->transaction = new TransactionClient($this->serializer, $this->jsonRpcClient); + $this->paymentChannels = new PaymentChannelClient($this->serializer, $this->jsonRpcClient); $this->serverInfo = new ServerInfoClient($this->serializer, $this->jsonRpcClient); $this->utility = new UtilityClient($this->serializer, $this->jsonRpcClient); } diff --git a/src/Model/Account/AccountTransactions.php b/src/Model/Account/AccountTransactions.php index cefbf5d..f0b38d8 100644 --- a/src/Model/Account/AccountTransactions.php +++ b/src/Model/Account/AccountTransactions.php @@ -4,9 +4,8 @@ namespace XRPL\Model\Account; -use Symfony\Component\Serializer\Attribute\Ignore; use XRPL\Model\AbstractResult; -use XRPL\Model\AbstractTransaction; +use XRPL\Model\Account\Nested\AccountTransaction; class AccountTransactions extends AbstractResult { @@ -16,8 +15,6 @@ class AccountTransactions extends AbstractResult public ?int $limit = null; public mixed $marker = null; public ?bool $validated = null; - - /** @var AbstractTransaction[] $transactions */ - #[Ignore] + /** @var AccountTransaction[] $transactions */ public array $transactions = []; } diff --git a/src/Model/Account/Nested/AccountTransaction.php b/src/Model/Account/Nested/AccountTransaction.php new file mode 100644 index 0000000..d09d7cd --- /dev/null +++ b/src/Model/Account/Nested/AccountTransaction.php @@ -0,0 +1,16 @@ +serializer->deserialize(json_encode($data), $transactionType->getClass(), 'json'); } - /** - * @param \stdClass[] $data - * @return AbstractTransaction[] - */ - public function deserializeList(array $data): array + public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool { - if (empty($data)) { - return []; - } - - if ($data[0] instanceOf \stdClass) { - $data = json_decode(json_encode($data), true); - } - - return array_map(fn(array $transaction) => $this->denormalize($transaction), $data); + return $type === AbstractTransaction::class; } - /** - * @param \stdClass[] $data - * @return AbstractTransaction[] - */ - public function deserializeListForAccount(array $data): array + public function getSupportedTypes(?string $format): array { - if (empty($data)) { - return []; - } - - if ($data[0] instanceOf \stdClass) { - $data = json_decode(json_encode($data), true); - } - - return array_map(fn(array $transaction) => $this->denormalize($transaction['tx']), $data); + return [ + AbstractTransaction::class => true, + ]; } } diff --git a/src/Service/Serializer.php b/src/Service/Serializer.php index 3221d72..6bd61b5 100644 --- a/src/Service/Serializer.php +++ b/src/Service/Serializer.php @@ -21,6 +21,7 @@ use XRPL\Service\Denormalizer\NFTokenObjectDenormalizer; use XRPL\Service\Denormalizer\NFTokenPageDenormalizer; use XRPL\Service\Denormalizer\ServerDefinitionsDenormalizer; +use XRPL\Service\Denormalizer\TransactionDenormalizer; class Serializer extends \Symfony\Component\Serializer\Serializer { @@ -41,6 +42,7 @@ public function __construct() new CurrencyAmountDenormalizer(), new LedgerEntryDenormalizer($this), new NFTokenPageDenormalizer($this), + new TransactionDenormalizer($this), new ServerDefinitionsDenormalizer(), new NFTokenObjectDenormalizer(), new NFTokenDenormalizer(),