From 956f788218bfc7f4032eeb57f19cf35866c8d465 Mon Sep 17 00:00:00 2001 From: viraladmin <00purple@gmail.com> Date: Thu, 30 Jul 2026 10:39:29 -0600 Subject: [PATCH] fixed ipfs image decoding --- src/Routes/NftMediaRoutes.php | 11 ++-- src/Routes/RemainingRoutes.php | 4 +- src/Rpc/RpcReplyDecoder.php | 106 +++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 9 deletions(-) diff --git a/src/Routes/NftMediaRoutes.php b/src/Routes/NftMediaRoutes.php index 5d92fc1..8e5474c 100644 --- a/src/Routes/NftMediaRoutes.php +++ b/src/Routes/NftMediaRoutes.php @@ -9,6 +9,7 @@ use Contractless\Api\Http\JsonResponse; use Contractless\Api\Http\Request; use Contractless\Api\Http\Router; use Contractless\Api\Nft\NftMediaService; +use Contractless\Api\Rpc\RpcReplyDecoder; final class NftMediaRoutes { @@ -52,12 +53,10 @@ final class NftMediaRoutes ); } $series = Request::queryInt('series', 0, 0, 4_294_967_295); - $reply = $application->client->nftDetails($name, $series); - \Contractless\Api\Rpc\RpcReplyDecoder::notNodeError($reply); - if (strlen($reply) < 173) { - throw new \RuntimeException('The node returned invalid NFT details.'); - } - $cid = trim(substr($reply, 73, 100), "\0 "); + $details = RpcReplyDecoder::nftDetails( + $application->client->nftDetails($name, $series), + ); + $cid = $details['item_ipfs']; return [$name, $series, $cid]; } } diff --git a/src/Routes/RemainingRoutes.php b/src/Routes/RemainingRoutes.php index 83a3087..c0adda2 100644 --- a/src/Routes/RemainingRoutes.php +++ b/src/Routes/RemainingRoutes.php @@ -222,10 +222,8 @@ final class RemainingRoutes $name = self::assetName(Request::queryString('name', 15)); $series = Request::queryInt('series', 0, 0, 4_294_967_295); JsonResponse::success( - ['name' => $name, 'series' => $series] - + RpcReplyDecoder::jsonTextOrRaw( + RpcReplyDecoder::nftDetails( $application->client->nftDetails($name, $series), - 'NFT details', ), ); }); diff --git a/src/Rpc/RpcReplyDecoder.php b/src/Rpc/RpcReplyDecoder.php index e1ad5f2..bcb86e6 100644 --- a/src/Rpc/RpcReplyDecoder.php +++ b/src/Rpc/RpcReplyDecoder.php @@ -232,6 +232,112 @@ final class RpcReplyDecoder return $nfts; } + /** + * @return array{ + * nft_name: string, + * series: int, + * asset_name: string, + * genesis_txid: string, + * creator: string, + * item_ipfs: string, + * metadata_uri: string, + * current_holder: string|null, + * history_count: int, + * history: list> + * } + */ + public static function nftDetails(string $reply): array + { + self::notNodeError($reply); + $fixedBytes = 15 + 4 + 32 + 22 + 100 + 22 + 4; + $historyBytes = 109; + if (strlen($reply) < $fixedBytes) { + throw new RuntimeException('The node returned truncated NFT details.'); + } + + $offset = 0; + $name = rtrim(substr($reply, $offset, 15), "\0 "); + $offset += 15; + $series = self::u32($reply, $offset); + $genesisTxid = bin2hex(substr($reply, $offset, 32)); + $offset += 32; + $creator = self::walletAddress(substr($reply, $offset, 22)); + $offset += 22; + $cid = rtrim(substr($reply, $offset, 100), "\0 "); + $offset += 100; + $holderBytes = substr($reply, $offset, 22); + $holder = $holderBytes === str_repeat("\0", 22) + ? null + : self::walletAddress($holderBytes); + $offset += 22; + $historyCount = self::u32($reply, $offset); + + $expected = $fixedBytes + ($historyCount * $historyBytes); + if (strlen($reply) !== $expected) { + throw new RuntimeException('The node returned invalid NFT history data.'); + } + + $actions = [ + 1 => 'Create', + 2 => 'Transfer', + 3 => 'Swap', + 4 => 'Loan Locked', + 5 => 'Loan Payment', + 6 => 'Collateral Claimed', + 7 => 'Loan Issued', + 8 => 'Burned', + ]; + $history = []; + for ($index = 0; $index < $historyCount; $index++) { + $txid = bin2hex(substr($reply, $offset, 32)); + $offset += 32; + $block = self::u32($reply, $offset); + $transactionType = ord($reply[$offset++]); + $actionType = ord($reply[$offset++]); + $fromBytes = substr($reply, $offset, 22); + $offset += 22; + $toBytes = substr($reply, $offset, 22); + $offset += 22; + $receivedAsset = rtrim(substr($reply, $offset, 15), "\0 "); + $offset += 15; + $receivedSeries = self::u32($reply, $offset); + $receivedValue = self::u64($reply, $offset); + $history[] = [ + 'txid' => $txid, + 'block' => $block, + 'transaction_type' => $transactionType, + 'action' => $actions[$actionType] ?? 'Unknown', + 'from' => $fromBytes === str_repeat("\0", 22) + ? null + : self::walletAddress($fromBytes), + 'to' => $toBytes === str_repeat("\0", 22) + ? null + : self::walletAddress($toBytes), + 'received_asset' => $receivedAsset, + 'received_series' => $receivedSeries, + 'received_value_atomic' => $receivedValue, + 'received_value' => self::decimal($receivedValue), + ]; + } + + $metadataUri = $series > 0 && !str_ends_with($cid, '.json') + ? 'ipfs://' . rtrim($cid, '/') . '/metadata/' . $series . '.json' + : 'ipfs://' . $cid; + + return [ + 'nft_name' => $name, + 'series' => $series, + 'asset_name' => $series > 0 ? $name . '_' . $series : $name, + 'genesis_txid' => $genesisTxid, + 'creator' => $creator, + 'item_ipfs' => $cid, + 'metadata_uri' => $metadataUri, + 'current_holder' => $holder, + 'history_count' => $historyCount, + 'history' => $history, + ]; + } + /** * @return array{ * total_bytes: int,