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: example usage in nft testing of our foundry library and hardhat plugin (#191) #209

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
20 changes: 17 additions & 3 deletions contracts/HtsSystemContractJson.sol
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ contract HtsSystemContractJson is HtsSystemContract {
tokenInfo.token = _getHederaToken(json);
tokenInfo.fixedFees = _getFixedFees(json);
tokenInfo.fractionalFees = _getFractionalFees(json);
tokenInfo.royaltyFees = _getRoyaltyFees(json);
tokenInfo.royaltyFees = _getRoyaltyFees(_sanitizeFeesStructure(json));
tokenInfo.ledgerId = _getLedgerId();
tokenInfo.defaultKycStatus = false; // not available in the fetched JSON from mirror node
tokenInfo.totalSupply = int64(vm.parseInt(vm.parseJsonString(json, ".total_supply")));
Expand All @@ -201,6 +201,16 @@ contract HtsSystemContractJson is HtsSystemContract {
return tokenInfo;
}

// In order to properly decode the bytes returned by the parseJson into the Solidity Structure, the full,
// correct structure has to be provided in the input json, with all of the corresponding fields.
function _sanitizeFeesStructure(string memory json) private pure returns (string memory) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fallback_fee is nullable in the json response from the mirror node, but abi decode requires it to be set in order to create proper structure. That is why I set the default value here when it is empty.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, but also there is a vm.parseJson(json, ".custom_fees.royalty_fees") that guards against null, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@acuarica Only when royalty_fees is null. For the barkaneer I had fallback_fee = null (substructure in the royalty fees object).

return vm.replace(
json,
"\"fallback_fee\":null}",
"\"fallback_fee\":{\"amount\":0,\"denominating_token_id\":\"\"}}"
);
}

function _getHederaToken(string memory json) private returns (HederaToken memory token) {
token.tokenKeys = _getTokenKeys(json);
token.name = vm.parseJsonString(json, ".name");
Expand Down Expand Up @@ -369,7 +379,6 @@ contract HtsSystemContractJson is HtsSystemContract {
if (!vm.keyExistsJson(json, ".custom_fees.royalty_fees")) {
return new RoyaltyFee[](0);
}

try vm.parseJson(json, ".custom_fees.royalty_fees") returns (bytes memory royaltyFeesBytes) {
if (royaltyFeesBytes.length == 0) {
return new RoyaltyFee[](0);
Expand All @@ -379,11 +388,16 @@ contract HtsSystemContractJson is HtsSystemContract {
for (uint i = 0; i < fees.length; i++) {
string memory path = vm.replace(".custom_fees.royalty_fees[{i}]", "{i}", vm.toString(i));
address collectorAccount = mirrorNode().getAccountAddress(vm.parseJsonString(json, string.concat(path, ".collector_account_id")));
bytes memory denominatingTokenBytes = vm.parseJson(json, string.concat(path, ".denominating_token_id"));
address denominatingToken;
if (keccak256(denominatingTokenBytes) != keccak256("")) {
denominatingToken = mirrorNode().getAccountAddress(vm.parseJsonString(json, string.concat(path, ".denominating_token_id")));
}
royaltyFees[i] = RoyaltyFee(
int64(vm.parseJsonInt(json, string.concat(path, ".amount.numerator"))),
int64(vm.parseJsonInt(json, string.concat(path, ".amount.denominator"))),
int64(vm.parseJsonInt(json, string.concat(path, ".fallback_fee.amount"))),
mirrorNode().getAccountAddress(vm.parseJsonString(json, string.concat(path, ".denominating_token_id"))),
denominatingToken,
collectorAccount == address(0),
collectorAccount
);
Expand Down
33 changes: 33 additions & 0 deletions examples/foundry-hts/NFT.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.0;

import {Test} from "forge-std/Test.sol";
import {htsSetup} from "hedera-forking/contracts/htsSetup.sol";
import {IERC721} from "hedera-forking/contracts/IERC721.sol";

contract NFTExampleTest is Test {
// https://hashscan.io/mainnet/token/0.0.5083205
address NFT_mainnet = 0x00000000000000000000000000000000004d9045;

address private user;

function setUp() external {
htsSetup();

user = makeAddr("user");
dealERC721(NFT_mainnet, user, 3);
}

function test_get_owner_of_existing_account() view external {
address owner = IERC721(NFT_mainnet).ownerOf(1);
assertNotEq(IERC721(NFT_mainnet).ownerOf(1), address(0));

// The assertion below cannot be guaranteed, since we can only query the current owner of the NFT,
// Note that the ownership of the NFT may change over time.
assertEq(owner, 0x000000000000000000000000000000000006889a);
}

function test_dealt_nft_assigned_to_local_account() view external {
assertEq(IERC721(NFT_mainnet).ownerOf(3), user);
}
}
25 changes: 25 additions & 0 deletions examples/foundry-hts/NFTConsole.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.0;

import {Test, console} from "forge-std/Test.sol";
import {htsSetup} from "hedera-forking/contracts/htsSetup.sol";
import {IERC721} from "hedera-forking/contracts/IERC721.sol";

contract NFTConsoleExampleTest is Test {
function setUp() external {
htsSetup();
}

function test_using_console_log() view external {
// https://hashscan.io/mainnet/token/0.0.5083205
address NFT_mainnet = 0x00000000000000000000000000000000004d9045;

string memory name = IERC721(NFT_mainnet).name();
string memory symbol = IERC721(NFT_mainnet).symbol();
string memory tokenURI = IERC721(NFT_mainnet).tokenURI(1);
assertEq(name, "THE BARKANEERS");
assertEq(symbol, "BARKANEERS");
assertEq(tokenURI, "ipfs://bafkreif4hpsgflzzvd7c4abx5u5xwrrjl7wkimbjtndvkxodklxdam5upm");
console.log("name: %s, symbol: %s, tokenURI: %s", name, symbol, tokenURI);
}
}
19 changes: 19 additions & 0 deletions examples/hardhat-hts/contracts/CallNFT.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.0;

import {IERC721} from "./IERC721.sol";
import {console} from "hardhat/console.sol";

contract CallNFT {
function getTokenName(address tokenAddress) external view returns (string memory) {
return IERC721(tokenAddress).name();
}

function invokeTransferFrom(address tokenAddress, address to, uint256 serialId) external {
// You can use `console.log` as usual
// https://hardhat.org/tutorial/debugging-with-hardhat-network#solidity--console.log
console.log("Transferring from %s to %s %s tokens", msg.sender, to, serialId);
address owner = IERC721(tokenAddress).ownerOf(serialId);
IERC721(tokenAddress).transferFrom(owner, to, serialId);
}
}
103 changes: 103 additions & 0 deletions examples/hardhat-hts/contracts/IERC721.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.0;

/**
* See https://ethereum.org/en/developers/docs/standards/tokens/erc-721/#events for more information.
*/
interface IERC721Events {
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
}

/**
* This interface is used to get the selectors and for testing.
*
* https://hips.hedera.com/hip/hip-218
* https://hips.hedera.com/hip/hip-376
*/
interface IERC721 {
/**
* @dev Returns the token collection name.
*/
function name() external view returns (string memory);

/**
* @dev Returns the token collection symbol.
*/
function symbol() external view returns (string memory);

/**
* @dev Returns the Uniform Resource Identifier (URI) for `serialId` token.
*/
function tokenURI(uint256 serialId) external view returns (string memory);

/**
* @dev Returns the total amount of tokens stored by the contract.
*/
function totalSupply() external view returns (uint256);

/**
* @dev Returns the number of tokens in `owner`'s account.
*/
function balanceOf(address owner) external view returns (uint256 balance);

/**
* @dev Returns the owner of the `serialId` token.
*
* Requirements:
* - `serialId` must exist.
*/
function ownerOf(uint256 serialId) external view returns (address);

/**
* @dev Transfers `serialId` token from `sender` to `recipient`.
*
* Requirements:
* - `sender` cannot be the zero address.
* - `recipient` cannot be the zero address.
* - `serialId` token must be owned by `sender`.
* - If the caller is not `sender`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
*
* Emits a {Transfer} event.
*/
function transferFrom(address sender, address recipient, uint256 serialId) external payable;

/**
* @dev Gives permission to `spender` to transfer `serialId` token to another account.
* The approval is cleared when the token is transferred.
*
* Only a single account can be approved at a time, so approving the zero address clears previous approvals.
*
* Requirements:
* - The caller must own the token or be an approved operator.
* - `serialId` must exist.
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 serialId) external payable;

/**
* @dev Approve or remove `operator` as an operator for the caller.
* Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller.
*
* Requirements:
* - The `operator` cannot be the address zero.
*
* Emits an {ApprovalForAll} event.
*/
function setApprovalForAll(address operator, bool approved) external;

/**
* @dev Returns the account approved for `serialId` token.
*
* Requirements:
* - `serialId` must exist.
*/
function getApproved(uint256 serialId) external view returns (address operator);

/**
* @dev Returns if the `operator` is allowed to manage all of the assets of `owner`.
*
* See {setApprovalForAll}
*/
function isApprovedForAll(address owner, address operator) external view returns (bool);
}
4 changes: 2 additions & 2 deletions examples/hardhat-hts/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions examples/hardhat-hts/test/nft-info.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*-
* Hedera Hardhat Forking Plugin
*
* Copyright (C) 2024 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

const { expect } = require('chai');
// prettier-ignore
const { ethers: { getContractAt } } = require('hardhat');

describe('NFT example -- informational', function () {
it('should get name and symbol', async function () {
// https://hashscan.io/mainnet/token/0.0.4970613
const nft = await getContractAt('IERC721', '0x00000000000000000000000000000000004bd875');
expect(await nft['name']()).to.be.equal('Concierge Collectibles');
expect(await nft['symbol']()).to.be.equal('Concierge Collectibles');
});
});
31 changes: 31 additions & 0 deletions examples/hardhat-hts/test/nft-ownerof.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*-
* Hedera Hardhat Forking Plugin
*
* Copyright (C) 2024 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

const { expect } = require('chai');
// prettier-ignore
const { ethers: { getContractAt } } = require('hardhat');

describe('NFT example -- ownerOf', function () {
it('should get `ownerOf` account holder', async function () {
// https://hashscan.io/mainnet/token/0.0.4970613
const nft = await getContractAt('IERC721', '0x00000000000000000000000000000000004bd875');
expect(await nft['ownerOf'](1)).to.not.be.equal(
'0x0000000000000000000000000000000000000000'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we compare against the current owner?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we can. But we get nft details for the latest block, so all it will take for our tests then is someone who owns this serial id = 1 to transfer it to someone else. (I'll apply your suggestion anyway :) ).

);
});
});
47 changes: 47 additions & 0 deletions examples/hardhat-hts/test/nft-transfer.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*-
* Hedera Hardhat Forking Plugin
*
* Copyright (C) 2024 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

const { expect } = require('chai');
// prettier-ignore
const { ethers: { getSigner, getSigners, getContractAt }, network: { provider } } = require('hardhat');
const { loadFixture } = require('@nomicfoundation/hardhat-toolbox/network-helpers');

describe('NFT example -- transferFrom', function () {
async function id() {
return [(await getSigners())[0]];
}

it("should `transferFrom` tokens from account holder to one of Hardhat' signers", async function () {
const [receiver] = await loadFixture(id);

// https://hashscan.io/mainnet/token/0.0.4970613
const nft = await getContractAt('IERC721', '0x00000000000000000000000000000000004bd875');

const holderAddress = await nft['ownerOf'](1n);

await provider.request({
method: 'hardhat_impersonateAccount',
params: [holderAddress],
});

const holder = await getSigner(holderAddress);
await nft.connect(holder)['transferFrom'](holder, receiver, 1n);

expect(await nft['ownerOf'](1n)).to.be.equal(receiver.address);
});
});
Loading