From 884a1a8506409b8dca0474998833d54a17615c78 Mon Sep 17 00:00:00 2001 From: Rob Walworth Date: Fri, 13 Dec 2024 11:22:40 -0500 Subject: [PATCH 1/6] feat: add burnToken tests (WIP) Signed-off-by: Rob Walworth --- .prettierrc | 1 - .../token-service/TokenBurnTransaction.md | 2 +- src/services/ConsensusInfoClient.ts | 12 + src/services/MirrorNodeClient.ts | 12 + .../test-token-burn-transaction.ts | 453 ++++++++++++++++++ src/utils/helpers/key.ts | 33 +- src/utils/helpers/verify-token-tx.ts | 8 +- 7 files changed, 502 insertions(+), 19 deletions(-) create mode 100644 src/tests/token-service/test-token-burn-transaction.ts diff --git a/.prettierrc b/.prettierrc index 07a2cea..ac0fa89 100644 --- a/.prettierrc +++ b/.prettierrc @@ -9,4 +9,3 @@ "arrowParens": "always", "endOfLine": "lf" } - diff --git a/docs/test-specifications/token-service/TokenBurnTransaction.md b/docs/test-specifications/token-service/TokenBurnTransaction.md index 1e552af..e807f88 100644 --- a/docs/test-specifications/token-service/TokenBurnTransaction.md +++ b/docs/test-specifications/token-service/TokenBurnTransaction.md @@ -46,7 +46,7 @@ https://docs.hedera.com/hedera/sdks-and-apis/rest-api ### Additional Notes -The tests contained in this specification will assume that a valid fungible token and a valid non-fungible token have already successfully created. The fungible token will have an initial supply of 9,223,372,036,854,775,807 (int64 max) and the non-fungible token will have three minted. will denote the ID of the created fungible token, will denote the supply key of the created fungible token as a DER-encoded hex string, and will denote the admin key of the created fungible token as a DER-encoded hex string. will denote the ID of the created non-fungible token, will denote the supply key of the created non-fungible token as a DER-encoded hex string, and will denote the admin key of the created non-fungible token as a DER-encoded hex string. , , and will denote the serial numbers of the three minted NFTs. +The tests contained in this specification will assume that a valid fungible token and a valid non-fungible token have already successfully created. The fungible token will have an initial supply of 1,000,000 and the non-fungible token will have three minted. will denote the ID of the created fungible token, will denote the supply key of the created fungible token as a DER-encoded hex string, and will denote the admin key of the created fungible token as a DER-encoded hex string. will denote the ID of the created non-fungible token, will denote the supply key of the created non-fungible token as a DER-encoded hex string, and will denote the admin key of the created non-fungible token as a DER-encoded hex string. , , and will denote the serial numbers of the three minted NFTs. ## Property Tests diff --git a/src/services/ConsensusInfoClient.ts b/src/services/ConsensusInfoClient.ts index 25e65b1..57ea9d6 100644 --- a/src/services/ConsensusInfoClient.ts +++ b/src/services/ConsensusInfoClient.ts @@ -5,8 +5,11 @@ import { AccountInfo, AccountInfoQuery, Client, + NftId, TokenInfo, TokenInfoQuery, + TokenNftInfo, + TokenNftInfoQuery, } from "@hashgraph/sdk"; class ConsensusInfoClient { @@ -51,6 +54,15 @@ class ConsensusInfoClient { return this.executeTokenMethod(tokenId, new TokenInfoQuery()); } + async getTokenNftInfo( + tokenId: string, + serialNumber: string, + ): Promise { + let query = new TokenNftInfoQuery(); + query.setNftId(NftId.fromString(tokenId + "/" + serialNumber)); + return query.execute(this.sdkClient); + } + async executeAccountMethod( accountId: string, method: AccountInfoQuery | AccountBalanceQuery, diff --git a/src/services/MirrorNodeClient.ts b/src/services/MirrorNodeClient.ts index 75e459e..bba64c6 100644 --- a/src/services/MirrorNodeClient.ts +++ b/src/services/MirrorNodeClient.ts @@ -25,6 +25,18 @@ class MirrorNodeClient { const url = `${this.mirrorNodeRestUrl}/api/v1/tokens/${tokenId}`; return retryOnError(async () => fetchData(url)); } + + // TODO: Get mirror node interface with OpenAPI + async getTokenRelationships(accountId: string, tokenId: string) { + const url = `${this.mirrorNodeRestUrl}/api/v1/accounts/${accountId}/tokens?token.id=${tokenId}`; + return retryOnError(async () => fetchData(url)); + } + + // TODO: Get mirror node interface with OpenAPI + async getAccountNfts(accountId: string, tokenId: string) { + const url = `${this.mirrorNodeRestUrl}/api/v1/accounts/${accountId}/nfts?token.id=${tokenId}`; + return retryOnError(async () => fetchData(url)); + } } export default new MirrorNodeClient(); diff --git a/src/tests/token-service/test-token-burn-transaction.ts b/src/tests/token-service/test-token-burn-transaction.ts new file mode 100644 index 0000000..7e07101 --- /dev/null +++ b/src/tests/token-service/test-token-burn-transaction.ts @@ -0,0 +1,453 @@ +import { assert, expect } from "chai"; + +import { JSONRPCRequest } from "@services/Client"; +import consensusInfoClient from "@services/ConsensusInfoClient"; +import mirrorNodeClient from "@services/MirrorNodeClient"; + +import { setOperator } from "@helpers/setup-tests"; +import { getPrivateKey } from "@helpers/key"; +import { retryOnError } from "@helpers/retry-on-error"; + +/** + * Tests for TokenBurnTransaction + */ +describe("TokenBurnTransaction", function () { + // Tests should not take longer than 30 seconds to fully execute. + this.timeout(30000); + + beforeEach(async function () { + await setOperator( + this, + process.env.OPERATOR_ACCOUNT_ID as string, + process.env.OPERATOR_ACCOUNT_PRIVATE_KEY as string, + ); + }); + afterEach(async function () { + await JSONRPCRequest(this, "reset"); + }); + + const treasuryAccountId = process.env.OPERATOR_ACCOUNT_ID as string; + const fungibleInitialSupply = "9223372036854775807"; + const nonFungibleMetadata = ["1234", "5678", "90ab"]; + + // All tests require either a fungible token or NFT to be created, but not both. + // These functions should be called at the start of each test depending on what token is needed. + async function createToken( + mochaTestContext: any, + fungible: boolean, + supplyKey: string | null = null, + adminKey: string | null = null, + pauseKey: string | null = null, + decimals: number | null = null, + maxSupply: string | null = null, + freezeKey: string | null = null, + ): Promise { + let params: any = { + name: "testname", + symbol: "testsymbol", + treasuryAccountId, + }; + + if (fungible) { + params.initialSupply = fungibleInitialSupply; + params.decimals = 0; + params.tokenType = "ft"; + } else { + params.tokenType = "nft"; + } + + // Add the supply key if its provided. + if (supplyKey) { + params.supplyKey = supplyKey; + } + + // Add and sign with the admin key if its provided. + if (adminKey) { + params.adminKey = adminKey; + params.commonTransactionParams = { + signers: [adminKey], + }; + } + + // Add the pause key if its provided. + if (pauseKey) { + params.pauseKey = pauseKey; + } + + // Add the decimals if its provided. + if (decimals) { + params.decimals = decimals; + } + + // Add the max supply if its provided. + if (maxSupply) { + params.supplyType = "finite"; + params.maxSupply = maxSupply; + } + + // Add the freeze key if its provided. + if (freezeKey) { + params.freezeKey = freezeKey; + } + + const tokenId = ( + await JSONRPCRequest(mochaTestContext, "createToken", params) + ).tokenId; + + // If creating an NFT, mint three. + if (!fungible) { + await JSONRPCRequest(mochaTestContext, "mintToken", { + tokenId, + metadata: nonFungibleMetadata, + commonTransactionParams: { + signers: [supplyKey], + }, + }); + } + + return tokenId; + } + + async function verifyFungibleTokenBurn(tokenId: string, amount: string) { + const consensusNodeInfo = + await consensusInfoClient.getBalance(treasuryAccountId); + expect(Number(fungibleInitialSupply) - Number(amount)).to.equal( + consensusNodeInfo.tokens?.get(tokenId), + ); + + await retryOnError(async () => { + const mirrorNodeInfo = await mirrorNodeClient.getTokenRelationships( + treasuryAccountId, + tokenId, + ); + + let foundToken = false; + for (let i = 0; i < mirrorNodeInfo.tokens.length; i++) { + if (mirrorNodeInfo.tokens[i].token_id === tokenId) { + expect(mirrorNodeInfo.tokens[i].balance.toString()).to.equal(amount); + foundToken = true; + break; + } + } + + if (!foundToken) { + expect.fail("Token ID not found"); + } + }); + } + + async function verifyNonFungibleTokenBurn( + tokenId: string, + serialNumber: string, + ) { + // Query the consensus node. + const consensusNodeInfo = await consensusInfoClient.getTokenNftInfo( + tokenId, + serialNumber, + ); + let foundNft = false; + for (let i = 0; i < consensusNodeInfo.length; i++) { + if ( + consensusNodeInfo[i].nftId.tokenId.toString() === tokenId && + consensusNodeInfo[i].nftId.serial.toString() === serialNumber + ) { + foundNft = true; + break; + } + } + + // Make sure the NFT was not found. + expect(foundNft).to.be.false; + + // Query the mirror node. + await retryOnError(async () => { + const mirrorNodeInfo = await mirrorNodeClient.getAccountNfts( + treasuryAccountId, + tokenId, + ); + foundNft = false; + for (let i = 0; i < mirrorNodeInfo.nfts.length; i++) { + if ( + mirrorNodeInfo.nfts[i].token_id === tokenId && + mirrorNodeInfo.nfts[i].serial_number.toString() === serialNumber + ) { + foundNft = true; + break; + } + } + + // Make sure the NFT was not found. + expect(foundNft).to.be.false; + }); + } + + describe("Token ID", function () { + it("(#1) Burns a valid amount of fungible token", async function () { + const supplyKey = await getPrivateKey(this, "ed25519"); + const tokenId = await createToken(this, true, supplyKey); + + const amount = "10"; + expect( + ( + await JSONRPCRequest(this, "burnToken", { + tokenId, + amount, + commonTransactionParams: { + signers: [supplyKey], + }, + }) + ).newTotalSupply, + ).to.equal((Number(fungibleInitialSupply) - Number(amount)).toString()); + await verifyFungibleTokenBurn(tokenId, amount); + }); + + it("(#2) Burns a valid non-fungible token", async function () { + const supplyKey = await getPrivateKey(this, "ed25519"); + const tokenId = await createToken(this, false, supplyKey); + + const response = await JSONRPCRequest(this, "burnToken", { + tokenId, + metadata: [nonFungibleMetadata[0]], + commonTransactionParams: { + signers: [supplyKey], + }, + }); + + expect(response.newTotalSupply).to.equal( + (Number(nonFungibleMetadata.length) - 1).toString(), + ); + await verifyNonFungibleTokenBurn(tokenId, "1"); + }); + + it("(#3) Mints a token with an empty token ID", async function () { + try { + await JSONRPCRequest(this, "burnToken", { + tokenId: "", + }); + } catch (err: any) { + assert.equal(err.message, "Internal error"); + return; + } + + assert.fail("Should throw an error"); + }); + + it("(#4) Burns a token with no token ID", async function () { + try { + await JSONRPCRequest(this, "burnToken", {}); + } catch (err: any) { + assert.equal(err.data.status, "INVALID_TOKEN_ID"); + return; + } + + assert.fail("Should throw an error"); + }); + + it("(#5) Burns a deleted token", async function () { + const supplyKey = await getPrivateKey(this, "ed25519"); + const adminKey = await getPrivateKey(this, "ecdsaSecp256k1"); + const tokenId = await createToken(this, true, supplyKey, adminKey); + + await JSONRPCRequest(this, "deleteToken", { + tokenId, + commonTransactionParams: { + signers: [adminKey], + }, + }); + + try { + await JSONRPCRequest(this, "burnToken", { + tokenId, + commonTransactionParams: { + signers: [supplyKey], + }, + }); + } catch (err: any) { + assert.equal(err.data.status, "TOKEN_WAS_DELETED"); + return; + } + + assert.fail("Should throw an error"); + }); + + it("(#6) Burns a token without signing with the token's supply key", async function () { + const supplyKey = await getPrivateKey(this, "ed25519"); + const tokenId = await createToken(this, true, supplyKey); + + try { + await JSONRPCRequest(this, "burnToken", { + tokenId, + amount: "10", + }); + } catch (err: any) { + assert.equal(err.data.status, "INVALID_SIGNATURE"); + return; + } + + assert.fail("Should throw an error"); + }); + + it("(#7) Burns a token but signs with the token's admin key", async function () { + const supplyKey = await getPrivateKey(this, "ed25519"); + const adminKey = await getPrivateKey(this, "ecdsaSecp256k1"); + const tokenId = await createToken(this, true, supplyKey, adminKey); + + try { + await JSONRPCRequest(this, "burnToken", { + tokenId, + amount: "10", + commonTransactionParams: { + signers: [adminKey], + }, + }); + } catch (err: any) { + assert.equal(err.data.status, "INVALID_SIGNATURE"); + return; + } + + assert.fail("Should throw an error"); + }); + + it("(#8) Burns a token but signs with an incorrect supply key", async function () { + const tokenId = await createToken( + this, + true, + await getPrivateKey(this, "ed25519"), + await getPrivateKey(this, "ecdsaSecp256k1"), + ); + + try { + await JSONRPCRequest(this, "burnToken", { + tokenId, + amount: "10", + commonTransactionParams: { + signers: [await getPrivateKey(this, "ecdsaSecp256k1")], + }, + }); + } catch (err: any) { + assert.equal(err.data.status, "INVALID_SIGNATURE"); + return; + } + + assert.fail("Should throw an error"); + }); + + it("(#9) Burns a token with no supply key", async function () { + const tokenId = await createToken(this, true); + + try { + await JSONRPCRequest(this, "burnToken", { + tokenId, + amount: "10", + }); + } catch (err: any) { + assert.equal(err.data.status, "TOKEN_HAS_NO_SUPPLY_KEY"); + return; + } + + assert.fail("Should throw an error"); + }); + + it("(#10) Burns a paused token", async function () { + const supplyKey = await getPrivateKey(this, "ed25519"); + const pauseKey = await getPrivateKey(this, "ecdsaSecp256k1"); + const tokenId = await createToken(this, true, supplyKey, null, pauseKey); + + await JSONRPCRequest(this, "pauseToken", { + tokenId, + commonTransactionParams: { + singers: [pauseKey], + }, + }); + + try { + await JSONRPCRequest(this, "burnToken", { + tokenId, + amount: "10", + commonTransactionParams: { + signers: [supplyKey], + }, + }); + } catch (err: any) { + assert.equal(err.data.status, "TOKEN_IS_PAUSED"); + return; + } + + assert.fail("Should throw an error"); + }); + }); + + describe("Amount", function () { + it("(#1) Burns an amount of 1,000,000 fungible tokens", async function () { + const supplyKey = await getPrivateKey(this, "ed25519"); + const tokenId = await createToken(this, true, supplyKey); + + const amount = "1000000"; + expect( + ( + await JSONRPCRequest(this, "burnToken", { + tokenId, + amount, + commonTransactionParams: { + signers: [supplyKey], + }, + }) + ).newTotalSupply, + ).to.equal((Number(fungibleInitialSupply) - Number(amount)).toString()); + await verifyFungibleTokenBurn(tokenId, amount); + }); + + it("(#2) Burns an amount of 0 fungible tokens", async function () { + const supplyKey = await getPrivateKey(this, "ed25519"); + const tokenId = await createToken(this, true, supplyKey); + + const amount = "0"; + expect( + ( + await JSONRPCRequest(this, "burnToken", { + tokenId, + amount, + commonTransactionParams: { + signers: [supplyKey], + }, + }) + ).newTotalSupply, + ).to.equal(fungibleInitialSupply); + await verifyFungibleTokenBurn(tokenId, amount); + }); + + it("(#3) Burns no fungible tokens", async function () { + const supplyKey = await getPrivateKey(this, "ed25519"); + const tokenId = await createToken(this, true, supplyKey); + + expect( + ( + await JSONRPCRequest(this, "burnToken", { + tokenId, + commonTransactionParams: { + signers: [supplyKey], + }, + }) + ).newTotalSupply, + ).to.equal(fungibleInitialSupply); + await verifyFungibleTokenBurn(tokenId, "0"); + }); + + it("(#4) Burns no fungible tokens", async function () { + const supplyKey = await getPrivateKey(this, "ed25519"); + const tokenId = await createToken(this, true, supplyKey); + + expect( + ( + await JSONRPCRequest(this, "burnToken", { + tokenId, + commonTransactionParams: { + signers: [supplyKey], + }, + }) + ).newTotalSupply, + ).to.equal(fungibleInitialSupply); + await verifyFungibleTokenBurn(tokenId, "0"); + }); + }); +}); diff --git a/src/utils/helpers/key.ts b/src/utils/helpers/key.ts index 1831b19..de0f60a 100644 --- a/src/utils/helpers/key.ts +++ b/src/utils/helpers/key.ts @@ -3,7 +3,9 @@ import { PublicKey } from "@hashgraph/sdk"; import mirrorNodeClient from "@services/MirrorNodeClient"; import consensusInfoClient from "@services/ConsensusInfoClient"; + import { keyTypeConvertFunctions } from "@constants/key-type"; +import { JSONRPCRequest } from "@services/Client"; /** * Retrieves the encoded hexadecimal representation of a specified dynamic key @@ -52,24 +54,33 @@ export const getEncodedKeyHexFromKeyListConsensus = async ( * @returns {Promise} - A promise that resolves to the public key object retrieved from the Mirror Node. */ export const getPublicKeyFromMirrorNode = async ( - mirrorClientMethod: keyof typeof mirrorNodeClient, - searchedId: string, - searchedKey: string, + keyMirrorNode: any, ): Promise => { - // Retrieve the desired data from Mirror node - const data = await mirrorNodeClient[mirrorClientMethod](searchedId); - - // Access the dynamic key (e.g., fee_schedule_key, admin_key, etc.) - const keyMirrorNode = data[searchedKey]; - + // If the key doesn't exist, it doesn't exist. if (keyMirrorNode == null) { return null; } // Use the appropriate key type function to convert the key - const publicKeyMirrorNode = keyTypeConvertFunctions[ + return keyTypeConvertFunctions[ keyMirrorNode._type as keyof typeof keyTypeConvertFunctions ](keyMirrorNode.key); +}; - return publicKeyMirrorNode; +/** + * Generate a private key of the specified type. + * + * @async + * @param {string} type - The type of private key to generate. MUST be "ed25519PrivateKey" or "ecdsaSecp256k1PrivateKey" + * @returns {Promise} - A promise that resolves to the DER-encoded hex string of the generated private key. + */ +export const getPrivateKey = async ( + mochaTestContext: any, + type: string, +): Promise => { + return ( + await JSONRPCRequest(mochaTestContext, "generateKey", { + type: type + "PrivateKey", + }) + ).key; }; diff --git a/src/utils/helpers/verify-token-tx.ts b/src/utils/helpers/verify-token-tx.ts index effce8f..560c99c 100644 --- a/src/utils/helpers/verify-token-tx.ts +++ b/src/utils/helpers/verify-token-tx.ts @@ -27,9 +27,7 @@ export const verifyTokenKey = async ( const mirrorNodeKey = transformConsensusToMirrorNodeProp(keyType); // Fetch the key from the mirror node const publicKeyMirrorNode = await getPublicKeyFromMirrorNode( - "getTokenData", - tokenId, - mirrorNodeKey, + (await mirrorNodeClient.getTokenData(tokenId))[mirrorNodeKey], ); // Verify that the key from the mirror node matches the raw key @@ -84,9 +82,7 @@ export const verifyTokenUpdateWithNullKey = async ( // Fetch the key from the mirror node and check if it is null const mirrorNodeKey = await getPublicKeyFromMirrorNode( - "getTokenData", - tokenId, - mirrorNodeKeyName, + (await mirrorNodeClient.getTokenData(tokenId))[mirrorNodeKeyName], ); expect(null).to.equal(mirrorNodeKey); }; From b938ea62f4fc953de960612c9d075f0102257f0a Mon Sep 17 00:00:00 2001 From: Rob Walworth Date: Fri, 13 Dec 2024 15:27:06 -0500 Subject: [PATCH 2/6] feat: finish burnToken tests Signed-off-by: Rob Walworth --- .../test-token-burn-transaction.ts | 406 +++++++++++++++++- 1 file changed, 401 insertions(+), 5 deletions(-) diff --git a/src/tests/token-service/test-token-burn-transaction.ts b/src/tests/token-service/test-token-burn-transaction.ts index 7e07101..bdcd051 100644 --- a/src/tests/token-service/test-token-burn-transaction.ts +++ b/src/tests/token-service/test-token-burn-transaction.ts @@ -41,6 +41,7 @@ describe("TokenBurnTransaction", function () { decimals: number | null = null, maxSupply: string | null = null, freezeKey: string | null = null, + initialSupply: string | null = null, ): Promise { let params: any = { name: "testname", @@ -49,7 +50,9 @@ describe("TokenBurnTransaction", function () { }; if (fungible) { - params.initialSupply = fungibleInitialSupply; + params.initialSupply = initialSupply + ? initialSupply + : fungibleInitialSupply; params.decimals = 0; params.tokenType = "ft"; } else { @@ -111,7 +114,7 @@ describe("TokenBurnTransaction", function () { async function verifyFungibleTokenBurn(tokenId: string, amount: string) { const consensusNodeInfo = await consensusInfoClient.getBalance(treasuryAccountId); - expect(Number(fungibleInitialSupply) - Number(amount)).to.equal( + expect(BigInt(fungibleInitialSupply) - BigInt(amount)).to.equal( consensusNodeInfo.tokens?.get(tokenId), ); @@ -197,7 +200,7 @@ describe("TokenBurnTransaction", function () { }, }) ).newTotalSupply, - ).to.equal((Number(fungibleInitialSupply) - Number(amount)).toString()); + ).to.equal((BigInt(fungibleInitialSupply) - BigInt(amount)).toString()); await verifyFungibleTokenBurn(tokenId, amount); }); @@ -393,7 +396,7 @@ describe("TokenBurnTransaction", function () { }, }) ).newTotalSupply, - ).to.equal((Number(fungibleInitialSupply) - Number(amount)).toString()); + ).to.equal((BigInt(fungibleInitialSupply) - BigInt(amount)).toString()); await verifyFungibleTokenBurn(tokenId, amount); }); @@ -433,10 +436,11 @@ describe("TokenBurnTransaction", function () { await verifyFungibleTokenBurn(tokenId, "0"); }); - it("(#4) Burns no fungible tokens", async function () { + it("(#4) Burns an amount of 9,223,372,036,854,775,806 (int64 max - 1) fungible tokens", async function () { const supplyKey = await getPrivateKey(this, "ed25519"); const tokenId = await createToken(this, true, supplyKey); + const amount = "9223372036854775806"; expect( ( await JSONRPCRequest(this, "burnToken", { @@ -446,6 +450,398 @@ describe("TokenBurnTransaction", function () { }, }) ).newTotalSupply, + ).to.equal(BigInt(fungibleInitialSupply) - BigInt(amount)); + await verifyFungibleTokenBurn(tokenId, amount); + }); + + it("(#5) Burns an amount of 9,223,372,036,854,775,807 (int64 max) fungible tokens", async function () { + const supplyKey = await getPrivateKey(this, "ed25519"); + const tokenId = await createToken(this, true, supplyKey); + + const amount = "9223372036854775807"; + expect( + ( + await JSONRPCRequest(this, "burnToken", { + tokenId, + commonTransactionParams: { + signers: [supplyKey], + }, + }) + ).newTotalSupply, + ).to.equal(BigInt(fungibleInitialSupply) - BigInt(amount)); + await verifyFungibleTokenBurn(tokenId, amount); + }); + + it("(#6) Burns an amount of 9,223,372,036,854,775,808 (int64 max + 1) fungible tokens", async function () { + const supplyKey = await getPrivateKey(this, "ed25519"); + + try { + await JSONRPCRequest(this, "burnToken", { + tokenId: await createToken(this, true, supplyKey), + amount: "9223372036854775808", + commonTransactionParams: { + signers: [supplyKey], + }, + }); + } catch (err: any) { + assert.equal(err.data.status, "INVALID_TOKEN_BURN_AMOUNT"); + return; + } + + assert.fail("Should throw an error"); + }); + + it("(#7) Burns an amount of 18,446,744,073,709,551,614 (uint64 max - 1) fungible tokens", async function () { + const supplyKey = await getPrivateKey(this, "ed25519"); + + try { + await JSONRPCRequest(this, "burnToken", { + tokenId: await createToken(this, true, supplyKey), + amount: "18446744073709551614", + commonTransactionParams: { + signers: [supplyKey], + }, + }); + } catch (err: any) { + assert.equal(err.data.status, "INVALID_TOKEN_BURN_AMOUNT"); + return; + } + + assert.fail("Should throw an error"); + }); + + it("(#8) Burns an amount of 18,446,744,073,709,551,615 (uint64 max) fungible tokens", async function () { + const supplyKey = await getPrivateKey(this, "ed25519"); + + try { + await JSONRPCRequest(this, "burnToken", { + tokenId: await createToken(this, true, supplyKey), + amount: "18446744073709551615", + commonTransactionParams: { + signers: [supplyKey], + }, + }); + } catch (err: any) { + assert.equal(err.data.status, "INVALID_TOKEN_BURN_AMOUNT"); + return; + } + + assert.fail("Should throw an error"); + }); + + it("(#9) Burns an amount of 10,000 fungible tokens with 2 decimals", async function () { + const supplyKey = await getPrivateKey(this, "ed25519"); + const tokenId = await createToken(this, true, supplyKey, null, null, 2); + + const amount = "10000"; + expect( + ( + await JSONRPCRequest(this, "burnToken", { + tokenId, + amount, + commonTransactionParams: { + signers: [supplyKey], + }, + }) + ).newTotalSupply, + ).to.equal((BigInt(fungibleInitialSupply) - BigInt(amount)).toString()); + await verifyFungibleTokenBurn(tokenId, amount); + }); + + it("(#10) Burns an amount of 10,000 fungible tokens with 1,000 max supply", async function () { + const supplyKey = await getPrivateKey(this, "ed25519"); + + try { + await JSONRPCRequest(this, "burnToken", { + tokenId: await createToken( + this, + true, + supplyKey, + null, + null, + null, + "1000", + null, + "1000", + ), + amount: "10000", + commonTransactionParams: { + signers: [supplyKey], + }, + }); + } catch (err: any) { + assert.equal(err.data.status, "INVALID_TOKEN_BURN_AMOUNT"); + return; + } + + assert.fail("Should throw an error"); + }); + + it("(#11) Burns fungible tokens with the treasury account frozen", async function () { + const supplyKey = await getPrivateKey(this, "ed25519"); + const freezeKey = await getPrivateKey(this, "ecdsaSecp256k1"); + const tokenId = await createToken( + this, + true, + supplyKey, + null, + null, + null, + null, + freezeKey, + ); + + await JSONRPCRequest(this, "freezeToken", { + tokenId, + accountId: treasuryAccountId, + commonTransactionParams: { + signers: [freezeKey], + }, + }); + + try { + await JSONRPCRequest(this, "burnToken", { + tokenId, + amount: "1000000", + commonTransactionParams: { + signers: [supplyKey], + }, + }); + } catch (err: any) { + assert.equal(err.data.status, "ACCOUNT_FROZEN_FOR_TOKEN"); + return; + } + + assert.fail("Should throw an error"); + }); + + it("(#12) Burns paused fungible tokens", async function () { + const supplyKey = await getPrivateKey(this, "ed25519"); + const pauseKey = await getPrivateKey(this, "ecdsaSecp256k1"); + const tokenId = await createToken(this, true, supplyKey, null, pauseKey); + + await JSONRPCRequest(this, "pauseToken", { + tokenId, + commonTransactionParams: { + signers: [pauseKey], + }, + }); + + try { + await JSONRPCRequest(this, "burnToken", { + tokenId, + amount: "1000000", + commonTransactionParams: { + signers: [supplyKey], + }, + }); + } catch (err: any) { + assert.equal(err.data.status, "TOKEN_IS_PAUSED"); + return; + } + + assert.fail("Should throw an error"); + }); + + it("(#13) Burns an amount of 1,000,000 NFTs", async function () { + const supplyKey = await getPrivateKey(this, "ed25519"); + const tokenId = await createToken(this, false, supplyKey); + + try { + await JSONRPCRequest(this, "burnToken", { + tokenId, + amount: "1000000", + commonTransactionParams: { + signers: [supplyKey], + }, + }); + } catch (err: any) { + assert.equal(err.data.status, "INVALID_TOKEN_BURN_METADATA"); + return; + } + + assert.fail("Should throw an error"); + }); + }); + + describe("Serial Numbers", function () { + it("(#1) Burns an NFT", async function () { + const supplyKey = await getPrivateKey(this, "ed25519"); + const tokenId = await createToken(this, false, supplyKey); + + const serialNumber = "1"; + expect( + ( + await JSONRPCRequest(this, "burnToken", { + tokenId, + serialNumbers: [serialNumber], + commonTransactionParams: { + signers: [supplyKey], + }, + }) + ).newTotalSupply, + ).to.equal(nonFungibleMetadata.length - 1); + await verifyNonFungibleTokenBurn(tokenId, serialNumber); + }); + + it("(#2) Burns 3 NFTs", async function () { + const supplyKey = await getPrivateKey(this, "ed25519"); + const tokenId = await createToken(this, false, supplyKey); + + const serialNumbers = ["1", "2", "3"]; + expect( + ( + await JSONRPCRequest(this, "burnToken", { + tokenId, + serialNumbers, + commonTransactionParams: { + signers: [supplyKey], + }, + }) + ).newTotalSupply, + ).to.equal(nonFungibleMetadata.length - serialNumbers.length); + await verifyNonFungibleTokenBurn(tokenId, serialNumbers[0]); + await verifyNonFungibleTokenBurn(tokenId, serialNumbers[1]); + await verifyNonFungibleTokenBurn(tokenId, serialNumbers[2]); + }); + + it("(#3) Burns 3 NFTs but one is already burned", async function () { + const supplyKey = await getPrivateKey(this, "ed25519"); + const tokenId = await createToken(this, false, supplyKey); + const serialNumbers = ["1", "2", "3"]; + + await JSONRPCRequest(this, "burnToken", { + tokenId, + serialNumbers: [serialNumbers[serialNumbers.length - 1]], + commonTransactionParams: { + signers: [supplyKey], + }, + }); + + try { + await JSONRPCRequest(this, "burnToken", { + tokenId, + serialNumbers, + commonTransactionParams: { + signers: [supplyKey], + }, + }); + } catch (err: any) { + assert.equal(err.data.status, "INVALID_NFT_ID"); + return; + } + + assert.fail("Should throw an error"); + }); + + it("(#4) Burns no NFTs", async function () { + const supplyKey = await getPrivateKey(this, "ed25519"); + const tokenId = await createToken(this, false, supplyKey); + + expect( + ( + await JSONRPCRequest(this, "burnToken", { + tokenId, + commonTransactionParams: { + signers: [supplyKey], + }, + }) + ).newTotalSupply, + ).to.equal(nonFungibleMetadata.length); + }); + + it("(#5) Burns an NFT that doesn't exist", async function () { + const supplyKey = await getPrivateKey(this, "ed25519"); + + try { + await JSONRPCRequest(this, "burnToken", { + tokenId: await createToken(this, false, supplyKey), + serialNumbers: ["12345678"], + commonTransactionParams: { + signers: [supplyKey], + }, + }); + } catch (err: any) { + assert.equal(err.data.status, "INVALID_TOKEN_NFT_SERIAL_NUMBER"); + return; + } + + assert.fail("Should throw an error"); + }); + + it("(#6) Burns NFTs with the treasury account frozen", async function () { + const supplyKey = await getPrivateKey(this, "ed25519"); + const freezeKey = await getPrivateKey(this, "ecdsaSecp256k1"); + const tokenId = await createToken( + this, + false, + supplyKey, + null, + null, + null, + null, + freezeKey, + ); + + try { + await JSONRPCRequest(this, "burnToken", { + tokenId, + serialNumbers: ["1"], + commonTransactionParams: { + signers: [supplyKey], + }, + }); + } catch (err: any) { + assert.equal(err.data.status, "INVALID_TOKEN_NFT_SERIAL_NUMBER"); + return; + } + + assert.fail("Should throw an error"); + }); + + it("(#7) Burns paused NFTs", async function () { + const supplyKey = await getPrivateKey(this, "ed25519"); + const pauseKey = await getPrivateKey(this, "ecdsaSecp256k1"); + const tokenId = await createToken(this, false, supplyKey, null, pauseKey); + + await JSONRPCRequest(this, "pauseToken", { + tokenId, + commonTransactionParams: { + signers: [pauseKey], + }, + }); + + try { + await JSONRPCRequest(this, "burnToken", { + tokenId, + serialNumbers: ["1"], + commonTransactionParams: { + signers: [supplyKey], + }, + }); + } catch (err: any) { + assert.equal(err.data.status, "TOKEN_IS_PAUSED"); + return; + } + + assert.fail("Should throw an error"); + }); + + it("(#8) Burns fungible tokens with serial numbers", async function () { + const supplyKey = await getPrivateKey(this, "ed25519"); + const pauseKey = await getPrivateKey(this, "ecdsaSecp256k1"); + const tokenId = await createToken(this, true, supplyKey); + + expect( + ( + await JSONRPCRequest(this, "burnToken", { + tokenId, + serialNumbers: ["1"], + commonTransactionParams: { + signers: [supplyKey], + }, + }) + ).newTotalSupply, ).to.equal(fungibleInitialSupply); await verifyFungibleTokenBurn(tokenId, "0"); }); From 99e15fd6c44911550fbc1eb5cb08b4a24011ee9b Mon Sep 17 00:00:00 2001 From: Rob Walworth Date: Fri, 13 Dec 2024 17:23:09 -0500 Subject: [PATCH 3/6] fix: burnToken tests Signed-off-by: Rob Walworth --- .../token-service/TokenBurnTransaction.md | 20 ++--- .../test-token-burn-transaction.ts | 78 +++++++++++-------- 2 files changed, 54 insertions(+), 44 deletions(-) diff --git a/docs/test-specifications/token-service/TokenBurnTransaction.md b/docs/test-specifications/token-service/TokenBurnTransaction.md index e807f88..3dbfd5e 100644 --- a/docs/test-specifications/token-service/TokenBurnTransaction.md +++ b/docs/test-specifications/token-service/TokenBurnTransaction.md @@ -150,16 +150,16 @@ The tests contained in this specification will assume that a valid fungible toke - The list of NFT serial numbers to burn. -| Test no | Name | Input | Expected response | Implemented (Y/N) | -|---------|---------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------|-------------------| -| 1 | Burns an NFT | tokenId=, serialNumbers=[], commonTransactionParams.signers=[] | The token burn succeeds and the token's treasury account no longer contains the first NFT. | N | -| 2 | Burns 3 NFTs | tokenId=, serialNumbers=[, , ], commonTransactionParams.signers=[] | The token burn succeeds and the token's treasury account no longer contains any NFTs. | N | -| 3 | Burns 3 NFTs but one is already burned | tokenId=, serialNumbers=[, , ], commonTransactionParams.signers=[] | The token burn fails with an INVALID_NFT_ID response code from the network. | N | -| 4 | Burns no NFTs | tokenId=, commonTransactionParams.signers=[] | The token burn succeeds and the token's treasury account contains all three NFTs. | N | -| 5 | Burns an NFT that doesn't exist | tokenId=, serialNumbers=["12345678"], commonTransactionParams.signers=[] | The token burn fails with an INVALID_TOKEN_NFT_SERIAL_NUMBER response code from the network. | N | -| 6 | Burns NFTs with the treasury account frozen | tokenId=, serialNumbers=[], commonTransactionParams.signers=[] | The token burn fails with an ACCOUNT_FROZEN_FOR_TOKEN response code from the network. | N | -| 7 | Burns paused NFTs | tokenId=, serialNumbers=[], commonTransactionParams.signers=[] | The token burn fails with an TOKEN_IS_PAUSED response code from the network. | N | -| 8 | Burns fungible tokens with serial numbers | tokenId=, serialNumbers=[], commonTransactionParams.signers=[] | The token burn succeeds and the token's treasury account contains the same NFTs. | N | +| Test no | Name | Input | Expected response | Implemented (Y/N) | +|---------|---------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------|-------------------| +| 1 | Burns an NFT | tokenId=, serialNumbers=[], commonTransactionParams.signers=[] | The token burn succeeds and the token's treasury account no longer contains the first NFT. | N | +| 2 | Burns 3 NFTs | tokenId=, serialNumbers=[, , ], commonTransactionParams.signers=[] | The token burn succeeds and the token's treasury account no longer contains any NFTs. | N | +| 3 | Burns 3 NFTs but one is already burned | tokenId=, serialNumbers=[, , ], commonTransactionParams.signers=[] | The token burn fails with an INVALID_NFT_ID response code from the network. | N | +| 4 | Burns no NFTs | tokenId=, commonTransactionParams.signers=[] | The token burn fails with an INVALID_TOKEN_BURN_METADATA response code from the network. | N | +| 5 | Burns an NFT that doesn't exist | tokenId=, serialNumbers=["12345678"], commonTransactionParams.signers=[] | The token burn fails with an INVALID_NFT_ID response code from the network. | N | +| 6 | Burns NFTs with the treasury account frozen | tokenId=, serialNumbers=[], commonTransactionParams.signers=[] | The token burn fails with an ACCOUNT_FROZEN_FOR_TOKEN response code from the network. | N | +| 7 | Burns paused NFTs | tokenId=, serialNumbers=[], commonTransactionParams.signers=[] | The token burn fails with an TOKEN_IS_PAUSED response code from the network. | N | +| 8 | Burns fungible tokens with serial numbers | tokenId=, serialNumbers=[], commonTransactionParams.signers=[] | The token burn succeeds and the token's treasury account contains the same NFTs. | N | #### JSON Request Example diff --git a/src/tests/token-service/test-token-burn-transaction.ts b/src/tests/token-service/test-token-burn-transaction.ts index bdcd051..100cc0d 100644 --- a/src/tests/token-service/test-token-burn-transaction.ts +++ b/src/tests/token-service/test-token-burn-transaction.ts @@ -114,8 +114,8 @@ describe("TokenBurnTransaction", function () { async function verifyFungibleTokenBurn(tokenId: string, amount: string) { const consensusNodeInfo = await consensusInfoClient.getBalance(treasuryAccountId); - expect(BigInt(fungibleInitialSupply) - BigInt(amount)).to.equal( - consensusNodeInfo.tokens?.get(tokenId), + expect(consensusNodeInfo.tokens?.get(tokenId)?.toString()).to.equal( + (BigInt(fungibleInitialSupply) - BigInt(amount)).toString(), ); await retryOnError(async () => { @@ -127,7 +127,9 @@ describe("TokenBurnTransaction", function () { let foundToken = false; for (let i = 0; i < mirrorNodeInfo.tokens.length; i++) { if (mirrorNodeInfo.tokens[i].token_id === tokenId) { - expect(mirrorNodeInfo.tokens[i].balance.toString()).to.equal(amount); + expect(mirrorNodeInfo.tokens[i].balance.toString()).to.equal( + (BigInt(fungibleInitialSupply) - BigInt(amount)).toString(), + ); foundToken = true; break; } @@ -143,20 +145,15 @@ describe("TokenBurnTransaction", function () { tokenId: string, serialNumber: string, ) { - // Query the consensus node. - const consensusNodeInfo = await consensusInfoClient.getTokenNftInfo( - tokenId, - serialNumber, - ); - let foundNft = false; - for (let i = 0; i < consensusNodeInfo.length; i++) { - if ( - consensusNodeInfo[i].nftId.tokenId.toString() === tokenId && - consensusNodeInfo[i].nftId.serial.toString() === serialNumber - ) { - foundNft = true; - break; - } + // Query the consensus node. Should throw since the NFT shouldn't exist anymore. + let foundNft = true; + try { + const consensusNodeInfo = await consensusInfoClient.getTokenNftInfo( + tokenId, + serialNumber, + ); + } catch (err: any) { + foundNft = false; } // Make sure the NFT was not found. @@ -210,7 +207,7 @@ describe("TokenBurnTransaction", function () { const response = await JSONRPCRequest(this, "burnToken", { tokenId, - metadata: [nonFungibleMetadata[0]], + serialNumbers: ["1"], commonTransactionParams: { signers: [supplyKey], }, @@ -445,12 +442,13 @@ describe("TokenBurnTransaction", function () { ( await JSONRPCRequest(this, "burnToken", { tokenId, + amount, commonTransactionParams: { signers: [supplyKey], }, }) ).newTotalSupply, - ).to.equal(BigInt(fungibleInitialSupply) - BigInt(amount)); + ).to.equal((BigInt(fungibleInitialSupply) - BigInt(amount)).toString()); await verifyFungibleTokenBurn(tokenId, amount); }); @@ -463,12 +461,13 @@ describe("TokenBurnTransaction", function () { ( await JSONRPCRequest(this, "burnToken", { tokenId, + amount, commonTransactionParams: { signers: [supplyKey], }, }) ).newTotalSupply, - ).to.equal(BigInt(fungibleInitialSupply) - BigInt(amount)); + ).to.equal((BigInt(fungibleInitialSupply) - BigInt(amount)).toString()); await verifyFungibleTokenBurn(tokenId, amount); }); @@ -680,7 +679,7 @@ describe("TokenBurnTransaction", function () { }, }) ).newTotalSupply, - ).to.equal(nonFungibleMetadata.length - 1); + ).to.equal((nonFungibleMetadata.length - 1).toString()); await verifyNonFungibleTokenBurn(tokenId, serialNumber); }); @@ -699,7 +698,7 @@ describe("TokenBurnTransaction", function () { }, }) ).newTotalSupply, - ).to.equal(nonFungibleMetadata.length - serialNumbers.length); + ).to.equal((nonFungibleMetadata.length - serialNumbers.length).toString()); await verifyNonFungibleTokenBurn(tokenId, serialNumbers[0]); await verifyNonFungibleTokenBurn(tokenId, serialNumbers[1]); await verifyNonFungibleTokenBurn(tokenId, serialNumbers[2]); @@ -738,16 +737,19 @@ describe("TokenBurnTransaction", function () { const supplyKey = await getPrivateKey(this, "ed25519"); const tokenId = await createToken(this, false, supplyKey); - expect( - ( - await JSONRPCRequest(this, "burnToken", { - tokenId, - commonTransactionParams: { - signers: [supplyKey], - }, - }) - ).newTotalSupply, - ).to.equal(nonFungibleMetadata.length); + try { + await JSONRPCRequest(this, "burnToken", { + tokenId, + commonTransactionParams: { + signers: [supplyKey], + }, + }); + } catch (err: any) { + assert.equal(err.data.status, "INVALID_TOKEN_BURN_METADATA"); + return; + } + + assert.fail("Should throw an error"); }); it("(#5) Burns an NFT that doesn't exist", async function () { @@ -762,7 +764,7 @@ describe("TokenBurnTransaction", function () { }, }); } catch (err: any) { - assert.equal(err.data.status, "INVALID_TOKEN_NFT_SERIAL_NUMBER"); + assert.equal(err.data.status, "INVALID_NFT_ID"); return; } @@ -783,6 +785,14 @@ describe("TokenBurnTransaction", function () { freezeKey, ); + await JSONRPCRequest(this, "freezeToken", { + tokenId, + accountId: treasuryAccountId, + commonTransactionParams: { + signers: [freezeKey], + }, + }); + try { await JSONRPCRequest(this, "burnToken", { tokenId, @@ -792,7 +802,7 @@ describe("TokenBurnTransaction", function () { }, }); } catch (err: any) { - assert.equal(err.data.status, "INVALID_TOKEN_NFT_SERIAL_NUMBER"); + assert.equal(err.data.status, "ACCOUNT_FROZEN_FOR_TOKEN"); return; } From e4e7f3d6c7ce9b5556e2e3d0379657ccce56d2f8 Mon Sep 17 00:00:00 2001 From: Rob Walworth Date: Fri, 13 Dec 2024 17:42:32 -0500 Subject: [PATCH 4/6] refactor: apply formatting Signed-off-by: Rob Walworth --- src/tests/token-service/test-token-burn-transaction.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/tests/token-service/test-token-burn-transaction.ts b/src/tests/token-service/test-token-burn-transaction.ts index 100cc0d..0efa005 100644 --- a/src/tests/token-service/test-token-burn-transaction.ts +++ b/src/tests/token-service/test-token-burn-transaction.ts @@ -698,7 +698,9 @@ describe("TokenBurnTransaction", function () { }, }) ).newTotalSupply, - ).to.equal((nonFungibleMetadata.length - serialNumbers.length).toString()); + ).to.equal( + (nonFungibleMetadata.length - serialNumbers.length).toString(), + ); await verifyNonFungibleTokenBurn(tokenId, serialNumbers[0]); await verifyNonFungibleTokenBurn(tokenId, serialNumbers[1]); await verifyNonFungibleTokenBurn(tokenId, serialNumbers[2]); From 547ecb2e37c50b5f06ad4798b80366ce0f8cc419 Mon Sep 17 00:00:00 2001 From: Rob Walworth Date: Fri, 3 Jan 2025 16:19:08 -0500 Subject: [PATCH 5/6] fix: address PR comments Signed-off-by: Rob Walworth --- .../token-service/test-token-burn-transaction.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/tests/token-service/test-token-burn-transaction.ts b/src/tests/token-service/test-token-burn-transaction.ts index 0efa005..1a1e096 100644 --- a/src/tests/token-service/test-token-burn-transaction.ts +++ b/src/tests/token-service/test-token-burn-transaction.ts @@ -213,9 +213,10 @@ describe("TokenBurnTransaction", function () { }, }); - expect(response.newTotalSupply).to.equal( - (Number(nonFungibleMetadata.length) - 1).toString(), - ); + const newTotalSupply = ( + Number(nonFungibleMetadata.length) - 1 + ).toString(); + expect(response.newTotalSupply).to.equal(newTotalSupply); await verifyNonFungibleTokenBurn(tokenId, "1"); }); @@ -356,7 +357,7 @@ describe("TokenBurnTransaction", function () { await JSONRPCRequest(this, "pauseToken", { tokenId, commonTransactionParams: { - singers: [pauseKey], + signers: [pauseKey], }, }); @@ -711,9 +712,10 @@ describe("TokenBurnTransaction", function () { const tokenId = await createToken(this, false, supplyKey); const serialNumbers = ["1", "2", "3"]; + const lastSerialNumber = serialNumbers[serialNumbers.length - 1]; await JSONRPCRequest(this, "burnToken", { tokenId, - serialNumbers: [serialNumbers[serialNumbers.length - 1]], + serialNumbers: [lastSerialNumber], commonTransactionParams: { signers: [supplyKey], }, From 6295dcde0a77006dd4372424d3180e9cf568603a Mon Sep 17 00:00:00 2001 From: Rob Walworth Date: Tue, 7 Jan 2025 16:02:56 -0500 Subject: [PATCH 6/6] refactor: address PR comments Signed-off-by: Rob Walworth --- .../test-token-burn-transaction.ts | 835 +++++++++++------- src/utils/helpers/key.ts | 3 +- src/utils/helpers/token.ts | 180 ++++ 3 files changed, 675 insertions(+), 343 deletions(-) diff --git a/src/tests/token-service/test-token-burn-transaction.ts b/src/tests/token-service/test-token-burn-transaction.ts index 1a1e096..79059da 100644 --- a/src/tests/token-service/test-token-burn-transaction.ts +++ b/src/tests/token-service/test-token-burn-transaction.ts @@ -1,12 +1,14 @@ import { assert, expect } from "chai"; import { JSONRPCRequest } from "@services/Client"; -import consensusInfoClient from "@services/ConsensusInfoClient"; -import mirrorNodeClient from "@services/MirrorNodeClient"; import { setOperator } from "@helpers/setup-tests"; import { getPrivateKey } from "@helpers/key"; -import { retryOnError } from "@helpers/retry-on-error"; +import { + createToken, + verifyFungibleTokenBurn, + verifyNonFungibleTokenBurn, +} from "@helpers/token"; /** * Tests for TokenBurnTransaction @@ -32,182 +34,63 @@ describe("TokenBurnTransaction", function () { // All tests require either a fungible token or NFT to be created, but not both. // These functions should be called at the start of each test depending on what token is needed. - async function createToken( - mochaTestContext: any, - fungible: boolean, - supplyKey: string | null = null, - adminKey: string | null = null, - pauseKey: string | null = null, - decimals: number | null = null, - maxSupply: string | null = null, - freezeKey: string | null = null, - initialSupply: string | null = null, - ): Promise { - let params: any = { - name: "testname", - symbol: "testsymbol", - treasuryAccountId, - }; - - if (fungible) { - params.initialSupply = initialSupply - ? initialSupply - : fungibleInitialSupply; - params.decimals = 0; - params.tokenType = "ft"; - } else { - params.tokenType = "nft"; - } - - // Add the supply key if its provided. - if (supplyKey) { - params.supplyKey = supplyKey; - } - - // Add and sign with the admin key if its provided. - if (adminKey) { - params.adminKey = adminKey; - params.commonTransactionParams = { - signers: [adminKey], - }; - } - - // Add the pause key if its provided. - if (pauseKey) { - params.pauseKey = pauseKey; - } - - // Add the decimals if its provided. - if (decimals) { - params.decimals = decimals; - } - - // Add the max supply if its provided. - if (maxSupply) { - params.supplyType = "finite"; - params.maxSupply = maxSupply; - } - - // Add the freeze key if its provided. - if (freezeKey) { - params.freezeKey = freezeKey; - } - - const tokenId = ( - await JSONRPCRequest(mochaTestContext, "createToken", params) - ).tokenId; - - // If creating an NFT, mint three. - if (!fungible) { - await JSONRPCRequest(mochaTestContext, "mintToken", { - tokenId, - metadata: nonFungibleMetadata, - commonTransactionParams: { - signers: [supplyKey], - }, - }); - } - - return tokenId; - } - async function verifyFungibleTokenBurn(tokenId: string, amount: string) { - const consensusNodeInfo = - await consensusInfoClient.getBalance(treasuryAccountId); - expect(consensusNodeInfo.tokens?.get(tokenId)?.toString()).to.equal( - (BigInt(fungibleInitialSupply) - BigInt(amount)).toString(), - ); - - await retryOnError(async () => { - const mirrorNodeInfo = await mirrorNodeClient.getTokenRelationships( + describe("Token ID", function () { + it("(#1) Burns a valid amount of fungible token", async function () { + const supplyKey = await getPrivateKey(this, "ed25519"); + const tokenId = await createToken( + this, + true, treasuryAccountId, - tokenId, + supplyKey, + fungibleInitialSupply, ); - let foundToken = false; - for (let i = 0; i < mirrorNodeInfo.tokens.length; i++) { - if (mirrorNodeInfo.tokens[i].token_id === tokenId) { - expect(mirrorNodeInfo.tokens[i].balance.toString()).to.equal( - (BigInt(fungibleInitialSupply) - BigInt(amount)).toString(), - ); - foundToken = true; - break; - } - } - - if (!foundToken) { - expect.fail("Token ID not found"); - } - }); - } - - async function verifyNonFungibleTokenBurn( - tokenId: string, - serialNumber: string, - ) { - // Query the consensus node. Should throw since the NFT shouldn't exist anymore. - let foundNft = true; - try { - const consensusNodeInfo = await consensusInfoClient.getTokenNftInfo( - tokenId, - serialNumber, + const amount = "10"; + const newTotalSupply = ( + await JSONRPCRequest(this, "burnToken", { + tokenId, + amount, + commonTransactionParams: { + signers: [supplyKey], + }, + }) + ).newTotalSupply; + expect(newTotalSupply).to.equal( + (BigInt(fungibleInitialSupply) - BigInt(amount)).toString(), ); - } catch (err: any) { - foundNft = false; - } - - // Make sure the NFT was not found. - expect(foundNft).to.be.false; - // Query the mirror node. - await retryOnError(async () => { - const mirrorNodeInfo = await mirrorNodeClient.getAccountNfts( - treasuryAccountId, + await verifyFungibleTokenBurn( tokenId, + treasuryAccountId, + fungibleInitialSupply, + amount, ); - foundNft = false; - for (let i = 0; i < mirrorNodeInfo.nfts.length; i++) { - if ( - mirrorNodeInfo.nfts[i].token_id === tokenId && - mirrorNodeInfo.nfts[i].serial_number.toString() === serialNumber - ) { - foundNft = true; - break; - } - } - - // Make sure the NFT was not found. - expect(foundNft).to.be.false; - }); - } - - describe("Token ID", function () { - it("(#1) Burns a valid amount of fungible token", async function () { - const supplyKey = await getPrivateKey(this, "ed25519"); - const tokenId = await createToken(this, true, supplyKey); - - const amount = "10"; - expect( - ( - await JSONRPCRequest(this, "burnToken", { - tokenId, - amount, - commonTransactionParams: { - signers: [supplyKey], - }, - }) - ).newTotalSupply, - ).to.equal((BigInt(fungibleInitialSupply) - BigInt(amount)).toString()); - await verifyFungibleTokenBurn(tokenId, amount); }); it("(#2) Burns a valid non-fungible token", async function () { const supplyKey = await getPrivateKey(this, "ed25519"); - const tokenId = await createToken(this, false, supplyKey); + const tokenId = await createToken( + this, + false, + treasuryAccountId, + supplyKey, + ); + + const serialNumbers = ( + await JSONRPCRequest(this, "mintToken", { + tokenId, + metadata: nonFungibleMetadata, + commonTransactionParams: { + signers: [supplyKey], + }, + }) + ).serialNumbers; + const serialNumber = serialNumbers[0]; const response = await JSONRPCRequest(this, "burnToken", { tokenId, - serialNumbers: ["1"], + serialNumbers: [serialNumber], commonTransactionParams: { signers: [supplyKey], }, @@ -217,7 +100,12 @@ describe("TokenBurnTransaction", function () { Number(nonFungibleMetadata.length) - 1 ).toString(); expect(response.newTotalSupply).to.equal(newTotalSupply); - await verifyNonFungibleTokenBurn(tokenId, "1"); + + await verifyNonFungibleTokenBurn( + tokenId, + treasuryAccountId, + serialNumber, + ); }); it("(#3) Mints a token with an empty token ID", async function () { @@ -247,7 +135,14 @@ describe("TokenBurnTransaction", function () { it("(#5) Burns a deleted token", async function () { const supplyKey = await getPrivateKey(this, "ed25519"); const adminKey = await getPrivateKey(this, "ecdsaSecp256k1"); - const tokenId = await createToken(this, true, supplyKey, adminKey); + const tokenId = await createToken( + this, + true, + treasuryAccountId, + supplyKey, + null, + adminKey, + ); await JSONRPCRequest(this, "deleteToken", { tokenId, @@ -273,7 +168,12 @@ describe("TokenBurnTransaction", function () { it("(#6) Burns a token without signing with the token's supply key", async function () { const supplyKey = await getPrivateKey(this, "ed25519"); - const tokenId = await createToken(this, true, supplyKey); + const tokenId = await createToken( + this, + true, + treasuryAccountId, + supplyKey, + ); try { await JSONRPCRequest(this, "burnToken", { @@ -291,7 +191,14 @@ describe("TokenBurnTransaction", function () { it("(#7) Burns a token but signs with the token's admin key", async function () { const supplyKey = await getPrivateKey(this, "ed25519"); const adminKey = await getPrivateKey(this, "ecdsaSecp256k1"); - const tokenId = await createToken(this, true, supplyKey, adminKey); + const tokenId = await createToken( + this, + true, + treasuryAccountId, + supplyKey, + null, + adminKey, + ); try { await JSONRPCRequest(this, "burnToken", { @@ -310,19 +217,24 @@ describe("TokenBurnTransaction", function () { }); it("(#8) Burns a token but signs with an incorrect supply key", async function () { + const supplyKey = await getPrivateKey(this, "ed25519"); + const adminKey = await getPrivateKey(this, "ecdsaSecp256k1"); const tokenId = await createToken( this, true, - await getPrivateKey(this, "ed25519"), - await getPrivateKey(this, "ecdsaSecp256k1"), + treasuryAccountId, + supplyKey, + null, + adminKey, ); + const incorrectKey = await getPrivateKey(this, "ecdsaSecp256k1"); try { await JSONRPCRequest(this, "burnToken", { tokenId, amount: "10", commonTransactionParams: { - signers: [await getPrivateKey(this, "ecdsaSecp256k1")], + signers: [incorrectKey], }, }); } catch (err: any) { @@ -334,7 +246,7 @@ describe("TokenBurnTransaction", function () { }); it("(#9) Burns a token with no supply key", async function () { - const tokenId = await createToken(this, true); + const tokenId = await createToken(this, true, treasuryAccountId); try { await JSONRPCRequest(this, "burnToken", { @@ -352,7 +264,15 @@ describe("TokenBurnTransaction", function () { it("(#10) Burns a paused token", async function () { const supplyKey = await getPrivateKey(this, "ed25519"); const pauseKey = await getPrivateKey(this, "ecdsaSecp256k1"); - const tokenId = await createToken(this, true, supplyKey, null, pauseKey); + const tokenId = await createToken( + this, + true, + treasuryAccountId, + supplyKey, + null, + null, + pauseKey, + ); await JSONRPCRequest(this, "pauseToken", { tokenId, @@ -379,105 +299,172 @@ describe("TokenBurnTransaction", function () { }); describe("Amount", function () { + let supplyKey: string; + + this.beforeEach(async function () { + supplyKey = await getPrivateKey(this, "ed25519"); + }); + it("(#1) Burns an amount of 1,000,000 fungible tokens", async function () { - const supplyKey = await getPrivateKey(this, "ed25519"); - const tokenId = await createToken(this, true, supplyKey); + const tokenId = await createToken( + this, + true, + treasuryAccountId, + supplyKey, + fungibleInitialSupply, + ); const amount = "1000000"; - expect( - ( - await JSONRPCRequest(this, "burnToken", { - tokenId, - amount, - commonTransactionParams: { - signers: [supplyKey], - }, - }) - ).newTotalSupply, - ).to.equal((BigInt(fungibleInitialSupply) - BigInt(amount)).toString()); - await verifyFungibleTokenBurn(tokenId, amount); + const newTotalSupply = ( + await JSONRPCRequest(this, "burnToken", { + tokenId, + amount, + commonTransactionParams: { + signers: [supplyKey], + }, + }) + ).newTotalSupply; + + expect(newTotalSupply).to.equal( + (BigInt(fungibleInitialSupply) - BigInt(amount)).toString(), + ); + await verifyFungibleTokenBurn( + tokenId, + treasuryAccountId, + fungibleInitialSupply, + amount, + ); }); it("(#2) Burns an amount of 0 fungible tokens", async function () { - const supplyKey = await getPrivateKey(this, "ed25519"); - const tokenId = await createToken(this, true, supplyKey); + const tokenId = await createToken( + this, + true, + treasuryAccountId, + supplyKey, + fungibleInitialSupply, + ); const amount = "0"; - expect( - ( - await JSONRPCRequest(this, "burnToken", { - tokenId, - amount, - commonTransactionParams: { - signers: [supplyKey], - }, - }) - ).newTotalSupply, - ).to.equal(fungibleInitialSupply); - await verifyFungibleTokenBurn(tokenId, amount); + const newTotalSupply = ( + await JSONRPCRequest(this, "burnToken", { + tokenId, + amount, + commonTransactionParams: { + signers: [supplyKey], + }, + }) + ).newTotalSupply; + + expect(newTotalSupply).to.equal(fungibleInitialSupply); + await verifyFungibleTokenBurn( + tokenId, + treasuryAccountId, + fungibleInitialSupply, + amount, + ); }); it("(#3) Burns no fungible tokens", async function () { - const supplyKey = await getPrivateKey(this, "ed25519"); - const tokenId = await createToken(this, true, supplyKey); + const tokenId = await createToken( + this, + true, + treasuryAccountId, + supplyKey, + fungibleInitialSupply, + ); - expect( - ( - await JSONRPCRequest(this, "burnToken", { - tokenId, - commonTransactionParams: { - signers: [supplyKey], - }, - }) - ).newTotalSupply, - ).to.equal(fungibleInitialSupply); - await verifyFungibleTokenBurn(tokenId, "0"); + const newTotalSupply = ( + await JSONRPCRequest(this, "burnToken", { + tokenId, + commonTransactionParams: { + signers: [supplyKey], + }, + }) + ).newTotalSupply; + + expect(newTotalSupply).to.equal(fungibleInitialSupply); + await verifyFungibleTokenBurn( + tokenId, + treasuryAccountId, + fungibleInitialSupply, + "0", + ); }); it("(#4) Burns an amount of 9,223,372,036,854,775,806 (int64 max - 1) fungible tokens", async function () { - const supplyKey = await getPrivateKey(this, "ed25519"); - const tokenId = await createToken(this, true, supplyKey); + const tokenId = await createToken( + this, + true, + treasuryAccountId, + supplyKey, + fungibleInitialSupply, + ); const amount = "9223372036854775806"; - expect( - ( - await JSONRPCRequest(this, "burnToken", { - tokenId, - amount, - commonTransactionParams: { - signers: [supplyKey], - }, - }) - ).newTotalSupply, - ).to.equal((BigInt(fungibleInitialSupply) - BigInt(amount)).toString()); - await verifyFungibleTokenBurn(tokenId, amount); + const newTotalSupply = ( + await JSONRPCRequest(this, "burnToken", { + tokenId, + amount, + commonTransactionParams: { + signers: [supplyKey], + }, + }) + ).newTotalSupply; + + expect(newTotalSupply).to.equal( + (BigInt(fungibleInitialSupply) - BigInt(amount)).toString(), + ); + await verifyFungibleTokenBurn( + tokenId, + treasuryAccountId, + fungibleInitialSupply, + amount, + ); }); it("(#5) Burns an amount of 9,223,372,036,854,775,807 (int64 max) fungible tokens", async function () { - const supplyKey = await getPrivateKey(this, "ed25519"); - const tokenId = await createToken(this, true, supplyKey); + const tokenId = await createToken( + this, + true, + treasuryAccountId, + supplyKey, + fungibleInitialSupply, + ); const amount = "9223372036854775807"; - expect( - ( - await JSONRPCRequest(this, "burnToken", { - tokenId, - amount, - commonTransactionParams: { - signers: [supplyKey], - }, - }) - ).newTotalSupply, - ).to.equal((BigInt(fungibleInitialSupply) - BigInt(amount)).toString()); - await verifyFungibleTokenBurn(tokenId, amount); + const newTotalSupply = ( + await JSONRPCRequest(this, "burnToken", { + tokenId, + amount, + commonTransactionParams: { + signers: [supplyKey], + }, + }) + ).newTotalSupply; + + expect(newTotalSupply).to.equal( + (BigInt(fungibleInitialSupply) - BigInt(amount)).toString(), + ); + await verifyFungibleTokenBurn( + tokenId, + treasuryAccountId, + fungibleInitialSupply, + amount, + ); }); it("(#6) Burns an amount of 9,223,372,036,854,775,808 (int64 max + 1) fungible tokens", async function () { - const supplyKey = await getPrivateKey(this, "ed25519"); + const tokenId = await createToken( + this, + true, + treasuryAccountId, + supplyKey, + ); try { await JSONRPCRequest(this, "burnToken", { - tokenId: await createToken(this, true, supplyKey), + tokenId, amount: "9223372036854775808", commonTransactionParams: { signers: [supplyKey], @@ -492,11 +479,16 @@ describe("TokenBurnTransaction", function () { }); it("(#7) Burns an amount of 18,446,744,073,709,551,614 (uint64 max - 1) fungible tokens", async function () { - const supplyKey = await getPrivateKey(this, "ed25519"); + const tokenId = await createToken( + this, + true, + treasuryAccountId, + supplyKey, + ); try { await JSONRPCRequest(this, "burnToken", { - tokenId: await createToken(this, true, supplyKey), + tokenId, amount: "18446744073709551614", commonTransactionParams: { signers: [supplyKey], @@ -511,11 +503,16 @@ describe("TokenBurnTransaction", function () { }); it("(#8) Burns an amount of 18,446,744,073,709,551,615 (uint64 max) fungible tokens", async function () { - const supplyKey = await getPrivateKey(this, "ed25519"); + const tokenId = await createToken( + this, + true, + treasuryAccountId, + supplyKey, + ); try { await JSONRPCRequest(this, "burnToken", { - tokenId: await createToken(this, true, supplyKey), + tokenId, amount: "18446744073709551615", commonTransactionParams: { signers: [supplyKey], @@ -530,40 +527,55 @@ describe("TokenBurnTransaction", function () { }); it("(#9) Burns an amount of 10,000 fungible tokens with 2 decimals", async function () { - const supplyKey = await getPrivateKey(this, "ed25519"); - const tokenId = await createToken(this, true, supplyKey, null, null, 2); + const tokenId = await createToken( + this, + true, + treasuryAccountId, + supplyKey, + fungibleInitialSupply, + null, + null, + 2, + ); const amount = "10000"; - expect( - ( - await JSONRPCRequest(this, "burnToken", { - tokenId, - amount, - commonTransactionParams: { - signers: [supplyKey], - }, - }) - ).newTotalSupply, - ).to.equal((BigInt(fungibleInitialSupply) - BigInt(amount)).toString()); - await verifyFungibleTokenBurn(tokenId, amount); + const newTotalSupply = ( + await JSONRPCRequest(this, "burnToken", { + tokenId, + amount, + commonTransactionParams: { + signers: [supplyKey], + }, + }) + ).newTotalSupply; + + expect(newTotalSupply).to.equal( + (BigInt(fungibleInitialSupply) - BigInt(amount)).toString(), + ); + await verifyFungibleTokenBurn( + tokenId, + treasuryAccountId, + fungibleInitialSupply, + amount, + ); }); it("(#10) Burns an amount of 10,000 fungible tokens with 1,000 max supply", async function () { - const supplyKey = await getPrivateKey(this, "ed25519"); + const tokenId = await createToken( + this, + true, + treasuryAccountId, + supplyKey, + "1000", + null, + null, + null, + "1000", + ); try { await JSONRPCRequest(this, "burnToken", { - tokenId: await createToken( - this, - true, - supplyKey, - null, - null, - null, - "1000", - null, - "1000", - ), + tokenId, amount: "10000", commonTransactionParams: { signers: [supplyKey], @@ -578,16 +590,17 @@ describe("TokenBurnTransaction", function () { }); it("(#11) Burns fungible tokens with the treasury account frozen", async function () { - const supplyKey = await getPrivateKey(this, "ed25519"); const freezeKey = await getPrivateKey(this, "ecdsaSecp256k1"); const tokenId = await createToken( this, true, + treasuryAccountId, supplyKey, null, null, null, null, + null, freezeKey, ); @@ -616,9 +629,16 @@ describe("TokenBurnTransaction", function () { }); it("(#12) Burns paused fungible tokens", async function () { - const supplyKey = await getPrivateKey(this, "ed25519"); const pauseKey = await getPrivateKey(this, "ecdsaSecp256k1"); - const tokenId = await createToken(this, true, supplyKey, null, pauseKey); + const tokenId = await createToken( + this, + true, + treasuryAccountId, + supplyKey, + null, + null, + pauseKey, + ); await JSONRPCRequest(this, "pauseToken", { tokenId, @@ -644,8 +664,20 @@ describe("TokenBurnTransaction", function () { }); it("(#13) Burns an amount of 1,000,000 NFTs", async function () { - const supplyKey = await getPrivateKey(this, "ed25519"); - const tokenId = await createToken(this, false, supplyKey); + const tokenId = await createToken( + this, + false, + treasuryAccountId, + supplyKey, + ); + + await JSONRPCRequest(this, "mintToken", { + tokenId, + metadata: nonFungibleMetadata, + commonTransactionParams: { + signers: [supplyKey], + }, + }); try { await JSONRPCRequest(this, "burnToken", { @@ -665,52 +697,111 @@ describe("TokenBurnTransaction", function () { }); describe("Serial Numbers", function () { + let supplyKey: string; + + this.beforeEach(async function () { + supplyKey = await getPrivateKey(this, "ed25519"); + }); + it("(#1) Burns an NFT", async function () { - const supplyKey = await getPrivateKey(this, "ed25519"); - const tokenId = await createToken(this, false, supplyKey); - - const serialNumber = "1"; - expect( - ( - await JSONRPCRequest(this, "burnToken", { - tokenId, - serialNumbers: [serialNumber], - commonTransactionParams: { - signers: [supplyKey], - }, - }) - ).newTotalSupply, - ).to.equal((nonFungibleMetadata.length - 1).toString()); - await verifyNonFungibleTokenBurn(tokenId, serialNumber); + const tokenId = await createToken( + this, + false, + treasuryAccountId, + supplyKey, + ); + + const serialNumbers = ( + await JSONRPCRequest(this, "mintToken", { + tokenId, + metadata: nonFungibleMetadata, + commonTransactionParams: { + signers: [supplyKey], + }, + }) + ).serialNumbers; + + const newTotalSupply = ( + await JSONRPCRequest(this, "burnToken", { + tokenId, + serialNumbers: [serialNumbers[0]], + commonTransactionParams: { + signers: [supplyKey], + }, + }) + ).newTotalSupply; + + expect(newTotalSupply).to.equal( + (nonFungibleMetadata.length - 1).toString(), + ); + await verifyNonFungibleTokenBurn( + tokenId, + treasuryAccountId, + serialNumbers[0], + ); }); it("(#2) Burns 3 NFTs", async function () { - const supplyKey = await getPrivateKey(this, "ed25519"); - const tokenId = await createToken(this, false, supplyKey); - - const serialNumbers = ["1", "2", "3"]; - expect( - ( - await JSONRPCRequest(this, "burnToken", { - tokenId, - serialNumbers, - commonTransactionParams: { - signers: [supplyKey], - }, - }) - ).newTotalSupply, - ).to.equal( + const tokenId = await createToken( + this, + false, + treasuryAccountId, + supplyKey, + ); + + const serialNumbers = ( + await JSONRPCRequest(this, "mintToken", { + tokenId, + metadata: nonFungibleMetadata, + commonTransactionParams: { + signers: [supplyKey], + }, + }) + ).serialNumbers; + + const newTotalSupply = ( + await JSONRPCRequest(this, "burnToken", { + tokenId, + serialNumbers, + commonTransactionParams: { + signers: [supplyKey], + }, + }) + ).newTotalSupply; + + expect(newTotalSupply).to.equal( (nonFungibleMetadata.length - serialNumbers.length).toString(), ); - await verifyNonFungibleTokenBurn(tokenId, serialNumbers[0]); - await verifyNonFungibleTokenBurn(tokenId, serialNumbers[1]); - await verifyNonFungibleTokenBurn(tokenId, serialNumbers[2]); + for ( + let serialNumber = 0; + serialNumber < serialNumbers.length; + serialNumber++ + ) { + await verifyNonFungibleTokenBurn( + tokenId, + treasuryAccountId, + serialNumbers[serialNumber], + ); + } }); it("(#3) Burns 3 NFTs but one is already burned", async function () { - const supplyKey = await getPrivateKey(this, "ed25519"); - const tokenId = await createToken(this, false, supplyKey); - const serialNumbers = ["1", "2", "3"]; + const tokenId = await createToken( + this, + false, + treasuryAccountId, + supplyKey, + ); + + const serialNumbers = ( + await JSONRPCRequest(this, "mintToken", { + tokenId, + metadata: nonFungibleMetadata, + commonTransactionParams: { + signers: [supplyKey], + }, + }) + ).serialNumbers; const lastSerialNumber = serialNumbers[serialNumbers.length - 1]; await JSONRPCRequest(this, "burnToken", { @@ -738,8 +829,20 @@ describe("TokenBurnTransaction", function () { }); it("(#4) Burns no NFTs", async function () { - const supplyKey = await getPrivateKey(this, "ed25519"); - const tokenId = await createToken(this, false, supplyKey); + const tokenId = await createToken( + this, + false, + treasuryAccountId, + supplyKey, + ); + + await JSONRPCRequest(this, "mintToken", { + tokenId, + metadata: nonFungibleMetadata, + commonTransactionParams: { + signers: [supplyKey], + }, + }); try { await JSONRPCRequest(this, "burnToken", { @@ -757,11 +860,24 @@ describe("TokenBurnTransaction", function () { }); it("(#5) Burns an NFT that doesn't exist", async function () { - const supplyKey = await getPrivateKey(this, "ed25519"); + const tokenId = await createToken( + this, + false, + treasuryAccountId, + supplyKey, + ); + + await JSONRPCRequest(this, "mintToken", { + tokenId, + metadata: nonFungibleMetadata, + commonTransactionParams: { + signers: [supplyKey], + }, + }); try { await JSONRPCRequest(this, "burnToken", { - tokenId: await createToken(this, false, supplyKey), + tokenId, serialNumbers: ["12345678"], commonTransactionParams: { signers: [supplyKey], @@ -776,19 +892,30 @@ describe("TokenBurnTransaction", function () { }); it("(#6) Burns NFTs with the treasury account frozen", async function () { - const supplyKey = await getPrivateKey(this, "ed25519"); const freezeKey = await getPrivateKey(this, "ecdsaSecp256k1"); const tokenId = await createToken( this, false, + treasuryAccountId, supplyKey, null, null, null, null, + null, freezeKey, ); + const serialNumbers = ( + await JSONRPCRequest(this, "mintToken", { + tokenId, + metadata: nonFungibleMetadata, + commonTransactionParams: { + signers: [supplyKey], + }, + }) + ).serialNumbers; + await JSONRPCRequest(this, "freezeToken", { tokenId, accountId: treasuryAccountId, @@ -800,7 +927,7 @@ describe("TokenBurnTransaction", function () { try { await JSONRPCRequest(this, "burnToken", { tokenId, - serialNumbers: ["1"], + serialNumbers: [serialNumbers[0]], commonTransactionParams: { signers: [supplyKey], }, @@ -814,9 +941,26 @@ describe("TokenBurnTransaction", function () { }); it("(#7) Burns paused NFTs", async function () { - const supplyKey = await getPrivateKey(this, "ed25519"); const pauseKey = await getPrivateKey(this, "ecdsaSecp256k1"); - const tokenId = await createToken(this, false, supplyKey, null, pauseKey); + const tokenId = await createToken( + this, + false, + treasuryAccountId, + supplyKey, + null, + null, + pauseKey, + ); + + const serialNumbers = ( + await JSONRPCRequest(this, "mintToken", { + tokenId, + metadata: nonFungibleMetadata, + commonTransactionParams: { + signers: [supplyKey], + }, + }) + ).serialNumbers; await JSONRPCRequest(this, "pauseToken", { tokenId, @@ -828,7 +972,7 @@ describe("TokenBurnTransaction", function () { try { await JSONRPCRequest(this, "burnToken", { tokenId, - serialNumbers: ["1"], + serialNumbers: [serialNumbers[0]], commonTransactionParams: { signers: [supplyKey], }, @@ -842,22 +986,31 @@ describe("TokenBurnTransaction", function () { }); it("(#8) Burns fungible tokens with serial numbers", async function () { - const supplyKey = await getPrivateKey(this, "ed25519"); - const pauseKey = await getPrivateKey(this, "ecdsaSecp256k1"); - const tokenId = await createToken(this, true, supplyKey); - - expect( - ( - await JSONRPCRequest(this, "burnToken", { - tokenId, - serialNumbers: ["1"], - commonTransactionParams: { - signers: [supplyKey], - }, - }) - ).newTotalSupply, - ).to.equal(fungibleInitialSupply); - await verifyFungibleTokenBurn(tokenId, "0"); + const tokenId = await createToken( + this, + true, + treasuryAccountId, + supplyKey, + fungibleInitialSupply, + ); + + const newTotalSupply = ( + await JSONRPCRequest(this, "burnToken", { + tokenId, + serialNumbers: ["1"], + commonTransactionParams: { + signers: [supplyKey], + }, + }) + ).newTotalSupply; + + expect(newTotalSupply).to.equal(fungibleInitialSupply); + await verifyFungibleTokenBurn( + tokenId, + treasuryAccountId, + fungibleInitialSupply, + "0", + ); }); }); }); diff --git a/src/utils/helpers/key.ts b/src/utils/helpers/key.ts index de0f60a..0f21647 100644 --- a/src/utils/helpers/key.ts +++ b/src/utils/helpers/key.ts @@ -1,11 +1,10 @@ import { proto } from "@hashgraph/proto"; import { PublicKey } from "@hashgraph/sdk"; -import mirrorNodeClient from "@services/MirrorNodeClient"; +import { JSONRPCRequest } from "@services/Client"; import consensusInfoClient from "@services/ConsensusInfoClient"; import { keyTypeConvertFunctions } from "@constants/key-type"; -import { JSONRPCRequest } from "@services/Client"; /** * Retrieves the encoded hexadecimal representation of a specified dynamic key diff --git a/src/utils/helpers/token.ts b/src/utils/helpers/token.ts index d8006e6..d83ea5a 100644 --- a/src/utils/helpers/token.ts +++ b/src/utils/helpers/token.ts @@ -4,6 +4,8 @@ import consensusInfoClient from "@services/ConsensusInfoClient"; import mirrorNodeClient from "@services/MirrorNodeClient"; import { JSONRPCRequest } from "@services/Client"; +import { retryOnError } from "@helpers/retry-on-error"; + /** * Verifies that a token has been deleted by checking both the Consensus Info * and the Mirror Node API. @@ -42,3 +44,181 @@ export const getNewFungibleTokenId = async ( return tokenResponse.tokenId; }; + +/** + * Creates a token with given parameters. + * + * @async + * @param {string} mochaTestContext - The context of the Mocha test. If provided, the test will be skipped if the method is not implemented. + * @param {boolean} fungible - Should the created token be fungible or non-fungible? + * @param {string} treasuryAccountId - The ID of the desired treasury account for the token. + * @param {string | null} supplyKey - The desired supply key for the token. + * @param {string | null} adminKey - The desired admin key for the token. + * @param {string | null} pauseKey - The desired pause key for the token. + * @param {string | null} decimals - The desired number of decimals for the token. + * @param {string | null} maxSupply - The desired max supply for the token. + * @param {string | null} freezeKey - The desired freeze key for the token. + * @returns {Promise} - The ID of the newly created token. + */ +export async function createToken( + mochaTestContext: any, + fungible: boolean, + treasuryAccountId: string, + supplyKey: string | null = null, + initialSupply: string | null = null, + adminKey: string | null = null, + pauseKey: string | null = null, + decimals: number | null = null, + maxSupply: string | null = null, + freezeKey: string | null = null, +): Promise { + const params: Record = { + name: "testname", + symbol: "testsymbol", + treasuryAccountId, + }; + + if (fungible) { + params.tokenType = "ft"; + } else { + params.tokenType = "nft"; + } + + // Add the supply key if its provided. + if (supplyKey) { + params.supplyKey = supplyKey; + } + + // Add the initial supply if its provided. + if (initialSupply) { + params.initialSupply = initialSupply; + } + + // Add and sign with the admin key if its provided. + if (adminKey) { + params.adminKey = adminKey; + params.commonTransactionParams = { + signers: [adminKey], + }; + } + + // Add the pause key if its provided. + if (pauseKey) { + params.pauseKey = pauseKey; + } + + // Add the decimals if its provided. + if (decimals) { + params.decimals = decimals; + } + + // Add the max supply if its provided. + if (maxSupply) { + params.supplyType = "finite"; + params.maxSupply = maxSupply; + } + + // Add the freeze key if its provided. + if (freezeKey) { + params.freezeKey = freezeKey; + } + + const tokenId = ( + await JSONRPCRequest(mochaTestContext, "createToken", params) + ).tokenId; + + return tokenId; +} + +/** + * Verify an amount of fungible token was burned. + * + * @async + * @param {string} tokenId - The ID of the token burned. + * @param {string} treasuryAccountId - The ID of the treasury account of the burned token. + * @param {string} initialSupply - The supply of the token before the burn. + * @param {string} amount - The amount of the token burned. + */ +export async function verifyFungibleTokenBurn( + tokenId: string, + treasuryAccountId: string, + initialSupply: string, + amount: string, +) { + const consensusNodeInfo = + await consensusInfoClient.getBalance(treasuryAccountId); + expect(consensusNodeInfo.tokens?.get(tokenId)?.toString()).to.equal( + (BigInt(initialSupply) - BigInt(amount)).toString(), + ); + + await retryOnError(async () => { + const mirrorNodeInfo = await mirrorNodeClient.getTokenRelationships( + treasuryAccountId, + tokenId, + ); + + let foundToken = false; + for (let i = 0; i < mirrorNodeInfo.tokens.length; i++) { + if (mirrorNodeInfo.tokens[i].token_id === tokenId) { + expect(mirrorNodeInfo.tokens[i].balance.toString()).to.equal( + (BigInt(initialSupply) - BigInt(amount)).toString(), + ); + foundToken = true; + break; + } + } + + if (!foundToken) { + expect.fail("Token ID not found"); + } + }); +} + +/** + * Verify an NFT was burned. + * + * @async + * @param {string} tokenId - The ID of the token burned. + * @param {string} treasuryAccountId - The ID of the treasury account of the burned token. + * @param {string} serialNumber - The serial number of the NFT burned. + */ +export async function verifyNonFungibleTokenBurn( + tokenId: string, + treasuryAccountId: string, + serialNumber: string, +) { + // Query the consensus node. Should throw since the NFT shouldn't exist anymore. + let foundNft = true; + try { + const consensusNodeInfo = await consensusInfoClient.getTokenNftInfo( + tokenId, + serialNumber, + ); + } catch (err: any) { + foundNft = false; + } + + // Make sure the NFT was not found. + expect(foundNft).to.be.false; + + // Query the mirror node. + await retryOnError(async () => { + const mirrorNodeInfo = await mirrorNodeClient.getAccountNfts( + treasuryAccountId, + tokenId, + ); + foundNft = false; + for (let i = 0; i < mirrorNodeInfo.nfts.length; i++) { + if ( + mirrorNodeInfo.nfts[i].token_id === tokenId && + mirrorNodeInfo.nfts[i].serial_number.toString() === serialNumber + ) { + foundNft = true; + break; + } + } + + // Make sure the NFT was not found. + expect(foundNft).to.be.false; + }); +}