Skip to content

Commit

Permalink
feat: return hts token address for new fungible token (#3305)
Browse files Browse the repository at this point in the history
* test poc

Signed-off-by: Konstantina Blazhukova <[email protected]>

* Simplify and improve redadbility of code

Signed-off-by: Konstantina Blazhukova <[email protected]>

* adds V1 function selector for createFungibleToken

Signed-off-by: Konstantina Blazhukova <[email protected]>

* Adds initial test

Signed-off-by: Konstantina Blazhukova <[email protected]>

* Adds function signatures for other functions

Signed-off-by: Konstantina Blazhukova <[email protected]>

* removes unecessary if

Signed-off-by: Konstantina Blazhukova <[email protected]>

* Adds acceptance tests

Signed-off-by: Konstantina Blazhukova <[email protected]>

* removes merge conflict marker and adds jsdoc

Signed-off-by: Konstantina Blazhukova <[email protected]>

* Improves code efficiency and readability

Signed-off-by: Konstantina Blazhukova <[email protected]>

* improves readability in tests

Signed-off-by: Konstantina Blazhukova <[email protected]>

* fixes error

Signed-off-by: Konstantina Blazhukova <[email protected]>

* Improves tests

Signed-off-by: Konstantina Blazhukova <[email protected]>

* Removes unused imports

Signed-off-by: Konstantina Blazhukova <[email protected]>

* adds clarification to new method

Signed-off-by: Konstantina Blazhukova <[email protected]>

---------

Signed-off-by: Konstantina Blazhukova <[email protected]>
  • Loading branch information
konstantinabl authored Dec 9, 2024
1 parent 66836b8 commit 28f9b33
Show file tree
Hide file tree
Showing 3 changed files with 232 additions and 22 deletions.
28 changes: 28 additions & 0 deletions packages/relay/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down
26 changes: 25 additions & 1 deletion packages/relay/src/lib/eth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2332,14 +2332,15 @@ export class EthImpl implements Eth {
});
});

const contractAddress = this.getContractAddressFromReceipt(receiptResponse);
const receipt: ITransactionReceipt = {
blockHash: toHash32(receiptResponse.block_hash),
blockNumber: numberTo0x(receiptResponse.block_number),
from: await this.resolveEvmAddress(receiptResponse.from, requestDetails),
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),
Expand Down Expand Up @@ -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<string> {
const block = await this.getBlockByHash(blockHash, false, requestDetails);
const timestampDecimal = parseInt(block ? block.timestamp : '0', 16);
Expand Down
200 changes: 179 additions & 21 deletions packages/server/tests/acceptance/htsPrecompile/precompileCalls.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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[] = [];
Expand All @@ -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,
Expand All @@ -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);
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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';
Expand Down

0 comments on commit 28f9b33

Please sign in to comment.