diff --git a/docs/test-specifications/token-service/TokenBurnTransaction.md b/docs/test-specifications/token-service/TokenBurnTransaction.md index 1e552af..3dbfd5e 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 @@ -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 new file mode 100644 index 0000000..79059da --- /dev/null +++ b/src/tests/token-service/test-token-burn-transaction.ts @@ -0,0 +1,1016 @@ +import { assert, expect } from "chai"; + +import { JSONRPCRequest } from "@services/Client"; + +import { setOperator } from "@helpers/setup-tests"; +import { getPrivateKey } from "@helpers/key"; +import { + createToken, + verifyFungibleTokenBurn, + verifyNonFungibleTokenBurn, +} from "@helpers/token"; + +/** + * 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. + + 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, + supplyKey, + fungibleInitialSupply, + ); + + const amount = "10"; + 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 a valid non-fungible token", async function () { + const supplyKey = await getPrivateKey(this, "ed25519"); + 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: [serialNumber], + commonTransactionParams: { + signers: [supplyKey], + }, + }); + + const newTotalSupply = ( + Number(nonFungibleMetadata.length) - 1 + ).toString(); + expect(response.newTotalSupply).to.equal(newTotalSupply); + + await verifyNonFungibleTokenBurn( + tokenId, + treasuryAccountId, + serialNumber, + ); + }); + + 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, + treasuryAccountId, + supplyKey, + null, + 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, + treasuryAccountId, + 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, + treasuryAccountId, + supplyKey, + null, + 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 supplyKey = await getPrivateKey(this, "ed25519"); + const adminKey = await getPrivateKey(this, "ecdsaSecp256k1"); + const tokenId = await createToken( + this, + true, + treasuryAccountId, + supplyKey, + null, + adminKey, + ); + + const incorrectKey = await getPrivateKey(this, "ecdsaSecp256k1"); + try { + await JSONRPCRequest(this, "burnToken", { + tokenId, + amount: "10", + commonTransactionParams: { + signers: [incorrectKey], + }, + }); + } 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, treasuryAccountId); + + 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, + treasuryAccountId, + supplyKey, + null, + null, + pauseKey, + ); + + await JSONRPCRequest(this, "pauseToken", { + tokenId, + commonTransactionParams: { + signers: [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 () { + 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 tokenId = await createToken( + this, + true, + treasuryAccountId, + supplyKey, + fungibleInitialSupply, + ); + + const amount = "1000000"; + 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 tokenId = await createToken( + this, + true, + treasuryAccountId, + supplyKey, + fungibleInitialSupply, + ); + + const amount = "0"; + 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 tokenId = await createToken( + this, + true, + treasuryAccountId, + supplyKey, + fungibleInitialSupply, + ); + + 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 tokenId = await createToken( + this, + true, + treasuryAccountId, + supplyKey, + fungibleInitialSupply, + ); + + const amount = "9223372036854775806"; + 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 tokenId = await createToken( + this, + true, + treasuryAccountId, + supplyKey, + fungibleInitialSupply, + ); + + const amount = "9223372036854775807"; + 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 tokenId = await createToken( + this, + true, + treasuryAccountId, + supplyKey, + ); + + try { + await JSONRPCRequest(this, "burnToken", { + tokenId, + 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 tokenId = await createToken( + this, + true, + treasuryAccountId, + supplyKey, + ); + + try { + await JSONRPCRequest(this, "burnToken", { + tokenId, + 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 tokenId = await createToken( + this, + true, + treasuryAccountId, + supplyKey, + ); + + try { + await JSONRPCRequest(this, "burnToken", { + tokenId, + 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 tokenId = await createToken( + this, + true, + treasuryAccountId, + supplyKey, + fungibleInitialSupply, + null, + null, + 2, + ); + + const amount = "10000"; + 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 tokenId = await createToken( + this, + true, + treasuryAccountId, + supplyKey, + "1000", + null, + null, + null, + "1000", + ); + + try { + await JSONRPCRequest(this, "burnToken", { + tokenId, + 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 freezeKey = await getPrivateKey(this, "ecdsaSecp256k1"); + const tokenId = await createToken( + this, + true, + treasuryAccountId, + supplyKey, + null, + 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 pauseKey = await getPrivateKey(this, "ecdsaSecp256k1"); + const tokenId = await createToken( + this, + true, + treasuryAccountId, + supplyKey, + null, + 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 tokenId = await createToken( + this, + false, + treasuryAccountId, + supplyKey, + ); + + await JSONRPCRequest(this, "mintToken", { + tokenId, + metadata: nonFungibleMetadata, + commonTransactionParams: { + signers: [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 () { + let supplyKey: string; + + this.beforeEach(async function () { + supplyKey = await getPrivateKey(this, "ed25519"); + }); + + it("(#1) Burns an NFT", async function () { + 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 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(), + ); + 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 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", { + tokenId, + serialNumbers: [lastSerialNumber], + 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 tokenId = await createToken( + this, + false, + treasuryAccountId, + supplyKey, + ); + + await JSONRPCRequest(this, "mintToken", { + tokenId, + metadata: nonFungibleMetadata, + commonTransactionParams: { + signers: [supplyKey], + }, + }); + + 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 () { + const tokenId = await createToken( + this, + false, + treasuryAccountId, + supplyKey, + ); + + await JSONRPCRequest(this, "mintToken", { + tokenId, + metadata: nonFungibleMetadata, + commonTransactionParams: { + signers: [supplyKey], + }, + }); + + try { + await JSONRPCRequest(this, "burnToken", { + tokenId, + serialNumbers: ["12345678"], + commonTransactionParams: { + signers: [supplyKey], + }, + }); + } catch (err: any) { + assert.equal(err.data.status, "INVALID_NFT_ID"); + return; + } + + assert.fail("Should throw an error"); + }); + + it("(#6) Burns NFTs with the treasury account frozen", async function () { + 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, + commonTransactionParams: { + signers: [freezeKey], + }, + }); + + try { + await JSONRPCRequest(this, "burnToken", { + tokenId, + serialNumbers: [serialNumbers[0]], + commonTransactionParams: { + signers: [supplyKey], + }, + }); + } catch (err: any) { + assert.equal(err.data.status, "ACCOUNT_FROZEN_FOR_TOKEN"); + return; + } + + assert.fail("Should throw an error"); + }); + + it("(#7) Burns paused NFTs", async function () { + const pauseKey = await getPrivateKey(this, "ecdsaSecp256k1"); + 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, + commonTransactionParams: { + signers: [pauseKey], + }, + }); + + try { + await JSONRPCRequest(this, "burnToken", { + tokenId, + serialNumbers: [serialNumbers[0]], + 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 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/token.ts b/src/utils/helpers/token.ts index 866478a..7ef352b 100644 --- a/src/utils/helpers/token.ts +++ b/src/utils/helpers/token.ts @@ -65,20 +65,20 @@ export async function createToken( 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 = { + const params: Record = { name: "testname", symbol: "testsymbol", treasuryAccountId, }; if (fungible) { - params.decimals = 0; params.tokenType = "ft"; } else { params.tokenType = "nft"; @@ -89,6 +89,11 @@ export async function createToken( 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; @@ -245,3 +250,95 @@ export async function verifyNonFungibleTokenMint( expect(foundNft).to.be.true; }); } + +/** + * 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; + }); +} \ No newline at end of file