Skip to content

Commit

Permalink
feat: [Foundry] implement nft support in hts emulation (#172)
Browse files Browse the repository at this point in the history
Signed-off-by: Mariusz Jasuwienas <[email protected]>
  • Loading branch information
arianejasuwienas authored Jan 8, 2025
1 parent f228465 commit baf5283
Show file tree
Hide file tree
Showing 28 changed files with 414 additions and 119 deletions.
5 changes: 3 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ jobs:
run: echo "./test/scripts" >> "$GITHUB_PATH"

- name: Run Forge tests using JSON-RPC Mainnet and Testnet servers (with forking)
run: forge test -vvv --fork-url https://testnet.hashio.io/api --fork-block-number 8535327
run: forge test -vvv --fork-url https://testnet.hashio.io/api --fork-block-number 13890397

- name: Show Mocked curl Requests
run: cat test/scripts/curl.log
Expand Down Expand Up @@ -201,7 +201,8 @@ jobs:
run: node test/scripts/json-rpc-mock.js &

- name: Run Forge tests using JSON-RPC mock server for storage emulation (with forking)
run: forge test -vvv --fork-url http://localhost:7546 --no-storage-caching
# Remove no-match-contract when the support for ERC721 is implemented in the JSON RPC mock server (#80 task)
run: forge test -vvv --fork-url http://localhost:7546 --no-storage-caching --no-match-contract ^ERC721TokenTest$

foundry-example-tests:
name: 'Foundry example tests w/mainnet'
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,7 @@ We provide a [`curl`](./test/scripts/curl) mock in the `test/scripts` folder to
By modifiying the `PATH` environment variable, the mocked `curl` is used instead.
```console
PATH=./test/scripts:$PATH forge test --fork-url https://testnet.hashio.io/api --fork-block-number 8535327
PATH=./test/scripts:$PATH forge test --fork-url https://testnet.hashio.io/api --fork-block-number 13890397
```
In case needed, there is a trace log of requests made by mocked `curl` in `scripts/curl.log`
Expand Down
71 changes: 71 additions & 0 deletions contracts/Base64.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// SPDX-License-Identifier: Apache-2.0

pragma solidity >=0.6.0;

bytes constant TABLE_DECODE = hex"0000000000000000000000000000000000000000000000000000000000000000"
hex"00000000000000000000003e0000003f3435363738393a3b3c3d000000000000"
hex"00000102030405060708090a0b0c0d0e0f101112131415161718190000000000"
hex"001a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132330000000000";

function decode(string memory _data) pure returns (bytes memory) {
bytes memory data = bytes(_data);

if (data.length == 0) return new bytes(0);
require(data.length % 4 == 0, "invalid base64 decoder input");

// load the table into memory
bytes memory table = TABLE_DECODE;

// every 4 characters represent 3 bytes
uint256 decodedLen = (data.length / 4) * 3;

// add some extra buffer at the end required for the writing
bytes memory result = new bytes(decodedLen + 32);

assembly {
// padding with '='
let lastBytes := mload(add(data, mload(data)))
if eq(and(lastBytes, 0xFF), 0x3d) {
decodedLen := sub(decodedLen, 1)
if eq(and(lastBytes, 0xFFFF), 0x3d3d) {
decodedLen := sub(decodedLen, 1)
}
}

// set the actual output length
mstore(result, decodedLen)

// prepare the lookup table
let tablePtr := add(table, 1)

// input ptr
let dataPtr := data
let endPtr := add(dataPtr, mload(data))

// result ptr, jump over length
let resultPtr := add(result, 32)

// run over the input, 4 characters at a time
for {} lt(dataPtr, endPtr) {}
{
// read 4 characters
dataPtr := add(dataPtr, 4)
let input := mload(dataPtr)

// write 3 bytes
let output := add(
add(
shl(18, and(mload(add(tablePtr, and(shr(24, input), 0xFF))), 0xFF)),
shl(12, and(mload(add(tablePtr, and(shr(16, input), 0xFF))), 0xFF))),
add(
shl( 6, and(mload(add(tablePtr, and(shr( 8, input), 0xFF))), 0xFF)),
and(mload(add(tablePtr, and( input , 0xFF))), 0xFF)
)
)
mstore(resultPtr, shl(232, output))
resultPtr := add(resultPtr, 3)
}
}

return result;
}
38 changes: 38 additions & 0 deletions contracts/HtsSystemContractJson.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity ^0.8.0;

import {Vm} from "forge-std/Vm.sol";
import {decode} from './Base64.sol';
import {HtsSystemContract, HTS_ADDRESS} from "./HtsSystemContract.sol";
import {IERC20} from "./IERC20.sol";
import {MirrorNode} from "./MirrorNode.sol";
Expand Down Expand Up @@ -427,6 +428,43 @@ contract HtsSystemContractJson is HtsSystemContract {
return slot;
}

function _tokenUriSlot(uint32 serialId) internal override virtual returns (bytes32) {
bytes32 slot = super._tokenUriSlot(serialId);
if (vm.load(_scratchAddr(), slot) == bytes32(0)) {
string memory metadata = mirrorNode().getNftMetadata(address(this), serialId);
string memory uri = string(decode(metadata));
storeString(address(this), uint256(slot), uri);
}
return slot;
}

function _ownerOfSlot(uint32 serialId) internal override virtual returns (bytes32) {
bytes32 slot = super._ownerOfSlot(serialId);
if (vm.load(_scratchAddr(), slot) == bytes32(0)) {
address owner = mirrorNode().getNftOwner(address(this), serialId);
_setValue(slot, bytes32(uint256(uint160(owner))));
}
return slot;
}

function _getApprovedSlot(uint32 serialId) internal override virtual returns (bytes32) {
bytes32 slot = super._getApprovedSlot(serialId);
if (vm.load(_scratchAddr(), slot) == bytes32(0)) {
address approved = mirrorNode().getNftSpender(address(this), serialId);
_setValue(slot, bytes32(uint256(uint160(approved))));
}
return slot;
}

function _isApprovedForAllSlot(address owner, address operator) internal override virtual returns (bytes32) {
bytes32 slot = super._isApprovedForAllSlot(owner, operator);
if (vm.load(_scratchAddr(), slot) == bytes32(0)) {
bool approved = mirrorNode().isApprovedForAll(address(this), owner, operator);
_setValue(slot, bytes32(uint256(approved ? 1 : 0)));
}
return slot;
}

function _setValue(bytes32 slot, bytes32 value) private {
vm.store(address(this), slot, value);
vm.store(_scratchAddr(), slot, bytes32(uint(1)));
Expand Down
12 changes: 12 additions & 0 deletions contracts/IMirrorNodeResponses.sol
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,16 @@ interface IMirrorNodeResponses {
string freeze_status;
string kyc_status;
}

struct NonFungibleToken {
string account_id;
string created_timestamp;
string delegating_spender;
bool deleted;
string metadata;
string modified_timestamp;
uint32 serial_number;
string spender;
string token_id;
}
}
64 changes: 50 additions & 14 deletions contracts/MirrorNode.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,57 @@ abstract contract MirrorNode {

function fetchAllowance(address token, uint32 ownerNum, uint32 spenderNum) external virtual returns (string memory json);

function fetchNftAllowance(address token, uint32 ownerNum, uint32 operatorNum) external virtual returns (string memory json);

function fetchAccount(string memory account) external virtual returns (string memory json);

function fetchTokenRelationshipOfAccount(string memory account, address token) external virtual returns (string memory json);

function fetchNonFungibleToken(address token, uint32 serial) external virtual returns (string memory json);

function getNftMetadata(address token, uint32 serial) external returns (string memory) {
string memory json = this.fetchNonFungibleToken(token, serial);
if (vm.keyExistsJson(json, ".metadata")) {
return vm.parseJsonString(json, ".metadata");
}
return "";
}

function getNftOwner(address token, uint32 serial) external returns (address) {
string memory json = this.fetchNonFungibleToken(token, serial);
if (vm.keyExistsJson(json, ".account_id")) {
string memory owner = vm.parseJsonString(json, ".account_id");
return getAccountAddress(owner);
}
return address(0);
}

function getNftSpender(address token, uint32 serial) external returns (address) {
string memory json = this.fetchNonFungibleToken(token, serial);
if (vm.keyExistsJson(json, ".spender")) {
string memory spender = vm.parseJsonString(json, ".spender");
return getAccountAddress(spender);
}
return address(0);
}

function isApprovedForAll(address token, address owner, address operator) external returns (bool) {
uint32 ownerNum = _getAccountNum(owner);
if (ownerNum == 0) return false;
uint32 operatorNum = _getAccountNum(operator);
if (operatorNum == 0) return false;
string memory json = this.fetchNftAllowance(token, ownerNum, operatorNum);
return vm.keyExistsJson(json, ".allowances[0].approved_for_all")
&& vm.parseJsonBool(json, ".allowances[0].approved_for_all");
}

function getBalance(address token, address account) external returns (uint256) {
uint32 accountNum = _getAccountNum(account);
if (accountNum == 0) return 0;

try this.fetchBalance(token, accountNum) returns (string memory json) {
if (vm.keyExistsJson(json, ".balances[0].balance")) {
return vm.parseJsonUint(json, ".balances[0].balance");
}
} catch {}
string memory json = this.fetchBalance(token, accountNum);
if (vm.keyExistsJson(json, ".balances[0].balance")) {
return vm.parseJsonUint(json, ".balances[0].balance");
}
return 0;
}

Expand All @@ -45,12 +83,10 @@ abstract contract MirrorNode {
if (ownerNum == 0) return 0;
uint32 spenderNum = _getAccountNum(spender);
if (spenderNum == 0) return 0;

try this.fetchAllowance(token, ownerNum, spenderNum) returns (string memory json) {
if (vm.keyExistsJson(json, ".allowances[0].amount")) {
return vm.parseJsonUint(json, ".allowances[0].amount");
}
} catch {}
string memory json = this.fetchAllowance(token, ownerNum, spenderNum);
if (vm.keyExistsJson(json, ".allowances[0].amount")) {
return vm.parseJsonUint(json, ".allowances[0].amount");
}
return 0;
}

Expand All @@ -65,9 +101,9 @@ abstract contract MirrorNode {
return false;
}

function getAccountAddress(string memory accountId) external returns (address) {
function getAccountAddress(string memory accountId) public returns (address) {
if (bytes(accountId).length == 0
|| keccak256(bytes(accountId)) == keccak256(bytes("null"))
|| keccak256(bytes(accountId)) == keccak256(bytes("null"))
|| keccak256(abi.encodePacked(accountId)) == keccak256(abi.encodePacked(bytes32(0)))) {
return address(0);
}
Expand Down
20 changes: 20 additions & 0 deletions contracts/MirrorNodeFFI.sol
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ contract MirrorNodeFFI is MirrorNode {
));
}

function fetchNftAllowance(address token, uint32 ownerNum, uint32 operatorNum) isValid(token) external override returns (string memory) {
return _get(string.concat(
"accounts/0.0.",
vm.toString(ownerNum),
"/allowances/nfts?token.id=0.0.",
vm.toString(uint160(token)),
"&account.id=0.0.",
vm.toString(operatorNum)
));
}

function fetchAccount(string memory idOrAliasOrEvmAddress) external override returns (string memory) {
return _get(string.concat(
"accounts/",
Expand All @@ -69,6 +80,15 @@ contract MirrorNodeFFI is MirrorNode {
));
}

function fetchNonFungibleToken(address token, uint32 serial) isValid(token) external override returns (string memory) {
return _get(string.concat(
"tokens/0.0.",
vm.toString(uint160(token)),
"/nfts/",
vm.toString(serial)
));
}

/**
* @dev Returns the block information by given number.
*
Expand Down
Loading

0 comments on commit baf5283

Please sign in to comment.