diff --git a/CHANGELOG.md b/CHANGELOG.md index cde770d..d39a43f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ CHANGELOG ========= +1.15.0 +----- + +* Added new method `Net::queryTransactionTree` +* Added new integration and unit tests + 1.14.0 ----- diff --git a/src/Entity/AbstractData.php b/src/Entity/AbstractData.php new file mode 100644 index 0000000..f93d43f --- /dev/null +++ b/src/Entity/AbstractData.php @@ -0,0 +1,174 @@ + */ + private array $data; + + /** + * @param array $data + */ + public function __construct(array $data) + { + $this->data = $data; + } + + /** + * @param string ...$keys + * @return array|mixed|null + */ + protected function getOriginData(string ...$keys) + { + $result = $this->data; + while ($key = array_shift($keys)) { + if (!is_array($result) || !isset($result[$key])) { + return null; + } + + $result = $result[$key]; + } + + return $result; + } + + /** + * @param string ...$keys + * @return array|null + */ + protected function getArray(string ...$keys): ?array + { + $result = $this->getOriginData(...$keys); + + if (!is_array($result)) { + return null; + } + + return $result; + } + + /** + * @param string ...$keys + * @return string|null + */ + protected function getString(string ...$keys): ?string + { + $result = $this->getOriginData(...$keys); + + if (!is_string($result)) { + return null; + } + + return $result; + } + + /** + * @param string ...$keys + * @return int|null + */ + protected function getInt(string ...$keys): ?int + { + $result = $this->getOriginData(...$keys); + + if (!is_int($result)) { + return null; + } + + return $result; + } + + /** + * @param string ...$keys + * @return mixed + * @throws DataException + */ + protected function requireData(string ...$keys) + { + $result = $this->getOriginData(...$keys); + + if ($result === null) { + $path = implode('.', $keys); + + throw new DataException(sprintf('Data not found by key %s.', $path)); + } + + return $result; + } + + /** + * @param string ...$keys + * @return array + * @throws DataException + */ + protected function requireArray(string ...$keys): array + { + $result = $this->requireData(...$keys); + + if (!is_array($result)) { + $path = implode('.', $keys); + + throw new DataException(sprintf('Data is corrupted by key %s.', $path)); + } + + return $result; + } + + /** + * @param string ...$keys + * @return string + * @throws DataException + */ + protected function requireString(string ...$keys): string + { + $result = $this->requireData(...$keys); + + if (!is_string($result)) { + $path = implode('.', $keys); + + throw new DataException(sprintf('Data is corrupted by key %s.', $path)); + } + + return $result; + } + + /** + * @param string ...$keys + * @return int + * @throws DataException + */ + protected function requireInt(string ...$keys): int + { + $result = $this->requireData(...$keys); + + if (!is_int($result)) { + $path = implode('.', $keys); + + throw new DataException(sprintf('Data is corrupted by key %s.', $path)); + } + + return $result; + } + + /** + * @param string ...$keys + * @return bool + * @throws DataException + */ + protected function requireBool(string ...$keys): bool + { + $result = $this->requireData(...$keys); + + if (!is_bool($result)) { + $path = implode('.', $keys); + + throw new DataException(sprintf('Data is corrupted by key %s.', $path)); + } + + return $result; + } +} \ No newline at end of file diff --git a/src/Entity/Net/AbstractQuery.php b/src/Entity/Net/AbstractQuery.php index 30b8dc9..3a6ac28 100644 --- a/src/Entity/Net/AbstractQuery.php +++ b/src/Entity/Net/AbstractQuery.php @@ -100,20 +100,11 @@ public function getCollection(): string */ public function getResult(): string { - $fields = array_merge( - ...array_map( - static fn ($resultField): array => explode(' ', $resultField), - $this->resultFields - ) - ); - - $fields = array_unique(array_filter(array_map('trim', $fields))); - - if (empty($fields)) { + if (empty($this->resultFields)) { throw new LogicException('Result fields cannot be empty'); } - return implode(' ', $fields); + return implode(' ', $this->resultFields); } /** diff --git a/src/Entity/Net/MessageNode.php b/src/Entity/Net/MessageNode.php new file mode 100644 index 0000000..7a621d4 --- /dev/null +++ b/src/Entity/Net/MessageNode.php @@ -0,0 +1,116 @@ +> $list + * @return array + */ + public static function createCollection(array $list): array + { + return array_map( + fn ($data): self => new self($data), + $list + ); + } + + /** + * Get message id + * + * @return string + */ + public function getId(): string + { + return $this->requireString('id'); + } + + /** + * Source transaction id. This field is missing for an external inbound messages. + * + * @return string|null + */ + public function getSrcTransactionId(): ?string + { + return $this->getString('src_transaction_id'); + } + + /** + * Destination transaction id. This field is missing for an external outbound messages. + * + * @return string|null + */ + public function getDstTransactionId(): ?string + { + return $this->getString('dst_transaction_id'); + } + + /** + * Get source address + * + * @return string|null + */ + public function getSrcAddress(): ?string + { + return $this->getString('src'); + } + + /** + * Destination address + * + * @return string|null + */ + public function getDstAddress(): ?string + { + return $this->getString('dst'); + } + + /** + * Get transferred tokens value + * + * @return string|null + */ + public function getValue(): ?string + { + return $this->getString('value'); + } + + /** + * Get bounce flag + * + * @return bool|null + */ + public function getBounce(): ?bool + { + return $this->requireBool('bounce'); + } + + /** + * Get decoded body. + * Library tries to decode message body using provided params.abi_registry. + * This field will be missing if none of the provided abi can be used to decode. + * + * @return DecodedMessageBody|null + */ + public function getDecodedBody(): ?DecodedMessageBody + { + $data = $this->getArray('decoded_body'); + if ($data === null) { + return null; + } + + return new DecodedMessageBody(new Response($data)); + } +} \ No newline at end of file diff --git a/src/Entity/Net/ResultOfQueryTransactionTree.php b/src/Entity/Net/ResultOfQueryTransactionTree.php new file mode 100644 index 0000000..4438cde --- /dev/null +++ b/src/Entity/Net/ResultOfQueryTransactionTree.php @@ -0,0 +1,33 @@ + + */ + public function getMessages(): array + { + return MessageNode::createCollection($this->requireArray('messages')); + } + + /** + * Get transactions + * + * @return array + */ + public function getTransactions(): array + { + return TransactionNode::createCollection($this->requireArray('transactions')); + } +} diff --git a/src/Entity/Net/TransactionNode.php b/src/Entity/Net/TransactionNode.php new file mode 100644 index 0000000..8ab8350 --- /dev/null +++ b/src/Entity/Net/TransactionNode.php @@ -0,0 +1,97 @@ +> $list + * @return array + */ + public static function createCollection(array $list): array + { + return array_map( + fn ($data): self => new self($data), + $list + ); + } + + /** + * Get transaction id + * + * @return string + */ + public function getId(): string + { + return $this->requireString('id'); + } + + /** + * Get in message id + * + * @return string + */ + public function getInMessage(): string + { + return $this->requireString('in_msg'); + } + + /** + * Get out message ids + * + * @return array + */ + public function getOutMessages(): array + { + return $this->requireArray('out_msgs'); + } + + /** + * Get account address + * + * @return string + */ + public function getAccountAddress(): string + { + return $this->requireString('account_addr'); + } + + /** + * Get transactions total fees + * + * @return string + */ + public function getTotalFees(): string + { + return $this->requireString('total_fees'); + } + + /** + * Get aborted flag + * + * @return bool + */ + public function getAborted(): bool + { + return $this->requireBool('aborted'); + } + + /** + * Get compute phase exit code + * + * @return int|null + */ + public function getExitCode(): ?int + { + return $this->getInt('exit_code'); + } +} diff --git a/src/Net.php b/src/Net.php index 8cd8466..e6070e5 100644 --- a/src/Net.php +++ b/src/Net.php @@ -4,6 +4,7 @@ namespace Extraton\TonClient; +use Extraton\TonClient\Entity\Abi\AbiType; use Extraton\TonClient\Entity\AbstractResult; use Extraton\TonClient\Entity\Net\EndpointsSet; use Extraton\TonClient\Entity\Net\ParamsOfAggregateCollection; @@ -18,6 +19,7 @@ use Extraton\TonClient\Entity\Net\ResultOfQuery; use Extraton\TonClient\Entity\Net\ResultOfQueryCollection; use Extraton\TonClient\Entity\Net\ResultOfQueryCounterparties; +use Extraton\TonClient\Entity\Net\ResultOfQueryTransactionTree; use Extraton\TonClient\Entity\Net\ResultOfSubscribeCollection; use Extraton\TonClient\Entity\Net\ResultOfWaitForCollection; use Extraton\TonClient\Exception\TonException; @@ -275,4 +277,29 @@ public function queryCounterparties( )->wait() ); } + + /** + * Returns transactions tree for specific message. + * Performs recursive retrieval of the transactions tree produced by the specific message: + * in_msg -> dst_transaction -> out_messages -> dst_transaction -> ... + * All retrieved messages and transactions will be included + * into result.messages and result.transactions respectively. + * + * @param string $inMsg + * @param AbiType[]|null $abiRegistry + * @return ResultOfQueryTransactionTree + * @throws TonException + */ + public function queryTransactionTree(string $inMsg, ?array $abiRegistry = null): ResultOfQueryTransactionTree + { + return new ResultOfQueryTransactionTree( + $this->tonClient->request( + 'net.query_transaction_tree', + [ + 'in_msg' => $inMsg, + 'abi_registry' => $abiRegistry, + ] + )->wait() + ); + } } diff --git a/tests/Integration/NetTest.php b/tests/Integration/NetTest.php index b91ce43..3dc41aa 100644 --- a/tests/Integration/NetTest.php +++ b/tests/Integration/NetTest.php @@ -4,8 +4,11 @@ namespace Extraton\Tests\Integration\TonClient; +use Extraton\TonClient\Abi; +use Extraton\TonClient\Entity\Abi\AbiType; use Extraton\TonClient\Entity\Net\Aggregation; use Extraton\TonClient\Entity\Net\Filters; +use Extraton\TonClient\Entity\Net\MessageNode; use Extraton\TonClient\Entity\Net\OrderBy; use Extraton\TonClient\Entity\Net\ParamsOfAggregateCollection; use Extraton\TonClient\Entity\Net\ParamsOfBatchQuery; @@ -14,7 +17,9 @@ use Extraton\TonClient\Entity\Net\ParamsOfWaitForCollection; use Extraton\TonClient\Entity\Net\ResultOfQueryCollection; use Extraton\TonClient\Entity\Net\ResultOfQueryCounterparties; +use Extraton\TonClient\Entity\Net\ResultOfQueryTransactionTree; use Extraton\TonClient\Entity\Net\ResultOfWaitForCollection; +use Extraton\TonClient\Entity\Net\TransactionNode; use Extraton\TonClient\Handler\Response; use function dechex; @@ -446,4 +451,63 @@ public function testQueryCounterparties(): void self::assertEquals($expected, $resultOfQueryCounterparties); } + + /** + * @covers ::queryTransactionTree + */ + public function testQueryTransactionTree(): void + { + $filters = new Filters(); + $filters->add('msg_type', Filters::EQ, 1); + + $query = new ParamsOfQueryCollection( + 'messages', + [], + $filters + ); + + $query->addResultField( + <<net->queryCollection($query); + + $abiRegistry = [ + AbiType::fromArray($this->dataProvider->getHelloAbiArray()) + ]; + + foreach ($resultOfQueryCollection->getResult() as $message) { + $messageId = $message['id'] ?? null; + if ($messageId === null) { + continue; + } + + $resultOfQueryTransactionTree = $this->net->queryTransactionTree( + $messageId, + $abiRegistry + ); + + self::assertContainsOnlyInstancesOf( + MessageNode::class, + $resultOfQueryTransactionTree->getMessages() + ); + + self::assertContainsOnlyInstancesOf( + TransactionNode::class, + $resultOfQueryTransactionTree->getTransactions() + ); + } + } } diff --git a/tests/Unit/NetTest.php b/tests/Unit/NetTest.php index 21969e2..053209d 100644 --- a/tests/Unit/NetTest.php +++ b/tests/Unit/NetTest.php @@ -17,6 +17,7 @@ use Extraton\TonClient\Entity\Net\ResultOfQuery; use Extraton\TonClient\Entity\Net\ResultOfQueryCollection; use Extraton\TonClient\Entity\Net\ResultOfQueryCounterparties; +use Extraton\TonClient\Entity\Net\ResultOfQueryTransactionTree; use Extraton\TonClient\Entity\Net\ResultOfSubscribeCollection; use Extraton\TonClient\Entity\Net\ResultOfWaitForCollection; use Extraton\TonClient\Handler\Response; @@ -550,4 +551,46 @@ public function testQueryCounterparties(): void $this->net->queryCounterparties($account, $result, $first, $after) ); } + + /** + * @covers ::queryTransactionTree + */ + public function testQueryTransactionTree(): void + { + $inMsg = uniqid(microtime(), true); + $abiRegistry = [ + uniqid(microtime(), true), + uniqid(microtime(), true), + uniqid(microtime(), true), + ]; + + $response = new Response( + [ + uniqid(microtime(), true) + ] + ); + + $this->mockPromise->expects(self::once()) + ->method('wait') + ->with() + ->willReturn($response); + + $this->mockTonClient->expects(self::once()) + ->method('request') + ->with( + 'net.query_transaction_tree', + [ + 'in_msg' => $inMsg, + 'abi_registry' => $abiRegistry, + ] + ) + ->willReturn($this->mockPromise); + + $expected = new ResultOfQueryTransactionTree($response); + + self::assertEquals( + $expected, + $this->net->queryTransactionTree($inMsg, $abiRegistry) + ); + } }