diff --git a/spell/cspell-list.txt b/spell/cspell-list.txt index e5797ec973..3b21d64e1b 100644 --- a/spell/cspell-list.txt +++ b/spell/cspell-list.txt @@ -1,3 +1,4 @@ +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAU Aksakov Aliaksandr alnum diff --git a/src/benchmarks/nft/run.ts b/src/benchmarks/nft/run.ts new file mode 100644 index 0000000000..0054c518f1 --- /dev/null +++ b/src/benchmarks/nft/run.ts @@ -0,0 +1,131 @@ +import "@ton/test-utils"; +import type { Address } from "@ton/core"; +import { Cell, beginCell, contractAddress } from "@ton/core"; + +import { + generateResults, + generateCodeSizeResults, + printBenchmarkTable, + type BenchmarkResult, + type CodeSizeResult, +} from "@/benchmarks/utils/gas"; +import { resolve } from "path"; +import { readFileSync } from "fs"; +import { posixNormalize } from "@/utils/filePath"; + +import { NFTCollection } from "@/benchmarks/nft/tact/output/collection_NFTCollection"; +import type { RoyaltyParams } from "@/benchmarks/nft/tact/output/collection_NFTCollection"; +import { NFTItem } from "@/benchmarks/nft/tact/output/collection_NFTItem"; + +import benchmarkResults from "@/benchmarks/nft/gas.json"; +import benchmarkCodeSizeResults from "@/benchmarks/nft/size.json"; + +const loadFunCNFTBoc = () => { + const bocCollection = readFileSync( + posixNormalize(resolve(__dirname, "./func/output/nft-collection.boc")), + ); + + const bocItem = readFileSync( + posixNormalize(resolve(__dirname, "./func/output/nft-item.boc")), + ); + + return { bocCollection, bocItem }; +}; + +const fromInitCollection = ( + owner: Address, + index: bigint, + content: Cell, + royaltyParams: RoyaltyParams, +) => { + const nftData = loadFunCNFTBoc(); + const __code = Cell.fromBoc(nftData.bocCollection)[0]!; + + const royaltyCell = beginCell() + .storeUint(royaltyParams.nominator, 16) + .storeUint(royaltyParams.dominator, 16) + .storeAddress(royaltyParams.owner) + .endCell(); + + const __data = beginCell() + .storeAddress(owner) + .storeUint(index, 64) + .storeRef(content) + .storeRef(Cell.fromBoc(nftData.bocItem)[0]!) + .storeRef(royaltyCell) + .endCell(); + + const __gen_init = { code: __code, data: __data }; + const address = contractAddress(0, __gen_init); + return Promise.resolve(new NFTCollection(address, __gen_init)); +}; + +const fromInitItem = ( + _owner: Address | null, + _content: Cell | null, + collectionAddress: Address, + itemIndex: bigint, +) => { + const nftData = loadFunCNFTBoc(); + const code = Cell.fromBoc(nftData.bocItem)[0]!; + + const data = beginCell() + .storeUint(itemIndex, 64) + .storeAddress(collectionAddress) + .endCell(); + + const init = { code, data }; + const address = contractAddress(0, init); + return Promise.resolve(new NFTItem(address, init)); +}; + +export const run = ( + testNFT: ( + benchmarkResults: BenchmarkResult, + codeSizeResults: CodeSizeResult, + fromInitCollection: ( + owner: Address, + index: bigint, + content: Cell, + royaltyParams: RoyaltyParams, + ) => Promise, + fromInitItem: ( + owner: Address | null, + content: Cell | null, + collectionAddress: Address, + itemIndex: bigint, + ) => Promise, + ) => void, +) => { + describe("NFT Gas Tests", () => { + const fullResults = generateResults(benchmarkResults); + const fullCodeSizeResults = generateCodeSizeResults( + benchmarkCodeSizeResults, + ); + + describe("func", () => { + const funcCodeSize = fullCodeSizeResults.at(0)!; + const funcResult = fullResults.at(0)!; + + testNFT(funcResult, funcCodeSize, fromInitCollection, fromInitItem); + }); + + describe("tact", () => { + const tactCodeSize = fullCodeSizeResults.at(-1)!; + const tactResult = fullResults.at(-1)!; + testNFT( + tactResult, + tactCodeSize, + NFTCollection.fromInit.bind(NFTCollection), + NFTItem.fromInit.bind(NFTItem), + ); + }); + + afterAll(() => { + printBenchmarkTable(fullResults, fullCodeSizeResults, { + implementationName: "FunC", + printMode: "full", + }); + }); + }); +}; diff --git a/src/benchmarks/nft/tact/item.tact b/src/benchmarks/nft/tact/item.tact index 33fc9eab2e..64cf18c916 100644 --- a/src/benchmarks/nft/tact/item.tact +++ b/src/benchmarks/nft/tact/item.tact @@ -49,7 +49,7 @@ contract NFTItem( throwUnless(NotInit, self.owner != null); throwUnless(IncorrectSender, sender() == self.owner); throwUnless(IncorrectForwardPayload, msg.forwardPayload.bits() >= 1); - forceBasechain(msg.newOwner); + forceBasechainFunc(msg.newOwner); let fwdFees = context().readForwardFee(); @@ -80,7 +80,7 @@ contract NFTItem( } if (needResponse) { - forceBasechain(msg.responseDestination!!); + forceBasechainFunc(msg.responseDestination!!); sendMsg( msg.responseDestination!!, restAmount, @@ -105,6 +105,8 @@ contract NFTItem( } } +asm fun forceBasechainFunc(address: Address) { REWRITESTDADDR DROP 333 THROWIF } + inline fun sendMsg(toAddress: Address, amount: Int, op: Int, queryId: Int, payload: Builder, sendMode: Int) { message(MessageParameters { bounce: false, diff --git a/src/benchmarks/nft/test.spec.ts b/src/benchmarks/nft/test.spec.ts new file mode 100644 index 0000000000..922876ddec --- /dev/null +++ b/src/benchmarks/nft/test.spec.ts @@ -0,0 +1,21 @@ +import { type FromInitItem, testItem } from "@/benchmarks/nft/tests/item"; +import { + type FromInitCollection, + testCollection, +} from "@/benchmarks/nft/tests/collection"; + +import type { BenchmarkResult, CodeSizeResult } from "@/benchmarks/utils/gas"; + +export const testNFT = ( + _benchmarkResults: BenchmarkResult, + _codeSizeResults: CodeSizeResult, + fromInitCollection: FromInitCollection, + fromInitItem: FromInitItem, +) => { + testItem(fromInitItem); + testCollection(fromInitCollection, fromInitItem); +}; + +import { run } from "@/benchmarks/nft/run"; + +run(testNFT); diff --git a/src/benchmarks/nft/tests/collection.ts b/src/benchmarks/nft/tests/collection.ts new file mode 100644 index 0000000000..a1f0bfe3ed --- /dev/null +++ b/src/benchmarks/nft/tests/collection.ts @@ -0,0 +1,851 @@ +import type { Address, Cell } from "@ton/core"; +import { beginCell, Dictionary } from "@ton/core"; +import type { + SandboxContract, + TreasuryContract, + SendMessageResult, +} from "@ton/sandbox"; +import { Blockchain } from "@ton/sandbox"; +import type { + DeployNFT, + GetRoyaltyParams, + BatchDeploy, + RoyaltyParams, + ReportRoyaltyParams, + InitNFTBody, + ChangeOwner, +} from "@/benchmarks/nft/tact/output/collection_NFTCollection"; +import { + storeReportRoyaltyParams, + storeRoyaltyParams, + type NFTCollection, +} from "@/benchmarks/nft/tact/output/collection_NFTCollection"; +import { + storeInitNFTBody, + type NFTItem, +} from "@/benchmarks/nft/tact/output/item_NFTItem"; +import "@ton/test-utils"; +import { step } from "@/test/allure/allure"; +import { + getOwner, + getNextItemIndex, + loadGetterTupleNFTData, + Storage, + ErrorCodes, + TestValues, + dictDeployNFTItem, +} from "@/benchmarks/nft/tests/utils"; + +type FromInitItem = ( + owner: Address | null, + content: Cell | null, + collectionAddress: Address, + itemIndex: bigint, +) => Promise; + +export type FromInitCollection = ( + owner: Address, + index: bigint, + content: Cell, + royaltyParams: RoyaltyParams, +) => Promise; + +const globalSetup = async (fromInitCollection: FromInitCollection) => { + const blockchain = await Blockchain.create(); + + const owner = await blockchain.treasury("owner"); + const notOwner = await blockchain.treasury("notOwner"); + + const defaultCommonContent = beginCell() + .storeStringTail("common") + .endCell(); + const defaultCollectionContent = beginCell() + .storeStringTail("collectionContent") + .endCell(); + const defaultNFTContent = beginCell().storeStringTail("1.json").endCell(); + const defaultContent = beginCell() + .storeRef(defaultCollectionContent) + .storeRef(defaultCommonContent) + .endCell(); + + const royaltyParams: RoyaltyParams = { + $$type: "RoyaltyParams", + nominator: 1n, + dominator: 100n, + owner: owner.address, + }; + + const collectionNFT = blockchain.openContract( + await fromInitCollection( + owner.address, + 0n, + defaultContent, + royaltyParams, + ), + ); + const deployCollectionMsg: GetRoyaltyParams = { + $$type: "GetRoyaltyParams", + queryId: 0n, + }; + + const deployResult = await collectionNFT.send( + owner.getSender(), + { value: Storage.DeployAmount }, + deployCollectionMsg, + ); + await step( + "Check that deployResult.transactions has correct transaction (collection)", + () => { + expect(deployResult.transactions).toHaveTransaction({ + from: owner.address, + to: collectionNFT.address, + deploy: true, + success: true, + }); + }, + ); + + return { + blockchain, + owner, + notOwner, + defaultContent, + defaultCommonContent, + defaultCollectionContent, + defaultNFTContent, + royaltyParams, + collectionNFT, + }; +}; + +const testEmptyMessages = (fromInitCollection: FromInitCollection) => { + const setup = async () => { + return await globalSetup(fromInitCollection); + }; + + describe("Empty messages cases", () => { + it("should ignore empty messages", async () => { + const { collectionNFT, owner } = await setup(); + const trxResult = await collectionNFT.send( + owner.getSender(), + { value: Storage.DeployAmount }, + null, + ); + await step( + "Check that trxResult.transactions has correct transaction (empty messages)", + () => { + expect(trxResult.transactions).toHaveTransaction({ + from: owner.address, + to: collectionNFT.address, + success: true, + }); + }, + ); + }); + }); +}; + +const setupWithItem = async ( + fromInitCollection: FromInitCollection, + fromInitItem: FromInitItem, +) => { + /** + * Helper function to deploy an NFT item + * @param itemIndex - Index of the NFT to deploy + * @param collectionNFT - Collection contract instance + * @param sender - Sender of the deployment transaction + * @param owner - Owner of the deployed NFT + * @returns Promise resolving to the NFT item contract and transaction result + */ + const deployNFT = async ( + itemIndex: bigint, + collectionNFT: SandboxContract, + sender: SandboxContract, + owner: SandboxContract, + defaultNFTContent: Cell, + blockchain: Blockchain, + ) => { + const initNFTBody: InitNFTBody = { + $$type: "InitNFTBody", + owner: owner.address, + content: defaultNFTContent, + }; + + const mintMsg: DeployNFT = { + $$type: "DeployNFT", + queryId: 1n, + itemIndex: itemIndex, + amount: Storage.NftMintAmount, + initNFTBody: beginCell() + .store(storeInitNFTBody(initNFTBody)) + .endCell(), + }; + + const itemNFT = blockchain.openContract( + await fromInitItem(null, null, collectionNFT.address, itemIndex), + ); + + const trxResult = await collectionNFT.send( + sender.getSender(), + { value: Storage.DeployAmount }, + mintMsg, + ); + return { itemNFT, trxResult }; + }; + return { ...(await globalSetup(fromInitCollection)), deployNFT }; +}; + +const testDeployItem = ( + fromInitCollection: FromInitCollection, + fromInitItem: FromInitItem, +) => { + describe("NFT deploy cases", () => { + const setup = async () => { + return await setupWithItem(fromInitCollection, fromInitItem); + }; + + it("should mint NFTItem correctly", async () => { + const { + collectionNFT, + owner, + defaultNFTContent, + blockchain, + deployNFT, + } = await setup(); + const nextItemIndex = await getNextItemIndex(collectionNFT); + const { itemNFT } = await deployNFT( + nextItemIndex, + collectionNFT, + owner, + owner, + defaultNFTContent, + blockchain, + ); + const nftData = await itemNFT.getGetNftData(); + + await step( + "Check that nftData.content equals defaultNFTContent", + () => { + expect(nftData.content).toEqualCell(defaultNFTContent); + }, + ); + await step("Check that nftData.owner equals owner.address", () => { + expect(nftData.owner).toEqualAddress(owner.address); + }); + await step("Check that nftData.itemIndex is nextItemIndex", () => { + expect(nftData.itemIndex).toBe(nextItemIndex); + }); + await step( + "Check that nftData.collectionAddress equals collectionNFT.address", + () => { + expect(nftData.collectionAddress).toEqualAddress( + collectionNFT.address, + ); + }, + ); + }); + + it("should not mint NFTItem if not owner", async () => { + const { + collectionNFT, + defaultNFTContent, + blockchain, + notOwner, + deployNFT, + } = await setup(); + const nextItemIndex = await getNextItemIndex(collectionNFT); + const { trxResult } = await deployNFT( + nextItemIndex, + collectionNFT, + notOwner, + notOwner, + defaultNFTContent, + blockchain, + ); + await step( + "Check that trx.transactions has correct transaction (not owner mint)", + () => { + expect(trxResult.transactions).toHaveTransaction({ + from: notOwner.address, + to: collectionNFT.address, + success: false, + exitCode: ErrorCodes.NotOwner, + }); + }, + ); + }); + + it("should not deploy previous nft", async () => { + const { + collectionNFT, + owner, + defaultNFTContent, + blockchain, + deployNFT, + } = await setup(); + let nextItemIndex: bigint = await getNextItemIndex(collectionNFT); + for (let i = 0; i < 10; i++) { + await deployNFT( + nextItemIndex, + collectionNFT, + owner, + owner, + defaultNFTContent, + blockchain, + ); + nextItemIndex++; + } + const { itemNFT, trxResult } = await deployNFT( + 0n, + collectionNFT, + owner, + owner, + defaultNFTContent, + blockchain, + ); + await step( + "Check that trx.transactions has correct transaction (should not deploy previous nft)", + () => { + expect(trxResult.transactions).toHaveTransaction({ + from: collectionNFT.address, + to: itemNFT.address, + deploy: false, + success: false, + exitCode: ErrorCodes.InvalidData, + }); + }, + ); + }); + + it("shouldn't mint item itemIndex > nextItemIndex", async () => { + const { + collectionNFT, + owner, + defaultNFTContent, + blockchain, + deployNFT, + } = await setup(); + const nextItemIndex = await getNextItemIndex(collectionNFT); + const { trxResult } = await deployNFT( + nextItemIndex + 1n, + collectionNFT, + owner, + owner, + defaultNFTContent, + blockchain, + ); + await step( + "Check that trx.transactions has correct transaction (itemIndex > nextItemIndex)", + () => { + expect(trxResult.transactions).toHaveTransaction({ + from: owner.address, + to: collectionNFT.address, + success: false, + exitCode: ErrorCodes.IncorrectIndex, + }); + }, + ); + }); + + it("should get nft by itemIndex correctly", async () => { + const { + collectionNFT, + owner, + defaultNFTContent, + blockchain, + deployNFT, + } = await setup(); + const nextItemIndex = await getNextItemIndex(collectionNFT); + + // deploy new nft to get itemIndex + await deployNFT( + nextItemIndex, + collectionNFT, + owner, + owner, + defaultNFTContent, + blockchain, + ); + + const nftAddress = + await collectionNFT.getGetNftAddressByIndex(nextItemIndex); + const newNFT = blockchain.getContract(nftAddress); + const getData = await (await newNFT).get("get_nft_data"); + const dataNFT = loadGetterTupleNFTData(getData.stack); + + await step("Check that dataNFT.itemIndex is nextItemIndex", () => { + expect(dataNFT.itemIndex).toBe(nextItemIndex); + }); + + await step( + "Check that dataNFT.collectionAddress equals collectionNFT.address", + () => { + expect(dataNFT.collectionAddress).toEqualAddress( + collectionNFT.address, + ); + }, + ); + }); + }); +}; + +const testRoyalty = (fromInitCollection: FromInitCollection) => { + const setup = async () => { + return await globalSetup(fromInitCollection); + }; + + describe("Royalty cases", () => { + it("should send royalty msg correctly", async () => { + const { collectionNFT, owner, royaltyParams } = await setup(); + const queryId = 0n; + + const msg: GetRoyaltyParams = { + $$type: "GetRoyaltyParams", + queryId: BigInt(queryId), + }; + + const trxResult = await collectionNFT.send( + owner.getSender(), + { value: Storage.DeployAmount }, + msg, + ); + + await step( + "Check that trxResult.transactions has correct transaction (royalty msg)", + () => { + expect(trxResult.transactions).toHaveTransaction({ + from: owner.address, + to: collectionNFT.address, + success: true, + }); + }, + ); + + const expectedMsg: ReportRoyaltyParams = { + $$type: "ReportRoyaltyParams", + queryId, + params: royaltyParams, + }; + + expect(trxResult.transactions).toHaveTransaction({ + from: collectionNFT.address, + to: owner.address, + body: beginCell() + .store(storeReportRoyaltyParams(expectedMsg)) + .endCell(), + }); + }); + + it("should get royalty params correctly", async () => { + const { collectionNFT, royaltyParams } = await setup(); + const currRoyaltyParams = await collectionNFT.getRoyaltyParams(); + + const currCell = beginCell() + .store(storeRoyaltyParams(currRoyaltyParams)) + .endCell(); + const expectedCell = beginCell() + .store(storeRoyaltyParams(royaltyParams)) + .endCell(); + expect(currCell).toEqualCell(expectedCell); + }); + }); +}; + +const testBatchDeploy = ( + fromInitCollection: FromInitCollection, + fromInitItem: FromInitItem, +) => { + const setup = async () => { + return await globalSetup(fromInitCollection); + }; + + describe("Batch mint cases", () => { + /** + * Helper function to batch mint NFTs + * @param collectionNFT - Collection contract instance + * @param sender - Sender of the batch mint transaction + * @param owner - Owner of the minted NFTs + * @param count - Number of NFTs to mint + * @param extra - Optional extra index to mint + * @returns Promise resolving to the transaction result + */ + const batchMintNFTProcess = async ( + collectionNFT: SandboxContract, + sender: SandboxContract, + owner: SandboxContract, + defaultNFTContent: Cell, + count: bigint, + extra: bigint = -1n, + ): Promise => { + const dict = Dictionary.empty( + Dictionary.Keys.BigUint(64), + dictDeployNFTItem, + ); + let i: bigint = 0n; + + const initNFTBody: InitNFTBody = { + $$type: "InitNFTBody", + owner: owner.address, + content: defaultNFTContent, + }; + + while (i < count) { + dict.set(i, { + amount: Storage.NftMintAmount, + initNFTBody: initNFTBody, + }); + i++; + } + + if (extra != -1n) { + // helper condition if we wanna deploy some extra nft + dict.set(extra, { + amount: Storage.NftMintAmount, + initNFTBody: initNFTBody, + }); + } + + const batchMintNFT: BatchDeploy = { + $$type: "BatchDeploy", + queryId: 0n, + deployList: beginCell().storeDictDirect(dict).endCell(), + }; + + return await collectionNFT.send( + sender.getSender(), + { + value: + Storage.BatchDeployAmount * + (count + TestValues.ExtraValues.BatchMultiplier), + }, + batchMintNFT, + ); + }; + + it.skip("test max batch mint", async () => { + const { collectionNFT, owner, defaultNFTContent } = await setup(); + let L = 1n; // left border + let R = 1000n; // right border + while (R - L > 1) { + const M = (L + R) / 2n; + const trxResult = await batchMintNFTProcess( + collectionNFT, + owner, + owner, + defaultNFTContent, + M, + ); + try { + expect(trxResult.transactions).toHaveTransaction({ + from: owner.address, + to: collectionNFT.address, + success: true, + }); + L = M; + } catch { + R = M; + } + } + console.log("maximum batch amount is", L); + }); + + it("should batch mint correctly", async () => { + const { collectionNFT, owner, defaultNFTContent, blockchain } = + await setup(); + const count = 100n; + const trxResult = await batchMintNFTProcess( + collectionNFT, + owner, + owner, + defaultNFTContent, + count, + ); + + await step( + "Check that trxResult.transactions has correct transaction (batch mint success)", + () => { + expect(trxResult.transactions).toHaveTransaction({ + from: owner.address, + to: collectionNFT.address, + success: true, + }); + }, + ); + const itemNFT = blockchain.openContract( + await fromInitItem( + null, + null, + collectionNFT.address, + count - 1n, + ), + ); + + // it was deployed, that's why we can get it + await step( + "Check that itemNFT.getGetNftData() has property itemIndex", + async () => { + expect(await itemNFT.getGetNftData()).toHaveProperty( + "itemIndex", + count - 1n, + ); + }, + ); + }); + + it("shouldn't batch mint more than 250 items", async () => { + const { collectionNFT, owner, defaultNFTContent } = await setup(); + const trxResult = await batchMintNFTProcess( + collectionNFT, + owner, + owner, + defaultNFTContent, + 250n + 1n, + ); + + await step( + "Check that trxResult.transactions has correct transaction (should not batch mint more than 250)", + () => { + expect(trxResult.transactions).toHaveTransaction({ + from: owner.address, + to: collectionNFT.address, + success: false, + }); + }, + ); + }); + + it("should return error if not owner tries to batch mint", async () => { + const { collectionNFT, owner, defaultNFTContent, notOwner } = + await setup(); + const trxResult = await batchMintNFTProcess( + collectionNFT, + notOwner, + owner, + defaultNFTContent, + 10n, + ); + + await step( + "Check that trxResult.transactions has correct transaction (not owner batch mint)", + () => { + expect(trxResult.transactions).toHaveTransaction({ + from: notOwner.address, + to: collectionNFT.address, + success: false, + exitCode: ErrorCodes.NotOwner, + }); + }, + ); + }); + }); +}; + +const testChangeOwner = (fromInitCollection: FromInitCollection) => { + const setup = async () => { + return await globalSetup(fromInitCollection); + }; + + describe("Change owner cases", () => { + it("should transfer ownership correctly", async () => { + const { collectionNFT, owner, notOwner } = await setup(); + const changeOwnerMsg: ChangeOwner = { + $$type: "ChangeOwner", + queryId: 1n, + newOwner: notOwner.address, + }; + + const trxResult = await collectionNFT.send( + owner.getSender(), + { value: Storage.ChangeOwnerAmount }, + changeOwnerMsg, + ); + + await step( + "Check that trxResult.transactions has correct transaction (owner transfer ownership)", + () => { + expect(trxResult.transactions).toHaveTransaction({ + from: owner.address, + to: collectionNFT.address, + success: true, + }); + }, + ); + await step( + "Check that collectionNFT.getOwner() equals notOwner.address", + async () => { + expect(await getOwner(collectionNFT)).toEqualAddress( + notOwner.address, + ); + }, + ); + }); + + it("should return error if not owner tries to transfer ownership", async () => { + const { collectionNFT, owner, notOwner } = await setup(); + const changeOwnerMsg: ChangeOwner = { + $$type: "ChangeOwner", + queryId: 1n, + newOwner: owner.address, + }; + + const trxResult = await collectionNFT.send( + notOwner.getSender(), + { value: Storage.ChangeOwnerAmount }, + changeOwnerMsg, + ); + + await step( + "Check that trxResult.transactions has correct transaction (not owner transfer ownership)", + () => { + expect(trxResult.transactions).toHaveTransaction({ + from: notOwner.address, + to: collectionNFT.address, + success: false, + exitCode: ErrorCodes.NotOwner, + }); + }, + ); + }); + }); +}; + +const testGetCollectionData = (fromInitCollection: FromInitCollection) => { + const setup = async () => { + return await globalSetup(fromInitCollection); + }; + + describe("Get collection data cases", () => { + it("should get collection data correctly", async () => { + const { collectionNFT, owner, defaultCollectionContent } = + await setup(); + const staticData = await collectionNFT.getGetCollectionData(); + await step( + "Check that staticData.owner equals owner.address", + () => { + expect(staticData.owner).toEqualAddress(owner.address); + }, + ); + await step("Check that staticData.nextItemIndex is 0", () => { + expect(staticData.nextItemIndex).toBe(0n); + }); + await step( + "Check that staticData.collectionContent equals defaultCollectionContent", + () => { + expect(staticData.collectionContent).toEqualCell( + defaultCollectionContent, + ); + }, + ); + }); + }); +}; + +const testGetNftAddressByIndex = ( + fromInitCollection: FromInitCollection, + fromInitItem: FromInitItem, +) => { + const setup = async () => { + return await setupWithItem(fromInitCollection, fromInitItem); + }; + + describe("Get nft address by index cases", () => { + it("should get nft address by index correctly", async () => { + const { + collectionNFT, + owner, + defaultNFTContent, + blockchain, + deployNFT, + } = await setup(); + const nftAddress = await collectionNFT.getGetNftAddressByIndex(0n); + + const { trxResult } = await deployNFT( + 0n, + collectionNFT, + owner, + owner, + defaultNFTContent, + blockchain, + ); + + await step( + "Check that trxDeploy.transactions has correct transaction (deploy item)", + () => { + expect(trxResult.transactions).toHaveTransaction({ + from: collectionNFT.address, + to: nftAddress, + success: true, + }); + }, + ); + }); + }); +}; + +const testGetRoyaltyParams = (fromInitCollection: FromInitCollection) => { + const setup = async () => { + return await globalSetup(fromInitCollection); + }; + + describe("Get royalty params cases", () => { + it("should get royalty params correctly", async () => { + const { collectionNFT, royaltyParams } = await setup(); + const currRoyaltyParams = await collectionNFT.getRoyaltyParams(); + expect( + beginCell() + .store(storeRoyaltyParams(currRoyaltyParams)) + .asSlice(), + ).toEqualSlice( + beginCell().store(storeRoyaltyParams(royaltyParams)).asSlice(), + ); + }); + }); +}; + +const testGetNftContent = (fromInitCollection: FromInitCollection) => { + const setup = async () => { + return await globalSetup(fromInitCollection); + }; + + describe("Get nft content cases", () => { + it("should get nft content correctly", async () => { + const { collectionNFT, defaultContent, defaultCommonContent } = + await setup(); + const content = await collectionNFT.getGetNftContent( + 0n, + defaultContent, + ); + + // standard detail + const expectedContent = beginCell() + .storeUint(1, 8) + .storeSlice(defaultCommonContent.asSlice()) + .storeRef(defaultContent) + .endCell(); + + await step( + "Check that content equals expectedContent (nft content)", + () => { + expect(content).toEqualCell(expectedContent); + }, + ); + }); + }); +}; + +export const testCollection = ( + fromInitCollection: FromInitCollection, + fromInitItem: FromInitItem, +) => { + describe("NFT Collection Contract", () => { + testEmptyMessages(fromInitCollection); + testDeployItem(fromInitCollection, fromInitItem); + testRoyalty(fromInitCollection); + testBatchDeploy(fromInitCollection, fromInitItem); + testChangeOwner(fromInitCollection); + testGetCollectionData(fromInitCollection); + testGetNftAddressByIndex(fromInitCollection, fromInitItem); + testGetRoyaltyParams(fromInitCollection); + testGetNftContent(fromInitCollection); + }); +}; diff --git a/src/benchmarks/nft/tests/item.ts b/src/benchmarks/nft/tests/item.ts new file mode 100644 index 0000000000..216af62f01 --- /dev/null +++ b/src/benchmarks/nft/tests/item.ts @@ -0,0 +1,539 @@ +import { Address, toNano, type Cell } from "@ton/core"; +import { beginCell } from "@ton/core"; +import type { SandboxContract, TreasuryContract } from "@ton/sandbox"; +import { Blockchain } from "@ton/sandbox"; +import { + OwnershipAssigned, + type GetStaticData, + type InitNFTBody, +} from "@/benchmarks/nft/tact/output/collection_NFTCollection"; +import { + storeInitNFTBody, + type NFTItem, + IncorrectForwardPayload, + IncorrectDeployer, + Excesses, +} from "@/benchmarks/nft/tact/output/item_NFTItem"; +import "@ton/test-utils"; +import { setStoragePrices } from "@/test/utils/gasUtils"; +import { step } from "@/test/allure/allure"; +import { + Storage, + ErrorCodes, + getItemOwner, + sendTransfer, +} from "@/benchmarks/nft/tests/utils"; + +import { + testTransferFee, + testTransferForwardFeeDouble, +} from "@/benchmarks/nft/tests/transfer-fee"; + +export type FromInitItem = ( + owner: Address | null, + content: Cell | null, + collectionAddress: Address, + itemIndex: bigint, +) => Promise; + +const messageGetStaticData = async ( + sender: SandboxContract, + itemNFT: SandboxContract, +) => { + const msg: GetStaticData = { + $$type: "GetStaticData", + queryId: 1n, + }; + const trxResult = await itemNFT.send( + sender.getSender(), + { value: Storage.DeployAmount }, + msg, + ); + return trxResult; +}; + +const globalSetup = async (fromInitItem: FromInitItem) => { + const blockchain = await Blockchain.create(); + const config = blockchain.config; + + blockchain.setConfig( + // set StorageFee to 0 in blockchain + setStoragePrices(config, { + unixTimeSince: 0, + bitPricePerSecond: 0n, + cellPricePerSecond: 0n, + masterChainBitPricePerSecond: 0n, + masterChainCellPricePerSecond: 0n, + }), + ); + const owner = await blockchain.treasury("owner"); + const notOwner = await blockchain.treasury("notOwner"); + const emptyAddress = null; + const defaultContent = beginCell().endCell(); + + const itemNFT = blockchain.openContract( + await fromInitItem(null, null, owner.address, 0n), + ); + + const deployItemMsg: InitNFTBody = { + $$type: "InitNFTBody", + owner: owner.address, + content: defaultContent, + }; + + const deployResult = await itemNFT.send( + owner.getSender(), + { value: Storage.DeployAmount }, + beginCell().store(storeInitNFTBody(deployItemMsg)).asSlice(), + ); + + await step( + "Check that deployResult.transactions has correct transaction", + () => { + expect(deployResult.transactions).toHaveTransaction({ + from: owner.address, + to: itemNFT.address, + deploy: true, + success: true, + }); + }, + ); + + const notInitItem = blockchain.openContract( + await fromInitItem(null, null, owner.address, 1n), + ); + + await messageGetStaticData(owner, notInitItem); // deploy in sandbox + + return { + blockchain, + itemNFT, + owner, + notOwner, + defaultContent, + emptyAddress, + notInitItem, + }; +}; + +const testGetStaticData = (fromInitItem: FromInitItem) => { + const setup = async () => { + return await globalSetup(fromInitItem); + }; + + describe("Get Static Data", () => { + it("should get static data correctly", async () => { + const { itemNFT, owner } = await setup(); + const trxResult = await messageGetStaticData(owner, itemNFT); + await step( + "Check that trxResult.transactions has correct transaction (get static data)", + () => { + expect(trxResult.transactions).toHaveTransaction({ + from: owner.address, + to: itemNFT.address, + success: true, + }); + }, + ); + }); + it("should throw exit code if nft not initialized", async () => { + const { notInitItem, owner } = await setup(); + const trxResult = await messageGetStaticData(owner, notInitItem); + await step( + "Check that trxResult.transactions has correct transaction (not initialized)", + () => { + expect(trxResult.transactions).toHaveTransaction({ + from: owner.address, + to: notInitItem.address, + success: false, + exitCode: ErrorCodes.NotInit, + }); + }, + ); + }); + }); +}; + +const testGetNftData = (fromInitItem: FromInitItem) => { + const setup = async () => { + return await globalSetup(fromInitItem); + }; + + describe("Get Nft Data", () => { + it("should get nft data correctly when item is initialized", async () => { + const { itemNFT, owner, defaultContent } = await setup(); + + const staticData = await itemNFT.getGetNftData(); + await step("Check that staticData.init is -1", () => { + expect(staticData.init).toBe(-1n); + }); + await step("Check that staticData.itemIndex is 0", () => { + expect(staticData.itemIndex).toBe(0n); + }); + await step( + "Check that staticData.collectionAddress equals owner.address", + () => { + expect(staticData.collectionAddress).toEqualAddress( + owner.address, + ); + }, + ); + await step( + "Check that staticData.owner equals owner.address", + () => { + expect(staticData.owner).toEqualAddress(owner.address); + }, + ); + await step( + "Check that staticData.content equals defaultContent", + () => { + expect(staticData.content).toEqualCell(defaultContent); + }, + ); + }); + + it("should get nft data correctly when item is not initialized", async () => { + const { notInitItem, owner } = await setup(); + + const staticData = await notInitItem.getGetNftData(); + + await step("Check that staticData.init is 0", () => { + expect(staticData.init).toBe(0n); + }); + + await step("Check that staticData.itemIndex is 1", () => { + expect(staticData.itemIndex).toBe(1n); + }); + + await step( + "Check that staticData.collectionAddress equals owner.address", + () => { + expect(staticData.collectionAddress).toEqualAddress( + owner.address, + ); + }, + ); + + await step( + "Check that staticData.owner equals owner.address", + () => { + expect(staticData.owner).toEqual(null); + }, + ); + await step( + "Check that staticData.content equals defaultContent", + () => { + expect(staticData.content).toEqual(null); + }, + ); + }); + }); +}; + +const testDeploy = (fromInitItem: FromInitItem) => { + const setup = async () => { + return await globalSetup(fromInitItem); + }; + + describe("Deploy", () => { + it("should deploy correctly", async () => { + await setup(); + }); + + it("should throw exit code if item is already initialized", async () => { + const { itemNFT, owner, defaultContent } = await setup(); + + const deployItemMsg: InitNFTBody = { + $$type: "InitNFTBody", + owner: owner.address, + content: defaultContent, + }; + + const trxResult = await itemNFT.send( + owner.getSender(), + { value: Storage.DeployAmount }, + beginCell().store(storeInitNFTBody(deployItemMsg)).asSlice(), + ); + + await step( + "Check that trxResult.transactions has correct transaction (item is already initialized)", + () => { + expect(trxResult.transactions).toHaveTransaction({ + from: owner.address, + to: itemNFT.address, + success: false, + exitCode: ErrorCodes.InvalidData, + }); + }, + ); + }); + + it("should throw exit code if deploy not from collection", async () => { + const { notInitItem, notOwner, defaultContent } = await setup(); + + const deployItemMsg: InitNFTBody = { + $$type: "InitNFTBody", + owner: notOwner.address, + content: defaultContent, + }; + + const deployResult = await notInitItem.send( + notOwner.getSender(), + { value: Storage.DeployAmount }, + beginCell().store(storeInitNFTBody(deployItemMsg)).asSlice(), + ); + + await step( + "Check that trxResult.transactions has correct transaction (deploy not from collection)", + () => { + expect(deployResult.transactions).toHaveTransaction({ + from: notOwner.address, + to: notInitItem.address, + success: false, + exitCode: Number(IncorrectDeployer), + }); + }, + ); + }); + }); +}; + +const testTransfer = (fromInitItem: FromInitItem) => { + const setup = async () => { + return await globalSetup(fromInitItem); + }; + + describe("Transfer", () => { + it("should throw exit code if item is not initialized", async () => { + const { notInitItem, owner } = await setup(); + const trxResult = await sendTransfer( + notInitItem, + owner.getSender(), + Storage.DeployAmount, + owner.address, + null, + 0n, + ); + await step( + "Check that trxResult.transactions has correct transaction (item is not initialized)", + () => { + expect(trxResult.transactions).toHaveTransaction({ + from: owner.address, + to: notInitItem.address, + success: false, + exitCode: ErrorCodes.NotInit, + }); + }, + ); + }); + + it("should return error if not owner tries to transfer ownership", async () => { + const { itemNFT, notOwner, emptyAddress } = await setup(); + const trxResult = await sendTransfer( + itemNFT, + notOwner.getSender(), + Storage.DeployAmount, + notOwner.address, + emptyAddress, + 0n, + ); + await step( + "Check that trxResult.transactions has correct transaction (not owner should not be able to transfer ownership)", + () => { + expect(trxResult.transactions).toHaveTransaction({ + from: notOwner.address, + to: itemNFT.address, + success: false, + exitCode: ErrorCodes.NotOwner, + }); + }, + ); + }); + + it("should throw exit code if forward payload is less than 1", async () => { + const { itemNFT, owner } = await setup(); + const trxResult = await sendTransfer( + itemNFT, + owner.getSender(), + Storage.DeployAmount, + owner.address, + null, + 0n, + beginCell().asSlice(), + ); + + await step( + "Check that trxResult.transactions has correct transaction (forward payload is less than 1)", + () => { + expect(trxResult.transactions).toHaveTransaction({ + from: owner.address, + to: itemNFT.address, + success: false, + exitCode: Number(IncorrectForwardPayload), + }); + }, + ); + }); + + it("should throw exit code if newOwner isn't from basechain", async () => { + const { itemNFT, owner } = await setup(); + const trxResult = await sendTransfer( + itemNFT, + owner.getSender(), + Storage.DeployAmount, + Address.parse( + "Ef8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAU", + ), + null, + 0n, + ); + + await step( + "Check that trxResult.transactions has correct transaction (newOwner is not from basechain)", + () => { + expect(trxResult.transactions).toHaveTransaction({ + from: owner.address, + to: itemNFT.address, + success: false, + exitCode: ErrorCodes.InvalidDestinationWorkchain, + }); + }, + ); + }); + + it("should assign ownership correctly", async () => { + const { itemNFT, owner, notOwner } = await setup(); + const oldOwner = await getItemOwner(itemNFT); + await step("Check that oldOwner equals owner.address", () => { + expect(oldOwner).toEqualAddress(owner.address); + }); + const trxRes = await sendTransfer( + itemNFT, + owner.getSender(), + Storage.DeployAmount, + notOwner.address, + owner.address, + 1n, + ); + const newOwner = await getItemOwner(itemNFT); + await step("Check that newOwner equals notOwner.address", () => { + expect(newOwner).toEqualAddress(notOwner.address); + }); + await step( + "Check that trxRes.transactions has correct transaction (ownership assigned)", + () => { + expect(trxRes.transactions).toHaveTransaction({ + from: owner.address, + to: itemNFT.address, + success: true, + }); + }, + ); + }); + + it("should transfer ownership without any messages", async () => { + const { itemNFT, owner, notOwner, emptyAddress } = await setup(); + const trxRes = await sendTransfer( + itemNFT, + owner.getSender(), + Storage.DeployAmount, + notOwner.address, + emptyAddress, + 0n, + ); + const newOwner = await getItemOwner(itemNFT); + await step( + "Check that newOwner equals notOwner.address (no messages)", + () => { + expect(newOwner).toEqualAddress(notOwner.address); + }, + ); + await step( + "Check that trxRes.transactions does NOT have transaction from itemNFT.address (no messages)", + () => { + expect(trxRes.transactions).not.toHaveTransaction({ + from: itemNFT.address, + }); + }, + ); + }); + + it("should transfer ownership with forward payload", async () => { + const { itemNFT, owner, notOwner } = await setup(); + const forwardAmount = toNano(0.1); + const forwardPayload = beginCell() + .storeStringTail("test forward payload") + .asSlice(); + + const trxRes = await sendTransfer( + itemNFT, + owner.getSender(), + Storage.DeployAmount, + notOwner.address, + null, + forwardAmount, + forwardPayload, + ); + + const expectedBody = beginCell() + .storeUint(OwnershipAssigned, 32) + .storeUint(0n, 64) + .storeAddress(owner.address) + .storeSlice(forwardPayload) + .endCell(); + + await step( + "Check that trxRes.transactions has correct transaction (ownership assigned with forward payload)", + () => { + expect(trxRes.transactions).toHaveTransaction({ + from: itemNFT.address, + to: notOwner.address, + value: forwardAmount, + body: expectedBody, + }); + }, + ); + }); + + it("should transfer ownership with response destination", async () => { + const { itemNFT, owner, notOwner } = await setup(); + const forwardAmount = 0n; + + const trxRes = await sendTransfer( + itemNFT, + owner.getSender(), + Storage.DeployAmount, + notOwner.address, + owner.address, + forwardAmount, + ); + + const expectedBody = beginCell() + .storeUint(Excesses, 32) + .storeUint(0n, 64) + .endCell(); + + await step( + "Check that trxRes.transactions has correct transaction (ownership assigned with response destination)", + () => { + expect(trxRes.transactions).toHaveTransaction({ + from: itemNFT.address, + to: owner.address, + body: expectedBody, + }); + }, + ); + }); + }); +}; + +export const testItem = (fromInitItem: FromInitItem) => { + describe("NFT Item Contract", () => { + testGetStaticData(fromInitItem); + testGetNftData(fromInitItem); + testDeploy(fromInitItem); + testTransfer(fromInitItem); + testTransferFee(fromInitItem); + testTransferForwardFeeDouble(fromInitItem); + }); +}; diff --git a/src/benchmarks/nft/tests/transfer-fee.ts b/src/benchmarks/nft/tests/transfer-fee.ts new file mode 100644 index 0000000000..e6cfd452ee --- /dev/null +++ b/src/benchmarks/nft/tests/transfer-fee.ts @@ -0,0 +1,313 @@ +import type { Address, Cell } from "@ton/core"; +import { beginCell } from "@ton/core"; +import { Blockchain } from "@ton/sandbox"; +import type { InitNFTBody } from "@/benchmarks/nft/tact/output/collection_NFTCollection"; +import { + storeInitNFTBody, + type NFTItem, + InvalidFees, +} from "@/benchmarks/nft/tact/output/item_NFTItem"; +import "@ton/test-utils"; +import { step } from "@/test/allure/allure"; +import { setStoragePrices } from "@/test/utils/gasUtils"; +import { Storage, sendTransfer } from "@/benchmarks/nft/tests/utils"; + +const globalSetup = async ( + fromInitItem: ( + owner: Address | null, + content: Cell | null, + collectionAddress: Address, + itemIndex: bigint, + ) => Promise, +) => { + const blockchain = await Blockchain.create(); + const config = blockchain.config; + blockchain.setConfig( + setStoragePrices(config, { + unixTimeSince: 0, + bitPricePerSecond: 0n, + cellPricePerSecond: 0n, + masterChainBitPricePerSecond: 0n, + masterChainCellPricePerSecond: 0n, + }), + ); + const owner = await blockchain.treasury("owner"); + + const notOwner = await blockchain.treasury("notOwner"); + const emptyAddress = null; + const defaultContent = beginCell().endCell(); + const itemNFT = blockchain.openContract( + await fromInitItem(null, null, owner.address, 0n), + ); + const deployItemMsg: InitNFTBody = { + $$type: "InitNFTBody", + owner: owner.address, + content: defaultContent, + }; + const deployResult = await itemNFT.send( + owner.getSender(), + { value: Storage.DeployAmount }, + beginCell().store(storeInitNFTBody(deployItemMsg)).asSlice(), + ); + await step( + "Check that deployResult.transactions has correct transaction", + () => { + expect(deployResult.transactions).toHaveTransaction({ + from: owner.address, + to: itemNFT.address, + deploy: true, + success: true, + }); + }, + ); + + const balance = await ( + await blockchain.getContract(itemNFT.address) + ).balance; + const fwdFee = Storage.ForwardFee.Base; + const fwdFeeDouble = Storage.ForwardFee.Double; + + return { + blockchain, + itemNFT, + owner, + notOwner, + defaultContent, + emptyAddress, + balance, + fwdFee, + fwdFeeDouble, + }; +}; + +export const testTransferFee = ( + fromInitItem: ( + owner: Address | null, + content: Cell | null, + collectionAddress: Address, + itemIndex: bigint, + ) => Promise, +) => { + async function setup() { + return await globalSetup(fromInitItem); + } + + describe("Transfer ownership Fee cases", () => { + // implementation detail + it("should return error if forward amount is too much", async () => { + const { itemNFT, owner, notOwner, emptyAddress } = await setup(); + + const trxResult = await sendTransfer( + itemNFT, + owner.getSender(), + Storage.DeployAmount, + notOwner.address, + emptyAddress, + Storage.TransferAmount, + ); + await step( + "Check that trxResult.transactions has correct transaction (transfer forward amount too much)", + () => { + expect(trxResult.transactions).toHaveTransaction({ + from: owner.address, + to: itemNFT.address, + success: false, + exitCode: Number(InvalidFees), + }); + }, + ); + }); + it("should return error if storage fee is not enough", async () => { + const { itemNFT, owner, notOwner, emptyAddress, balance, fwdFee } = + await setup(); + const trxResult = await sendTransfer( + itemNFT, + owner.getSender(), + Storage.TransferAmount + fwdFee, + notOwner.address, + emptyAddress, + Storage.TransferAmount + balance, + ); + await step( + "Check that trxResult.transactions has correct transaction (test transfer storage fee)", + () => { + expect(trxResult.transactions).toHaveTransaction({ + from: owner.address, + to: itemNFT.address, + success: false, + exitCode: Number(InvalidFees), + }); + }, + ); + }); + it("should work with 2 fwdFee on balance", async () => { + const { + balance, + fwdFee, + itemNFT, + owner, + notOwner, + emptyAddress, + blockchain, + } = await setup(); + const trxResult = await sendTransfer( + itemNFT, + owner.getSender(), + Storage.TransferAmount + Storage.MinTons + fwdFee, + notOwner.address, + emptyAddress, + Storage.TransferAmount + balance, + ); + expect(trxResult.transactions).toHaveTransaction({ + from: owner.address, + to: itemNFT.address, + success: true, + }); + await step( + "Check that trxResult.transactions has correct transaction (test transfer forward fee 2.0)", + () => { + expect(trxResult.transactions).toHaveTransaction({ + from: owner.address, + to: itemNFT.address, + success: true, + }); + }, + ); + const newBalance = await ( + await blockchain.getContract(itemNFT.address) + ).balance; + await step( + "Check that balance is less than Storage.MinTons (test transfer forward fee 2.0)", + () => { + expect(newBalance).toBeLessThan(Storage.MinTons); + }, + ); + }); + it("should work with 1 fwdFee on balance", async () => { + const { itemNFT, owner, notOwner, emptyAddress, balance } = + await setup(); + const trxResult = await sendTransfer( + itemNFT, + owner.getSender(), + Storage.TransferAmount + Storage.ForwardFee.Base, + notOwner.address, + emptyAddress, + Storage.TransferAmount + balance - Storage.MinTons, + beginCell() + .storeUint(1, 1) + .storeStringTail("testing") + .asSlice(), + ); + expect(trxResult.transactions).toHaveTransaction({ + from: owner.address, + to: itemNFT.address, + success: true, + }); + await step( + "Check that trxResult.transactions has correct transaction (test transfer forward fee single)", + () => { + expect(trxResult.transactions).toHaveTransaction({ + from: owner.address, + to: itemNFT.address, + success: true, + }); + }, + ); + }); + }); +}; + +export const testTransferForwardFeeDouble = ( + fromInitItem: ( + owner: Address | null, + content: Cell | null, + collectionAddress: Address, + itemIndex: bigint, + ) => Promise, +) => { + describe("Transfer forward fee double cases", function () { + const setup = async () => { + return await globalSetup(fromInitItem); + }; + it("should fail with only one fwd fee on balance", async () => { + const { itemNFT, owner, notOwner, balance, fwdFeeDouble } = + await setup(); + const trxResult = await sendTransfer( + itemNFT, + owner.getSender(), + Storage.TransferAmount + fwdFeeDouble, + notOwner.address, + owner.address, + Storage.TransferAmount + balance - Storage.MinTons, + beginCell() + .storeUint(1, 1) + .storeStringTail("testing") + .asSlice(), + ); + expect(trxResult.transactions).toHaveTransaction({ + from: owner.address, + to: itemNFT.address, + success: false, + exitCode: Number(InvalidFees), + }); + await step( + "Check that trxResult.transactions has correct transaction (double forward fee, not enough for both)", + () => { + expect(trxResult.transactions).toHaveTransaction({ + from: owner.address, + to: itemNFT.address, + success: false, + exitCode: Number(InvalidFees), + }); + }, + ); + }); + it("should work with 2 fwdFee on balance", async () => { + const { + itemNFT, + owner, + notOwner, + balance, + fwdFeeDouble, + blockchain, + } = await setup(); + const trxResult = await sendTransfer( + itemNFT, + owner.getSender(), + Storage.TransferAmount + 2n * fwdFeeDouble, + notOwner.address, + owner.address, + Storage.TransferAmount + balance - Storage.MinTons, + beginCell() + .storeUint(1, 1) + .storeStringTail("testing") + .asSlice(), + ); + expect(trxResult.transactions).toHaveTransaction({ + from: owner.address, + to: itemNFT.address, + success: true, + }); + const newBalance = await ( + await blockchain.getContract(itemNFT.address) + ).balance; + expect(newBalance).toBeLessThan(Storage.MinTons); + await step( + "Check that trxResult.transactions has correct transaction (double forward fee, enough for both)", + () => { + expect(trxResult.transactions).toHaveTransaction({ + from: owner.address, + to: itemNFT.address, + success: true, + }); + }, + ); + await step( + "Check that balance is less than Storage.MinTons (double forward fee)", + () => { + expect(newBalance).toBeLessThan(Storage.MinTons); + }, + ); + }); + }); +}; diff --git a/src/benchmarks/nft/tests/utils.ts b/src/benchmarks/nft/tests/utils.ts new file mode 100644 index 0000000000..d195f0c21a --- /dev/null +++ b/src/benchmarks/nft/tests/utils.ts @@ -0,0 +1,228 @@ +// Type imports +import type { + Address, + Slice, + Builder, + TupleItem, + TupleItemInt, + TupleItemSlice, + TupleItemCell, + Sender, +} from "@ton/core"; +// Value imports +import { beginCell, toNano } from "@ton/core"; + +import type { SandboxContract, SendMessageResult } from "@ton/sandbox"; +// NFT Collection imports +import type { + InitNFTBody, + NFTCollection, + Transfer, +} from "@/benchmarks/nft/tact/output/collection_NFTCollection"; +import { + IncorrectDeployer, + IncorrectIndex, + IncorrectSender, + InvalidData, + InvalidDestinationWorkchain, + InvalidFees, + NotInit, +} from "@/benchmarks/nft/tact/output/collection_NFTCollection"; +import { loadInitNFTBody } from "@/benchmarks/nft/tact/output/collection_NFTCollection"; + +// NFT Item imports +import type { NFTData } from "@/benchmarks/nft/tact/output/item_NFTItem"; +import { + storeInitNFTBody, + type NFTItem, +} from "@/benchmarks/nft/tact/output/item_NFTItem"; + +import "@ton/test-utils"; + +/** Storage and transaction related constants */ +export const Storage = { + /** Minimum amount of TONs required for storage */ + MinTons: 50000000n, + /** Amount of TONs for deployment operations */ + DeployAmount: toNano("0.1"), + /** Amount of TONs for transfer operations */ + TransferAmount: toNano("1"), + /** Amount of TONs for batch deployment */ + BatchDeployAmount: toNano("100"), + /** Amount of TONs for ownership change */ + ChangeOwnerAmount: 100000000n, + /** Amount of TONs for NFT minting */ + NftMintAmount: 10000000n, + /** Forward fee values */ + ForwardFee: { + /** Base forward fee for single message */ + Base: 623605n, + /** Forward fee for double message */ + Double: 729606n, + }, +}; + +/** Error codes */ +export const ErrorCodes = { + /** Error code for not initialized contract */ + NotInit: Number(NotInit), + /** Error code for not owner */ + NotOwner: Number(IncorrectSender), + /** Error code for invalid fees */ + InvalidFees: Number(InvalidFees), + /** Error code for incorrect index */ + IncorrectIndex: Number(IncorrectIndex), + /** Error code for incorrect deployer */ + IncorrectDeployer: Number(IncorrectDeployer), + /** Error code for invalid data */ + InvalidData: Number(InvalidData), + /** Error code for invalid destination workchain */ + InvalidDestinationWorkchain: Number(InvalidDestinationWorkchain), +}; + +/** Test related constants */ +export const TestValues = { + /** Default item index used in tests */ + ItemIndex: 100n, + /** Batch operation sizes */ + BatchSize: { + Min: 1n, + Max: 250n, + Default: 50n, + OverLimit: 260n, + Small: 10n, + }, + /** Range for random number generation */ + RandomRange: 1337, + /** Royalty parameters */ + Royalty: { + Nominator: 1n, + Dominator: 100n, + }, + /** Bit sizes for different data types */ + BitSizes: { + Uint1: 1, + Uint8: 8, + Uint16: 16, + Uint32: 32, + Uint64: 64, + }, + /** Additional test values */ + ExtraValues: { + BatchMultiplier: 10n, + }, +}; + +/** Dictionary type for NFT deployment data */ +type dictDeployNFT = { + amount: bigint; + initNFTBody: InitNFTBody; +}; + +/** Dictionary value parser for NFT deployment */ +export const dictDeployNFTItem = { + serialize: (src: dictDeployNFT, builder: Builder) => { + builder + .storeCoins(src.amount) + .storeRef( + beginCell().store(storeInitNFTBody(src.initNFTBody)).endCell(), + ); + }, + parse: (src: Slice) => { + return { + amount: src.loadCoins(), + initNFTBody: loadInitNFTBody(src.loadRef().asSlice()), + }; + }, +}; + +/** + * Helper function to load NFT data from a tuple of contract getter results + * @param source - Array of tuple items containing NFT data + * @returns Parsed NFT data object + */ +export function loadGetterTupleNFTData(source: TupleItem[]): NFTData { + const _init = (source[0] as TupleItemInt).value; + const _index = (source[1] as TupleItemInt).value; + const _collectionAddress = (source[2] as TupleItemSlice).cell + .asSlice() + .loadAddress(); + const _owner = (source[3] as TupleItemSlice).cell.asSlice().loadAddress(); + const _content = (source[4] as TupleItemCell).cell; + return { + $$type: "NFTData" as const, + init: _init, + itemIndex: _index, + collectionAddress: _collectionAddress, + owner: _owner, + content: _content, + }; +} + +/** + * Retrieves the owner of the NFT collection. + * @param collection - The sandbox contract instance of the NFT collection. + * @returns The address of the collection owner. + */ +export const getOwner = async ( + collection: SandboxContract, +): Promise
=> { + const res = await collection.getGetCollectionData(); + return res.owner; +}; + +/** + * Retrieves the next item index from the NFT collection. + * @param collection - The sandbox contract instance of the NFT collection. + * @returns The next item index to be minted. + */ +export const getNextItemIndex = async ( + collection: SandboxContract, +): Promise => { + const res = await collection.getGetCollectionData(); + return res.nextItemIndex; +}; + +/** + * Retrieves the owner of a specific NFT item. + * @param item - The sandbox contract instance of the NFT item. + * @returns The address of the NFT item's owner. + */ +export const getItemOwner = async ( + item: SandboxContract, +): Promise
=> { + const res = await item.getGetNftData(); + return res.owner!; +}; + +/** + * Sends a transfer transaction for an NFT item. + * @param itemNFT - The sandbox contract instance of the NFT item to be transferred. + * @param from - The sender of the transaction. + * @param value - The value to be sent with the transaction. + * @param newOwner - The address of the new owner. + * @param responseDestination - The address where the response should be sent. + * @param forwardAmount - The amount of TONs to be forwarded to the new owner. + * @param forwardPayload - The payload to be forwarded to the new owner. + * @returns The result of the sent message. + */ +export const sendTransfer = async ( + itemNFT: SandboxContract, + from: Sender, + value: bigint, + newOwner: Address, + responseDestination: Address | null, + forwardAmount: bigint, + forwardPayload: Slice = beginCell().storeUint(0, 1).asSlice(), +): Promise => { + const msg: Transfer = { + $$type: "Transfer", + queryId: 0n, + newOwner: newOwner, + responseDestination: responseDestination, + customPayload: null, + forwardAmount: forwardAmount, + forwardPayload: forwardPayload, + }; + return await itemNFT.send(from, { value }, msg); +};