diff --git a/packages/relay/src/lib/constants.ts b/packages/relay/src/lib/constants.ts index 26b0ac072b..504c17d262 100644 --- a/packages/relay/src/lib/constants.ts +++ b/packages/relay/src/lib/constants.ts @@ -69,6 +69,20 @@ export enum CallType { CALL = 'CALL', } +// HTS create function selectors taken from https://github.com/hashgraph/hedera-smart-contracts/tree/main/contracts/system-contracts/hedera-token-service +const CREATE_NON_FUNGIBLE_TOKEN_FUNCTION_SELECTOR_V1 = '0x9dc711e0'; +const CREATE_NON_FUNGIBLE_TOKEN_FUNCTION_SELECTOR_V2 = '0x9c89bb35'; +const CREATE_NON_FUNGIBLE_TOKEN_FUNCTION_SELECTOR_V3 = '0xea83f293'; +const CREATE_NON_FUNGIBLE_WITH_CUSTOM_FEES_TOKEN_FUNCTION_SELECTOR_V1 = '0x5bc7c0e6'; +const CREATE_NON_FUNGIBLE_WITH_CUSTOM_FEES_TOKEN_FUNCTION_SELECTOR_V2 = '0x45733969'; +const CREATE_NON_FUNGIBLE_WITH_CUSTOM_FEES_TOKEN_FUNCTION_SELECTOR_V3 = '0xabb54eb5'; +const CREATE_FUNGIBLE_TOKEN_FUNCTION_SELECTOR_V1 = '0x27d97be3'; +const CREATE_FUNGIBLE_TOKEN_FUNCTION_SELECTOR_V2 = '0xc23baeb6'; +const CREATE_FUNGIBLE_TOKEN_FUNCTION_SELECTOR_V3 = '0x0fb65bf3'; +const CREATE_FUNGIBLE_TOKEN_WITH_CUSTOM_FEES_FUNCTION_SELECTOR_V1 = '0xef2d1098'; +const CREATE_FUNGIBLE_TOKEN_WITH_CUSTOM_FEES_FUNCTION_SELECTOR_V2 = '0xb937581a'; +const CREATE_FUNGIBLE_TOKEN_WITH_CUSTOM_FEES_FUNCTION_SELECTOR_V3 = '0x2af0c59a'; + export default { HBAR_TO_TINYBAR_COEF: 100_000_000, TINYBAR_TO_WEIBAR_COEF: 10_000_000_000, @@ -210,6 +224,20 @@ export default { DEFAULT_ROOT_HASH: '0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421', MASKED_IP_ADDRESS: 'xxx.xxx.xxx.xxx', + HTS_CREATE_FUNCTIONS_SELECTORS: [ + CREATE_FUNGIBLE_TOKEN_FUNCTION_SELECTOR_V1, + CREATE_FUNGIBLE_TOKEN_FUNCTION_SELECTOR_V2, + CREATE_FUNGIBLE_TOKEN_FUNCTION_SELECTOR_V3, + CREATE_FUNGIBLE_TOKEN_WITH_CUSTOM_FEES_FUNCTION_SELECTOR_V1, + CREATE_FUNGIBLE_TOKEN_WITH_CUSTOM_FEES_FUNCTION_SELECTOR_V2, + CREATE_FUNGIBLE_TOKEN_WITH_CUSTOM_FEES_FUNCTION_SELECTOR_V3, + CREATE_NON_FUNGIBLE_TOKEN_FUNCTION_SELECTOR_V1, + CREATE_NON_FUNGIBLE_TOKEN_FUNCTION_SELECTOR_V2, + CREATE_NON_FUNGIBLE_TOKEN_FUNCTION_SELECTOR_V3, + CREATE_NON_FUNGIBLE_WITH_CUSTOM_FEES_TOKEN_FUNCTION_SELECTOR_V1, + CREATE_NON_FUNGIBLE_WITH_CUSTOM_FEES_TOKEN_FUNCTION_SELECTOR_V2, + CREATE_NON_FUNGIBLE_WITH_CUSTOM_FEES_TOKEN_FUNCTION_SELECTOR_V3, + ], // The fee is calculated via the fee calculator: https://docs.hedera.com/hedera/networks/mainnet/fees // The maximum fileAppendChunkSize is currently set to 5KB by default; therefore, the estimated fees for FileCreate below are based on a file size of 5KB. diff --git a/packages/relay/src/lib/eth.ts b/packages/relay/src/lib/eth.ts index 337e7bf3a9..ccb4180a69 100644 --- a/packages/relay/src/lib/eth.ts +++ b/packages/relay/src/lib/eth.ts @@ -2332,6 +2332,7 @@ export class EthImpl implements Eth { }); }); + const contractAddress = this.getContractAddressFromReceipt(receiptResponse); const receipt: ITransactionReceipt = { blockHash: toHash32(receiptResponse.block_hash), blockNumber: numberTo0x(receiptResponse.block_number), @@ -2339,7 +2340,7 @@ export class EthImpl implements Eth { to: await this.resolveEvmAddress(receiptResponse.to, requestDetails), cumulativeGasUsed: numberTo0x(receiptResponse.block_gas_used), gasUsed: nanOrNumberTo0x(receiptResponse.gas_used), - contractAddress: receiptResponse.address, + contractAddress: contractAddress, logs: logs, logsBloom: receiptResponse.bloom === EthImpl.emptyHex ? EthImpl.emptyBloom : receiptResponse.bloom, transactionHash: toHash32(receiptResponse.hash), @@ -2371,6 +2372,29 @@ export class EthImpl implements Eth { } } + /** + * This method retrieves the contract address from the receipt response. + * If the contract creation is via a system contract, it handles the system contract creation. + * If not, it returns the address from the receipt response. + * + * @param {any} receiptResponse - The receipt response object. + * @returns {string} The contract address. + */ + private getContractAddressFromReceipt(receiptResponse: any): string { + const isCreationViaSystemContract = constants.HTS_CREATE_FUNCTIONS_SELECTORS.includes( + receiptResponse.function_parameters.substring(0, constants.FUNCTION_SELECTOR_CHAR_LENGTH), + ); + + if (!isCreationViaSystemContract) { + return receiptResponse.address; + } + + // Handle system contract creation + // reason for substring from the 90th character is described in the design doc in this repo: docs/design/hts_address_tx_receipt.md + const tokenAddress = receiptResponse.call_result.substring(90); + return prepend0x(tokenAddress); + } + private async getCurrentGasPriceForBlock(blockHash: string, requestDetails: RequestDetails): Promise { const block = await this.getBlockByHash(blockHash, false, requestDetails); const timestampDecimal = parseInt(block ? block.timestamp : '0', 16); diff --git a/packages/server/tests/acceptance/htsPrecompile/precompileCalls.spec.ts b/packages/server/tests/acceptance/htsPrecompile/precompileCalls.spec.ts index de4441b695..bf6c040f75 100644 --- a/packages/server/tests/acceptance/htsPrecompile/precompileCalls.spec.ts +++ b/packages/server/tests/acceptance/htsPrecompile/precompileCalls.spec.ts @@ -19,32 +19,44 @@ */ // external resources -import { solidity } from 'ethereum-waffle'; -import chai, { expect } from 'chai'; +import { numberTo0x } from '@hashgraph/json-rpc-relay/dist/formatters'; +import { predefined } from '@hashgraph/json-rpc-relay/dist/lib/errors/JsonRpcError'; import { ContractId } from '@hashgraph/sdk'; -//Constants are imported with different definitions for better readability in the code. -import Constants from '../../helpers/constants'; -import RelayCall from '../../helpers/constants'; - -import { AliasAccount } from '../../types/AliasAccount'; +import chai, { expect } from 'chai'; +import { solidity } from 'ethereum-waffle'; import { ethers } from 'ethers'; -import IERC20MetadataJson from '../../contracts/openzeppelin/IERC20Metadata.json'; + +import MirrorClient from '../../clients/mirrorClient'; +import RelayClient from '../../clients/relayClient'; +import ServicesClient from '../../clients/servicesClient'; +import HederaTokenServiceImplJson from '../../contracts/HederaTokenServiceImpl.json'; +import IHederaTokenServiceJson from '../../contracts/IHederaTokenService.json'; import IERC20Json from '../../contracts/openzeppelin/IERC20.json'; -import IERC721MetadataJson from '../../contracts/openzeppelin/IERC721Metadata.json'; -import IERC721EnumerableJson from '../../contracts/openzeppelin/IERC721Enumerable.json'; +import IERC20MetadataJson from '../../contracts/openzeppelin/IERC20Metadata.json'; import IERC721Json from '../../contracts/openzeppelin/IERC721.json'; -import IHederaTokenServiceJson from '../../contracts/IHederaTokenService.json'; -import HederaTokenServiceImplJson from '../../contracts/HederaTokenServiceImpl.json'; +import IERC721EnumerableJson from '../../contracts/openzeppelin/IERC721Enumerable.json'; +import IERC721MetadataJson from '../../contracts/openzeppelin/IERC721Metadata.json'; import TokenManagementContractJson from '../../contracts/TokenManagementContract.json'; - -import { predefined } from '@hashgraph/json-rpc-relay/dist/lib/errors/JsonRpcError'; +//Constants are imported with different definitions for better readability in the code. +import Constants from '../../helpers/constants'; +import RelayCall from '../../helpers/constants'; import { Utils } from '../../helpers/utils'; -import { numberTo0x } from '@hashgraph/json-rpc-relay/dist/formatters'; +import { AliasAccount } from '../../types/AliasAccount'; chai.use(solidity); describe('@precompile-calls Tests for eth_call with HTS', async function () { this.timeout(240 * 1000); // 240 seconds - const { servicesNode, mirrorNode, relay }: any = global; + + //@ts-ignore + const { + servicesNode, + mirrorNode, + relay, + }: { + servicesNode: ServicesClient; + mirrorNode: MirrorClient; + relay: RelayClient; + } = global; const TX_SUCCESS_CODE = BigInt(22); @@ -58,6 +70,7 @@ describe('@precompile-calls Tests for eth_call with HTS', async function () { const NFT_METADATA = 'ABCDE'; const ZERO_HEX = '0x0000000000000000000000000000000000000000'; + const HTS_SYTEM_CONTRACT_ADDRESS = '0x0000000000000000000000000000000000000167'; const EMPTY_HEX = '0x'; const accounts: AliasAccount[] = []; @@ -68,9 +81,9 @@ describe('@precompile-calls Tests for eth_call with HTS', async function () { IERC721Metadata, IERC721Enumerable, IERC721, - IHederaTokenService, TokenManager, - TokenManagementSigner; + TokenManagementSigner, + IHederaTokenService; let nftSerial, tokenAddress, nftAddress, @@ -87,11 +100,14 @@ describe('@precompile-calls Tests for eth_call with HTS', async function () { tokenAddressAllFees, nftAddressRoyaltyFees, tokenAddresses, - nftAddresses; + nftAddresses, + createTokenCost; before(async () => { requestId = Utils.generateRequestId(); + const hbarToWeibar = 100_000_000; + createTokenCost = 35 * Constants.TINYBAR_TO_WEIBAR_COEF * hbarToWeibar; // create accounts const initialAccount: AliasAccount = global.accounts[0]; const contractDeployer = await Utils.createAliasAccount(mirrorNode, initialAccount, requestId); @@ -198,7 +214,7 @@ describe('@precompile-calls Tests for eth_call with HTS', async function () { const mintResult1 = await servicesNode.mintNFT({ ...mintArgs, tokenId: nftTokenId1 }); // associate tokens, grant KYC - for (let account of [accounts[1], accounts[2]]) { + for (const account of [accounts[1], accounts[2]]) { await servicesNode.associateHTSToken( account.accountId, htsResult1.receipt.tokenId, @@ -526,7 +542,6 @@ describe('@precompile-calls Tests for eth_call with HTS', async function () { 'token with a fractional fee', 'token with all custom fees', ]; - const nftTests = ['nft with no custom fees', 'nft with a royalty fee']; }); //TODO After adding the additional expects after getTokenKeyPublic in tokenManagementContract, the whole describe can be deleted. -> https://github.com/hashgraph/hedera-json-rpc-relay/issues/1131 @@ -603,6 +618,149 @@ describe('@precompile-calls Tests for eth_call with HTS', async function () { }); }); + describe('Create HTS token via direct call to Hedera Token service', async () => { + let myNFT, myImmutableFungibleToken, fixedFee; + + before(async () => { + const compressedPublicKey = accounts[0].wallet.signingKey.compressedPublicKey.replace('0x', ''); + const supplyKey = { + inheritAccountKey: false, + contractId: ethers.ZeroAddress, + ed25519: '0x', + ECDSA_secp256k1: Buffer.from(compressedPublicKey, 'hex'), + delegatableContractId: ethers.ZeroAddress, + }; + + myNFT = { + name: NFT_NAME, + symbol: NFT_SYMBOL, + treasury: accounts[0].wallet.address, + memo: 'NFT memo', + tokenSupplyType: true, // true for finite, false for infinite + maxSupply: 1000000, + freezeDefault: false, // true to freeze by default, false to not freeze by default + tokenKeys: [[16, supplyKey]], // No keys. The token is immutable + expiry: { + second: 0, + autoRenewAccount: accounts[0].wallet.address, + autoRenewPeriod: 8000000, + }, + }; + + myImmutableFungibleToken = { + name: 'myImmutableFungibleToken', + symbol: 'MIFT', + treasury: accounts[0].wallet.address, // The key for this address must sign the transaction or be the caller + memo: 'This is an immutable fungible token created via the HTS system contract', + tokenSupplyType: true, // true for finite, false for infinite + maxSupply: 1000000, + freezeDefault: false, // true to freeze by default, false to not freeze by default + tokenKeys: [], // No keys. The token is immutable + expiry: { + second: 0, + autoRenewAccount: accounts[0].wallet.address, + autoRenewPeriod: 8000000, + }, + }; + + fixedFee = [ + { + amount: 10, + tokenId: ethers.ZeroAddress, + useHbarsForPayment: true, + useCurrentTokenForPayment: false, + feeCollector: accounts[0].wallet.address, + }, + ]; + }); + + async function getTokenInfoFromMirrorNode(transactionHash: string) { + setTimeout(() => { + console.log('waiting for mirror node...'); + }, 1000); + const tokenInfo = await mirrorNode.get(`contracts/results/${transactionHash}`); + return tokenInfo; + } + + it('calls createFungibleToken', async () => { + const contract = new ethers.Contract(HTS_SYTEM_CONTRACT_ADDRESS, IHederaTokenServiceJson.abi, accounts[0].wallet); + const tx = await contract.createFungibleToken(myImmutableFungibleToken, 100, 18, { + value: BigInt(createTokenCost), + gasLimit: 10_000_000, + }); + const receipt = await tx.wait(); + + const tokenInfo = await getTokenInfoFromMirrorNode(receipt.hash); + const tokenAddress = receipt.contractAddress.toLowerCase(); + + expect(tokenAddress).to.not.equal(HTS_SYTEM_CONTRACT_ADDRESS); + expect(tokenAddress).to.equal(`0x${tokenInfo.call_result.substring(90).toLowerCase()}`); + }); + + it('calls createFungibleToken with custom fees', async () => { + const fractionalFee = []; + const contract = new ethers.Contract(HTS_SYTEM_CONTRACT_ADDRESS, IHederaTokenServiceJson.abi, accounts[0].wallet); + const tx = await contract.createFungibleTokenWithCustomFees( + myImmutableFungibleToken, + 100, + 18, + fixedFee, + fractionalFee, + { + value: BigInt(createTokenCost), + gasLimit: 10_000_000, + }, + ); + const receipt = await tx.wait(); + + const tokenInfo = await getTokenInfoFromMirrorNode(receipt.hash); + const tokenAddress = receipt.contractAddress.toLowerCase(); + + expect(tokenAddress).to.not.equal(HTS_SYTEM_CONTRACT_ADDRESS); + expect(tokenAddress).to.equal(`0x${tokenInfo.call_result.substring(90).toLowerCase()}`); + }); + + it('calls createNonFungibleToken', async () => { + const contract = new ethers.Contract(HTS_SYTEM_CONTRACT_ADDRESS, IHederaTokenServiceJson.abi, accounts[0].wallet); + const tx = await contract.createNonFungibleToken(myNFT, { + value: BigInt(createTokenCost), + gasLimit: 10_000_000, + }); + const receipt = await tx.wait(); + + const tokenInfo = await getTokenInfoFromMirrorNode(receipt.hash); + const tokenAddress = receipt.contractAddress.toLowerCase(); + + expect(tokenAddress).to.not.equal(HTS_SYTEM_CONTRACT_ADDRESS); + expect(tokenAddress).to.equal(`0x${tokenInfo.call_result.substring(90).toLowerCase()}`); + }); + + it('calls createNonFungibleToken with fees', async () => { + const royaltyFee = [ + { + numerator: 10, + denominator: 100, + amount: 10 * 100000000, + tokenId: ethers.ZeroAddress, + useHbarsForPayment: true, + feeCollector: accounts[1].wallet.address, + }, + ]; + const contract = new ethers.Contract(HTS_SYTEM_CONTRACT_ADDRESS, IHederaTokenServiceJson.abi, accounts[0].wallet); + const tx = await contract.createNonFungibleTokenWithCustomFees(myNFT, fixedFee, royaltyFee, { + value: BigInt(createTokenCost), + gasLimit: 10_000_000, + }); + const receipt = await tx.wait(); + + const tokenInfo = await getTokenInfoFromMirrorNode(receipt.hash); + const tokenAddress = receipt.contractAddress.toLowerCase(); + + expect(tokenAddress).to.not.equal(HTS_SYTEM_CONTRACT_ADDRESS); + expect(tokenAddress).to.equal(`0x${tokenInfo.call_result.substring(90).toLowerCase()}`); + }); + }); + //Relay test, move to the acceptance tests. Check if there are existing similar tests. describe('Negative tests', async () => { const CALLDATA_BALANCE_OF = '0x70a08231';