diff --git a/package-lock.json b/package-lock.json index ef17c5ef3..2a8292979 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@multiversx/sdk-core", - "version": "13.3.0", + "version": "13.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@multiversx/sdk-core", - "version": "13.3.0", + "version": "13.4.0", "license": "MIT", "dependencies": { "@multiversx/sdk-transaction-decoder": "1.0.2", diff --git a/package.json b/package.json index 44a8feb4a..fd87ca9e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@multiversx/sdk-core", - "version": "13.3.0", + "version": "13.4.0", "description": "MultiversX SDK for JavaScript and TypeScript", "main": "out/index.js", "types": "out/index.d.js", diff --git a/src/constants.ts b/src/constants.ts index e19c22ef1..9300d9780 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/converters/transactionsConverter.ts b/src/converters/transactionsConverter.ts index 2f0df2813..c6001c5d6 100644 --- a/src/converters/transactionsConverter.ts +++ b/src/converters/transactionsConverter.ts @@ -28,6 +28,10 @@ export class TransactionsConverter { guardian: transaction.guardian ? transaction.guardian : undefined, signature: this.toHexOrUndefined(transaction.signature), guardianSignature: this.toHexOrUndefined(transaction.guardianSignature), + relayer: transaction.relayer ? transaction.relayer : undefined, + innerTransactions: transaction.innerTransactions.length + ? transaction.innerTransactions.map((tx) => this.transactionToPlainObject(tx)) + : undefined, }; return plainObject; @@ -58,6 +62,10 @@ export class TransactionsConverter { options: Number(object.options), signature: this.bufferFromHex(object.signature), guardianSignature: this.bufferFromHex(object.guardianSignature), + relayer: object.relayer, + innerTransactions: object.innerTransactions + ? object.innerTransactions.map((tx) => this.plainObjectToTransaction(tx)) + : undefined, }); return transaction; diff --git a/src/converters/transactionsConverters.spec.ts b/src/converters/transactionsConverters.spec.ts index dd44b173f..a61dd3bc8 100644 --- a/src/converters/transactionsConverters.spec.ts +++ b/src/converters/transactionsConverters.spec.ts @@ -59,6 +59,85 @@ describe("test transactions converter", async () => { guardian: undefined, signature: undefined, guardianSignature: undefined, + relayer: undefined, + innerTransactions: undefined, + }); + }); + + it("converts relayedV3 transaction to plain object and back", () => { + const converter = new TransactionsConverter(); + + const innerTx = new Transaction({ + nonce: 90, + value: BigInt("123456789000000000000000000000"), + sender: "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + receiver: "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx", + senderUsername: "alice", + receiverUsername: "bob", + gasPrice: 1000000000, + gasLimit: 80000, + data: Buffer.from("hello"), + chainID: "localnet", + version: 2, + relayer: "erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8", + }); + + const relayedTx = new Transaction({ + nonce: 77, + value: BigInt("0"), + sender: "erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8", + receiver: "erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8", + gasPrice: 1000000000, + gasLimit: 50000, + chainID: "localnet", + version: 2, + innerTransactions: [innerTx], + }); + + const plainObject = converter.transactionToPlainObject(relayedTx); + const restoredTransaction = converter.plainObjectToTransaction(plainObject); + + assert.deepEqual(plainObject, relayedTx.toPlainObject()); + assert.deepEqual(restoredTransaction, Transaction.fromPlainObject(plainObject)); + assert.deepEqual(restoredTransaction, relayedTx); + assert.deepEqual(plainObject, { + nonce: 77, + value: "0", + sender: "erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8", + receiver: "erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8", + senderUsername: undefined, + receiverUsername: undefined, + gasPrice: 1000000000, + gasLimit: 50000, + data: undefined, + chainID: "localnet", + version: 2, + options: undefined, + guardian: undefined, + signature: undefined, + guardianSignature: undefined, + relayer: undefined, + innerTransactions: [ + { + nonce: 90, + value: "123456789000000000000000000000", + sender: "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + receiver: "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx", + senderUsername: "YWxpY2U=", + receiverUsername: "Ym9i", + gasPrice: 1000000000, + gasLimit: 80000, + data: "aGVsbG8=", + chainID: "localnet", + version: 2, + options: undefined, + guardian: undefined, + signature: undefined, + guardianSignature: undefined, + relayer: "erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8", + innerTransactions: undefined, + }, + ], }); }); diff --git a/src/interface.ts b/src/interface.ts index 97c86a54f..1a1bc6324 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -24,6 +24,8 @@ export interface IPlainTransactionObject { options?: number; signature?: string; guardianSignature?: string; + relayer?: string; + innerTransactions?: IPlainTransactionObject[]; } export interface ISignature { @@ -104,4 +106,6 @@ export interface ITransaction { guardian: string; signature: Uint8Array; guardianSignature: Uint8Array; + relayer: string; + innerTransactions: ITransaction[]; } diff --git a/src/proto/compiled.js b/src/proto/compiled.js index feed796f9..2d02a2512 100644 --- a/src/proto/compiled.js +++ b/src/proto/compiled.js @@ -46,6 +46,8 @@ * @property {number|null} [Options] Transaction Options * @property {Uint8Array|null} [GuardianAddr] Transaction GuardianAddr * @property {Uint8Array|null} [GuardianSignature] Transaction GuardianSignature + * @property {Uint8Array|null} [Relayer] Transaction Relayer + * @property {Array.|null} [InnerTransactions] Transaction InnerTransactions */ /** @@ -57,6 +59,7 @@ * @param {proto.ITransaction=} [properties] Properties to set */ function Transaction(properties) { + this.InnerTransactions = []; if (properties) for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) if (properties[keys[i]] != null) @@ -183,6 +186,22 @@ */ Transaction.prototype.GuardianSignature = $util.newBuffer([]); + /** + * Transaction Relayer. + * @member {Uint8Array} Relayer + * @memberof proto.Transaction + * @instance + */ + Transaction.prototype.Relayer = $util.newBuffer([]); + + /** + * Transaction InnerTransactions. + * @member {Array.} InnerTransactions + * @memberof proto.Transaction + * @instance + */ + Transaction.prototype.InnerTransactions = $util.emptyArray; + /** * Creates a new Transaction instance using the specified properties. * @function create @@ -237,6 +256,11 @@ writer.uint32(/* id 14, wireType 2 =*/114).bytes(message.GuardianAddr); if (message.GuardianSignature != null && Object.hasOwnProperty.call(message, "GuardianSignature")) writer.uint32(/* id 15, wireType 2 =*/122).bytes(message.GuardianSignature); + if (message.Relayer != null && Object.hasOwnProperty.call(message, "Relayer")) + writer.uint32(/* id 16, wireType 2 =*/130).bytes(message.Relayer); + if (message.InnerTransactions != null && message.InnerTransactions.length) + for (var i = 0; i < message.InnerTransactions.length; ++i) + $root.proto.Transaction.encode(message.InnerTransactions[i], writer.uint32(/* id 17, wireType 2 =*/138).fork()).ldelim(); return writer; }; @@ -331,6 +355,16 @@ message.GuardianSignature = reader.bytes(); break; } + case 16: { + message.Relayer = reader.bytes(); + break; + } + case 17: { + if (!(message.InnerTransactions && message.InnerTransactions.length)) + message.InnerTransactions = []; + message.InnerTransactions.push($root.proto.Transaction.decode(reader, reader.uint32())); + break; + } default: reader.skipType(tag & 7); break; @@ -411,6 +445,18 @@ if (message.GuardianSignature != null && message.hasOwnProperty("GuardianSignature")) if (!(message.GuardianSignature && typeof message.GuardianSignature.length === "number" || $util.isString(message.GuardianSignature))) return "GuardianSignature: buffer expected"; + if (message.Relayer != null && message.hasOwnProperty("Relayer")) + if (!(message.Relayer && typeof message.Relayer.length === "number" || $util.isString(message.Relayer))) + return "Relayer: buffer expected"; + if (message.InnerTransactions != null && message.hasOwnProperty("InnerTransactions")) { + if (!Array.isArray(message.InnerTransactions)) + return "InnerTransactions: array expected"; + for (var i = 0; i < message.InnerTransactions.length; ++i) { + var error = $root.proto.Transaction.verify(message.InnerTransactions[i]); + if (error) + return "InnerTransactions." + error; + } + } return null; }; @@ -507,6 +553,21 @@ $util.base64.decode(object.GuardianSignature, message.GuardianSignature = $util.newBuffer($util.base64.length(object.GuardianSignature)), 0); else if (object.GuardianSignature.length >= 0) message.GuardianSignature = object.GuardianSignature; + if (object.Relayer != null) + if (typeof object.Relayer === "string") + $util.base64.decode(object.Relayer, message.Relayer = $util.newBuffer($util.base64.length(object.Relayer)), 0); + else if (object.Relayer.length >= 0) + message.Relayer = object.Relayer; + if (object.InnerTransactions) { + if (!Array.isArray(object.InnerTransactions)) + throw TypeError(".proto.Transaction.InnerTransactions: array expected"); + message.InnerTransactions = []; + for (var i = 0; i < object.InnerTransactions.length; ++i) { + if (typeof object.InnerTransactions[i] !== "object") + throw TypeError(".proto.Transaction.InnerTransactions: object expected"); + message.InnerTransactions[i] = $root.proto.Transaction.fromObject(object.InnerTransactions[i]); + } + } return message; }; @@ -523,6 +584,8 @@ if (!options) options = {}; var object = {}; + if (options.arrays || options.defaults) + object.InnerTransactions = []; if (options.defaults) { if ($util.Long) { var long = new $util.Long(0, 0, true); @@ -611,6 +674,13 @@ if (options.bytes !== Array) object.GuardianSignature = $util.newBuffer(object.GuardianSignature); } + if (options.bytes === String) + object.Relayer = ""; + else { + object.Relayer = []; + if (options.bytes !== Array) + object.Relayer = $util.newBuffer(object.Relayer); + } } if (message.Nonce != null && message.hasOwnProperty("Nonce")) if (typeof message.Nonce === "number") @@ -651,6 +721,13 @@ object.GuardianAddr = options.bytes === String ? $util.base64.encode(message.GuardianAddr, 0, message.GuardianAddr.length) : options.bytes === Array ? Array.prototype.slice.call(message.GuardianAddr) : message.GuardianAddr; if (message.GuardianSignature != null && message.hasOwnProperty("GuardianSignature")) object.GuardianSignature = options.bytes === String ? $util.base64.encode(message.GuardianSignature, 0, message.GuardianSignature.length) : options.bytes === Array ? Array.prototype.slice.call(message.GuardianSignature) : message.GuardianSignature; + if (message.Relayer != null && message.hasOwnProperty("Relayer")) + object.Relayer = options.bytes === String ? $util.base64.encode(message.Relayer, 0, message.Relayer.length) : options.bytes === Array ? Array.prototype.slice.call(message.Relayer) : message.Relayer; + if (message.InnerTransactions && message.InnerTransactions.length) { + object.InnerTransactions = []; + for (var j = 0; j < message.InnerTransactions.length; ++j) + object.InnerTransactions[j] = $root.proto.Transaction.toObject(message.InnerTransactions[j], options); + } return object; }; diff --git a/src/proto/serializer.spec.ts b/src/proto/serializer.spec.ts index 97a88b5ca..53ecfbb68 100644 --- a/src/proto/serializer.spec.ts +++ b/src/proto/serializer.spec.ts @@ -7,6 +7,7 @@ import { TokenTransfer } from "../tokens"; import { Transaction } from "../transaction"; import { TransactionPayload } from "../transactionPayload"; import { ProtoSerializer } from "./serializer"; +import { TransactionComputer } from "../transactionComputer"; describe("serialize transactions", () => { let wallets: Record; @@ -145,4 +146,42 @@ describe("serialize transactions", () => { "08cc011209000de0b6b3a76400001a200139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e12205616c6963652a20b2a11555ce521e4944e09ab17549d85b487dcd26c84b5017a39e31a3670889ba32056361726f6c388094ebdc0340d086035201545802624051e6cd78fb3ab4b53ff7ad6864df27cb4a56d70603332869d47a5cf6ea977c30e696103e41e8dddf2582996ad335229fdf4acb726564dbc1a0bc9e705b511f06", ); }); + + it("serialize with inner transactions", async () => { + const innerTransaction = new Transaction({ + nonce: 204, + value: "1000000000000000000", + sender: Address.fromBech32("erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8"), + receiver: Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + senderUsername: "carol", + receiverUsername: "alice", + gasLimit: 50000, + chainID: "T", + }); + + const signer = wallets.carol.signer; + const txComputer = new TransactionComputer(); + innerTransaction.signature = await signer.sign(txComputer.computeBytesForSigning(innerTransaction)); + + const relayedTransaction = new Transaction({ + nonce: 204, + value: "1000000000000000000", + sender: Address.fromBech32("erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8"), + receiver: Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + senderUsername: "carol", + receiverUsername: "alice", + gasLimit: 50000, + chainID: "T", + relayer: wallets["carol"].address.toBech32(), + innerTransactions: [innerTransaction], + }); + + relayedTransaction.signature = await signer.sign(txComputer.computeBytesForSigning(relayedTransaction)); + + const serializedTransaction = serializer.serializeTransaction(relayedTransaction); + assert.equal( + serializedTransaction.toString("hex"), + "08cc011209000de0b6b3a76400001a200139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e12205616c6963652a20b2a11555ce521e4944e09ab17549d85b487dcd26c84b5017a39e31a3670889ba32056361726f6c388094ebdc0340d0860352015458026240901a6a974d6ab36546e7881c6e0364ec4c61a891aa70e5eb60f818d6c92a39cfa0beac6fab73f503853cfe8fe6149b4be207ddb93788f8450d75a07fa8759d06820120b2a11555ce521e4944e09ab17549d85b487dcd26c84b5017a39e31a3670889ba8a01b10108cc011209000de0b6b3a76400001a200139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e12205616c6963652a20b2a11555ce521e4944e09ab17549d85b487dcd26c84b5017a39e31a3670889ba32056361726f6c388094ebdc0340d086035201545802624051e6cd78fb3ab4b53ff7ad6864df27cb4a56d70603332869d47a5cf6ea977c30e696103e41e8dddf2582996ad335229fdf4acb726564dbc1a0bc9e705b511f06", + ); + }); }); diff --git a/src/proto/serializer.ts b/src/proto/serializer.ts index 8852dcd7a..2fff28ff1 100644 --- a/src/proto/serializer.ts +++ b/src/proto/serializer.ts @@ -1,8 +1,8 @@ import BigNumber from "bignumber.js"; import { Address } from "../address"; -import { TRANSACTION_OPTIONS_DEFAULT } from "../constants"; +import { TRANSACTION_OPTIONS_DEFAULT, TRANSACTION_OPTIONS_TX_GUARDED } from "../constants"; import * as errors from "../errors"; -import { ITransactionValue } from "../interface"; +import { ITransaction, ITransactionValue } from "../interface"; import { bigIntToBuffer } from "../smartcontracts/codec/utils"; import { Transaction } from "../transaction"; @@ -17,43 +17,56 @@ export class ProtoSerializer { */ serializeTransaction(transaction: Transaction): Buffer { const proto = require("./compiled").proto; - const receiverPubkey = new Address(transaction.getReceiver().bech32()).pubkey(); - const senderPubkey = new Address(transaction.getSender().bech32()).pubkey(); + + const protoTransaction = this.convertToProtoMessage(transaction); + const encoded = proto.Transaction.encode(protoTransaction).finish(); + const buffer = Buffer.from(encoded); + + return buffer; + } + + private convertToProtoMessage(transaction: ITransaction) { + const proto = require("./compiled").proto; + + const receiverPubkey = new Address(transaction.receiver).getPublicKey(); + const senderPubkey = new Address(transaction.sender).getPublicKey(); let protoTransaction = new proto.Transaction({ // mx-chain-go's serializer handles nonce == 0 differently, thus we treat 0 as "undefined". - Nonce: transaction.getNonce().valueOf() ? transaction.getNonce().valueOf() : undefined, - Value: this.serializeTransactionValue(transaction.getValue()), + Nonce: Number(transaction.nonce) ? Number(transaction.nonce) : undefined, + Value: this.serializeTransactionValue(transaction.value), RcvAddr: receiverPubkey, - RcvUserName: transaction.getReceiverUsername() - ? Buffer.from(transaction.getReceiverUsername()).toString("base64") + RcvUserName: transaction.receiverUsername + ? Buffer.from(transaction.receiverUsername).toString("base64") : undefined, SndAddr: senderPubkey, - SndUserName: transaction.getSenderUsername() - ? Buffer.from(transaction.getSenderUsername()).toString("base64") + SndUserName: transaction.senderUsername + ? Buffer.from(transaction.senderUsername).toString("base64") : undefined, - GasPrice: transaction.getGasPrice().valueOf(), - GasLimit: transaction.getGasLimit().valueOf(), - Data: transaction.getData().length() == 0 ? null : transaction.getData().valueOf(), - ChainID: Buffer.from(transaction.getChainID().valueOf()), - Version: transaction.getVersion().valueOf(), - Signature: transaction.getSignature(), + GasPrice: Number(transaction.gasPrice), + GasLimit: Number(transaction.gasLimit), + Data: transaction.data.length == 0 ? null : transaction.data, + ChainID: Buffer.from(transaction.chainID), + Version: transaction.version, + Signature: transaction.signature, }); - if (transaction.getOptions().valueOf() !== TRANSACTION_OPTIONS_DEFAULT) { - protoTransaction.Options = transaction.getOptions().valueOf(); + if (transaction.options !== TRANSACTION_OPTIONS_DEFAULT) { + protoTransaction.Options = transaction.options; } - if (transaction.isGuardedTransaction()) { - const guardianAddress = transaction.getGuardian(); - protoTransaction.GuardianAddr = new Address(guardianAddress.bech32()).pubkey(); - protoTransaction.GuardianSignature = transaction.getGuardianSignature(); + if (this.isGuardedTransaction(transaction)) { + protoTransaction.GuardianAddr = new Address(transaction.guardian).getPublicKey(); + protoTransaction.GuardianSignature = transaction.guardianSignature; } - const encoded = proto.Transaction.encode(protoTransaction).finish(); - const buffer = Buffer.from(encoded); + if (transaction.relayer) { + protoTransaction.Relayer = new Address(transaction.relayer).getPublicKey(); + } - return buffer; + protoTransaction.InnerTransactions = transaction.innerTransactions.map((tx) => this.convertToProtoMessage(tx)); + + return protoTransaction; } /** @@ -72,6 +85,16 @@ export class ProtoSerializer { return buffer; } + private isGuardedTransaction(transaction: ITransaction): boolean { + const hasGuardian = transaction.guardian.length > 0; + const hasGuardianSignature = transaction.guardianSignature.length > 0; + return this.isWithGuardian(transaction) && hasGuardian && hasGuardianSignature; + } + + private isWithGuardian(transaction: ITransaction): boolean { + return (transaction.options & TRANSACTION_OPTIONS_TX_GUARDED) == TRANSACTION_OPTIONS_TX_GUARDED; + } + deserializeTransaction(_buffer: Buffer): Transaction { // Not needed (yet). throw new errors.ErrUnsupportedOperation("deserializeTransaction"); diff --git a/src/proto/transaction.proto b/src/proto/transaction.proto index c3e46e993..90f4feecd 100644 --- a/src/proto/transaction.proto +++ b/src/proto/transaction.proto @@ -10,19 +10,21 @@ package proto; // Transaction holds all the data needed for a value transfer or SC call message Transaction { - uint64 Nonce = 1; - bytes Value = 2; - bytes RcvAddr = 3; - bytes RcvUserName = 4; - bytes SndAddr = 5; - bytes SndUserName = 6; - uint64 GasPrice = 7; - uint64 GasLimit = 8; - bytes Data = 9; - bytes ChainID = 10; - uint32 Version = 11; - bytes Signature = 12; - uint32 Options = 13; - bytes GuardianAddr = 14; - bytes GuardianSignature = 15; + uint64 Nonce = 1; + bytes Value = 2; + bytes RcvAddr = 3; + bytes RcvUserName = 4; + bytes SndAddr = 5; + bytes SndUserName = 6; + uint64 GasPrice = 7; + uint64 GasLimit = 8; + bytes Data = 9; + bytes ChainID = 10; + uint32 Version = 11; + bytes Signature = 12; + uint32 Options = 13; + bytes GuardianAddr = 14; + bytes GuardianSignature = 15; + bytes Relayer = 16; + repeated Transaction InnerTransactions = 17; } diff --git a/src/tokens.spec.ts b/src/tokens.spec.ts index 0228a2f7c..9a415815b 100644 --- a/src/tokens.spec.ts +++ b/src/tokens.spec.ts @@ -95,4 +95,12 @@ describe("test token transfer (legacy)", () => { assert.equal(transfer.nonce, nonce); assert.equal(transfer.toPrettyString(), "1 TEST-38f249"); }); + + it("should create TokenTransfer from native token amount", () => { + const transfer = TokenTransfer.newFromEgldAmount(1000000000000000000n); + + assert.equal(transfer.token.identifier, "EGLD-000000"); + assert.equal(transfer.token.nonce, 0n); + assert.equal(transfer.amount, 1000000000000000000n); + }); }); diff --git a/src/tokens.ts b/src/tokens.ts index f6b29bc91..62b22a04e 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -1,5 +1,6 @@ import BigNumber from "bignumber.js"; import { ErrInvalidArgument, ErrInvalidTokenIdentifier } from "./errors"; +import { EGLD_IDENTIFIER_FOR_MULTI_ESDTNFT_TRANSFER } from "./constants"; // Legacy constants: const EGLDTokenIdentifier = "EGLD"; @@ -84,6 +85,11 @@ export class TokenTransfer { } } + static newFromEgldAmount(amount: bigint): TokenTransfer { + const token = new Token({ identifier: EGLD_IDENTIFIER_FOR_MULTI_ESDTNFT_TRANSFER }); + return new TokenTransfer({ token, amount }); + } + private isLegacyTokenTransferOptions(options: any): options is ILegacyTokenTransferOptions { return options.tokenIdentifier !== undefined; } diff --git a/src/transaction.ts b/src/transaction.ts index 9678fa25f..a602cca17 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -11,6 +11,7 @@ import { INonce, IPlainTransactionObject, ISignature, + ITransaction, ITransactionOptions, ITransactionPayload, ITransactionValue, @@ -101,6 +102,16 @@ export class Transaction { */ public guardianSignature: Uint8Array; + /** + * The relayer in case it is a relayedV3 Transaction. + */ + public relayer: string; + + /** + * The inner transactions in case it is a relayedV3 Transaction. + */ + public innerTransactions: ITransaction[]; + /** * Creates a new Transaction object. */ @@ -120,6 +131,8 @@ export class Transaction { guardian?: IAddress | string; signature?: Uint8Array; guardianSignature?: Uint8Array; + relayer?: string; + innerTransactions?: ITransaction[]; }) { this.nonce = BigInt(options.nonce?.valueOf() || 0n); // We still rely on "bigNumber" for value, because client code might be passing a BigNumber object as a legacy "ITransactionValue", @@ -139,6 +152,9 @@ export class Transaction { this.signature = options.signature || Buffer.from([]); this.guardianSignature = options.guardianSignature || Buffer.from([]); + + this.relayer = options.relayer || ""; + this.innerTransactions = options.innerTransactions || []; } private addressAsBech32(address: IAddress | string): string { diff --git a/src/transactionComputer.ts b/src/transactionComputer.ts index 8ff65ff40..1a835b04a 100644 --- a/src/transactionComputer.ts +++ b/src/transactionComputer.ts @@ -49,7 +49,7 @@ export class TransactionComputer { computeBytesForSigning(transaction: ITransaction): Uint8Array { this.ensureValidTransactionFields(transaction); - const plainTransaction = this.toPlainObjectForSigning(transaction); + const plainTransaction = this.toPlainObject(transaction); const serialized = JSON.stringify(plainTransaction); return new Uint8Array(Buffer.from(serialized)); } @@ -64,7 +64,7 @@ export class TransactionComputer { } computeHashForSigning(transaction: ITransaction): Uint8Array { - const plainTransaction = this.toPlainObjectForSigning(transaction); + const plainTransaction = this.toPlainObject(transaction); const signable = Buffer.from(JSON.stringify(plainTransaction)); return createKeccakHash("keccak256").update(signable).digest(); } @@ -101,8 +101,8 @@ export class TransactionComputer { transaction.options = transaction.options | TRANSACTION_OPTIONS_TX_HASH_SIGN; } - private toPlainObjectForSigning(transaction: ITransaction) { - return { + private toPlainObject(transaction: ITransaction, withSignature?: boolean) { + let obj: any = { nonce: Number(transaction.nonce), value: transaction.value.toString(), receiver: transaction.receiver, @@ -112,11 +112,26 @@ export class TransactionComputer { gasPrice: Number(transaction.gasPrice), gasLimit: Number(transaction.gasLimit), data: this.toBase64OrUndefined(transaction.data), - chainID: transaction.chainID, - version: transaction.version, - options: transaction.options ? transaction.options : undefined, - guardian: transaction.guardian ? transaction.guardian : undefined, }; + + if (withSignature) { + obj.signature = this.toHexOrUndefined(transaction.signature); + } + + obj.chainID = transaction.chainID; + obj.version = transaction.version; + obj.options = transaction.options ? transaction.options : undefined; + obj.guardian = transaction.guardian ? transaction.guardian : undefined; + obj.relayer = transaction.relayer ? transaction.relayer : undefined; + obj.innerTransactions = transaction.innerTransactions.length + ? transaction.innerTransactions.map((tx) => this.toPlainObject(tx, true)) + : undefined; + + return obj; + } + + private toHexOrUndefined(value?: Uint8Array) { + return value && value.length ? Buffer.from(value).toString("hex") : undefined; } private toBase64OrUndefined(value?: string | Uint8Array) { diff --git a/src/transactionsFactories/delegationTransactionsFactory.spec.ts b/src/transactionsFactories/delegationTransactionsFactory.spec.ts index c76d1917c..7d5591cc6 100644 --- a/src/transactionsFactories/delegationTransactionsFactory.spec.ts +++ b/src/transactionsFactories/delegationTransactionsFactory.spec.ts @@ -176,13 +176,14 @@ describe("test delegation transactions factory", function () { sender: sender, delegationContract: delegationContract, publicKeys: [publicKey], + amount: 25000000000000000000n, }); assert.equal(transaction.sender, "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); assert.equal(transaction.receiver, "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); assert.isDefined(transaction.data); assert.deepEqual(transaction.data, Buffer.from("unJailNodes@61626261")); - assert.equal(transaction.value, 0n); + assert.equal(transaction.value, 25000000000000000000n); }); it("should create 'Transaction' for changing service fee", async function () { diff --git a/src/transactionsFactories/delegationTransactionsFactory.ts b/src/transactionsFactories/delegationTransactionsFactory.ts index 040d9ac4f..0b8062ec2 100644 --- a/src/transactionsFactories/delegationTransactionsFactory.ts +++ b/src/transactionsFactories/delegationTransactionsFactory.ts @@ -205,6 +205,7 @@ export class DelegationTransactionsFactory { sender: IAddress; delegationContract: IAddress; publicKeys: IValidatorPublicKey[]; + amount: bigint; }): Transaction { const dataParts = ["unJailNodes"]; @@ -220,6 +221,7 @@ export class DelegationTransactionsFactory { dataParts: dataParts, gasLimit: this.computeExecutionGasLimitForNodesManagement(numNodes), addDataMovementGas: true, + amount: options.amount, }).build(); } diff --git a/src/transactionsFactories/relayedTransactionsFactory.spec.ts b/src/transactionsFactories/relayedTransactionsFactory.spec.ts index e48c995d5..f45fac443 100644 --- a/src/transactionsFactories/relayedTransactionsFactory.spec.ts +++ b/src/transactionsFactories/relayedTransactionsFactory.spec.ts @@ -5,7 +5,7 @@ import { TransactionComputer } from "../transactionComputer"; import { RelayedTransactionsFactory } from "./relayedTransactionsFactory"; import { TransactionsFactoryConfig } from "./transactionsFactoryConfig"; -describe("test relayed v1 transaction builder", function () { +describe("test relayed transactions factory", function () { const config = new TransactionsFactoryConfig({ chainID: "T" }); const factory = new RelayedTransactionsFactory({ config: config }); const transactionComputer = new TransactionComputer(); @@ -275,4 +275,69 @@ describe("test relayed v1 transaction builder", function () { "relayedTxV2@000000000000000000010000000000000000000000000000000000000002ffff@0f@676574436f6e7472616374436f6e666967@fc3ed87a51ee659f937c1a1ed11c1ae677e99629fae9cc289461f033e6514d1a8cfad1144ae9c1b70f28554d196bd6ba1604240c1c1dc19c959e96c1c3b62d0c", ); }); + + it("should create relayed v3 transaction", async function () { + let innerTransaction = new Transaction({ + sender: bob.address.toBech32(), + receiver: bob.address.toBech32(), + gasLimit: 50000n, + chainID: config.chainID, + nonce: 0n, + version: 2, + relayer: alice.address.toBech32(), + }); + + const serializedInnerTransaction = transactionComputer.computeBytesForSigning(innerTransaction); + innerTransaction.signature = await bob.signer.sign(serializedInnerTransaction); + + const relayedTransaction = factory.createRelayedV3Transaction({ + relayerAddress: alice.address, + innerTransactions: [innerTransaction], + }); + + const serializedRelayedTransaction = transactionComputer.computeBytesForSigning(relayedTransaction); + relayedTransaction.signature = await alice.signer.sign(serializedRelayedTransaction); + + assert.equal( + Buffer.from(relayedTransaction.signature).toString("hex"), + "88b9bce6fe62a641fca593f95c12ad09032a44b34c9e5cf16d070f0563b1695bf9d452a9df52bce3373fd5e10ed96c3d65cd189f5873e3a3184a89f4980c9e0c", + ); + assert.equal(relayedTransaction.gasLimit, 100000n); + }); + + it("should fail to create relayed v3 transaction", async function () { + assert.throws(() => { + factory.createRelayedV3Transaction({ + relayerAddress: alice.address, + innerTransactions: [], + }); + }, "No inner transctions provided"); + + const innerTransaction = new Transaction({ + sender: bob.address.toBech32(), + receiver: bob.address.toBech32(), + gasLimit: 50000n, + chainID: config.chainID, + nonce: 0n, + version: 2, + relayer: carol.address.toBech32(), + }); + + assert.throws(() => { + factory.createRelayedV3Transaction({ + relayerAddress: alice.address, + innerTransactions: [innerTransaction], + }); + }, "Inner transaction is not signed"); + + const serializedInnerTransaction = transactionComputer.computeBytesForSigning(innerTransaction); + innerTransaction.signature = await bob.signer.sign(serializedInnerTransaction); + + assert.throws(() => { + factory.createRelayedV3Transaction({ + relayerAddress: alice.address, + innerTransactions: [innerTransaction], + }); + }, "The inner transaction has an incorrect relayer address"); + }); }); diff --git a/src/transactionsFactories/relayedTransactionsFactory.ts b/src/transactionsFactories/relayedTransactionsFactory.ts index fdf12be61..7464fc259 100644 --- a/src/transactionsFactories/relayedTransactionsFactory.ts +++ b/src/transactionsFactories/relayedTransactionsFactory.ts @@ -84,11 +84,41 @@ export class RelayedTransactionsFactory { }); } + createRelayedV3Transaction(options: { relayerAddress: IAddress; innerTransactions: ITransaction[] }): Transaction { + if (!options.innerTransactions.length) { + throw new ErrInvalidInnerTransaction("No inner transctions provided"); + } + + let innerTransactionsGasLimit = 0n; + for (const innerTx of options.innerTransactions) { + if (!innerTx.signature.length) { + throw new ErrInvalidInnerTransaction("Inner transaction is not signed"); + } + + if (innerTx.relayer !== options.relayerAddress.bech32()) { + throw new ErrInvalidInnerTransaction("The inner transaction has an incorrect relayer address"); + } + + innerTransactionsGasLimit += innerTx.gasLimit; + } + + const moveBalanceGas = this.config.minGasLimit * BigInt(options.innerTransactions.length); + const gasLimit = moveBalanceGas + innerTransactionsGasLimit; + + return new Transaction({ + sender: options.relayerAddress.bech32(), + receiver: options.relayerAddress.bech32(), + chainID: this.config.chainID, + gasLimit: gasLimit, + innerTransactions: options.innerTransactions, + }); + } + private prepareInnerTransactionForRelayedV1(innerTransaction: ITransaction): string { const txObject = { nonce: innerTransaction.nonce, - sender: Address.fromBech32(innerTransaction.sender).pubkey().toString("base64"), - receiver: Address.fromBech32(innerTransaction.receiver).pubkey().toString("base64"), + sender: Address.newFromBech32(innerTransaction.sender).getPublicKey().toString("base64"), + receiver: Address.newFromBech32(innerTransaction.receiver).getPublicKey().toString("base64"), value: innerTransaction.value, gasPrice: innerTransaction.gasPrice, gasLimit: innerTransaction.gasLimit, @@ -98,7 +128,7 @@ export class RelayedTransactionsFactory { version: innerTransaction.version, options: innerTransaction.options.valueOf() == 0 ? undefined : innerTransaction.options, guardian: innerTransaction.guardian - ? Address.fromBech32(innerTransaction.guardian).pubkey().toString("base64") + ? Address.newFromBech32(innerTransaction.guardian).getPublicKey().toString("base64") : undefined, guardianSignature: innerTransaction.guardianSignature.length ? Buffer.from(innerTransaction.guardianSignature).toString("base64") diff --git a/src/transactionsFactories/smartContractTransactionsFactory.spec.ts b/src/transactionsFactories/smartContractTransactionsFactory.spec.ts index d108c103d..096c37ea7 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 c439139f5..6102ebac5 100644 --- a/src/transactionsFactories/smartContractTransactionsFactory.ts +++ b/src/transactionsFactories/smartContractTransactionsFactory.ts @@ -1,6 +1,6 @@ import { Address } from "../address"; import { CONTRACT_DEPLOY_ADDRESS_HEX, VM_TYPE_WASM_VM } from "../constants"; -import { Err, ErrBadUsage } from "../errors"; +import { Err } from "../errors"; import { IAddress } from "../interface"; import { Logger } from "../logger"; import { ArgSerializer, CodeMetadata, ContractFunction, EndpointDefinition } from "../smartcontracts"; @@ -91,19 +91,21 @@ 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 ? [...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)"); + tokenTransfers.push(TokenTransfer.newFromEgldAmount(nativeTransferAmount)); + 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 +114,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/tokenManagementTransactionIntentsFactory.spec.ts b/src/transactionsFactories/tokenManagementTransactionIntentsFactory.spec.ts index 6dee64169..9d7ada3e8 100644 --- a/src/transactionsFactories/tokenManagementTransactionIntentsFactory.spec.ts +++ b/src/transactionsFactories/tokenManagementTransactionIntentsFactory.spec.ts @@ -1,8 +1,9 @@ import { assert } from "chai"; -import { ESDT_CONTRACT_ADDRESS } from "../constants"; +import { ESDT_CONTRACT_ADDRESS_HEX } from "../constants"; import { loadTestWallets, TestWallet } from "../testutils"; import { TokenManagementTransactionsFactory } from "./tokenManagementTransactionsFactory"; import { TransactionsFactoryConfig } from "./transactionsFactoryConfig"; +import { Address } from "../address"; describe("test token management transactions factory", () => { let frank: TestWallet, grace: TestWallet; @@ -53,7 +54,7 @@ describe("test token management transactions factory", () => { ), ); assert.equal(transaction.sender, frank.address.toString()); - assert.equal(transaction.receiver, ESDT_CONTRACT_ADDRESS); + assert.equal(transaction.receiver, Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, config.addressHrp).toBech32()); assert.deepEqual(transaction.value, config.issueCost); }); @@ -78,7 +79,7 @@ describe("test token management transactions factory", () => { ), ); assert.equal(transaction.sender, frank.address.toString()); - assert.equal(transaction.receiver, ESDT_CONTRACT_ADDRESS); + assert.equal(transaction.receiver, Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, config.addressHrp).toBech32()); assert.deepEqual(transaction.value, config.issueCost); }); @@ -103,7 +104,7 @@ describe("test token management transactions factory", () => { ), ); assert.equal(transaction.sender, frank.address.toString()); - assert.equal(transaction.receiver, ESDT_CONTRACT_ADDRESS); + assert.equal(transaction.receiver, Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, config.addressHrp).toBech32()); assert.deepEqual(transaction.value, config.issueCost); }); @@ -129,7 +130,7 @@ describe("test token management transactions factory", () => { ), ); assert.equal(transaction.sender, frank.address.toString()); - assert.equal(transaction.receiver, ESDT_CONTRACT_ADDRESS); + assert.equal(transaction.receiver, Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, config.addressHrp).toBech32()); assert.deepEqual(transaction.value, config.issueCost); }); @@ -150,7 +151,7 @@ describe("test token management transactions factory", () => { ), ); assert.equal(transaction.sender, frank.address.toString()); - assert.equal(transaction.receiver, ESDT_CONTRACT_ADDRESS); + assert.equal(transaction.receiver, Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, config.addressHrp).toBech32()); assert.equal(transaction.value, 0n); }); @@ -171,7 +172,7 @@ describe("test token management transactions factory", () => { ), ); assert.equal(transaction.sender, frank.address.toString()); - assert.equal(transaction.receiver, ESDT_CONTRACT_ADDRESS); + assert.equal(transaction.receiver, Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, config.addressHrp).toBech32()); assert.equal(transaction.value, 0n); }); @@ -185,16 +186,18 @@ describe("test token management transactions factory", () => { addRoleNFTUpdateAttributes: true, addRoleNFTAddURI: true, addRoleESDTTransferRole: false, + addRoleESDTModifyCreator: true, + addRoleNFTRecreate: true, }); assert.deepEqual( transaction.data, Buffer.from( - "setSpecialRole@4652414e4b2d313163653365@1e8a8b6b49de5b7be10aaa158a5a6a4abb4b56cc08f524bb5e6cd5f211ad3e13@45534454526f6c654e4654437265617465@45534454526f6c654e465455706461746541747472696275746573@45534454526f6c654e4654416464555249", + "setSpecialRole@4652414e4b2d313163653365@1e8a8b6b49de5b7be10aaa158a5a6a4abb4b56cc08f524bb5e6cd5f211ad3e13@45534454526f6c654e4654437265617465@45534454526f6c654e465455706461746541747472696275746573@45534454526f6c654e4654416464555249@45534454526f6c654d6f6469667943726561746f72@45534454526f6c654e46545265637265617465", ), ); assert.equal(transaction.sender, frank.address.toString()); - assert.equal(transaction.receiver, ESDT_CONTRACT_ADDRESS); + assert.equal(transaction.receiver, Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, config.addressHrp).toBech32()); assert.equal(transaction.value, 0n); }); @@ -218,4 +221,158 @@ describe("test token management transactions factory", () => { assert.equal(transaction.receiver, grace.address.toString()); assert.equal(transaction.value, 0n); }); + + it("should create 'Transaction' for modifying royalties", () => { + const transaction = tokenManagementFactory.createTransactionForModifyingRoyalties({ + sender: grace.address, + tokenIdentifier: "TEST-123456", + tokenNonce: 1n, + newRoyalties: 1234n, + }); + + assert.deepEqual(transaction.data, Buffer.from("ESDTModifyRoyalties@544553542d313233343536@01@04d2")); + assert.equal(transaction.sender, grace.address.toString()); + assert.equal(transaction.receiver, grace.address.toString()); + assert.equal(transaction.value, 0n); + assert.equal(transaction.gasLimit, 60125000n); + }); + + it("should create 'Transaction' for setting new URIs", () => { + const transaction = tokenManagementFactory.createTransactionForSettingNewUris({ + sender: grace.address, + tokenIdentifier: "TEST-123456", + tokenNonce: 1n, + newUris: ["firstURI", "secondURI"], + }); + + assert.deepEqual( + transaction.data, + Buffer.from("ESDTSetNewURIs@544553542d313233343536@01@6669727374555249@7365636f6e64555249"), + ); + assert.equal(transaction.sender, grace.address.toString()); + assert.equal(transaction.receiver, grace.address.toString()); + assert.equal(transaction.value, 0n); + assert.equal(transaction.gasLimit, 60164000n); + }); + + it("should create 'Transaction' for modifying creator", () => { + const transaction = tokenManagementFactory.createTransactionForModifyingCreator({ + sender: grace.address, + tokenIdentifier: "TEST-123456", + tokenNonce: 1n, + }); + + assert.deepEqual(transaction.data, Buffer.from("ESDTModifyCreator@544553542d313233343536@01")); + assert.equal(transaction.sender, grace.address.toString()); + assert.equal(transaction.receiver, grace.address.toString()); + assert.equal(transaction.value, 0n); + assert.equal(transaction.gasLimit, 60114500n); + }); + + it("should create 'Transaction' for updating metadata", () => { + const transaction = tokenManagementFactory.createTransactionForUpdatingMetadata({ + sender: grace.address, + tokenIdentifier: "TEST-123456", + tokenNonce: 1n, + newTokenName: "Test", + newRoyalties: 1234n, + newHash: "abba", + newAttributes: Buffer.from("test"), + newUris: ["firstURI", "secondURI"], + }); + + assert.deepEqual( + transaction.data, + Buffer.from( + "ESDTMetaDataUpdate@544553542d313233343536@01@54657374@04d2@61626261@74657374@6669727374555249@7365636f6e64555249", + ), + ); + assert.equal(transaction.sender, grace.address.toString()); + assert.equal(transaction.receiver, grace.address.toString()); + assert.equal(transaction.value, 0n); + assert.equal(transaction.gasLimit, 60218000n); + }); + + it("should create 'Transaction' for recreating metadata", () => { + const transaction = tokenManagementFactory.createTransactionForMetadataRecreate({ + sender: grace.address, + tokenIdentifier: "TEST-123456", + tokenNonce: 1n, + newTokenName: "Test", + newRoyalties: 1234n, + newHash: "abba", + newAttributes: Buffer.from("test"), + newUris: ["firstURI", "secondURI"], + }); + + assert.deepEqual( + transaction.data, + Buffer.from( + "ESDTMetaDataRecreate@544553542d313233343536@01@54657374@04d2@61626261@74657374@6669727374555249@7365636f6e64555249", + ), + ); + assert.equal(transaction.sender, grace.address.toString()); + assert.equal(transaction.receiver, grace.address.toString()); + assert.equal(transaction.value, 0n); + assert.equal(transaction.gasLimit, 60221000n); + }); + + it("should create 'Transaction' for changing to dynamic", () => { + const transaction = tokenManagementFactory.createTransactionForChangingTokenToDynamic({ + sender: grace.address, + tokenIdentifier: "TEST-123456", + }); + + assert.deepEqual(transaction.data, Buffer.from("changeToDynamic@544553542d313233343536")); + assert.equal(transaction.sender, grace.address.toString()); + assert.equal(transaction.receiver, Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, config.addressHrp).toBech32()); + assert.equal(transaction.value, 0n); + assert.equal(transaction.gasLimit, 60107000n); + }); + + it("should create 'Transaction' for updating token id", () => { + const transaction = tokenManagementFactory.createTransactionForUpdatingTokenId({ + sender: grace.address, + tokenIdentifier: "TEST-123456", + }); + + assert.deepEqual(transaction.data, Buffer.from("updateTokenID@544553542d313233343536")); + assert.equal(transaction.sender, grace.address.toString()); + assert.equal(transaction.receiver, Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, config.addressHrp).toBech32()); + assert.equal(transaction.value, 0n); + assert.equal(transaction.gasLimit, 60104000n); + }); + + it("should create 'Transaction' for registering dynamic", () => { + const transaction = tokenManagementFactory.createTransactionForRegisteringDynamicToken({ + sender: grace.address, + tokenName: "Test", + tokenTicker: "TEST-123456", + tokenType: "FNG", + }); + + assert.deepEqual(transaction.data, Buffer.from("registerDynamic@54657374@544553542d313233343536@464e47")); + assert.equal(transaction.sender, grace.address.toString()); + assert.equal(transaction.receiver, Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, config.addressHrp).toBech32()); + assert.equal(transaction.value, 0n); + assert.equal(transaction.gasLimit, 60131000n); + }); + + it("should create 'Transaction' for registering and setting all roles", () => { + const transaction = tokenManagementFactory.createTransactionForRegisteringDynamicAndSettingRoles({ + sender: grace.address, + tokenName: "Test", + tokenTicker: "TEST-123456", + tokenType: "FNG", + }); + + assert.deepEqual( + transaction.data, + Buffer.from("registerAndSetAllRolesDynamic@54657374@544553542d313233343536@464e47"), + ); + assert.equal(transaction.sender, grace.address.toString()); + assert.equal(transaction.receiver, Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, config.addressHrp).toBech32()); + assert.equal(transaction.value, 0n); + assert.equal(transaction.gasLimit, 60152000n); + }); }); diff --git a/src/transactionsFactories/tokenManagementTransactionsFactory.ts b/src/transactionsFactories/tokenManagementTransactionsFactory.ts index 18c61f1e2..eef02df80 100644 --- a/src/transactionsFactories/tokenManagementTransactionsFactory.ts +++ b/src/transactionsFactories/tokenManagementTransactionsFactory.ts @@ -1,5 +1,6 @@ import { Address } from "../address"; import { ESDT_CONTRACT_ADDRESS_HEX } from "../constants"; +import { ErrBadUsage } from "../errors"; import { IAddress } from "../interface"; import { Logger } from "../logger"; import { AddressValue, ArgSerializer, BigUIntValue, BytesValue, StringValue } from "../smartcontracts"; @@ -24,10 +25,18 @@ interface IConfig { gasLimitEsdtNftAddQuantity: bigint; gasLimitEsdtNftBurn: bigint; gasLimitStorePerByte: bigint; + gasLimitEsdtModifyRoyalties: bigint; + gasLimitEsdtModifyCreator: bigint; + gasLimitEsdtMetadataUpdate: bigint; + gasLimitSetNewUris: bigint; + gasLimitNftMetadataRecreate: bigint; + gasLimitNftChangeToDynamic: bigint; + gasLimitUpdateTokenId: bigint; + gasLimitRegisterDynamic: bigint; issueCost: bigint; } -type RegisterAndSetAllRolesTokenType = "NFT" | "SFT" | "META" | "FNG"; +type TokenType = "NFT" | "SFT" | "META" | "FNG"; /** * Use this class to create token management transactions like issuing ESDTs, creating NFTs, setting roles, etc. @@ -238,7 +247,7 @@ export class TokenManagementTransactionsFactory { sender: IAddress; tokenName: string; tokenTicker: string; - tokenType: RegisterAndSetAllRolesTokenType; + tokenType: TokenType; numDecimals: bigint; }): Transaction { this.notifyAboutUnsettingBurnRoleGlobally(); @@ -330,6 +339,7 @@ export class TokenManagementTransactionsFactory { addRoleNFTBurn: boolean; addRoleNFTAddQuantity: boolean; addRoleESDTTransferRole: boolean; + addRoleESDTModifyCreator?: boolean; }): Transaction { const args = [new StringValue(options.tokenIdentifier), new AddressValue(options.user)]; @@ -337,6 +347,7 @@ export class TokenManagementTransactionsFactory { options.addRoleNFTBurn ? args.push(new StringValue("ESDTRoleNFTBurn")) : 0; options.addRoleNFTAddQuantity ? args.push(new StringValue("ESDTRoleNFTAddQuantity")) : 0; options.addRoleESDTTransferRole ? args.push(new StringValue("ESDTTransferRole")) : 0; + options.addRoleESDTModifyCreator ? args.push(new StringValue("ESDTRoleModifyCreator")) : 0; const dataParts = ["setSpecialRole", ...this.argSerializer.valuesToStrings(args)]; @@ -371,6 +382,10 @@ export class TokenManagementTransactionsFactory { addRoleNFTUpdateAttributes: boolean; addRoleNFTAddURI: boolean; addRoleESDTTransferRole: boolean; + addRoleESDTModifyCreator?: boolean; + addRoleNFTRecreate?: boolean; + addRoleESDTSetNewURI?: boolean; + addRoleESDTModifyRoyalties?: boolean; }): Transaction { const args = [new StringValue(options.tokenIdentifier), new AddressValue(options.user)]; @@ -379,6 +394,10 @@ export class TokenManagementTransactionsFactory { options.addRoleNFTUpdateAttributes ? args.push(new StringValue("ESDTRoleNFTUpdateAttributes")) : 0; options.addRoleNFTAddURI ? args.push(new StringValue("ESDTRoleNFTAddURI")) : 0; options.addRoleESDTTransferRole ? args.push(new StringValue("ESDTTransferRole")) : 0; + options.addRoleESDTModifyCreator ? args.push(new StringValue("ESDTRoleModifyCreator")) : 0; + options.addRoleNFTRecreate ? args.push(new StringValue("ESDTRoleNFTRecreate")) : 0; + options.addRoleESDTSetNewURI ? args.push(new StringValue("ESDTRoleSetNewURI")) : 0; + options.addRoleESDTModifyRoyalties ? args.push(new StringValue("ESDTRoleModifyRoyalties")) : 0; const dataParts = ["setSpecialRole", ...this.argSerializer.valuesToStrings(args)]; @@ -640,6 +659,231 @@ export class TokenManagementTransactionsFactory { }).build(); } + createTransactionForModifyingRoyalties(options: { + sender: IAddress; + tokenIdentifier: string; + tokenNonce: bigint; + newRoyalties: bigint; + }): Transaction { + const dataParts = [ + "ESDTModifyRoyalties", + ...this.argSerializer.valuesToStrings([ + new StringValue(options.tokenIdentifier), + new BigUIntValue(options.tokenNonce), + new BigUIntValue(options.newRoyalties), + ]), + ]; + + return new TransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: options.sender, + dataParts: dataParts, + gasLimit: this.config.gasLimitEsdtModifyRoyalties, + addDataMovementGas: true, + }).build(); + } + + createTransactionForSettingNewUris(options: { + sender: IAddress; + tokenIdentifier: string; + tokenNonce: bigint; + newUris: string[]; + }): Transaction { + if (!options.newUris.length) { + throw new ErrBadUsage("No URIs provided"); + } + + const dataParts = [ + "ESDTSetNewURIs", + ...this.argSerializer.valuesToStrings([ + new StringValue(options.tokenIdentifier), + new BigUIntValue(options.tokenNonce), + ...options.newUris.map((uri) => new StringValue(uri)), + ]), + ]; + + return new TransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: options.sender, + dataParts: dataParts, + gasLimit: this.config.gasLimitSetNewUris, + addDataMovementGas: true, + }).build(); + } + + createTransactionForModifyingCreator(options: { + sender: IAddress; + tokenIdentifier: string; + tokenNonce: bigint; + }): Transaction { + const dataParts = [ + "ESDTModifyCreator", + ...this.argSerializer.valuesToStrings([ + new StringValue(options.tokenIdentifier), + new BigUIntValue(options.tokenNonce), + ]), + ]; + + return new TransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: options.sender, + dataParts: dataParts, + gasLimit: this.config.gasLimitEsdtModifyCreator, + addDataMovementGas: true, + }).build(); + } + + createTransactionForUpdatingMetadata(options: { + sender: IAddress; + tokenIdentifier: string; + tokenNonce: bigint; + newTokenName?: string; + newRoyalties?: bigint; + newHash?: string; + newAttributes?: Uint8Array; + newUris?: string[]; + }): Transaction { + const dataParts = [ + "ESDTMetaDataUpdate", + ...this.argSerializer.valuesToStrings([ + new StringValue(options.tokenIdentifier), + new BigUIntValue(options.tokenNonce), + ...(options.newTokenName ? [new StringValue(options.newTokenName)] : []), + ...(options.newRoyalties ? [new BigUIntValue(options.newRoyalties)] : []), + ...(options.newHash ? [new StringValue(options.newHash)] : []), + ...(options.newAttributes ? [new BytesValue(Buffer.from(options.newAttributes))] : []), + ...(options.newUris ? options.newUris.map((uri) => new StringValue(uri)) : []), + ]), + ]; + + return new TransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: options.sender, + dataParts: dataParts, + gasLimit: this.config.gasLimitEsdtMetadataUpdate, + addDataMovementGas: true, + }).build(); + } + + createTransactionForMetadataRecreate(options: { + sender: IAddress; + tokenIdentifier: string; + tokenNonce: bigint; + newTokenName: string; + newRoyalties: bigint; + newHash: string; + newAttributes: Uint8Array; + newUris: string[]; + }): Transaction { + const dataParts = [ + "ESDTMetaDataRecreate", + ...this.argSerializer.valuesToStrings([ + new StringValue(options.tokenIdentifier), + new BigUIntValue(options.tokenNonce), + ...(options.newTokenName ? [new StringValue(options.newTokenName)] : []), + ...(options.newRoyalties ? [new BigUIntValue(options.newRoyalties)] : []), + ...(options.newHash ? [new StringValue(options.newHash)] : []), + ...(options.newAttributes ? [new BytesValue(Buffer.from(options.newAttributes))] : []), + ...(options.newUris ? options.newUris.map((uri) => new StringValue(uri)) : []), + ]), + ]; + + return new TransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: options.sender, + dataParts: dataParts, + gasLimit: this.config.gasLimitNftMetadataRecreate, + addDataMovementGas: true, + }).build(); + } + + createTransactionForChangingTokenToDynamic(options: { sender: IAddress; tokenIdentifier: string }): Transaction { + const dataParts = [ + "changeToDynamic", + ...this.argSerializer.valuesToStrings([new StringValue(options.tokenIdentifier)]), + ]; + + return new TransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: this.esdtContractAddress, + dataParts: dataParts, + gasLimit: this.config.gasLimitNftChangeToDynamic, + addDataMovementGas: true, + }).build(); + } + + createTransactionForUpdatingTokenId(options: { sender: IAddress; tokenIdentifier: string }): Transaction { + const dataParts = [ + "updateTokenID", + ...this.argSerializer.valuesToStrings([new StringValue(options.tokenIdentifier)]), + ]; + + return new TransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: this.esdtContractAddress, + dataParts: dataParts, + gasLimit: this.config.gasLimitUpdateTokenId, + addDataMovementGas: true, + }).build(); + } + + createTransactionForRegisteringDynamicToken(options: { + sender: IAddress; + tokenName: string; + tokenTicker: string; + tokenType: TokenType; + }): Transaction { + const dataParts = [ + "registerDynamic", + ...this.argSerializer.valuesToStrings([ + new StringValue(options.tokenName), + new StringValue(options.tokenTicker), + new StringValue(options.tokenType), + ]), + ]; + + return new TransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: this.esdtContractAddress, + dataParts: dataParts, + gasLimit: this.config.gasLimitRegisterDynamic, + addDataMovementGas: true, + }).build(); + } + + createTransactionForRegisteringDynamicAndSettingRoles(options: { + sender: IAddress; + tokenName: string; + tokenTicker: string; + tokenType: TokenType; + }): Transaction { + const dataParts = [ + "registerAndSetAllRolesDynamic", + ...this.argSerializer.valuesToStrings([ + new StringValue(options.tokenName), + new StringValue(options.tokenTicker), + new StringValue(options.tokenType), + ]), + ]; + + return new TransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: this.esdtContractAddress, + dataParts: dataParts, + gasLimit: this.config.gasLimitRegisterDynamic, + addDataMovementGas: true, + }).build(); + } + private notifyAboutUnsettingBurnRoleGlobally() { Logger.info(` ========== diff --git a/src/transactionsFactories/transactionsFactoryConfig.ts b/src/transactionsFactories/transactionsFactoryConfig.ts index a05c187bf..c1d21df92 100644 --- a/src/transactionsFactories/transactionsFactoryConfig.ts +++ b/src/transactionsFactories/transactionsFactoryConfig.ts @@ -36,6 +36,14 @@ export class TransactionsFactoryConfig { gasLimitUnguardAccount: bigint; gasLimitClaimDeveloperRewards: bigint; gasLimitChangeOwnerAddress: bigint; + gasLimitEsdtModifyRoyalties: bigint; + gasLimitEsdtModifyCreator: bigint; + gasLimitEsdtMetadataUpdate: bigint; + gasLimitSetNewUris: bigint; + gasLimitNftMetadataRecreate: bigint; + gasLimitNftChangeToDynamic: bigint; + gasLimitUpdateTokenId: bigint; + gasLimitRegisterDynamic: bigint; constructor(options: { chainID: string }) { // General-purpose configuration @@ -59,6 +67,14 @@ export class TransactionsFactoryConfig { this.gasLimitEsdtNftBurn = 1000000n; this.gasLimitStorePerByte = 10000n; this.issueCost = 50000000000000000n; + this.gasLimitEsdtModifyRoyalties = 60000000n; + this.gasLimitEsdtModifyCreator = 60000000n; + this.gasLimitEsdtMetadataUpdate = 60000000n; + this.gasLimitSetNewUris = 60000000n; + this.gasLimitNftMetadataRecreate = 60000000n; + this.gasLimitNftChangeToDynamic = 60000000n; + this.gasLimitUpdateTokenId = 60000000n; + this.gasLimitRegisterDynamic = 60000000n; // Configuration for delegation operations this.gasLimitStake = 5000000n; diff --git a/src/transactionsFactories/transferTransactionsFactory.spec.ts b/src/transactionsFactories/transferTransactionsFactory.spec.ts index 7251b9c42..1296be14f 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,13 +109,100 @@ 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( transaction.data.toString(), "MultiESDTNFTTransfer@8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8@02@4e46542d313233343536@0a@01@544553542d393837363534@01@01", ); + + const secondTransaction = transferFactory.createTransactionForTransfer({ + sender: alice, + receiver: bob, + tokenTransfers: [firstTransfer, secondTransfer], + }); + + assert.deepEqual(transaction, secondTransaction); + }); + + it("should fail to create transaction for token transfers", async () => { + assert.throws(() => { + const nft = new Token({ identifier: "NFT-123456", nonce: 10n }); + const transfer = new TokenTransfer({ token: nft, amount: 1n }); + + transferFactory.createTransactionForTransfer({ + sender: alice, + receiver: bob, + tokenTransfers: [transfer], + data: Buffer.from("test"), + }); + }, "Can't set data field when sending esdt tokens"); + }); + + it("should create transaction for native transfers", async () => { + const transaction = transferFactory.createTransactionForTransfer({ + sender: alice, + receiver: bob, + nativeAmount: 1000000000000000000n, + }); + + assert.equal(transaction.sender, alice.toBech32()); + assert.equal(transaction.receiver, bob.toBech32()); + assert.equal(transaction.value.valueOf(), 1000000000000000000n); + assert.equal(transaction.gasLimit.valueOf(), 50000n); + }); + + it("should create transaction for native transfers and set data field", async () => { + const transaction = transferFactory.createTransactionForTransfer({ + sender: alice, + receiver: bob, + nativeAmount: 1000000000000000000n, + data: Buffer.from("hello"), + }); + + assert.equal(transaction.sender, alice.toBech32()); + assert.equal(transaction.receiver, bob.toBech32()); + assert.equal(transaction.value.valueOf(), 1000000000000000000n); + assert.equal(transaction.gasLimit.valueOf(), 57500n); + assert.deepEqual(transaction.data, Buffer.from("hello")); + }); + + it("should create transaction for notarizing", async () => { + const transaction = transferFactory.createTransactionForTransfer({ + sender: alice, + receiver: bob, + data: Buffer.from("hello"), + }); + + assert.equal(transaction.sender, alice.toBech32()); + assert.equal(transaction.receiver, bob.toBech32()); + assert.equal(transaction.gasLimit.valueOf(), 57500n); + assert.deepEqual(transaction.data, Buffer.from("hello")); + }); + + 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.createTransactionForTransfer({ + 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 4a431bea7..834fa6dd8 100644 --- a/src/transactionsFactories/transferTransactionsFactory.ts +++ b/src/transactionsFactories/transferTransactionsFactory.ts @@ -53,7 +53,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 +82,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 +94,7 @@ export class TransferTransactionsFactory { nativeAmount: bigint; data?: Uint8Array; }): Transaction { - this.ensureMembersAreDefined(); + this.ensureConfigIsDefined(); const data = options.data || new Uint8Array(); @@ -121,7 +113,7 @@ export class TransferTransactionsFactory { receiver: IAddress; tokenTransfers: TokenTransfer[]; }): Transaction { - this.ensureMembersAreDefined(); + this.ensureConfigIsDefined(); const numberOfTransfers = options.tokenTransfers.length; @@ -152,6 +144,42 @@ export class TransferTransactionsFactory { }).build(); } + createTransactionForTransfer(options: { + sender: IAddress; + receiver: IAddress; + nativeAmount?: bigint; + tokenTransfers?: TokenTransfer[]; + data?: Uint8Array; + }): Transaction { + const nativeAmount = options.nativeAmount ?? 0n; + let tokenTransfers = options.tokenTransfers ? [...options.tokenTransfers] : []; + const numberOfTokens = tokenTransfers.length; + + if (numberOfTokens && options.data?.length) { + throw new ErrBadUsage("Can't set data field when sending esdt tokens"); + } + + if ((nativeAmount && numberOfTokens === 0) || options.data) { + return this.createTransactionForNativeTokenTransfer({ + sender: options.sender, + receiver: options.receiver, + nativeAmount: nativeAmount, + data: options.data, + }); + } + + const nativeTransfer = nativeAmount ? TokenTransfer.newFromEgldAmount(nativeAmount) : undefined; + if (nativeTransfer) { + 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 +364,7 @@ export class TransferTransactionsFactory { receiver: IAddress; tokenTransfers: TokenTransfer[]; }): Transaction { - this.ensureMembersAreDefined(); + this.ensureConfigIsDefined(); let dataParts: string[] = []; const transfer = options.tokenTransfers[0];