blockchainofthings / catenis-api-client
PHP 的 Catenis API 客户端库
Requires
- php: >=5.6.0
- ext-json: *
- clue/buzz-react: dev-decode-content
- evenement/evenement: ^3.0 || ^2.0
- guzzlehttp/guzzle: ^6.5.8
- ratchet/pawl: ^0.3.4
- wyrihaximus/react-guzzle-http-client: dev-decode-content
- wyrihaximus/react-guzzle-psr7: dev-decode-content
Requires (Dev)
- phpunit/php-invoker: ^2.0
- phpunit/phpunit: ^8
- react/event-loop: ^1.0
- squizlabs/php_codesniffer: 3.*
README
此库用于简化从 PHP 应用程序访问 Catenis API 服务。
当前版本(6.0.1)针对 Catenis API 的 0.12 版本。
安装
推荐使用 Composer 安装 Catenis API PHP 客户端。
要将 Catenis API 客户端作为依赖项添加到您的项目中,请按照以下步骤操作
- 将以下存储库条目添加到您的
composer.json
文件中
{ "repositories": [ { "type": "vcs", "url": "https://github.com/claudiosdc/react-guzzle-psr7.git" }, { "type": "vcs", "url": "https://github.com/claudiosdc/react-guzzle-http-client.git" }, { "type": "vcs", "url": "https://github.com/claudiosdc/reactphp-buzz.git" } ] }
- 然后,通过以下方式添加所需的依赖项
运行以下命令
composer require blockchainofthings/catenis-api-client:~3.0 wyrihaximus/react-guzzle-psr7:dev-decode-content wyrihaximus/react-guzzle-http-client:dev-decode-content clue/buzz-react:dev-decode-content
或直接编辑 composer.json
文件
{ "require": { "blockchainofthings/catenis-api-client": "~6.0", "wyrihaximus/react-guzzle-psr7": "dev-decode-content", "wyrihaximus/react-guzzle-http-client": "dev-decode-content", "clue/buzz-react": "dev-decode-content" } }
注意:通常,只需要列出 blockchainofthings/catenis-api-client 包。但是,由于此版本的 Catenis API 库依赖于其他三个包的特殊修补版本,因此它们也需要在这里列出。
用法
只需包含 Composer 的 vendor/autoload.php
文件,Catenis API 客户端组件即可在您的代码中使用。
require __DIR__ . 'vendor/autoload.php';
实例化客户端
$ctnApiClient = new \Catenis\ApiClient( $deviceId, $apiAccessSecret, [ 'environment' => 'sandbox' ] );
可选地,客户端可以不传递 $deviceId
和 $apiAccessSecret
参数或将它们设置为 null 来实例化,如下所示。在这种情况下,应仅使用生成的客户端对象调用 公共 API 方法。
$ctnApiClient = new \Catenis\ApiClient(null, null, [ 'environment' => 'sandbox' ] );
构造函数选项
在实例化客户端时可以使用以下选项
- host [字符串] - (可选,默认: 'catenis.io') 目标 Catenis API 服务器的主机名(可选端口号)。
- environment [字符串] - (可选,默认: 'prod') 目标 Catenis API 服务器环境。有效值:'prod'、'sandbox'。
- secure [布尔值] - (可选,默认: true) 指示是否应使用安全连接(HTTPS)。
- version [字符串] - (可选,默认: '0.12') 要针对的 Catenis API 版本。
- useCompression [布尔值] - (可选,默认: true) 指示是否应压缩请求/响应正文。
- compressThreshold [整数] - (可选,默认: 1024) 请求正文的最小大小(以字节为单位),以便压缩。
- timeout [浮点数|整数] - (可选,默认: 0,无超时) 等待响应的超时时间(以秒为单位)。
- eventLoop [React\EventLoop\LoopInterface] - (可选) 用于异步 API 方法调用机制的事件循环。
- pumpTaskQueue [布尔值] - (可选,默认: true) 指示是否强制定期运行承诺任务队列。
- pumpInterval [整数] - (可选,默认: 10) 指定定期运行任务队列的时间间隔(以毫秒为单位)。
异步方法调用
每个 API 方法都有一个异步对应方法,该方法具有 Async 后缀,例如 logMessageAsync
。
异步方法返回一个承诺,并且当与事件循环一起使用时,可以在异步方式中处理其结果。
要使用事件循环,请在实例化 ApiClient 对象时传递事件循环实例作为选项。
$loop = \React\EventLoop\Factory::create(); $ctnApiClient = new \Catenis\ApiClient( $deviceId, $apiAccessSecret, [ 'environment' => 'sandbox' 'eventLoop' => $loop ] );
异步 API 方法调用处理的示例。
$ctnApiClient->logMessageAsync('My message')->then( function (stdClass $data) { // Process returned data }, function (\Catenis\Exception\CatenisException $ex) { // Process exception } );
注意:为了异步解析承诺,应定期运行承诺任务队列(即
\GuzzleHttp\Promise\queue()->run()
)。默认情况下,Catenis API PHP客户端将每10毫秒运行一次承诺任务队列。要设置运行承诺任务队列的不同间隔,在实例化Catenis API PHP客户端时传递可选参数 pumpInterval,该参数为与所需毫秒数相对应的整数值(例如,'pumpInterval' => 5
)。或者,为了避免运行承诺任务队列,在实例化客户端时传递可选参数 pumpTaskQueue 并将其设置为 false(即'pumpTaskQueue' => false
)。在这种情况下,最终用户应负责运行承诺任务队列。
要强制返回的承诺完成并获取API方法的返回数据,请使用其 wait()
方法。
try { $data = $ctnApiClient->logMessageAsync('My message')->wait(); // Process returned data } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
返回的数据
在成功调用Catenis API时,客户端库方法返回的数据仅包括Catenis API请求原响应中返回的JSON的 data
属性。
例如,您应该期望从成功调用 logMessage
方法的以下数据被返回
object(stdClass)#54 (1) { ["messageId"]=> string(20) "m57enyYQK7QmqSxgP94j" }
将消息记录到区块链
一次性传递整个消息的内容
try { $data = $ctnApiClient->logMessage('My message', [ 'encoding' => 'utf8', 'encrypt' => true, 'offChain' => true, 'storage' => 'auto' ]); // Process returned data echo 'ID of logged message: ' . $data->messageId . PHP_EOL; } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
分块传递消息的内容
$message = [ 'First part of message', 'Second part of message', 'Third and last part of message' ]; try { $continuationToken = null; foreach ($message as $chunk) { $data = $ctnApiClient->logMessage([ 'data' => $chunk, 'isFinal' => false, 'continuationToken' => $continuationToken ], [ 'encoding' => 'utf8' ]); $continuationToken = $data->continuationToken; } // Signal that message has ended and get result $data = $ctnApiClient->logMessage([ 'isFinal' => true, 'continuationToken' => $continuationToken ], [ 'encrypt' => true, 'offChain' => true, 'storage' => 'auto' ]); echo 'ID of logged message: ' . $data->messageId . PHP_EOL; } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
异步记录消息
try { $data = $ctnApiClient->logMessage('My message', [ 'encoding' => 'utf8', 'encrypt' => true, 'offChain' => true, 'storage' => 'auto', 'async' => true ]); // Start pooling for asynchronous processing progress $provisionalMessageId = $data->provisionalMessageId; $done = false; $result = null; wait(1); do { $data = $ctnApiClient->retrieveMessageProgress($provisionalMessageId); // Process returned data echo 'Number of bytes processed so far: ' . $data->progress->bytesProcessed . PHP_EOL; if ($data->progress->done) { if ($data->progress->success) { // Get result $result = $data->result; } else { // Process error echo 'Asynchronous processing error: [' . $data->progress->error->code . '] - ' . $data->progress->error->message . PHP_EOL; } $done = true; } else { // Asynchronous processing not done yet. Wait before continuing pooling wait(3); } } while (!$done); if (!is_null($result)) { echo 'ID of logged message: ' . $result->messageId . PHP_EOL; } } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
向另一个设备发送消息
一次性传递整个消息的内容
try { $data = $ctnApiClient->sendMessage('My message', [ 'id' => $targetDeviceId, 'isProdUniqueId' => false ], [ 'encoding' => 'utf8', 'encrypt' => true, 'offChain' => true, 'storage' => 'auto', 'readConfirmation' => true ]); // Process returned data echo 'ID of sent message: ' . $data->messageId . PHP_EOL; } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
分块传递消息的内容
$message = [ 'First part of message', 'Second part of message', 'Third and last part of message' ]; try { $continuationToken = null; foreach ($message as $chunk) { $data = $ctnApiClient->sendMessage([ 'data' => $chunk, 'isFinal' => false, 'continuationToken' => $continuationToken ], [ 'id' => $targetDeviceId, 'isProdUniqueId' => false ], [ 'encoding' => 'utf8' ]); $continuationToken = $data->continuationToken; } // Signal that message has ended and get result $data = $ctnApiClient->sendMessage([ 'isFinal' => true, 'continuationToken' => $continuationToken ], [ 'id' => $targetDeviceId, 'isProdUniqueId' => false ], [ 'encrypt' => true, 'offChain' => true, 'storage' => 'auto', 'readConfirmation' => true ]); echo 'ID of sent message: ' . $data->messageId . PHP_EOL; } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
异步发送消息
try { $data = $ctnApiClient->sendMessage('My message', [ 'id' => $targetDeviceId, 'isProdUniqueId' => false ], [ 'encoding' => 'utf8', 'encrypt' => true, 'offChain' => true, 'storage' => 'auto', 'readConfirmation' => true, 'async' => true ]); // Start pooling for asynchronous processing progress $provisionalMessageId = $data->provisionalMessageId; $done = false; $result = null; wait(1); do { $data = $ctnApiClient->retrieveMessageProgress($provisionalMessageId); // Process returned data echo 'Number of bytes processed so far: ' . $data->progress->bytesProcessed . PHP_EOL; if ($data->progress->done) { if ($data->progress->success) { // Get result $result = $data->result; } else { // Process error echo 'Asynchronous processing error: [' . $data->progress->error->code . '] - ' . $data->progress->error->message . PHP_EOL; } $done = true; } else { // Asynchronous processing not done yet. Wait before continuing pooling wait(3); } } while (!$done); if (!is_null($result)) { echo 'ID of sent message: ' . $result->messageId . PHP_EOL; } } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
读取消息
一次性检索整个已读消息的内容
try { $data = $ctnApiClient->readMessage($messageId, 'utf8'); // Process returned data if ($data->msgInfo->action === 'send') { echo 'Message sent from: ' . print_r($data->msgInfo->from, true); } echo 'Read message: ' . $data->msgData . PHP_EOL; } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
分块检索已读消息的内容
try { $continuationToken = null; $chunkCount = 1; do { $data = $ctnApiClient->readMessage($messageId, [ 'encoding' => 'utf8', 'continuationToken' => $continuationToken, 'dataChunkSize' => 1024 ]); // Process returned data if (isset($data->msgInfo) && $data->msgInfo->action == 'send') { echo 'Message sent from: ' . $data->msgInfo->from); } echo 'Read message (chunk ' . $chunkCount . '): ' . $data->msgData); if (isset($data->continuationToken)) { // Get continuation token to continue reading message $continuationToken = $data->continuationToken; $chunkCount += 1; } else { $continuationToken = null; } } while (!is_null($continuationToken)); } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
异步读取消息
try { // Request to read message asynchronously $data = $ctnApiClient->readMessage($messageId, [ 'async' => true ]); // Start pooling for asynchronous processing progress $cachedMessageId = $data->cachedMessageId; $done = false; $result = null; wait(1); do { $data = $ctnApiClient->retrieveMessageProgress($cachedMessageId); // Process returned data echo 'Number of bytes processed so far: ' . $data->progress->bytesProcessed . PHP_EOL; if ($data->progress->done) { if ($data->progress->success) { // Get result $result = $data->result; } else { // Process error echo 'Asynchronous processing error: [' . $data->progress->error->code . '] - ' . $data->progress->error->message . PHP_EOL; } $done = true; } else { // Asynchronous processing not done yet. Wait before continuing pooling wait(3); } } while (!$done); if (!is_null($result)) { // Retrieve read message $data = $ctnApiClient->readMessage($messageId, [ 'encoding' => 'utf8', 'continuationToken' => $result->continuationToken ]); if ($data->msgInfo->action === 'send') { echo 'Message sent from: ' . print_r($data->msgInfo->from, true); } echo 'Read message: ' . $data->msgData . PHP_EOL; } } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
检索消息容器的信息
try { $data = $ctnApiClient->retrieveMessageContainer($messageId); // Process returned data if (isset($data->offChain)) { echo 'IPFS CID of Catenis off-chain message envelope: ' . $data->offChain->cid . PHP_EOL; } if (isset($data->blockchain)) { echo 'ID of blockchain transaction containing the message: ' . $data->blockchain->txid . PHP_EOL; } if (isset($data->externalStorage)) { echo 'IPFS reference to message: ' . $data->externalStorage->ipfs . PHP_EOL; } } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
检索消息源的信息
try { $data = $ctnApiClient->retrieveMessageOrigin($messageId, 'Any text to be signed'); // Process returned data if (isset($data->tx)) { echo 'Catenis message transaction info: ' . print_r($data->tx, true); } if (isset($data->offChainMsgEnvelope)) { echo 'Off-chain message envelope info: ' . print_r($data->offChainMsgEnvelope, true); } if (isset($data->proof)) { echo 'Origin proof info: ' . print_r($data->proof, true); } } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
检索异步消息处理进度
try { $data = $ctnApiClient->retrieveMessageProgress($provisionalMessageId); // Process returned data echo 'Number of bytes processed so far: ' . $data->progress->bytesProcessed . PHP_EOL; if ($data->progress->done) { if ($data->progress->success) { // Get result echo 'Asynchronous processing result: ' . $data->result . PHP_EOL; } else { // Process error echo 'Asynchronous processing error: [' . $data->progress->error->code . '] - ' . $data->progress->error->message . PHP_EOL; } } else { // Asynchronous processing not done yet. Continue pooling } } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
注意:请参阅上面的 异步记录消息、异步发送消息 和 异步读取消息 部分,以获取更完整的示例。
列出消息
try { $data = $ctnApiClient->listMessages([ 'action' => 'send', 'direction' => 'inbound', 'readState' => 'unread', 'startDate' => new \DateTime('20170101T000000Z') ], 200, 0); // Process returned data if ($data->msgCount > 0) { echo 'Returned messages: ' . print_r($data->messages, true); if ($data->hasMore) { echo 'Not all messages have been returned' . PHP_EOL; } } } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
注意:listMessages 方法接受的参数与List Messages Catenis API方法接受的参数不完全相同。大多数参数(除了最后两个
limit
和skip
)都映射到listMessages 方法的第一个参数($selector
)的键,有几个特殊性:参数fromDeviceIds
和fromDeviceProdUniqueIds
与参数toDeviceIds
和toDeviceProdUniqueIds
分别替换为键fromDevices
和toDevices
。这些键接受值为设备ID的关联数组索引数组,这与sendMessage 方法的第一个参数($targetDevice
)接受的关联数组类型相同。日期键startDate
和endDate
不仅接受包含ISO 8601格式日期/时间的字符串值,还接受DateTime对象。
发行一种新的资产数量
try { $data = $ctnApiClient->issueAsset([ 'name' => 'XYZ001', 'description' => 'My first test asset', 'canReissue' => true, 'decimalPlaces' => 2 ], 1500.00, null); // Process returned data echo 'ID of newly issued asset: ' . $data->assetId . PHP_EOL; } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
发行现有资产额外的数量
try { $data = $ctnApiClient->reissueAsset($assetId, 650.25, [ 'id' => $otherDeviceId, 'isProdUniqueId' => false ]); // Process returned data echo 'Total existent asset balance (after issuance): ' . $data->totalExistentBalance . PHP_EOL; } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
将资产的数量转移到另一个设备
try { $data = $ctnApiClient->transferAsset($assetId, 50.75, [ 'id' => $otherDeviceId, 'isProdUniqueId' => false ]); // Process returned data echo 'Remaining asset balance: ' . $data->remainingBalance . PHP_EOL; } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
创建一个新的非同质化资产并发行其(初始)非同质化代币
在单次调用中传递非同质化代币内容
try { $data = $ctnApiClient->issueNonFungibleAsset([ 'assetInfo' => [ 'name' => 'Catenis NFA 1', 'description' => 'Non-fungible asset #1 for testing', 'canReissue' => true ] ], [ [ 'metadata' => [ 'name' => 'NFA1 NFT 1', 'description' => 'First token of Catenis non-fungible asset #1' ], 'contents' => [ 'data' => 'Contents of first token of Catenis non-fungible asset #1', 'encoding' => 'utf8' ] ], [ 'metadata' => [ 'name' => 'NFA1 NFT 2', 'description' => 'Second token of Catenis non-fungible asset #1' ], 'contents' => [ 'data' => 'Contents of second token of Catenis non-fungible asset #1', 'encoding' => 'utf8' ] ] ]); // Process returned data echo 'ID of newly created non-fungible asset: ' . $data->assetId . PHP_EOL; echo 'IDs of newly issued non-fungible tokens: ' . implode(', ', $data->nfTokenIds) . PHP_EOL; } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
在多次调用中传递非同质化代币内容
$issuanceInfo = [ 'assetInfo' => [ 'name' => 'Catenis NFA 1', 'description' => 'Non-fungible asset #1 for testing', 'canReissue' => true ] ]; $nftMetadata = [ [ 'name' => 'NFA1 NFT 1', 'description' => 'First token of Catenis non-fungible asset #1' ], [ 'name' => 'NFA1 NFT 2', 'description' => 'Second token of Catenis non-fungible asset #1' ] ]; $nftContents = [ [ [ 'data' => 'Contents of first token of Catenis non-fungible asset #1', 'encoding' => 'utf8' ] ], [ [ 'data' => 'Here is the contents of the second token of Catenis non-fungible asset #1 (part #1)', 'encoding' => 'utf8' ], [ 'data' => '; and here is the last part of the contents of the second token of Catenis non-fungible asset #1.', 'encoding' => 'utf8' ] ] ]; try { $continuationToken = null; $data = null; $nfTokens = null; $callIdx = -1; do { $nfTokens = null; $callIdx++; if ($continuationToken === null) { foreach ($nftMetadata as $tokenIdx => $metadata) { $nfToken = [ 'metadata' => $metadata ]; if (isset($nftContents[$tokenIdx])) { $nfToken['contents'] = $nftContents[$tokenIdx][$callIdx]; } $nfTokens[] = $nfToken; } } else { // Continuation call foreach ($nftContents as $tokenIdx => $contents) { $nfTokens[] = isset($contents) && $callIdx < count($callIdx) ? ['contents' => $contents[$callIdx]] : null; } if (is_array($nfTokens)) { $allNull = true; foreach ($nfTokens as $tokenIdx => $nfToken) { if ($nfToken !== null) { $allNull = false; break; } } if ($allNull) { $nfTokens = null; } } } $data = $ctnApiClient->issueNonFungibleAsset( $continuationToken !== null ? $continuationToken : $issuanceInfo, $nfTokens, !isset($nfTokens) ); $continuationToken = isset($data->continuationToken) ? $data->continuationToken : null; } while ($continuationToken !== null); // Process returned data echo 'ID of newly created non-fungible asset: ' . $data->assetId . PHP_EOL; echo 'IDs of newly issued non-fungible tokens: ' . implode(', ', $data->nfTokenIds) . PHP_EOL; } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
异步进行发行
try { $data = $ctnApiClient->issueNonFungibleAsset([ 'assetInfo' => [ 'name' => 'Catenis NFA 1', 'description' => 'Non-fungible asset #1 for testing', 'canReissue' => true ], 'async' => true ], [ [ 'metadata' => [ 'name' => 'NFA1 NFT 1', 'description' => 'First token of Catenis non-fungible asset #1' ], 'contents' => [ 'data' => 'Contents of first token of Catenis non-fungible asset #1', 'encoding' => 'utf8' ] ], [ 'metadata' => [ 'name' => 'NFA1 NFT 2', 'description' => 'Second token of Catenis non-fungible asset #1' ], 'contents' => [ 'data' => 'Contents of second token of Catenis non-fungible asset #1', 'encoding' => 'utf8' ] ] ]); // Start pooling for asynchronous processing progress $assetIssuanceId = $data->assetIssuanceId; $done = false; $result = null; wait(1); do { $data = $ctnApiClient->retrieveNonFungibleAssetIssuanceProgress($assetIssuanceId); // Process returned data echo 'Percent processed: ', $data->progress->percentProcessed . PHP_EOL; if ($data->progress->done) { if ($data->progress->success) { // Get result $result = $data->result; } else { // Process error echo 'Asynchronous processing error: [' . $data->progress->error->code . '] - ' . $data->progress->error->message . PHP_EOL; } $done = true; } else { // Asynchronous processing not done yet. Wait before continuing pooling wait(3); } } while (!$done); if ($result !== null) { echo 'ID of newly created non-fungible asset: ' . $result->assetId . PHP_EOL; echo 'IDs of newly issued non-fungible tokens: ' . implode(', ', $result->nfTokenIds) . PHP_EOL; } } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
为先前创建的非同质化资产发行更多非同质化代币
在单次调用中传递非同质化代币内容
try { $data = $ctnApiClient->reissueNonFungibleAsset($assetId, null, [ [ 'metadata' => [ 'name' => 'NFA1 NFT 3', 'description' => 'Third token of Catenis non-fungible asset #1' ], 'contents' => [ 'data' => 'Contents of third token of Catenis non-fungible asset #1', 'encoding' => 'utf8' ] ], [ 'metadata' => [ 'name' => 'NFA1 NFT 4', 'description' => 'Forth token of Catenis non-fungible asset #1' ], 'contents' => [ 'data' => 'Contents of forth token of Catenis non-fungible asset #1', 'encoding' => 'utf8' ] ] ]); // Process returned data echo 'IDs of newly issued non-fungible tokens: ' . $data->nfTokenIds . PHP_EOL; } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
在多次调用中传递非同质化代币内容
$nftMetadata = [ [ 'name' => 'NFA1 NFT 3', 'description' => 'Third token of Catenis non-fungible asset #1' ], [ 'name' => 'NFA1 NFT 4', 'description' => 'Forth token of Catenis non-fungible asset #1' ] ]; $nftContents = [ [ [ 'data' => 'Contents of third token of Catenis non-fungible asset #1', 'encoding' => 'utf8' ] ], [ [ 'data' => 'Here is the contents of the forth token of Catenis non-fungible asset #1 (part #1)', 'encoding' => 'utf8' ], [ 'data' => '; and here is the last part of the contents of the forth token of Catenis non-fungible asset #1.', 'encoding' => 'utf8' ] ] ]; try { $continuationToken = null; $data = null; $nfTokens = null; $callIdx = -1; do { $nfTokens = null; $callIdx++; if ($continuationToken === null) { foreach ($nftMetadata as $tokenIdx => $metadata) { $nfToken = [ 'metadata' => $metadata ]; if (isset($nftContents[$tokenIdx])) { $nfToken['contents'] = $nftContents[$tokenIdx][$callIdx]; } $nfTokens[] = $nfToken; } } else { // Continuation call foreach ($nftContents as $tokenIdx => $contents) { $nfTokens[] = isset($contents) && $callIdx < count($callIdx) ? ['contents' => $contents[$callIdx]] : null; } if (is_array($nfTokens)) { $allNull = true; foreach ($nfTokens as $tokenIdx => $nfToken) { if ($nfToken !== null) { $allNull = false; break; } } if ($allNull) { $nfTokens = null; } } } $data = $ctnApiClient->reissueNonFungibleAsset( $assetId, $continuationToken, $nfTokens, !isset($nfTokens) ); $continuationToken = isset($data->continuationToken) ? $data->continuationToken : null; } while ($continuationToken !== null); // Process returned data echo 'IDs of newly issued non-fungible tokens: ' . implode(', ', $data->nfTokenIds) . PHP_EOL; } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
异步进行发行
try { $data = $ctnApiClient->reissueNonFungibleAsset($assetId, [ 'async' => true ], [ [ 'metadata' => [ 'name' => 'NFA1 NFT 3', 'description' => 'Third token of Catenis non-fungible asset #1' ], 'contents' => [ 'data' => 'Contents of third token of Catenis non-fungible asset #1', 'encoding' => 'utf8' ] ], [ 'metadata' => [ 'name' => 'NFA1 NFT 4', 'description' => 'Forth token of Catenis non-fungible asset #1' ], 'contents' => [ 'data' => 'Contents of forth token of Catenis non-fungible asset #1', 'encoding' => 'utf8' ] ] ]); // Start pooling for asynchronous processing progress $assetIssuanceId = $data->assetIssuanceId; $done = false; $result = null; wait(1); do { $data = $ctnApiClient->retrieveNonFungibleAssetIssuanceProgress($assetIssuanceId); // Process returned data echo 'Percent processed: ', $data->progress->percentProcessed . PHP_EOL; if ($data->progress->done) { if ($data->progress->success) { // Get result $result = $data->result; } else { // Process error echo 'Asynchronous processing error: [' . $data->progress->error->code . '] - ' . $data->progress->error->message . PHP_EOL; } $done = true; } else { // Asynchronous processing not done yet. Wait before continuing pooling wait(3); } } while (!$done); if ($result !== null) { echo 'IDs of newly issued non-fungible tokens: ' . implode(', ', $result->nfTokenIds) . PHP_EOL; } } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
检索与非同质化代币关联的数据
同步检索
try { $continuationToken = null; $data = null; $nfTokenData = null; do { $data = $ctnApiClient->retrieveNonFungibleToken( $tokenId, isset($continuationToken) ? ['continuationToken' => $continuationToken] : null ); if (!isset($nfTokenData)) { // Get token data $nfTokenData = (object)[ 'assetId' => $data->nonFungibleToken->assetId, 'metadata' => $data->nonFungibleToken->metadata, 'contents' => [$data->nonFungibleToken->contents->data] ]; } else { // Add next contents part to token data $nfTokenData->contents[] = $data->nonFungibleToken->contents->data; } $continuationToken = isset($data->continuationToken) ? $data->continuationToken : null; } while ($continuationToken !== null); // Process returned data echo 'Non-fungible token data: ' . print_r($nfTokenData, true); } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
异步检索
try { $data = $ctnApiClient->retrieveNonFungibleToken($tokenId, [ 'async' => true ]); // Start pooling for asynchronous processing progress $tokenRetrievalId = $data->tokenRetrievalId; $done = false; $continuationToken = null; wait(1); do { $data = $ctnApiClient->retrieveNonFungibleTokenRetrievalProgress($tokenId, $tokenRetrievalId); // Process returned data echo 'Bytes already retrieved: ', $data->progress->bytesRetrieved . PHP_EOL; if ($data->progress->done) { if ($data->progress->success) { // Prepare to finish retrieving the non-fungible token data $continuationToken = $data->continuationToken; } else { // Process error echo 'Asynchronous processing error: [' . $data->progress->error->code . '] - ' . $data->progress->error->message . PHP_EOL; } $done = true; } else { // Asynchronous processing not done yet. Wait before continuing pooling wait(3); } } while (!$done); if ($continuationToken !== null) { // Finish retrieving the non-fungible token data $nfTokenData = null; do { $data = $ctnApiClient->retrieveNonFungibleToken( $tokenId, $continuationToken ); if (!isset($nfTokenData)) { // Get token data $nfTokenData = (object)[ 'assetId' => $data->nonFungibleToken->assetId, 'metadata' => $data->nonFungibleToken->metadata, 'contents' => [$data->nonFungibleToken->contents->data] ]; } else { // Add next contents part to token data $nfTokenData->contents[] = $data->nonFungibleToken->contents->data; } $continuationToken = isset($data->continuationToken) ? $data->continuationToken : null; } while ($continuationToken !== null); // Process returned data echo 'Non-fungible token data: ' . print_r($nfTokenData, true); } } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
将非同质化代币转移到另一个设备
同步转移
try { $data = $ctnApiClient->transferNonFungibleToken($tokenId, [ 'id' => $otherDeviceId, 'isProdUniqueId' => false ]); // Process returned data echo 'Non-fungible token successfully transferred' . PHP_EOL; } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
异步转移
try { $data = $ctnApiClient->transferNonFungibleToken($tokenId, [ 'id' => $otherDeviceId, 'isProdUniqueId' => false ], true); // Start pooling for asynchronous processing progress $tokenTransferId = $data->tokenTransferId; $done = false; wait(1); do { $data = $ctnApiClient->retrieveNonFungibleTokenTransferProgress($tokenId, $tokenTransferId); // Process returned data echo 'Current data manipulation: ', print_r($data->progress->dataManipulation, true); if ($data->progress->done) { if ($data->progress->success) { // Display result echo 'Non-fungible token successfully transferred' . PHP_EOL; } else { // Process error echo 'Asynchronous processing error: [' . $data->progress->error->code . '] - ' . $data->progress->error->message . PHP_EOL; } $done = true; } else { // Asynchronous processing not done yet. Wait before continuing pooling wait(3); } } while (!$done); } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
检索关于特定资产的信息
try { $data = $ctnApiClient->retrieveAssetInfo($assetId); // Process returned data echo 'Asset info:' . print_r($data, true); } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
获取设备持有的特定资产的当前余额
try { $data = $ctnApiClient->getAssetBalance($assetId); // Process returned data echo 'Current asset balance: ' . $data->balance->total . PHP_EOL; echo 'Amount not yet confirmed: ' . $data->balance->unconfirmed . PHP_EOL; } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
列出设备拥有的资产
try { $data = $ctnApiClient->listOwnedAssets(200, 0); // Process returned data foreach ($data->ownedAssets as $idx => $ownedAsset) { echo 'Owned asset #' . ($idx + 1) . ':' . PHP_EOL; echo ' - asset ID: ' . $ownedAsset->assetId . PHP_EOL; echo ' - current asset balance: ' . $ownedAsset->balance->total . PHP_EOL; echo ' - amount not yet confirmed: ' . $ownedAsset->balance->unconfirmed . PHP_EOL; } if ($data->hasMore) { echo 'Not all owned assets have been returned' . PHP_EOL; } } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
列出设备发行的资产
try { $data = $ctnApiClient->listIssuedAssets(200, 0); // Process returned data foreach ($data->issuedAssets as $idx => $issuedAsset) { echo 'Issued asset #' . ($idx + 1) . ':' . PHP_EOL; echo ' - asset ID: ' . $issuedAsset->assetId . PHP_EOL; echo ' - total existent balance: ' . $issuedAsset->totalExistentBalance . PHP_EOL; } if ($data->hasMore) { echo 'Not all issued assets have been returned' . PHP_EOL; } } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
检索特定资产的发行历史
try { $data = $ctnApiClient->retrieveAssetIssuanceHistory($assetId, new \DateTime('20170101T000000Z'), null, 200, 0); // Process returned data foreach ($data->issuanceEvents as $idx => $issuanceEvent) { echo 'Issuance event #', ($idx + 1) . ':' . PHP_EOL; if (!isset($issuanceEvent->nfTokenIds)) { echo ' - issued amount: ' . $issuanceEvent->amount . PHP_EOL; } else { echo ' - IDs of issued non-fungible tokens:' . print_r($issuanceEvent->nfTokenIds, true); } if (!isset($issuanceEvent->holdingDevices)) { echo ' - device to which issued amount has been assigned: ' . print_r($issuanceEvent->holdingDevice, true); } else { echo ' - devices to which issued non-fungible tokens have been assigned:', print_r($issuanceEvent->holdingDevices, true); } echo ' - date of issuance: ' . $issuanceEvent->date . PHP_EOL; } if ($data->hasMore) { echo 'Not all asset issuance events have been returned' . PHP_EOL; } } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
注意:方法 retrieveAssetIssuanceHistory 的参数与 Catenis API 中获取资产发行历史的方法的参数略有不同。特别是日期参数
$startDate
和$endDate
,不仅接受包含 ISO 8601 格式日期/时间的字符串,还接受 DateTime 对象。
列出当前持有特定资产任何数量的设备
try { $data = $ctnApiClient->listAssetHolders($assetId, 200, 0); // Process returned data foreach ($data->assetHolders as $idx => $assetHolder) { if (isset($assetHolder->holder)) { echo 'Asset holder #' . ($idx + 1) . ':' . PHP_EOL; echo ' - device holding an amount of the asset: ' . print_r($assetHolder->holder, true); echo ' - amount of asset currently held by device: ' . $assetHolder->balance->total . PHP_EOL; echo ' - amount not yet confirmed: ' . $assetHolder->balance->unconfirmed . PHP_EOL; } else { echo 'Migrated asset:' . PHP_EOL; echo ' - total migrated amount: ' . $assetHolder->balance->total . PHP_EOL; echo ' - amount not yet confirmed: ' . $assetHolder->balance->unconfirmed . PHP_EOL; } } if ($data->hasMore) { echo 'Not all asset holders have been returned' . PHP_EOL; } } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
将资产导出到国外区块链
估算在国外区块链的本位币中的导出成本
try { $foreignBlockchain = 'ethereum'; $data = $ctnApiClient->exportAsset($assetId, $foreignBlockchain, [ 'name' => 'Test Catenis token #01', 'symbol' => 'CTK01' ], [ 'estimateOnly' => true ]); // Process returned data echo 'Estimated foreign blockchain transaction execution price: ' . $data->estimatedPrice . PHP_EOL; } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
执行导出操作
try { $foreignBlockchain = 'ethereum'; $data = $ctnApiClient->exportAsset($assetId, $foreignBlockchain, [ 'name' => 'Test Catenis token #01', 'symbol' => 'CTK01' ]); // Process returned data echo 'Foreign blockchain transaction ID (hash): ' . $data->foreignTransaction->id . PHP_EOL; // Start polling for asset export outcome $done = false; $tokenId = null; wait(1); do { $data = $ctnApiClient->assetExportOutcome($assetId, $foreignBlockchain); // Process returned data if ($data->status === 'success') { // Asset successfully exported $tokenId = $data->token->id; $done = true; } elseif ($data->status === 'pending') { // Final asset export state not yet reached. Wait before continuing pooling wait(3); } else { // Asset export has failed. Process error echo 'Error executing foreign blockchain transaction: ' . $data->foreignTransaction->error . PHP_EOL; $done = true; } } while (!$done); if (!is_null($tokenId)) { echo 'Foreign token ID (address): ' . $tokenId . PHP_EOL; } } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
将资产数量迁移到国外区块链
估算在国外区块链的本位币中的迁移成本
try { $foreignBlockchain = 'ethereum'; $data = $ctnApiClient->migrateAsset($assetId, $foreignBlockchain, [ 'direction' => 'outward', 'amount' => 50, 'destAddress' => '0xe247c9BfDb17e7D8Ae60a744843ffAd19C784943' ], [ 'estimateOnly' => true ]); // Process returned data echo 'Estimated foreign blockchain transaction execution price: ' . $data->estimatedPrice . PHP_EOL; } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
执行迁移操作
try { $foreignBlockchain = 'ethereum'; $data = $ctnApiClient->migrateAsset($assetId, $foreignBlockchain, [ 'direction' => 'outward', 'amount' => 50, 'destAddress' => '0xe247c9BfDb17e7D8Ae60a744843ffAd19C784943' ]); // Process returned data $migrationId = $data->migrationId; echo 'Asset migration ID: ' . $migrationId . PHP_EOL; // Start polling for asset migration outcome $done = false; wait(1); do { $data = $ctnApiClient->assetMigrationOutcome($migrationId); // Process returned data if ($data->status === 'success') { // Asset amount successfully migrated echo 'Asset amount successfully migrated' . PHP_EOL; $done = true; } elseif ($data->status === 'pending') { // Final asset migration state not yet reached. Wait before continuing pooling wait(3); } else { // Asset migration has failed. Process error if (isset($data->catenisService->error)) { echo 'Error executing Catenis service: ' . $data->catenisService->error . PHP_EOL; } if (isset($data->foreignTransaction->error)) { echo 'Error executing foreign blockchain transaction: ' . $data->foreignTransaction->error . PHP_EOL; } $done = true; } } while (!$done); } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
重新处理(失败的)迁移
try { $foreignBlockchain = 'ethereum'; $data = $ctnApiClient->migrateAsset($assetId, $foreignBlockchain, $migrationId); // Start polling for asset migration outcome } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
获取资产导出结果
try { $foreignBlockchain = 'ethereum'; $data = $ctnApiClient->assetExportOutcome($assetId, $foreignBlockchain); // Process returned data if ($data->status === 'success') { // Asset successfully exported echo 'Foreign token ID (address): ' . $data->token->id . PHP_EOL; } elseif ($data->status === 'pending') { // Final asset export state not yet reached } else { // Asset export has failed. Process error echo 'Error executing foreign blockchain transaction: ' . $data->foreignTransaction->error . PHP_EOL; } } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
获取资产迁移结果
try { $data = $ctnApiClient->assetMigrationOutcome($migrationId); // Process returned data if ($data->status === 'success') { // Asset amount successfully migrated echo 'Asset amount successfully migrated' . PHP_EOL; } elseif ($data->status === 'pending') { // Final asset migration state not yet reached } else { // Asset migration has failed. Process error if (isset($data->catenisService->error)) { echo 'Error executing Catenis service: ' . $data->catenisService->error . PHP_EOL; } if (isset($data->foreignTransaction->error)) { echo 'Error executing foreign blockchain transaction: ' . $data->foreignTransaction->error . PHP_EOL; } } } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
列出已导出的资产
try { $data = $ctnApiClient->listExportedAssets([ 'foreignBlockchain' => 'ethereum', 'status' => 'success', 'startDate' => new \DateTime('20210801T000000Z') ], 200, 0); // Process returned data if (count($data->exportedAssets) > 0) { echo 'Returned asset exports: ' . print_r($data->exportedAssets, true); if ($data->hasMore) { echo 'Not all asset exports have been returned' . PHP_EOL; } } } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
注意:方法 listExportedAssets 所接受的参数与 List Exported Assets Catenis API 方法的参数不完全匹配。大部分参数(除最后两个
limit
和skip
外)被映射到 listExportedAssets 方法的第一个参数($selector
)的键,有一些特殊之处:日期键startDate
和endDate
,接受值为包含 ISO 8601 格式日期/时间的字符串,也接受 DateTime 对象。
列出资产迁移
try { $data = $ctnApiClient->listAssetMigrations([ 'foreignBlockchain' => 'ethereum', 'direction' => 'outward', 'status' => 'success', 'startDate' => new \DateTime('20210801T000000Z') ], 200, 0); // Process returned data if (count($data->assetMigrations) > 0) { echo 'Returned asset migrations: ' . print_r($data->assetMigrations, true); if ($data->hasMore) { echo 'Not all asset migrations have been returned' . PHP_EOL; } } } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
注意:方法 listAssetMigrations 所接受的参数与 List Asset Migrations Catenis API 方法的参数不完全匹配。大部分参数(除最后两个
limit
和skip
外)被映射到 listAssetMigrations 方法的第一个参数($selector
)的键,有一些特殊之处:日期键startDate
和endDate
,接受值为包含 ISO 8601 格式日期/时间的字符串,也接受 DateTime 对象。
列出系统定义的权限事件
try { $data = $ctnApiClient->listPermissionEvents(); // Process returned data foreach ($data as $eventName => $description) { echo 'Event name: ' . $eventName . '; event description: ' . $description . PHP_EOL; } } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
检索指定权限事件的当前权限
try { $data = $ctnApiClient->retrievePermissionRights('receive-msg'); // Process returned data echo 'Default (system) permission right: ' . $data->system . PHP_EOL; if (isset($data->catenisNode)) { if (isset($data->catenisNode->allow)) { echo 'Index of Catenis nodes with \'allow\' permission right: ' . implode(', ', $data->catenisNode->allow) . PHP_EOL; } if (isset($data->catenisNode->deny)) { echo 'Index of Catenis nodes with \'deny\' permission right: ' . implode(', ', $data->catenisNode->deny) . PHP_EOL; } } if (isset($data->client)) { if (isset($data->client->allow)) { echo 'ID of clients with \'allow\' permission right: ' . implode(', ', $data->client->allow) . PHP_EOL; } if (isset($data->client->deny)) { echo 'ID of clients with \'deny\' permission right: ' . implode(', ', $data->client->deny) . PHP_EOL; } } if (isset($data->device)) { if (isset($data->device->allow)) { echo 'Devices with \'allow\' permission right: ' . print_r($data->device->allow, true); } if (isset($data->device->deny)) { echo 'Devices with \'deny\' permission right: ' . print_r($data->device->deny, true); } } } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
为指定权限事件设置不同级别的权限
try { $data = $ctnApiClient->setPermissionRights( 'receive-msg', [ 'system' => 'deny', 'catenisNode' => [ 'allow' => 'self' ], 'client' => [ 'allow' => [ 'self', $clientId ] ], 'device' => [ 'deny' => [[ 'id' => $deviceId1 ], [ 'id' => 'ABCD001', 'isProdUniqueId' => true ]] ] ] ); // Process returned data echo 'Permission rights successfully set' . PHP_EOL; } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
检查针对指定权限事件应用于特定设备的有效权限
try { $data = $ctnApiClient->checkEffectivePermissionRight('receive-msg', $deviceProdUniqueId, true); // Process returned data $deviceId = array_keys(get_object_vars($data))[0]; echo 'Effective right for device ' . $deviceId . ': ' . $data->$deviceId . PHP_EOL; } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
检索特定设备的标识信息
try { $data = $ctnApiClient->retrieveDeviceIdentificationInfo($deviceId, false); // Process returned data echo 'Device\'s Catenis node ID info:' . print_r($data->catenisNode, true); echo 'Device\'s client ID info:' . print_r($data->client, true); echo 'Device\'s own ID info:' . print_r($data->device, true); } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
列出系统定义的通知事件
try { $data = $ctnApiClient->listNotificationEvents(); // Process returned data foreach ($data as $eventName => $description) { echo 'Event name: ' . $eventName . '; event description: ' . $description . PHP_EOL; } } catch (\Catenis\Exception\CatenisException $ex) { // Process exception }
通知
Catenis API PHP 客户端通过嵌入 WebSocket 客户端,使得接收来自 Catenis 系统的通知变得简单。所有终端用户需要做的就是为所需的 Catenis 通知事件打开一个 WebSocket 通知通道,并监控该通道的活动。
通知需要使用事件循环。您应该在实例化 ApiClient 对象时将事件循环实例作为选项传递,就像使用异步 API 方法一样。
$loop = \React\EventLoop\Factory::create(); $ctnApiClient = new \Catenis\ApiClient( $deviceId, $apiAccessSecret, [ 'environment' => 'sandbox' 'eventLoop' => $loop ] );
注意:如果在实例化 ApiClient 对象时没有传递事件循环实例,将创建一个内部事件循环。然而,在这种情况下,通知只有在应用程序关闭(事件循环最终运行)后才会被处理。
接收通知
实例化 WebSocket 通知通道对象。
$wsNtfyChannel = $ctnApiClient->createWsNotifyChannel($eventName);
添加监听器。
$wsNtfyChannel->on('error', function ($error) { // Process error in the underlying WebSocket connection }); $wsNtfyChannel->on('close', function ($code, $reason) { // Process indication that underlying WebSocket connection has been closed }); $wsNtfyChannel->on('open', function () { // Process indication that notification channel is successfully open // and ready to send notifications }); $wsNtfyChannel->on('notify', function ($data) { // Process received notification echo 'Received notification:' . PHP_EOL; print_r($data); });
注意:通知事件 notify 的
data
参数包含相应的通知事件的反序列化 JSON 通知消息(一个 stdClass 实例)。
打开通知通道。
$wsNtfyChannel->open()->then( function () { // WebSocket client successfully connected. Wait for open event to make // sure that notification channel is ready to send notifications }, function (\Catenis\Exception\WsNotificationException $ex) { // Process exception } );
注意:WebSocket 通知通道对象的
open()
方法以异步方式工作,因此它返回一个像异步 API 方法一样的承诺。
关闭通知通道。
$wsNtfyChannel->close();
错误处理
通过异常对象报告错误条件,在同步方法中抛出,在异步方法中作为参数传递。
API 方法异常
调用 API 方法时可能会发生以下异常
- CatenisClientException - 表示在尝试调用 Catenis API 端点时发生错误。
- CatenisApiException - 表示 Catenis API 端点返回了错误。
注意:这两个异常源于一个单一的异常,即CatenisException。
CatenisApiException对象提供了一些自定义方法,可以用来获取有关错误状态的一些特定数据,如下所示
-
getHttpStatusCode()
- 返回从Catenis API端点接收到的HTTP响应的数字状态码。 -
getHttpStatusMessage()
- 返回从Catenis API端点接收到的HTTP响应状态码的文本。 -
getCatenisErrorMessage()
- 返回从Catenis API端点返回的Catenis错误消息。
用法示例
try { $data = $ctnApiClient->readMessage('INVALID_MSG_ID', null); // Process returned data } catch (\Catenis\Exception\CatenisException $ex) { if ($ex instanceof \Catenis\Exception\CatenisApiException) { // Catenis API error echo 'HTTP status code: ' . $ex->getHttpStatusCode() . PHP_EOL; echo 'HTTP status message: ' . $ex->getHttpStatusMessage() . PHP_EOL; echo 'Catenis error message: ' . $ex->getCatenisErrorMessage() . PHP_EOL; echo 'Compiled error message: ' . $ex->getMessage() . PHP_EOL; } else { // Client error echo $ex . PHP_EOL; } }
预期结果
HTTP status code: 400
HTTP status message: Bad Request
Catenis error message: Invalid message ID
Compiled error message: Error returned from Catenis API endpoint: [400] Invalid message ID
WebSocket通知异常
在打开WebSocket通知通道时可能会发生以下异常
- OpenWsConnException - 表示在建立底层WebSocket连接时发生错误。
- WsNotifyChannelAlreadyOpenException - 表示WebSocket通知通道(对于该设备和通知事件)已经打开。
注意:这两个异常源于一个单一的异常,即WsNotificationException,而WsNotificationException本身也源于CatenisException。
用法示例
$wsNtfyChannel->open()->then( function () { // WebSocket client successfully connected. Wait for open event to make // sure that notification channel is ready to send notifications }, function (\Catenis\Exception\WsNotificationException $ex) { if ($ex instanceof \Catenis\Exception\OpenWsConnException) { // Error opening WebSocket connection echo $ex . PHP_EOL; } else { // WebSocket nofitication channel already open } } );
Catenis API文档
有关Catenis API的更多信息,请参阅Catenis API文档。
许可证
此库基于MIT许可证发布。请随意分支和修改!
版权所有 © 2018-2022,区块链事物公司