Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: getnftinfo will return its metadata and creation time (#198) #225

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 30 additions & 13 deletions contracts/HtsSystemContract.sol
Original file line number Diff line number Diff line change
Expand Up @@ -412,19 +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;
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);

return (responseCode, nonFungibleTokenInfo);
return IHederaTokenService(token).getNonFungibleTokenInfo(
token,
serialNumber
);
}

function isToken(address token) htsCall external returns (int64, bool) {
Expand Down Expand Up @@ -593,6 +584,19 @@ 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 nonFungibleTokenInfo;
(int64 creationTime, bytes memory metadata) = __nftInfo(serialId);
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");
address from = address(bytes20(msg.data[40:60]));
Expand Down Expand Up @@ -788,6 +792,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;
Expand Down Expand Up @@ -825,6 +835,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) }
Expand Down
11 changes: 11 additions & 0 deletions contracts/HtsSystemContractJson.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
8 changes: 8 additions & 0 deletions contracts/MirrorNode.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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")) {
Expand Down
35 changes: 34 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
5 changes: 3 additions & 2 deletions src/slotmap.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
'',
Expand Down
7 changes: 7 additions & 0 deletions test/HTS.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down