diff --git a/src/constants.ts b/src/constants.ts index 66d72198..f9350f03 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -10,3 +10,8 @@ export const ESDTNFT_TRANSFER_FUNCTION_NAME = "ESDTNFTTransfer"; export const MULTI_ESDTNFT_TRANSFER_FUNCTION_NAME = "MultiESDTNFTTransfer"; export const ESDT_TRANSFER_VALUE = "0"; export const ARGUMENTS_SEPARATOR = "@"; +export const VM_TYPE_WASM_VM = new Uint8Array([0x05, 0x00]); +export const CONTRACT_DEPLOY_ADDRESS = "erd1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6gq4hu"; +export const DELEGATION_MANAGER_SC_ADDRESS = "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6"; +export const DEFAULT_HRP = "erd"; +export const ESDT_CONTRACT_ADDRESS = "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u"; diff --git a/src/draftTransaction.ts b/src/draftTransaction.ts new file mode 100644 index 00000000..25dcfc0d --- /dev/null +++ b/src/draftTransaction.ts @@ -0,0 +1,23 @@ +import { BigNumber } from "bignumber.js"; + +export class DraftTransaction { + public sender: string; + public receiver: string; + public gasLimit: BigNumber.Value; + public value: BigNumber.Value; + public data: Uint8Array; + + public constructor(options: { + sender: string, + receiver: string, + gasLimit: BigNumber.Value, + value?: BigNumber.Value, + data?: Uint8Array + }) { + this.sender = options.sender; + this.receiver = options.receiver; + this.gasLimit = options.gasLimit; + this.value = options.value ?? 0; + this.data = options.data ?? new Uint8Array(); + } +} diff --git a/src/errors.ts b/src/errors.ts index 80d052db..53d83bc6 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -326,8 +326,29 @@ export class ErrGasLimitShouldBe0ForInnerTransaction extends Err { } } +/** + * Signals that the `isCompleted` property is missing on the transaction obect and is needed for the Transaction Watcher + */ export class ErrIsCompletedFieldIsMissingOnTransaction extends Err { public constructor() { super("The transaction watcher requires the `isCompleted` property to be defined on the transaction object. Perhaps you've used the sdk-network-provider's `ProxyNetworkProvider.getTransaction()` and in that case you should also pass `withProcessStatus=true`.") } } + +/** + * Signals that the provided token identifier is not valid + */ +export class ErrInvalidTokenIdentifier extends Err { + public constructor(message: string) { + super(message); + } +} + +/** + * Signals a generic bad usage error + */ +export class ErrBadUsage extends Err { + public constructor(message: string) { + super(message); + } +} diff --git a/src/smartcontracts/codeMetadata.spec.ts b/src/smartcontracts/codeMetadata.spec.ts new file mode 100644 index 00000000..753be461 --- /dev/null +++ b/src/smartcontracts/codeMetadata.spec.ts @@ -0,0 +1,17 @@ +import { assert } from "chai"; +import { CodeMetadata } from "./codeMetadata"; + +describe("test code metadata", function () { + it("should test code metadata from bytes", () => { + const bytes = new Uint8Array([1, 0]); + const codeMetadata = CodeMetadata.fromBytes(bytes); + + assert.equal(codeMetadata.toString(), "0100"); + assert.deepEqual(codeMetadata.toJSON(), { + upgradeable: true, + readable: false, + payable: false, + payableBySc: false + }); + }); +}); diff --git a/src/smartcontracts/codeMetadata.ts b/src/smartcontracts/codeMetadata.ts index 46e74a13..6b493b5b 100644 --- a/src/smartcontracts/codeMetadata.ts +++ b/src/smartcontracts/codeMetadata.ts @@ -6,6 +6,7 @@ export class CodeMetadata { private readable: boolean; private payable: boolean; private payableBySc: boolean; + private static readonly codeMetadataLength = 2; /** * Creates a metadata object. By default, set the `upgradeable` attribute, and uset all others. @@ -22,6 +23,22 @@ export class CodeMetadata { this.payableBySc = payableBySc } + static fromBytes(bytes: Uint8Array): CodeMetadata { + if (bytes.length !== this.codeMetadataLength) { + return new CodeMetadata(); + } + + const byteZero = bytes[0]; + const byteOne = bytes[1]; + + const upgradeable = (byteZero & ByteZero.Upgradeable) !== 0; + const readable = (byteZero & ByteZero.Readable) !== 0; + const payable = (byteOne & ByteOne.Payable) !== 0; + const payableBySc = (byteOne & ByteOne.PayableBySc) !== 0; + + return new CodeMetadata(upgradeable, readable, payable, payableBySc); + } + /** * Adjust the metadata (the `upgradeable` attribute), when preparing the deployment transaction. */ @@ -49,7 +66,7 @@ export class CodeMetadata { togglePayableBySc(value: boolean) { this.payableBySc = value; } - + /** * Converts the metadata to the protocol-friendly representation. */ diff --git a/src/smartcontracts/interaction.spec.ts b/src/smartcontracts/interaction.spec.ts index 2fb119d1..9c43dae9 100644 --- a/src/smartcontracts/interaction.spec.ts +++ b/src/smartcontracts/interaction.spec.ts @@ -33,6 +33,7 @@ describe("test smart contract interactor", function () { let interaction = new Interaction(contract, dummyFunction, []); let transaction = interaction + .withSender(alice.address) .withNonce(7) .withValue(TokenTransfer.egldFromAmount(1)) .withGasLimit(20000000) @@ -63,6 +64,7 @@ describe("test smart contract interactor", function () { // ESDT, single let transaction = new Interaction(contract, dummyFunction, []) + .withSender(alice) .withSingleESDTTransfer(TokenFoo(10)) .buildTransaction(); @@ -180,7 +182,7 @@ describe("test smart contract interactor", function () { assert.isTrue(queryCode.equals(ReturnCode.Ok)); // Execute, do not wait for execution - let transaction = interaction.withNonce(0).buildTransaction(); + let transaction = interaction.withSender(alice.address).withNonce(0).buildTransaction(); transaction.setSender(alice.address); await alice.signer.sign(transaction); await provider.sendTransaction(transaction); @@ -235,7 +237,7 @@ describe("test smart contract interactor", function () { assert.deepEqual(counterValue!.valueOf(), new BigNumber(7)); - let incrementTransaction = incrementInteraction.withNonce(14).buildTransaction(); + let incrementTransaction = incrementInteraction.withSender(alice.address).withNonce(14).buildTransaction(); await alice.signer.sign(incrementTransaction); provider.mockGetTransactionWithAnyHashAsNotarizedWithOneResult("@6f6b@08"); let { bundle: { firstValue: valueAfterIncrement } } = await controller.execute(incrementInteraction, incrementTransaction); @@ -243,7 +245,7 @@ describe("test smart contract interactor", function () { // Decrement three times (simulate three parallel broadcasts). Wait for execution of the latter (third transaction). Return fake "5". // Decrement #1 - let decrementTransaction = decrementInteraction.withNonce(15).buildTransaction(); + let decrementTransaction = decrementInteraction.withSender(alice.address).withNonce(15).buildTransaction(); await alice.signer.sign(decrementTransaction); await provider.sendTransaction(decrementTransaction); // Decrement #2 @@ -292,7 +294,7 @@ describe("test smart contract interactor", function () { ); // start() - let startTransaction = startInteraction.withNonce(14).buildTransaction(); + let startTransaction = startInteraction.withSender(alice.address).withNonce(14).buildTransaction(); await alice.signer.sign(startTransaction); provider.mockGetTransactionWithAnyHashAsNotarizedWithOneResult("@6f6b"); let { bundle: { returnCode: startReturnCode, values: startReturnValues } } = await controller.execute(startInteraction, startTransaction); @@ -302,7 +304,7 @@ describe("test smart contract interactor", function () { assert.lengthOf(startReturnValues, 0); // status() (this is a view function, but for the sake of the test, we'll execute it) - let statusTransaction = statusInteraction.withNonce(15).buildTransaction(); + let statusTransaction = statusInteraction.withSender(alice.address).withNonce(15).buildTransaction(); await alice.signer.sign(statusTransaction); provider.mockGetTransactionWithAnyHashAsNotarizedWithOneResult("@6f6b@01"); let { bundle: { returnCode: statusReturnCode, values: statusReturnValues, firstValue: statusFirstValue } } = await controller.execute(statusInteraction, statusTransaction); @@ -313,7 +315,7 @@ describe("test smart contract interactor", function () { assert.deepEqual(statusFirstValue!.valueOf(), { name: "Running", fields: [] }); // lotteryInfo() (this is a view function, but for the sake of the test, we'll execute it) - let getLotteryInfoTransaction = getLotteryInfoInteraction.withNonce(15).buildTransaction(); + let getLotteryInfoTransaction = getLotteryInfoInteraction.withSender(alice.address).withNonce(15).buildTransaction(); await alice.signer.sign(getLotteryInfoTransaction); provider.mockGetTransactionWithAnyHashAsNotarizedWithOneResult("@6f6b@0000000b6c75636b792d746f6b656e000000010100000000000000005fc2b9dbffffffff00000001640000000a140ec80fa7ee88000000"); let { bundle: { returnCode: infoReturnCode, values: infoReturnValues, firstValue: infoFirstValue } } = await controller.execute(getLotteryInfoInteraction, getLotteryInfoTransaction); diff --git a/src/smartcontracts/interface.ts b/src/smartcontracts/interface.ts index 2aa1815a..6771d057 100644 --- a/src/smartcontracts/interface.ts +++ b/src/smartcontracts/interface.ts @@ -31,7 +31,7 @@ export interface ISmartContract { export interface DeployArguments { code: ICode; codeMetadata?: ICodeMetadata; - initArguments?: TypedValue[]; + initArguments?: any[]; value?: ITransactionValue; gasLimit: IGasLimit; gasPrice?: IGasPrice; @@ -42,7 +42,7 @@ export interface DeployArguments { export interface UpgradeArguments { code: ICode; codeMetadata?: ICodeMetadata; - initArguments?: TypedValue[]; + initArguments?: any[]; value?: ITransactionValue; gasLimit: IGasLimit; gasPrice?: IGasPrice; @@ -52,7 +52,7 @@ export interface UpgradeArguments { export interface CallArguments { func: IContractFunction; - args?: TypedValue[]; + args?: any[]; value?: ITransactionValue; gasLimit: IGasLimit; receiver?: IAddress; diff --git a/src/smartcontracts/smartContract.spec.ts b/src/smartcontracts/smartContract.spec.ts index 51233d05..337f7ac1 100644 --- a/src/smartcontracts/smartContract.spec.ts +++ b/src/smartcontracts/smartContract.spec.ts @@ -125,4 +125,43 @@ describe("test contract", () => { assert.isTrue((await provider.getTransactionStatus(hashOne)).isExecuted()); assert.isTrue((await provider.getTransactionStatus(hashTwo)).isExecuted()); }); + + it("should upgrade", async () => { + setupUnitTestWatcherTimeouts(); + let watcher = new TransactionWatcher(provider); + + let contract = new SmartContract(); + contract.setAddress(Address.fromBech32("erd1qqqqqqqqqqqqqpgq3ytm9m8dpeud35v3us20vsafp77smqghd8ss4jtm0q")) + + let deployTransaction = contract.upgrade({ + code: Code.fromBuffer(Buffer.from([1, 2, 3, 4])), + gasLimit: 1000000, + chainID: chainID, + caller: alice.address + }); + + provider.mockUpdateAccount(alice.address, account => { + account.nonce = 42; + }); + + await alice.sync(provider); + deployTransaction.setNonce(alice.account.nonce); + + assert.equal(deployTransaction.getData().valueOf().toString(), "upgradeContract@01020304@0100"); + assert.equal(deployTransaction.getGasLimit().valueOf(), 1000000); + assert.equal(deployTransaction.getNonce().valueOf(), 42); + + // Sign the transaction + alice.signer.sign(deployTransaction); + + // Now let's broadcast the deploy transaction, and wait for its execution. + let hash = await provider.sendTransaction(deployTransaction); + + await Promise.all([ + provider.mockTransactionTimeline(deployTransaction, [new Wait(40), new TransactionStatus("pending"), new Wait(40), new TransactionStatus("executed"), new MarkCompleted()]), + watcher.awaitCompleted(deployTransaction) + ]); + + assert.isTrue((await provider.getTransactionStatus(hash)).isExecuted()); + }); }); diff --git a/src/smartcontracts/smartContract.ts b/src/smartcontracts/smartContract.ts index 0ab5e8e0..09ba8858 100644 --- a/src/smartcontracts/smartContract.ts +++ b/src/smartcontracts/smartContract.ts @@ -9,14 +9,20 @@ import { bigIntToBuffer } from "./codec/utils"; import { CodeMetadata } from "./codeMetadata"; import { ContractFunction } from "./function"; import { Interaction } from "./interaction"; -import { CallArguments, DeployArguments, ISmartContract, QueryArguments, UpgradeArguments } from "./interface"; +import { CallArguments, DeployArguments, ICodeMetadata, ISmartContract, QueryArguments, UpgradeArguments } from "./interface"; import { NativeSerializer } from "./nativeSerializer"; import { Query } from "./query"; -import { ArwenVirtualMachine, ContractCallPayloadBuilder, ContractDeployPayloadBuilder, ContractUpgradePayloadBuilder } from "./transactionPayloadBuilders"; +import { WasmVirtualMachine } from "./transactionPayloadBuilders"; import { EndpointDefinition, TypedValue } from "./typesystem"; +import { SmartContractTransactionsFactory } from "../transactionsFactories/smartContractTransactionsFactory"; +import { TransactionsFactoryConfig } from "../transactionsFactories/transactionsFactoryConfig"; +import { TRANSACTION_MIN_GAS_PRICE } from "../constants"; +import { TokenComputer } from "../tokens"; const createKeccakHash = require("keccak"); interface IAbi { + constructorDefinition: EndpointDefinition; + getEndpoints(): EndpointDefinition[]; getEndpoint(name: string | ContractFunction): EndpointDefinition; } @@ -110,29 +116,58 @@ export class SmartContract implements ISmartContract { deploy({ deployer, code, codeMetadata, initArguments, value, gasLimit, gasPrice, chainID }: DeployArguments): Transaction { Compatibility.guardAddressIsSetAndNonZero(deployer, "'deployer' of SmartContract.deploy()", "pass the actual address to deploy()"); - codeMetadata = codeMetadata || new CodeMetadata(); - initArguments = initArguments || []; - value = value || 0; + const config = new TransactionsFactoryConfig(chainID.valueOf()); + const scDraftTransactionFactory = new SmartContractTransactionsFactory({ + config: config, + abi: this.abi, + tokenComputer: new TokenComputer() + }); - let payload = new ContractDeployPayloadBuilder() - .setCode(code) - .setCodeMetadata(codeMetadata) - .setInitArgs(initArguments) - .build(); + const bytecode = Buffer.from(code.toString(), 'hex'); + const metadataAsJson = this.getMetadataPropertiesAsObject(codeMetadata); - let transaction = new Transaction({ - receiver: Address.Zero(), + const draftTx = scDraftTransactionFactory.createTransactionForDeploy({ sender: deployer, - value: value, - gasLimit: gasLimit, - gasPrice: gasPrice, - data: payload, - chainID: chainID + bytecode: bytecode, + gasLimit: gasLimit.valueOf(), + args: initArguments, + isUpgradeable: metadataAsJson.upgradeable, + isReadable: metadataAsJson.readable, + isPayable: metadataAsJson.payable, + isPayableBySmartContract: metadataAsJson.payableBySc }); + const transaction = Transaction.fromDraft(draftTx); + transaction.setChainID(chainID); + transaction.setValue(value ?? 0); + transaction.setGasPrice(gasPrice ?? TRANSACTION_MIN_GAS_PRICE) + return transaction; } + private getMetadataPropertiesAsObject(codeMetadata?: ICodeMetadata): { + upgradeable: boolean, + readable: boolean, + payable: boolean, + payableBySc: boolean + } { + let metadata: CodeMetadata; + if (codeMetadata) { + metadata = CodeMetadata.fromBytes(Buffer.from(codeMetadata.toString(), "hex")); + } + else { + metadata = new CodeMetadata(); + } + const metadataAsJson = metadata.toJSON() as { + upgradeable: boolean, + readable: boolean, + payable: boolean, + payableBySc: boolean + }; + + return metadataAsJson; + } + /** * Creates a {@link Transaction} for upgrading the Smart Contract on the Network. */ @@ -141,25 +176,32 @@ export class SmartContract implements ISmartContract { this.ensureHasAddress(); - codeMetadata = codeMetadata || new CodeMetadata(); - initArguments = initArguments || []; - value = value || 0; + const config = new TransactionsFactoryConfig(chainID.valueOf()); + const scDraftTransactionFactory = new SmartContractTransactionsFactory({ + config: config, + abi: this.abi, + tokenComputer: new TokenComputer() + }); - let payload = new ContractUpgradePayloadBuilder() - .setCode(code) - .setCodeMetadata(codeMetadata) - .setInitArgs(initArguments) - .build(); + const bytecode = Uint8Array.from(Buffer.from(code.toString(), 'hex')); + const metadataAsJson = this.getMetadataPropertiesAsObject(codeMetadata); - let transaction = new Transaction({ + const draftTx = scDraftTransactionFactory.createTransactionForUpgrade({ sender: caller, - receiver: this.getAddress(), - value: value, - gasLimit: gasLimit, - gasPrice: gasPrice, - data: payload, - chainID: chainID - }); + contract: this.getAddress(), + bytecode: bytecode, + gasLimit: gasLimit.valueOf(), + args: initArguments, + isUpgradeable: metadataAsJson.upgradeable, + isReadable: metadataAsJson.readable, + isPayable: metadataAsJson.payable, + isPayableBySmartContract: metadataAsJson.payableBySc + }) + + const transaction = Transaction.fromDraft(draftTx); + transaction.setChainID(chainID); + transaction.setValue(value ?? 0); + transaction.setGasPrice(gasPrice ?? TRANSACTION_MIN_GAS_PRICE) return transaction; } @@ -172,23 +214,28 @@ export class SmartContract implements ISmartContract { this.ensureHasAddress(); + const config = new TransactionsFactoryConfig(chainID.valueOf()); + const scDraftTransactionFactory = new SmartContractTransactionsFactory({ + config: config, + abi: this.abi, + tokenComputer: new TokenComputer() + }); + args = args || []; value = value || 0; - let payload = new ContractCallPayloadBuilder() - .setFunction(func) - .setArgs(args) - .build(); - - let transaction = new Transaction({ + const draftTx = scDraftTransactionFactory.createTransactionForExecute({ sender: caller, - receiver: receiver ? receiver : this.getAddress(), - value: value, - gasLimit: gasLimit, - gasPrice: gasPrice, - data: payload, - chainID: chainID, - }); + contract: receiver ? receiver : this.getAddress(), + functionName: func.toString(), + gasLimit: gasLimit.valueOf(), + args: args + }) + + const transaction = Transaction.fromDraft(draftTx); + transaction.setChainID(chainID); + transaction.setValue(value); + transaction.setGasPrice(gasPrice ?? TRANSACTION_MIN_GAS_PRICE) return transaction; } @@ -230,7 +277,7 @@ export class SmartContract implements ISmartContract { let bytesToHash = Buffer.concat([ownerPubkey, ownerNonceBytes]); let hash = createKeccakHash("keccak256").update(bytesToHash).digest(); - let vmTypeBytes = Buffer.from(ArwenVirtualMachine, "hex"); + let vmTypeBytes = Buffer.from(WasmVirtualMachine, "hex"); let addressBytes = Buffer.concat([ initialPadding, vmTypeBytes, diff --git a/src/smartcontracts/transactionPayloadBuilders.ts b/src/smartcontracts/transactionPayloadBuilders.ts index f1bf9b37..38bd2b02 100644 --- a/src/smartcontracts/transactionPayloadBuilders.ts +++ b/src/smartcontracts/transactionPayloadBuilders.ts @@ -1,11 +1,14 @@ - import { TransactionPayload } from "../transactionPayload"; import { guardValueIsSet } from "../utils"; import { ArgSerializer } from "./argSerializer"; import { ICode, ICodeMetadata, IContractFunction } from "./interface"; import { TypedValue } from "./typesystem"; +/** + * @deprecated This constant should not be used. Use {@link WasmVirtualMachine} instead. + */ export const ArwenVirtualMachine = "0500"; +export const WasmVirtualMachine = "0500"; /** * A builder for {@link TransactionPayload} objects, to be used for Smart Contract deployment transactions. @@ -55,7 +58,7 @@ export class ContractDeployPayloadBuilder { let code = this.code!.toString(); let codeMetadata = this.codeMetadata.toString(); - let data = `${code}@${ArwenVirtualMachine}@${codeMetadata}`; + let data = `${code}@${WasmVirtualMachine}@${codeMetadata}`; data = appendArgumentsToString(data, this.arguments); return new TransactionPayload(data); diff --git a/src/testdata/adder.abi.json b/src/testdata/adder.abi.json new file mode 100644 index 00000000..88d5bf13 --- /dev/null +++ b/src/testdata/adder.abi.json @@ -0,0 +1,62 @@ +{ + "buildInfo": { + "rustc": { + "version": "1.71.0-nightly", + "commitHash": "7f94b314cead7059a71a265a8b64905ef2511796", + "commitDate": "2023-04-23", + "channel": "Nightly", + "short": "rustc 1.71.0-nightly (7f94b314c 2023-04-23)" + }, + "contractCrate": { + "name": "adder", + "version": "0.0.0" + }, + "framework": { + "name": "multiversx-sc", + "version": "0.41.3" + } + }, + "docs": [ + "One of the simplest smart contracts possible,", + "it holds a single variable in storage, which anyone can increment." + ], + "name": "Adder", + "constructor": { + "inputs": [ + { + "name": "initial_value", + "type": "BigUint" + } + ], + "outputs": [] + }, + "endpoints": [ + { + "name": "getSum", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "BigUint" + } + ] + }, + { + "docs": [ + "Add desired amount to the storage variable." + ], + "name": "add", + "mutability": "mutable", + "inputs": [ + { + "name": "value", + "type": "BigUint" + } + ], + "outputs": [] + } + ], + "events": [], + "hasCallback": false, + "types": {} +} diff --git a/src/testdata/adder.wasm b/src/testdata/adder.wasm new file mode 100755 index 00000000..77ce7e23 Binary files /dev/null and b/src/testdata/adder.wasm differ diff --git a/src/tokenOperations/codec.ts b/src/tokenOperations/codec.ts index 3b33abfd..64b4c484 100644 --- a/src/tokenOperations/codec.ts +++ b/src/tokenOperations/codec.ts @@ -1,6 +1,4 @@ import BigNumber from "bignumber.js"; -import { Address } from "../address"; -import { IAddress } from "../interface"; import * as contractsCodecUtils from "../smartcontracts/codec/utils"; import * as codecUtils from "../utils.codec"; @@ -24,25 +22,9 @@ export function bigIntToBuffer(value: BigNumber.Value): Buffer { return contractsCodecUtils.bigIntToBuffer(value); } -export function bigIntToHex(value: BigNumber.Value): string { - if (value == 0) { - return ""; - } - - return contractsCodecUtils.getHexMagnitudeOfBigInt(value); -} - -export function utf8ToHex(value: string) { - const hex = Buffer.from(value).toString("hex"); - return codecUtils.zeroPadStringIfOddLength(hex); -} +export { utf8ToHex, bigIntToHex, addressToHex } from "../utils.codec"; export function bufferToHex(value: Buffer) { const hex = value.toString("hex"); return codecUtils.zeroPadStringIfOddLength(hex); } - -export function addressToHex(address: IAddress): string { - const buffer = Address.fromBech32(address.toString()).pubkey(); - return buffer.toString("hex"); -} diff --git a/src/tokens.spec.ts b/src/tokens.spec.ts new file mode 100644 index 00000000..9fd54cd4 --- /dev/null +++ b/src/tokens.spec.ts @@ -0,0 +1,34 @@ +import { Token, TokenComputer } from "./tokens"; +import { assert } from "chai"; + +describe("test token computer", async () => { + const tokenComputer = new TokenComputer(); + + it("should test if token is fungible", async () => { + const fungibleToken = new Token("TEST-123456", 0); + const nonFungibleToken = new Token("NFT-987654", 7); + + assert.equal(tokenComputer.isFungible(fungibleToken), true); + assert.equal(tokenComputer.isFungible(nonFungibleToken), false); + }); + + it("should extract nonce from extended identifier", async () => { + const extendedIdentifier = "TEST-123456-0a"; + let nonce = tokenComputer.extractNonceFromExtendedIdentifier(extendedIdentifier); + assert.equal(nonce, 10); + + const fungibleTokenIdentifier = "FNG-123456"; + nonce = tokenComputer.extractNonceFromExtendedIdentifier(fungibleTokenIdentifier); + assert.equal(nonce, 0); + }); + + it("should extract identifier from extended identifier", async () => { + const extendedIdentifier = "TEST-123456-0a"; + let identifier = tokenComputer.extractIdentifierFromExtendedIdentifier(extendedIdentifier); + assert.equal(identifier, "TEST-123456"); + + const fungibleTokenIdentifier = "FNG-123456"; + identifier = tokenComputer.extractIdentifierFromExtendedIdentifier(fungibleTokenIdentifier); + assert.equal(identifier, "FNG-123456"); + }); +}); diff --git a/src/tokens.ts b/src/tokens.ts new file mode 100644 index 00000000..ddc0981a --- /dev/null +++ b/src/tokens.ts @@ -0,0 +1,102 @@ +import BigNumber from "bignumber.js"; +import { ErrInvalidTokenIdentifier } from "./errors"; + +export class Token { + identifier: string; + nonce: BigNumber.Value; + + constructor(identifier: string, nonce: BigNumber.Value) { + this.identifier = identifier; + this.nonce = nonce; + } +} + +export class NextTokenTransfer { + token: Token; + amount: BigNumber.Value; + + constructor(token: Token, amount: BigNumber.Value) { + this.token = token; + this.amount = amount; + } +} + +export class TokenComputer { + constructor() {} + + isFungible(token: Token): boolean { + return token.nonce === 0; + } + + extractNonceFromExtendedIdentifier(identifier: string): number { + const parts = identifier.split("-"); + + this.checkIfExtendedIdentifierWasProvided(parts); + this.checkLengthOfRandomSequence(parts[1]); + + // in case the identifier of a fungible token is provided + if (parts.length == 2) { + return 0; + } + + const hexNonce = Buffer.from(parts[2], "hex"); + return decodeUnsignedNumber(hexNonce); + } + + extractIdentifierFromExtendedIdentifier(identifier: string): string { + const parts = identifier.split("-"); + + this.checkIfExtendedIdentifierWasProvided(parts); + this.ensureTokenTickerValidity(parts[0]); + this.checkLengthOfRandomSequence(parts[1]); + + return parts[0] + "-" + parts[1]; + } + + private checkIfExtendedIdentifierWasProvided(tokenParts: string[]): void { + // this is for the identifiers of fungible tokens + const MIN_EXTENDED_IDENTIFIER_LENGTH_IF_SPLITTED = 2; + // this is for the identifiers of nft, sft and meta-esdt + const MAX_EXTENDED_IDENTIFIER_LENGTH_IF_SPLITTED = 3; + + if ( + tokenParts.length < MIN_EXTENDED_IDENTIFIER_LENGTH_IF_SPLITTED || + tokenParts.length > MAX_EXTENDED_IDENTIFIER_LENGTH_IF_SPLITTED + ) { + throw new ErrInvalidTokenIdentifier("Invalid extended token identifier provided"); + } + } + + private checkLengthOfRandomSequence(randomSequence: string): void { + const TOKEN_RANDOM_SEQUENCE_LENGTH = 6; + + if (randomSequence.length !== TOKEN_RANDOM_SEQUENCE_LENGTH) { + throw new ErrInvalidTokenIdentifier( + "The identifier is not valid. The random sequence does not have the right length" + ); + } + } + + private ensureTokenTickerValidity(ticker: string) { + const MIN_TICKER_LENGTH = 3; + const MAX_TICKER_LENGTH = 10; + + if (ticker.length < MIN_TICKER_LENGTH || ticker.length > MAX_TICKER_LENGTH) { + throw new ErrInvalidTokenIdentifier( + `The token ticker should be between ${MIN_TICKER_LENGTH} and ${MAX_TICKER_LENGTH} characters` + ); + } + + if (!ticker.match(/^[a-zA-Z0-9]+$/)) { + throw new ErrInvalidTokenIdentifier("The token ticker should only contain alphanumeric characters"); + } + + if (!(ticker == ticker.toUpperCase())) { + throw new ErrInvalidTokenIdentifier("The token ticker should be upper case"); + } + } +} + +function decodeUnsignedNumber(arg: Buffer): number { + return arg.readUIntBE(0, arg.length); +} diff --git a/src/transaction.spec.ts b/src/transaction.spec.ts index 52dd1f75..dbb8ac9e 100644 --- a/src/transaction.spec.ts +++ b/src/transaction.spec.ts @@ -6,6 +6,8 @@ import { TestWallet, loadTestWallets } from "./testutils"; import { TokenTransfer } from "./tokenTransfer"; import { Transaction } from "./transaction"; import { TransactionPayload } from "./transactionPayload"; +import { DraftTransaction } from "./draftTransaction"; +import { TRANSACTION_MIN_GAS_PRICE } from "./constants"; describe("test transaction construction", async () => { @@ -17,6 +19,27 @@ describe("test transaction construction", async () => { wallets = await loadTestWallets(); }); + it("create transaction from draft transaction", async () => { + const draftTransaction = new DraftTransaction({ + sender: "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + receiver: "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx", + gasLimit: 56000, + value: "1000000000000000000", + data: Buffer.from("test") + }); + + const transaction = Transaction.fromDraft(draftTransaction); + assert.deepEqual(transaction.getSender(), Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th")); + assert.deepEqual(transaction.getReceiver(), Address.fromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx")); + assert.equal(transaction.getGasLimit().valueOf(), 56000); + assert.equal(transaction.getValue().toString(), "1000000000000000000"); + assert.equal(transaction.getData().toString(), "test"); + assert.equal(transaction.getChainID().valueOf(), ""); + assert.equal(transaction.getNonce().valueOf(), 0); + assert.equal(transaction.getGasPrice().valueOf(), TRANSACTION_MIN_GAS_PRICE); + assert.deepEqual(transaction.getSignature(), Buffer.from([])); + }); + it("with no data, no value", async () => { let transaction = new Transaction({ nonce: 89, diff --git a/src/transaction.ts b/src/transaction.ts index 092aae7d..4f7b483f 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -11,6 +11,7 @@ import { ProtoSerializer } from "./proto"; import { Signature } from "./signature"; import { TransactionPayload } from "./transactionPayload"; import { guardNotEmpty } from "./utils"; +import { DraftTransaction } from "./draftTransaction"; const createTransactionHasher = require("blake2b"); const TRANSACTION_HASH_LENGTH = 32; @@ -152,6 +153,20 @@ export class Transaction { this.hash = TransactionHash.empty(); } + /** + * Creates a new Transaction object from a DraftTransaction. + */ + static fromDraft(draft: DraftTransaction): Transaction { + return new Transaction({ + sender: Address.fromBech32(draft.sender), + receiver: Address.fromBech32(draft.receiver), + gasLimit: new BigNumber(draft.gasLimit).toNumber(), + chainID: "", + value: draft.value, + data: new TransactionPayload(Buffer.from(draft.data)) + }) + } + getNonce(): INonce { return this.nonce; } diff --git a/src/transactionsFactories/delegationTransactionsFactory.spec.ts b/src/transactionsFactories/delegationTransactionsFactory.spec.ts new file mode 100644 index 00000000..1afc908a --- /dev/null +++ b/src/transactionsFactories/delegationTransactionsFactory.spec.ts @@ -0,0 +1,292 @@ +import BigNumber from "bignumber.js"; +import { Address } from "../address"; +import { DelegationTransactionsFactory } from "./delegationTransactionsFactory"; +import { assert } from "chai"; +import { DELEGATION_MANAGER_SC_ADDRESS } from "../constants"; +import { ValidatorPublicKey } from "@multiversx/sdk-wallet-next"; +import { TransactionsFactoryConfig } from "./transactionsFactoryConfig"; + +describe("test delegation transactions factory", function () { + const config = new TransactionsFactoryConfig("D"); + const delegationFactory = new DelegationTransactionsFactory(config); + + it("should create draft transaction for new delegation contract", async function () { + const sender = Address.fromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + const delagationCap = "5000000000000000000000"; + const serviceFee = 10; + const value = new BigNumber("1250000000000000000000"); + + const draft = delegationFactory.createTransactionForNewDelegationContract({ + sender: sender, + totalDelegationCap: delagationCap, + serviceFee: serviceFee, + amount: value + }); + + assert.equal(draft.sender, "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + assert.equal(draft.receiver, DELEGATION_MANAGER_SC_ADDRESS); + assert.isDefined(draft.data); + assert.deepEqual(draft.data, Buffer.from("createNewDelegationContract@010f0cf064dd59200000@0a")); + assert.equal(draft.gasLimit.valueOf(), 60126500); + assert.equal(draft.value, value); + }); + + it("should create draft transaction for adding nodes", async function () { + const sender = Address.fromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + const delegationContract = Address.fromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); + const publicKey = new ValidatorPublicKey(Buffer.from("e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208", "hex")); + + const mockMessage = { + getSignature: () => Buffer.from("81109fa1c8d3dc7b6c2d6e65206cc0bc1a83c9b2d1eb91a601d66ad32def430827d5eb52917bd2b0d04ce195738db216", "hex") + } + + const draft = delegationFactory.createTransactionForAddingNodes({ + sender: sender, + delegationContract: delegationContract, + publicKeys: [publicKey], + signedMessages: [mockMessage.getSignature()] + }); + + assert.equal(draft.sender, "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + assert.equal(draft.receiver, "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); + assert.isDefined(draft.data); + assert.deepEqual(draft.data, Buffer.from("addNodes@e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208@81109fa1c8d3dc7b6c2d6e65206cc0bc1a83c9b2d1eb91a601d66ad32def430827d5eb52917bd2b0d04ce195738db216")); + assert.equal(draft.value, 0); + }); + + it("should create draft transaction for removing nodes", async function () { + const sender = Address.fromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + const delegationContract = Address.fromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); + + const publicKey = { + hex(): string { + return Buffer.from("abba").toString("hex"); + } + }; + + const draft = delegationFactory.createTransactionForRemovingNodes({ + sender: sender, + delegationContract: delegationContract, + publicKeys: [publicKey] + }); + + assert.equal(draft.sender, "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + assert.equal(draft.receiver, "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); + assert.isDefined(draft.data); + assert.deepEqual(draft.data, Buffer.from("removeNodes@61626261")); + assert.equal(draft.value, 0); + }); + + it("should create draft transaction for staking nodes", async function () { + const sender = Address.fromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + const delegationContract = Address.fromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); + + const publicKey = { + hex(): string { + return Buffer.from("abba").toString("hex"); + } + }; + + const draft = delegationFactory.createTransactionForStakingNodes({ + sender: sender, + delegationContract: delegationContract, + publicKeys: [publicKey] + }); + + assert.equal(draft.sender, "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + assert.equal(draft.receiver, "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); + assert.isDefined(draft.data); + assert.deepEqual(draft.data, Buffer.from("stakeNodes@61626261")); + assert.equal(draft.value, 0); + }); + + it("should create draft transaction for unbonding nodes", async function () { + const sender = Address.fromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + const delegationContract = Address.fromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); + + const publicKey = { + hex(): string { + return Buffer.from("abba").toString("hex"); + } + }; + + const draft = delegationFactory.createTransactionForUnbondingNodes({ + sender: sender, + delegationContract: delegationContract, + publicKeys: [publicKey] + }); + + assert.equal(draft.sender, "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + assert.equal(draft.receiver, "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); + assert.isDefined(draft.data); + assert.deepEqual(draft.data, Buffer.from("unBondNodes@61626261")); + assert.equal(draft.value, 0); + assert.equal(draft.gasLimit.valueOf(), 12080000); + }); + + it("should create draft transaction for unstaking nodes", async function () { + const sender = Address.fromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + const delegationContract = Address.fromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); + + const publicKey = { + hex(): string { + return Buffer.from("abba").toString("hex"); + } + }; + + const draft = delegationFactory.createTransactionForUnstakingNodes({ + sender: sender, + delegationContract: delegationContract, + publicKeys: [publicKey] + }); + + assert.equal(draft.sender, "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + assert.equal(draft.receiver, "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); + assert.isDefined(draft.data); + assert.deepEqual(draft.data, Buffer.from("unStakeNodes@61626261")); + assert.equal(draft.value, 0); + assert.equal(draft.gasLimit.valueOf(), 12081500); + }); + + it("should create draft transaction for unjailing nodes", async function () { + const sender = Address.fromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + const delegationContract = Address.fromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); + + const publicKey = { + hex(): string { + return Buffer.from("abba").toString("hex"); + } + }; + + const draft = delegationFactory.createTransactionForUnjailingNodes({ + sender: sender, + delegationContract: delegationContract, + publicKeys: [publicKey] + }); + + assert.equal(draft.sender, "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + assert.equal(draft.receiver, "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); + assert.isDefined(draft.data); + assert.deepEqual(draft.data, Buffer.from("unJailNodes@61626261")); + assert.equal(draft.value, 0); + }); + + it("should create draft transaction for changing service fee", async function () { + const sender = Address.fromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + const delegationContract = Address.fromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); + const serviceFee = new BigNumber(10); + + const draft = delegationFactory.createTransactionForChangingServiceFee({ + sender: sender, + delegationContract: delegationContract, + serviceFee: serviceFee + }); + + assert.equal(draft.sender, "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + assert.equal(draft.receiver, "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); + assert.isDefined(draft.data); + assert.deepEqual(draft.data, Buffer.from("changeServiceFee@0a")); + assert.equal(draft.value, 0); + }); + + it("should create draft transaction for changing delegation cap", async function () { + const sender = Address.fromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + const delegationContract = Address.fromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); + const delegationCap = new BigNumber("5000000000000000000000"); + + const draft = delegationFactory.createTransactionForModifyingDelegationCap({ + sender: sender, + delegationContract: delegationContract, + delegationCap: delegationCap + }); + + assert.equal(draft.sender, "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + assert.equal(draft.receiver, "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); + assert.isDefined(draft.data); + assert.deepEqual(draft.data, Buffer.from("modifyTotalDelegationCap@010f0cf064dd59200000")); + assert.equal(draft.value, 0); + }); + + it("should create draft transaction for setting automatic activation", async function () { + const sender = Address.fromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + const delegationContract = Address.fromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); + + const draft = delegationFactory.createTransactionForSettingAutomaticActivation({ + sender: sender, + delegationContract: delegationContract + }); + + assert.equal(draft.sender, "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + assert.equal(draft.receiver, "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); + assert.isDefined(draft.data); + assert.deepEqual(draft.data, Buffer.from("setAutomaticActivation@74727565")); + assert.equal(draft.value, 0); + }); + + it("should create draft transaction for unsetting automatic activation", async function () { + const sender = Address.fromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + const delegationContract = Address.fromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); + + const draft = delegationFactory.createTransactionForUnsettingAutomaticActivation({ + sender: sender, + delegationContract: delegationContract + }); + + assert.equal(draft.sender, "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + assert.equal(draft.receiver, "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); + assert.isDefined(draft.data); + assert.deepEqual(draft.data, Buffer.from("setAutomaticActivation@66616c7365")); + assert.equal(draft.value, 0); + }); + + it("should create draft transaction for setting cap check on redelegate rewards", async function () { + const sender = Address.fromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + const delegationContract = Address.fromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); + + const draft = delegationFactory.createTransactionForSettingCapCheckOnRedelegateRewards({ + sender: sender, + delegationContract: delegationContract + }); + + assert.equal(draft.sender, "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + assert.equal(draft.receiver, "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); + assert.isDefined(draft.data); + assert.deepEqual(draft.data, Buffer.from("setCheckCapOnReDelegateRewards@74727565")); + assert.equal(draft.value, 0); + }); + + it("should create draft transaction for unsetting cap check on redelegate rewards", async function () { + const sender = Address.fromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + const delegationContract = Address.fromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); + + const draft = delegationFactory.createTransactionForUnsettingCapCheckOnRedelegateRewards({ + sender: sender, + delegationContract: delegationContract + }); + + assert.equal(draft.sender, "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + assert.equal(draft.receiver, "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); + assert.isDefined(draft.data); + assert.deepEqual(draft.data, Buffer.from("setCheckCapOnReDelegateRewards@66616c7365")); + assert.equal(draft.value, 0); + }); + + it("should create draft transaction for setting metadata", async function () { + const sender = Address.fromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + const delegationContract = Address.fromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); + + const draft = delegationFactory.createTransactionForSettingMetadata({ + sender: sender, + delegationContract: delegationContract, + name: "name", + website: "website", + identifier: "identifier" + }); + + assert.equal(draft.sender, "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + assert.equal(draft.receiver, "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); + assert.isDefined(draft.data); + assert.deepEqual(draft.data, Buffer.from("setMetaData@6e616d65@77656273697465@6964656e746966696572")); + assert.equal(draft.value, 0); + }); +}); diff --git a/src/transactionsFactories/delegationTransactionsFactory.ts b/src/transactionsFactories/delegationTransactionsFactory.ts new file mode 100644 index 00000000..fe7133a0 --- /dev/null +++ b/src/transactionsFactories/delegationTransactionsFactory.ts @@ -0,0 +1,357 @@ +import { IAddress } from "../interface"; +import { BigNumber } from "bignumber.js"; +import { numberToPaddedHex, byteArrayToHex, utf8ToHex } from "../utils.codec"; +import { DraftTransaction } from "../draftTransaction"; +import { DraftTransactionBuilder } from "./draftTransactionBuilder"; +import { Address } from "../address"; +import { DELEGATION_MANAGER_SC_ADDRESS } from "../constants"; +import { Err } from "../errors"; + +interface Config { + chainID: string; + minGasLimit: BigNumber.Value; + gasLimitPerByte: BigNumber.Value; + gasLimitStake: BigNumber.Value; + gasLimitUnstake: BigNumber.Value; + gasLimitUnbond: BigNumber.Value; + gasLimitCreateDelegationContract: BigNumber.Value; + gasLimitDelegationOperations: BigNumber.Value; + additionalGasLimitPerValidatorNode: BigNumber.Value; + additionalGasLimitForDelegationOperations: BigNumber.Value; +} + +interface IValidatorPublicKey { + hex(): string; +} + +export class DelegationTransactionsFactory { + private readonly config: Config; + + constructor(config: Config) { + this.config = config; + } + + createTransactionForNewDelegationContract(options: { + sender: IAddress, + totalDelegationCap: BigNumber.Value, + serviceFee: BigNumber.Value, + amount: BigNumber.Value + }): DraftTransaction { + const dataParts = [ + "createNewDelegationContract", + numberToPaddedHex(options.totalDelegationCap.toString()), + numberToPaddedHex(options.serviceFee.toString()) + ]; + + const executionGasLimit = new BigNumber(this.config.gasLimitCreateDelegationContract).plus(this.config.additionalGasLimitForDelegationOperations); + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: Address.fromBech32(DELEGATION_MANAGER_SC_ADDRESS), + dataParts: dataParts, + gasLimit: executionGasLimit, + addDataMovementGas: true, + amount: options.amount + }).build(); + } + + createTransactionForAddingNodes(options: { + sender: IAddress, + delegationContract: IAddress, + publicKeys: IValidatorPublicKey[], + signedMessages: Uint8Array[] + }): DraftTransaction { + if (options.publicKeys.length !== options.signedMessages.length) { + throw new Err("The number of public keys should match the number of signed messages"); + } + + const numNodes = options.publicKeys.length; + + const dataParts = ["addNodes"]; + for (let i = 0; i < numNodes; i++) { + dataParts.push(...[options.publicKeys[i].hex(), byteArrayToHex(options.signedMessages[i])]); + } + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: options.delegationContract, + dataParts: dataParts, + gasLimit: this.computeExecutionGasLimitForNodesManagement(numNodes), + addDataMovementGas: true + }).build(); + } + + createTransactionForRemovingNodes(options: { + sender: IAddress, + delegationContract: IAddress, + publicKeys: IValidatorPublicKey[] + }): DraftTransaction { + const dataParts = ["removeNodes"]; + + for (const key of options.publicKeys) { + dataParts.push(key.hex()); + } + + const numNodes = options.publicKeys.length; + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: options.delegationContract, + dataParts: dataParts, + gasLimit: this.computeExecutionGasLimitForNodesManagement(numNodes), + addDataMovementGas: true + }).build(); + } + + createTransactionForStakingNodes(options: { + sender: IAddress, + delegationContract: IAddress, + publicKeys: IValidatorPublicKey[] + }): DraftTransaction { + let dataParts = ["stakeNodes"]; + + for (const key of options.publicKeys) { + dataParts = dataParts.concat(key.hex()); + } + + const numNodes = options.publicKeys.length; + const additionalGasForAllNodes = new BigNumber(numNodes).multipliedBy(this.config.additionalGasLimitPerValidatorNode); + const executionGasLimit = additionalGasForAllNodes.plus(this.config.gasLimitStake).plus( + this.config.gasLimitDelegationOperations + ); + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: options.delegationContract, + dataParts: dataParts, + gasLimit: executionGasLimit, + addDataMovementGas: true + }).build(); + } + + createTransactionForUnbondingNodes(options: { + sender: IAddress, + delegationContract: IAddress, + publicKeys: IValidatorPublicKey[] + }): DraftTransaction { + let dataParts = ["unBondNodes"]; + + for (const key of options.publicKeys) { + dataParts = dataParts.concat(key.hex()); + } + + const numNodes = options.publicKeys.length; + const executionGasLimit = new BigNumber(numNodes).multipliedBy( + this.config.additionalGasLimitPerValidatorNode).plus( + this.config.gasLimitUnbond + ).plus(this.config.gasLimitDelegationOperations); + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: options.delegationContract, + dataParts: dataParts, + gasLimit: executionGasLimit, + addDataMovementGas: true + }).build(); + } + + createTransactionForUnstakingNodes(options: { + sender: IAddress, + delegationContract: IAddress, + publicKeys: IValidatorPublicKey[] + }): DraftTransaction { + let dataParts = ["unStakeNodes"]; + + for (const key of options.publicKeys) { + dataParts = dataParts.concat(key.hex()); + } + + const numNodes = options.publicKeys.length; + const executionGasLimit = new BigNumber(numNodes).multipliedBy( + this.config.additionalGasLimitPerValidatorNode).plus( + this.config.gasLimitUnstake + ).plus(this.config.gasLimitDelegationOperations); + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: options.delegationContract, + dataParts: dataParts, + gasLimit: executionGasLimit, + addDataMovementGas: true + }).build(); + } + + createTransactionForUnjailingNodes(options: { + sender: IAddress, + delegationContract: IAddress, + publicKeys: IValidatorPublicKey[] + }): DraftTransaction { + const dataParts = ["unJailNodes"]; + + for (const key of options.publicKeys) { + dataParts.push(key.hex()); + } + + const numNodes = options.publicKeys.length; + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: options.delegationContract, + dataParts: dataParts, + gasLimit: this.computeExecutionGasLimitForNodesManagement(numNodes), + addDataMovementGas: true + }).build(); + } + + createTransactionForChangingServiceFee(options: { + sender: IAddress, + delegationContract: IAddress, + serviceFee: BigNumber.Value + }): DraftTransaction { + const dataParts = [ + "changeServiceFee", + numberToPaddedHex(options.serviceFee) + ]; + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: options.delegationContract, + dataParts: dataParts, + gasLimit: new BigNumber(this.config.gasLimitDelegationOperations).plus(this.config.additionalGasLimitForDelegationOperations), + addDataMovementGas: true + }).build(); + } + + createTransactionForModifyingDelegationCap(options: { + sender: IAddress, + delegationContract: IAddress, + delegationCap: BigNumber.Value + }): DraftTransaction { + const dataParts = [ + "modifyTotalDelegationCap", + numberToPaddedHex(options.delegationCap) + ]; + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: options.delegationContract, + dataParts: dataParts, + gasLimit: new BigNumber(this.config.gasLimitDelegationOperations).plus(this.config.additionalGasLimitForDelegationOperations), + addDataMovementGas: true + }).build(); + } + + createTransactionForSettingAutomaticActivation(options: { + sender: IAddress, + delegationContract: IAddress + }): DraftTransaction { + const dataParts = [ + "setAutomaticActivation", + utf8ToHex("true") + ]; + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: options.delegationContract, + dataParts: dataParts, + gasLimit: new BigNumber(this.config.gasLimitDelegationOperations).plus(this.config.additionalGasLimitForDelegationOperations), + addDataMovementGas: true + }).build(); + } + + createTransactionForUnsettingAutomaticActivation(options: { + sender: IAddress, + delegationContract: IAddress + }): DraftTransaction { + const dataParts = [ + "setAutomaticActivation", + utf8ToHex("false") + ]; + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: options.delegationContract, + dataParts: dataParts, + gasLimit: new BigNumber(this.config.gasLimitDelegationOperations).plus(this.config.additionalGasLimitForDelegationOperations), + addDataMovementGas: true + }).build(); + } + + createTransactionForSettingCapCheckOnRedelegateRewards(options: { + sender: IAddress, + delegationContract: IAddress + }): DraftTransaction { + const dataParts = [ + "setCheckCapOnReDelegateRewards", + utf8ToHex("true") + ]; + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: options.delegationContract, + dataParts: dataParts, + gasLimit: new BigNumber(this.config.gasLimitDelegationOperations).plus(this.config.additionalGasLimitForDelegationOperations), + addDataMovementGas: true + }).build(); + } + + createTransactionForUnsettingCapCheckOnRedelegateRewards(options: { + sender: IAddress, + delegationContract: IAddress + }): DraftTransaction { + const dataParts = [ + "setCheckCapOnReDelegateRewards", + utf8ToHex("false") + ]; + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: options.delegationContract, + dataParts: dataParts, + gasLimit: new BigNumber(this.config.gasLimitDelegationOperations).plus(this.config.additionalGasLimitForDelegationOperations), + addDataMovementGas: true + }).build(); + } + + createTransactionForSettingMetadata(options: { + sender: IAddress, + delegationContract: IAddress, + name: string, + website: string, + identifier: string + }): DraftTransaction { + const dataParts = [ + "setMetaData", + utf8ToHex(options.name), + utf8ToHex(options.website), + utf8ToHex(options.identifier) + ]; + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: options.delegationContract, + dataParts: dataParts, + gasLimit: new BigNumber(this.config.gasLimitDelegationOperations).plus(this.config.additionalGasLimitForDelegationOperations), + addDataMovementGas: true + }).build(); + } + + private computeExecutionGasLimitForNodesManagement(numNodes: number): BigNumber.Value { + const additionalGasForAllNodes = new BigNumber(this.config.additionalGasLimitPerValidatorNode).multipliedBy(numNodes); + return new BigNumber(this.config.gasLimitDelegationOperations).plus(additionalGasForAllNodes); + } +} diff --git a/src/transactionsFactories/draftTransactionBuilder.ts b/src/transactionsFactories/draftTransactionBuilder.ts new file mode 100644 index 00000000..99d57c5a --- /dev/null +++ b/src/transactionsFactories/draftTransactionBuilder.ts @@ -0,0 +1,66 @@ +import { BigNumber } from "bignumber.js"; +import { IAddress, ITransactionPayload } from "../interface"; +import { ARGUMENTS_SEPARATOR } from "../constants"; +import { TransactionPayload } from "../transactionPayload"; +import { DraftTransaction } from "../draftTransaction"; + +interface Config { + minGasLimit: BigNumber.Value; + gasLimitPerByte: BigNumber.Value; +} + +export class DraftTransactionBuilder { + private config: Config; + private sender: IAddress; + private receiver: IAddress; + private dataParts: string[]; + private providedGasLimit: BigNumber; + private addDataMovementGas: boolean; + private amount?: BigNumber.Value; + + constructor(options: { + config: Config, + sender: IAddress, + receiver: IAddress, + dataParts: string[], + gasLimit: BigNumber.Value, + addDataMovementGas: boolean, + amount?: BigNumber.Value + }) { + this.config = options.config; + this.sender = options.sender; + this.receiver = options.receiver; + this.dataParts = options.dataParts; + this.providedGasLimit = new BigNumber(options.gasLimit); + this.addDataMovementGas = options.addDataMovementGas; + this.amount = options.amount; + } + + private computeGasLimit(payload: ITransactionPayload): BigNumber.Value { + if (!this.addDataMovementGas) { + return this.providedGasLimit; + } + + const dataMovementGas = new BigNumber(this.config.minGasLimit).plus(new BigNumber(this.config.gasLimitPerByte).multipliedBy(payload.length())); + const gasLimit = dataMovementGas.plus(this.providedGasLimit); + return gasLimit; + } + + private buildTransactionPayload(): TransactionPayload { + const data = this.dataParts.join(ARGUMENTS_SEPARATOR); + return new TransactionPayload(data); + } + + build(): DraftTransaction { + const data = this.buildTransactionPayload() + const gasLimit = this.computeGasLimit(data); + + return new DraftTransaction({ + sender: this.sender.bech32(), + receiver: this.receiver.bech32(), + gasLimit: gasLimit, + value: this.amount || 0, + data: data.valueOf() + }) + } +} diff --git a/src/transactionsFactories/smartContractTransactionsFactory.spec.ts b/src/transactionsFactories/smartContractTransactionsFactory.spec.ts new file mode 100644 index 00000000..67777156 --- /dev/null +++ b/src/transactionsFactories/smartContractTransactionsFactory.spec.ts @@ -0,0 +1,346 @@ +import { assert, expect } from "chai"; +import { SmartContractTransactionsFactory } from "./smartContractTransactionsFactory"; +import { Address } from "../address"; +import { Code } from "../smartcontracts/code"; +import { AbiRegistry } from "../smartcontracts/typesystem/abiRegistry"; +import { U32Value } from "../smartcontracts"; +import { CONTRACT_DEPLOY_ADDRESS } from "../constants"; +import { loadContractCode, loadAbiRegistry } from "../testutils/utils"; +import { Err } from "../errors"; +import { TransactionsFactoryConfig } from "./transactionsFactoryConfig"; +import BigNumber from "bignumber.js"; +import { Token, NextTokenTransfer, TokenComputer } from "../tokens"; + +describe("test smart contract transactions factory", function () { + const config = new TransactionsFactoryConfig("D"); + let factory: SmartContractTransactionsFactory; + let abiAwareFactory: SmartContractTransactionsFactory; + let adderByteCode: Code; + let abiRegistry: AbiRegistry; + + before(async function () { + factory = new SmartContractTransactionsFactory({ + config: config, + tokenComputer: new TokenComputer(), + }); + + adderByteCode = await loadContractCode("src/testdata/adder.wasm"); + abiRegistry = await loadAbiRegistry("src/testdata/adder.abi.json"); + + abiAwareFactory = new SmartContractTransactionsFactory({ + config: config, + abi: abiRegistry, + tokenComputer: new TokenComputer(), + }); + }); + + it("should throw error when args are not of type 'TypedValue'", async function () { + const sender = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + const gasLimit = 6000000; + const args = [0]; + + assert.throws( + () => + factory.createTransactionForDeploy({ + sender: sender, + bytecode: adderByteCode.valueOf(), + gasLimit: gasLimit, + args: args, + }), + Err, + "Can't convert args to TypedValues" + ); + }); + + it("should create draft transaction for deploy", async function () { + const sender = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + const gasLimit = 6000000; + const args = [new U32Value(0)]; + + const deployDraft = factory.createTransactionForDeploy({ + sender: sender, + bytecode: adderByteCode.valueOf(), + gasLimit: gasLimit, + args: args, + }); + const abiDeployDraft = abiAwareFactory.createTransactionForDeploy({ + sender: sender, + bytecode: adderByteCode.valueOf(), + gasLimit: gasLimit, + args: args, + }); + + assert.equal(deployDraft.sender, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + assert.equal(deployDraft.receiver, CONTRACT_DEPLOY_ADDRESS); + expect(deployDraft.data.length).to.be.greaterThan(0); + assert.equal(deployDraft.gasLimit.valueOf(), gasLimit); + assert.equal(deployDraft.value, 0); + + assert.deepEqual(deployDraft, abiDeployDraft); + }); + + it("should create draft transaction for execute without transfer", async function () { + const sender = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + const contract = Address.fromBech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); + const func = "add"; + const gasLimit = 6000000; + const args = [new U32Value(7)]; + + const executeDraft = factory.createTransactionForExecute({ + sender: sender, + contract: contract, + functionName: func, + gasLimit: gasLimit, + args: args, + }); + const abiExecuteDraft = abiAwareFactory.createTransactionForExecute({ + sender: sender, + contract: contract, + functionName: func, + gasLimit: gasLimit, + args: args, + }); + + assert.equal(executeDraft.sender, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + assert.equal(executeDraft.receiver, "erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); + assert.deepEqual(executeDraft.data, Buffer.from("add@07")); + assert.equal(executeDraft.gasLimit.valueOf(), gasLimit); + assert.equal(executeDraft.value, 0); + + assert.deepEqual(executeDraft, abiExecuteDraft); + }); + + it("should create draft transaction for execute and transfer native token", async function () { + const sender = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + const contract = Address.fromBech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); + const func = "add"; + const gasLimit = 6000000; + const egldAmount = new BigNumber("1000000000000000000"); + + const executeDraft = factory.createTransactionForExecute({ + sender: sender, + contract: contract, + functionName: func, + gasLimit: gasLimit, + args: [new U32Value(7)], + nativeTransferAmount: egldAmount, + }); + const abiExecuteDraft = abiAwareFactory.createTransactionForExecute({ + sender: sender, + contract: contract, + functionName: func, + gasLimit: gasLimit, + args: [7], + nativeTransferAmount: egldAmount, + }); + + assert.equal(executeDraft.sender, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + assert.equal(executeDraft.receiver, "erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); + assert.deepEqual(executeDraft.data, Buffer.from("add@07")); + assert.equal(executeDraft.gasLimit.valueOf(), gasLimit); + assert.equal(executeDraft.value.valueOf(), "1000000000000000000"); + + assert.deepEqual(executeDraft, abiExecuteDraft); + }); + + it("should create draft transaction for execute and transfer single esdt", async function () { + const sender = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + const contract = Address.fromBech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); + const func = "add"; + const gasLimit = 6000000; + const args = [new U32Value(7)]; + const token = new Token("FOO-6ce17b", 0); + const transfer = new NextTokenTransfer(token, 10); + + const executeDraft = factory.createTransactionForExecute({ + sender: sender, + contract: contract, + functionName: func, + gasLimit: gasLimit, + args: args, + tokenTransfers: [transfer], + }); + const abiExecuteDraft = abiAwareFactory.createTransactionForExecute({ + sender: sender, + contract: contract, + functionName: func, + gasLimit: gasLimit, + args: args, + tokenTransfers: [transfer], + }); + + assert.equal(executeDraft.sender, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + assert.equal(executeDraft.receiver, "erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); + assert.deepEqual(executeDraft.data, Buffer.from("ESDTTransfer@464f4f2d366365313762@0a@616464@07")); + assert.equal(executeDraft.gasLimit.valueOf(), gasLimit); + assert.equal(executeDraft.value.valueOf(), "0"); + + assert.deepEqual(executeDraft, abiExecuteDraft); + }); + + it("should create draft transaction for execute and transfer multiple esdts", async function () { + const sender = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + const contract = Address.fromBech32("erd1qqqqqqqqqqqqqpgqak8zt22wl2ph4tswtyc39namqx6ysa2sd8ss4xmlj3"); + const func = "add"; + const gasLimit = 6000000; + const args = [new U32Value(7)]; + + const fooToken = new Token("FOO-6ce17b", 0); + const fooTransfer = new NextTokenTransfer(fooToken, 10); + const barToken = new Token("BAR-5bc08f", 0); + const barTransfer = new NextTokenTransfer(barToken, 3140); + + const executeDraft = factory.createTransactionForExecute({ + sender: sender, + contract: contract, + functionName: func, + gasLimit: gasLimit, + args: args, + tokenTransfers: [fooTransfer, barTransfer], + }); + const abiExecuteDraft = abiAwareFactory.createTransactionForExecute({ + sender: sender, + contract: contract, + functionName: func, + gasLimit: gasLimit, + args: args, + tokenTransfers: [fooTransfer, barTransfer], + }); + + assert.equal(executeDraft.sender, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + assert.equal(executeDraft.receiver, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + + assert.deepEqual( + executeDraft.data, + Buffer.from( + "MultiESDTNFTTransfer@00000000000000000500ed8e25a94efa837aae0e593112cfbb01b448755069e1@02@464f4f2d366365313762@00@0a@4241522d356263303866@00@0c44@616464@07" + ) + ); + + assert.equal(executeDraft.gasLimit.valueOf(), gasLimit); + assert.equal(executeDraft.value.valueOf(), "0"); + + assert.deepEqual(executeDraft, abiExecuteDraft); + }); + + it("should create draft transaction for execute and transfer single nft", async function () { + const sender = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + const contract = Address.fromBech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); + const func = "add"; + const gasLimit = 6000000; + const args = [new U32Value(7)]; + + const token = new Token("NFT-123456", 1); + const transfer = new NextTokenTransfer(token, 1); + + const executeDraft = factory.createTransactionForExecute({ + sender: sender, + contract: contract, + functionName: func, + gasLimit: gasLimit, + args: args, + tokenTransfers: [transfer], + }); + const abiExecuteDraft = abiAwareFactory.createTransactionForExecute({ + sender: sender, + contract: contract, + functionName: func, + gasLimit: gasLimit, + args: args, + tokenTransfers: [transfer], + }); + + assert.equal(executeDraft.sender, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + assert.equal(executeDraft.receiver, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + + assert.isDefined(executeDraft.data); + assert.deepEqual( + executeDraft.data, + Buffer.from( + "ESDTNFTTransfer@4e46542d313233343536@01@01@00000000000000000500b9353fe8407f87310c87e12fa1ac807f0485da39d152@616464@07" + ) + ); + + assert.equal(executeDraft.gasLimit.valueOf(), gasLimit); + assert.equal(executeDraft.value.valueOf(), "0"); + + assert.deepEqual(executeDraft, abiExecuteDraft); + }); + + it("should create draft transaction for execute and transfer multiple nfts", async function () { + const sender = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + const contract = Address.fromBech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); + const func = "add"; + const gasLimit = 6000000; + const args = [new U32Value(7)]; + + const firstToken = new Token("NFT-123456", 1); + const firstTransfer = new NextTokenTransfer(firstToken, 1); + const secondToken = new Token("NFT-123456", 42); + const secondTransfer = new NextTokenTransfer(secondToken, 1); + + const executeDraft = factory.createTransactionForExecute({ + sender: sender, + contract: contract, + functionName: func, + gasLimit: gasLimit, + args: args, + tokenTransfers: [firstTransfer, secondTransfer], + }); + const abiExecuteDraft = abiAwareFactory.createTransactionForExecute({ + sender: sender, + contract: contract, + functionName: func, + gasLimit: gasLimit, + args: args, + tokenTransfers: [firstTransfer, secondTransfer], + }); + + assert.equal(executeDraft.sender, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + assert.equal(executeDraft.receiver, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + + assert.isDefined(executeDraft.data); + assert.deepEqual( + executeDraft.data, + Buffer.from( + "MultiESDTNFTTransfer@00000000000000000500b9353fe8407f87310c87e12fa1ac807f0485da39d152@02@4e46542d313233343536@01@01@4e46542d313233343536@2a@01@616464@07" + ) + ); + + assert.equal(executeDraft.gasLimit.valueOf(), gasLimit); + assert.equal(executeDraft.value.valueOf(), "0"); + + assert.deepEqual(executeDraft, abiExecuteDraft); + }); + + it("should create draft transaction for upgrade", async function () { + const sender = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + const contract = Address.fromBech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); + const gasLimit = 6000000; + const args = [new U32Value(0)]; + + const upgradeDraft = factory.createTransactionForUpgrade({ + sender: sender, + contract: contract, + bytecode: adderByteCode.valueOf(), + gasLimit: gasLimit, + args: args, + }); + + const abiUpgradeDraft = abiAwareFactory.createTransactionForUpgrade({ + sender: sender, + contract: contract, + bytecode: adderByteCode.valueOf(), + gasLimit: gasLimit, + args: args, + }); + + assert.equal(upgradeDraft.sender, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + assert.equal(upgradeDraft.receiver, "erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); + assert.isTrue(Buffer.from(upgradeDraft.data!).toString().startsWith("upgradeContract@")); + assert.equal(upgradeDraft.gasLimit.valueOf(), gasLimit); + assert.equal(upgradeDraft.value, 0); + + assert.deepEqual(upgradeDraft, abiUpgradeDraft); + }); +}); diff --git a/src/transactionsFactories/smartContractTransactionsFactory.ts b/src/transactionsFactories/smartContractTransactionsFactory.ts new file mode 100644 index 00000000..143913ad --- /dev/null +++ b/src/transactionsFactories/smartContractTransactionsFactory.ts @@ -0,0 +1,188 @@ +import { BigNumber } from "bignumber.js"; +import { IAddress } from "../interface"; +import { DraftTransaction } from "../draftTransaction"; +import { ArgSerializer, CodeMetadata, ContractFunction, EndpointDefinition } from "../smartcontracts"; +import { byteArrayToHex, utf8ToHex } from "../utils.codec"; +import { CONTRACT_DEPLOY_ADDRESS, VM_TYPE_WASM_VM } from "../constants"; +import { NativeSerializer } from "../smartcontracts/nativeSerializer"; +import { Err, ErrBadUsage } from "../errors"; +import { Address } from "../address"; +import { DraftTransactionBuilder } from "./draftTransactionBuilder"; +import { Token, NextTokenTransfer } from "../tokens"; +import { TokenTransfersDataBuilder } from "./tokenTransfersDataBuilder"; + +interface Config { + chainID: string; + minGasLimit: BigNumber.Value; + gasLimitPerByte: BigNumber.Value; +} + +interface Abi { + constructorDefinition: EndpointDefinition; + + getEndpoint(name: string | ContractFunction): EndpointDefinition; +} + +interface TokenComputer { + isFungible(token: Token): boolean; +} + +export class SmartContractTransactionsFactory { + private readonly config: Config; + private readonly abiRegistry?: Abi; + private readonly tokenComputer: TokenComputer; + private readonly dataArgsBuilder: TokenTransfersDataBuilder; + + constructor({ config, abi, tokenComputer }: { config: Config; abi?: Abi; tokenComputer: TokenComputer }) { + this.config = config; + this.abiRegistry = abi; + this.tokenComputer = tokenComputer; + this.dataArgsBuilder = new TokenTransfersDataBuilder(); + } + + createTransactionForDeploy(options: { + sender: IAddress; + bytecode: Uint8Array; + gasLimit: BigNumber.Value; + args?: any[]; + nativeTransferAmount?: BigNumber.Value; + isUpgradeable?: boolean; + isReadable?: boolean; + isPayable?: boolean; + isPayableBySmartContract?: boolean; + }): DraftTransaction { + const nativeTransferAmount = options.nativeTransferAmount ?? 0; + + const isUpgradeable = options.isUpgradeable ?? true; + const isReadable = options.isReadable ?? true; + const isPayable = options.isPayable ?? false; + const isPayableBySmartContract = options.isPayableBySmartContract ?? true; + + const args = options.args || []; + + const metadata = new CodeMetadata(isUpgradeable, isReadable, isPayable, isPayableBySmartContract); + let parts = [byteArrayToHex(options.bytecode), byteArrayToHex(VM_TYPE_WASM_VM), metadata.toString()]; + + const preparedArgs = this.argsToDataParts(args, this.abiRegistry?.constructorDefinition); + parts = parts.concat(preparedArgs); + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: Address.fromBech32(CONTRACT_DEPLOY_ADDRESS), + dataParts: parts, + gasLimit: options.gasLimit, + addDataMovementGas: false, + amount: nativeTransferAmount, + }).build(); + } + + createTransactionForExecute(options: { + sender: IAddress; + contract: IAddress; + functionName: string; + gasLimit: BigNumber.Value; + args?: any[]; + nativeTransferAmount?: BigNumber.Value; + tokenTransfers?: NextTokenTransfer[]; + }): DraftTransaction { + const args = options.args || []; + const tokenTransfer = options.tokenTransfers || []; + const nativeTransferAmount = options.nativeTransferAmount ?? 0; + const numberOfTokens = tokenTransfer.length; + + if (nativeTransferAmount && numberOfTokens) { + throw new ErrBadUsage("Can't send both native tokens and custom tokens(ESDT/NFT)"); + } + + let receiver = options.contract; + let dataParts: string[] = []; + + if (numberOfTokens === 1) { + const transfer = tokenTransfer[0]; + + if (this.tokenComputer.isFungible(transfer.token)) { + dataParts = this.dataArgsBuilder.buildArgsForESDTTransfer(transfer); + } else { + dataParts = this.dataArgsBuilder.buildArgsForSingleESDTNFTTransfer(transfer, receiver); + receiver = options.sender; + } + } else if (numberOfTokens > 1) { + dataParts = this.dataArgsBuilder.buildArgsForMultiESDTNFTTransfer(receiver, tokenTransfer); + receiver = options.sender; + } + + dataParts.push(dataParts.length ? utf8ToHex(options.functionName) : options.functionName); + dataParts = dataParts.concat(this.argsToDataParts(args, this.abiRegistry?.getEndpoint(options.functionName))); + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: receiver, + dataParts: dataParts, + gasLimit: options.gasLimit, + addDataMovementGas: false, + amount: nativeTransferAmount, + }).build(); + } + + createTransactionForUpgrade(options: { + sender: IAddress; + contract: IAddress; + bytecode: Uint8Array; + gasLimit: BigNumber.Value; + args?: any[]; + nativeTransferAmount?: BigNumber.Value; + isUpgradeable?: boolean; + isReadable?: boolean; + isPayable?: boolean; + isPayableBySmartContract?: boolean; + }): DraftTransaction { + const nativeTransferAmount = options.nativeTransferAmount ?? 0; + + const isUpgradeable = options.isUpgradeable ?? true; + const isReadable = options.isReadable ?? true; + const isPayable = options.isPayable ?? false; + const isPayableBySmartContract = options.isPayableBySmartContract ?? true; + + const args = options.args || []; + const metadata = new CodeMetadata(isUpgradeable, isReadable, isPayable, isPayableBySmartContract); + + let parts = ["upgradeContract", byteArrayToHex(options.bytecode), metadata.toString()]; + + const preparedArgs = this.argsToDataParts(args, this.abiRegistry?.constructorDefinition); + parts = parts.concat(preparedArgs); + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: options.contract, + dataParts: parts, + gasLimit: options.gasLimit, + addDataMovementGas: false, + amount: nativeTransferAmount, + }).build(); + } + + private argsToDataParts(args: any[], endpoint?: EndpointDefinition): string[] { + if (endpoint) { + const typedArgs = NativeSerializer.nativeToTypedValues(args, endpoint); + return new ArgSerializer().valuesToStrings(typedArgs); + } + + if (this.areArgsOfTypedValue(args)) { + return new ArgSerializer().valuesToStrings(args); + } + + throw new Err("Can't convert args to TypedValues"); + } + + private areArgsOfTypedValue(args: any[]): boolean { + for (const arg of args) { + if (!arg.belongsToTypesystem) { + return false; + } + } + return true; + } +} diff --git a/src/transactionsFactories/tokenManagementTransactionIntentsFactory.spec.ts b/src/transactionsFactories/tokenManagementTransactionIntentsFactory.spec.ts new file mode 100644 index 00000000..01b49506 --- /dev/null +++ b/src/transactionsFactories/tokenManagementTransactionIntentsFactory.spec.ts @@ -0,0 +1,153 @@ +import { assert } from "chai"; +import { loadTestWallets, TestWallet } from "../testutils"; +import { TokenManagementTransactionsFactory } from "./tokenManagementTransactionsFactory"; +import { TransactionsFactoryConfig } from "./transactionsFactoryConfig"; +import BigNumber from "bignumber.js"; +import { ESDT_CONTRACT_ADDRESS } from "../constants"; + +describe("test token management transactions factory", () => { + let frank: TestWallet, grace: TestWallet; + let factory: TokenManagementTransactionsFactory; + let config: TransactionsFactoryConfig; + + before(async function () { + ({ frank, grace } = await loadTestWallets()); + config = new TransactionsFactoryConfig("T"); + factory = new TokenManagementTransactionsFactory(config); + }); + + it("should create draft transaction for registering and setting roles", () => { + const draft = factory.createTransactionForRegisteringAndSettingRoles({ + sender: frank.address, + tokenName: "TEST", + tokenTicker: "TEST", + tokenType: "FNG", + numDecimals: 2 + }); + + assert.deepEqual(draft.data, Buffer.from("registerAndSetAllRoles@54455354@54455354@464e47@02")); + assert.equal(draft.sender, frank.address.toString()); + assert.equal(draft.receiver, "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u"); + assert.deepEqual(draft.value, config.issueCost); + assert.deepEqual(draft.gasLimit, new BigNumber("60125000")); + }); + + it("should create draft transaction for issuing fungible token", () => { + const draft = factory.createTransactionForIssuingFungible({ + sender: frank.address, + tokenName: "FRANK", + tokenTicker: "FRANK", + initialSupply: 100, + numDecimals: 0, + canFreeze: true, + canWipe: true, + canPause: true, + canTransferNFTCreateRole: false, + canChangeOwner: true, + canUpgrade: true, + canAddSpecialRoles: true + }); + + assert.deepEqual(draft.data, Buffer.from("issue@4652414e4b@4652414e4b@64@@63616e467265657a65@74727565@63616e57697065@74727565@63616e5061757365@74727565@63616e4368616e67654f776e6572@74727565@63616e55706772616465@74727565@63616e4164645370656369616c526f6c6573@74727565")); + assert.equal(draft.sender, frank.address.toString()); + assert.equal(draft.receiver, ESDT_CONTRACT_ADDRESS); + assert.deepEqual(draft.value, config.issueCost); + }); + + it("should create draft transaction for issuing semi-fungible token", () => { + const draft = factory.createTransactionForIssuingSemiFungible({ + sender: frank.address, + tokenName: "FRANK", + tokenTicker: "FRANK", + canFreeze: true, + canWipe: true, + canPause: true, + canTransferNFTCreateRole: true, + canChangeOwner: true, + canUpgrade: true, + canAddSpecialRoles: true + }); + + assert.deepEqual(draft.data, Buffer.from("issueSemiFungible@4652414e4b@4652414e4b@63616e467265657a65@74727565@63616e57697065@74727565@63616e5061757365@74727565@63616e5472616e736665724e4654437265617465526f6c65@74727565@63616e4368616e67654f776e6572@74727565@63616e55706772616465@74727565@63616e4164645370656369616c526f6c6573@74727565")); + assert.equal(draft.sender, frank.address.toString()); + assert.equal(draft.receiver, ESDT_CONTRACT_ADDRESS); + assert.deepEqual(draft.value, config.issueCost); + }); + + it("should create draft transaction for issuing non-fungible token", () => { + const draft = factory.createTransactionForIssuingNonFungible({ + sender: frank.address, + tokenName: "FRANK", + tokenTicker: "FRANK", + canFreeze: true, + canWipe: true, + canPause: true, + canTransferNFTCreateRole: true, + canChangeOwner: true, + canUpgrade: true, + canAddSpecialRoles: true + }); + + assert.deepEqual(draft.data, Buffer.from("issueNonFungible@4652414e4b@4652414e4b@63616e467265657a65@74727565@63616e57697065@74727565@63616e5061757365@74727565@63616e5472616e736665724e4654437265617465526f6c65@74727565@63616e4368616e67654f776e6572@74727565@63616e55706772616465@74727565@63616e4164645370656369616c526f6c6573@74727565")); + assert.equal(draft.sender, frank.address.toString()); + assert.equal(draft.receiver, ESDT_CONTRACT_ADDRESS); + assert.deepEqual(draft.value, config.issueCost); + }); + + it("should create draft transaction for registering metaEsdt", () => { + const draft = factory.createTransactionForRegisteringMetaESDT({ + sender: frank.address, + tokenName: "FRANK", + tokenTicker: "FRANK", + numDecimals: 10, + canFreeze: true, + canWipe: true, + canPause: true, + canTransferNFTCreateRole: true, + canChangeOwner: true, + canUpgrade: true, + canAddSpecialRoles: true + }); + + assert.deepEqual(draft.data, Buffer.from("registerMetaESDT@4652414e4b@4652414e4b@0a@63616e467265657a65@74727565@63616e57697065@74727565@63616e5061757365@74727565@63616e5472616e736665724e4654437265617465526f6c65@74727565@63616e4368616e67654f776e6572@74727565@63616e55706772616465@74727565@63616e4164645370656369616c526f6c6573@74727565")); + assert.equal(draft.sender, frank.address.toString()); + assert.equal(draft.receiver, ESDT_CONTRACT_ADDRESS); + assert.deepEqual(draft.value, config.issueCost); + }); + + it("should create draft transaction for setting spcial role on non-fungible token", () => { + const draft = factory.createTransactionForSettingSpecialRoleOnNonFungibleToken({ + sender: frank.address, + user: grace.address, + tokenIdentifier: "FRANK-11ce3e", + addRoleNFTCreate: true, + addRoleNFTBurn: false, + addRoleNFTUpdateAttributes: true, + addRoleNFTAddURI: true, + addRoleESDTTransferRole: false + }); + + assert.deepEqual(draft.data, Buffer.from("setSpecialRole@4652414e4b2d313163653365@1e8a8b6b49de5b7be10aaa158a5a6a4abb4b56cc08f524bb5e6cd5f211ad3e13@45534454526f6c654e4654437265617465@45534454526f6c654e465455706461746541747472696275746573@45534454526f6c654e4654416464555249")); + assert.equal(draft.sender, frank.address.toString()); + assert.equal(draft.receiver, ESDT_CONTRACT_ADDRESS); + assert.equal(draft.value, 0); + }); + + it("should create draft transaction for creating nft", () => { + const draft = factory.createTransactionForCreatingNFT({ + sender: grace.address, + tokenIdentifier: "FRANK-aa9e8d", + initialQuantity: 1, + name: `test`, + royalties: 1000, + hash: "abba", + attributes: Buffer.from("test"), + uris: ["a", "b"] + }); + + assert.deepEqual(draft.data, Buffer.from("ESDTNFTCreate@4652414e4b2d616139653864@01@74657374@03e8@61626261@74657374@61@62")); + assert.equal(draft.sender, grace.address.toString()); + assert.equal(draft.receiver, grace.address.toString()); + assert.equal(draft.value, 0); + }); +}); diff --git a/src/transactionsFactories/tokenManagementTransactionsFactory.ts b/src/transactionsFactories/tokenManagementTransactionsFactory.ts new file mode 100644 index 00000000..1cb05cf8 --- /dev/null +++ b/src/transactionsFactories/tokenManagementTransactionsFactory.ts @@ -0,0 +1,617 @@ +import BigNumber from "bignumber.js"; +import { DraftTransaction } from "../draftTransaction"; +import { DraftTransactionBuilder } from "./draftTransactionBuilder"; +import { IAddress } from "../interface"; +import { utf8ToHex, boolToHex, bigIntToHex, addressToHex, byteArrayToHex } from "../utils.codec"; +import { ESDT_CONTRACT_ADDRESS } from "../constants"; +import { Address } from "../address"; +import { Logger } from "../logger"; + +interface Config { + chainID: string + minGasLimit: BigNumber.Value + gasLimitPerByte: BigNumber.Value + gasLimitIssue: BigNumber.Value + gasLimitToggleBurnRoleGlobally: BigNumber.Value + gasLimitEsdtLocalMint: BigNumber.Value + gasLimitEsdtLocalBurn: BigNumber.Value + gasLimitSetSpecialRole: BigNumber.Value + gasLimitPausing: BigNumber.Value + gasLimitFreezing: BigNumber.Value + gasLimitWiping: BigNumber.Value + gasLimitEsdtNftCreate: BigNumber.Value + gasLimitEsdtNftUpdateAttributes: BigNumber.Value + gasLimitEsdtNftAddQuantity: BigNumber.Value + gasLimitEsdtNftBurn: BigNumber.Value + gasLimitStorePerByte: BigNumber.Value + issueCost: BigNumber.Value +} + +type RegisterAndSetAllRolesTokenType = "NFT" | "SFT" | "META" | "FNG"; + +export class TokenManagementTransactionsFactory { + private readonly config: Config; + private readonly trueAsHex: string; + + constructor(config: Config) { + this.config = config; + this.trueAsHex = utf8ToHex("true"); + } + + createTransactionForIssuingFungible(options: { + sender: IAddress, + tokenName: string, + tokenTicker: string, + initialSupply: BigNumber.Value, + numDecimals: BigNumber.Value, + canFreeze: boolean, + canWipe: boolean, + canPause: boolean, + canTransferNFTCreateRole: boolean, + canChangeOwner: boolean, + canUpgrade: boolean, + canAddSpecialRoles: boolean + }): DraftTransaction { + this.notifyAboutUnsettingBurnRoleGlobally(); + + const dataParts = [ + "issue", + utf8ToHex(options.tokenName), + utf8ToHex(options.tokenTicker), + bigIntToHex(options.initialSupply), + bigIntToHex(options.numDecimals), + ...(options.canFreeze ? [utf8ToHex("canFreeze"), this.trueAsHex] : []), + ...(options.canWipe ? [utf8ToHex("canWipe"), this.trueAsHex] : []), + ...(options.canPause ? [utf8ToHex("canPause"), this.trueAsHex] : []), + ...(options.canTransferNFTCreateRole ? [utf8ToHex("canTransferNFTCreateRole"), this.trueAsHex] : []), + ...(options.canChangeOwner ? [utf8ToHex("canChangeOwner"), this.trueAsHex] : []), + ...[utf8ToHex("canUpgrade"), boolToHex(options.canUpgrade)], + ...[utf8ToHex("canAddSpecialRoles"), boolToHex(options.canAddSpecialRoles)] + ]; + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: Address.fromBech32(ESDT_CONTRACT_ADDRESS), + dataParts: dataParts, + gasLimit: this.config.gasLimitIssue, + addDataMovementGas: true, + amount: this.config.issueCost + }).build(); + } + + createTransactionForIssuingSemiFungible(options: { + sender: IAddress; + tokenName: string; + tokenTicker: string; + canFreeze: boolean; + canWipe: boolean; + canPause: boolean; + canTransferNFTCreateRole: boolean; + canChangeOwner: boolean; + canUpgrade: boolean; + canAddSpecialRoles: boolean; + }): DraftTransaction { + this.notifyAboutUnsettingBurnRoleGlobally(); + + const dataParts = [ + "issueSemiFungible", + utf8ToHex(options.tokenName), + utf8ToHex(options.tokenTicker), + ...(options.canFreeze ? [utf8ToHex("canFreeze"), this.trueAsHex] : []), + ...(options.canWipe ? [utf8ToHex("canWipe"), this.trueAsHex] : []), + ...(options.canPause ? [utf8ToHex("canPause"), this.trueAsHex] : []), + ...(options.canTransferNFTCreateRole ? [utf8ToHex("canTransferNFTCreateRole"), this.trueAsHex] : []), + ...(options.canChangeOwner ? [utf8ToHex("canChangeOwner"), this.trueAsHex] : []), + ...[utf8ToHex("canUpgrade"), boolToHex(options.canUpgrade)], + ...[utf8ToHex("canAddSpecialRoles"), boolToHex(options.canAddSpecialRoles)] + ]; + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: Address.fromBech32(ESDT_CONTRACT_ADDRESS), + dataParts: dataParts, + gasLimit: this.config.gasLimitIssue, + addDataMovementGas: true, + amount: this.config.issueCost + }).build(); + } + + createTransactionForIssuingNonFungible(options: { + sender: IAddress; + tokenName: string; + tokenTicker: string; + canFreeze: boolean; + canWipe: boolean; + canPause: boolean; + canTransferNFTCreateRole: boolean; + canChangeOwner: boolean; + canUpgrade: boolean; + canAddSpecialRoles: boolean; + }): DraftTransaction { + this.notifyAboutUnsettingBurnRoleGlobally(); + + const dataParts = [ + "issueNonFungible", + utf8ToHex(options.tokenName), + utf8ToHex(options.tokenTicker), + ...(options.canFreeze ? [utf8ToHex("canFreeze"), this.trueAsHex] : []), + ...(options.canWipe ? [utf8ToHex("canWipe"), this.trueAsHex] : []), + ...(options.canPause ? [utf8ToHex("canPause"), this.trueAsHex] : []), + ...(options.canTransferNFTCreateRole ? [utf8ToHex("canTransferNFTCreateRole"), this.trueAsHex] : []), + ...(options.canChangeOwner ? [utf8ToHex("canChangeOwner"), this.trueAsHex] : []), + ...[utf8ToHex("canUpgrade"), boolToHex(options.canUpgrade)], + ...[utf8ToHex("canAddSpecialRoles"), boolToHex(options.canAddSpecialRoles)] + ]; + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: Address.fromBech32(ESDT_CONTRACT_ADDRESS), + dataParts: dataParts, + gasLimit: this.config.gasLimitIssue, + addDataMovementGas: true, + amount: this.config.issueCost + }).build(); + } + + createTransactionForRegisteringMetaESDT(options: { + sender: IAddress; + tokenName: string; + tokenTicker: string; + numDecimals: BigNumber.Value, + canFreeze: boolean; + canWipe: boolean; + canPause: boolean; + canTransferNFTCreateRole: boolean; + canChangeOwner: boolean; + canUpgrade: boolean; + canAddSpecialRoles: boolean; + }): DraftTransaction { + this.notifyAboutUnsettingBurnRoleGlobally(); + + const dataParts = [ + "registerMetaESDT", + utf8ToHex(options.tokenName), + utf8ToHex(options.tokenTicker), + bigIntToHex(options.numDecimals), + ...(options.canFreeze ? [utf8ToHex("canFreeze"), this.trueAsHex] : []), + ...(options.canWipe ? [utf8ToHex("canWipe"), this.trueAsHex] : []), + ...(options.canPause ? [utf8ToHex("canPause"), this.trueAsHex] : []), + ...(options.canTransferNFTCreateRole ? [utf8ToHex("canTransferNFTCreateRole"), this.trueAsHex] : []), + ...(options.canChangeOwner ? [utf8ToHex("canChangeOwner"), this.trueAsHex] : []), + ...(options.canUpgrade ? [utf8ToHex("canUpgrade"), this.trueAsHex] : []), + ...(options.canAddSpecialRoles ? [utf8ToHex("canAddSpecialRoles"), this.trueAsHex] : []), + ]; + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: Address.fromBech32(ESDT_CONTRACT_ADDRESS), + dataParts: dataParts, + gasLimit: this.config.gasLimitIssue, + addDataMovementGas: true, + amount: this.config.issueCost + }).build(); + } + + createTransactionForRegisteringAndSettingRoles(options: { + sender: IAddress; + tokenName: string; + tokenTicker: string; + tokenType: RegisterAndSetAllRolesTokenType; + numDecimals: BigNumber.Value; + }): DraftTransaction { + this.notifyAboutUnsettingBurnRoleGlobally(); + + const dataParts = [ + "registerAndSetAllRoles", + utf8ToHex(options.tokenName), + utf8ToHex(options.tokenTicker), + utf8ToHex(options.tokenType), + bigIntToHex(options.numDecimals) + ]; + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: Address.fromBech32(ESDT_CONTRACT_ADDRESS), + dataParts: dataParts, + gasLimit: this.config.gasLimitIssue, + addDataMovementGas: true, + amount: this.config.issueCost + }).build(); + } + + createTransactionForSettingBurnRoleGlobally(options: { + sender: IAddress, + tokenIdentifier: string + }): DraftTransaction { + const dataParts = [ + "setBurnRoleGlobally", + utf8ToHex(options.tokenIdentifier) + ]; + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: Address.fromBech32(ESDT_CONTRACT_ADDRESS), + dataParts: dataParts, + gasLimit: this.config.gasLimitToggleBurnRoleGlobally, + addDataMovementGas: true + }).build(); + } + + createTransactionForUnsettingBurnRoleGlobally(options: { + sender: IAddress, + tokenIdentifier: string + }): DraftTransaction { + const dataParts = [ + "unsetBurnRoleGlobally", + utf8ToHex(options.tokenIdentifier) + ]; + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: Address.fromBech32(ESDT_CONTRACT_ADDRESS), + dataParts: dataParts, + gasLimit: this.config.gasLimitToggleBurnRoleGlobally, + addDataMovementGas: true, + }).build(); + } + + createTransactionForSettingSpecialRoleOnFungibleToken(options: { + sender: IAddress; + user: IAddress; + tokenIdentifier: string; + addRoleLocalMint: boolean; + addRoleLocalBurn: boolean; + }): DraftTransaction { + const dataParts = [ + "setSpecialRole", + utf8ToHex(options.tokenIdentifier), + addressToHex(options.user), + ...(options.addRoleLocalMint ? [utf8ToHex("ESDTRoleLocalMint")] : []), + ...(options.addRoleLocalBurn ? [utf8ToHex("ESDTRoleLocalBurn")] : []), + ]; + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: Address.fromBech32(ESDT_CONTRACT_ADDRESS), + dataParts: dataParts, + gasLimit: this.config.gasLimitSetSpecialRole, + addDataMovementGas: true + }).build(); + } + + createTransactionForSettingSpecialRoleOnSemiFungibleToken(options: { + sender: IAddress; + user: IAddress; + tokenIdentifier: string; + addRoleNFTCreate: boolean; + addRoleNFTBurn: boolean; + addRoleNFTAddQuantity: boolean; + addRoleESDTTransferRole: boolean; + }): DraftTransaction { + const dataParts = [ + "setSpecialRole", + utf8ToHex(options.tokenIdentifier), + addressToHex(options.user), + ...(options.addRoleNFTCreate ? [utf8ToHex("ESDTRoleNFTCreate")] : []), + ...(options.addRoleNFTBurn ? [utf8ToHex("ESDTRoleNFTBurn")] : []), + ...(options.addRoleNFTAddQuantity ? [utf8ToHex("ESDTRoleNFTAddQuantity")] : []), + ...(options.addRoleESDTTransferRole ? [utf8ToHex("ESDTTransferRole")] : []), + ]; + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: Address.fromBech32(ESDT_CONTRACT_ADDRESS), + dataParts: dataParts, + gasLimit: this.config.gasLimitSetSpecialRole, + addDataMovementGas: true + }).build(); + } + + createTransactionForSettingSpecialRoleOnMetaESDT(options: { + sender: IAddress; + user: IAddress; + tokenIdentifier: string; + addRoleNFTCreate: boolean; + addRoleNFTBurn: boolean; + addRoleNFTAddQuantity: boolean; + addRoleESDTTransferRole: boolean; + }): DraftTransaction { + return this.createTransactionForSettingSpecialRoleOnSemiFungibleToken(options); + } + + createTransactionForSettingSpecialRoleOnNonFungibleToken(options: { + sender: IAddress; + user: IAddress; + tokenIdentifier: string; + addRoleNFTCreate: boolean; + addRoleNFTBurn: boolean; + addRoleNFTUpdateAttributes: boolean; + addRoleNFTAddURI: boolean; + addRoleESDTTransferRole: boolean; + }): DraftTransaction { + const dataParts = [ + "setSpecialRole", + utf8ToHex(options.tokenIdentifier), + addressToHex(options.user), + ...(options.addRoleNFTCreate ? [utf8ToHex("ESDTRoleNFTCreate")] : []), + ...(options.addRoleNFTBurn ? [utf8ToHex("ESDTRoleNFTBurn")] : []), + ...(options.addRoleNFTUpdateAttributes ? [utf8ToHex("ESDTRoleNFTUpdateAttributes")] : []), + ...(options.addRoleNFTAddURI ? [utf8ToHex("ESDTRoleNFTAddURI")] : []), + ...(options.addRoleESDTTransferRole ? [utf8ToHex("ESDTTransferRole")] : []), + ]; + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: Address.fromBech32(ESDT_CONTRACT_ADDRESS), + dataParts: dataParts, + gasLimit: this.config.gasLimitSetSpecialRole, + addDataMovementGas: true + }).build(); + } + + createTransactionForCreatingNFT(options: { + sender: IAddress; + tokenIdentifier: string; + initialQuantity: BigNumber.Value; + name: string; + royalties: number; + hash: string; + attributes: Uint8Array; + uris: string[]; + }): DraftTransaction { + const dataParts = [ + "ESDTNFTCreate", + utf8ToHex(options.tokenIdentifier), + bigIntToHex(options.initialQuantity), + utf8ToHex(options.name), + bigIntToHex(options.royalties), + utf8ToHex(options.hash), + byteArrayToHex(options.attributes), + ...options.uris.map(utf8ToHex), + ]; + + // Note that the following is an approximation (a reasonable one): + const nftData = options.name + options.hash + options.attributes + options.uris.join(""); + const storageGasLimit = new BigNumber(this.config.gasLimitPerByte).multipliedBy(nftData.length); + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: options.sender, + dataParts: dataParts, + gasLimit: new BigNumber(this.config.gasLimitEsdtNftCreate).plus(storageGasLimit), + addDataMovementGas: true + }).build(); + } + + createTransactionForPausing(options: { + sender: IAddress; + tokenIdentifier: string; + }): DraftTransaction { + const dataParts = [ + "pause", + utf8ToHex(options.tokenIdentifier) + ]; + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: options.sender, + dataParts: dataParts, + gasLimit: this.config.gasLimitPausing, + addDataMovementGas: true + }).build(); + } + + createTransactionForUnpausing(options: { + sender: IAddress; + tokenIdentifier: string; + }): DraftTransaction { + const dataParts = [ + "unPause", + utf8ToHex(options.tokenIdentifier) + ]; + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: options.sender, + dataParts: dataParts, + gasLimit: this.config.gasLimitPausing, + addDataMovementGas: true + }).build(); + } + + createTransactionForFreezing(options: { + sender: IAddress; + user: IAddress; + tokenIdentifier: string; + }): DraftTransaction { + const dataParts = [ + "freeze", + utf8ToHex(options.tokenIdentifier), + addressToHex(options.user) + ]; + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: options.sender, + dataParts: dataParts, + gasLimit: this.config.gasLimitFreezing, + addDataMovementGas: true + }).build(); + } + + createTransactionForUnfreezing(options: { + sender: IAddress; + user: IAddress; + tokenIdentifier: string; + }): DraftTransaction { + const dataParts = [ + "UnFreeze", + utf8ToHex(options.tokenIdentifier), + addressToHex(options.user) + ]; + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: options.sender, + dataParts: dataParts, + gasLimit: this.config.gasLimitFreezing, + addDataMovementGas: true + }).build(); + } + + createTransactionForWiping(options: { + sender: IAddress; + user: IAddress; + tokenIdentifier: string; + }): DraftTransaction { + const dataParts = [ + "wipe", + utf8ToHex(options.tokenIdentifier), + addressToHex(options.user) + ]; + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: options.sender, + dataParts: dataParts, + gasLimit: this.config.gasLimitWiping, + addDataMovementGas: true + }).build(); + } + + createTransactionForLocalMint(options: { + sender: IAddress; + tokenIdentifier: string; + supplyToMint: BigNumber.Value; + }): DraftTransaction { + const dataParts = [ + "ESDTLocalMint", + utf8ToHex(options.tokenIdentifier), + bigIntToHex(options.supplyToMint), + ]; + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: options.sender, + dataParts: dataParts, + gasLimit: this.config.gasLimitEsdtLocalMint, + addDataMovementGas: true + }).build(); + } + + createTransactionForLocalBurning(options: { + sender: IAddress; + tokenIdentifier: string; + supplyToBurn: BigNumber.Value; + }): DraftTransaction { + const dataParts = [ + "ESDTLocalBurn", + utf8ToHex(options.tokenIdentifier), + bigIntToHex(options.supplyToBurn), + ]; + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: options.sender, + dataParts: dataParts, + gasLimit: this.config.gasLimitEsdtLocalBurn, + addDataMovementGas: true + }).build(); + } + + createTransactionForUpdatingAttributes(options: { + sender: IAddress; + tokenIdentifier: string; + tokenNonce: BigNumber.Value; + attributes: Uint8Array + }): DraftTransaction { + const dataParts = [ + "ESDTNFTUpdateAttributes", + utf8ToHex(options.tokenIdentifier), + bigIntToHex(options.tokenNonce), + byteArrayToHex(options.attributes), + ]; + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: options.sender, + dataParts: dataParts, + gasLimit: this.config.gasLimitEsdtNftUpdateAttributes, + addDataMovementGas: true + }).build(); + } + + createTransactionForAddingQuantity(options: { + sender: IAddress; + tokenIdentifier: string; + tokenNonce: BigNumber.Value; + quantityToAdd: BigNumber.Value + }): DraftTransaction { + const dataParts = [ + "ESDTNFTAddQuantity", + utf8ToHex(options.tokenIdentifier), + bigIntToHex(options.tokenNonce), + bigIntToHex(options.quantityToAdd) + ]; + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: options.sender, + dataParts: dataParts, + gasLimit: this.config.gasLimitEsdtNftAddQuantity, + addDataMovementGas: true + }).build(); + } + + createTransactionForBurningQuantity(options: { + sender: IAddress; + tokenIdentifier: string; + tokenNonce: BigNumber.Value; + quantityToBurn: BigNumber.Value + }): DraftTransaction { + const dataParts = [ + "ESDTNFTBurn", + utf8ToHex(options.tokenIdentifier), + bigIntToHex(options.tokenNonce), + bigIntToHex(options.quantityToBurn) + ]; + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: options.sender, + dataParts: dataParts, + gasLimit: this.config.gasLimitEsdtNftBurn, + addDataMovementGas: true + }).build(); + } + + private notifyAboutUnsettingBurnRoleGlobally() { + Logger.info(` +========== +IMPORTANT! +========== +You are about to issue (register) a new token. This will set the role "ESDTRoleBurnForAll" (globally). +Once the token is registered, you can unset this role by calling "unsetBurnRoleGlobally" (in a separate transaction).`); + } +} diff --git a/src/transactionsFactories/tokenTransfersDataBuilder.ts b/src/transactionsFactories/tokenTransfersDataBuilder.ts new file mode 100644 index 00000000..512cd2eb --- /dev/null +++ b/src/transactionsFactories/tokenTransfersDataBuilder.ts @@ -0,0 +1,47 @@ +import { IAddress } from "../interface"; +import { NextTokenTransfer, TokenComputer } from "../tokens"; +import { numberToPaddedHex, utf8ToHex, addressToHex } from "../utils.codec"; + +export class TokenTransfersDataBuilder { + private tokenComputer: TokenComputer; + + constructor() { + this.tokenComputer = new TokenComputer(); + } + + buildArgsForESDTTransfer(transfer: NextTokenTransfer): string[] { + let args = ["ESDTTransfer"]; + args.push(...[utf8ToHex(transfer.token.identifier), numberToPaddedHex(transfer.amount)]); + return args; + } + + buildArgsForSingleESDTNFTTransfer(transfer: NextTokenTransfer, receiver: IAddress) { + let args = ["ESDTNFTTransfer"]; + + const token = transfer.token; + const identifier = this.tokenComputer.extractIdentifierFromExtendedIdentifier(token.identifier); + + args.push( + ...[ + utf8ToHex(identifier), + numberToPaddedHex(token.nonce), + numberToPaddedHex(transfer.amount), + addressToHex(receiver), + ] + ); + return args; + } + + buildArgsForMultiESDTNFTTransfer(receiver: IAddress, transfers: NextTokenTransfer[]) { + let args = ["MultiESDTNFTTransfer", addressToHex(receiver), numberToPaddedHex(transfers.length)]; + + for (let transfer of transfers) { + const identifier = this.tokenComputer.extractIdentifierFromExtendedIdentifier(transfer.token.identifier); + args.push( + ...[utf8ToHex(identifier), numberToPaddedHex(transfer.token.nonce), numberToPaddedHex(transfer.amount)] + ); + } + + return args; + } +} diff --git a/src/transactionsFactories/transactionsFactoryConfig.ts b/src/transactionsFactories/transactionsFactoryConfig.ts new file mode 100644 index 00000000..c0727670 --- /dev/null +++ b/src/transactionsFactories/transactionsFactoryConfig.ts @@ -0,0 +1,71 @@ +import { BigNumber } from "bignumber.js"; +import { DEFAULT_HRP } from "../constants"; + +export class TransactionsFactoryConfig { + chainID: string; + addressHrp: string; + minGasLimit: BigNumber.Value; + gasLimitPerByte: BigNumber.Value; + gasLimitIssue: BigNumber.Value; + gasLimitToggleBurnRoleGlobally: BigNumber.Value; + gasLimitEsdtLocalMint: BigNumber.Value; + gasLimitEsdtLocalBurn: BigNumber.Value; + gasLimitSetSpecialRole: BigNumber.Value; + gasLimitPausing: BigNumber.Value; + gasLimitFreezing: BigNumber.Value; + gasLimitWiping: BigNumber.Value; + gasLimitEsdtNftCreate: BigNumber.Value; + gasLimitEsdtNftUpdateAttributes: BigNumber.Value; + gasLimitEsdtNftAddQuantity: BigNumber.Value; + gasLimitEsdtNftBurn: BigNumber.Value; + gasLimitStorePerByte: BigNumber.Value; + issueCost: BigNumber.Value; + gasLimitStake: BigNumber.Value; + gasLimitUnstake: BigNumber.Value; + gasLimitUnbond: BigNumber.Value; + gasLimitCreateDelegationContract: BigNumber.Value; + gasLimitDelegationOperations: BigNumber.Value; + additionalGasLimitPerValidatorNode: BigNumber.Value; + additionalGasLimitForDelegationOperations: BigNumber.Value; + gasLimitESDTTransfer: BigNumber.Value; + gasLimitESDTNFTTransfer: BigNumber.Value; + gasLimitMultiESDTNFTTransfer: BigNumber.Value; + + constructor(chainId: string) { + // General-purpose configuration + this.chainID = chainId; + this.addressHrp = DEFAULT_HRP; + this.minGasLimit = new BigNumber(50000); + this.gasLimitPerByte = new BigNumber(1500); + + // Configuration for token operations + this.gasLimitIssue = new BigNumber(60000000); + this.gasLimitToggleBurnRoleGlobally = new BigNumber(60000000); + this.gasLimitEsdtLocalMint = new BigNumber(300000); + this.gasLimitEsdtLocalBurn = new BigNumber(300000); + this.gasLimitSetSpecialRole = new BigNumber(60000000); + this.gasLimitPausing = new BigNumber(60000000); + this.gasLimitFreezing = new BigNumber(60000000); + this.gasLimitWiping = new BigNumber(60000000); + this.gasLimitEsdtNftCreate = new BigNumber(3000000); + this.gasLimitEsdtNftUpdateAttributes = new BigNumber(1000000); + this.gasLimitEsdtNftAddQuantity = new BigNumber(1000000); + this.gasLimitEsdtNftBurn = new BigNumber(1000000); + this.gasLimitStorePerByte = new BigNumber(50000); + this.issueCost = new BigNumber("50000000000000000"); + + // Configuration for delegation operations + this.gasLimitStake = new BigNumber(5000000); + this.gasLimitUnstake = new BigNumber(5000000); + this.gasLimitUnbond = new BigNumber(5000000); + this.gasLimitCreateDelegationContract = new BigNumber(50000000); + this.gasLimitDelegationOperations = new BigNumber(1000000); + this.additionalGasLimitPerValidatorNode = new BigNumber(6000000); + this.additionalGasLimitForDelegationOperations = new BigNumber(10000000); + + // Configuration for token transfers + this.gasLimitESDTTransfer = new BigNumber(200000); + this.gasLimitESDTNFTTransfer = new BigNumber(200000); + this.gasLimitMultiESDTNFTTransfer = new BigNumber(200000); + } +} diff --git a/src/transactionsFactories/transferTransactionsFactory.spec.ts b/src/transactionsFactories/transferTransactionsFactory.spec.ts new file mode 100644 index 00000000..1f3b21de --- /dev/null +++ b/src/transactionsFactories/transferTransactionsFactory.spec.ts @@ -0,0 +1,119 @@ +import { assert } from "chai"; +import { Address } from "../address"; +import { Token, NextTokenTransfer, TokenComputer } from "../tokens"; +import { TransactionsFactoryConfig } from "./transactionsFactoryConfig"; +import { TransferTransactionsFactory } from "./transferTransactionsFactory"; +import { ErrBadUsage } from "../errors"; + +describe("test transfer transcations factory", function () { + const config = new TransactionsFactoryConfig("D"); + const factory = new TransferTransactionsFactory(config, new TokenComputer()); + + const alice = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + const bob = Address.fromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + + it("should throw error, no token transfer provided", async () => { + let transfers: any = []; + + assert.throw( + () => { + factory.createTransactionForESDTTokenTransfer({ + sender: alice, + receiver: bob, + tokenTransfers: transfers, + }); + }, + ErrBadUsage, + "No token transfer has been provided" + ); + }); + + it("should create draft transaction for native token transfer without data", async () => { + const transaction = factory.createTransactionForNativeTokenTransfer({ + sender: alice, + receiver: bob, + nativeAmount: "1000000000000000000", + }); + + assert.equal(transaction.sender, alice.bech32()); + assert.equal(transaction.receiver, bob.bech32()); + assert.equal(transaction.value.valueOf(), "1000000000000000000"); + assert.equal(transaction.gasLimit.valueOf(), "50000"); + assert.deepEqual(transaction.data, new Uint8Array()); + }); + + it("should create draft transaction for native token transfer with data", async () => { + const transaction = factory.createTransactionForNativeTokenTransfer({ + sender: alice, + receiver: bob, + nativeAmount: "1000000000000000000", + data: "test data", + }); + + assert.equal(transaction.sender, alice.bech32()); + assert.equal(transaction.receiver, bob.bech32()); + assert.equal(transaction.value.valueOf(), "1000000000000000000"); + assert.equal(transaction.gasLimit.valueOf(), "63500"); + assert.deepEqual(transaction.data, Buffer.from("test data")); + }); + + it("should create draft transaction for esdt transfer", async () => { + const fooToken = new Token("FOO-123456", 0); + const transfer = new NextTokenTransfer(fooToken, 1000000); + + const transaction = factory.createTransactionForESDTTokenTransfer({ + sender: alice, + receiver: bob, + tokenTransfers: [transfer], + }); + + assert.equal(transaction.sender, alice.bech32()); + assert.equal(transaction.receiver, bob.bech32()); + assert.equal(transaction.value.valueOf(), "0"); + assert.equal(transaction.gasLimit.valueOf(), "410000"); + assert.deepEqual(transaction.data.toString(), "ESDTTransfer@464f4f2d313233343536@0f4240"); + }); + + it("should create draft transaction for nft transfer", async () => { + const nft = new Token("NFT-123456", 10); + const transfer = new NextTokenTransfer(nft, 1); + + const transaction = factory.createTransactionForESDTTokenTransfer({ + sender: alice, + receiver: bob, + tokenTransfers: [transfer], + }); + + assert.equal(transaction.sender, alice.bech32()); + assert.equal(transaction.receiver, alice.bech32()); + assert.equal(transaction.value.valueOf(), "0"); + assert.equal(transaction.gasLimit.valueOf(), "1210500"); + assert.deepEqual( + transaction.data.toString(), + "ESDTNFTTransfer@4e46542d313233343536@0a@01@8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8" + ); + }); + + it("should create draft transaction for multiple nft transfers", async () => { + const firstNft = new Token("NFT-123456", 10); + const firstTransfer = new NextTokenTransfer(firstNft, 1); + + const secondNft = new Token("TEST-987654", 1); + const secondTransfer = new NextTokenTransfer(secondNft, 1); + + const transaction = factory.createTransactionForESDTTokenTransfer({ + sender: alice, + receiver: bob, + tokenTransfers: [firstTransfer, secondTransfer], + }); + + assert.equal(transaction.sender, alice.bech32()); + assert.equal(transaction.receiver, alice.bech32()); + assert.equal(transaction.value.valueOf(), "0"); + assert.equal(transaction.gasLimit.valueOf(), "1466000"); + assert.deepEqual( + transaction.data.toString(), + "MultiESDTNFTTransfer@8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8@02@4e46542d313233343536@0a@01@544553542d393837363534@01@01" + ); + }); +}); diff --git a/src/transactionsFactories/transferTransactionsFactory.ts b/src/transactionsFactories/transferTransactionsFactory.ts new file mode 100644 index 00000000..588c740e --- /dev/null +++ b/src/transactionsFactories/transferTransactionsFactory.ts @@ -0,0 +1,120 @@ +import BigNumber from "bignumber.js"; +import { TokenTransfersDataBuilder } from "./tokenTransfersDataBuilder"; +import { IAddress } from "../interface"; +import { DraftTransaction } from "../draftTransaction"; +import { DraftTransactionBuilder } from "./draftTransactionBuilder"; +import { NextTokenTransfer, Token } from "../tokens"; +import { ErrBadUsage } from "../errors"; + +const ADDITIONAL_GAS_FOR_ESDT_TRANSFER = 100000; +const ADDITIONAL_GAS_FOR_ESDT_NFT_TRANSFER = 800000; + +interface IConfig { + chainID: string; + minGasLimit: BigNumber.Value; + gasLimitPerByte: BigNumber.Value; + gasLimitESDTTransfer: BigNumber.Value; + gasLimitESDTNFTTransfer: BigNumber.Value; + gasLimitMultiESDTNFTTransfer: BigNumber.Value; +} + +interface TokenComputer { + isFungible(token: Token): boolean; +} + +export class TransferTransactionsFactory { + private readonly config: IConfig; + private readonly dataArgsBuilder: TokenTransfersDataBuilder; + private readonly tokenComputer: TokenComputer; + + constructor(config: IConfig, tokenComputer: TokenComputer) { + this.config = config; + this.tokenComputer = tokenComputer; + this.dataArgsBuilder = new TokenTransfersDataBuilder(); + } + + createTransactionForNativeTokenTransfer(options: { + sender: IAddress; + receiver: IAddress; + nativeAmount: BigNumber.Value; + data?: string; + }): DraftTransaction { + const data = options.data || ""; + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: options.receiver, + dataParts: [data], + gasLimit: 0, + addDataMovementGas: true, + amount: options.nativeAmount, + }).build(); + } + + createTransactionForESDTTokenTransfer(options: { + sender: IAddress; + receiver: IAddress; + tokenTransfers: NextTokenTransfer[]; + }): DraftTransaction { + const numberOfTransfers = options.tokenTransfers.length; + + if (numberOfTransfers === 0) { + throw new ErrBadUsage("No token transfer has been provided"); + } + + if (numberOfTransfers === 1) { + return this.createSingleESDTTransferDraft(options); + } + + const transferArgs = this.dataArgsBuilder.buildArgsForMultiESDTNFTTransfer( + options.receiver, + options.tokenTransfers + ); + + const extraGasForTransfer = new BigNumber(this.config.gasLimitMultiESDTNFTTransfer) + .multipliedBy(new BigNumber(numberOfTransfers)) + .plus(new BigNumber(ADDITIONAL_GAS_FOR_ESDT_NFT_TRANSFER)); + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: options.sender, + dataParts: transferArgs, + gasLimit: extraGasForTransfer, + addDataMovementGas: true, + }).build(); + } + + private createSingleESDTTransferDraft(options: { + sender: IAddress; + receiver: IAddress; + tokenTransfers: NextTokenTransfer[]; + }): DraftTransaction { + let transferArgs: string[] = []; + const transfer = options.tokenTransfers[0]; + let extraGasForTransfer = new BigNumber(0); + let receiver = options.receiver; + + if (this.tokenComputer.isFungible(transfer.token)) { + transferArgs = this.dataArgsBuilder.buildArgsForESDTTransfer(transfer); + extraGasForTransfer = new BigNumber(this.config.gasLimitESDTTransfer).plus( + new BigNumber(ADDITIONAL_GAS_FOR_ESDT_TRANSFER) + ); + } else { + transferArgs = this.dataArgsBuilder.buildArgsForSingleESDTNFTTransfer(transfer, receiver); + extraGasForTransfer = new BigNumber(this.config.gasLimitESDTNFTTransfer).plus( + new BigNumber(ADDITIONAL_GAS_FOR_ESDT_NFT_TRANSFER) + ); + receiver = options.sender; + } + + return new DraftTransactionBuilder({ + config: this.config, + sender: options.sender, + receiver: receiver, + dataParts: transferArgs, + gasLimit: extraGasForTransfer, + addDataMovementGas: true, + }).build(); + } +} diff --git a/src/utils.codec.spec.ts b/src/utils.codec.spec.ts index 1ab1660b..ba38f632 100644 --- a/src/utils.codec.spec.ts +++ b/src/utils.codec.spec.ts @@ -1,5 +1,5 @@ import { assert } from "chai"; -import { isPaddedHex, numberToPaddedHex, zeroPadStringIfOddLength } from "./utils.codec"; +import { isPaddedHex, numberToPaddedHex, zeroPadStringIfOddLength, byteArrayToHex, utf8ToHex } from "./utils.codec"; describe("test codec utils", () => { it("should convert numberToPaddedHex", () => { @@ -21,4 +21,18 @@ describe("test codec utils", () => { assert.equal(zeroPadStringIfOddLength("1"), "01"); assert.equal(zeroPadStringIfOddLength("01"), "01"); }); + + it("should convert byteArrayToHex", () => { + const firstArray = new Uint8Array([0x05, 0x00]); + const secondArray = new Uint8Array([0x7]); + + assert.equal(byteArrayToHex(firstArray), "0500"); + assert.equal(byteArrayToHex(secondArray), "07"); + }); + + it("should convert utf8ToHex", () => { + assert.equal(utf8ToHex("stringandnumber7"), "737472696e67616e646e756d62657237"); + assert.equal(utf8ToHex("somestring"), "736f6d65737472696e67"); + assert.equal(utf8ToHex("aaa"), "616161"); + }); }); diff --git a/src/utils.codec.ts b/src/utils.codec.ts index 8867c889..6d326412 100644 --- a/src/utils.codec.ts +++ b/src/utils.codec.ts @@ -1,4 +1,7 @@ import BigNumber from "bignumber.js"; +import * as contractsCodecUtils from "./smartcontracts/codec/utils"; +import { Address } from "./address"; +import { IAddress } from "./interface"; export function numberToPaddedHex(value: BigNumber.Value) { let hex = new BigNumber(value).toString(16); @@ -20,3 +23,30 @@ export function zeroPadStringIfOddLength(input: string): string { return input; } + +export function utf8ToHex(value: string) { + const hex = Buffer.from(value).toString("hex"); + return zeroPadStringIfOddLength(hex); +} + +export function boolToHex(value: boolean) { + return utf8ToHex(value.toString()); +} + +export function byteArrayToHex(byteArray: Uint8Array): string { + const hexString = Buffer.from(byteArray).toString("hex"); + return zeroPadStringIfOddLength(hexString); +} + +export function bigIntToHex(value: BigNumber.Value): string { + if (value == 0) { + return ""; + } + + return contractsCodecUtils.getHexMagnitudeOfBigInt(value); +} + +export function addressToHex(address: IAddress): string { + const buffer = Address.fromBech32(address.toString()).pubkey(); + return buffer.toString("hex"); +}