From 012735bbafc35bf00fe29603648139e9738e57c7 Mon Sep 17 00:00:00 2001 From: Mariusz Jasuwienas Date: Thu, 30 Jan 2025 13:58:08 +0100 Subject: [PATCH 1/4] feat: getnftinfo will return its metadata and creation time (#198) Signed-off-by: Mariusz Jasuwienas --- contracts/HtsSystemContract.sol | 37 ++++++++++++++++++++++------- contracts/HtsSystemContractJson.sol | 11 +++++++++ contracts/MirrorNode.sol | 8 +++++++ test/HTS.t.sol | 7 ++++++ 4 files changed, 55 insertions(+), 8 deletions(-) diff --git a/contracts/HtsSystemContract.sol b/contracts/HtsSystemContract.sol index 4a897b8b..8208cad9 100644 --- a/contracts/HtsSystemContract.sol +++ b/contracts/HtsSystemContract.sol @@ -414,16 +414,15 @@ contract HtsSystemContract is IHederaTokenService { returns (int64, NonFungibleTokenInfo memory) { (int64 responseCode, TokenInfo memory tokenInfo) = getTokenInfo(token); require(responseCode == HederaResponseCodes.SUCCESS, "getNonFungibleTokenInfo: failed to get token data"); - NonFungibleTokenInfo memory nonFungibleTokenInfo; + (, NonFungibleTokenInfo memory nonFungibleTokenInfo) = IHederaTokenService(token).getNonFungibleTokenInfo( + token, + serialNumber + ); nonFungibleTokenInfo.tokenInfo = tokenInfo; nonFungibleTokenInfo.serialNumber = serialNumber; - nonFungibleTokenInfo.spenderId = IERC721(token).getApproved(uint256(uint64(serialNumber))); - nonFungibleTokenInfo.ownerId = IERC721(token).ownerOf(uint256(uint64(serialNumber))); - - // ToDo: - // nonFungibleTokenInfo.metadata = bytes(IERC721(token).tokenURI(uint256(uint64(serialNumber)))); - // nonFungibleTokenInfo.creationTime = int64(0); - + uint256 serial = uint256(uint64(serialNumber)); + nonFungibleTokenInfo.spenderId = IERC721(token).getApproved(serial); + nonFungibleTokenInfo.ownerId = IERC721(token).ownerOf(serial); return (responseCode, nonFungibleTokenInfo); } @@ -593,6 +592,15 @@ contract HtsSystemContract is IHederaTokenService { _setApprovalForAll(from, to, approved); return abi.encode(true); } + if (selector == this.getNonFungibleTokenInfo.selector) { + require(msg.data.length >= 92, "getNonFungibleTokenInfo: Not enough calldata"); + uint256 serialId = uint256(bytes32(msg.data[60:92])); + NonFungibleTokenInfo memory info; + (int64 creationTime, bytes memory metadata) = __nftInfo(serialId); + info.creationTime = creationTime; + info.metadata = metadata; + return abi.encode(HederaResponseCodes.SUCCESS, info); + } if (selector == this._update.selector) { require(msg.data.length >= 124, "update: Not enough calldata"); address from = address(bytes20(msg.data[40:60])); @@ -788,6 +796,12 @@ contract HtsSystemContract is IHederaTokenService { return bytes32(abi.encodePacked(selector, pad, serialId)); } + function __nftInfoSlot(uint32 serialId) internal virtual returns (bytes32) { + bytes4 selector = IHederaTokenService.getNonFungibleTokenInfo.selector; + uint192 pad = 0x0; + return bytes32(abi.encodePacked(selector, pad, serialId)); + } + function _ownerOfSlot(uint32 serialId) internal virtual returns (bytes32) { bytes4 selector = IERC721.ownerOf.selector; uint192 pad = 0x0; @@ -825,6 +839,13 @@ contract HtsSystemContract is IHederaTokenService { uri = _uri; } + function __nftInfo(uint256 serialId) private returns (int64, bytes memory) { + bytes32 slot = __nftInfoSlot(uint32(serialId)); + bytes storage _nftInfo; + assembly { _nftInfo.slot := slot } + return abi.decode(_nftInfo, (int64, bytes)); + } + function __ownerOf(uint256 serialId) private returns (address owner) { bytes32 slot = _ownerOfSlot(uint32(serialId)); assembly { owner := sload(slot) } diff --git a/contracts/HtsSystemContractJson.sol b/contracts/HtsSystemContractJson.sol index 2b6b59e0..e1e6c74b 100644 --- a/contracts/HtsSystemContractJson.sol +++ b/contracts/HtsSystemContractJson.sol @@ -463,6 +463,17 @@ contract HtsSystemContractJson is HtsSystemContract { return slot; } + function __nftInfoSlot(uint32 serialId) internal override virtual returns (bytes32) { + bytes32 slot = super.__nftInfoSlot(serialId); + if (_shouldFetch(slot)) { + string memory metadata = mirrorNode().getNftMetadata(address(this), serialId); + string memory createdTimestamp = mirrorNode().getNftCreatedTimestamp(address(this), serialId); + int64 creationTime = int64(vm.parseInt(vm.split(createdTimestamp, ".")[0])); + storeBytes(address(this), uint256(slot), abi.encode(creationTime, bytes(metadata))); + } + return slot; + } + function _ownerOfSlot(uint32 serialId) internal override virtual returns (bytes32) { bytes32 slot = super._ownerOfSlot(serialId); if (_shouldFetch(slot)) { diff --git a/contracts/MirrorNode.sol b/contracts/MirrorNode.sol index 8aef3422..f4616077 100644 --- a/contracts/MirrorNode.sol +++ b/contracts/MirrorNode.sol @@ -40,6 +40,14 @@ abstract contract MirrorNode { return ""; } + function getNftCreatedTimestamp(address token, uint32 serial) external returns (string memory) { + string memory json = this.fetchNonFungibleToken(token, serial); + if (vm.keyExistsJson(json, ".created_timestamp")) { + return vm.parseJsonString(json, ".created_timestamp"); + } + return ""; + } + function getNftOwner(address token, uint32 serial) external returns (address) { string memory json = this.fetchNonFungibleToken(token, serial); if (vm.keyExistsJson(json, ".account_id")) { diff --git a/test/HTS.t.sol b/test/HTS.t.sol index 12597d5c..bade938a 100644 --- a/test/HTS.t.sol +++ b/test/HTS.t.sol @@ -628,6 +628,13 @@ contract HTSTest is Test, TestSetup { assertEq(nonFungibleTokenInfo.tokenInfo.fractionalFees.length, 0); assertEq(nonFungibleTokenInfo.tokenInfo.royaltyFees.length, 0); assertEq(nonFungibleTokenInfo.tokenInfo.ledgerId, testMode == TestMode.FFI ? "0x01" : "0x00"); + + // Additional information, not a part of the IERC721 interface. + assertEq( + string(nonFungibleTokenInfo.metadata), + "aHR0cHM6Ly92ZXJ5LWxvbmctc3RyaW5nLXdoaWNoLWV4Y2VlZHMtMzEtYnl0ZXMtYW5kLXJlcXVpcmVzLW11bHRpcGxlLXN0b3JhZ2Utc2xvdHMuY29tLzE=" + ); + assertEq(nonFungibleTokenInfo.creationTime, 1734948254); } function test_HTS_transferToken() external { From bb0b6e64e25ad1146c05ef7cc9cd034d0d1a4f42 Mon Sep 17 00:00:00 2001 From: Mariusz Jasuwienas Date: Thu, 30 Jan 2025 16:20:29 +0100 Subject: [PATCH 2/4] feat: nft creation time and metadata support in hardhat (#198) Signed-off-by: Mariusz Jasuwienas --- src/index.js | 35 ++++++++++++++++++++++++++++++++++- src/slotmap.js | 5 +++-- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/index.js b/src/index.js index afee3601..10dfc3f5 100644 --- a/src/index.js +++ b/src/index.js @@ -244,7 +244,40 @@ async function getHtsStorageAt(address, requestedSlot, blockNumber, mirrorNodeCl ZERO_HEX_32_BYTE, `Failed to get the metadata of the NFT ${tokenId}#${serialId}` ); - persistentStorage.store(tokenId, blockNumber, nrequestedSlot, atob(metadata)); + persistentStorage.store( + tokenId, + blockNumber, + nrequestedSlot, + atob(metadata), + 't_string_storage' + ); + } + // Encoded `address(tokenId).getNonFungibleTokenInfo(token,serialId)` slot + // slot(256) = `getNonFungibleTokenInfo`selector(32) + padding(192) + serialId(32) + if ( + nrequestedSlot >> 32n === + 0x287e1da8_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000n + ) { + const serialId = parseInt(requestedSlot.slice(-8), 16); + const { metadata, created_timestamp } = (await mirrorNodeClient.getNftByTokenIdAndSerial( + tokenId, + serialId, + blockNumber + )) ?? { + metadata: null, + created_timestamp: null, + }; + if (typeof metadata !== 'string' || typeof created_timestamp !== 'string') + return ret( + ZERO_HEX_32_BYTE, + `Failed to get the metadata of the NFT ${tokenId}#${serialId}` + ); + const timestamp = Number(created_timestamp.split('.')[0]).toString(16).padStart(64, '0'); + const metadataEncoded = Buffer.from(metadata).toString('hex'); + const metadataLength = metadata.length.toString(16).padStart(64, '0'); + const stringTypeIndicator = '40'.padStart(64, '0'); + const bytes = `${timestamp}${stringTypeIndicator}${metadataLength}${metadataEncoded}`; + persistentStorage.store(tokenId, blockNumber, nrequestedSlot, bytes, 't_bytes_storage'); } let unresolvedValues = persistentStorage.load(tokenId, blockNumber, nrequestedSlot); if (unresolvedValues === undefined) { diff --git a/src/slotmap.js b/src/slotmap.js index 2752f5ce..59e1b021 100644 --- a/src/slotmap.js +++ b/src/slotmap.js @@ -316,10 +316,11 @@ class PersistentStorageMap { * @param {number} blockNumber * @param {bigint} slot * @param {Value} value + * @param {string} type */ - store(tokenId, blockNumber, slot, value) { + store(tokenId, blockNumber, slot, value, type) { visit( - { label: 'value', slot: slot.toString(), type: 't_string_storage', offset: 0 }, + { label: 'value', slot: slot.toString(), type, offset: 0 }, 0n, { value }, '', From 4b2d7e40760a76d9cc05cc88444e006bdb518bef Mon Sep 17 00:00:00 2001 From: Mariusz Jasuwienas Date: Fri, 31 Jan 2025 09:05:24 +0100 Subject: [PATCH 3/4] feat: reverting not needed change (#198) Signed-off-by: Mariusz Jasuwienas --- contracts/HtsSystemContract.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/HtsSystemContract.sol b/contracts/HtsSystemContract.sol index 8208cad9..31aba1f9 100644 --- a/contracts/HtsSystemContract.sol +++ b/contracts/HtsSystemContract.sol @@ -420,9 +420,9 @@ contract HtsSystemContract is IHederaTokenService { ); nonFungibleTokenInfo.tokenInfo = tokenInfo; nonFungibleTokenInfo.serialNumber = serialNumber; - uint256 serial = uint256(uint64(serialNumber)); - nonFungibleTokenInfo.spenderId = IERC721(token).getApproved(serial); - nonFungibleTokenInfo.ownerId = IERC721(token).ownerOf(serial); + nonFungibleTokenInfo.spenderId = IERC721(token).getApproved(uint256(uint64(serialNumber))); + nonFungibleTokenInfo.ownerId = IERC721(token).ownerOf(uint256(uint64(serialNumber))); + return (responseCode, nonFungibleTokenInfo); } From 5e7bb9e02c65f9d87d1799375504fea3f1c96be0 Mon Sep 17 00:00:00 2001 From: Mariusz Jasuwienas Date: Fri, 31 Jan 2025 09:14:09 +0100 Subject: [PATCH 4/4] feat: reduce number of token'redirectfortoken calls to one (#198) Signed-off-by: Mariusz Jasuwienas --- contracts/HtsSystemContract.sol | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/contracts/HtsSystemContract.sol b/contracts/HtsSystemContract.sol index 31aba1f9..a507f404 100644 --- a/contracts/HtsSystemContract.sol +++ b/contracts/HtsSystemContract.sol @@ -412,18 +412,10 @@ contract HtsSystemContract is IHederaTokenService { function getNonFungibleTokenInfo(address token, int64 serialNumber) htsCall external returns (int64, NonFungibleTokenInfo memory) { - (int64 responseCode, TokenInfo memory tokenInfo) = getTokenInfo(token); - require(responseCode == HederaResponseCodes.SUCCESS, "getNonFungibleTokenInfo: failed to get token data"); - (, NonFungibleTokenInfo memory nonFungibleTokenInfo) = IHederaTokenService(token).getNonFungibleTokenInfo( + return IHederaTokenService(token).getNonFungibleTokenInfo( token, serialNumber ); - nonFungibleTokenInfo.tokenInfo = tokenInfo; - nonFungibleTokenInfo.serialNumber = serialNumber; - nonFungibleTokenInfo.spenderId = IERC721(token).getApproved(uint256(uint64(serialNumber))); - nonFungibleTokenInfo.ownerId = IERC721(token).ownerOf(uint256(uint64(serialNumber))); - - return (responseCode, nonFungibleTokenInfo); } function isToken(address token) htsCall external returns (int64, bool) { @@ -595,11 +587,15 @@ contract HtsSystemContract is IHederaTokenService { if (selector == this.getNonFungibleTokenInfo.selector) { require(msg.data.length >= 92, "getNonFungibleTokenInfo: Not enough calldata"); uint256 serialId = uint256(bytes32(msg.data[60:92])); - NonFungibleTokenInfo memory info; + NonFungibleTokenInfo memory nonFungibleTokenInfo; (int64 creationTime, bytes memory metadata) = __nftInfo(serialId); - info.creationTime = creationTime; - info.metadata = metadata; - return abi.encode(HederaResponseCodes.SUCCESS, info); + nonFungibleTokenInfo.creationTime = creationTime; + nonFungibleTokenInfo.metadata = metadata; + nonFungibleTokenInfo.tokenInfo = _tokenInfo; + nonFungibleTokenInfo.serialNumber = int64(int256(serialId)); + nonFungibleTokenInfo.spenderId = __getApproved(serialId); + nonFungibleTokenInfo.ownerId = __ownerOf(serialId); + return abi.encode(HederaResponseCodes.SUCCESS, nonFungibleTokenInfo); } if (selector == this._update.selector) { require(msg.data.length >= 124, "update: Not enough calldata");