From aff16b6a716d3c57da6933c3612c2983d390d785 Mon Sep 17 00:00:00 2001 From: Mariusz Jasuwienas Date: Fri, 7 Feb 2025 12:03:02 +0100 Subject: [PATCH] feat: returning non-success response code instead of reverting when supplykey is missing (#167) Signed-off-by: Mariusz Jasuwienas --- contracts/HederaResponseCodes.sol | 1 + contracts/HtsSystemContract.sol | 27 +++++- contracts/HtsSystemContractJson.sol | 8 +- contracts/MirrorNode.sol | 14 +++ contracts/MirrorNodeFFI.sol | 4 + src/forwarder/mirror-node-client.js | 10 +++ src/index.d.ts | 11 +++ src/index.js | 15 ++++ src/slotmap.js | 16 +++- test/HTS.t.sol | 90 +++++++++++++++---- ...32463407e283f602ce8ef2cbe324823561b6f.json | 83 +++++++++++++++++ test/getHtsStorageAt.test.js | 3 + test/lib/MirrorNodeMock.sol | 5 ++ test/scripts/curl | 3 + test/scripts/json-rpc-mock.js | 3 + 15 files changed, 270 insertions(+), 23 deletions(-) create mode 100644 test/data/getAccountsByPublicKey_0242b7c3beea2af6dfcc874c41d1332463407e283f602ce8ef2cbe324823561b6f.json diff --git a/contracts/HederaResponseCodes.sol b/contracts/HederaResponseCodes.sol index 53b8e4ac..a46b065e 100644 --- a/contracts/HederaResponseCodes.sol +++ b/contracts/HederaResponseCodes.sol @@ -3,4 +3,5 @@ pragma solidity ^0.8.0; library HederaResponseCodes { int32 internal constant SUCCESS = 22; // The transaction succeeded + int32 internal constant TOKEN_HAS_NO_SUPPLY_KEY = 180; } diff --git a/contracts/HtsSystemContract.sol b/contracts/HtsSystemContract.sol index 7f8f73ca..a3f8042f 100644 --- a/contracts/HtsSystemContract.sol +++ b/contracts/HtsSystemContract.sol @@ -55,7 +55,15 @@ contract HtsSystemContract is IHederaTokenService { assembly { accountId := sload(slot) } } - function mintToken(address token, int64 amount, bytes[] memory) htsCall external returns ( + function mintToken(address token, int64 amount, bytes[] memory data) htsCall external returns ( + int64 responseCode, + int64 newTotalSupply, + int64[] memory serialNumbers + ) { + return mintToken(token, amount, false); + } + + function mintToken(address token, int64 amount, bool ignoreSupplyKeyCheck) htsCall internal returns ( int64 responseCode, int64 newTotalSupply, int64[] memory serialNumbers @@ -64,6 +72,9 @@ contract HtsSystemContract is IHederaTokenService { require(amount > 0, "mintToken: invalid amount"); (int64 tokenInfoResponseCode, TokenInfo memory tokenInfo) = IHederaTokenService(token).getTokenInfo(token); + if (!ignoreSupplyKeyCheck && _extractKeyAddress(0x10, tokenInfo) == address(0)) { // 0x10 - supply key + return (HederaResponseCodes.TOKEN_HAS_NO_SUPPLY_KEY, tokenInfo.totalSupply, new int64[](0)); + } require(tokenInfoResponseCode == HederaResponseCodes.SUCCESS, "mintToken: failed to get token info"); address treasuryAccount = tokenInfo.token.treasury; @@ -85,6 +96,9 @@ contract HtsSystemContract is IHederaTokenService { require(amount > 0, "burnToken: invalid amount"); (int64 tokenInfoResponseCode, TokenInfo memory tokenInfo) = IHederaTokenService(token).getTokenInfo(token); + if (_extractKeyAddress(0x10, tokenInfo) == address(0)) { + return (HederaResponseCodes.TOKEN_HAS_NO_SUPPLY_KEY, tokenInfo.totalSupply); + } require(tokenInfoResponseCode == HederaResponseCodes.SUCCESS, "burnToken: failed to get token info"); address treasuryAccount = tokenInfo.token.treasury; @@ -188,7 +202,7 @@ contract HtsSystemContract is IHederaTokenService { deployHIP719Proxy(tokenAddress); if (initialTotalSupply > 0) { - this.mintToken(tokenAddress, initialTotalSupply, new bytes[](0)); + mintToken(tokenAddress, initialTotalSupply, true); } responseCode = HederaResponseCodes.SUCCESS; @@ -940,4 +954,13 @@ contract HtsSystemContract is IHederaTokenService { assembly { sstore(slot, approved) } emit IERC721.ApprovalForAll(sender, operator, approved); } + + function _extractKeyAddress(uint keyType, TokenInfo memory tokenInfo) private pure returns (address) { + for (uint256 i = 0; i < tokenInfo.token.tokenKeys.length; i++) { + if (tokenInfo.token.tokenKeys[i].keyType == keyType) { + return tokenInfo.token.tokenKeys[i].key.contractId; + } + } + return address(0); + } } diff --git a/contracts/HtsSystemContractJson.sol b/contracts/HtsSystemContractJson.sol index 1d8c3535..682be763 100644 --- a/contracts/HtsSystemContractJson.sol +++ b/contracts/HtsSystemContractJson.sol @@ -265,7 +265,7 @@ contract HtsSystemContractJson is HtsSystemContract { return token; } - function _getTokenKeys(string memory json) private pure returns (TokenKey[] memory tokenKeys) { + function _getTokenKeys(string memory json) private returns (TokenKey[] memory tokenKeys) { tokenKeys = new TokenKey[](7); try vm.parseJson(json, ".admin_key") returns (bytes memory keyBytes) { @@ -320,9 +320,8 @@ contract HtsSystemContractJson is HtsSystemContract { return tokenKeys; } - function _getTokenKey(IMirrorNodeResponses.Key memory key, uint8 keyType) internal pure returns (TokenKey memory) { + function _getTokenKey(IMirrorNodeResponses.Key memory key, uint8 keyType) internal returns (TokenKey memory) { bool inheritAccountKey = false; - address contractId = address(0); address delegatableContractId = address(0); bytes memory ed25519 = keccak256(bytes(key._type)) == keccak256(bytes("ED25519")) ? vm.parseBytes(key.key) @@ -330,6 +329,9 @@ contract HtsSystemContractJson is HtsSystemContract { bytes memory ECDSA_secp256k1 = keccak256(bytes(key._type)) == keccak256(bytes("ECDSA_SECP256K1")) ? vm.parseBytes(key.key) : new bytes(0); + address contractId = ed25519.length + ECDSA_secp256k1.length > 0 + ? mirrorNode().getAccountAddressByPublicKey(key.key) + : address(0); return TokenKey( keyType, KeyValue( diff --git a/contracts/MirrorNode.sol b/contracts/MirrorNode.sol index 8aef3422..c4a95c7b 100644 --- a/contracts/MirrorNode.sol +++ b/contracts/MirrorNode.sol @@ -28,6 +28,8 @@ abstract contract MirrorNode { function fetchAccount(string memory account) external virtual returns (string memory json); + function fetchAccountByPublicKey(string memory publicKey) 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); @@ -101,6 +103,18 @@ abstract contract MirrorNode { return false; } + function getAccountAddressByPublicKey(string memory publicKey) public returns (address) { + try this.fetchAccountByPublicKey(publicKey) returns (string memory json) { + if (vm.keyExistsJson(json, ".accounts[0].evm_address")) { + return vm.parseJsonAddress(json, ".accounts[0].evm_address"); + } + } catch { + // Do nothing + } + // generate a deterministic address based on the account public key as a fallback + return address(uint160(uint256(keccak256(bytes(publicKey))))); + } + function getAccountAddress(string memory accountId) public returns (address) { if (bytes(accountId).length == 0 || keccak256(bytes(accountId)) == keccak256(bytes("null")) diff --git a/contracts/MirrorNodeFFI.sol b/contracts/MirrorNodeFFI.sol index aebd1890..7fa887b2 100644 --- a/contracts/MirrorNodeFFI.sol +++ b/contracts/MirrorNodeFFI.sol @@ -71,6 +71,10 @@ contract MirrorNodeFFI is MirrorNode { )); } + function fetchAccountByPublicKey(string memory publicKey) external override returns (string memory) { + return _get(string.concat("accounts?limit=1&account.publickey=", publicKey)); + } + function fetchTokenRelationshipOfAccount(string memory idOrAliasOrEvmAddress, address token) external override returns (string memory) { return _get(string.concat( "accounts/", diff --git a/src/forwarder/mirror-node-client.js b/src/forwarder/mirror-node-client.js index 012788ef..3dbcbd10 100644 --- a/src/forwarder/mirror-node-client.js +++ b/src/forwarder/mirror-node-client.js @@ -137,6 +137,16 @@ class MirrorNodeClient { return this._get(`accounts/${idOrAliasOrEvmAddress}?transactions=false&${timestamp}`); } + /** + * Fetches accounts information by account public key. + * + * @param {string} publicKey The account public key to fetch. + * @returns {Promise<{ accounts: {evm_address: string}[] } | null>} A `Promise` resolving to the account information or `null` if not found. + */ + async getAccountsByPublicKey(publicKey) { + return this._get(`accounts?limit=1&account.publickey=${publicKey}`); + } + /** * @param {number} blockNumber * @returns {Promise<{timestamp: {to: string}} | null>} diff --git a/src/index.d.ts b/src/index.d.ts index cb626934..1bdb1605 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -149,6 +149,17 @@ interface IMirrorNodeClient { account: string; evm_address: string; } | null>; + + /** + * Get accounts by public key. + * + * This method should call the Mirror Node API endpoint `GET /api/v1/accounts?limit=1&account.publickey={publicKey}`. + * + * @param publicKey + */ + getAccountsByPublicKey(publicKey: string): Promise<{ + accounts: { evm_address: string }[]; + } | null>; } /** diff --git a/src/index.js b/src/index.js index afee3601..b77baf6c 100644 --- a/src/index.js +++ b/src/index.js @@ -250,6 +250,21 @@ async function getHtsStorageAt(address, requestedSlot, blockNumber, mirrorNodeCl if (unresolvedValues === undefined) { const token = await mirrorNodeClient.getTokenById(tokenId, blockNumber); if (token === null) return ret(ZERO_HEX_32_BYTE, `Token \`${tokenId}\` not found`); + + for (const key of ['admin', 'kyc', 'freeze', 'wipe', 'supply', 'fee_schedule', 'pause']) { + const value = /**@type{{contractId: string}}*/ (token[`${key}_key`]); + if (!value) { + continue; + } + assert(typeof value === 'object' && 'key' in value && typeof value.key === 'string'); + const result = await mirrorNodeClient.getAccountsByPublicKey(value.key); + if (result === undefined || result === null || result.accounts.length === 0) { + value.contractId = ZERO_HEX_32_BYTE; + continue; + } + value.contractId = result.accounts[0].evm_address; + } + unresolvedValues = slotMapOf(token).load(nrequestedSlot); if (unresolvedValues === undefined) diff --git a/src/slotmap.js b/src/slotmap.js index f0e16e4b..09d90bd0 100644 --- a/src/slotmap.js +++ b/src/slotmap.js @@ -241,13 +241,23 @@ function slotMapOf(token) { ['fee_schedule_key', 0x20], ['pause_key', 0x40], ]).map(([prop, key_type]) => { - const key = token[prop]; + const key = /**@type{{contractId: string}}*/ (token[prop]); if (key === null) return { key_type, ed25519: '', _e_c_d_s_a_secp256k1: '' }; assert(typeof key === 'object'); assert('_type' in key && 'key' in key); if (key._type === 'ED25519') - return { key_type, ed25519: key.key, _e_c_d_s_a_secp256k1: '' }; - return { key_type, ed25519: '', _e_c_d_s_a_secp256k1: key.key }; + return { + key_type, + contract_id: key.contractId, + ed25519: key.key, + _e_c_d_s_a_secp256k1: '', + }; + return { + key_type, + contract_id: key.contractId, + ed25519: '', + _e_c_d_s_a_secp256k1: key.key, + }; }); const customFees = /**@type {Record[]>}*/ ( token['custom_fees'] diff --git a/test/HTS.t.sol b/test/HTS.t.sol index 6ad0b25d..df0516f0 100644 --- a/test/HTS.t.sol +++ b/test/HTS.t.sol @@ -75,21 +75,21 @@ contract HTSTest is Test, TestSetup { // AdminKey assertEq(tokenInfo.token.tokenKeys[0].keyType, 0x1); assertEq(tokenInfo.token.tokenKeys[0].key.inheritAccountKey, false); - assertEq(tokenInfo.token.tokenKeys[0].key.contractId, address(0)); + assertEq(tokenInfo.token.tokenKeys[0].key.contractId, 0xF287CfB303322B6dCDA77ce0824Bd1a3fA5d0e47); assertEq(tokenInfo.token.tokenKeys[0].key.ed25519, hex"5db29fb3f19f8618cc4689cf13e78a935621845d67547719faf49f65d5c367cc"); assertEq(tokenInfo.token.tokenKeys[0].key.ECDSA_secp256k1, bytes("")); assertEq(tokenInfo.token.tokenKeys[0].key.delegatableContractId, address(0)); // FreezeKey assertEq(tokenInfo.token.tokenKeys[2].keyType, 0x4); assertEq(tokenInfo.token.tokenKeys[2].key.inheritAccountKey, false); - assertEq(tokenInfo.token.tokenKeys[2].key.contractId, address(0)); + assertEq(tokenInfo.token.tokenKeys[2].key.contractId, 0x6c273d045CC8219F98e7F5944B49Ae1e2fdA1449); assertEq(tokenInfo.token.tokenKeys[2].key.ed25519, hex"baa2dd1684d8445d41b22f2b2c913484a7d885cf25ce525f8bf3fe8d5c8cb85d"); assertEq(tokenInfo.token.tokenKeys[2].key.ECDSA_secp256k1, bytes("")); assertEq(tokenInfo.token.tokenKeys[2].key.delegatableContractId, address(0)); // SupplyKey assertEq(tokenInfo.token.tokenKeys[4].keyType, 0x10); assertEq(tokenInfo.token.tokenKeys[4].key.inheritAccountKey, false); - assertEq(tokenInfo.token.tokenKeys[4].key.contractId, address(0)); + assertEq(tokenInfo.token.tokenKeys[4].key.contractId, 0x4bc2a5E2099e0536C7B21610BD5A0E1EFbb032A0); assertEq(tokenInfo.token.tokenKeys[4].key.ed25519, hex"4e4658983980d1b25a634eeeb26cb2b0f0e2e9c83263ba5b056798d35f2139a8"); assertEq(tokenInfo.token.tokenKeys[4].key.ECDSA_secp256k1, bytes("")); assertEq(tokenInfo.token.tokenKeys[4].key.delegatableContractId, address(0)); @@ -218,6 +218,9 @@ contract HTSTest is Test, TestSetup { bytes[] memory metadata = new bytes[](0); int64 initialTotalSupply = 10000000005000000; IHederaTokenService.TokenInfo memory tokenInfo; + IHederaTokenService.TokenKey[] memory keys = new IHederaTokenService.TokenKey[](1); + keys[0].keyType = 0x10; // Supply key + keys[0].key.contractId = msg.sender; tokenInfo.token = IHederaTokenService.HederaToken( "USD Coin", "USDC", @@ -226,7 +229,7 @@ contract HTSTest is Test, TestSetup { false, initialTotalSupply + amount, false, - new IHederaTokenService.TokenKey[](0), + keys, IHederaTokenService.Expiry(0, address(0), 0) ); @@ -239,6 +242,33 @@ contract HTSTest is Test, TestSetup { IHederaTokenService(HTS_ADDRESS).mintToken(token, amount, metadata); } + function test_mintToken_should_fail_with_np_supplier() external { + address token = USDC; + int64 amount = 1000; + bytes[] memory metadata = new bytes[](0); + int64 initialTotalSupply = 10000000005000000; + IHederaTokenService.TokenInfo memory tokenInfo; + tokenInfo.token = IHederaTokenService.HederaToken( + "USD Coin", + "USDC", + address(0), + "USDC HBAR", + false, + initialTotalSupply + amount, + false, + new IHederaTokenService.TokenKey[](0), + IHederaTokenService.Expiry(0, address(0), 0) + ); + + vm.mockCall( + token, + abi.encode(IHederaTokenService.getTokenInfo.selector), + abi.encode(HederaResponseCodes.SUCCESS, tokenInfo) + ); + (int64 code, , ) = IHederaTokenService(HTS_ADDRESS).mintToken(token, amount, metadata); + assertEq(code, HederaResponseCodes.TOKEN_HAS_NO_SUPPLY_KEY); + } + function test_mintToken_should_revert_with_invalid_token() external { address token = address(0); int64 amount = 1000; @@ -258,10 +288,10 @@ contract HTSTest is Test, TestSetup { } function test_burnToken_should_succeed_with_valid_input() external { - address token = MFCT; - address treasury = MFCT_TREASURY; + address token = USDC; + address treasury = USDC_TREASURY; int64 amount = 1000; - int64 initialTotalSupply = 5000; + int64 initialTotalSupply = int64(int256(IERC20(token).totalSupply())); uint256 initialTreasuryBalance = IERC20(token).balanceOf(treasury); (int64 responseCodeMint, int64 newTotalSupplyAfterMint, int64[] memory serialNumbers) = IHederaTokenService(HTS_ADDRESS).mintToken(token, amount, new bytes[](0)); @@ -287,6 +317,9 @@ contract HTSTest is Test, TestSetup { int64 initialTotalSupply = 5000; int64[] memory serialNumbers = new int64[](0); IHederaTokenService.TokenInfo memory tokenInfo; + IHederaTokenService.TokenKey[] memory keys = new IHederaTokenService.TokenKey[](1); + keys[0].keyType = 0x10; // Supply key + keys[0].key.contractId = msg.sender; tokenInfo.token = IHederaTokenService.HederaToken( "My Crypto Token is the name which the string length is greater than 31", "Token symbol must be exactly 32!", @@ -295,7 +328,7 @@ contract HTSTest is Test, TestSetup { false, initialTotalSupply + amount, false, - new IHederaTokenService.TokenKey[](0), + keys, IHederaTokenService.Expiry(0, address(0), 0) ); @@ -308,6 +341,33 @@ contract HTSTest is Test, TestSetup { IHederaTokenService(HTS_ADDRESS).burnToken(token, amount, serialNumbers); } + function test_burnToken_should_revert_with_no_supplier() external { + address token = MFCT; + int64 amount = 1000; + int64 initialTotalSupply = 5000; + int64[] memory serialNumbers = new int64[](0); + IHederaTokenService.TokenInfo memory tokenInfo; + tokenInfo.token = IHederaTokenService.HederaToken( + "My Crypto Token is the name which the string length is greater than 31", + "Token symbol must be exactly 32!", + address(0), + "", + false, + initialTotalSupply + amount, + false, + new IHederaTokenService.TokenKey[](0), + IHederaTokenService.Expiry(0, address(0), 0) + ); + + vm.mockCall( + token, + abi.encode(IHederaTokenService.getTokenInfo.selector), + abi.encode(HederaResponseCodes.SUCCESS, tokenInfo) + ); + (int64 code, ) = IHederaTokenService(HTS_ADDRESS).burnToken(token, amount, serialNumbers); + assertEq(code, HederaResponseCodes.TOKEN_HAS_NO_SUPPLY_KEY); + } + function test_burnToken_should_revert_with_invalid_token() external { address token = address(0); int64 amount = 1000; @@ -428,7 +488,7 @@ contract HTSTest is Test, TestSetup { = IHederaTokenService(HTS_ADDRESS).getTokenKey(token, 0x1); assertEq(adminKeyStatusCode, HederaResponseCodes.SUCCESS); assertEq(adminKey.inheritAccountKey, false); - assertEq(adminKey.contractId, address(0)); + assertEq(adminKey.contractId, 0xF287CfB303322B6dCDA77ce0824Bd1a3fA5d0e47); assertEq(adminKey.ed25519, hex"5db29fb3f19f8618cc4689cf13e78a935621845d67547719faf49f65d5c367cc"); assertEq(adminKey.ECDSA_secp256k1, bytes("")); assertEq(adminKey.delegatableContractId, address(0)); @@ -437,7 +497,7 @@ contract HTSTest is Test, TestSetup { = IHederaTokenService(HTS_ADDRESS).getTokenKey(token, 0x4); assertEq(freezeKeyStatusCode, HederaResponseCodes.SUCCESS); assertEq(freezeKey.inheritAccountKey, false); - assertEq(freezeKey.contractId, address(0)); + assertEq(freezeKey.contractId, 0x6c273d045CC8219F98e7F5944B49Ae1e2fdA1449); assertEq(freezeKey.ed25519, hex"baa2dd1684d8445d41b22f2b2c913484a7d885cf25ce525f8bf3fe8d5c8cb85d"); assertEq(freezeKey.ECDSA_secp256k1, bytes("")); assertEq(freezeKey.delegatableContractId, address(0)); @@ -446,7 +506,7 @@ contract HTSTest is Test, TestSetup { = IHederaTokenService(HTS_ADDRESS).getTokenKey(token, 0x10); assertEq(supplyKeyStatusCode, HederaResponseCodes.SUCCESS); assertEq(supplyKey.inheritAccountKey, false); - assertEq(supplyKey.contractId, address(0)); + assertEq(supplyKey.contractId, 0x4bc2a5E2099e0536C7B21610BD5A0E1EFbb032A0); assertEq(supplyKey.ed25519, hex"4e4658983980d1b25a634eeeb26cb2b0f0e2e9c83263ba5b056798d35f2139a8"); assertEq(supplyKey.ECDSA_secp256k1, bytes("")); assertEq(supplyKey.delegatableContractId, address(0)); @@ -562,21 +622,21 @@ contract HTSTest is Test, TestSetup { // AdminKey assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[0].keyType, 0x1); assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[0].key.inheritAccountKey, false); - assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[0].key.contractId, address(0)); + assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[0].key.contractId, 0xF287CfB303322B6dCDA77ce0824Bd1a3fA5d0e47); assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[0].key.ed25519, hex"5db29fb3f19f8618cc4689cf13e78a935621845d67547719faf49f65d5c367cc"); assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[0].key.ECDSA_secp256k1, bytes("")); assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[0].key.delegatableContractId, address(0)); // FreezeKey assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[2].keyType, 0x4); assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[2].key.inheritAccountKey, false); - assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[2].key.contractId, address(0)); + assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[2].key.contractId, 0x6c273d045CC8219F98e7F5944B49Ae1e2fdA1449); assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[2].key.ed25519, hex"baa2dd1684d8445d41b22f2b2c913484a7d885cf25ce525f8bf3fe8d5c8cb85d"); assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[2].key.ECDSA_secp256k1, bytes("")); assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[2].key.delegatableContractId, address(0)); // SupplyKey assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[4].keyType, 0x10); assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[4].key.inheritAccountKey, false); - assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[4].key.contractId, address(0)); + assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[4].key.contractId, 0x4bc2a5E2099e0536C7B21610BD5A0E1EFbb032A0); assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[4].key.ed25519, hex"4e4658983980d1b25a634eeeb26cb2b0f0e2e9c83263ba5b056798d35f2139a8"); assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[4].key.ECDSA_secp256k1, bytes("")); assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[4].key.delegatableContractId, address(0)); @@ -613,7 +673,7 @@ contract HTSTest is Test, TestSetup { // AdminKey assertEq(nonFungibleTokenInfo.tokenInfo.token.tokenKeys[0].keyType, 0x1); assertEq(nonFungibleTokenInfo.tokenInfo.token.tokenKeys[0].key.inheritAccountKey, false); - assertEq(nonFungibleTokenInfo.tokenInfo.token.tokenKeys[0].key.contractId, address(0)); + assertEq(nonFungibleTokenInfo.tokenInfo.token.tokenKeys[0].key.contractId, 0x435d7D41D4f69F958bda7A8D9f549a0dD9B64c86); assertEq(nonFungibleTokenInfo.tokenInfo.token.tokenKeys[0].key.ed25519, bytes("")); assertEq(nonFungibleTokenInfo.tokenInfo.token.tokenKeys[0].key.ECDSA_secp256k1, hex"0242b7c3beea2af6dfcc874c41d1332463407e283f602ce8ef2cbe324823561b6f"); assertEq(nonFungibleTokenInfo.tokenInfo.token.tokenKeys[0].key.delegatableContractId, address(0)); diff --git a/test/data/getAccountsByPublicKey_0242b7c3beea2af6dfcc874c41d1332463407e283f602ce8ef2cbe324823561b6f.json b/test/data/getAccountsByPublicKey_0242b7c3beea2af6dfcc874c41d1332463407e283f602ce8ef2cbe324823561b6f.json new file mode 100644 index 00000000..f8fc66b5 --- /dev/null +++ b/test/data/getAccountsByPublicKey_0242b7c3beea2af6dfcc874c41d1332463407e283f602ce8ef2cbe324823561b6f.json @@ -0,0 +1,83 @@ +{ + "accounts": [ + { + "account": "0.0.4436613", + "alias": "HIQQEQVXYO7OUKXW37GIOTCB2EZSIY2APYUD6YBM5DXSZPRSJARVMG3P", + "auto_renew_period": 7776000, + "balance": { + "balance": 93759089270, + "timestamp": "1738160761.562934888", + "tokens": [ + { + "token_id": "0.0.4966953", + "balance": 1000 + }, + { + "token_id": "0.0.5308300", + "balance": 0 + }, + { + "token_id": "0.0.5308324", + "balance": 2 + }, + { + "token_id": "0.0.5308343", + "balance": 2 + }, + { + "token_id": "0.0.5308348", + "balance": 2 + }, + { + "token_id": "0.0.5308353", + "balance": 2 + }, + { + "token_id": "0.0.5308359", + "balance": 2 + }, + { + "token_id": "0.0.5308360", + "balance": 2 + }, + { + "token_id": "0.0.5308364", + "balance": 2 + }, + { + "token_id": "0.0.5308365", + "balance": 2 + }, + { + "token_id": "0.0.5308378", + "balance": 2 + }, + { + "token_id": "0.0.5308380", + "balance": 2 + } + ] + }, + "created_timestamp": "1718132880.546033646", + "decline_reward": false, + "deleted": false, + "ethereum_nonce": 62, + "evm_address": "0x435d7d41d4f69f958bda7a8d9f549a0dd9b64c86", + "expiry_timestamp": "1725908880.546033646", + "key": { + "_type": "ECDSA_SECP256K1", + "key": "0242b7c3beea2af6dfcc874c41d1332463407e283f602ce8ef2cbe324823561b6f" + }, + "max_automatic_token_associations": 0, + "memo": "auto-created account", + "pending_reward": 0, + "receiver_sig_required": false, + "staked_account_id": null, + "staked_node_id": null, + "stake_period_start": null + } + ], + "links": { + "next": "/api/v1/accounts?account.publickey=0242b7c3beea2af6dfcc874c41d1332463407e283f602ce8ef2cbe324823561b6f&limit=1&account.id=gt:0.0.4436613" + } +} diff --git a/test/getHtsStorageAt.test.js b/test/getHtsStorageAt.test.js index bf5cc316..db0369c7 100644 --- a/test/getHtsStorageAt.test.js +++ b/test/getHtsStorageAt.test.js @@ -54,6 +54,9 @@ describe('::getHtsStorageAt', function () { async getAccount(_idOrAliasOrEvmAddress) { throw Error('Not implemented'); }, + async getAccountsByPublicKey(_publicKey) { + return { accounts: [{ evm_address: '0x435d7d41d4f69f958bda7a8d9f549a0dd9b64c86' }] }; + }, async getBalanceOfToken(_tokenId, _accountId) { throw Error('Not implemented'); }, diff --git a/test/lib/MirrorNodeMock.sol b/test/lib/MirrorNodeMock.sol index c42e079b..e6a7f733 100644 --- a/test/lib/MirrorNodeMock.sol +++ b/test/lib/MirrorNodeMock.sol @@ -52,6 +52,11 @@ contract MirrorNodeMock is MirrorNode { return vm.readFile(path); } + function fetchAccountByPublicKey(string memory publicKey) external override returns (string memory) { + string memory path = string.concat("./test/data/getAccountsByPublicKey_", vm.toLowercase(publicKey), ".json"); + return vm.readFile(path); + } + function fetchTokenRelationshipOfAccount(string memory idOrAliasOrEvmAddress, address token) external override view returns (string memory) { string memory symbol = _symbolOf[token]; string memory path = string.concat("./test/data/", symbol, "/getTokenRelationship_", vm.toLowercase(idOrAliasOrEvmAddress), ".json"); diff --git a/test/scripts/curl b/test/scripts/curl index 76a5fa30..f446afe8 100755 --- a/test/scripts/curl +++ b/test/scripts/curl @@ -53,6 +53,9 @@ for (const [re, fn] of /** @type {const} */([ return require(`../data/${token.symbol}/getNonFungibleToken_${nftNumber}.json`); }], + [/^accounts\?limit=1&account\.publickey=([\w.]+)$/, publicKey => { + return require(`../data/getAccountsByPublicKey_${publicKey.toLowerCase()}.json`); + }], [/^accounts\/((0x[0-9a-fA-F]{40})|(0\.0\.\d+))$/, idOrAliasOrEvmAddress => { assert(typeof idOrAliasOrEvmAddress === 'string'); try { diff --git a/test/scripts/json-rpc-mock.js b/test/scripts/json-rpc-mock.js index 2142ac44..21c38469 100755 --- a/test/scripts/json-rpc-mock.js +++ b/test/scripts/json-rpc-mock.js @@ -112,6 +112,9 @@ const mirrorNodeClient = { } return requireOrDefault(`getAccount_${idOrAliasOrEvmAddress.toLowerCase()}.json`, null); }, + async getAccountsByPublicKey(publicKey) { + return requireOrDefault(`getAccountsByPublicKey_${publicKey.toLowerCase()}.json`, null); + }, async getBalanceOfToken(tokenId, accountId, blockNumber) { this.append('getBalanceOfToken', tokenId, accountId, blockNumber); const noBalance = { balances: [] };