diff --git a/src/constants.ts b/src/constants.ts index e19c22ef..9300d978 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -25,6 +25,8 @@ export const METACHAIN_ID = 4294967295; export const SDK_JS_SIGNER = "sdk-js"; export const UNKNOWN_SIGNER = "unknown"; +export const EGLD_IDENTIFIER_FOR_MULTI_ESDTNFT_TRANSFER = "EGLD-000000"; + /** * @deprecated */ diff --git a/src/transactionsFactories/smartContractTransactionsFactory.spec.ts b/src/transactionsFactories/smartContractTransactionsFactory.spec.ts index d108c103..096c37ea 100644 --- a/src/transactionsFactories/smartContractTransactionsFactory.spec.ts +++ b/src/transactionsFactories/smartContractTransactionsFactory.spec.ts @@ -316,6 +316,55 @@ describe("test smart contract transactions factory", function () { assert.deepEqual(transaction, transactionAbiAware); }); + it("should create 'Transaction' for execute and transfer native and nfts", async function () { + const sender = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + const contract = Address.fromBech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); + const func = "add"; + const gasLimit = 6000000n; + const args = [new U32Value(7)]; + + const firstToken = new Token({ identifier: "NFT-123456", nonce: 1n }); + const firstTransfer = new TokenTransfer({ token: firstToken, amount: 1n }); + const secondToken = new Token({ identifier: "NFT-123456", nonce: 42n }); + const secondTransfer = new TokenTransfer({ token: secondToken, amount: 1n }); + + const transaction = factory.createTransactionForExecute({ + sender: sender, + contract: contract, + function: func, + gasLimit: gasLimit, + arguments: args, + nativeTransferAmount: 1000000000000000000n, + tokenTransfers: [firstTransfer, secondTransfer], + }); + + const transactionAbiAware = abiAwareFactory.createTransactionForExecute({ + sender: sender, + contract: contract, + function: func, + gasLimit: gasLimit, + arguments: args, + nativeTransferAmount: 1000000000000000000n, + tokenTransfers: [firstTransfer, secondTransfer], + }); + + assert.equal(transaction.sender, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + assert.equal(transaction.receiver, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + + assert.isDefined(transaction.data); + assert.deepEqual( + transaction.data, + Buffer.from( + "MultiESDTNFTTransfer@00000000000000000500b9353fe8407f87310c87e12fa1ac807f0485da39d152@03@4e46542d313233343536@01@01@4e46542d313233343536@2a@01@45474c442d303030303030@@0de0b6b3a7640000@616464@07", + ), + ); + + assert.equal(transaction.gasLimit, gasLimit); + assert.equal(transaction.value, 0n); + + assert.deepEqual(transaction, transactionAbiAware); + }); + it("should create 'Transaction' for upgrade", async function () { const sender = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); const contract = Address.fromBech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); diff --git a/src/transactionsFactories/smartContractTransactionsFactory.ts b/src/transactionsFactories/smartContractTransactionsFactory.ts index c439139f..25a1d6ab 100644 --- a/src/transactionsFactories/smartContractTransactionsFactory.ts +++ b/src/transactionsFactories/smartContractTransactionsFactory.ts @@ -1,12 +1,12 @@ import { Address } from "../address"; -import { CONTRACT_DEPLOY_ADDRESS_HEX, VM_TYPE_WASM_VM } from "../constants"; -import { Err, ErrBadUsage } from "../errors"; +import { CONTRACT_DEPLOY_ADDRESS_HEX, EGLD_IDENTIFIER_FOR_MULTI_ESDTNFT_TRANSFER, VM_TYPE_WASM_VM } from "../constants"; +import { Err } from "../errors"; import { IAddress } from "../interface"; import { Logger } from "../logger"; import { ArgSerializer, CodeMetadata, ContractFunction, EndpointDefinition } from "../smartcontracts"; import { NativeSerializer } from "../smartcontracts/nativeSerializer"; import { isTyped } from "../smartcontracts/typesystem"; -import { TokenComputer, TokenTransfer } from "../tokens"; +import { Token, TokenComputer, TokenTransfer } from "../tokens"; import { Transaction } from "../transaction"; import { byteArrayToHex, utf8ToHex } from "../utils.codec"; import { TokenTransfersDataBuilder } from "./tokenTransfersDataBuilder"; @@ -91,19 +91,24 @@ export class SmartContractTransactionsFactory { tokenTransfers?: TokenTransfer[]; }): Transaction { const args = options.arguments || []; - const tokenTransfer = options.tokenTransfers || []; - const nativeTransferAmount = options.nativeTransferAmount ?? 0n; - const numberOfTokens = tokenTransfer.length; + let tokenTransfers = options.tokenTransfers || []; + let nativeTransferAmount = options.nativeTransferAmount ?? 0n; + let numberOfTokens = tokenTransfers.length; if (nativeTransferAmount && numberOfTokens) { - throw new ErrBadUsage("Can't send both native tokens and custom tokens(ESDT/NFT)"); + const nativeToken = new Token({ identifier: EGLD_IDENTIFIER_FOR_MULTI_ESDTNFT_TRANSFER }); + const nativeTransfer = new TokenTransfer({ token: nativeToken, amount: nativeTransferAmount }); + tokenTransfers.push(nativeTransfer); + + nativeTransferAmount = 0n; + numberOfTokens++; } let receiver = options.contract; let dataParts: string[] = []; if (numberOfTokens === 1) { - const transfer = tokenTransfer[0]; + const transfer = tokenTransfers[0]; if (this.tokenComputer.isFungible(transfer.token)) { dataParts = this.dataArgsBuilder.buildDataPartsForESDTTransfer(transfer); @@ -112,7 +117,7 @@ export class SmartContractTransactionsFactory { receiver = options.sender; } } else if (numberOfTokens > 1) { - dataParts = this.dataArgsBuilder.buildDataPartsForMultiESDTNFTTransfer(receiver, tokenTransfer); + dataParts = this.dataArgsBuilder.buildDataPartsForMultiESDTNFTTransfer(receiver, tokenTransfers); receiver = options.sender; } diff --git a/src/transactionsFactories/transferTransactionsFactory.spec.ts b/src/transactionsFactories/transferTransactionsFactory.spec.ts index 7251b9c4..d949f38f 100644 --- a/src/transactionsFactories/transferTransactionsFactory.spec.ts +++ b/src/transactionsFactories/transferTransactionsFactory.spec.ts @@ -5,7 +5,7 @@ import { Token, TokenTransfer } from "../tokens"; import { TransactionsFactoryConfig } from "./transactionsFactoryConfig"; import { TransferTransactionsFactory } from "./transferTransactionsFactory"; -describe("test transfer transcations factory", function () { +describe("test transfer transactions factory", function () { const config = new TransactionsFactoryConfig({ chainID: "D" }); const transferFactory = new TransferTransactionsFactory({ config: config, @@ -37,8 +37,8 @@ describe("test transfer transcations factory", function () { nativeAmount: 1000000000000000000n, }); - assert.equal(transaction.sender, alice.bech32()); - assert.equal(transaction.receiver, bob.bech32()); + assert.equal(transaction.sender, alice.toBech32()); + assert.equal(transaction.receiver, bob.toBech32()); assert.equal(transaction.value.valueOf(), 1000000000000000000n); assert.equal(transaction.gasLimit.valueOf(), 50000n); assert.deepEqual(transaction.data, new Uint8Array()); @@ -52,8 +52,8 @@ describe("test transfer transcations factory", function () { data: Buffer.from("test data"), }); - assert.equal(transaction.sender, alice.bech32()); - assert.equal(transaction.receiver, bob.bech32()); + assert.equal(transaction.sender, alice.toBech32()); + assert.equal(transaction.receiver, bob.toBech32()); assert.equal(transaction.value.valueOf(), 1000000000000000000n); assert.equal(transaction.gasLimit.valueOf(), 63500n); assert.deepEqual(transaction.data, Buffer.from("test data")); @@ -69,8 +69,8 @@ describe("test transfer transcations factory", function () { tokenTransfers: [transfer], }); - assert.equal(transaction.sender, alice.bech32()); - assert.equal(transaction.receiver, bob.bech32()); + assert.equal(transaction.sender, alice.toBech32()); + assert.equal(transaction.receiver, bob.toBech32()); assert.equal(transaction.value.valueOf(), 0n); assert.equal(transaction.gasLimit.valueOf(), 410000n); assert.deepEqual(transaction.data.toString(), "ESDTTransfer@464f4f2d313233343536@0f4240"); @@ -86,8 +86,8 @@ describe("test transfer transcations factory", function () { tokenTransfers: [transfer], }); - assert.equal(transaction.sender, alice.bech32()); - assert.equal(transaction.receiver, alice.bech32()); + assert.equal(transaction.sender, alice.toBech32()); + assert.equal(transaction.receiver, alice.toBech32()); assert.equal(transaction.value.valueOf(), 0n); assert.equal(transaction.gasLimit.valueOf(), 1210500n); assert.deepEqual( @@ -109,8 +109,8 @@ describe("test transfer transcations factory", function () { tokenTransfers: [firstTransfer, secondTransfer], }); - assert.equal(transaction.sender, alice.bech32()); - assert.equal(transaction.receiver, alice.bech32()); + assert.equal(transaction.sender, alice.toBech32()); + assert.equal(transaction.receiver, alice.toBech32()); assert.equal(transaction.value.valueOf(), 0n); assert.equal(transaction.gasLimit.valueOf(), 1466000n); assert.deepEqual( @@ -118,4 +118,49 @@ describe("test transfer transcations factory", function () { "MultiESDTNFTTransfer@8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8@02@4e46542d313233343536@0a@01@544553542d393837363534@01@01", ); }); + + it("should fail to create transaction for token transfers", async () => { + assert.throws(() => { + transferFactory.createTransactionForTokenTransfer({ + sender: alice, + receiver: bob, + }); + }, "No native token amount or token transfers provided"); + + assert.throws(() => { + const nft = new Token({ identifier: "NFT-123456", nonce: 10n }); + const transfer = new TokenTransfer({ token: nft, amount: 1n }); + + transferFactory.createTransactionForTokenTransfer({ + sender: alice, + receiver: bob, + tokenTransfers: [transfer], + data: Buffer.from("test"), + }); + }, "Can't set data field when sending esdt tokens"); + }); + + it("should create 'Transaction' for token transfers", async () => { + const firstNft = new Token({ identifier: "NFT-123456", nonce: 10n }); + const firstTransfer = new TokenTransfer({ token: firstNft, amount: 1n }); + + const secondNft = new Token({ identifier: "TEST-987654", nonce: 1n }); + const secondTransfer = new TokenTransfer({ token: secondNft, amount: 1n }); + + const transaction = transferFactory.createTransactionForTokenTransfer({ + sender: alice, + receiver: bob, + nativeAmount: 1000000000000000000n, + tokenTransfers: [firstTransfer, secondTransfer], + }); + + assert.equal(transaction.sender, alice.toBech32()); + assert.equal(transaction.receiver, alice.toBech32()); + assert.equal(transaction.value.valueOf(), 0n); + assert.equal(transaction.gasLimit.valueOf(), 1727500n); + assert.deepEqual( + transaction.data.toString(), + "MultiESDTNFTTransfer@8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8@03@4e46542d313233343536@0a@01@544553542d393837363534@01@01@45474c442d303030303030@@0de0b6b3a7640000", + ); + }); }); diff --git a/src/transactionsFactories/transferTransactionsFactory.ts b/src/transactionsFactories/transferTransactionsFactory.ts index 4a431bea..dcccdf2e 100644 --- a/src/transactionsFactories/transferTransactionsFactory.ts +++ b/src/transactionsFactories/transferTransactionsFactory.ts @@ -1,3 +1,4 @@ +import { EGLD_IDENTIFIER_FOR_MULTI_ESDTNFT_TRANSFER } from "../constants"; import { Err, ErrBadUsage } from "../errors"; import { IAddress, @@ -18,7 +19,7 @@ import { U16Value, U64Value, } from "../smartcontracts"; -import { TokenComputer, TokenTransfer } from "../tokens"; +import { Token, TokenComputer, TokenTransfer } from "../tokens"; import { Transaction } from "../transaction"; import { TransactionPayload } from "../transactionPayload"; import { TokenTransfersDataBuilder } from "./tokenTransfersDataBuilder"; @@ -53,7 +54,7 @@ export class TransferTransactionsFactory { private readonly gasEstimator?: IGasEstimator; /** - * Should be instantiated using `Config` and `TokenComputer`. + * Should be instantiated using `Config`. * Instantiating this class using GasEstimator represents the legacy version of this class. * The legacy version contains methods like `createEGLDTransfer`, `createESDTTransfer`, `createESDTNFTTransfer` and `createMultiESDTNFTTransfer`. * This was done in order to minimize breaking changes in client code. @@ -82,18 +83,10 @@ export class TransferTransactionsFactory { return this.gasEstimator !== undefined; } - private ensureMembersAreDefined() { + private ensureConfigIsDefined() { if (this.config === undefined) { throw new Err("'config' is not defined"); } - - if (this.tokenTransfersDataBuilder === undefined) { - throw new Err("`dataArgsBuilder is not defined`"); - } - - if (this.tokenComputer === undefined) { - throw new Err("`tokenComputer is not defined`"); - } } createTransactionForNativeTokenTransfer(options: { @@ -102,7 +95,7 @@ export class TransferTransactionsFactory { nativeAmount: bigint; data?: Uint8Array; }): Transaction { - this.ensureMembersAreDefined(); + this.ensureConfigIsDefined(); const data = options.data || new Uint8Array(); @@ -121,7 +114,7 @@ export class TransferTransactionsFactory { receiver: IAddress; tokenTransfers: TokenTransfer[]; }): Transaction { - this.ensureMembersAreDefined(); + this.ensureConfigIsDefined(); const numberOfTransfers = options.tokenTransfers.length; @@ -152,6 +145,53 @@ export class TransferTransactionsFactory { }).build(); } + createTransactionForTokenTransfer(options: { + sender: IAddress; + receiver: IAddress; + nativeAmount?: bigint; + tokenTransfers?: TokenTransfer[]; + data?: Uint8Array; + }): Transaction { + if (!options.nativeAmount && !options.tokenTransfers?.length) { + throw new ErrBadUsage("No native token amount or token transfers provided"); + } + + if (options.tokenTransfers?.length && options.data?.length) { + throw new ErrBadUsage("Can't set data field when sending esdt tokens"); + } + + if (options.nativeAmount && !options.tokenTransfers) { + return this.createTransactionForNativeTokenTransfer({ + sender: options.sender, + receiver: options.receiver, + nativeAmount: options.nativeAmount, + data: options.data, + }); + } + + if (options.tokenTransfers?.length && !options.nativeAmount) { + return this.createTransactionForESDTTokenTransfer({ + sender: options.sender, + receiver: options.receiver, + tokenTransfers: options.tokenTransfers, + }); + } + + // if the method does not return until here it means both nativeAmount and tokenTransfers have been provided + const nativeAmount = options.nativeAmount || 0n; + let tokenTransfers = options.tokenTransfers ? [...options.tokenTransfers] : []; + + const nativeToken = new Token({ identifier: EGLD_IDENTIFIER_FOR_MULTI_ESDTNFT_TRANSFER }); + const nativeTransfer = new TokenTransfer({ token: nativeToken, amount: nativeAmount }); + + tokenTransfers.push(nativeTransfer); + return this.createTransactionForESDTTokenTransfer({ + sender: options.sender, + receiver: options.receiver, + tokenTransfers: tokenTransfers, + }); + } + /** * This is a legacy method. Can only be used if the class was instantiated using `GasEstimator`. * Use {@link createTransactionForNativeTokenTransfer} instead. @@ -336,7 +376,7 @@ export class TransferTransactionsFactory { receiver: IAddress; tokenTransfers: TokenTransfer[]; }): Transaction { - this.ensureMembersAreDefined(); + this.ensureConfigIsDefined(); let dataParts: string[] = []; const transfer = options.tokenTransfers[0];