diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5881ae129..26d36b49b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,7 +5,6 @@ name: Build and Test on: pull_request: - branches: [ main, development, feat/*, release/* ] workflow_dispatch: jobs: diff --git a/.github/workflows/test-localnet.yml b/.github/workflows/test-localnet.yml index 61cdae4c0..d324b54d1 100644 --- a/.github/workflows/test-localnet.yml +++ b/.github/workflows/test-localnet.yml @@ -42,7 +42,7 @@ jobs: mkdir -p ~/localnet && cd ~/localnet mxpy localnet setup --configfile=${GITHUB_WORKSPACE}/localnet.toml nohup mxpy localnet start --configfile=${GITHUB_WORKSPACE}/localnet.toml > localnet.log 2>&1 & echo $! > localnet.pid - sleep 120 # Allow time for the testnet to fully start + sleep 10 # Allow time for the testnet to fully start # Step 6: Install Node.js and dependencies - name: Set up Node.js environment diff --git a/package-lock.json b/package-lock.json index 16bc74ae8..1d40a9d65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@multiversx/sdk-core", - "version": "13.17.2", + "version": "14.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@multiversx/sdk-core", - "version": "13.17.2", + "version": "14.0.0", "license": "MIT", "dependencies": { "@multiversx/sdk-transaction-decoder": "1.0.2", @@ -886,9 +886,9 @@ } }, "node_modules/axios": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", - "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", + "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==", "optional": true, "dependencies": { "follow-redirects": "^1.15.6", @@ -1779,10 +1779,11 @@ } }, "node_modules/elliptic": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.0.tgz", - "integrity": "sha512-dpwoQcLc/2WLQvJvLRHKZ+f9FgOdjnq11rurqwekGQygGPsYSK29OMMD2WalatiqQ+XGFDglTNixpPfI+lpaAA==", + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", "dev": true, + "license": "MIT", "dependencies": { "bn.js": "^4.11.9", "brorand": "^1.1.0", @@ -5828,9 +5829,9 @@ "dev": true }, "axios": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", - "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", + "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==", "optional": true, "requires": { "follow-redirects": "^1.15.6", @@ -6586,9 +6587,9 @@ } }, "elliptic": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.0.tgz", - "integrity": "sha512-dpwoQcLc/2WLQvJvLRHKZ+f9FgOdjnq11rurqwekGQygGPsYSK29OMMD2WalatiqQ+XGFDglTNixpPfI+lpaAA==", + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", "dev": true, "requires": { "bn.js": "^4.11.9", diff --git a/package.json b/package.json index 1dd8d7fbe..0aad3fb9c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@multiversx/sdk-core", - "version": "13.17.2", + "version": "14.0.0", "description": "MultiversX SDK for JavaScript and TypeScript", "author": "MultiversX", "homepage": "https://multiversx.com", diff --git a/src/smartcontracts/argSerializer.spec.ts b/src/abi/argSerializer.spec.ts similarity index 100% rename from src/smartcontracts/argSerializer.spec.ts rename to src/abi/argSerializer.spec.ts diff --git a/src/smartcontracts/argSerializer.ts b/src/abi/argSerializer.ts similarity index 99% rename from src/smartcontracts/argSerializer.ts rename to src/abi/argSerializer.ts index 4adf35a7f..7ede36894 100644 --- a/src/smartcontracts/argSerializer.ts +++ b/src/abi/argSerializer.ts @@ -1,4 +1,4 @@ -import { ARGUMENTS_SEPARATOR } from "../constants"; +import { ARGUMENTS_SEPARATOR } from "../core/constants"; import { BinaryCodec } from "./codec"; import { Type, TypedValue, U32Type, U32Value } from "./typesystem"; import { OptionalType, OptionalValue } from "./typesystem/algebraic"; diff --git a/src/smartcontracts/argumentErrorContext.ts b/src/abi/argumentErrorContext.ts similarity index 97% rename from src/smartcontracts/argumentErrorContext.ts rename to src/abi/argumentErrorContext.ts index 661f62b22..bef4d74a6 100644 --- a/src/smartcontracts/argumentErrorContext.ts +++ b/src/abi/argumentErrorContext.ts @@ -1,4 +1,4 @@ -import { ErrInvalidArgument } from "../errors"; +import { ErrInvalidArgument } from "../core/errors"; import { EndpointParameterDefinition, Type } from "./typesystem"; export class ArgumentErrorContext { diff --git a/src/smartcontracts/code.spec.ts b/src/abi/code.spec.ts similarity index 100% rename from src/smartcontracts/code.spec.ts rename to src/abi/code.spec.ts diff --git a/src/smartcontracts/code.ts b/src/abi/code.ts similarity index 95% rename from src/smartcontracts/code.ts rename to src/abi/code.ts index e7a8b68d2..dd3946052 100644 --- a/src/smartcontracts/code.ts +++ b/src/abi/code.ts @@ -2,6 +2,7 @@ const createHasher = require("blake2b"); const CODE_HASH_LENGTH = 32; /** + * * @deprecated Use the bytecode directly * Bytecode of a Smart Contract, as an abstraction. */ export class Code { diff --git a/src/smartcontracts/codec/address.ts b/src/abi/codec/address.ts similarity index 86% rename from src/smartcontracts/codec/address.ts rename to src/abi/codec/address.ts index 8e640d540..0772922f9 100644 --- a/src/smartcontracts/codec/address.ts +++ b/src/abi/codec/address.ts @@ -1,4 +1,4 @@ -import { Address } from "../../address"; +import { Address } from "../../core/address"; import { AddressValue } from "../typesystem"; export class AddressBinaryCodec { @@ -29,13 +29,13 @@ export class AddressBinaryCodec { * Encodes an AddressValue to a buffer. */ encodeNested(primitive: AddressValue): Buffer { - return primitive.valueOf().pubkey(); + return primitive.valueOf().getPublicKey(); } /** * Encodes an AddressValue to a buffer. */ encodeTopLevel(primitive: AddressValue): Buffer { - return primitive.valueOf().pubkey(); + return primitive.valueOf().getPublicKey(); } } diff --git a/src/smartcontracts/codec/arrayVec.ts b/src/abi/codec/arrayVec.ts similarity index 100% rename from src/smartcontracts/codec/arrayVec.ts rename to src/abi/codec/arrayVec.ts diff --git a/src/smartcontracts/codec/binary.spec.ts b/src/abi/codec/binary.spec.ts similarity index 98% rename from src/smartcontracts/codec/binary.spec.ts rename to src/abi/codec/binary.spec.ts index 8e6f92eca..983196a55 100644 --- a/src/smartcontracts/codec/binary.spec.ts +++ b/src/abi/codec/binary.spec.ts @@ -1,7 +1,7 @@ import BigNumber from "bignumber.js"; import { assert } from "chai"; -import { Address } from "../../address"; -import * as errors from "../../errors"; +import { Address } from "../../core/address"; +import * as errors from "../../core/errors"; import { AddressType, AddressValue, @@ -13,10 +13,16 @@ import { BigUIntValue, BooleanType, BooleanValue, + BytesType, + BytesValue, EnumType, EnumValue, EnumVariantDefinition, + ExplicitEnumType, + ExplicitEnumValue, + ExplicitEnumVariantDefinition, Field, + FieldDefinition, I16Type, I16Value, I32Type, @@ -45,9 +51,6 @@ import { U8Type, U8Value, } from "../typesystem"; -import { BytesType, BytesValue } from "../typesystem/bytes"; -import { ExplicitEnumType, ExplicitEnumValue, ExplicitEnumVariantDefinition } from "../typesystem/explicit-enum"; -import { FieldDefinition } from "../typesystem/fields"; import { BinaryCodec, BinaryCodecConstraints } from "./binary"; import { isMsbOne } from "./utils"; diff --git a/src/smartcontracts/codec/binary.ts b/src/abi/codec/binary.ts similarity index 98% rename from src/smartcontracts/codec/binary.ts rename to src/abi/codec/binary.ts index bd6be3ad3..17a41d8b4 100644 --- a/src/smartcontracts/codec/binary.ts +++ b/src/abi/codec/binary.ts @@ -1,5 +1,5 @@ -import * as errors from "../../errors"; -import { guardTrue } from "../../utils"; +import * as errors from "../../core/errors"; +import { guardTrue } from "../../core/utils"; import { ArrayVec, ArrayVecType, diff --git a/src/smartcontracts/codec/binaryCodecUtils.ts b/src/abi/codec/binaryCodecUtils.ts similarity index 100% rename from src/smartcontracts/codec/binaryCodecUtils.ts rename to src/abi/codec/binaryCodecUtils.ts diff --git a/src/smartcontracts/codec/boolean.ts b/src/abi/codec/boolean.ts similarity index 96% rename from src/smartcontracts/codec/boolean.ts rename to src/abi/codec/boolean.ts index d76184a70..5023bf304 100644 --- a/src/smartcontracts/codec/boolean.ts +++ b/src/abi/codec/boolean.ts @@ -1,4 +1,4 @@ -import * as errors from "../../errors"; +import * as errors from "../../core/errors"; import { BooleanValue } from "../typesystem"; /** diff --git a/src/smartcontracts/codec/bytes.ts b/src/abi/codec/bytes.ts similarity index 100% rename from src/smartcontracts/codec/bytes.ts rename to src/abi/codec/bytes.ts diff --git a/src/smartcontracts/codec/codemetadata.ts b/src/abi/codec/codemetadata.ts similarity index 58% rename from src/smartcontracts/codec/codemetadata.ts rename to src/abi/codec/codemetadata.ts index e5502ad8e..458a465f4 100644 --- a/src/smartcontracts/codec/codemetadata.ts +++ b/src/abi/codec/codemetadata.ts @@ -1,22 +1,22 @@ -import { CodeMetadata, CodeMetadataLength } from "../codeMetadata"; +import { CodeMetadata, CodeMetadataLength } from "../../core/codeMetadata"; import { CodeMetadataValue } from "../typesystem/codeMetadata"; export class CodeMetadataCodec { decodeNested(buffer: Buffer): [CodeMetadataValue, number] { - const codeMetadata = CodeMetadata.fromBuffer(buffer.slice(0, CodeMetadataLength)); + const codeMetadata = CodeMetadata.newFromBytes(buffer.slice(0, CodeMetadataLength)); return [new CodeMetadataValue(codeMetadata), CodeMetadataLength]; } decodeTopLevel(buffer: Buffer): CodeMetadataValue { - const codeMetadata = CodeMetadata.fromBuffer(buffer); + const codeMetadata = CodeMetadata.newFromBytes(buffer); return new CodeMetadataValue(codeMetadata); } encodeNested(codeMetadata: CodeMetadataValue): Buffer { - return codeMetadata.valueOf().toBuffer(); + return Buffer.from(codeMetadata.valueOf().toBytes()); } encodeTopLevel(codeMetadata: CodeMetadataValue): Buffer { - return codeMetadata.valueOf().toBuffer(); + return Buffer.from(codeMetadata.valueOf().toBytes()); } } diff --git a/src/smartcontracts/codec/constants.ts b/src/abi/codec/constants.ts similarity index 100% rename from src/smartcontracts/codec/constants.ts rename to src/abi/codec/constants.ts diff --git a/src/smartcontracts/codec/enum.ts b/src/abi/codec/enum.ts similarity index 100% rename from src/smartcontracts/codec/enum.ts rename to src/abi/codec/enum.ts diff --git a/src/smartcontracts/codec/explicit-enum.ts b/src/abi/codec/explicit-enum.ts similarity index 100% rename from src/smartcontracts/codec/explicit-enum.ts rename to src/abi/codec/explicit-enum.ts diff --git a/src/smartcontracts/codec/fields.ts b/src/abi/codec/fields.ts similarity index 100% rename from src/smartcontracts/codec/fields.ts rename to src/abi/codec/fields.ts diff --git a/src/smartcontracts/codec/h256.ts b/src/abi/codec/h256.ts similarity index 100% rename from src/smartcontracts/codec/h256.ts rename to src/abi/codec/h256.ts diff --git a/src/smartcontracts/codec/index.ts b/src/abi/codec/index.ts similarity index 100% rename from src/smartcontracts/codec/index.ts rename to src/abi/codec/index.ts diff --git a/src/smartcontracts/codec/list.ts b/src/abi/codec/list.ts similarity index 100% rename from src/smartcontracts/codec/list.ts rename to src/abi/codec/list.ts diff --git a/src/smartcontracts/codec/managedDecimal.ts b/src/abi/codec/managedDecimal.ts similarity index 100% rename from src/smartcontracts/codec/managedDecimal.ts rename to src/abi/codec/managedDecimal.ts diff --git a/src/smartcontracts/codec/managedDecimalSigned.ts b/src/abi/codec/managedDecimalSigned.ts similarity index 100% rename from src/smartcontracts/codec/managedDecimalSigned.ts rename to src/abi/codec/managedDecimalSigned.ts diff --git a/src/smartcontracts/codec/nothing.ts b/src/abi/codec/nothing.ts similarity index 100% rename from src/smartcontracts/codec/nothing.ts rename to src/abi/codec/nothing.ts diff --git a/src/smartcontracts/codec/numerical.ts b/src/abi/codec/numerical.ts similarity index 100% rename from src/smartcontracts/codec/numerical.ts rename to src/abi/codec/numerical.ts diff --git a/src/smartcontracts/codec/option.ts b/src/abi/codec/option.ts similarity index 97% rename from src/smartcontracts/codec/option.ts rename to src/abi/codec/option.ts index 37cdc58ef..af1d7c5f7 100644 --- a/src/smartcontracts/codec/option.ts +++ b/src/abi/codec/option.ts @@ -1,4 +1,4 @@ -import * as errors from "../../errors"; +import * as errors from "../../core/errors"; import { OptionValue, Type } from "../typesystem"; import { BinaryCodec } from "./binary"; diff --git a/src/smartcontracts/codec/primitive.ts b/src/abi/codec/primitive.ts similarity index 100% rename from src/smartcontracts/codec/primitive.ts rename to src/abi/codec/primitive.ts diff --git a/src/smartcontracts/codec/string.ts b/src/abi/codec/string.ts similarity index 100% rename from src/smartcontracts/codec/string.ts rename to src/abi/codec/string.ts diff --git a/src/smartcontracts/codec/struct.ts b/src/abi/codec/struct.ts similarity index 100% rename from src/smartcontracts/codec/struct.ts rename to src/abi/codec/struct.ts diff --git a/src/smartcontracts/codec/tokenIdentifier.ts b/src/abi/codec/tokenIdentifier.ts similarity index 100% rename from src/smartcontracts/codec/tokenIdentifier.ts rename to src/abi/codec/tokenIdentifier.ts diff --git a/src/smartcontracts/codec/tuple.ts b/src/abi/codec/tuple.ts similarity index 100% rename from src/smartcontracts/codec/tuple.ts rename to src/abi/codec/tuple.ts diff --git a/src/smartcontracts/codec/utils.ts b/src/abi/codec/utils.ts similarity index 96% rename from src/smartcontracts/codec/utils.ts rename to src/abi/codec/utils.ts index a1ce1225d..928d32d30 100644 --- a/src/smartcontracts/codec/utils.ts +++ b/src/abi/codec/utils.ts @@ -1,5 +1,5 @@ import BigNumber from "bignumber.js"; -import { numberToPaddedHex } from "../../utils.codec"; +import { numberToPaddedHex } from "../../core/utils.codec"; /** * Returns whether the most significant bit of a given byte (within a buffer) is 1. diff --git a/src/smartcontracts/function.ts b/src/abi/function.ts similarity index 95% rename from src/smartcontracts/function.ts rename to src/abi/function.ts index b8547e46a..6648ef757 100644 --- a/src/smartcontracts/function.ts +++ b/src/abi/function.ts @@ -1,4 +1,4 @@ -import * as errors from "../errors"; +import * as errors from "../core/errors"; /** * A function of a Smart Contract, as an abstraction. diff --git a/src/smartcontracts/index.ts b/src/abi/index.ts similarity index 68% rename from src/smartcontracts/index.ts rename to src/abi/index.ts index 58f299694..e72a5528f 100644 --- a/src/smartcontracts/index.ts +++ b/src/abi/index.ts @@ -1,15 +1,12 @@ export * from "./argSerializer"; +export * from "./argumentErrorContext"; export * from "./code"; export * from "./codec"; -export * from "./codeMetadata"; export * from "./function"; export * from "./interaction"; -export * from "./interactionChecker"; export * from "./interface"; export * from "./nativeSerializer"; export * from "./query"; -export * from "./resultsParser"; export * from "./returnCode"; export * from "./smartContract"; -export * from "./transactionPayloadBuilders"; export * from "./typesystem"; diff --git a/src/abi/interaction.local.net.spec.ts b/src/abi/interaction.local.net.spec.ts new file mode 100644 index 000000000..f5c8f8066 --- /dev/null +++ b/src/abi/interaction.local.net.spec.ts @@ -0,0 +1,448 @@ +import BigNumber from "bignumber.js"; +import { assert } from "chai"; +import { promises } from "fs"; +import { Account } from "../accounts"; +import { Transaction } from "../core/transaction"; +import { TransactionsFactoryConfig } from "../core/transactionsFactoryConfig"; +import { TransactionWatcher } from "../core/transactionWatcher"; +import { + SmartContractController, + SmartContractTransactionsFactory, + SmartContractTransactionsOutcomeParser, +} from "../smartContracts"; +import { loadAbiRegistry, prepareDeployment } from "../testutils"; +import { createLocalnetProvider } from "../testutils/networkProviders"; +import { getTestWalletsPath } from "../testutils/utils"; +import { Interaction } from "./interaction"; +import { SmartContract } from "./smartContract"; +import { ManagedDecimalValue } from "./typesystem"; +describe("test smart contract interactor", function () { + let provider = createLocalnetProvider(); + let alice: Account; + + before(async function () { + alice = await Account.newFromPem(`${getTestWalletsPath()}/alice.pem`); + }); + + it("should interact with 'answer' (local testnet) using the SmartContractTransactionsFactory", async function () { + this.timeout(80000); + + let abi = await loadAbiRegistry("src/testdata/answer.abi.json"); + + let network = await provider.getNetworkConfig(); + + const config = new TransactionsFactoryConfig({ chainID: network.chainID }); + const factory = new SmartContractTransactionsFactory({ + config: config, + abi: abi, + }); + + const bytecode = await promises.readFile("src/testdata/answer.wasm"); + alice.nonce = (await provider.getAccount(alice.address)).nonce; + + const deployTransaction = factory.createTransactionForDeploy(alice.address, { + bytecode: bytecode, + gasLimit: 3000000n, + }); + deployTransaction.nonce = alice.nonce; + + deployTransaction.signature = await alice.signTransaction(deployTransaction); + + const contractAddress = SmartContract.computeAddress(alice.address, alice.nonce); + + const transactionCompletionAwaiter = new TransactionWatcher({ + getTransaction: async (hash: string) => { + return await provider.getTransaction(hash); + }, + }); + + const deployTxHash = await provider.sendTransaction(deployTransaction); + alice.incrementNonce(); + + const queryController = new SmartContractController({ + chainID: "localnet", + networkProvider: provider, + abi: abi, + }); + let transactionOnNetwork = await transactionCompletionAwaiter.awaitCompleted(deployTxHash); + let response = queryController.parseDeploy(transactionOnNetwork); + assert.isTrue(response.returnCode == "ok"); + + const query = queryController.createQuery({ + contract: contractAddress, + caller: alice.address, + function: "getUltimateAnswer", + arguments: [], + }); + + const queryResponse = await queryController.runQuery(query); + const parsed = queryController.parseQueryResponse(queryResponse); + assert.lengthOf(parsed, 1); + assert.deepEqual(parsed[0], new BigNumber(42)); + + // Query + let transaction = factory.createTransactionForExecute(alice.address, { + contract: contractAddress, + function: "getUltimateAnswer", + gasLimit: 3000000n, + }); + transaction.nonce = alice.getNonceThenIncrement(); + transaction.signature = await alice.signTransaction(transaction); + + await provider.sendTransaction(transaction); + + // Execute, and wait for execution + transaction = factory.createTransactionForExecute(alice.address, { + contract: contractAddress, + function: "getUltimateAnswer", + gasLimit: 3000000n, + }); + transaction.nonce = alice.getNonceThenIncrement(); + transaction.signature = await alice.signTransaction(transaction); + + const executeTxHash = await provider.sendTransaction(transaction); + transactionOnNetwork = await transactionCompletionAwaiter.awaitCompleted(executeTxHash); + const executeResponse = queryController.parseExecute(transactionOnNetwork); + + assert.isTrue(executeResponse.values.length == 1); + assert.deepEqual(executeResponse.values[0], new BigNumber(42)); + assert.isTrue(executeResponse.returnCode == "ok"); + }); + + it("should interact with 'basic-features' (local testnet)", async function () { + this.timeout(140000); + + let abi = await loadAbiRegistry("src/testdata/basic-features.abi.json"); + let contract = new SmartContract({ abi: abi }); + let controller = new SmartContractController({ + chainID: "localnet", + networkProvider: provider, + abi: abi, + }); + + let network = await provider.getNetworkConfig(); + alice.nonce = (await provider.getAccount(alice.address)).nonce; + // Deploy the contract + let deployTransaction = await prepareDeployment({ + contract: contract, + deployer: alice, + codePath: "src/testdata/basic-features.wasm", + gasLimit: 600000000n, + initArguments: [], + chainID: network.chainID, + }); + let deployTxHash = await provider.sendTransaction(deployTransaction); + let deployResponse = await controller.awaitCompletedDeploy(deployTxHash); + assert.isTrue(deployResponse.returnCode == "ok"); + + let returnEgldInteraction = ( + contract.methods + .returns_egld_decimal([]) + .withGasLimit(10000000n) + .withChainID(network.chainID) + .withSender(alice.address) + .withValue(1n) + ); + + // returnEgld() + let returnEgldTransaction = returnEgldInteraction + .withSender(alice.address) + .useThenIncrementNonceOf(alice) + .buildTransaction(); + + let additionInteraction = contract.methods + .managed_decimal_addition([new ManagedDecimalValue("2.5", 2), new ManagedDecimalValue("2.7", 2)]) + .withGasLimit(10000000n) + .withChainID(network.chainID) + .withSender(alice.address) + .withValue(0n); + + // addition() + let additionTransaction = additionInteraction + .withSender(alice.address) + .useThenIncrementNonceOf(alice) + .buildTransaction(); + + // log + let mdLnInteraction = contract.methods + .managed_decimal_ln([new ManagedDecimalValue("23", 9)]) + .withGasLimit(10000000n) + .withChainID(network.chainID) + .withSender(alice.address) + .withValue(0n); + + // mdLn() + let mdLnTransaction = mdLnInteraction + .withSender(alice.address) + .useThenIncrementNonceOf(alice) + .buildTransaction(); + + let additionVarInteraction = contract.methods + .managed_decimal_addition_var([ + new ManagedDecimalValue("4", 2, true), + new ManagedDecimalValue("5", 2, true), + ]) + .withGasLimit(50000000n) + .withChainID(network.chainID) + .withSender(alice.address) + .withValue(0n); + + // addition() + let additionVarTransaction = additionVarInteraction + .withSender(alice.address) + .useThenIncrementNonceOf(alice) + .buildTransaction(); + + let lnVarInteraction = contract.methods + .managed_decimal_ln_var([new ManagedDecimalValue("23", 9, true)]) + .withGasLimit(50000000n) + .withChainID(network.chainID) + .withSender(alice.address) + .withValue(0n); + + // managed_decimal_ln_var() + let lnVarTransaction = lnVarInteraction + .withSender(alice.address) + .useThenIncrementNonceOf(alice) + .buildTransaction(); + + // returnEgld() + await signTransaction({ transaction: returnEgldTransaction, wallet: alice }); + let txHash = await provider.sendTransaction(returnEgldTransaction); + let response = await controller.awaitCompletedExecute(txHash); + assert.isTrue(response.returnCode == "ok"); + assert.lengthOf(response.values, 1); + assert.deepEqual(response.values[0], new BigNumber("0.000000000000000001")); + + // addition with const decimals() + await signTransaction({ transaction: additionTransaction, wallet: alice }); + txHash = await provider.sendTransaction(additionTransaction); + response = await controller.awaitCompletedExecute(txHash); + assert.isTrue(response.returnCode == "ok"); + assert.lengthOf(response.values, 1); + assert.deepEqual(response.values[0], new BigNumber("5.2")); + + // log + await signTransaction({ transaction: mdLnTransaction, wallet: alice }); + txHash = await provider.sendTransaction(mdLnTransaction); + response = await controller.awaitCompletedExecute(txHash); + + assert.isTrue(response.returnCode == "ok"); + assert.lengthOf(response.values, 1); + assert.deepEqual(response.values[0], new BigNumber("3.135553845")); + + // addition with var decimals + await signTransaction({ transaction: additionVarTransaction, wallet: alice }); + txHash = await provider.sendTransaction(additionVarTransaction); + response = await controller.awaitCompletedExecute(txHash); + assert.isTrue(response.returnCode == "ok"); + assert.lengthOf(response.values, 1); + assert.deepEqual(response.values[0], new BigNumber("9")); + + // log + await signTransaction({ transaction: lnVarTransaction, wallet: alice }); + txHash = await provider.sendTransaction(lnVarTransaction); + response = await controller.awaitCompletedExecute(txHash); + assert.isTrue(response.returnCode == "ok"); + assert.lengthOf(response.values, 1); + assert.deepEqual(response.values[0], new BigNumber("3.135553845")); + }); + + it("should interact with 'counter' (local testnet) using the SmartContractTransactionsFactory", async function () { + this.timeout(120000); + + let abi = await loadAbiRegistry("src/testdata/counter.abi.json"); + + let network = await provider.getNetworkConfig(); + + const config = new TransactionsFactoryConfig({ chainID: network.chainID }); + const factory = new SmartContractTransactionsFactory({ + config: config, + abi: abi, + }); + const parser = new SmartContractTransactionsOutcomeParser({ abi: abi }); + + const bytecode = await promises.readFile("src/testdata/counter.wasm"); + alice.nonce = (await provider.getAccount(alice.address)).nonce; + + const deployTransaction = factory.createTransactionForDeploy(alice.address, { + bytecode: bytecode, + gasLimit: 3000000n, + }); + deployTransaction.nonce = alice.nonce; + + deployTransaction.signature = await alice.signTransaction(deployTransaction); + + const contractAddress = SmartContract.computeAddress(alice.address, alice.nonce); + + const transactionCompletionAwaiter = new TransactionWatcher({ + getTransaction: async (hash: string) => { + return await provider.getTransaction(hash); + }, + }); + + const deployTxHash = await provider.sendTransaction(deployTransaction); + alice.incrementNonce(); + let transactionOnNetwork = await transactionCompletionAwaiter.awaitCompleted(deployTxHash); + const deployResponse = parser.parseDeploy({ transactionOnNetwork }); + assert.isTrue(deployResponse.returnCode == "ok"); + + const queryController = new SmartContractController({ + chainID: "localnet", + networkProvider: provider, + abi: abi, + }); + + let incrementTransaction = factory.createTransactionForExecute(alice.address, { + contract: contractAddress, + function: "increment", + gasLimit: 3000000n, + }); + incrementTransaction.nonce = alice.getNonceThenIncrement(); + + incrementTransaction.signature = await alice.signTransaction(incrementTransaction); + + // Query "get()" + const query = queryController.createQuery({ + contract: contractAddress, + function: "get", + arguments: [], + }); + const queryResponse = await queryController.runQuery(query); + const parsed = queryController.parseQueryResponse(queryResponse); + assert.deepEqual(parsed[0], new BigNumber(1)); + + const incrementTxHash = await provider.sendTransaction(incrementTransaction); + transactionOnNetwork = await transactionCompletionAwaiter.awaitCompleted(incrementTxHash); + + let response = parser.parseExecute({ transactionOnNetwork }); + assert.deepEqual(response.values[0], new BigNumber(2)); + + let decrementTransaction = factory.createTransactionForExecute(alice.address, { + contract: contractAddress, + function: "decrement", + gasLimit: 3000000n, + }); + decrementTransaction.nonce = alice.getNonceThenIncrement(); + decrementTransaction.signature = await alice.signTransaction(decrementTransaction); + + await provider.sendTransaction(decrementTransaction); + + decrementTransaction.nonce = alice.nonce; + decrementTransaction.signature = await alice.signTransaction(decrementTransaction); + + const decrementTxHash = await provider.sendTransaction(decrementTransaction); + transactionOnNetwork = await transactionCompletionAwaiter.awaitCompleted(decrementTxHash); + response = parser.parseExecute({ transactionOnNetwork }); + }); + + it("should interact with 'lottery-esdt' (local testnet) using the SmartContractTransactionsFactory", async function () { + this.timeout(140000); + + let abi = await loadAbiRegistry("src/testdata/lottery-esdt.abi.json"); + let parser = new SmartContractTransactionsOutcomeParser({ abi: abi }); + alice.nonce = (await provider.getAccount(alice.address)).nonce; + + let network = await provider.getNetworkConfig(); + + const config = new TransactionsFactoryConfig({ chainID: network.chainID }); + const factory = new SmartContractTransactionsFactory({ + config: config, + abi: abi, + }); + + const bytecode = await promises.readFile("src/testdata/lottery-esdt.wasm"); + + // Deploy the contract + const deployTransaction = factory.createTransactionForDeploy(alice.address, { + bytecode: bytecode, + gasLimit: 100000000n, + }); + deployTransaction.nonce = alice.nonce; + + deployTransaction.signature = await alice.signTransaction(deployTransaction); + + const contractAddress = SmartContract.computeAddress(alice.address, alice.nonce); + + const transactionCompletionAwaiter = new TransactionWatcher({ + getTransaction: async (hash: string) => { + return await provider.getTransaction(hash); + }, + }); + + const deployTxHash = await provider.sendTransaction(deployTransaction); + alice.incrementNonce(); + let transactionOnNetwork = await transactionCompletionAwaiter.awaitCompleted(deployTxHash); + const deployResponse = parser.parseDeploy({ transactionOnNetwork }); + assert.isTrue(deployResponse.returnCode == "ok"); + + // start() + let startTransaction = factory.createTransactionForExecute(alice.address, { + contract: contractAddress, + function: "start", + arguments: ["lucky", "EGLD", 1, null, null, 1, null, null], + gasLimit: 30000000n, + }); + startTransaction.nonce = alice.getNonceThenIncrement(); + startTransaction.signature = await alice.signTransaction(startTransaction); + + const startTxHash = await provider.sendTransaction(startTransaction); + transactionOnNetwork = await transactionCompletionAwaiter.awaitCompleted(startTxHash); + let response = parser.parseExecute({ transactionOnNetwork }); + assert.isTrue(response.returnCode == "ok"); + assert.lengthOf(response.values, 0); + + // status() + let lotteryStatusTransaction = factory.createTransactionForExecute(alice.address, { + contract: contractAddress, + function: "status", + arguments: ["lucky"], + gasLimit: 5000000n, + }); + lotteryStatusTransaction.nonce = alice.getNonceThenIncrement(); + lotteryStatusTransaction.signature = await alice.signTransaction(lotteryStatusTransaction); + + const statusTxHash = await provider.sendTransaction(lotteryStatusTransaction); + transactionOnNetwork = await transactionCompletionAwaiter.awaitCompleted(statusTxHash); + response = parser.parseExecute({ transactionOnNetwork }); + assert.isTrue(response.returnCode == "ok"); + assert.lengthOf(response.values, 1); + assert.equal(response.values[0].name, "Running"); + + // getlotteryInfo() (this is a view function, but for the sake of the test, we'll execute it) + let lotteryInfoTransaction = factory.createTransactionForExecute(alice.address, { + contract: contractAddress, + function: "getLotteryInfo", + arguments: ["lucky"], + gasLimit: 5000000n, + }); + lotteryInfoTransaction.nonce = alice.getNonceThenIncrement(); + lotteryInfoTransaction.signature = await alice.signTransaction(lotteryInfoTransaction); + + const infoTxHash = await provider.sendTransaction(lotteryInfoTransaction); + transactionOnNetwork = await transactionCompletionAwaiter.awaitCompleted(infoTxHash); + response = parser.parseExecute({ transactionOnNetwork }); + assert.isTrue(response.returnCode == "ok"); + assert.lengthOf(response.values, 1); + + // Ignore "deadline" field in our test + let info = response.values[0]!.valueOf(); + delete info.deadline; + + assert.deepEqual(info, { + token_identifier: "EGLD", + ticket_price: new BigNumber("1"), + tickets_left: new BigNumber(800), + max_entries_per_user: new BigNumber(1), + prize_distribution: Buffer.from([0x64]), + prize_pool: new BigNumber("0"), + }); + }); + + async function signTransaction(options: { transaction: Transaction; wallet: Account }) { + const transaction = options.transaction; + const wallet = options.wallet; + + transaction.signature = await wallet.signTransaction(transaction); + } +}); diff --git a/src/abi/interaction.spec.ts b/src/abi/interaction.spec.ts new file mode 100644 index 000000000..b7ed0a261 --- /dev/null +++ b/src/abi/interaction.spec.ts @@ -0,0 +1,428 @@ +import BigNumber from "bignumber.js"; +import { assert } from "chai"; +import { Account } from "../accounts"; +import { Address } from "../core/address"; +import { SmartContractQueryResponse } from "../core/smartContractQuery"; +import { Token, TokenTransfer } from "../core/tokens"; +import { Transaction } from "../core/transaction"; +import { SmartContractController } from "../smartContracts"; +import { loadAbiRegistry, MockNetworkProvider } from "../testutils"; +import { getTestWalletsPath } from "../testutils/utils"; +import { ContractFunction } from "./function"; +import { Interaction } from "./interaction"; +import { SmartContract } from "./smartContract"; +import { BigUIntValue, BytesValue, OptionalValue, OptionValue, TokenIdentifierValue, U32Value } from "./typesystem"; + +describe("test smart contract interactor", function () { + let dummyAddress = new Address("erd1qqqqqqqqqqqqqpgqak8zt22wl2ph4tswtyc39namqx6ysa2sd8ss4xmlj3"); + let provider = new MockNetworkProvider(); + let alice: Account; + + before(async function () { + alice = await Account.newFromPem(`${getTestWalletsPath()}/alice.pem`); + }); + + it("should set transaction fields", async function () { + let contract = new SmartContract({ address: dummyAddress }); + let dummyFunction = new ContractFunction("dummy"); + let interaction = new Interaction(contract, dummyFunction, []); + + let transaction = interaction + .withSender(alice.address) + .withNonce(7n) + .withValue(TokenTransfer.newFromNativeAmount(1000000000000000000n).amount) + .withGasLimit(20000000n) + .buildTransaction(); + + assert.deepEqual(transaction.receiver, dummyAddress); + assert.equal(transaction.value.toString(), "1000000000000000000"); + assert.equal(transaction.nonce, 7n); + assert.equal(transaction.gasLimit, 20000000n); + }); + + it("should set transfers (payments) on contract calls (transfer and execute)", async function () { + let contract = new SmartContract({ address: dummyAddress }); + let dummyFunction = new ContractFunction("dummy"); + let alice = new Address("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + + const TokenFoo = (amount: BigNumber.Value) => + new TokenTransfer({ token: new Token({ identifier: "FOO-6ce17b" }), amount: BigInt(amount.toString()) }); + const TokenBar = (amount: BigNumber.Value) => + new TokenTransfer({ token: new Token({ identifier: "BAR-5bc08f" }), amount: BigInt(amount.toString()) }); + const LKMEX = (nonce: number, amount: BigNumber.Value) => + new TokenTransfer({ + token: new Token({ identifier: "LKMEX-aab910", nonce: BigInt(nonce) }), + amount: BigInt(amount.toString()), + }); + const nonFungibleToken = (nonce: number) => + new TokenTransfer({ token: new Token({ identifier: "MOS-b9b4b2", nonce: BigInt(nonce) }), amount: 1n }); + + const hexFoo = "464f4f2d366365313762"; + const hexBar = "4241522d356263303866"; + const hexLKMEX = "4c4b4d45582d616162393130"; + const hexNFT = "4d4f532d623962346232"; + const hexContractAddress = contract.getAddress().toHex(); + const hexDummyFunction = "64756d6d79"; + + // ESDT, single + let transaction = new Interaction(contract, dummyFunction, []) + .withSender(alice) + .withSingleESDTTransfer(TokenFoo(10)) + .buildTransaction(); + + assert.equal(transaction.data.toString(), `ESDTTransfer@${hexFoo}@0a@${hexDummyFunction}`); + + // Meta ESDT (special SFT), single + transaction = new Interaction(contract, dummyFunction, []) + .withSender(alice) + .withSingleESDTNFTTransfer(LKMEX(123456, "123456000000000000000")) + .buildTransaction(); + + assert.equal(transaction.sender.toBech32(), alice.toBech32()); + assert.equal(transaction.receiver.toBech32(), alice.toBech32()); + assert.equal( + transaction.data.toString(), + `ESDTNFTTransfer@${hexLKMEX}@01e240@06b14bd1e6eea00000@${hexContractAddress}@${hexDummyFunction}`, + ); + + // Meta ESDT (special SFT), single, but using "withSender()" (recommended) + transaction = new Interaction(contract, dummyFunction, []) + .withSingleESDTNFTTransfer(LKMEX(123456, 123456000000000000000)) + .withSender(alice) + .buildTransaction(); + + assert.equal(transaction.sender.toBech32(), alice.toBech32()); + assert.equal(transaction.receiver.toBech32(), alice.toBech32()); + assert.equal( + transaction.data.toString(), + `ESDTNFTTransfer@${hexLKMEX}@01e240@06b14bd1e6eea00000@${hexContractAddress}@${hexDummyFunction}`, + ); + + // NFT, single + transaction = new Interaction(contract, dummyFunction, []) + .withSender(alice) + .withSingleESDTNFTTransfer(nonFungibleToken(1)) + .buildTransaction(); + + assert.equal(transaction.sender.toBech32(), alice.toBech32()); + assert.equal(transaction.receiver.toBech32(), alice.toBech32()); + assert.equal( + transaction.data.toString(), + `ESDTNFTTransfer@${hexNFT}@01@01@${hexContractAddress}@${hexDummyFunction}`, + ); + + // NFT, single, but using "withSender()" (recommended) + transaction = new Interaction(contract, dummyFunction, []) + .withSingleESDTNFTTransfer(nonFungibleToken(1)) + .withSender(alice) + .buildTransaction(); + + assert.equal(transaction.sender.toBech32(), alice.toBech32()); + assert.equal(transaction.receiver.toBech32(), alice.toBech32()); + assert.equal( + transaction.data.toString(), + `ESDTNFTTransfer@${hexNFT}@01@01@${hexContractAddress}@${hexDummyFunction}`, + ); + + // ESDT, multiple + transaction = new Interaction(contract, dummyFunction, []) + .withSender(alice) + .withMultiESDTNFTTransfer([TokenFoo(3), TokenBar(3140)]) + .buildTransaction(); + + assert.equal(transaction.sender.toBech32(), alice.toBech32()); + assert.equal(transaction.receiver.toBech32(), alice.toBech32()); + assert.equal( + transaction.data.toString(), + `MultiESDTNFTTransfer@${hexContractAddress}@02@${hexFoo}@@03@${hexBar}@@0c44@${hexDummyFunction}`, + ); + + // ESDT, multiple, but using "withSender()" (recommended) + transaction = new Interaction(contract, dummyFunction, []) + .withMultiESDTNFTTransfer([TokenFoo(3), TokenBar(3140)]) + .withSender(alice) + .buildTransaction(); + + assert.equal(transaction.sender.toBech32(), alice.toBech32()); + assert.equal(transaction.receiver.toBech32(), alice.toBech32()); + assert.equal( + transaction.data.toString(), + `MultiESDTNFTTransfer@${hexContractAddress}@02@${hexFoo}@@03@${hexBar}@@0c44@${hexDummyFunction}`, + ); + + // NFT, multiple + transaction = new Interaction(contract, dummyFunction, []) + .withSender(alice) + .withMultiESDTNFTTransfer([nonFungibleToken(1), nonFungibleToken(42)]) + .buildTransaction(); + + assert.equal(transaction.sender.toBech32(), alice.toBech32()); + assert.equal(transaction.receiver.toBech32(), alice.toBech32()); + assert.equal( + transaction.data.toString(), + `MultiESDTNFTTransfer@${hexContractAddress}@02@${hexNFT}@01@01@${hexNFT}@2a@01@${hexDummyFunction}`, + ); + + // NFT, multiple, but using "withSender()" (recommended) + transaction = new Interaction(contract, dummyFunction, []) + .withMultiESDTNFTTransfer([nonFungibleToken(1), nonFungibleToken(42)]) + .withSender(alice) + .buildTransaction(); + + assert.equal(transaction.sender.toBech32(), alice.toBech32()); + assert.equal(transaction.receiver.toBech32(), alice.toBech32()); + }); + + it("should create transaction, with ABI, with transfer & execute", async function () { + const abi = await loadAbiRegistry("src/testdata/answer.abi.json"); + const contract = new SmartContract({ address: dummyAddress, abi: abi }); + const alice = new Address("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + const token = new Token({ identifier: "FOO-abcdef", nonce: 0n }); + + const transaction = contract.methods + .getUltimateAnswer() + .withChainID("T") + .withSender(alice) + .withGasLimit(543210n) + .withSingleESDTTransfer(new TokenTransfer({ token, amount: 100n })) + .withNonce(42n) + .buildTransaction(); + + assert.deepEqual( + transaction, + new Transaction({ + chainID: "T", + sender: alice, + receiver: dummyAddress, + data: Buffer.from("ESDTTransfer@464f4f2d616263646566@64@676574556c74696d617465416e73776572"), + gasLimit: 543210n, + value: 0n, + version: 2, + nonce: 42n, + }), + ); + }); + + it("should interact with 'answer'", async function () { + this.timeout(30000); + let abi = await loadAbiRegistry("src/testdata/answer.abi.json"); + let contract = new SmartContract({ address: dummyAddress, abi: abi }); + let controller = new SmartContractController({ chainID: "D", networkProvider: provider, abi: abi }); + + let interaction = contract.methods.getUltimateAnswer().withGasLimit(543210n).withChainID("T"); + + assert.equal(contract.getAddress(), dummyAddress); + assert.deepEqual(interaction.getFunction(), new ContractFunction("getUltimateAnswer")); + assert.lengthOf(interaction.getArguments(), 0); + assert.deepEqual(interaction.getGasLimit(), 543210n); + + provider.mockQueryContractOnFunction( + "getUltimateAnswer", + new SmartContractQueryResponse({ + returnDataParts: [Buffer.from([42])], + returnCode: "ok", + returnMessage: "msg", + function: "getUltimateAnswer", + }), + ); + + // Query; + + const interactionQuery = interaction.buildQuery(); + let response = await controller.query({ + contract: interactionQuery.address, + arguments: interactionQuery.getEncodedArguments(), + function: interactionQuery.func.toString(), + caller: interactionQuery.caller, + value: BigInt(interactionQuery.value.toString()), + }); + assert.isTrue(response.length == 1); + assert.deepEqual(response[0], new BigNumber(42)); + + // Execute, do not wait for execution + let transaction = interaction.withSender(alice.address).withNonce(0n).buildTransaction(); + transaction.sender = alice.address; + transaction.signature = await alice.signTransaction(transaction); + let hash = await provider.sendTransaction(transaction); + assert.equal(transaction.nonce, 0n); + assert.equal(transaction.data.toString(), "getUltimateAnswer"); + assert.equal(hash, "3579ad09099feb9755c860ddd225251170806d833342e912fccdfe2ed5c3a364"); + + transaction = interaction.withNonce(1n).buildTransaction(); + transaction.sender = alice.address; + transaction.signature = await alice.signTransaction(transaction); + hash = await provider.sendTransaction(transaction); + assert.equal(transaction.nonce, 1n); + assert.equal(hash, "ad513ce7c5d371d30e48f073326899766736eac1ac231d847d45bc3facbcb496"); + + // Execute, and wait for execution + transaction = interaction.withNonce(2n).buildTransaction(); + transaction.sender = alice.address; + transaction.signature = await alice.signTransaction(transaction); + provider.mockGetTransactionWithAnyHashAsNotarizedWithOneResult("@6f6b@2bs", "getUltimateAnswer"); + hash = await provider.sendTransaction(transaction); + let responseExecute = await controller.awaitCompletedExecute(hash); + + assert.isTrue(responseExecute.values.length == 1); + assert.deepEqual(responseExecute.values[0], new BigNumber(43)); + assert.isTrue(responseExecute.returnCode == "ok"); + }); + + it("should interact with 'counter'", async function () { + this.timeout(30000); + let abi = await loadAbiRegistry("src/testdata/counter.abi.json"); + let contract = new SmartContract({ address: dummyAddress, abi: abi }); + let controller = new SmartContractController({ chainID: "D", networkProvider: provider, abi: abi }); + + let getInteraction = contract.methodsExplicit.get(); + let incrementInteraction = (contract.methods.increment()).withGasLimit(543210n); + let decrementInteraction = (contract.methods.decrement()).withGasLimit(987654n); + + // For "get()", return fake 7 + provider.mockQueryContractOnFunction( + "get", + new SmartContractQueryResponse({ + returnDataParts: [Buffer.from([7])], + returnCode: "ok", + function: "get", + returnMessage: "", + }), + ); + + // Query "get()" + const interactionQuery = getInteraction.buildQuery(); + let response = await controller.query({ + contract: interactionQuery.address, + arguments: interactionQuery.getEncodedArguments(), + function: interactionQuery.func.toString(), + caller: interactionQuery.caller, + value: BigInt(interactionQuery.value.toString()), + }); + assert.deepEqual(response[0], new BigNumber(7)); + + let incrementTransaction = incrementInteraction + .withSender(alice.address) + .withNonce(14n) + .withChainID("mock") + .buildTransaction(); + + incrementTransaction.signature = await alice.signTransaction(incrementTransaction); + provider.mockGetTransactionWithAnyHashAsNotarizedWithOneResult("@6f6b@08", "increment"); + let hash = await provider.sendTransaction(incrementTransaction); + let responseExecute = await controller.awaitCompletedExecute(hash); + assert.deepEqual(responseExecute.values[0], new BigNumber(8)); + + // Decrement three times (simulate three parallel broadcasts). Wait for execution of the latter (third transaction). Return fake "5". + // Decrement #1 + let decrementTransaction = decrementInteraction + .withSender(alice.address) + .withNonce(15n) + .withChainID("mock") + .buildTransaction(); + + decrementTransaction.signature = await alice.signTransaction(decrementTransaction); + await provider.sendTransaction(decrementTransaction); + // Decrement #2 + decrementTransaction = decrementInteraction.withNonce(16n).buildTransaction(); + decrementTransaction.signature = await alice.signTransaction(decrementTransaction); + await provider.sendTransaction(decrementTransaction); + // Decrement #3 + + decrementTransaction = decrementInteraction.withNonce(17n).buildTransaction(); + decrementTransaction.signature = await alice.signTransaction(decrementTransaction); + provider.mockGetTransactionWithAnyHashAsNotarizedWithOneResult("@6f6b@05", "decrement"); + hash = await provider.sendTransaction(decrementTransaction); + responseExecute = await controller.awaitCompletedExecute(hash); + assert.deepEqual(responseExecute.values[0], new BigNumber(5)); + }); + + it("should interact with 'lottery-esdt'", async function () { + this.timeout(30000); + let abi = await loadAbiRegistry("src/testdata/lottery-esdt.abi.json"); + let contract = new SmartContract({ address: dummyAddress, abi: abi }); + let controller = new SmartContractController({ chainID: "D", networkProvider: provider, abi: abi }); + + let startInteraction = ( + contract.methodsExplicit + .start([ + BytesValue.fromUTF8("lucky"), + new TokenIdentifierValue("lucky-token"), + new BigUIntValue(1), + OptionValue.newMissing(), + OptionValue.newMissing(), + OptionValue.newProvided(new U32Value(1)), + OptionValue.newMissing(), + OptionValue.newMissing(), + OptionalValue.newMissing(), + ]) + .withGasLimit(5000000n) + ); + + let statusInteraction = contract.methods.status(["lucky"]).withGasLimit(5000000n); + + let getLotteryInfoInteraction = contract.methods.getLotteryInfo(["lucky"]).withGasLimit(5000000n); + + // start() + let startTransaction = startInteraction + .withSender(alice.address) + .withNonce(14n) + .withChainID("mock") + .buildTransaction(); + + startTransaction.signature = await alice.signTransaction(startTransaction); + + provider.mockGetTransactionWithAnyHashAsNotarizedWithOneResult("@6f6b", "start"); + let hash = await provider.sendTransaction(startTransaction); + let response = await controller.awaitCompletedExecute(hash); + + assert.equal(startTransaction.data.toString(), "start@6c75636b79@6c75636b792d746f6b656e@01@@@0100000001@@"); + assert.isTrue(response.returnCode == "ok"); + assert.isTrue(response.values.length == 0); + + // status() (this is a view function, but for the sake of the test, we'll execute it) + let statusTransaction = statusInteraction + .withSender(alice.address) + .withNonce(15n) + .withChainID("mock") + .buildTransaction(); + + statusTransaction.signature = await alice.signTransaction(statusTransaction); + provider.mockGetTransactionWithAnyHashAsNotarizedWithOneResult("@6f6b@01", "status"); + + hash = await provider.sendTransaction(startTransaction); + response = await controller.awaitCompletedExecute(hash); + + assert.equal(statusTransaction.data.toString(), "status@6c75636b79"); + assert.isTrue(response.returnCode == "ok"); + assert.isTrue(response.values.length == 1); + assert.deepEqual(response.values[0]!.valueOf(), { name: "Running", fields: [] }); + + // lotteryInfo() (this is a view function, but for the sake of the test, we'll execute it) + let getLotteryInfoTransaction = getLotteryInfoInteraction + .withSender(alice.address) + .withNonce(15n) + .withChainID("mock") + .buildTransaction(); + + getLotteryInfoTransaction.signature = await alice.signTransaction(getLotteryInfoTransaction); + provider.mockGetTransactionWithAnyHashAsNotarizedWithOneResult( + "@6f6b@0000000b6c75636b792d746f6b656e000000010100000000000000005fc2b9dbffffffff00000001640000000a140ec80fa7ee88000000", + "getLotteryInfo", + ); + hash = await provider.sendTransaction(startTransaction); + response = await controller.awaitCompletedExecute(hash); + assert.equal(getLotteryInfoTransaction.data.toString(), "getLotteryInfo@6c75636b79"); + assert.isTrue(response.returnCode == "ok"); + assert.isTrue(response.values.length == 1); + + assert.deepEqual(response.values[0]!.valueOf(), { + token_identifier: "lucky-token", + ticket_price: new BigNumber("1"), + tickets_left: new BigNumber(0), + deadline: new BigNumber("0x000000005fc2b9db", 16), + max_entries_per_user: new BigNumber(0xffffffff), + prize_distribution: Buffer.from([0x64]), + prize_pool: new BigNumber("94720000000000000000000"), + }); + }); +}); diff --git a/src/smartcontracts/interaction.ts b/src/abi/interaction.ts similarity index 66% rename from src/smartcontracts/interaction.ts rename to src/abi/interaction.ts index 2568dc7dc..2dc379b52 100644 --- a/src/smartcontracts/interaction.ts +++ b/src/abi/interaction.ts @@ -1,13 +1,12 @@ -import { Account } from "../account"; -import { Address } from "../address"; -import { Compatibility } from "../compatibility"; -import { TRANSACTION_VERSION_DEFAULT } from "../constants"; -import { IAddress, IChainID, IGasLimit, IGasPrice, INonce, ITokenTransfer, ITransactionValue } from "../interface"; -import { TokenTransfer } from "../tokens"; -import { Transaction } from "../transaction"; -import { SmartContractTransactionsFactory, TransactionsFactoryConfig } from "../transactionsFactories"; +import { Account } from "../accounts"; +import { Address } from "../core/address"; +import { Compatibility } from "../core/compatibility"; +import { TRANSACTION_VERSION_DEFAULT } from "../core/constants"; +import { TokenTransfer } from "../core/tokens"; +import { Transaction } from "../core/transaction"; +import { TransactionsFactoryConfig } from "../core/transactionsFactoryConfig"; +import { SmartContractTransactionsFactory } from "../smartContracts"; import { ContractFunction } from "./function"; -import { InteractionChecker } from "./interactionChecker"; import { CallArguments } from "./interface"; import { Query } from "./query"; import { EndpointDefinition, TypedValue } from "./typesystem"; @@ -17,12 +16,12 @@ import { EndpointDefinition, TypedValue } from "./typesystem"; */ interface ISmartContractWithinInteraction { call({ func, args, value, gasLimit, receiver }: CallArguments): Transaction; - getAddress(): IAddress; + getAddress(): Address; getEndpoint(name: ContractFunction): EndpointDefinition; } /** - * Legacy component. Use "SmartContractTransactionsFactory" (for transactions) or "SmartContractQueriesController" (for queries), instead. + * @deprecated component. Use "SmartContractTransactionsFactory" or "SmartContractController", instead. * * Interactions can be seen as mutable transaction & query builders. * @@ -34,14 +33,14 @@ export class Interaction { private readonly function: ContractFunction; private readonly args: TypedValue[]; - private nonce: INonce = 0; - private value: ITransactionValue = "0"; - private gasLimit: IGasLimit = 0; - private gasPrice: IGasPrice | undefined = undefined; - private chainID: IChainID = ""; - private querent: IAddress = Address.empty(); - private explicitReceiver?: IAddress; - private sender: IAddress = Address.empty(); + private nonce: bigint = 0n; + private value: bigint = 0n; + private gasLimit: bigint = 0n; + private gasPrice: bigint | undefined = undefined; + private chainID: string = ""; + private querent: Address = Address.empty(); + private explicitReceiver?: Address; + private sender: Address = Address.empty(); private version: number = TRANSACTION_VERSION_DEFAULT; private tokenTransfers: TokenTransfer[]; @@ -53,7 +52,7 @@ export class Interaction { this.tokenTransfers = []; } - getContractAddress(): IAddress { + getContractAddress(): Address { return this.contract.getAddress(); } @@ -69,19 +68,19 @@ export class Interaction { return this.args; } - getValue(): ITransactionValue { + getValue(): bigint { return this.value; } - getTokenTransfers(): ITokenTransfer[] { + getTokenTransfers(): TokenTransfer[] { return this.tokenTransfers; } - getGasLimit(): IGasLimit { + getGasLimit(): bigint { return this.gasLimit; } - getExplicitReceiver(): IAddress | undefined { + getExplicitReceiver(): Address | undefined { return this.explicitReceiver; } @@ -97,8 +96,7 @@ export class Interaction { config: factoryConfig, }); - const transaction = factory.createTransactionForExecute({ - sender: this.sender, + const transaction = factory.createTransactionForExecute(this.sender, { contract: this.contract.getAddress(), function: this.function.valueOf(), gasLimit: BigInt(this.gasLimit.valueOf()), @@ -129,37 +127,37 @@ export class Interaction { }); } - withValue(value: ITransactionValue): Interaction { + withValue(value: bigint): Interaction { this.value = value; return this; } - withSingleESDTTransfer(transfer: ITokenTransfer): Interaction { + withSingleESDTTransfer(transfer: TokenTransfer): Interaction { this.tokenTransfers = [transfer].map((transfer) => new TokenTransfer(transfer)); return this; } - withSingleESDTNFTTransfer(transfer: ITokenTransfer): Interaction { + withSingleESDTNFTTransfer(transfer: TokenTransfer): Interaction { this.tokenTransfers = [transfer].map((transfer) => new TokenTransfer(transfer)); return this; } - withMultiESDTNFTTransfer(transfers: ITokenTransfer[]): Interaction { + withMultiESDTNFTTransfer(transfers: TokenTransfer[]): Interaction { this.tokenTransfers = transfers.map((transfer) => new TokenTransfer(transfer)); return this; } - withGasLimit(gasLimit: IGasLimit): Interaction { + withGasLimit(gasLimit: bigint): Interaction { this.gasLimit = gasLimit; return this; } - withGasPrice(gasPrice: IGasPrice): Interaction { + withGasPrice(gasPrice: bigint): Interaction { this.gasPrice = gasPrice; return this; } - withNonce(nonce: INonce): Interaction { + withNonce(nonce: bigint): Interaction { this.nonce = nonce; return this; } @@ -168,12 +166,12 @@ export class Interaction { return this.withNonce(account.getNonceThenIncrement()); } - withChainID(chainID: IChainID): Interaction { + withChainID(chainID: string): Interaction { this.chainID = chainID; return this; } - withSender(sender: IAddress): Interaction { + withSender(sender: Address): Interaction { this.sender = sender; return this; } @@ -186,21 +184,13 @@ export class Interaction { /** * Sets the "caller" field on contract queries. */ - withQuerent(querent: IAddress): Interaction { + withQuerent(querent: Address): Interaction { this.querent = querent; return this; } - withExplicitReceiver(receiver: IAddress): Interaction { + withExplicitReceiver(receiver: Address): Interaction { this.explicitReceiver = receiver; return this; } - - /** - * To perform custom checking, extend {@link Interaction} and override this method. - */ - check(): Interaction { - new InteractionChecker().checkInteraction(this, this.getEndpoint()); - return this; - } } diff --git a/src/smartcontracts/interface.ts b/src/abi/interface.ts similarity index 74% rename from src/smartcontracts/interface.ts rename to src/abi/interface.ts index b474ad8ed..7502b5623 100644 --- a/src/smartcontracts/interface.ts +++ b/src/abi/interface.ts @@ -1,5 +1,5 @@ -import { IAddress, IChainID, IGasLimit, IGasPrice, ITransactionValue } from "../interface"; -import { Transaction } from "../transaction"; +import { Address } from "../core/address"; +import { Transaction } from "../core/transaction"; import { ReturnCode } from "./returnCode"; import { TypedValue } from "./typesystem"; @@ -10,7 +10,7 @@ export interface ISmartContract { /** * Gets the address of the Smart Contract. */ - getAddress(): IAddress; + getAddress(): Address; /** * Creates a {@link Transaction} for deploying the Smart Contract to the Network. @@ -32,40 +32,40 @@ export interface DeployArguments { code: ICode; codeMetadata?: ICodeMetadata; initArguments?: any[]; - value?: ITransactionValue; - gasLimit: IGasLimit; - gasPrice?: IGasPrice; - chainID: IChainID; - deployer: IAddress; + value?: bigint; + gasLimit: bigint; + gasPrice?: bigint; + chainID: string; + deployer: Address; } export interface UpgradeArguments { code: ICode; codeMetadata?: ICodeMetadata; initArguments?: any[]; - value?: ITransactionValue; - gasLimit: IGasLimit; - gasPrice?: IGasPrice; - chainID: IChainID; - caller: IAddress; + value?: bigint; + gasLimit: bigint; + gasPrice?: bigint; + chainID: string; + caller: Address; } export interface CallArguments { func: IContractFunction; args?: any[]; - value?: ITransactionValue; - gasLimit: IGasLimit; - receiver?: IAddress; - gasPrice?: IGasPrice; - chainID: IChainID; - caller: IAddress; + value?: bigint; + gasLimit: bigint; + receiver?: Address; + gasPrice?: bigint; + chainID: string; + caller: Address; } export interface QueryArguments { func: IContractFunction; args?: TypedValue[]; - value?: ITransactionValue; - caller?: IAddress; + value?: bigint; + caller?: Address; } export interface TypedOutcomeBundle { diff --git a/src/smartcontracts/nativeSerializer.spec.ts b/src/abi/nativeSerializer.spec.ts similarity index 96% rename from src/smartcontracts/nativeSerializer.spec.ts rename to src/abi/nativeSerializer.spec.ts index 060ee3a8d..548aa877f 100644 --- a/src/smartcontracts/nativeSerializer.spec.ts +++ b/src/abi/nativeSerializer.spec.ts @@ -1,15 +1,16 @@ import BigNumber from "bignumber.js"; import { assert } from "chai"; -import { Address } from "../address"; -import { ErrInvalidArgument } from "../errors"; +import { Address, ErrInvalidArgument } from "../core"; import { NativeSerializer } from "./nativeSerializer"; import { - AbiRegistry, + Abi, AddressType, AddressValue, BigUIntType, BooleanType, BooleanValue, + BytesType, + BytesValue, CompositeType, CompositeValue, EndpointDefinition, @@ -34,7 +35,6 @@ import { VariadicType, VariadicValue, } from "./typesystem"; -import { BytesType, BytesValue } from "./typesystem/bytes"; describe("test native serializer", () => { it("should perform type inference", async () => { @@ -172,7 +172,7 @@ describe("test native serializer", () => { }); it("should handle optionals in a strict manner (but it does not)", async () => { - const endpoint = AbiRegistry.create({ + const endpoint = Abi.create({ endpoints: [ { name: "foo", @@ -222,7 +222,7 @@ describe("test native serializer", () => { }); it("should accept a mix between typed values and regular JavaScript objects (variadic, optionals)", async () => { - const endpoint = AbiRegistry.create({ + const endpoint = Abi.create({ endpoints: [ { name: "foo", @@ -271,7 +271,7 @@ describe("test native serializer", () => { }); it("should accept a mix between typed values and regular JavaScript objects (composite, optionals)", async () => { - const endpoint = AbiRegistry.create({ + const endpoint = Abi.create({ endpoints: [ { name: "foo", @@ -288,7 +288,7 @@ describe("test native serializer", () => { const compositeType = new CompositeType(new AddressType(), new U64Type()); const optionalCompositeType = new OptionalType(compositeType); const addressBech32 = "erd1dc3yzxxeq69wvf583gw0h67td226gu2ahpk3k50qdgzzym8npltq7ndgha"; - const address = Address.fromBech32(addressBech32); + const address = Address.newFromBech32(addressBech32); const compositeValue = CompositeValue.fromItems(new AddressValue(address), new U64Value(42)); const optionalCompositeValue = new OptionalValue(optionalCompositeType, compositeValue); @@ -331,7 +331,7 @@ describe("test native serializer", () => { }); it("should accept a mix between typed values and regular JavaScript objects (tuples)", async () => { - const endpoint = AbiRegistry.create({ + const endpoint = Abi.create({ endpoints: [ { name: "foo", @@ -404,7 +404,7 @@ describe("test native serializer", () => { }); it("should accept managed decimals with constants and variable decimals", async () => { - const endpoint = AbiRegistry.create({ + const endpoint = Abi.create({ endpoints: [ { name: "foo", @@ -445,7 +445,7 @@ describe("test native serializer", () => { }); it("should accept no value for variadic types", async () => { - const endpoint = AbiRegistry.create({ + const endpoint = Abi.create({ endpoints: [ { name: "foo", @@ -474,7 +474,7 @@ describe("test native serializer", () => { }); it("should accept null or undefined for option types and optionals", async () => { - const endpoint = AbiRegistry.create({ + const endpoint = Abi.create({ endpoints: [ { name: "foo", @@ -502,7 +502,7 @@ describe("test native serializer", () => { }); it("should perform type inference (enums)", async () => { - const abiRegistry = AbiRegistry.create({ + const abi = Abi.create({ endpoints: [ { name: "foo", @@ -573,8 +573,8 @@ describe("test native serializer", () => { }, }); - const endpoint = abiRegistry.getEndpoint("foo"); - const enumType = abiRegistry.getEnum("MyEnum"); + const endpoint = abi.getEndpoint("foo"); + const enumType = abi.getEnum("MyEnum"); // Simple enum by discriminant const p0 = 0; @@ -604,7 +604,7 @@ describe("test native serializer", () => { }); it("should perform type inference (explicit-enums)", async () => { - const abiRegistry = AbiRegistry.create({ + const abi = Abi.create({ endpoints: [ { name: "foo", @@ -633,8 +633,8 @@ describe("test native serializer", () => { }, }); - const endpoint = abiRegistry.getEndpoint("foo"); - const enumType = abiRegistry.getExplicitEnum("OperationCompletionStatus"); + const endpoint = abi.getEndpoint("foo"); + const enumType = abi.getExplicitEnum("OperationCompletionStatus"); const enumString = "completed"; const typedValues = NativeSerializer.nativeToTypedValues([enumString], endpoint); @@ -644,7 +644,7 @@ describe("test native serializer", () => { }); it("should getArgumentsCardinality", async () => { - const abi = AbiRegistry.create({ + const abi = Abi.create({ endpoints: [ { name: "a", @@ -708,7 +708,7 @@ describe("test native serializer", () => { }); it("should accept a mixed of values for boolen type", async () => { - const endpoint = AbiRegistry.create({ + const endpoint = Abi.create({ endpoints: [ { name: "foo", diff --git a/src/smartcontracts/nativeSerializer.ts b/src/abi/nativeSerializer.ts similarity index 98% rename from src/smartcontracts/nativeSerializer.ts rename to src/abi/nativeSerializer.ts index e1c10c610..5ea5dc133 100644 --- a/src/smartcontracts/nativeSerializer.ts +++ b/src/abi/nativeSerializer.ts @@ -1,9 +1,8 @@ /* eslint-disable @typescript-eslint/no-namespace */ import BigNumber from "bignumber.js"; -import { Address } from "../address"; -import { ErrInvalidArgument } from "../errors"; -import { IAddress } from "../interface"; -import { numberToPaddedHex } from "../utils.codec"; +import { Address } from "../core/address"; +import { ErrInvalidArgument } from "../core/errors"; +import { numberToPaddedHex } from "../core/utils.codec"; import { ArgumentErrorContext } from "./argumentErrorContext"; import { AddressType, @@ -67,7 +66,7 @@ import { export namespace NativeTypes { export type NativeBuffer = Buffer | string; export type NativeBytes = Buffer | { valueOf(): Buffer } | string; - export type NativeAddress = string | Buffer | IAddress | { getAddress(): IAddress }; + export type NativeAddress = string | Buffer | Address | { getAddress(): Address }; export type NativeBigNumber = BigNumber.Value | bigint; } @@ -403,9 +402,9 @@ export namespace NativeSerializer { export function convertNativeToAddress( native: NativeTypes.NativeAddress, errorContext: ArgumentErrorContext, - ): IAddress { + ): Address { if ((native).bech32) { - return native; + return
native; } if ((native).getAddress) { return (native).getAddress(); diff --git a/src/smartcontracts/query.spec.ts b/src/abi/query.spec.ts similarity index 96% rename from src/smartcontracts/query.spec.ts rename to src/abi/query.spec.ts index 090166b1a..10319f9fb 100644 --- a/src/smartcontracts/query.spec.ts +++ b/src/abi/query.spec.ts @@ -1,9 +1,9 @@ +import BigNumber from "bignumber.js"; import { assert } from "chai"; -import { Address } from "../address"; +import { Address } from "../core/address"; import { ContractFunction } from "./function"; import { Query } from "./query"; import { BigUIntValue, U32Value } from "./typesystem"; -import BigNumber from "bignumber.js"; import { BytesValue } from "./typesystem/bytes"; describe("test smart contract queries", () => { diff --git a/src/smartcontracts/query.ts b/src/abi/query.ts similarity index 66% rename from src/smartcontracts/query.ts rename to src/abi/query.ts index 7249ebd6a..07e44c3e1 100644 --- a/src/smartcontracts/query.ts +++ b/src/abi/query.ts @@ -1,28 +1,27 @@ -import { Address } from "../address"; -import { TypedValue } from "./typesystem"; +import { Address } from "../core/address"; import { ArgSerializer } from "./argSerializer"; -import { IAddress, ITransactionValue } from "../interface"; import { IContractFunction } from "./interface"; +import { TypedValue } from "./typesystem"; export class Query { - caller: IAddress; - address: IAddress; + caller: Address; + address: Address; func: IContractFunction; args: TypedValue[]; - value: ITransactionValue; + value: bigint; constructor(obj: { - caller?: IAddress; - address: IAddress; + caller?: Address; + address: Address; func: IContractFunction; args?: TypedValue[]; - value?: ITransactionValue; + value?: bigint; }) { this.caller = obj.caller || Address.empty(); this.address = obj.address; this.func = obj.func; this.args = obj.args || []; - this.value = obj.value || 0; + this.value = obj.value || 0n; } getEncodedArguments(): string[] { diff --git a/src/smartcontracts/returnCode.ts b/src/abi/returnCode.ts similarity index 100% rename from src/smartcontracts/returnCode.ts rename to src/abi/returnCode.ts diff --git a/src/abi/smartContract.local.net.spec.ts b/src/abi/smartContract.local.net.spec.ts new file mode 100644 index 000000000..9e97bbfb6 --- /dev/null +++ b/src/abi/smartContract.local.net.spec.ts @@ -0,0 +1,352 @@ +import { assert } from "chai"; +import { promises } from "fs"; +import { Account } from "../accounts"; +import { Logger } from "../core/logger"; +import { TransactionsFactoryConfig } from "../core/transactionsFactoryConfig"; +import { TransactionWatcher } from "../core/transactionWatcher"; +import { + SmartContractController, + SmartContractTransactionsFactory, + SmartContractTransactionsOutcomeParser, +} from "../smartContracts"; +import { createLocalnetProvider } from "../testutils/networkProviders"; +import { getTestWalletsPath, stringifyBigIntJSON } from "../testutils/utils"; +import { decodeUnsignedNumber } from "./codec"; +import { SmartContract } from "./smartContract"; +import { + AddressValue, + BigUIntValue, + BytesValue, + OptionalValue, + OptionValue, + TokenIdentifierValue, + U32Value, +} from "./typesystem"; + +describe("test on local testnet", function () { + let alice: Account, bob: Account, carol: Account; + let provider = createLocalnetProvider(); + let watcher: TransactionWatcher; + let parser: SmartContractTransactionsOutcomeParser; + + before(async function () { + alice = await Account.newFromPem(`${getTestWalletsPath()}/alice.pem`); + bob = await Account.newFromPem(`${getTestWalletsPath()}/bob.pem`); + carol = await Account.newFromPem(`${getTestWalletsPath()}/carol.pem`); + + watcher = new TransactionWatcher( + { + getTransaction: async (hash: string) => { + return await provider.getTransaction(hash); + }, + }, + { + pollingIntervalMilliseconds: 5000, + timeoutMilliseconds: 50000, + }, + ); + parser = new SmartContractTransactionsOutcomeParser(); + }); + + it("counter: should deploy, then simulate transactions using SmartContractTransactionsFactory", async function () { + this.timeout(60000); + + let network = await provider.getNetworkConfig(); + + const config = new TransactionsFactoryConfig({ chainID: network.chainID }); + const factory = new SmartContractTransactionsFactory({ config: config }); + + const bytecode = await promises.readFile("src/testdata/counter.wasm"); + alice.nonce = (await provider.getAccount(alice.address)).nonce; + + const deployTransaction = factory.createTransactionForDeploy(alice.address, { + bytecode: bytecode, + gasLimit: 4000000n, + }); + deployTransaction.nonce = alice.nonce; + + deployTransaction.signature = await alice.signTransaction(deployTransaction); + + const contractAddress = SmartContract.computeAddress(alice.address, alice.nonce); + + const smartContractCallTransaction = factory.createTransactionForExecute(alice.address, { + contract: contractAddress, + function: "increment", + gasLimit: 4000000n, + }); + alice.incrementNonce(); + smartContractCallTransaction.nonce = alice.getNonceThenIncrement(); + smartContractCallTransaction.signature = await alice.signTransaction(smartContractCallTransaction); + + const simulateOne = factory.createTransactionForExecute(alice.address, { + function: "increment", + contract: contractAddress, + gasLimit: 200000n, + }); + simulateOne.nonce = alice.getNonceThenIncrement(); + simulateOne.signature = await alice.signTransaction(simulateOne); + + const simulateTwo = factory.createTransactionForExecute(alice.address, { + function: "foobar", + contract: contractAddress, + gasLimit: 700000n, + }); + simulateTwo.nonce = alice.getNonceThenIncrement(); + simulateTwo.signature = await alice.signTransaction(simulateTwo); + + const simulateThree = factory.createTransactionForExecute(alice.address, { + function: "foobar", + contract: contractAddress, + gasLimit: 700000n, + }); + simulateThree.nonce = alice.getNonceThenIncrement(); + simulateThree.signature = await alice.signTransaction(simulateThree); + + // Broadcast & execute + const deployTxHash = await provider.sendTransaction(deployTransaction); + const callTxHash = await provider.sendTransaction(smartContractCallTransaction); + await watcher.awaitCompleted(deployTxHash); + let transactionOnNetwork = await provider.getTransaction(deployTxHash); + let response = parser.parseExecute({ transactionOnNetwork }); + + assert.isTrue(response.returnCode == "ok"); + + await watcher.awaitCompleted(callTxHash); + transactionOnNetwork = await provider.getTransaction(callTxHash); + response = parser.parseExecute({ transactionOnNetwork }); + assert.isTrue(response.returnCode == "ok"); + + // Simulate + Logger.trace(stringifyBigIntJSON(await provider.simulateTransaction(simulateOne))); + Logger.trace(stringifyBigIntJSON(await provider.simulateTransaction(simulateTwo))); + }); + + it("counter: should deploy, call and query contract using SmartContractTransactionsFactory", async function () { + this.timeout(80000); + + let network = await provider.getNetworkConfig(); + + const config = new TransactionsFactoryConfig({ chainID: network.chainID }); + const factory = new SmartContractTransactionsFactory({ config: config }); + + alice.nonce = (await provider.getAccount(alice.address)).nonce; + const bytecode = await promises.readFile("src/testdata/counter.wasm"); + + const deployTransaction = factory.createTransactionForDeploy(alice.address, { + bytecode: bytecode, + gasLimit: 3000000n, + }); + deployTransaction.nonce = alice.nonce; + + deployTransaction.signature = await alice.signTransaction(deployTransaction); + + const contractAddress = SmartContract.computeAddress(alice.address, alice.nonce); + alice.incrementNonce(); + const firstScCallTransaction = factory.createTransactionForExecute(alice.address, { + contract: contractAddress, + function: "increment", + gasLimit: 3000000n, + }); + firstScCallTransaction.nonce = alice.getNonceThenIncrement(); + firstScCallTransaction.signature = await alice.signTransaction(firstScCallTransaction); + + const secondScCallTransaction = factory.createTransactionForExecute(alice.address, { + contract: contractAddress, + function: "increment", + gasLimit: 3000000n, + }); + secondScCallTransaction.nonce = alice.getNonceThenIncrement(); + secondScCallTransaction.signature = await alice.signTransaction(secondScCallTransaction); + + // Broadcast & execute + const deployTxHash = await provider.sendTransaction(deployTransaction); + const firstScCallHash = await provider.sendTransaction(firstScCallTransaction); + const secondScCallHash = await provider.sendTransaction(secondScCallTransaction); + + await watcher.awaitCompleted(deployTxHash); + await watcher.awaitCompleted(firstScCallHash); + await watcher.awaitCompleted(secondScCallHash); + + // Check counter + const smartContractQueriesController = new SmartContractController({ + chainID: "localnet", + networkProvider: provider, + }); + + const query = smartContractQueriesController.createQuery({ + contract: contractAddress, + function: "get", + arguments: [], + }); + + const queryResponse = await smartContractQueriesController.runQuery(query); + assert.lengthOf(queryResponse.returnDataParts, 1); + assert.equal(3, decodeUnsignedNumber(Buffer.from(queryResponse.returnDataParts[0]))); + }); + + it("erc20: should deploy, call and query contract using SmartContractTransactionsFactory", async function () { + this.timeout(60000); + + let network = await provider.getNetworkConfig(); + + const config = new TransactionsFactoryConfig({ chainID: network.chainID }); + const factory = new SmartContractTransactionsFactory({ config: config }); + + alice.nonce = (await provider.getAccount(alice.address)).nonce; + const bytecode = await promises.readFile("src/testdata/erc20.wasm"); + + const deployTransaction = factory.createTransactionForDeploy(alice.address, { + bytecode: bytecode, + gasLimit: 50000000n, + arguments: [new U32Value(10000)], + }); + deployTransaction.nonce = alice.nonce; + deployTransaction.signature = await alice.signTransaction(deployTransaction); + + const contractAddress = SmartContract.computeAddress(alice.address, alice.nonce); + alice.incrementNonce(); + const transactionMintBob = factory.createTransactionForExecute(alice.address, { + contract: contractAddress, + function: "transferToken", + gasLimit: 9000000n, + arguments: [new AddressValue(bob.address), new U32Value(1000)], + }); + transactionMintBob.nonce = alice.getNonceThenIncrement(); + transactionMintBob.signature = await alice.signTransaction(transactionMintBob); + + const transactionMintCarol = factory.createTransactionForExecute(alice.address, { + contract: contractAddress, + function: "transferToken", + gasLimit: 9000000n, + arguments: [new AddressValue(carol.address), new U32Value(1500)], + }); + transactionMintCarol.nonce = alice.getNonceThenIncrement(); + transactionMintCarol.signature = await alice.signTransaction(transactionMintCarol); + + // Broadcast & execute + const deployTxHash = await provider.sendTransaction(deployTransaction); + const mintBobTxHash = await provider.sendTransaction(transactionMintBob); + const mintCarolTxHash = await provider.sendTransaction(transactionMintCarol); + + await watcher.awaitCompleted(deployTxHash); + await watcher.awaitCompleted(mintBobTxHash); + await watcher.awaitCompleted(mintCarolTxHash); + + // Query state, do some assertions + const smartContractController = new SmartContractController({ + chainID: "localnet", + networkProvider: provider, + }); + + let query = smartContractController.createQuery({ + contract: contractAddress, + function: "totalSupply", + arguments: [], + }); + let queryResponse = await smartContractController.runQuery(query); + assert.lengthOf(queryResponse.returnDataParts, 1); + assert.equal(10000, decodeUnsignedNumber(Buffer.from(queryResponse.returnDataParts[0]))); + + query = smartContractController.createQuery({ + contract: contractAddress, + function: "balanceOf", + arguments: [new AddressValue(alice.address)], + }); + queryResponse = await smartContractController.runQuery(query); + assert.equal(7500, decodeUnsignedNumber(Buffer.from(queryResponse.returnDataParts[0]))); + + query = smartContractController.createQuery({ + contract: contractAddress, + function: "balanceOf", + arguments: [new AddressValue(bob.address)], + }); + queryResponse = await smartContractController.runQuery(query); + assert.equal(1000, decodeUnsignedNumber(Buffer.from(queryResponse.returnDataParts[0]))); + + query = smartContractController.createQuery({ + contract: contractAddress, + function: "balanceOf", + arguments: [new AddressValue(carol.address)], + }); + queryResponse = await smartContractController.runQuery(query); + assert.equal(1500, decodeUnsignedNumber(Buffer.from(queryResponse.returnDataParts[0]))); + }); + + it("lottery: should deploy, call and query contract using SmartContractTransactionsFactory", async function () { + this.timeout(60000); + let network = await provider.getNetworkConfig(); + alice.nonce = (await provider.getAccount(alice.address)).nonce; + + const config = new TransactionsFactoryConfig({ chainID: network.chainID }); + const factory = new SmartContractTransactionsFactory({ config: config }); + + const bytecode = await promises.readFile("src/testdata/lottery-esdt.wasm"); + + const deployTransaction = factory.createTransactionForDeploy(alice.address, { + bytecode: bytecode, + gasLimit: 50000000n, + }); + deployTransaction.nonce = alice.nonce; + + deployTransaction.signature = await alice.signTransaction(deployTransaction); + + const contractAddress = SmartContract.computeAddress(alice.address, alice.nonce); + alice.incrementNonce(); + const startTransaction = factory.createTransactionForExecute(alice.address, { + contract: contractAddress, + function: "start", + gasLimit: 10000000n, + arguments: [ + BytesValue.fromUTF8("lucky"), + new TokenIdentifierValue("EGLD"), + new BigUIntValue(1), + OptionValue.newMissing(), + OptionValue.newMissing(), + OptionValue.newProvided(new U32Value(1)), + OptionValue.newMissing(), + OptionValue.newMissing(), + OptionalValue.newMissing(), + ], + }); + startTransaction.nonce = alice.getNonceThenIncrement(); + startTransaction.signature = await alice.signTransaction(startTransaction); + + // Broadcast & execute + const deployTx = await provider.sendTransaction(deployTransaction); + const startTx = await provider.sendTransaction(startTransaction); + + await watcher.awaitAllEvents(deployTx, ["SCDeploy"]); + await watcher.awaitAnyEvent(startTx, ["completedTxEvent"]); + + // Let's check the SCRs + let transactionOnNetwork = await provider.getTransaction(deployTx); + let response = parser.parseExecute({ transactionOnNetwork }); + assert.isTrue(response.returnCode == "ok"); + + transactionOnNetwork = await provider.getTransaction(startTx); + response = parser.parseExecute({ transactionOnNetwork }); + assert.isTrue(response.returnCode == "ok"); + + // Query state, do some assertions + const smartContractQueriesController = new SmartContractController({ + chainID: "localnet", + networkProvider: provider, + }); + + let query = smartContractQueriesController.createQuery({ + contract: contractAddress, + function: "status", + arguments: [BytesValue.fromUTF8("lucky")], + }); + let queryResponse = await smartContractQueriesController.runQuery(query); + assert.equal(decodeUnsignedNumber(Buffer.from(queryResponse.returnDataParts[0])), 1); + + query = smartContractQueriesController.createQuery({ + contract: contractAddress, + function: "status", + arguments: [BytesValue.fromUTF8("missingLottery")], + }); + queryResponse = await smartContractQueriesController.runQuery(query); + assert.equal(decodeUnsignedNumber(Buffer.from(queryResponse.returnDataParts[0])), 0); + }); +}); diff --git a/src/smartcontracts/smartContract.spec.ts b/src/abi/smartContract.spec.ts similarity index 64% rename from src/smartcontracts/smartContract.spec.ts rename to src/abi/smartContract.spec.ts index bf5e2a132..273783100 100644 --- a/src/smartcontracts/smartContract.spec.ts +++ b/src/abi/smartContract.spec.ts @@ -1,72 +1,72 @@ -import { TransactionStatus } from "../networkProviders"; import { assert } from "chai"; -import { Address } from "../address"; -import { - loadTestWallets, - MarkCompleted, - MockNetworkProvider, - setupUnitTestWatcherTimeouts, - TestWallet, - Wait, -} from "../testutils"; -import { TransactionWatcher } from "../transactionWatcher"; +import { Account } from "../accounts"; +import { Address } from "../core/address"; +import { TransactionComputer } from "../core/transactionComputer"; +import { TransactionStatus } from "../core/transactionStatus"; +import { TransactionWatcher } from "../core/transactionWatcher"; +import { getTestWalletsPath, MarkCompleted, MockNetworkProvider, Wait } from "../testutils"; import { Code } from "./code"; import { ContractFunction } from "./function"; import { SmartContract } from "./smartContract"; -import { AbiRegistry, OptionalValue, U32Value, U8Value, VariadicValue } from "./typesystem"; +import { Abi, OptionalValue, U32Value, U8Value, VariadicValue } from "./typesystem"; import { BytesValue } from "./typesystem/bytes"; describe("test contract", () => { let provider = new MockNetworkProvider(); let chainID = "test"; - let alice: TestWallet; + let alice: Account; + const computer = new TransactionComputer(); before(async function () { - ({ alice } = await loadTestWallets()); + alice = await Account.newFromPem(`${getTestWalletsPath()}/alice.pem`); }); it("should compute contract address", async () => { let owner = new Address("93ee6143cdc10ce79f15b2a6c2ad38e9b6021c72a1779051f47154fd54cfbd5e"); - let firstContractAddress = SmartContract.computeAddress(owner, 0); - assert.equal(firstContractAddress.bech32(), "erd1qqqqqqqqqqqqqpgqhdjjyq8dr7v5yq9tv6v5vt9tfvd00vg7h40q6779zn"); + let firstContractAddress = SmartContract.computeAddress(owner, 0n); + assert.equal(firstContractAddress.toBech32(), "erd1qqqqqqqqqqqqqpgqhdjjyq8dr7v5yq9tv6v5vt9tfvd00vg7h40q6779zn"); - let secondContractAddress = SmartContract.computeAddress(owner, 1); - assert.equal(secondContractAddress.bech32(), "erd1qqqqqqqqqqqqqpgqde8eqjywyu6zlxjxuxqfg5kgtmn3setxh40qen8egy"); + let secondContractAddress = SmartContract.computeAddress(owner, 1n); + assert.equal( + secondContractAddress.toBech32(), + "erd1qqqqqqqqqqqqqpgqde8eqjywyu6zlxjxuxqfg5kgtmn3setxh40qen8egy", + ); }); it("should deploy", async () => { - setupUnitTestWatcherTimeouts(); - let watcher = new TransactionWatcher(provider); + let watcher = new TransactionWatcher(provider, { + pollingIntervalMilliseconds: 42, + timeoutMilliseconds: 42 * 42, + }); let contract = new SmartContract(); let deployTransaction = contract.deploy({ code: Code.fromBuffer(Buffer.from([1, 2, 3, 4])), - gasLimit: 1000000, + gasLimit: 1000000n, chainID: chainID, deployer: alice.address, }); - provider.mockUpdateAccount(alice.address, (account) => { - account.nonce = 42; - }); - - await alice.sync(provider); - deployTransaction.setNonce(alice.account.nonce); + deployTransaction.nonce = alice.nonce; - assert.equal(deployTransaction.getData().valueOf().toString(), "01020304@0500@0100"); - assert.equal(deployTransaction.getGasLimit().valueOf(), 1000000); - assert.equal(deployTransaction.getNonce().valueOf(), 42); + assert.equal(deployTransaction.data.toString(), "01020304@0500@0100"); + assert.equal(deployTransaction.gasLimit, 1000000n); + assert.equal(deployTransaction.nonce, alice.nonce); // Compute & set the contract address - contract.setAddress(SmartContract.computeAddress(alice.address, 42)); - assert.equal(contract.getAddress().bech32(), "erd1qqqqqqqqqqqqqpgq3ytm9m8dpeud35v3us20vsafp77smqghd8ss4jtm0q"); + contract.setAddress(SmartContract.computeAddress(alice.address, 42n)); + assert.equal( + contract.getAddress().toBech32(), + "erd1qqqqqqqqqqqqqpgq3ytm9m8dpeud35v3us20vsafp77smqghd8ss4jtm0q", + ); // Sign the transaction - deployTransaction.applySignature(await alice.signer.sign(deployTransaction.serializeForSigning())); + deployTransaction.signature = await alice.signTransaction(deployTransaction); // Now let's broadcast the deploy transaction, and wait for its execution. let hash = await provider.sendTransaction(deployTransaction); + const hashString = computer.computeTransactionHash(deployTransaction); await Promise.all([ provider.mockTransactionTimeline(deployTransaction, [ @@ -76,28 +76,28 @@ describe("test contract", () => { new TransactionStatus("executed"), new MarkCompleted(), ]), - watcher.awaitCompleted(deployTransaction.getHash().hex()), + watcher.awaitCompleted(hashString), ]); - assert.isTrue((await provider.getTransactionStatus(hash)).isExecuted()); + assert.isTrue((await provider.getTransaction(hash)).status.isCompleted()); }); it("should call", async () => { - setupUnitTestWatcherTimeouts(); - let watcher = new TransactionWatcher(provider); + let watcher = new TransactionWatcher(provider, { + pollingIntervalMilliseconds: 42, + timeoutMilliseconds: 42 * 42, + }); let contract = new SmartContract({ address: new Address("erd1qqqqqqqqqqqqqpgqak8zt22wl2ph4tswtyc39namqx6ysa2sd8ss4xmlj3"), }); - provider.mockUpdateAccount(alice.address, (account) => { - account.nonce = 42; - }); + alice.nonce = 42n; let callTransactionOne = contract.call({ func: new ContractFunction("helloEarth"), args: [new U32Value(5), BytesValue.fromHex("0123")], - gasLimit: 150000, + gasLimit: 150000n, chainID: chainID, caller: alice.address, }); @@ -105,26 +105,25 @@ describe("test contract", () => { let callTransactionTwo = contract.call({ func: new ContractFunction("helloMars"), args: [new U32Value(5), BytesValue.fromHex("0123")], - gasLimit: 1500000, + gasLimit: 1500000n, chainID: chainID, caller: alice.address, }); - await alice.sync(provider); - callTransactionOne.setNonce(alice.account.nonce); - alice.account.incrementNonce(); - callTransactionTwo.setNonce(alice.account.nonce); + callTransactionOne.nonce = alice.nonce; + alice.incrementNonce(); + callTransactionTwo.nonce = alice.nonce; - assert.equal(callTransactionOne.getNonce().valueOf(), 42); - assert.equal(callTransactionOne.getData().valueOf().toString(), "helloEarth@05@0123"); - assert.equal(callTransactionOne.getGasLimit().valueOf(), 150000); - assert.equal(callTransactionTwo.getNonce().valueOf(), 43); - assert.equal(callTransactionTwo.getData().valueOf().toString(), "helloMars@05@0123"); - assert.equal(callTransactionTwo.getGasLimit().valueOf(), 1500000); + assert.equal(callTransactionOne.nonce, 42n); + assert.equal(callTransactionOne.data.toString(), "helloEarth@05@0123"); + assert.equal(callTransactionOne.gasLimit, 150000n); + assert.equal(callTransactionTwo.nonce, 43n); + assert.equal(callTransactionTwo.data.toString(), "helloMars@05@0123"); + assert.equal(callTransactionTwo.gasLimit, 1500000n); // Sign transactions, broadcast them - callTransactionOne.applySignature(await alice.signer.sign(callTransactionOne.serializeForSigning())); - callTransactionTwo.applySignature(await alice.signer.sign(callTransactionTwo.serializeForSigning())); + callTransactionOne.signature = await alice.signTransaction(callTransactionOne); + callTransactionTwo.signature = await alice.signTransaction(callTransactionTwo); let hashOne = await provider.sendTransaction(callTransactionOne); let hashTwo = await provider.sendTransaction(callTransactionTwo); @@ -144,41 +143,39 @@ describe("test contract", () => { new TransactionStatus("executed"), new MarkCompleted(), ]), - watcher.awaitCompleted(callTransactionOne.getHash().hex()), - watcher.awaitCompleted(callTransactionTwo.getHash().hex()), + watcher.awaitCompleted(hashOne), + watcher.awaitCompleted(hashTwo), ]); - assert.isTrue((await provider.getTransactionStatus(hashOne)).isExecuted()); - assert.isTrue((await provider.getTransactionStatus(hashTwo)).isExecuted()); + assert.isTrue((await provider.getTransaction(hashOne)).status.isCompleted()); + assert.isTrue((await provider.getTransaction(hashTwo)).status.isCompleted()); }); it("should upgrade", async () => { - setupUnitTestWatcherTimeouts(); - let watcher = new TransactionWatcher(provider); + let watcher = new TransactionWatcher(provider, { + pollingIntervalMilliseconds: 42, + timeoutMilliseconds: 42 * 42, + }); let contract = new SmartContract(); - contract.setAddress(Address.fromBech32("erd1qqqqqqqqqqqqqpgq3ytm9m8dpeud35v3us20vsafp77smqghd8ss4jtm0q")); + contract.setAddress(Address.newFromBech32("erd1qqqqqqqqqqqqqpgq3ytm9m8dpeud35v3us20vsafp77smqghd8ss4jtm0q")); let deployTransaction = contract.upgrade({ code: Code.fromBuffer(Buffer.from([1, 2, 3, 4])), - gasLimit: 1000000, + gasLimit: 1000000n, chainID: chainID, caller: alice.address, }); - provider.mockUpdateAccount(alice.address, (account) => { - account.nonce = 42; - }); - - await alice.sync(provider); - deployTransaction.setNonce(alice.account.nonce); + alice.nonce = 42n; + deployTransaction.nonce = alice.nonce; - assert.equal(deployTransaction.getData().valueOf().toString(), "upgradeContract@01020304@0100"); - assert.equal(deployTransaction.getGasLimit().valueOf(), 1000000); - assert.equal(deployTransaction.getNonce().valueOf(), 42); + assert.equal(deployTransaction.data.toString(), "upgradeContract@01020304@0100"); + assert.equal(deployTransaction.gasLimit, 1000000n); + assert.equal(deployTransaction.nonce, 42n); // Sign the transaction - deployTransaction.applySignature(await alice.signer.sign(deployTransaction.serializeForSigning())); + deployTransaction.signature = await alice.signTransaction(deployTransaction); // Now let's broadcast the deploy transaction, and wait for its execution. let hash = await provider.sendTransaction(deployTransaction); @@ -191,10 +188,10 @@ describe("test contract", () => { new TransactionStatus("executed"), new MarkCompleted(), ]), - watcher.awaitCompleted(deployTransaction.getHash().hex()), + watcher.awaitCompleted(hash), ]); - assert.isTrue((await provider.getTransactionStatus(hash)).isExecuted()); + assert.isTrue((await provider.getTransaction(hash)).status.isCompleted()); }); it("v13 should be stricter than v12 on optional> (exotic) parameters (since NativeSerializer is used under the hood)", async () => { @@ -205,7 +202,7 @@ describe("test contract", () => { // These parameters are exotic and, generally speaking, can be avoided in contracts: // https://docs.multiversx.com/developers/data/multi-values/ - const abi = AbiRegistry.create({ + const abi = Abi.create({ endpoints: [ { name: "foo", @@ -218,8 +215,8 @@ describe("test contract", () => { ], }); - const callerAddress = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - const contractAddress = Address.fromBech32("erd1qqqqqqqqqqqqqpgqaxa53w6uk43n6dhyt2la6cd5lyv32qn4396qfsqlnk"); + const callerAddress = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + const contractAddress = Address.newFromBech32("erd1qqqqqqqqqqqqqpgqaxa53w6uk43n6dhyt2la6cd5lyv32qn4396qfsqlnk"); const contract = new SmartContract({ abi, @@ -233,7 +230,7 @@ describe("test contract", () => { func: "foo", args: [new U8Value(1), new U8Value(2), new U8Value(3)], chainID: "D", - gasLimit: 1000000, + gasLimit: 1000000n, caller: callerAddress, }); }, "Wrong number of arguments for endpoint foo: expected between 0 and 1 arguments, have 3"); @@ -243,7 +240,7 @@ describe("test contract", () => { func: "foo", args: [[new U8Value(1), new U8Value(2), new U8Value(3)]], chainID: "D", - gasLimit: 1000000, + gasLimit: 1000000n, caller: callerAddress, }); @@ -252,7 +249,7 @@ describe("test contract", () => { func: "foo", args: [[1, 2, 3]], chainID: "D", - gasLimit: 1000000, + gasLimit: 1000000n, caller: callerAddress, }); @@ -274,7 +271,7 @@ describe("test contract", () => { }); it("v13 should be stricter than v12 on variadic parameters (since NativeSerializer is used under the hood)", async () => { - const abi = AbiRegistry.create({ + const abi = Abi.create({ endpoints: [ { name: "foo", @@ -287,8 +284,8 @@ describe("test contract", () => { ], }); - const callerAddress = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - const contractAddress = Address.fromBech32("erd1qqqqqqqqqqqqqpgqaxa53w6uk43n6dhyt2la6cd5lyv32qn4396qfsqlnk"); + const callerAddress = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + const contractAddress = Address.newFromBech32("erd1qqqqqqqqqqqqqpgqaxa53w6uk43n6dhyt2la6cd5lyv32qn4396qfsqlnk"); const contract = new SmartContract({ abi, @@ -302,7 +299,7 @@ describe("test contract", () => { func: "foo", args: [new U8Value(1), new U8Value(2), new U8Value(3)], chainID: "D", - gasLimit: 1000000, + gasLimit: 1000000n, caller: callerAddress, }); }, "Invalid argument: Wrong argument type for endpoint foo: typed value provided; expected variadic type, have U8Value"); @@ -312,7 +309,7 @@ describe("test contract", () => { func: "foo", args: [VariadicValue.fromItems(new U8Value(1), new U8Value(2), new U8Value(3))], chainID: "D", - gasLimit: 1000000, + gasLimit: 1000000n, caller: callerAddress, }); @@ -321,7 +318,7 @@ describe("test contract", () => { func: "foo", args: [1, 2, 3], chainID: "D", - gasLimit: 1000000, + gasLimit: 1000000n, caller: callerAddress, }); diff --git a/src/smartcontracts/smartContract.ts b/src/abi/smartContract.ts similarity index 80% rename from src/smartcontracts/smartContract.ts rename to src/abi/smartContract.ts index b06b7a5a5..7282430be 100644 --- a/src/smartcontracts/smartContract.ts +++ b/src/abi/smartContract.ts @@ -1,13 +1,12 @@ -import { Address, AddressComputer } from "../address"; -import { Compatibility } from "../compatibility"; -import { TRANSACTION_MIN_GAS_PRICE } from "../constants"; -import { ErrContractHasNoAddress } from "../errors"; -import { IAddress, INonce } from "../interface"; -import { Transaction } from "../transaction"; -import { SmartContractTransactionsFactory } from "../transactionsFactories/smartContractTransactionsFactory"; -import { TransactionsFactoryConfig } from "../transactionsFactories/transactionsFactoryConfig"; -import { guardValueIsSet } from "../utils"; -import { CodeMetadata } from "./codeMetadata"; +import { Address, AddressComputer } from "../core/address"; +import { CodeMetadata } from "../core/codeMetadata"; +import { Compatibility } from "../core/compatibility"; +import { TRANSACTION_MIN_GAS_PRICE } from "../core/constants"; +import { ErrContractHasNoAddress } from "../core/errors"; +import { Transaction } from "../core/transaction"; +import { TransactionsFactoryConfig } from "../core/transactionsFactoryConfig"; +import { guardValueIsSet } from "../core/utils"; +import { SmartContractTransactionsFactory } from "./../smartContracts/smartContractTransactionsFactory"; import { ContractFunction } from "./function"; import { Interaction } from "./interaction"; import { @@ -30,10 +29,12 @@ interface IAbi { } /** + * * @deprecated component. Use "SmartContractTransactionsFactory" or "SmartContractController", instead. + * * An abstraction for deploying and interacting with Smart Contracts. */ export class SmartContract implements ISmartContract { - private address: IAddress = Address.empty(); + private address: Address = Address.empty(); private abi?: IAbi; /** @@ -54,7 +55,7 @@ export class SmartContract implements ISmartContract { /** * Create a SmartContract object by providing its address on the Network. */ - constructor(options: { address?: IAddress; abi?: IAbi } = {}) { + constructor(options: { address?: Address; abi?: IAbi } = {}) { this.address = options.address || Address.empty(); this.abi = options.abi; @@ -93,14 +94,14 @@ export class SmartContract implements ISmartContract { /** * Sets the address, as on Network. */ - setAddress(address: IAddress) { + setAddress(address: Address) { this.address = address; } /** * Gets the address, as on Network. */ - getAddress(): IAddress { + getAddress(): Address { return this.address; } @@ -141,20 +142,19 @@ export class SmartContract implements ISmartContract { const bytecode = Buffer.from(code.toString(), "hex"); const metadataAsJson = this.getMetadataPropertiesAsObject(codeMetadata); - const transaction = factory.createTransactionForDeploy({ - sender: deployer, + const transaction = factory.createTransactionForDeploy(deployer, { bytecode: bytecode, gasLimit: BigInt(gasLimit.valueOf()), - arguments: initArguments, + arguments: initArguments ?? [], isUpgradeable: metadataAsJson.upgradeable, isReadable: metadataAsJson.readable, isPayable: metadataAsJson.payable, isPayableBySmartContract: metadataAsJson.payableBySc, }); - transaction.setChainID(chainID); - transaction.setValue(value ?? 0); - transaction.setGasPrice(gasPrice ?? TRANSACTION_MIN_GAS_PRICE); + transaction.chainID = chainID; + transaction.value = value ?? 0n; + transaction.gasPrice = gasPrice ?? BigInt(TRANSACTION_MIN_GAS_PRICE); return transaction; } @@ -167,7 +167,7 @@ export class SmartContract implements ISmartContract { } { let metadata: CodeMetadata; if (codeMetadata) { - metadata = CodeMetadata.fromBytes(Buffer.from(codeMetadata.toString(), "hex")); + metadata = CodeMetadata.newFromBytes(Buffer.from(codeMetadata.toString(), "hex")); } else { metadata = new CodeMetadata(); } @@ -211,21 +211,20 @@ export class SmartContract implements ISmartContract { const bytecode = Uint8Array.from(Buffer.from(code.toString(), "hex")); const metadataAsJson = this.getMetadataPropertiesAsObject(codeMetadata); - const transaction = factory.createTransactionForUpgrade({ - sender: caller, + const transaction = factory.createTransactionForUpgrade(caller, { contract: this.getAddress(), bytecode: bytecode, gasLimit: BigInt(gasLimit.valueOf()), - arguments: initArguments, + arguments: initArguments ?? [], isUpgradeable: metadataAsJson.upgradeable, isReadable: metadataAsJson.readable, isPayable: metadataAsJson.payable, isPayableBySmartContract: metadataAsJson.payableBySc, }); - transaction.setChainID(chainID); - transaction.setValue(value ?? 0); - transaction.setGasPrice(gasPrice ?? TRANSACTION_MIN_GAS_PRICE); + transaction.chainID = chainID; + transaction.value = value ?? 0n; + transaction.gasPrice = gasPrice ?? BigInt(TRANSACTION_MIN_GAS_PRICE); return transaction; } @@ -249,19 +248,18 @@ export class SmartContract implements ISmartContract { }); args = args || []; - value = value || 0; + value = value || 0n; - const transaction = factory.createTransactionForExecute({ - sender: caller, + const transaction = factory.createTransactionForExecute(caller, { contract: receiver ? receiver : this.getAddress(), function: func.toString(), - gasLimit: BigInt(gasLimit.valueOf()), + gasLimit: gasLimit, arguments: args, }); - transaction.setChainID(chainID); - transaction.setValue(value); - transaction.setGasPrice(gasPrice ?? TRANSACTION_MIN_GAS_PRICE); + transaction.chainID = chainID; + transaction.value = value; + transaction.gasPrice = gasPrice ?? BigInt(TRANSACTION_MIN_GAS_PRICE); return transaction; } @@ -279,7 +277,7 @@ export class SmartContract implements ISmartContract { } private ensureHasAddress() { - if (!this.getAddress().bech32()) { + if (!this.getAddress().toBech32()) { throw new ErrContractHasNoAddress(); } } @@ -291,9 +289,8 @@ export class SmartContract implements ISmartContract { * @param owner The owner of the Smart Contract * @param nonce The owner nonce used for the deployment transaction */ - static computeAddress(owner: IAddress, nonce: INonce): IAddress { - const deployer = Address.fromBech32(owner.bech32()); + static computeAddress(owner: Address, nonce: bigint): Address { const addressComputer = new AddressComputer(); - return addressComputer.computeContractAddress(deployer, BigInt(nonce.valueOf())); + return addressComputer.computeContractAddress(owner, BigInt(nonce.valueOf())); } } diff --git a/src/smartcontracts/smartContractResults.local.net.spec.ts b/src/abi/smartContractResults.local.net.spec.ts similarity index 52% rename from src/smartcontracts/smartContractResults.local.net.spec.ts rename to src/abi/smartContractResults.local.net.spec.ts index 1ad837676..b66dbbd51 100644 --- a/src/smartcontracts/smartContractResults.local.net.spec.ts +++ b/src/abi/smartContractResults.local.net.spec.ts @@ -1,39 +1,42 @@ import { assert } from "chai"; -import { loadTestWallets, prepareDeployment, TestWallet } from "../testutils"; +import { promises } from "fs"; +import { Account } from "../accounts"; +import { TransactionsFactoryConfig } from "../core/transactionsFactoryConfig"; +import { TransactionWatcher } from "../core/transactionWatcher"; +import { SmartContractTransactionsFactory, SmartContractTransactionsOutcomeParser } from "../smartContracts"; +import { getTestWalletsPath, prepareDeployment } from "../testutils"; import { createLocalnetProvider } from "../testutils/networkProviders"; -import { TransactionWatcher } from "../transactionWatcher"; import { ContractFunction } from "./function"; -import { ResultsParser } from "./resultsParser"; import { SmartContract } from "./smartContract"; -import { TransactionsFactoryConfig } from "../transactionsFactories/transactionsFactoryConfig"; -import { SmartContractTransactionsFactory } from "../transactionsFactories/smartContractTransactionsFactory"; -import { promises } from "fs"; -import { TransactionComputer } from "../transactionComputer"; describe("fetch transactions from local testnet", function () { - let alice: TestWallet; + let alice: Account; let provider = createLocalnetProvider(); let watcher: TransactionWatcher; - - let resultsParser = new ResultsParser(); + let parser: SmartContractTransactionsOutcomeParser; before(async function () { - ({ alice } = await loadTestWallets()); - watcher = new TransactionWatcher({ - getTransaction: async (hash: string) => { - return await provider.getTransaction(hash, true); + alice = await Account.newFromPem(`${getTestWalletsPath()}/alice.pem`); + watcher = new TransactionWatcher( + { + getTransaction: async (hash: string) => { + return await provider.getTransaction(hash); + }, }, - }); + { + pollingIntervalMilliseconds: 5000, + timeoutMilliseconds: 50000, + }, + ); + + parser = new SmartContractTransactionsOutcomeParser(); }); it("counter smart contract", async function () { this.timeout(60000); - TransactionWatcher.DefaultPollingInterval = 5000; - TransactionWatcher.DefaultTimeout = 50000; - let network = await provider.getNetworkConfig(); - await alice.sync(provider); + alice.nonce = (await provider.getAccount(alice.address)).nonce; // Deploy let contract = new SmartContract({}); @@ -42,23 +45,21 @@ describe("fetch transactions from local testnet", function () { contract: contract, deployer: alice, codePath: "src/testdata/counter.wasm", - gasLimit: 3000000, + gasLimit: 3000000n, initArguments: [], - chainID: network.ChainID, + chainID: network.chainID, }); // ++ let transactionIncrement = contract.call({ func: new ContractFunction("increment"), - gasLimit: 3000000, - chainID: network.ChainID, + gasLimit: 3000000n, + chainID: network.chainID, caller: alice.address, }); - transactionIncrement.setNonce(alice.account.nonce); - transactionIncrement.applySignature(await alice.signer.sign(transactionIncrement.serializeForSigning())); - - alice.account.incrementNonce(); + transactionIncrement.nonce = alice.getNonceThenIncrement(); + transactionIncrement.signature = await alice.signTransaction(transactionIncrement); // Broadcast & execute const txHashDeploy = await provider.sendTransaction(transactionDeploy); @@ -70,54 +71,40 @@ describe("fetch transactions from local testnet", function () { const transactionOnNetworkDeploy = await provider.getTransaction(txHashDeploy); const transactionOnNetworkIncrement = await provider.getTransaction(txHashIncrement); - let bundle = resultsParser.parseUntypedOutcome(transactionOnNetworkDeploy); - assert.isTrue(bundle.returnCode.isSuccess()); + let response = parser.parseExecute({ transactionOnNetwork: transactionOnNetworkDeploy }); + assert.isTrue(response.returnCode == "ok"); - bundle = resultsParser.parseUntypedOutcome(transactionOnNetworkIncrement); - assert.isTrue(bundle.returnCode.isSuccess()); + response = parser.parseExecute({ transactionOnNetwork: transactionOnNetworkIncrement }); + assert.isTrue(response.returnCode == "ok"); }); it("interact with counter smart contract using SmartContractTransactionsFactory", async function () { this.timeout(60000); - TransactionWatcher.DefaultPollingInterval = 5000; - TransactionWatcher.DefaultTimeout = 50000; - let network = await provider.getNetworkConfig(); - await alice.sync(provider); - const config = new TransactionsFactoryConfig({ chainID: network.ChainID }); + const config = new TransactionsFactoryConfig({ chainID: network.chainID }); const factory = new SmartContractTransactionsFactory({ config: config }); const bytecode = await promises.readFile("src/testdata/counter.wasm"); + alice.nonce = (await provider.getAccount(alice.address)).nonce; - const deployTransaction = factory.createTransactionForDeploy({ - sender: alice.address, + const deployTransaction = factory.createTransactionForDeploy(alice.address, { bytecode: bytecode, gasLimit: 3000000n, }); - deployTransaction.nonce = BigInt(alice.account.nonce.valueOf()); - - const transactionComputer = new TransactionComputer(); - deployTransaction.signature = await alice.signer.sign( - Buffer.from(transactionComputer.computeBytesForSigning(deployTransaction)), - ); - - const contractAddress = SmartContract.computeAddress(alice.address, alice.account.nonce); - alice.account.incrementNonce(); + deployTransaction.nonce = alice.nonce; + deployTransaction.signature = await alice.signTransaction(deployTransaction); - const smartContractCallTransaction = factory.createTransactionForExecute({ - sender: alice.address, + const contractAddress = SmartContract.computeAddress(alice.address, alice.nonce); + alice.incrementNonce(); + const smartContractCallTransaction = factory.createTransactionForExecute(alice.address, { contract: contractAddress, function: "increment", gasLimit: 3000000n, }); - smartContractCallTransaction.nonce = BigInt(alice.account.nonce.valueOf()); - smartContractCallTransaction.signature = await alice.signer.sign( - Buffer.from(transactionComputer.computeBytesForSigning(smartContractCallTransaction)), - ); - - alice.account.incrementNonce(); + smartContractCallTransaction.nonce = alice.getNonceThenIncrement(); + smartContractCallTransaction.signature = await alice.signTransaction(smartContractCallTransaction); // Broadcast & execute const deployTxHash = await provider.sendTransaction(deployTransaction); @@ -129,10 +116,10 @@ describe("fetch transactions from local testnet", function () { let transactionOnNetworkDeploy = await provider.getTransaction(deployTxHash); let transactionOnNetworkIncrement = await provider.getTransaction(callTxHash); - let bundle = resultsParser.parseUntypedOutcome(transactionOnNetworkDeploy); - assert.isTrue(bundle.returnCode.isSuccess()); + let response = parser.parseExecute({ transactionOnNetwork: transactionOnNetworkDeploy }); + assert.isTrue(response.returnCode == "ok"); - bundle = resultsParser.parseUntypedOutcome(transactionOnNetworkIncrement); - assert.isTrue(bundle.returnCode.isSuccess()); + response = parser.parseExecute({ transactionOnNetwork: transactionOnNetworkIncrement }); + assert.isTrue(response.returnCode == "ok"); }); }); diff --git a/src/smartcontracts/typesystem/abiRegistry.spec.ts b/src/abi/typesystem/abi.spec.ts similarity index 59% rename from src/smartcontracts/typesystem/abiRegistry.spec.ts rename to src/abi/typesystem/abi.spec.ts index 216cddba2..b68a34219 100644 --- a/src/smartcontracts/typesystem/abiRegistry.spec.ts +++ b/src/abi/typesystem/abi.spec.ts @@ -12,29 +12,29 @@ import { StructType } from "./struct"; import { TokenIdentifierType } from "./tokenIdentifier"; import { VariadicType } from "./variadic"; -describe("test abi registry", () => { +describe("test abi", () => { it("load should also remap known to types", async () => { // Ultimate answer - let registry = await loadAbiRegistry("src/testdata/answer.abi.json"); - let getUltimateAnswer = registry.getEndpoint("getUltimateAnswer"); + let abi = await loadAbiRegistry("src/testdata/answer.abi.json"); + let getUltimateAnswer = abi.getEndpoint("getUltimateAnswer"); assert.instanceOf(getUltimateAnswer.output[0].type, I64Type); // Counter - registry = await loadAbiRegistry("src/testdata/counter.abi.json"); - let getCounter = registry.getEndpoint("get"); + abi = await loadAbiRegistry("src/testdata/counter.abi.json"); + let getCounter = abi.getEndpoint("get"); assert.instanceOf(getCounter.output[0].type, I64Type); // Lottery - registry = await loadAbiRegistry("src/testdata/lottery-esdt.abi.json"); - let start = registry.getEndpoint("start"); - let getStatus = registry.getEndpoint("status"); - let getLotteryInfo = registry.getEndpoint("getLotteryInfo"); + abi = await loadAbiRegistry("src/testdata/lottery-esdt.abi.json"); + let start = abi.getEndpoint("start"); + let getStatus = abi.getEndpoint("status"); + let getLotteryInfo = abi.getEndpoint("getLotteryInfo"); // basic-features - registry = await loadAbiRegistry("src/testdata/basic-features.abi.json"); - let returnManagedDecimal = registry.getEndpoint("returns_egld_decimal"); - let returnsManagedDecimalSigned = registry.getEndpoint("managed_decimal_ln"); - let returnsManagedDecimalVariable = registry.getEndpoint("managed_decimal_addition_var"); + abi = await loadAbiRegistry("src/testdata/basic-features.abi.json"); + let returnManagedDecimal = abi.getEndpoint("returns_egld_decimal"); + let returnsManagedDecimalSigned = abi.getEndpoint("managed_decimal_ln"); + let returnsManagedDecimalVariable = abi.getEndpoint("managed_decimal_addition_var"); assert.isFalse(start.modifiers.isReadonly()); assert.isTrue(getStatus.modifiers.isReadonly()); @@ -78,8 +78,8 @@ describe("test abi registry", () => { "hex", ); - let registry = await loadAbiRegistry("src/testdata/multisig-full.abi.json"); - let performAction = registry.getEndpoint("getActionData"); + let abi = await loadAbiRegistry("src/testdata/multisig-full.abi.json"); + let performAction = abi.getEndpoint("getActionData"); assert.equal(performAction.output[0].type.getName(), "Action"); let result = bc.decodeTopLevel(buff, performAction.output[0].type); @@ -91,10 +91,10 @@ describe("test abi registry", () => { }); it("should load ABI containing arrayN and nested structs", async () => { - let registry = await loadAbiRegistry("src/testdata/array-in-nested-structs.abi.json"); - let dummyType = registry.getStruct("Dummy"); - let fooType = registry.getStruct("Foo"); - let barType = registry.getStruct("Bar"); + let abi = await loadAbiRegistry("src/testdata/array-in-nested-structs.abi.json"); + let dummyType = abi.getStruct("Dummy"); + let fooType = abi.getStruct("Foo"); + let barType = abi.getStruct("Bar"); let fooTypeFromBarType = barType.getFieldDefinition("foo")!.type; let dummyTypeFromFooTypeFromBarType = fooTypeFromBarType.getFieldDefinition("dummy")!.type; @@ -107,40 +107,40 @@ describe("test abi registry", () => { }); it("should load ABI when custom types are out of order (a)", async () => { - const registry = await loadAbiRegistry("src/testdata/custom-types-out-of-order-a.abi.json"); + const abi = await loadAbiRegistry("src/testdata/custom-types-out-of-order-a.abi.json"); - assert.deepEqual(registry.getStruct("EsdtTokenTransfer").getNamesOfDependencies(), [ + assert.deepEqual(abi.getStruct("EsdtTokenTransfer").getNamesOfDependencies(), [ "EsdtTokenType", "TokenIdentifier", "u64", "BigUint", ]); - assert.deepEqual(registry.getEnum("EsdtTokenType").getNamesOfDependencies(), []); - assert.deepEqual(registry.getStruct("TypeA").getNamesOfDependencies(), ["TypeB", "TypeC", "u64"]); - assert.deepEqual(registry.getStruct("TypeB").getNamesOfDependencies(), ["TypeC", "u64"]); - assert.deepEqual(registry.getStruct("TypeC").getNamesOfDependencies(), ["u64"]); + assert.deepEqual(abi.getEnum("EsdtTokenType").getNamesOfDependencies(), []); + assert.deepEqual(abi.getStruct("TypeA").getNamesOfDependencies(), ["TypeB", "TypeC", "u64"]); + assert.deepEqual(abi.getStruct("TypeB").getNamesOfDependencies(), ["TypeC", "u64"]); + assert.deepEqual(abi.getStruct("TypeC").getNamesOfDependencies(), ["u64"]); }); it("should load ABI when custom types are out of order (b)", async () => { - const registry = await loadAbiRegistry("src/testdata/custom-types-out-of-order-b.abi.json"); + const abi = await loadAbiRegistry("src/testdata/custom-types-out-of-order-b.abi.json"); - assert.deepEqual(registry.getStruct("EsdtTokenTransfer").getNamesOfDependencies(), [ + assert.deepEqual(abi.getStruct("EsdtTokenTransfer").getNamesOfDependencies(), [ "EsdtTokenType", "TokenIdentifier", "u64", "BigUint", ]); - assert.deepEqual(registry.getEnum("EsdtTokenType").getNamesOfDependencies(), []); - assert.deepEqual(registry.getStruct("TypeA").getNamesOfDependencies(), ["TypeB", "TypeC", "u64"]); - assert.deepEqual(registry.getStruct("TypeB").getNamesOfDependencies(), ["TypeC", "u64"]); - assert.deepEqual(registry.getStruct("TypeC").getNamesOfDependencies(), ["u64"]); + assert.deepEqual(abi.getEnum("EsdtTokenType").getNamesOfDependencies(), []); + assert.deepEqual(abi.getStruct("TypeA").getNamesOfDependencies(), ["TypeB", "TypeC", "u64"]); + assert.deepEqual(abi.getStruct("TypeB").getNamesOfDependencies(), ["TypeC", "u64"]); + assert.deepEqual(abi.getStruct("TypeC").getNamesOfDependencies(), ["u64"]); }); it("should load ABI when custom types are out of order (community example: c)", async () => { - const registry = await loadAbiRegistry("src/testdata/custom-types-out-of-order-c.abi.json"); + const abi = await loadAbiRegistry("src/testdata/custom-types-out-of-order-c.abi.json"); - assert.lengthOf(registry.customTypes, 5); - assert.deepEqual(registry.getStruct("LoanCreateOptions").getNamesOfDependencies(), [ + assert.lengthOf(abi.customTypes, 5); + assert.deepEqual(abi.getStruct("LoanCreateOptions").getNamesOfDependencies(), [ "BigUint", "Address", "TokenIdentifier", @@ -150,10 +150,10 @@ describe("test abi registry", () => { }); it("should load ABI when custom types are out of order (community example: d)", async () => { - const registry = await loadAbiRegistry("src/testdata/custom-types-out-of-order-d.abi.json"); + const abi = await loadAbiRegistry("src/testdata/custom-types-out-of-order-d.abi.json"); - assert.lengthOf(registry.customTypes, 12); - assert.deepEqual(registry.getStruct("AuctionItem").getNamesOfDependencies(), [ + assert.lengthOf(abi.customTypes, 12); + assert.deepEqual(abi.getStruct("AuctionItem").getNamesOfDependencies(), [ "u64", "Address", "BigUint", @@ -166,36 +166,36 @@ describe("test abi registry", () => { }); it("should load ABI with counted-variadic", async () => { - const registry = await loadAbiRegistry("src/testdata/counted-variadic.abi.json"); - const dummyType = registry.getStruct("Dummy"); - - assert.deepEqual(registry.getEndpoint("foo").input[0].type, new VariadicType(dummyType, true)); - assert.deepEqual(registry.getEndpoint("bar").input[0].type, new VariadicType(new U32Type(), true)); - assert.deepEqual(registry.getEndpoint("bar").input[1].type, new VariadicType(new BytesType(), true)); - assert.deepEqual(registry.getEndpoint("bar").output[0].type, new VariadicType(new U32Type(), true)); - assert.deepEqual(registry.getEndpoint("bar").output[1].type, new VariadicType(new BytesType(), true)); + const abi = await loadAbiRegistry("src/testdata/counted-variadic.abi.json"); + const dummyType = abi.getStruct("Dummy"); + + assert.deepEqual(abi.getEndpoint("foo").input[0].type, new VariadicType(dummyType, true)); + assert.deepEqual(abi.getEndpoint("bar").input[0].type, new VariadicType(new U32Type(), true)); + assert.deepEqual(abi.getEndpoint("bar").input[1].type, new VariadicType(new BytesType(), true)); + assert.deepEqual(abi.getEndpoint("bar").output[0].type, new VariadicType(new U32Type(), true)); + assert.deepEqual(abi.getEndpoint("bar").output[1].type, new VariadicType(new BytesType(), true)); }); it("should load ABI wih events", async () => { - const registry = await loadAbiRegistry("src/testdata/esdt-safe.abi.json"); + const abi = await loadAbiRegistry("src/testdata/esdt-safe.abi.json"); - assert.lengthOf(registry.events, 8); + assert.lengthOf(abi.events, 8); - const depositEvent = registry.getEvent("deposit"); + const depositEvent = abi.getEvent("deposit"); assert.deepEqual(depositEvent.inputs[0].type, new AddressType()); - assert.deepEqual(depositEvent.inputs[1].type, new ListType(registry.getCustomType("EsdtTokenPayment"))); - assert.deepEqual(depositEvent.inputs[2].type, registry.getCustomType("DepositEvent")); + assert.deepEqual(depositEvent.inputs[1].type, new ListType(abi.getCustomType("EsdtTokenPayment"))); + assert.deepEqual(depositEvent.inputs[2].type, abi.getCustomType("DepositEvent")); - const setStatusEvent = registry.getEvent("setStatusEvent"); + const setStatusEvent = abi.getEvent("setStatusEvent"); assert.deepEqual(setStatusEvent.inputs[0].type, new U64Type()); assert.deepEqual(setStatusEvent.inputs[1].type, new U64Type()); - assert.deepEqual(setStatusEvent.inputs[2].type, registry.getCustomType("TransactionStatus")); + assert.deepEqual(setStatusEvent.inputs[2].type, abi.getCustomType("TransactionStatus")); }); it("should load ABI explicit-enum", async () => { - const registry = await loadAbiRegistry("src/testdata/basic-features.abi.json"); + const abi = await loadAbiRegistry("src/testdata/basic-features.abi.json"); - const enumType = registry.getExplicitEnum("OperationCompletionStatus"); + const enumType = abi.getExplicitEnum("OperationCompletionStatus"); assert.deepEqual(enumType.variants[0].name, "completed"); @@ -203,9 +203,9 @@ describe("test abi registry", () => { }); it("should load abi with title for endpoint", async () => { - const registry = await loadAbiRegistry("src/testdata/lottery-esdt.abi.json"); + const abi = await loadAbiRegistry("src/testdata/lottery-esdt.abi.json"); - const endpoint = registry.getEndpoint("createLotteryPool"); + const endpoint = abi.getEndpoint("createLotteryPool"); assert.equal(endpoint.title, "Create lottery pool"); }); diff --git a/src/smartcontracts/typesystem/abiRegistry.ts b/src/abi/typesystem/abi.ts similarity index 94% rename from src/smartcontracts/typesystem/abiRegistry.ts rename to src/abi/typesystem/abi.ts index 60a6c46ff..1c462ede8 100644 --- a/src/smartcontracts/typesystem/abiRegistry.ts +++ b/src/abi/typesystem/abi.ts @@ -1,5 +1,5 @@ -import * as errors from "../../errors"; -import { guardValueIsSetWithMessage } from "../../utils"; +import * as errors from "../../core/errors"; +import { guardValueIsSetWithMessage } from "../../core/utils"; import { EndpointDefinition, EndpointParameterDefinition } from "./endpoint"; import { EnumType } from "./enum"; import { EventDefinition, EventTopicDefinition } from "./event"; @@ -18,7 +18,7 @@ export class AbiRegistry { readonly customTypes: CustomType[] = []; readonly events: EventDefinition[] = []; - private constructor(options: { + protected constructor(options: { name: string; constructorDefinition: EndpointDefinition; upgradeConstructorDefinition?: EndpointDefinition; @@ -240,3 +240,19 @@ function mapEvent(event: EventDefinition, mapper: TypeMapper): EventDefinition { return new EventDefinition(event.identifier, newInputs); } + +export class Abi extends AbiRegistry { + /** + * + */ + constructor(options: { + name: string; + constructorDefinition: EndpointDefinition; + upgradeConstructorDefinition?: EndpointDefinition; + endpoints: EndpointDefinition[]; + customTypes: CustomType[]; + events?: EventDefinition[]; + }) { + super(options); + } +} diff --git a/src/smartcontracts/typesystem/address.ts b/src/abi/typesystem/address.ts similarity index 83% rename from src/smartcontracts/typesystem/address.ts rename to src/abi/typesystem/address.ts index ef8db6d02..c7e0e4b1e 100644 --- a/src/smartcontracts/typesystem/address.ts +++ b/src/abi/typesystem/address.ts @@ -1,5 +1,4 @@ -import { Address } from "../../address"; -import { IAddress } from "../../interface"; +import { Address } from "../../core/address"; import { PrimitiveType, PrimitiveValue } from "./types"; export class AddressType extends PrimitiveType { @@ -21,9 +20,9 @@ export class AddressValue extends PrimitiveValue { static ClassName = "AddressValue"; private readonly value: Address; - constructor(value: IAddress) { + constructor(value: Address) { super(new AddressType()); - this.value = Address.newFromBech32(value.bech32()); + this.value = Address.newFromBech32(value.toBech32()); } getClassName(): string { diff --git a/src/smartcontracts/typesystem/algebraic.ts b/src/abi/typesystem/algebraic.ts similarity index 97% rename from src/smartcontracts/typesystem/algebraic.ts rename to src/abi/typesystem/algebraic.ts index 91225dee0..249ba5003 100644 --- a/src/smartcontracts/typesystem/algebraic.ts +++ b/src/abi/typesystem/algebraic.ts @@ -1,4 +1,4 @@ -import { guardValueIsSet } from "../../utils"; +import { guardValueIsSet } from "../../core/utils"; import { NullType, Type, TypeCardinality, TypedValue } from "./types"; /** diff --git a/src/smartcontracts/typesystem/boolean.ts b/src/abi/typesystem/boolean.ts similarity index 100% rename from src/smartcontracts/typesystem/boolean.ts rename to src/abi/typesystem/boolean.ts diff --git a/src/smartcontracts/typesystem/bytes.ts b/src/abi/typesystem/bytes.ts similarity index 100% rename from src/smartcontracts/typesystem/bytes.ts rename to src/abi/typesystem/bytes.ts diff --git a/src/smartcontracts/typesystem/codeMetadata.ts b/src/abi/typesystem/codeMetadata.ts similarity index 91% rename from src/smartcontracts/typesystem/codeMetadata.ts rename to src/abi/typesystem/codeMetadata.ts index 60ff6c210..2db85c44e 100644 --- a/src/smartcontracts/typesystem/codeMetadata.ts +++ b/src/abi/typesystem/codeMetadata.ts @@ -1,4 +1,4 @@ -import { CodeMetadata } from "../codeMetadata"; +import { CodeMetadata } from "../../core"; import { PrimitiveType, PrimitiveValue } from "./types"; export class CodeMetadataType extends PrimitiveType { diff --git a/src/smartcontracts/typesystem/collections.ts b/src/abi/typesystem/collections.ts similarity index 100% rename from src/smartcontracts/typesystem/collections.ts rename to src/abi/typesystem/collections.ts diff --git a/src/smartcontracts/typesystem/composite.spec.ts b/src/abi/typesystem/composite.spec.ts similarity index 100% rename from src/smartcontracts/typesystem/composite.spec.ts rename to src/abi/typesystem/composite.spec.ts diff --git a/src/smartcontracts/typesystem/composite.ts b/src/abi/typesystem/composite.ts similarity index 97% rename from src/smartcontracts/typesystem/composite.ts rename to src/abi/typesystem/composite.ts index 6892a9a1f..27ed7a88a 100644 --- a/src/smartcontracts/typesystem/composite.ts +++ b/src/abi/typesystem/composite.ts @@ -1,4 +1,4 @@ -import { guardLength } from "../../utils"; +import { guardLength } from "../../core/utils"; import { Type, TypeCardinality, TypedValue } from "./types"; export class CompositeType extends Type { diff --git a/src/smartcontracts/typesystem/endpoint.spec.ts b/src/abi/typesystem/endpoint.spec.ts similarity index 100% rename from src/smartcontracts/typesystem/endpoint.spec.ts rename to src/abi/typesystem/endpoint.spec.ts diff --git a/src/smartcontracts/typesystem/endpoint.ts b/src/abi/typesystem/endpoint.ts similarity index 100% rename from src/smartcontracts/typesystem/endpoint.ts rename to src/abi/typesystem/endpoint.ts diff --git a/src/smartcontracts/typesystem/enum.spec.ts b/src/abi/typesystem/enum.spec.ts similarity index 98% rename from src/smartcontracts/typesystem/enum.spec.ts rename to src/abi/typesystem/enum.spec.ts index 50ede8e21..7002a3db4 100644 --- a/src/smartcontracts/typesystem/enum.spec.ts +++ b/src/abi/typesystem/enum.spec.ts @@ -1,10 +1,10 @@ -import * as errors from "../../errors"; +import BigNumber from "bignumber.js"; import { assert } from "chai"; -import { U8Type, U8Value } from "./numerical"; -import { Field, FieldDefinition } from "./fields"; +import * as errors from "../../core/errors"; import { EnumType, EnumValue, EnumVariantDefinition } from "./enum"; +import { Field, FieldDefinition } from "./fields"; +import { U8Type, U8Value } from "./numerical"; import { StringType, StringValue } from "./string"; -import BigNumber from "bignumber.js"; describe("test enums", () => { it("should get fields", () => { diff --git a/src/smartcontracts/typesystem/enum.ts b/src/abi/typesystem/enum.ts similarity index 97% rename from src/smartcontracts/typesystem/enum.ts rename to src/abi/typesystem/enum.ts index a0a2b24d6..424a550a2 100644 --- a/src/smartcontracts/typesystem/enum.ts +++ b/src/abi/typesystem/enum.ts @@ -1,5 +1,5 @@ -import { ErrMissingFieldOnEnum } from "../../errors"; -import { guardTrue, guardValueIsSet } from "../../utils"; +import { ErrMissingFieldOnEnum } from "../../core/errors"; +import { guardTrue, guardValueIsSet } from "../../core/utils"; import { Field, FieldDefinition, Fields } from "./fields"; import { CustomType, TypedValue } from "./types"; diff --git a/src/smartcontracts/typesystem/event.ts b/src/abi/typesystem/event.ts similarity index 100% rename from src/smartcontracts/typesystem/event.ts rename to src/abi/typesystem/event.ts diff --git a/src/smartcontracts/typesystem/explicit-enum.spec.ts b/src/abi/typesystem/explicit-enum.spec.ts similarity index 100% rename from src/smartcontracts/typesystem/explicit-enum.spec.ts rename to src/abi/typesystem/explicit-enum.spec.ts diff --git a/src/smartcontracts/typesystem/explicit-enum.ts b/src/abi/typesystem/explicit-enum.ts similarity index 97% rename from src/smartcontracts/typesystem/explicit-enum.ts rename to src/abi/typesystem/explicit-enum.ts index 264e18fa1..ac93c1fed 100644 --- a/src/smartcontracts/typesystem/explicit-enum.ts +++ b/src/abi/typesystem/explicit-enum.ts @@ -1,4 +1,4 @@ -import { guardValueIsSet } from "../../utils"; +import { guardValueIsSet } from "../../core/utils"; import { CustomType, TypedValue } from "./types"; export class ExplicitEnumType extends CustomType { diff --git a/src/smartcontracts/typesystem/factory.spec.ts b/src/abi/typesystem/factory.spec.ts similarity index 96% rename from src/smartcontracts/typesystem/factory.spec.ts rename to src/abi/typesystem/factory.spec.ts index 8645a16cd..368ffadee 100644 --- a/src/smartcontracts/typesystem/factory.spec.ts +++ b/src/abi/typesystem/factory.spec.ts @@ -1,9 +1,9 @@ import { assert } from "chai"; -import { TokenIdentifierType } from "./tokenIdentifier"; -import { Address } from "../../address"; +import { Address } from "../../core/address"; +import { AddressType } from "./address"; import { createListOfAddresses, createListOfTokenIdentifiers } from "./factory"; import { ListType } from "./generic"; -import { AddressType } from "./address"; +import { TokenIdentifierType } from "./tokenIdentifier"; describe("test factory", () => { it("should create lists of addresses", () => { diff --git a/src/smartcontracts/typesystem/factory.ts b/src/abi/typesystem/factory.ts similarity index 92% rename from src/smartcontracts/typesystem/factory.ts rename to src/abi/typesystem/factory.ts index 651de575e..c14319923 100644 --- a/src/smartcontracts/typesystem/factory.ts +++ b/src/abi/typesystem/factory.ts @@ -1,4 +1,4 @@ -import { Address } from "../../address"; +import { Address } from "../../core/address"; import { AddressValue } from "./address"; import { List } from "./generic"; import { TokenIdentifierValue } from "./tokenIdentifier"; diff --git a/src/smartcontracts/typesystem/fields.ts b/src/abi/typesystem/fields.ts similarity index 98% rename from src/smartcontracts/typesystem/fields.ts rename to src/abi/typesystem/fields.ts index d8953cce4..1a546a1e2 100644 --- a/src/smartcontracts/typesystem/fields.ts +++ b/src/abi/typesystem/fields.ts @@ -1,4 +1,4 @@ -import * as errors from "../../errors"; +import * as errors from "../../core/errors"; import { TypeExpressionParser } from "./typeExpressionParser"; import { Type, TypedValue } from "./types"; diff --git a/src/smartcontracts/typesystem/generic.ts b/src/abi/typesystem/generic.ts similarity index 97% rename from src/smartcontracts/typesystem/generic.ts rename to src/abi/typesystem/generic.ts index cfce5eb15..410351f1c 100644 --- a/src/smartcontracts/typesystem/generic.ts +++ b/src/abi/typesystem/generic.ts @@ -1,6 +1,6 @@ -import { guardValueIsSet } from "../../utils"; +import { guardValueIsSet } from "../../core/utils"; import { CollectionOfTypedValues } from "./collections"; -import { Type, TypedValue, NullType, TypePlaceholder } from "./types"; +import { NullType, Type, TypedValue, TypePlaceholder } from "./types"; // TODO: Move to a new file, "genericOption.ts" export class OptionType extends Type { diff --git a/src/smartcontracts/typesystem/genericArray.ts b/src/abi/typesystem/genericArray.ts similarity index 95% rename from src/smartcontracts/typesystem/genericArray.ts rename to src/abi/typesystem/genericArray.ts index ff353b84d..f5b56e88e 100644 --- a/src/smartcontracts/typesystem/genericArray.ts +++ b/src/abi/typesystem/genericArray.ts @@ -1,4 +1,4 @@ -import { guardLength, guardTrue } from "../../utils"; +import { guardLength, guardTrue } from "../../core/utils"; import { CollectionOfTypedValues } from "./collections"; import { Type, TypedValue } from "./types"; diff --git a/src/smartcontracts/typesystem/h256.ts b/src/abi/typesystem/h256.ts similarity index 100% rename from src/smartcontracts/typesystem/h256.ts rename to src/abi/typesystem/h256.ts diff --git a/src/smartcontracts/typesystem/index.ts b/src/abi/typesystem/index.ts similarity index 94% rename from src/smartcontracts/typesystem/index.ts rename to src/abi/typesystem/index.ts index 936c40328..e98aff96f 100644 --- a/src/smartcontracts/typesystem/index.ts +++ b/src/abi/typesystem/index.ts @@ -3,7 +3,7 @@ * @module typesystem */ -export * from "./abiRegistry"; +export * from "./abi"; export * from "./address"; export * from "./algebraic"; export * from "./boolean"; @@ -12,6 +12,7 @@ export * from "./codeMetadata"; export * from "./composite"; export * from "./endpoint"; export * from "./enum"; +export * from "./event"; export * from "./explicit-enum"; export * from "./factory"; export * from "./fields"; diff --git a/src/smartcontracts/typesystem/managedDecimal.spec.ts b/src/abi/typesystem/managedDecimal.spec.ts similarity index 100% rename from src/smartcontracts/typesystem/managedDecimal.spec.ts rename to src/abi/typesystem/managedDecimal.spec.ts diff --git a/src/smartcontracts/typesystem/managedDecimal.ts b/src/abi/typesystem/managedDecimal.ts similarity index 100% rename from src/smartcontracts/typesystem/managedDecimal.ts rename to src/abi/typesystem/managedDecimal.ts diff --git a/src/smartcontracts/typesystem/managedDecimalSigned.ts b/src/abi/typesystem/managedDecimalSigned.ts similarity index 100% rename from src/smartcontracts/typesystem/managedDecimalSigned.ts rename to src/abi/typesystem/managedDecimalSigned.ts diff --git a/src/smartcontracts/typesystem/matchers.ts b/src/abi/typesystem/matchers.ts similarity index 99% rename from src/smartcontracts/typesystem/matchers.ts rename to src/abi/typesystem/matchers.ts index a877a9660..2304619c0 100644 --- a/src/smartcontracts/typesystem/matchers.ts +++ b/src/abi/typesystem/matchers.ts @@ -1,4 +1,4 @@ -import * as errors from "../../errors"; +import * as errors from "../../core/errors"; import { AddressType, AddressValue } from "./address"; import { BooleanType, BooleanValue } from "./boolean"; import { BytesType, BytesValue } from "./bytes"; diff --git a/src/smartcontracts/typesystem/nothing.ts b/src/abi/typesystem/nothing.ts similarity index 100% rename from src/smartcontracts/typesystem/nothing.ts rename to src/abi/typesystem/nothing.ts diff --git a/src/smartcontracts/typesystem/numerical.ts b/src/abi/typesystem/numerical.ts similarity index 99% rename from src/smartcontracts/typesystem/numerical.ts rename to src/abi/typesystem/numerical.ts index 440600284..de2a97df0 100644 --- a/src/smartcontracts/typesystem/numerical.ts +++ b/src/abi/typesystem/numerical.ts @@ -1,5 +1,5 @@ import BigNumber from "bignumber.js"; -import * as errors from "../../errors"; +import * as errors from "../../core/errors"; import { PrimitiveType, PrimitiveValue } from "./types"; export class NumericalType extends PrimitiveType { diff --git a/src/smartcontracts/typesystem/string.ts b/src/abi/typesystem/string.ts similarity index 100% rename from src/smartcontracts/typesystem/string.ts rename to src/abi/typesystem/string.ts diff --git a/src/smartcontracts/typesystem/struct.spec.ts b/src/abi/typesystem/struct.spec.ts similarity index 96% rename from src/smartcontracts/typesystem/struct.spec.ts rename to src/abi/typesystem/struct.spec.ts index 36209019d..38de0a00d 100644 --- a/src/smartcontracts/typesystem/struct.spec.ts +++ b/src/abi/typesystem/struct.spec.ts @@ -1,9 +1,9 @@ -import * as errors from "../../errors"; import { assert } from "chai"; -import { BigUIntType, BigUIntValue, U32Type, U32Value } from "./numerical"; +import * as errors from "../../core/errors"; import { BytesType, BytesValue } from "./bytes"; -import { Struct, StructType } from "./struct"; import { Field, FieldDefinition } from "./fields"; +import { BigUIntType, BigUIntValue, U32Type, U32Value } from "./numerical"; +import { Struct, StructType } from "./struct"; import { TokenIdentifierType, TokenIdentifierValue } from "./tokenIdentifier"; describe("test structs", () => { diff --git a/src/smartcontracts/typesystem/struct.ts b/src/abi/typesystem/struct.ts similarity index 97% rename from src/smartcontracts/typesystem/struct.ts rename to src/abi/typesystem/struct.ts index 94e367f8f..4d44a8c0e 100644 --- a/src/smartcontracts/typesystem/struct.ts +++ b/src/abi/typesystem/struct.ts @@ -1,4 +1,4 @@ -import { ErrMissingFieldOnStruct } from "../../errors"; +import { ErrMissingFieldOnStruct } from "../../core/errors"; import { Field, FieldDefinition, Fields } from "./fields"; import { CustomType, TypedValue } from "./types"; diff --git a/src/smartcontracts/typesystem/tokenIdentifier.ts b/src/abi/typesystem/tokenIdentifier.ts similarity index 100% rename from src/smartcontracts/typesystem/tokenIdentifier.ts rename to src/abi/typesystem/tokenIdentifier.ts diff --git a/src/smartcontracts/typesystem/tuple.ts b/src/abi/typesystem/tuple.ts similarity index 97% rename from src/smartcontracts/typesystem/tuple.ts rename to src/abi/typesystem/tuple.ts index f249913db..e52fe3c0f 100644 --- a/src/smartcontracts/typesystem/tuple.ts +++ b/src/abi/typesystem/tuple.ts @@ -1,4 +1,4 @@ -import * as errors from "../../errors"; +import * as errors from "../../core/errors"; import { Field, FieldDefinition } from "./fields"; import { Struct, StructType } from "./struct"; import { Type, TypedValue } from "./types"; diff --git a/src/smartcontracts/typesystem/typeExpressionParser.spec.ts b/src/abi/typesystem/typeExpressionParser.spec.ts similarity index 99% rename from src/smartcontracts/typesystem/typeExpressionParser.spec.ts rename to src/abi/typesystem/typeExpressionParser.spec.ts index 0aa09ae2c..91065da00 100644 --- a/src/smartcontracts/typesystem/typeExpressionParser.spec.ts +++ b/src/abi/typesystem/typeExpressionParser.spec.ts @@ -1,5 +1,5 @@ import { assert } from "chai"; -import { ErrTypingSystem } from "../../errors"; +import { ErrTypingSystem } from "../../core"; import { TypeExpressionParser } from "./typeExpressionParser"; import { Type } from "./types"; diff --git a/src/smartcontracts/typesystem/typeExpressionParser.ts b/src/abi/typesystem/typeExpressionParser.ts similarity index 95% rename from src/smartcontracts/typesystem/typeExpressionParser.ts rename to src/abi/typesystem/typeExpressionParser.ts index 0c61d502f..076f44a04 100644 --- a/src/smartcontracts/typesystem/typeExpressionParser.ts +++ b/src/abi/typesystem/typeExpressionParser.ts @@ -1,6 +1,6 @@ import { TypeFormula } from "../../abi/typeFormula"; import { TypeFormulaParser } from "../../abi/typeFormulaParser"; -import { ErrTypingSystem } from "../../errors"; +import { ErrTypingSystem } from "../../core/errors"; import { Type } from "./types"; export class TypeExpressionParser { diff --git a/src/smartcontracts/typesystem/typeMapper.spec.ts b/src/abi/typesystem/typeMapper.spec.ts similarity index 100% rename from src/smartcontracts/typesystem/typeMapper.spec.ts rename to src/abi/typesystem/typeMapper.spec.ts diff --git a/src/smartcontracts/typesystem/typeMapper.ts b/src/abi/typesystem/typeMapper.ts similarity index 99% rename from src/smartcontracts/typesystem/typeMapper.ts rename to src/abi/typesystem/typeMapper.ts index 1d6fc13e3..3320f7757 100644 --- a/src/smartcontracts/typesystem/typeMapper.ts +++ b/src/abi/typesystem/typeMapper.ts @@ -1,4 +1,4 @@ -import * as errors from "../../errors"; +import * as errors from "../../core/errors"; import { AddressType } from "./address"; import { OptionalType } from "./algebraic"; import { BooleanType } from "./boolean"; diff --git a/src/smartcontracts/typesystem/types.spec.ts b/src/abi/typesystem/types.spec.ts similarity index 99% rename from src/smartcontracts/typesystem/types.spec.ts rename to src/abi/typesystem/types.spec.ts index eb77985aa..540183847 100644 --- a/src/smartcontracts/typesystem/types.spec.ts +++ b/src/abi/typesystem/types.spec.ts @@ -1,16 +1,16 @@ import BigNumber from "bignumber.js"; import { assert } from "chai"; -import * as errors from "../../errors"; +import * as errors from "../../core/errors"; import { AddressType } from "./address"; import { BooleanType } from "./boolean"; import { BytesType, BytesValue } from "./bytes"; import { OptionType } from "./generic"; +import { ManagedDecimalType } from "./managedDecimal"; +import { ManagedDecimalSignedType } from "./managedDecimalSigned"; import { I64Type, NumericalValue, U16Type, U32Type, U32Value } from "./numerical"; import { StringType } from "./string"; import { TypeExpressionParser } from "./typeExpressionParser"; import { NullType, PrimitiveType, Type } from "./types"; -import { ManagedDecimalType } from "./managedDecimal"; -import { ManagedDecimalSignedType } from "./managedDecimalSigned"; describe("test types", () => { let parser = new TypeExpressionParser(); diff --git a/src/smartcontracts/typesystem/types.ts b/src/abi/typesystem/types.ts similarity index 98% rename from src/smartcontracts/typesystem/types.ts rename to src/abi/typesystem/types.ts index 792d66e4b..40f51f0ff 100644 --- a/src/smartcontracts/typesystem/types.ts +++ b/src/abi/typesystem/types.ts @@ -1,5 +1,5 @@ -import { getJavascriptPrototypesInHierarchy } from "../../reflection"; -import { guardTrue, guardValueIsSet } from "../../utils"; +import { getJavascriptPrototypesInHierarchy } from "../../core/reflection"; +import { guardTrue, guardValueIsSet } from "../../core/utils"; /** * An abstraction that represents a Type. Handles both generic and non-generic types. diff --git a/src/smartcontracts/typesystem/variadic.ts b/src/abi/typesystem/variadic.ts similarity index 100% rename from src/smartcontracts/typesystem/variadic.ts rename to src/abi/typesystem/variadic.ts diff --git a/src/account.ts b/src/account.ts deleted file mode 100644 index d371afdc6..000000000 --- a/src/account.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Address } from "./address"; -import { IAccountBalance, IAddress, INonce } from "./interface"; - -/** - * An abstraction representing an account (user or Smart Contract) on the Network. - */ -export class Account { - /** - * The address of the account. - */ - readonly address: IAddress = Address.empty(); - - /** - * The nonce of the account (the account sequence number). - */ - nonce: INonce = 0; - - /** - * The balance of the account. - */ - balance: IAccountBalance = "0"; - - /** - * Creates an account object from an address - */ - constructor(address: IAddress) { - this.address = address; - } - - /** - * Updates account properties (such as nonce, balance). - */ - update(obj: { nonce: INonce; balance: IAccountBalance }) { - this.nonce = obj.nonce; - this.balance = obj.balance; - } - - /** - * Increments (locally) the nonce (the account sequence number). - */ - incrementNonce() { - this.nonce = this.nonce.valueOf() + 1; - } - - /** - * Gets then increments (locally) the nonce (the account sequence number). - */ - getNonceThenIncrement(): INonce { - let nonce = this.nonce; - this.nonce = this.nonce.valueOf() + 1; - return nonce; - } - - /** - * Converts the account to a pretty, plain JavaScript object. - */ - toJSON(): any { - return { - address: this.address.bech32(), - nonce: this.nonce.valueOf(), - balance: this.balance.toString(), - }; - } -} diff --git a/src/accountManagement/accountController.ts b/src/accountManagement/accountController.ts new file mode 100644 index 000000000..90f747c10 --- /dev/null +++ b/src/accountManagement/accountController.ts @@ -0,0 +1,79 @@ +import { Address, BaseController, BaseControllerInput } from "../core"; +import { IAccount } from "../core/interfaces"; +import { Transaction } from "../core/transaction"; +import { TransactionsFactoryConfig } from "../core/transactionsFactoryConfig"; +import { AccountTransactionsFactory } from "./accountTransactionsFactory"; +import { SaveKeyValueInput, SetGuardianInput } from "./resources"; + +export class AccountController extends BaseController { + private factory: AccountTransactionsFactory; + + constructor(options: { chainID: string }) { + super(); + this.factory = new AccountTransactionsFactory({ + config: new TransactionsFactoryConfig(options), + }); + } + + async createTransactionForSavingKeyValue( + sender: IAccount, + nonce: bigint, + options: SaveKeyValueInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForSavingKeyValue(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async createTransactionForSettingGuardian( + sender: IAccount, + nonce: bigint, + options: SetGuardianInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForSettingGuardian(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async createTransactionForGuardingAccount( + sender: IAccount, + nonce: bigint, + options: { relayer?: Address; gasPrice?: bigint; gasLimit?: bigint }, + ): Promise { + const transaction = this.factory.createTransactionForGuardingAccount(sender.address); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async createTransactionForUnguardingAccount( + sender: IAccount, + nonce: bigint, + options: BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForUnguardingAccount(sender.address); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } +} diff --git a/src/transactionsFactories/accountTransactionsFactory.spec.ts b/src/accountManagement/accountTransactionsFactory.spec.ts similarity index 52% rename from src/transactionsFactories/accountTransactionsFactory.spec.ts rename to src/accountManagement/accountTransactionsFactory.spec.ts index 6dc4ec8ee..2bee04226 100644 --- a/src/transactionsFactories/accountTransactionsFactory.spec.ts +++ b/src/accountManagement/accountTransactionsFactory.spec.ts @@ -1,6 +1,5 @@ import { assert } from "chai"; -import { Address } from "../address"; -import { TransactionsFactoryConfig } from "./transactionsFactoryConfig"; +import { Address, TransactionsFactoryConfig } from "../core"; import { AccountTransactionsFactory } from "./accountTransactionsFactory"; describe("test account transactions factory", function () { @@ -8,16 +7,21 @@ describe("test account transactions factory", function () { const factory = new AccountTransactionsFactory({ config: config }); it("should create 'Transaction' for saving key value", async function () { - const sender = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + const sender = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); const keyValuePairs = new Map([[Buffer.from("key0"), Buffer.from("value0")]]); - const transaction = factory.createTransactionForSavingKeyValue({ - sender: sender, + const transaction = factory.createTransactionForSavingKeyValue(sender, { keyValuePairs: keyValuePairs, }); - assert.equal(transaction.sender, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - assert.equal(transaction.receiver, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + ); + assert.deepEqual( + transaction.receiver, + Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + ); assert.equal(Buffer.from(transaction.data).toString(), "SaveKeyValue@6b657930@76616c756530"); assert.equal(transaction.value, 0n); assert.equal(transaction.chainID, config.chainID); @@ -25,18 +29,23 @@ describe("test account transactions factory", function () { }); it("should create 'Transaction' for setting guardian", async function () { - const sender = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - const guardian = Address.fromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + const sender = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + const guardian = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); const serviceID = "MultiversXTCSService"; - const transaction = factory.createTransactionForSettingGuardian({ - sender: sender, + const transaction = factory.createTransactionForSettingGuardian(sender, { guardianAddress: guardian, serviceID: serviceID, }); - assert.equal(transaction.sender, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - assert.equal(transaction.receiver, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + ); + assert.deepEqual( + transaction.receiver, + Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + ); assert.equal( Buffer.from(transaction.data).toString(), "SetGuardian@8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8@4d756c7469766572735854435353657276696365", @@ -47,14 +56,18 @@ describe("test account transactions factory", function () { }); it("should create 'Transaction' for guarding account", async function () { - const sender = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + const sender = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - const transaction = factory.createTransactionForGuardingAccount({ - sender: sender, - }); + const transaction = factory.createTransactionForGuardingAccount(sender); - assert.equal(transaction.sender, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - assert.equal(transaction.receiver, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + ); + assert.deepEqual( + transaction.receiver, + Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + ); assert.equal(Buffer.from(transaction.data).toString(), "GuardAccount"); assert.equal(transaction.value, 0n); assert.equal(transaction.chainID, config.chainID); @@ -62,14 +75,18 @@ describe("test account transactions factory", function () { }); it("should create 'Transaction' for unguarding account", async function () { - const sender = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + const sender = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - const transaction = factory.createTransactionForUnguardingAccount({ - sender: sender, - }); + const transaction = factory.createTransactionForUnguardingAccount(sender); - assert.equal(transaction.sender, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - assert.equal(transaction.receiver, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + ); + assert.deepEqual( + transaction.receiver, + Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + ); assert.equal(Buffer.from(transaction.data).toString(), "UnGuardAccount"); assert.equal(transaction.value, 0n); assert.equal(transaction.chainID, config.chainID); diff --git a/src/transactionsFactories/accountTransactionsFactory.ts b/src/accountManagement/accountTransactionsFactory.ts similarity index 71% rename from src/transactionsFactories/accountTransactionsFactory.ts rename to src/accountManagement/accountTransactionsFactory.ts index 01784bd21..a87fd37ef 100644 --- a/src/transactionsFactories/accountTransactionsFactory.ts +++ b/src/accountManagement/accountTransactionsFactory.ts @@ -1,7 +1,7 @@ -import { Address } from "../address"; -import { IAddress } from "../interface"; -import { Transaction } from "../transaction"; -import { TransactionBuilder } from "./transactionBuilder"; +import { Address } from "../core/address"; +import { Transaction } from "../core/transaction"; +import { TransactionBuilder } from "../core/transactionBuilder"; +import { SaveKeyValueInput, SetGuardianInput } from "./resources"; interface IConfig { chainID: string; @@ -22,10 +22,7 @@ export class AccountTransactionsFactory { this.config = options.config; } - createTransactionForSavingKeyValue(options: { - sender: IAddress; - keyValuePairs: Map; - }): Transaction { + createTransactionForSavingKeyValue(sender: Address, options: SaveKeyValueInput): Transaction { const functionName = "SaveKeyValue"; const keyValueParts = this.computeDataPartsForSavingKeyValue(options.keyValuePairs); const dataParts = [functionName, ...keyValueParts]; @@ -33,8 +30,8 @@ export class AccountTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, - receiver: options.sender, + sender: sender, + receiver: sender, dataParts: dataParts, gasLimit: extraGas, addDataMovementGas: true, @@ -63,47 +60,43 @@ export class AccountTransactionsFactory { return dataParts; } - createTransactionForSettingGuardian(options: { - sender: IAddress; - guardianAddress: IAddress; - serviceID: string; - }): Transaction { + createTransactionForSettingGuardian(sender: Address, options: SetGuardianInput): Transaction { const dataParts = [ "SetGuardian", - Address.fromBech32(options.guardianAddress.bech32()).toHex(), + options.guardianAddress.toHex(), Buffer.from(options.serviceID).toString("hex"), ]; return new TransactionBuilder({ config: this.config, - sender: options.sender, - receiver: options.sender, + sender: sender, + receiver: sender, dataParts: dataParts, gasLimit: this.config.gasLimitSetGuardian, addDataMovementGas: true, }).build(); } - createTransactionForGuardingAccount(options: { sender: IAddress }): Transaction { + createTransactionForGuardingAccount(sender: Address): Transaction { const dataParts = ["GuardAccount"]; return new TransactionBuilder({ config: this.config, - sender: options.sender, - receiver: options.sender, + sender: sender, + receiver: sender, dataParts: dataParts, gasLimit: this.config.gasLimitGuardAccount, addDataMovementGas: true, }).build(); } - createTransactionForUnguardingAccount(options: { sender: IAddress }): Transaction { + createTransactionForUnguardingAccount(sender: Address): Transaction { const dataParts = ["UnGuardAccount"]; return new TransactionBuilder({ config: this.config, - sender: options.sender, - receiver: options.sender, + sender: sender, + receiver: sender, dataParts: dataParts, gasLimit: this.config.gasLimitUnguardAccount, addDataMovementGas: true, diff --git a/src/accountManagement/index.ts b/src/accountManagement/index.ts new file mode 100644 index 000000000..9049c3d2b --- /dev/null +++ b/src/accountManagement/index.ts @@ -0,0 +1,3 @@ +export * from "./accountController"; +export * from "./accountTransactionsFactory"; +export * from "./resources"; diff --git a/src/accountManagement/resources.ts b/src/accountManagement/resources.ts new file mode 100644 index 000000000..9f8c34f71 --- /dev/null +++ b/src/accountManagement/resources.ts @@ -0,0 +1,4 @@ +import { Address } from "../core/address"; + +export type SetGuardianInput = { guardianAddress: Address; serviceID: string }; +export type SaveKeyValueInput = { keyValuePairs: Map }; diff --git a/src/accounts/account.spec.ts b/src/accounts/account.spec.ts new file mode 100644 index 000000000..cf4e1cfd2 --- /dev/null +++ b/src/accounts/account.spec.ts @@ -0,0 +1,132 @@ +import { assert } from "chai"; +import { Address, Message, Transaction } from "../core"; +import { getTestWalletsPath } from "../testutils/utils"; +import { KeyPair, UserSecretKey } from "../wallet"; +import { Account } from "./account"; + +describe("test account methods", function () { + const DUMMY_MNEMONIC = + "moral volcano peasant pass circle pen over picture flat shop clap goat never lyrics gather prepare woman film husband gravity behind test tiger improve"; + const alice = `${getTestWalletsPath()}/alice.pem`; + it("should create account from pem file", async function () { + const account = await Account.newFromPem(alice); + + assert.equal( + account.secretKey.valueOf().toString("hex"), + "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9", + ); + assert.equal(account.address.toBech32(), "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + }); + + it("should create account from keystore", async function () { + const account = Account.newFromKeystore(`${getTestWalletsPath()}/withDummyMnemonic.json`, "password"); + + assert.equal( + account.secretKey.valueOf().toString("hex"), + "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9", + ); + assert.equal(account.address.toBech32(), "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + }); + + it("should create account from mnemonic", async function () { + const account = Account.newFromMnemonic(DUMMY_MNEMONIC); + + assert.equal( + account.secretKey.valueOf().toString("hex"), + "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9", + ); + assert.equal(account.address.toBech32(), "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + }); + + it("should create account from keypair", async function () { + const secretKey = UserSecretKey.fromString("413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9"); + const keypair = new KeyPair(secretKey); + const account = Account.newFromKeypair(keypair); + + assert.deepEqual(account.secretKey, secretKey); + assert.equal(account.address.toBech32(), "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + }); + + it("should increase nonce on account", async function () { + const account = Account.newFromMnemonic(DUMMY_MNEMONIC); + account.nonce = 42n; + + assert.equal(account.getNonceThenIncrement(), 42n); + assert.equal(account.getNonceThenIncrement(), 43n); + }); + + it("should sign transaction", async function () { + const transaction = new Transaction({ + nonce: 89n, + value: 0n, + receiver: Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"), + sender: Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + gasPrice: 1000000000n, + gasLimit: 50000n, + data: new Uint8Array(), + chainID: "local-testnet", + version: 1, + options: 0, + }); + + const account = Account.newFromMnemonic(DUMMY_MNEMONIC); + transaction.signature = await account.signTransaction(transaction); + + assert.equal( + Buffer.from(transaction.signature).toString("hex"), + "b56769014f2bdc5cf9fc4a05356807d71fcf8775c819b0f1b0964625b679c918ffa64862313bfef86f99b38cb84fcdb16fa33ad6eb565276616723405cd8f109", + ); + }); + + it("should sign message", async function () { + const message = new Message({ + data: new Uint8Array(Buffer.from("hello")), + }); + + const account = Account.newFromMnemonic(DUMMY_MNEMONIC); + message.signature = await account.signMessage(message); + + assert.equal( + Buffer.from(message.signature).toString("hex"), + "561bc58f1dc6b10de208b2d2c22c9a474ea5e8cabb59c3d3ce06bbda21cc46454aa71a85d5a60442bd7784effa2e062fcb8fb421c521f898abf7f5ec165e5d0f", + ); + }); + + it("should verify message", async function () { + const message = new Message({ + data: new Uint8Array(Buffer.from("hello")), + }); + + const account = Account.newFromMnemonic(DUMMY_MNEMONIC); + message.signature = await account.signMessage(message); + const isVerified = await account.verifyMessageSignature(message, message.signature); + + assert.isTrue(isVerified); + }); + + it("should sign and verify transaction", async function () { + const transaction = new Transaction({ + nonce: 89n, + value: 0n, + receiver: Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"), + sender: Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + gasPrice: 1000000000n, + gasLimit: 50000n, + data: new Uint8Array(), + chainID: "local-testnet", + version: 1, + options: 0, + }); + + const account = Account.newFromMnemonic(DUMMY_MNEMONIC); + transaction.signature = await account.signTransaction(transaction); + + assert.equal( + Buffer.from(transaction.signature).toString("hex"), + "b56769014f2bdc5cf9fc4a05356807d71fcf8775c819b0f1b0964625b679c918ffa64862313bfef86f99b38cb84fcdb16fa33ad6eb565276616723405cd8f109", + ); + + const isVerified = await account.verifyTransactionSignature(transaction, transaction.signature); + assert.isTrue(isVerified); + }); +}); diff --git a/src/accounts/account.ts b/src/accounts/account.ts new file mode 100644 index 000000000..f32e54833 --- /dev/null +++ b/src/accounts/account.ts @@ -0,0 +1,174 @@ +import * as fs from "fs"; +import { PathLike } from "fs"; +import { Address, IAccount, LibraryConfig, Message, MessageComputer, Transaction, TransactionComputer } from "../core"; +import { KeyPair, Mnemonic, UserPem, UserPublicKey, UserSecretKey, UserSigner, UserWallet } from "../wallet"; + +/** + * An abstraction representing an account (user or Smart Contract) on the Network. + */ +export class Account implements IAccount { + /** + * The address of the account. + */ + readonly address: Address; + + /** + * Local copy of the account nonce. + * Must be explicitly managed by client code. + */ + nonce: bigint = 0n; + + /** + * The secret key of the account. + */ + readonly secretKey: UserSecretKey; + + /** + * The public key of the account. + */ + readonly publicKey: UserPublicKey; + + /** + * Creates an account object from a secret key + */ + constructor(secretKey: UserSecretKey, hrp: string = LibraryConfig.DefaultAddressHrp) { + this.secretKey = secretKey; + this.publicKey = secretKey.generatePublicKey(); + this.address = this.publicKey.toAddress(hrp); + } + + /** + * Named constructor + * Loads a secret key from a PEM file. PEM files may contain multiple accounts - thus, an (optional) "index" is used to select the desired secret key. + * Returns an Account object, initialized with the secret key. + */ + static async newFromPem( + path: PathLike, + index: number = 0, + hrp: string = LibraryConfig.DefaultAddressHrp, + ): Promise { + const text = await fs.promises.readFile(path, { encoding: "utf8" }); + const userSigner = UserSigner.fromPem(text, index); + return new Account(userSigner.secretKey, hrp); + } + + /** + * Named constructor + * Loads a secret key from an encrypted keystore file. Handles both keystores that hold a mnemonic and ones that hold a secret key (legacy). + * For keystores that hold an encrypted mnemonic, the optional "addressIndex" parameter is used to derive the desired secret key. + * Returns an Account object, initialized with the secret key. + */ + static newFromKeystore( + filePath: string, + password: string, + addressIndex?: number, + hrp: string = LibraryConfig.DefaultAddressHrp, + ): Account { + const secretKey = UserWallet.loadSecretKey(filePath, password, addressIndex); + return new Account(secretKey, hrp); + } + + /** + * Named constructor + * Loads (derives) a secret key from a mnemonic. The optional "addressIndex" parameter is used to guide the derivation. + * Returns an Account object, initialized with the secret key. + */ + static newFromMnemonic( + mnemonic: string, + addressIndex: number = 0, + hrp: string = LibraryConfig.DefaultAddressHrp, + ): Account { + const mnemonicHandler = Mnemonic.fromString(mnemonic); + const secretKey = mnemonicHandler.deriveKey(addressIndex); + return new Account(secretKey, hrp); + } + /** + * Named constructor + * Returns an Account object, initialized with the secret key. + */ + static newFromKeypair(keypair: KeyPair, hrp: string = LibraryConfig.DefaultAddressHrp): Account { + return new Account(keypair.secretKey, hrp); + } + + /** + * Increments (locally) the nonce (the account sequence number). + */ + incrementNonce() { + this.nonce = this.nonce + 1n; + } + + /** + * Signs using the secret key of the account. + */ + async sign(data: Uint8Array): Promise { + return this.secretKey.sign(data); + } + + /** + * Verifies the signature using the public key of the account. + */ + async verify(data: Uint8Array, signature: Uint8Array): Promise { + return this.publicKey.verify(data, signature); + } + + /** + * Serializes the transaction, computes the signature and returns it. + */ + async signTransaction(transaction: Transaction): Promise { + const transactionComputer = new TransactionComputer(); + const serializedTransaction = transactionComputer.computeBytesForSigning(transaction); + return this.secretKey.sign(serializedTransaction); + } + + /** + * Verifies the transaction signature using the public key of the account. + */ + async verifyTransactionSignature(transaction: Transaction, signature: Uint8Array): Promise { + const transactionComputer = new TransactionComputer(); + const serializedTransaction = transactionComputer.computeBytesForVerifying(transaction); + return this.publicKey.verify(serializedTransaction, signature); + } + + /** + * Serializes the message, computes the signature and returns it. + */ + async signMessage(message: Message): Promise { + const messageComputer = new MessageComputer(); + const serializedMessage = messageComputer.computeBytesForSigning(message); + return this.secretKey.sign(serializedMessage); + } + + /** + * Verifies the message signature using the public key of the account. + */ + async verifyMessageSignature(message: Message, signature: Uint8Array): Promise { + const messageComputer = new MessageComputer(); + const serializedMessage = messageComputer.computeBytesForVerifying(message); + return this.publicKey.verify(serializedMessage, signature); + } + + /** + * Gets the nonce (the one from the object's state) and increments it. + */ + getNonceThenIncrement(): bigint { + let nonce = this.nonce; + this.nonce = this.nonce + 1n; + return nonce; + } + + /** + * Saves the wallet to a pem file + */ + saveToPem(path: string): void { + const pem = new UserPem(this.address.toBech32(), this.secretKey); + pem.save(path); + } + + /** + * Saves the wallet to a keystore file + */ + saveToKeystore(path: PathLike, password: string): void { + const wallet = UserWallet.fromSecretKey({ secretKey: this.secretKey, password }); + wallet.save(path, this.address.getHrp()); + } +} diff --git a/src/accounts/index.ts b/src/accounts/index.ts new file mode 100644 index 000000000..ed4079f3e --- /dev/null +++ b/src/accounts/index.ts @@ -0,0 +1 @@ +export * from "./account"; diff --git a/src/adapters/index.ts b/src/adapters/index.ts deleted file mode 100644 index e73a1b2d8..000000000 --- a/src/adapters/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./queryRunnerAdapter"; diff --git a/src/adapters/queryRunnerAdapter.ts b/src/adapters/queryRunnerAdapter.ts deleted file mode 100644 index db673de4c..000000000 --- a/src/adapters/queryRunnerAdapter.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Address } from "../address"; -import { IAddress } from "../interface"; -import { IContractQueryResponse } from "../interfaceOfNetwork"; -import { SmartContractQuery, SmartContractQueryResponse } from "../smartContractQuery"; - -interface INetworkProvider { - queryContract(query: IQuery): Promise; -} - -interface IQuery { - address: IAddress; - caller?: IAddress; - func: { toString(): string }; - value?: { toString(): string }; - getEncodedArguments(): string[]; -} - -export class QueryRunnerAdapter { - private readonly networkProvider: INetworkProvider; - - constructor(options: { networkProvider: INetworkProvider }) { - this.networkProvider = options.networkProvider; - } - - async runQuery(query: SmartContractQuery): Promise { - const adaptedQuery: IQuery = { - address: Address.fromBech32(query.contract), - caller: query.caller ? Address.fromBech32(query.caller) : undefined, - func: query.function, - value: query.value, - getEncodedArguments: () => query.arguments.map((arg) => Buffer.from(arg).toString("hex")), - }; - - const adaptedQueryResponse = await this.networkProvider.queryContract(adaptedQuery); - return new SmartContractQueryResponse({ - function: query.function, - returnCode: adaptedQueryResponse.returnCode.toString(), - returnMessage: adaptedQueryResponse.returnMessage, - returnDataParts: adaptedQueryResponse.getReturnDataParts(), - }); - } -} diff --git a/src/converters/index.ts b/src/converters/index.ts deleted file mode 100644 index f43e67fcc..000000000 --- a/src/converters/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./transactionsConverter"; diff --git a/src/converters/transactionsConverter.ts b/src/converters/transactionsConverter.ts deleted file mode 100644 index c3798fbef..000000000 --- a/src/converters/transactionsConverter.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { Address } from "../address"; -import { IPlainTransactionObject, ITransaction } from "../interface"; -import { IContractResultItem, ITransactionEvent, ITransactionOnNetwork } from "../interfaceOfNetwork"; -import { ResultsParser } from "../smartcontracts"; -import { Transaction } from "../transaction"; -import { - SmartContractCallOutcome, - SmartContractResult, - TransactionEvent, - TransactionLogs, - TransactionOutcome, -} from "../transactionsOutcomeParsers/resources"; - -export class TransactionsConverter { - public transactionToPlainObject(transaction: ITransaction): IPlainTransactionObject { - const plainObject = { - nonce: Number(transaction.nonce), - value: transaction.value.toString(), - receiver: transaction.receiver, - sender: transaction.sender, - senderUsername: this.toBase64OrUndefined(transaction.senderUsername), - receiverUsername: this.toBase64OrUndefined(transaction.receiverUsername), - gasPrice: Number(transaction.gasPrice), - gasLimit: Number(transaction.gasLimit), - data: this.toBase64OrUndefined(transaction.data), - chainID: transaction.chainID.valueOf(), - version: transaction.version, - options: transaction.options == 0 ? undefined : transaction.options, - relayer: transaction.relayer.isEmpty() ? undefined : transaction.relayer.toBech32(), - guardian: transaction.guardian ? transaction.guardian : undefined, - signature: this.toHexOrUndefined(transaction.signature), - guardianSignature: this.toHexOrUndefined(transaction.guardianSignature), - relayerSignature: this.toHexOrUndefined(transaction.relayerSignature), - }; - - return plainObject; - } - - private toBase64OrUndefined(value?: string | Uint8Array) { - return value && value.length ? Buffer.from(value).toString("base64") : undefined; - } - - private toHexOrUndefined(value?: Uint8Array) { - return value && value.length ? Buffer.from(value).toString("hex") : undefined; - } - - public plainObjectToTransaction(object: IPlainTransactionObject): Transaction { - const transaction = new Transaction({ - nonce: BigInt(object.nonce), - value: BigInt(object.value || ""), - receiver: object.receiver, - relayer: object.relayer ? Address.newFromBech32(object.relayer) : Address.empty(), - receiverUsername: this.bufferFromBase64(object.receiverUsername).toString(), - sender: object.sender, - senderUsername: this.bufferFromBase64(object.senderUsername).toString(), - guardian: object.guardian, - gasPrice: BigInt(object.gasPrice), - gasLimit: BigInt(object.gasLimit), - data: this.bufferFromBase64(object.data), - chainID: String(object.chainID), - version: Number(object.version), - options: Number(object.options), - signature: this.bufferFromHex(object.signature), - guardianSignature: this.bufferFromHex(object.guardianSignature), - relayerSignature: this.bufferFromHex(object.relayerSignature), - }); - - return transaction; - } - - private bufferFromBase64(value?: string) { - return Buffer.from(value || "", "base64"); - } - - private bufferFromHex(value?: string) { - return Buffer.from(value || "", "hex"); - } - - /** - * @deprecated Where {@link TransactionOutcome} was needed (throughout the SDK), pass the {@link ITransactionOnNetwork} object instead. - * - * Summarizes the outcome of a transaction on the network, and maps it to the "standard" resources (according to the sdk-specs). - * - * In the future, this converter function will become obsolete, - * as the impedance mismatch between the network components and the "core" components will be reduced. - */ - public transactionOnNetworkToOutcome(transactionOnNetwork: ITransactionOnNetwork): TransactionOutcome { - // In the future, this will not be needed because the transaction, as returned from the API, - // will hold the data corresponding to the direct smart contract call outcome (in case of smart contract calls). - const legacyResultsParser = new ResultsParser(); - const callOutcomeBundle = legacyResultsParser.parseUntypedOutcome(transactionOnNetwork); - const callOutcome = new SmartContractCallOutcome({ - function: transactionOnNetwork.function, - returnCode: callOutcomeBundle.returnCode.toString(), - returnMessage: callOutcomeBundle.returnMessage, - returnDataParts: callOutcomeBundle.values, - }); - - const contractResults = transactionOnNetwork.contractResults.items.map((result) => - this.smartContractResultOnNetworkToSmartContractResult(result), - ); - - const logs = new TransactionLogs({ - address: transactionOnNetwork.logs.address.bech32(), - events: transactionOnNetwork.logs.events.map((event) => this.eventOnNetworkToEvent(event)), - }); - - return new TransactionOutcome({ - logs: logs, - smartContractResults: contractResults, - directSmartContractCallOutcome: callOutcome, - }); - } - - private smartContractResultOnNetworkToSmartContractResult( - resultOnNetwork: IContractResultItem, - ): SmartContractResult { - return new SmartContractResult({ - sender: resultOnNetwork.sender.bech32(), - receiver: resultOnNetwork.receiver.bech32(), - data: Buffer.from(resultOnNetwork.data), - logs: new TransactionLogs({ - address: resultOnNetwork.logs.address.bech32(), - events: resultOnNetwork.logs.events.map((event) => this.eventOnNetworkToEvent(event)), - }), - }); - } - - private eventOnNetworkToEvent(eventOnNetwork: ITransactionEvent): TransactionEvent { - // Before Sirius, there was no "additionalData" field on transaction logs. - // After Sirius, the "additionalData" field includes the payload of the legacy "data" field, as well (as its first element): - // https://github.com/multiversx/mx-chain-go/blob/v1.6.18/process/transactionLog/process.go#L159 - const legacyData = eventOnNetwork.dataPayload?.valueOf() || Buffer.from(eventOnNetwork.data || ""); - const dataItems = eventOnNetwork.additionalData?.map((data) => Buffer.from(data.valueOf())) || []; - - if (dataItems.length === 0) { - if (legacyData.length) { - dataItems.push(Buffer.from(legacyData)); - } - } - - return new TransactionEvent({ - address: eventOnNetwork.address.bech32(), - identifier: eventOnNetwork.identifier, - topics: eventOnNetwork.topics.map((topic) => Buffer.from(topic.hex(), "hex")), - dataItems: dataItems, - }); - } -} diff --git a/src/converters/transactionsConverters.spec.ts b/src/converters/transactionsConverters.spec.ts deleted file mode 100644 index ed143f248..000000000 --- a/src/converters/transactionsConverters.spec.ts +++ /dev/null @@ -1,222 +0,0 @@ -import { assert } from "chai"; -import { Address } from "../address"; -import { - ContractResultItem, - ContractResults, - TransactionEventData, - TransactionEventOnNetwork, - TransactionEventTopic, - TransactionLogsOnNetwork, - TransactionOnNetwork, -} from "../networkProviders"; -import { Transaction } from "../transaction"; -import { - SmartContractCallOutcome, - SmartContractResult, - TransactionEvent, - TransactionLogs, - TransactionOutcome, -} from "../transactionsOutcomeParsers/resources"; -import { TransactionsConverter } from "./transactionsConverter"; - -describe("test transactions converter", async () => { - it("converts transaction to plain object and back", () => { - const converter = new TransactionsConverter(); - - const transaction = new Transaction({ - nonce: 90, - value: BigInt("123456789000000000000000000000"), - sender: "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", - receiver: "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx", - senderUsername: "alice", - receiverUsername: "bob", - gasPrice: 1000000000, - gasLimit: 80000, - data: Buffer.from("hello"), - chainID: "localnet", - version: 2, - }); - - const plainObject = converter.transactionToPlainObject(transaction); - const restoredTransaction = converter.plainObjectToTransaction(plainObject); - - assert.deepEqual(plainObject, transaction.toPlainObject()); - assert.deepEqual(restoredTransaction, Transaction.fromPlainObject(plainObject)); - assert.deepEqual(restoredTransaction, transaction); - assert.deepEqual(plainObject, { - nonce: 90, - value: "123456789000000000000000000000", - sender: "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", - receiver: "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx", - senderUsername: "YWxpY2U=", - receiverUsername: "Ym9i", - gasPrice: 1000000000, - gasLimit: 80000, - data: "aGVsbG8=", - chainID: "localnet", - version: 2, - options: undefined, - guardian: undefined, - signature: undefined, - relayer: undefined, - guardianSignature: undefined, - relayerSignature: undefined, - }); - }); - - it("converts transaction on network to transaction outcome", () => { - const converter = new TransactionsConverter(); - - const transactionOnNetwork = new TransactionOnNetwork({ - nonce: 7, - function: "hello", - logs: new TransactionLogsOnNetwork({ - address: Address.fromBech32("erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8"), - events: [ - new TransactionEventOnNetwork({ - identifier: "foobar", - topics: [], - dataPayload: new TransactionEventData(Buffer.from("foo")), - additionalData: [], - }), - ], - }), - contractResults: new ContractResults([ - new ContractResultItem({ - nonce: 8, - data: "@6f6b@2a", - logs: new TransactionLogsOnNetwork({ - address: Address.fromBech32("erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8"), - events: [ - new TransactionEventOnNetwork({ - identifier: "writeLog", - topics: [ - new TransactionEventTopic( - // '@too much gas provided for processing: gas provided = 596384500, gas used = 733010' - "QHRvbyBtdWNoIGdhcyBwcm92aWRlZCBmb3IgcHJvY2Vzc2luZzogZ2FzIHByb3ZpZGVkID0gNTk2Mzg0NTAwLCBnYXMgdXNlZCA9IDczMzAxMA==", - ), - ], - dataPayload: TransactionEventData.fromBase64("QDZmNmI="), - }), - ], - }), - }), - ]), - }); - - const actualTransactionOutcome = converter.transactionOnNetworkToOutcome(transactionOnNetwork); - const expectedTransactionOutcome = new TransactionOutcome({ - directSmartContractCallOutcome: new SmartContractCallOutcome({ - function: "hello", - returnCode: "ok", - returnMessage: "ok", - returnDataParts: [Buffer.from([42])], - }), - smartContractResults: [ - new SmartContractResult({ - sender: "", - receiver: "", - data: Buffer.from("@6f6b@2a"), - logs: { - address: "erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8", - events: [ - new TransactionEvent({ - address: "", - identifier: "writeLog", - topics: [ - Buffer.from( - "@too much gas provided for processing: gas provided = 596384500, gas used = 733010", - ), - ], - dataItems: [Buffer.from("QDZmNmI=", "base64")], - }), - ], - }, - }), - ], - logs: new TransactionLogs({ - address: "erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8", - events: [ - new TransactionEvent({ - address: "", - identifier: "foobar", - topics: [], - dataItems: [Buffer.from("foo")], - }), - ], - }), - }); - - assert.deepEqual(actualTransactionOutcome, expectedTransactionOutcome); - }); - - it("converts transaction on network to transaction outcome (with signal error)", () => { - const converter = new TransactionsConverter(); - - const transactionOnNetwork = new TransactionOnNetwork({ - nonce: 42, - function: "hello", - contractResults: new ContractResults([ - new ContractResultItem({ - nonce: 42, - data: "@657865637574696f6e206661696c6564", - logs: new TransactionLogsOnNetwork({ - address: Address.fromBech32("erd1qqqqqqqqqqqqqpgqj8k976l59n7fyth8ujl4as5uyn3twn0ha0wsge5r5x"), - events: [ - new TransactionEventOnNetwork({ - address: Address.fromBech32( - "erd1qqqqqqqqqqqqqpgqj8k976l59n7fyth8ujl4as5uyn3twn0ha0wsge5r5x", - ), - identifier: "signalError", - topics: [ - new TransactionEventTopic("XmC5/yOF6ie6DD2kaJd5qPc2Ss7h2w7nvuWaxmCiiXQ="), - new TransactionEventTopic("aW5zdWZmaWNpZW50IGZ1bmRz"), - ], - dataPayload: new TransactionEventData(Buffer.from("@657865637574696f6e206661696c6564")), - additionalData: [ - new TransactionEventData(Buffer.from("@657865637574696f6e206661696c6564")), - new TransactionEventData(Buffer.from("foobar")), - ], - }), - ], - }), - }), - ]), - }); - - const actualTransactionOutcome = converter.transactionOnNetworkToOutcome(transactionOnNetwork); - const expectedTransactionOutcome = new TransactionOutcome({ - directSmartContractCallOutcome: new SmartContractCallOutcome({ - function: "hello", - returnCode: "execution failed", - returnMessage: "execution failed", - returnDataParts: [], - }), - smartContractResults: [ - new SmartContractResult({ - sender: "", - receiver: "", - data: Buffer.from("@657865637574696f6e206661696c6564"), - logs: { - address: "erd1qqqqqqqqqqqqqpgqj8k976l59n7fyth8ujl4as5uyn3twn0ha0wsge5r5x", - events: [ - new TransactionEvent({ - address: "erd1qqqqqqqqqqqqqpgqj8k976l59n7fyth8ujl4as5uyn3twn0ha0wsge5r5x", - identifier: "signalError", - topics: [ - Address.fromBech32( - "erd1testnlersh4z0wsv8kjx39me4rmnvjkwu8dsaea7ukdvvc9z396qykv7z7", - ).getPublicKey(), - Buffer.from("insufficient funds"), - ], - dataItems: [Buffer.from("@657865637574696f6e206661696c6564"), Buffer.from("foobar")], - }), - ], - }, - }), - ], - }); - - assert.deepEqual(actualTransactionOutcome, expectedTransactionOutcome); - }); -}); diff --git a/src/address.spec.ts b/src/core/address.spec.ts similarity index 81% rename from src/address.spec.ts rename to src/core/address.spec.ts index 46552556a..e44da62f5 100644 --- a/src/address.spec.ts +++ b/src/core/address.spec.ts @@ -20,12 +20,12 @@ describe("test address", () => { }); it("should create address (custom hrp)", async () => { - let address = Address.fromHex(aliceHex, "test"); + let address = Address.newFromHex(aliceHex, "test"); assert.deepEqual(address.getPublicKey(), Buffer.from(aliceHex, "hex")); assert.equal(address.getHrp(), "test"); assert.equal(address.toBech32(), "test1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ss5hqhtr"); - address = Address.fromHex(bobHex, "xerd"); + address = Address.newFromHex(bobHex, "xerd"); assert.deepEqual(address.getPublicKey(), Buffer.from(bobHex, "hex")); assert.equal(address.getHrp(), "xerd"); assert.equal(address.toBech32(), "xerd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruq9thc9j"); @@ -34,8 +34,8 @@ describe("test address", () => { it("should create empty address", async () => { const nobody = Address.empty(); - assert.isEmpty(nobody.hex()); - assert.isEmpty(nobody.bech32()); + assert.isEmpty(nobody.toHex()); + assert.isEmpty(nobody.toBech32()); assert.deepEqual(nobody.toJSON(), { bech32: "", pubkey: "" }); }); @@ -73,19 +73,19 @@ describe("test address", () => { it("should check whether isSmartContract", () => { assert.isFalse( - Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th").isSmartContract(), + Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th").isSmartContract(), ); assert.isTrue( - Address.fromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l").isSmartContract(), + Address.newFromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l").isSmartContract(), ); assert.isTrue( - Address.fromBech32("erd1qqqqqqqqqqqqqpgqxwakt2g7u9atsnr03gqcgmhcv38pt7mkd94q6shuwt").isSmartContract(), + Address.newFromBech32("erd1qqqqqqqqqqqqqpgqxwakt2g7u9atsnr03gqcgmhcv38pt7mkd94q6shuwt").isSmartContract(), ); }); it("should contract address", () => { const addressComputer = new AddressComputer(); - const deployer = Address.fromBech32("erd1j0hxzs7dcyxw08c4k2nv9tfcaxmqy8rj59meq505w92064x0h40qcxh3ap"); + const deployer = Address.newFromBech32("erd1j0hxzs7dcyxw08c4k2nv9tfcaxmqy8rj59meq505w92064x0h40qcxh3ap"); let contractAddress = addressComputer.computeContractAddress(deployer, 0n); assert.equal(contractAddress.toHex(), "00000000000000000500bb652200ed1f994200ab6699462cab4b1af7b11ebd5e"); @@ -99,15 +99,15 @@ describe("test address", () => { it("should get address shard", () => { const addressComputer = new AddressComputer(); - let address = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + let address = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); let shard = addressComputer.getShardOfAddress(address); assert.equal(shard, 1); - address = Address.fromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + address = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); shard = addressComputer.getShardOfAddress(address); assert.equal(shard, 0); - address = Address.fromBech32("erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8"); + address = Address.newFromBech32("erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8"); shard = addressComputer.getShardOfAddress(address); assert.equal(shard, 2); }); diff --git a/src/address.ts b/src/core/address.ts similarity index 89% rename from src/address.ts rename to src/core/address.ts index 00241fee8..652ade8ef 100644 --- a/src/address.ts +++ b/src/core/address.ts @@ -1,9 +1,9 @@ import * as bech32 from "bech32"; import BigNumber from "bignumber.js"; +import { bigIntToBuffer } from "../tokenOperations/codec"; import { LibraryConfig } from "./config"; import { CURRENT_NUMBER_OF_SHARDS_WITHOUT_META, METACHAIN_ID, WasmVirtualMachine } from "./constants"; import * as errors from "./errors"; -import { bigIntToBuffer } from "./tokenOperations/codec"; const createKeccakHash = require("keccak"); /** @@ -13,11 +13,6 @@ const PUBKEY_LENGTH = 32; const SMART_CONTRACT_HEX_PUBKEY_PREFIX = "0".repeat(16); -interface IAddress { - getPublicKey(): Buffer; - getHrp(): string; -} - /** * An Address, as an immutable object. */ @@ -90,6 +85,7 @@ export class Address { } /** + * Named constructor * Creates an address object from a bech32-encoded string */ static newFromBech32(value: string): Address { @@ -98,7 +94,7 @@ export class Address { } /** - * Use {@link newFromBech32} instead. + * @deprecated Use {@link newFromBech32} instead. */ static fromBech32(value: string): Address { // On this legacy flow, we do not accept addresses with custom hrp (in order to avoid behavioral breaking changes). @@ -107,6 +103,7 @@ export class Address { } /** + * Named constructor * Creates an address object from a hex-encoded string */ static newFromHex(value: string, hrp?: string): Address { @@ -118,62 +115,18 @@ export class Address { } /** - * Use {@link newFromHex} instead. + * @deprecated Use {@link newFromHex} instead. */ static fromHex(value: string, hrp?: string): Address { return Address.newFromHex(value, hrp); } - /** - * @deprecated Constructing an address object from another object is deprecated. - */ - static fromAddress(address: Address): Address { - return new Address(address); - } - - /** - * @deprecated Use the constructor, instead. - */ - static fromBuffer(buffer: Buffer, hrp?: string): Address { - return new Address(buffer, hrp); - } - - /** - * @deprecated Use {@link newFromBech32} or {@link newFromHex}. - */ - static fromString(value: string, hrp?: string): Address { - return new Address(value, hrp); - } - private static isValidHex(value: string) { return Buffer.from(value, "hex").length == PUBKEY_LENGTH; } /** - * Creates an empty address object. - * Generally speaking, this should not be used by client code (internal use only). - */ - static empty(): Address { - return new Address(""); - } - - /** - * Performs address validation without throwing errors - */ - static isValid(value: string): boolean { - const decoded = bech32.decodeUnsafe(value); - const prefix = decoded?.prefix; - const pubkey = decoded ? Buffer.from(bech32.fromWords(decoded.words)) : undefined; - - if (prefix !== LibraryConfig.DefaultAddressHrp || pubkey?.length !== PUBKEY_LENGTH) { - return false; - } - - return true; - } - - /** - * Use {@link toHex} instead. + * @deprecated Use {@link toHex} instead. */ hex(): string { return this.toHex(); @@ -191,7 +144,7 @@ export class Address { } /** - * Use {@link toBech32} instead. + * @deprecated Use {@link toBech32} instead. */ bech32(): string { return this.toBech32(); @@ -205,20 +158,20 @@ export class Address { return ""; } - let words = bech32.toWords(this.pubkey()); + let words = bech32.toWords(this.getPublicKey()); let address = bech32.encode(this.hrp, words); return address; } /** - * Use {@link getPublicKey} instead. + * @deprecated Use {@link getPublicKey} instead. */ pubkey(): Buffer { return this.getPublicKey(); } /** - * Returns the pubkey as raw bytes (buffer) + * Returns the underlying public key. */ getPublicKey(): Buffer { return this.publicKey; @@ -231,6 +184,29 @@ export class Address { return this.hrp; } + /** + * Creates an empty address object. + * Generally speaking, this should not be used by client code (internal use only). + */ + static empty(): Address { + return new Address(""); + } + + /** + * Performs address validation without throwing errors + */ + static isValid(value: string): boolean { + const decoded = bech32.decodeUnsafe(value); + const prefix = decoded?.prefix; + const pubkey = decoded ? Buffer.from(bech32.fromWords(decoded.words)) : undefined; + + if (prefix !== LibraryConfig.DefaultAddressHrp || pubkey?.length !== PUBKEY_LENGTH) { + return false; + } + + return true; + } + /** * Returns whether the address is empty. */ @@ -275,7 +251,7 @@ export class Address { } /** - * Use {@link isSmartContract} instead. + * @deprecated Use {@link isSmartContract} instead. */ isContractAddress(): boolean { return this.isSmartContract(); @@ -296,7 +272,7 @@ export class AddressComputer { this.numberOfShardsWithoutMeta = numberOfShardsWithoutMeta || CURRENT_NUMBER_OF_SHARDS_WITHOUT_META; } - computeContractAddress(deployer: IAddress, deploymentNonce: bigint): Address { + computeContractAddress(deployer: Address, deploymentNonce: bigint): Address { const initialPadding = Buffer.alloc(8, 0); const ownerPubkey = deployer.getPublicKey(); const shardSelector = ownerPubkey.slice(30); @@ -314,7 +290,7 @@ export class AddressComputer { return new Address(addressBytes); } - getShardOfAddress(address: IAddress): number { + getShardOfAddress(address: Address): number { return this.getShardOfPubkey(address.getPublicKey(), this.numberOfShardsWithoutMeta); } diff --git a/src/asyncTimer.spec.ts b/src/core/asyncTimer.spec.ts similarity index 100% rename from src/asyncTimer.spec.ts rename to src/core/asyncTimer.spec.ts diff --git a/src/asyncTimer.ts b/src/core/asyncTimer.ts similarity index 100% rename from src/asyncTimer.ts rename to src/core/asyncTimer.ts diff --git a/src/core/baseController.ts b/src/core/baseController.ts new file mode 100644 index 000000000..56fc8cdfb --- /dev/null +++ b/src/core/baseController.ts @@ -0,0 +1,33 @@ +import { Address } from "./address"; +import { EXTRA_GAS_LIMIT_FOR_GUARDED_TRANSACTIONS, EXTRA_GAS_LIMIT_FOR_RELAYED_TRANSACTIONS } from "./constants"; +import { Transaction } from "./transaction"; + +export type BaseControllerInput = { + guardian?: Address; + relayer?: Address; + gasPrice?: bigint; + gasLimit?: bigint; +}; + +export class BaseController { + protected setTransactionGasOptions(transaction: Transaction, options: { gasLimit?: bigint; gasPrice?: bigint }) { + if (options.gasLimit) { + transaction.gasLimit = options.gasLimit; + } else { + this.addExtraGasLimitIfRequired(transaction); + } + if (options.gasPrice) { + transaction.gasPrice = options.gasPrice; + } + } + + protected addExtraGasLimitIfRequired(transaction: Transaction): void { + if (transaction.guardian && !transaction.guardian.isEmpty()) { + transaction.gasLimit += BigInt(EXTRA_GAS_LIMIT_FOR_GUARDED_TRANSACTIONS); + } + + if (transaction.relayer && !transaction.relayer.isEmpty()) { + transaction.gasLimit += BigInt(EXTRA_GAS_LIMIT_FOR_RELAYED_TRANSACTIONS); + } + } +} diff --git a/src/smartcontracts/codeMetadata.spec.ts b/src/core/codeMetadata.spec.ts similarity index 90% rename from src/smartcontracts/codeMetadata.spec.ts rename to src/core/codeMetadata.spec.ts index 7d184c4eb..a0a4a4282 100644 --- a/src/smartcontracts/codeMetadata.spec.ts +++ b/src/core/codeMetadata.spec.ts @@ -25,7 +25,7 @@ describe("CodeMetadata Class Tests", function () { it("should convert to buffer correctly", function () { const metadata = new CodeMetadata(true, true, true, true); - const buffer = metadata.toBuffer(); + const buffer = metadata.toBytes(); assert.equal(buffer.length, 2); assert.equal(buffer[0], CodeMetadata.ByteZero.Upgradeable | CodeMetadata.ByteZero.Readable); @@ -37,7 +37,7 @@ describe("CodeMetadata Class Tests", function () { CodeMetadata.ByteZero.Upgradeable | CodeMetadata.ByteZero.Readable, CodeMetadata.ByteOne.Payable | CodeMetadata.ByteOne.PayableBySc, ]); - const metadata = CodeMetadata.fromBuffer(buffer); + const metadata = CodeMetadata.newFromBytes(buffer); assert.isTrue(metadata.upgradeable); assert.isTrue(metadata.readable); @@ -47,7 +47,7 @@ describe("CodeMetadata Class Tests", function () { it("should create from buffer correctly when some flags are set", function () { const buffer = Buffer.from([CodeMetadata.ByteZero.Upgradeable, CodeMetadata.ByteOne.PayableBySc]); - const metadata = CodeMetadata.fromBuffer(buffer); + const metadata = CodeMetadata.newFromBytes(buffer); assert.isTrue(metadata.upgradeable); assert.isFalse(metadata.readable); @@ -60,7 +60,7 @@ describe("CodeMetadata Class Tests", function () { assert.throws( () => { - CodeMetadata.fromBuffer(buffer); + CodeMetadata.newFromBytes(buffer); }, Error, "code metadata buffer has length 1, expected 2", @@ -69,7 +69,7 @@ describe("CodeMetadata Class Tests", function () { it("should test code metadata from bytes", () => { const bytes = new Uint8Array([1, 0]); - const codeMetadata = CodeMetadata.fromBytes(bytes); + const codeMetadata = CodeMetadata.newFromBytes(bytes); assert.equal(codeMetadata.toString(), "0100"); assert.deepEqual(codeMetadata.toJSON(), { diff --git a/src/smartcontracts/codeMetadata.ts b/src/core/codeMetadata.ts similarity index 76% rename from src/smartcontracts/codeMetadata.ts rename to src/core/codeMetadata.ts index 7efff036a..77c682866 100644 --- a/src/smartcontracts/codeMetadata.ts +++ b/src/core/codeMetadata.ts @@ -41,20 +41,18 @@ export class CodeMetadata { this.payableBySc = payableBySc; } - static fromBytes(bytes: Uint8Array): CodeMetadata { - return CodeMetadata.fromBuffer(Buffer.from(bytes)); - } - /** + * Named constructor * Creates a metadata object from a buffer. + * Also checks that data has correct length (2 bytes) */ - static fromBuffer(buffer: Buffer): CodeMetadata { - if (buffer.length != CodeMetadataLength) { - throw new Error(`code metadata buffer has length ${buffer.length}, expected ${CodeMetadataLength}`); + static newFromBytes(bytes: Uint8Array): CodeMetadata { + if (bytes.length != CodeMetadataLength) { + throw new Error(`code metadata buffer has length ${bytes.length}, expected ${CodeMetadataLength}`); } - const byteZero = buffer[0]; - const byteOne = buffer[1]; + const byteZero = bytes[0]; + const byteOne = bytes[1]; const upgradeable = (byteZero & CodeMetadata.ByteZero.Upgradeable) !== 0; const readable = (byteZero & CodeMetadata.ByteZero.Readable) !== 0; @@ -65,35 +63,38 @@ export class CodeMetadata { } /** - * Adjust the metadata (the `upgradeable` attribute), when preparing the deployment transaction. + * @deprecated Use {@link newFromBytes} instead. + * Creates a metadata object from a buffer. */ - toggleUpgradeable(value: boolean) { - this.upgradeable = value; + static newFromBuffer(buffer: Buffer): CodeMetadata { + return this.newFromBytes(buffer); } /** - * Adjust the metadata (the `readable` attribute), when preparing the deployment transaction. + * Converts the metadata to the protocol-friendly representation. */ - toggleReadable(value: boolean) { - this.readable = value; - } + toBytes(): Uint8Array { + let byteZero = 0; + let byteOne = 0; - /** - * Adjust the metadata (the `payable` attribute), when preparing the deployment transaction. - */ - togglePayable(value: boolean) { - this.payable = value; - } + if (this.upgradeable) { + byteZero |= CodeMetadata.ByteZero.Upgradeable; + } + if (this.readable) { + byteZero |= CodeMetadata.ByteZero.Readable; + } + if (this.payable) { + byteOne |= CodeMetadata.ByteOne.Payable; + } + if (this.payableBySc) { + byteOne |= CodeMetadata.ByteOne.PayableBySc; + } - /** - * Adjust the metadata (the `payableBySc` attribute), when preparing the deployment transaction. - */ - togglePayableBySc(value: boolean) { - this.payableBySc = value; + return new Uint8Array(Buffer.from([byteZero, byteOne])); } /** - * Converts the metadata to the protocol-friendly representation. + * @deprecated Use {@link toBytes} instead. */ toBuffer(): Buffer { let byteZero = 0; @@ -119,7 +120,35 @@ export class CodeMetadata { * Converts the metadata to a hex-encoded string. */ toString() { - return this.toBuffer().toString("hex"); + return Buffer.from(this.toBytes()).toString("hex"); + } + + /** + * Adjust the metadata (the `upgradeable` attribute), when preparing the deployment transaction. + */ + toggleUpgradeable(value: boolean) { + this.upgradeable = value; + } + + /** + * Adjust the metadata (the `readable` attribute), when preparing the deployment transaction. + */ + toggleReadable(value: boolean) { + this.readable = value; + } + + /** + * Adjust the metadata (the `payable` attribute), when preparing the deployment transaction. + */ + togglePayable(value: boolean) { + this.payable = value; + } + + /** + * Adjust the metadata (the `payableBySc` attribute), when preparing the deployment transaction. + */ + togglePayableBySc(value: boolean) { + this.payableBySc = value; } /** diff --git a/src/compatibility.ts b/src/core/compatibility.ts similarity index 69% rename from src/compatibility.ts rename to src/core/compatibility.ts index 9caf1977a..25d584aa2 100644 --- a/src/compatibility.ts +++ b/src/core/compatibility.ts @@ -1,5 +1,4 @@ import { Address } from "./address"; -import { IAddress } from "./interface"; /** * For internal use only. @@ -8,12 +7,12 @@ export class Compatibility { /** * For internal use only. */ - static guardAddressIsSetAndNonZero(address: IAddress | undefined, context: string, resolution: string) { - if (!address || address.bech32() == "") { + static guardAddressIsSetAndNonZero(address: Address | undefined, context: string, resolution: string) { + if (!address || address.toBech32() == "") { console.warn( `${context}: address should be set; ${resolution}. In the future, this will throw an exception instead of emitting a WARN.`, ); - } else if (address.bech32() == Address.Zero().bech32()) { + } else if (address.toBech32() == Address.Zero().toBech32()) { console.warn( `${context}: address should not be the 'zero' address (also known as the 'contracts deployment address'); ${resolution}. In the future, this will throw an exception instead of emitting a WARN.`, ); diff --git a/src/config.ts b/src/core/config.ts similarity index 100% rename from src/config.ts rename to src/core/config.ts diff --git a/src/constants.ts b/src/core/constants.ts similarity index 67% rename from src/constants.ts rename to src/core/constants.ts index 9300d9780..765ed156c 100644 --- a/src/constants.ts +++ b/src/core/constants.ts @@ -26,28 +26,5 @@ export const SDK_JS_SIGNER = "sdk-js"; export const UNKNOWN_SIGNER = "unknown"; export const EGLD_IDENTIFIER_FOR_MULTI_ESDTNFT_TRANSFER = "EGLD-000000"; - -/** - * @deprecated - */ -export const DEFAULT_HRP = "erd"; - -/** - * @deprecated - */ -export const BECH32_ADDRESS_LENGTH = 62; - -/** - * @deprecated Use {@link CONTRACT_DEPLOY_ADDRESS_HEX} instead. - */ -export const CONTRACT_DEPLOY_ADDRESS = "erd1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6gq4hu"; - -/** - * @deprecated Use {@link DELEGATION_MANAGER_SC_ADDRESS_HEX} instead. - */ -export const DELEGATION_MANAGER_SC_ADDRESS = "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6"; - -/** - * @deprecated Use {@link 000000000000000000010000000000000000000000000000000000000002ffff} instead. - */ -export const ESDT_CONTRACT_ADDRESS = "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u"; +export const EXTRA_GAS_LIMIT_FOR_GUARDED_TRANSACTIONS = 50_000; +export const EXTRA_GAS_LIMIT_FOR_RELAYED_TRANSACTIONS = 50_000; diff --git a/src/errors.ts b/src/core/errors.ts similarity index 92% rename from src/errors.ts rename to src/core/errors.ts index a0a47f0ab..793d2c3f2 100644 --- a/src/errors.ts +++ b/src/core/errors.ts @@ -101,7 +101,7 @@ export class ErrAddressEmpty extends Err { } /** - * Signals an invalid value for {@link GasLimit} objects. + * Signals an invalid value for gas limit. */ export class ErrNotEnoughGas extends Err { public constructor(value: number) { @@ -110,7 +110,7 @@ export class ErrNotEnoughGas extends Err { } /** - * Signals an invalid value for {@link Nonce} objects. + * Signals an invalid value for nonce. */ export class ErrNonceInvalid extends Err { public constructor(value: number) { @@ -119,7 +119,7 @@ export class ErrNonceInvalid extends Err { } /** - * Signals an invalid value for {@link TransactionVersion} objects. + * Signals an invalid value for transaction version. */ export class ErrTransactionVersionInvalid extends Err { public constructor(value: number) { @@ -128,7 +128,7 @@ export class ErrTransactionVersionInvalid extends Err { } /** - * Signals an invalid value for {@link TransactionOptions} objects. + * Signals an invalid value for transaction options. */ export class ErrTransactionOptionsInvalid extends Err { public constructor(value: number) { @@ -299,33 +299,6 @@ export class ErrNotImplemented extends Err { } } -/** - * Signals invalid arguments when using the relayed v1 builder - */ -export class ErrInvalidRelayedV1BuilderArguments extends Err { - public constructor() { - super("invalid arguments for relayed v1 builder"); - } -} - -/** - * Signals invalid arguments when using the relayed v2 builder - */ -export class ErrInvalidRelayedV2BuilderArguments extends Err { - public constructor() { - super("invalid arguments for relayed v2 builder"); - } -} - -/** - * Signals that Gas Limit isn't 0 for an inner tx when using relayed v2 builder - */ -export class ErrGasLimitShouldBe0ForInnerTransaction extends Err { - public constructor() { - super("gas limit must be 0 for the inner transaction for relayed v2"); - } -} - /** * Signals that the `isCompleted` property is missing on the transaction obect and is needed for the Transaction Watcher */ @@ -448,3 +421,21 @@ export class ErrContractQuery extends Err { super(originalError.message.replace("executeQuery:", "")); } } + +/** + * Signals that the network provider provided is not valid + */ +export class ErrInvalidNetworkProviderKind extends Err { + public constructor() { + super("Invalid network provider kind. Choose between `api` and `proxy`."); + } +} + +/** + * Signals that the account condition was not reached + */ +export class ExpectedAccountConditionNotReachedError extends Err { + public constructor() { + super("The expected account condition was not reached."); + } +} diff --git a/src/globals.ts b/src/core/globals.ts similarity index 100% rename from src/globals.ts rename to src/core/globals.ts diff --git a/src/core/index.ts b/src/core/index.ts new file mode 100644 index 000000000..cc5f927df --- /dev/null +++ b/src/core/index.ts @@ -0,0 +1,24 @@ +require("./globals"); + +export * from "./address"; +export * from "./asyncTimer"; +export * from "./baseController"; +export * from "./codeMetadata"; +export * from "./config"; +export * from "./errors"; +export * from "./interfaces"; +export * from "./logger"; +export * from "./message"; +export * from "./networkParams"; +export * from "./smartContractQuery"; +export * from "./tokens"; +export * from "./transaction"; +export * from "./transactionComputer"; +export * from "./transactionEvents"; +export * from "./transactionLogs"; +export * from "./transactionOnNetwork"; +export * from "./transactionPayload"; +export * from "./transactionsFactoryConfig"; +export * from "./transactionStatus"; +export * from "./transactionWatcher"; +export * from "./utils"; diff --git a/src/core/interfaces.ts b/src/core/interfaces.ts new file mode 100644 index 000000000..ff273b6a8 --- /dev/null +++ b/src/core/interfaces.ts @@ -0,0 +1,45 @@ +import { Address, Message, Transaction, TransactionOnNetwork } from "."; + +export interface IAccount { + readonly address: Address; + + sign(data: Uint8Array): Promise; + signTransaction(transaction: Transaction): Promise; + verifyTransactionSignature(transaction: Transaction, signature: Uint8Array): Promise; + signMessage(message: Message): Promise; + verifyMessageSignature(message: Message, signature: Uint8Array): Promise; +} + +export interface ITransactionFetcher { + /** + * Fetches the state of a {@link Transaction}. + */ + getTransaction(txHash: string): Promise; +} + +export interface IPlainTransactionObject { + nonce: number; + value: string; + receiver: string; + sender: string; + receiverUsername?: string; + senderUsername?: string; + guardian?: string; + relayer?: string; + gasPrice: number; + gasLimit: number; + data?: string; + chainID: string; + version: number; + options?: number; + signature?: string; + guardianSignature?: string; + relayerSignature?: string; +} + +export interface INetworkConfig { + minGasLimit: bigint; + gasPerDataByte: bigint; + gasPriceModifier: number; + chainID: string; +} diff --git a/src/logger.ts b/src/core/logger.ts similarity index 100% rename from src/logger.ts rename to src/core/logger.ts diff --git a/src/message.spec.ts b/src/core/message.spec.ts similarity index 83% rename from src/message.spec.ts rename to src/core/message.spec.ts index c7809d835..8f81c3466 100644 --- a/src/message.spec.ts +++ b/src/core/message.spec.ts @@ -1,15 +1,15 @@ -import { UserVerifier } from "./wallet"; import { assert } from "chai"; +import { Account } from "../accounts"; +import { getTestWalletsPath } from "../testutils/utils"; import { DEFAULT_MESSAGE_VERSION, SDK_JS_SIGNER, UNKNOWN_SIGNER } from "./constants"; import { Message, MessageComputer } from "./message"; -import { TestWallet, loadTestWallets } from "./testutils"; describe("test message", () => { - let alice: TestWallet; + let alice: Account; const messageComputer = new MessageComputer(); before(async function () { - ({ alice } = await loadTestWallets()); + alice = await Account.newFromPem(`${getTestWalletsPath()}/alice.pem`); }); it("should test message compute bytes for signing", async () => { @@ -32,10 +32,10 @@ describe("test message", () => { const message = new Message({ data: data, - address: alice.getAddress(), + address: alice.address, }); - message.signature = await alice.signer.sign(messageComputer.computeBytesForSigning(message)); + message.signature = await alice.signMessage(message); assert.equal( Buffer.from(message.signature).toString("hex"), @@ -53,18 +53,14 @@ describe("test message", () => { }); const unpackedMessage = messageComputer.unpackMessage(packedMessage); - assert.deepEqual(unpackedMessage.address, alice.getAddress()); + assert.deepEqual(unpackedMessage.address, alice.address); assert.deepEqual(unpackedMessage.data, message.data); assert.deepEqual(unpackedMessage.signature, message.signature); assert.deepEqual(unpackedMessage.version, message.version); assert.deepEqual(unpackedMessage.signer, message.signer); - const verifier = UserVerifier.fromAddress(alice.getAddress()); - const isValid = verifier.verify( - Buffer.from(messageComputer.computeBytesForVerifying(unpackedMessage)), - Buffer.from(unpackedMessage.signature!), - ); - assert.equal(isValid, true); + const isValid = await alice.verifyMessageSignature(unpackedMessage, Buffer.from(unpackedMessage.signature!)); + assert.isTrue(isValid); }); it("should unpack legacy message", async () => { @@ -78,7 +74,7 @@ describe("test message", () => { }; const message = messageComputer.unpackMessage(legacyMessage); - assert.deepEqual(message.address, alice.getAddress()); + assert.deepEqual(message.address, alice.address); assert.deepEqual(Buffer.from(message.data).toString(), "this is a test message"); assert.deepEqual( Buffer.from(message.signature!).toString("hex"), @@ -97,7 +93,7 @@ describe("test message", () => { }; const message = messageComputer.unpackMessage(packedMessage); - assert.deepEqual(message.address, alice.getAddress()); + assert.deepEqual(message.address, alice.address); assert.deepEqual(Buffer.from(message.data).toString(), "this is a test message"); assert.deepEqual( Buffer.from(message.signature!).toString("hex"), diff --git a/src/message.ts b/src/core/message.ts similarity index 88% rename from src/message.ts rename to src/core/message.ts index 05c047abf..fb291dd77 100644 --- a/src/message.ts +++ b/src/core/message.ts @@ -1,6 +1,5 @@ -import { IAddress } from "./interface"; -import { DEFAULT_MESSAGE_VERSION, MESSAGE_PREFIX, SDK_JS_SIGNER, UNKNOWN_SIGNER } from "./constants"; import { Address } from "./address"; +import { DEFAULT_MESSAGE_VERSION, MESSAGE_PREFIX, SDK_JS_SIGNER, UNKNOWN_SIGNER } from "./constants"; const createKeccakHash = require("keccak"); @@ -16,7 +15,7 @@ export class Message { /** * Address of the wallet that performed the signing operation. */ - public address?: IAddress; + public address?: Address; /** * Number representing the message version. */ @@ -29,7 +28,7 @@ export class Message { constructor(options: { data: Uint8Array; signature?: Uint8Array; - address?: IAddress; + address?: Address; version?: number; signer?: string; }) { @@ -52,6 +51,9 @@ export class MessageComputer { return createKeccakHash("keccak256").update(bytesToHash).digest(); } + /** + * returns the result of `computeBytesForSigning` + */ computeBytesForVerifying(message: Message): Uint8Array { return this.computeBytesForSigning(message); } @@ -66,12 +68,16 @@ export class MessageComputer { return { message: Buffer.from(message.data).toString("hex"), signature: message.signature ? Buffer.from(message.signature).toString("hex") : "", - address: message.address ? message.address.bech32() : "", + address: message.address ? message.address.toBech32() : "", version: message.version, signer: message.signer, }; } + /** + * packedMessage should be the one obtained from calling `packMessage()` + * should treat both 'legacy message' and current message + */ unpackMessage(packedMessage: { message: string; signature?: string; @@ -87,7 +93,7 @@ export class MessageComputer { let address: Address | undefined = undefined; if (packedMessage.address) { - address = Address.fromBech32(packedMessage.address); + address = Address.newFromBech32(packedMessage.address); } const version = packedMessage.version || DEFAULT_MESSAGE_VERSION; diff --git a/src/networkParams.spec.ts b/src/core/networkParams.spec.ts similarity index 100% rename from src/networkParams.spec.ts rename to src/core/networkParams.spec.ts diff --git a/src/networkParams.ts b/src/core/networkParams.ts similarity index 100% rename from src/networkParams.ts rename to src/core/networkParams.ts diff --git a/src/reflection.ts b/src/core/reflection.ts similarity index 100% rename from src/reflection.ts rename to src/core/reflection.ts diff --git a/src/signature.ts b/src/core/signature.ts similarity index 100% rename from src/signature.ts rename to src/core/signature.ts diff --git a/src/core/smartContractQuery.ts b/src/core/smartContractQuery.ts new file mode 100644 index 000000000..1f0702d54 --- /dev/null +++ b/src/core/smartContractQuery.ts @@ -0,0 +1,58 @@ +import { Address } from "./address"; + +export class SmartContractQuery { + contract: Address; + caller?: Address; + value?: bigint; + function: string; + arguments?: Uint8Array[]; + + constructor(options: { + contract: Address; + caller?: Address; + value?: bigint; + function: string; + arguments?: Uint8Array[]; + }) { + this.contract = options.contract; + this.caller = options.caller; + this.value = options.value; + this.function = options.function; + this.arguments = options.arguments; + } +} + +export type SmartContractQueryInput = { + contract: Address; + caller?: Address; + value?: bigint; + function: string; + arguments: any[]; +}; + +export class SmartContractQueryResponse { + function: string; + returnCode: string; + returnMessage: string; + returnDataParts: Uint8Array[]; + + constructor(obj: { function: string; returnCode: string; returnMessage: string; returnDataParts: Uint8Array[] }) { + this.function = obj.function; + this.returnCode = obj.returnCode; + this.returnMessage = obj.returnMessage; + this.returnDataParts = obj.returnDataParts; + } + + static fromHttpResponse(payload: any, functionName: string): SmartContractQueryResponse { + let returnData = payload["returnData"] || payload["ReturnData"]; + let returnCode = payload["returnCode"] || payload["ReturnCode"]; + let returnMessage = payload["returnMessage"] || payload["ReturnMessage"]; + + return new SmartContractQueryResponse({ + returnDataParts: returnData?.map((item) => Buffer.from(item || "", "base64")), + returnCode: returnCode, + returnMessage: returnMessage, + function: functionName, + }); + } +} diff --git a/src/transactionsFactories/tokenTransfersDataBuilder.ts b/src/core/tokenTransfersDataBuilder.ts similarity index 81% rename from src/transactionsFactories/tokenTransfersDataBuilder.ts rename to src/core/tokenTransfersDataBuilder.ts index 45cd92162..3bdcf4e39 100644 --- a/src/transactionsFactories/tokenTransfersDataBuilder.ts +++ b/src/core/tokenTransfersDataBuilder.ts @@ -1,7 +1,6 @@ -import { IAddress } from "../interface"; -import { ArgSerializer } from "../smartcontracts/argSerializer"; -import { AddressValue, BigUIntValue, TokenIdentifierValue, TypedValue, U32Value } from "../smartcontracts/typesystem"; -import { TokenComputer, TokenTransfer } from "../tokens"; +import { AddressValue, ArgSerializer, BigUIntValue, TokenIdentifierValue, TypedValue, U32Value } from "../abi"; +import { Address } from "./address"; +import { TokenComputer, TokenTransfer } from "./tokens"; export class TokenTransfersDataBuilder { private tokenComputer: TokenComputer; @@ -21,7 +20,7 @@ export class TokenTransfersDataBuilder { return ["ESDTTransfer", ...args]; } - buildDataPartsForSingleESDTNFTTransfer(transfer: TokenTransfer, receiver: IAddress) { + buildDataPartsForSingleESDTNFTTransfer(transfer: TokenTransfer, receiver: Address) { const token = transfer.token; const identifier = this.tokenComputer.extractIdentifierFromExtendedIdentifier(token.identifier); @@ -35,7 +34,7 @@ export class TokenTransfersDataBuilder { return ["ESDTNFTTransfer", ...args]; } - buildDataPartsForMultiESDTNFTTransfer(receiver: IAddress, transfers: TokenTransfer[]) { + buildDataPartsForMultiESDTNFTTransfer(receiver: Address, transfers: TokenTransfer[]) { const argsTyped: TypedValue[] = [new AddressValue(receiver), new U32Value(transfers.length)]; for (const transfer of transfers) { diff --git a/src/tokens.spec.ts b/src/core/tokens.spec.ts similarity index 53% rename from src/tokens.spec.ts rename to src/core/tokens.spec.ts index aa2cdb205..08f75e446 100644 --- a/src/tokens.spec.ts +++ b/src/core/tokens.spec.ts @@ -61,7 +61,7 @@ describe("test tokens and token computer", async () => { const numericTokenTickerWithPrefixAndNonce = "t0-2065-65td7s"; identifier = tokenComputer.extractIdentifierFromExtendedIdentifier(numericTokenTickerWithPrefixAndNonce); - assert.equal(identifier, "t0-2065-65td7s") + assert.equal(identifier, "t0-2065-65td7s"); }); it("should fail if prefix longer than expected", async () => { @@ -73,71 +73,32 @@ describe("test tokens and token computer", async () => { }); }); -describe("test token transfer (legacy)", () => { - it("should work with EGLD", () => { - assert.equal(TokenTransfer.egldFromAmount("1").toString(), "1000000000000000000"); - assert.equal(TokenTransfer.egldFromAmount("10").toString(), "10000000000000000000"); - assert.equal(TokenTransfer.egldFromAmount("100").toString(), "100000000000000000000"); - assert.equal(TokenTransfer.egldFromAmount("1000").toString(), "1000000000000000000000"); - assert.equal(TokenTransfer.egldFromAmount("0.1").toString(), "100000000000000000"); - assert.equal(TokenTransfer.egldFromAmount("0.123456789").toString(), "123456789000000000"); - assert.equal(TokenTransfer.egldFromAmount("0.123456789123456789").toString(), "123456789123456789"); - assert.equal(TokenTransfer.egldFromAmount("0.123456789123456789777").toString(), "123456789123456789"); - assert.equal(TokenTransfer.egldFromAmount("0.123456789123456789777777888888").toString(), "123456789123456789"); - - assert.equal(TokenTransfer.egldFromAmount(0.1).toPrettyString(), "0.100000000000000000 EGLD"); - assert.equal(TokenTransfer.egldFromAmount(1).toPrettyString(), "1.000000000000000000 EGLD"); - assert.equal(TokenTransfer.egldFromAmount(10).toPrettyString(), "10.000000000000000000 EGLD"); - assert.equal(TokenTransfer.egldFromAmount(100).toPrettyString(), "100.000000000000000000 EGLD"); - assert.equal(TokenTransfer.egldFromAmount(1000).toPrettyString(), "1000.000000000000000000 EGLD"); - assert.equal(TokenTransfer.egldFromAmount("0.123456789").toPrettyString(), "0.123456789000000000 EGLD"); - assert.equal( - TokenTransfer.egldFromAmount("0.123456789123456789777777888888").toPrettyString(), - "0.123456789123456789 EGLD", - ); - - assert.equal(TokenTransfer.egldFromBigInteger("1").toString(), "1"); - assert.equal(TokenTransfer.egldFromBigInteger("1").toPrettyString(), "0.000000000000000001 EGLD"); - assert.isTrue(TokenTransfer.egldFromAmount("1").isEgld()); - }); - - it("should work with USDC (legacy)", () => { - const identifier = "USDC-c76f1f"; - const numDecimals = 6; - - assert.equal(TokenTransfer.fungibleFromAmount(identifier, "1", numDecimals).toString(), "1000000"); - assert.equal(TokenTransfer.fungibleFromAmount(identifier, "0.1", numDecimals).toString(), "100000"); - assert.equal(TokenTransfer.fungibleFromAmount(identifier, "0.123456789", numDecimals).toString(), "123456"); - assert.equal(TokenTransfer.fungibleFromBigInteger(identifier, "1000000", numDecimals).toString(), "1000000"); - assert.equal( - TokenTransfer.fungibleFromBigInteger(identifier, "1000000", numDecimals).toPrettyString(), - "1.000000 USDC-c76f1f", - ); - }); - - it("should work with MetaESDT (legacy)", () => { +describe("test token transfer", () => { + it("should work with custom token type", () => { const identifier = "MEXFARML-28d646"; - const numDecimals = 18; - const nonce = 12345678; - const transfer = TokenTransfer.metaEsdtFromAmount(identifier, nonce, "0.1", numDecimals); - - assert.equal(transfer.tokenIdentifier, identifier); - assert.equal(transfer.nonce, nonce); + const nonce = 12345678n; + const transfer = new TokenTransfer({ + token: new Token({ identifier, nonce }), + amount: BigInt(100000000000000000), + }); + + assert.equal(transfer.token.identifier, identifier); + assert.equal(transfer.token.nonce, nonce); assert.equal(transfer.toString(), "100000000000000000"); }); - it("should work with NFTs (legacy)", () => { + it("should work with NFTs", () => { const identifier = "TEST-38f249"; - const nonce = 1; - const transfer = TokenTransfer.nonFungible(identifier, nonce); + const nonce = 1n; + const transfer = new TokenTransfer({ token: new Token({ identifier, nonce }), amount: 1n }); assert.equal(transfer.tokenIdentifier, identifier); - assert.equal(transfer.nonce, nonce); - assert.equal(transfer.toPrettyString(), "1 TEST-38f249"); + assert.equal(transfer.token.nonce, nonce); + assert.equal(transfer.amount, 1n); }); it("should create TokenTransfer from native token amount", () => { - const transfer = TokenTransfer.newFromEgldAmount(1000000000000000000n); + const transfer = TokenTransfer.newFromNativeAmount(1000000000000000000n); assert.equal(transfer.token.identifier, "EGLD-000000"); assert.equal(transfer.token.nonce, 0n); diff --git a/src/tokens.ts b/src/core/tokens.ts similarity index 78% rename from src/tokens.ts rename to src/core/tokens.ts index 57f21bf16..e7567c50f 100644 --- a/src/tokens.ts +++ b/src/core/tokens.ts @@ -1,6 +1,7 @@ import BigNumber from "bignumber.js"; import { EGLD_IDENTIFIER_FOR_MULTI_ESDTNFT_TRANSFER } from "./constants"; import { ErrInvalidArgument, ErrInvalidTokenIdentifier } from "./errors"; +import { numberToPaddedHex } from "./utils.codec"; // Legacy constants: const EGLDTokenIdentifier = "EGLD"; @@ -17,7 +18,12 @@ interface ILegacyTokenTransferOptions { numDecimals?: number; } +export type TokenType = "NFT" | "SFT" | "META" | "FNG"; + export class Token { + /** + * E.g. "FOO-abcdef", "EGLD-000000". + */ readonly identifier: string; readonly nonce: bigint; @@ -32,22 +38,22 @@ export class TokenTransfer { readonly amount: bigint; /** - * Legacy field. Use "token.identifier" instead. + * @deprecated field. Use "token.identifier" instead. */ readonly tokenIdentifier: string; /** - * Legacy field. Use "token.nonce" instead. + * @deprecated field. Use "token.nonce" instead. */ readonly nonce: number; /** - * Legacy field. Use "amount" instead. + * @deprecated field. Use "amount" instead. */ readonly amountAsBigInteger: BigNumber; /** - * Legacy field. The number of decimals is not a concern of "sdk-core". + * @deprecated field. The number of decimals is not a concern of "sdk-core". * For formatting and parsing amounts, use "sdk-dapp" or "bignumber.js" directly. */ readonly numDecimals: number; @@ -85,7 +91,11 @@ export class TokenTransfer { } } - static newFromEgldAmount(amount: bigint): TokenTransfer { + /** * + * @param amount + * @returns @TokenTransfer from native token + */ + static newFromNativeAmount(amount: bigint): TokenTransfer { const token = new Token({ identifier: EGLD_IDENTIFIER_FOR_MULTI_ESDTNFT_TRANSFER }); return new TokenTransfer({ token, amount }); } @@ -95,7 +105,7 @@ export class TokenTransfer { } /** - * Legacy function. Use the constructor instead: new TokenTransfer({ token, amount }); + * @deprecated Use {@link newFromNativeAmount} instead. */ static egldFromAmount(amount: BigNumber.Value) { const amountAsBigInteger = new BigNumber(amount).shiftedBy(EGLDNumDecimals).decimalPlaces(0); @@ -103,7 +113,7 @@ export class TokenTransfer { } /** - * Legacy function. Use the constructor instead: new TokenTransfer({ token, amount }); + * @deprecated Use {@link newFromNativeAmount} instead. */ static egldFromBigInteger(amountAsBigInteger: BigNumber.Value) { return new TokenTransfer({ @@ -115,7 +125,7 @@ export class TokenTransfer { } /** - * Legacy function. Use the constructor instead: new TokenTransfer({ token, amount }); + * @deprecated Use the constructor instead: new TokenTransfer({ token, amount }); */ static fungibleFromAmount(tokenIdentifier: string, amount: BigNumber.Value, numDecimals: number) { const amountAsBigInteger = new BigNumber(amount).shiftedBy(numDecimals).decimalPlaces(0); @@ -123,7 +133,7 @@ export class TokenTransfer { } /** - * Legacy function. Use the constructor instead: new TokenTransfer({ token, amount }); + * @deprecated Use the constructor instead: new TokenTransfer({ token, amount }); */ static fungibleFromBigInteger( tokenIdentifier: string, @@ -139,7 +149,7 @@ export class TokenTransfer { } /** - * Legacy function. Use the constructor instead: new TokenTransfer({ token, amount }); + * @deprecated Use the constructor instead: new TokenTransfer({ token, amount }); */ static nonFungible(tokenIdentifier: string, nonce: number) { return new TokenTransfer({ @@ -151,7 +161,7 @@ export class TokenTransfer { } /** - * Legacy function. Use the constructor instead: new TokenTransfer({ token, amount }); + * @deprecated Use the constructor instead: new TokenTransfer({ token, amount }); */ static semiFungible(tokenIdentifier: string, nonce: number, quantity: number) { return new TokenTransfer({ @@ -163,7 +173,7 @@ export class TokenTransfer { } /** - * Legacy function. Use the constructor instead: new TokenTransfer({ token, amount }); + * @deprecated Use the constructor instead: new TokenTransfer({ token, amount }); */ static metaEsdtFromAmount(tokenIdentifier: string, nonce: number, amount: BigNumber.Value, numDecimals: number) { const amountAsBigInteger = new BigNumber(amount).shiftedBy(numDecimals).decimalPlaces(0); @@ -171,7 +181,7 @@ export class TokenTransfer { } /** - * Legacy function. Use the constructor instead: new TokenTransfer({ token, amount }); + * @deprecated Use the constructor instead: new TokenTransfer({ token, amount }); */ static metaEsdtFromBigInteger( tokenIdentifier: string, @@ -192,14 +202,14 @@ export class TokenTransfer { } /** - * Legacy function. Use the "amount" field instead. + * @deprecated Use the "amount" field instead. */ valueOf(): BigNumber { return new BigNumber(this.amount.toString()); } /** - * Legacy function. For formatting and parsing amounts, use "sdk-dapp" or "bignumber.js" directly. + * @deprecated For formatting and parsing amounts, use "sdk-dapp" or "bignumber.js" directly. */ toPrettyString(): string { return `${this.toAmount()} ${this.tokenIdentifier}`; @@ -210,7 +220,7 @@ export class TokenTransfer { } /** - * Legacy function. Within your code, don't mix native values (EGLD) and custom (ESDT) tokens. + * @deprecated Within your code, don't mix native values (EGLD) and custom (ESDT) tokens. * See "TransferTransactionsFactory.createTransactionForNativeTokenTransfer()" vs. "TransferTransactionsFactory.createTransactionForESDTTokenTransfer()". */ isEgld(): boolean { @@ -218,7 +228,7 @@ export class TokenTransfer { } /** - * Legacy function. Use "TokenComputer.isFungible(token)" instead. + * @deprecated Use "TokenComputer.isFungible(token)" instead. */ isFungible(): boolean { return this.token.nonce == 0n; @@ -229,10 +239,16 @@ export class TokenComputer { TOKEN_RANDOM_SEQUENCE_LENGTH = 6; constructor() {} + /** + * Returns token.nonce == 0 + */ isFungible(token: Token): boolean { return token.nonce === 0n; } + /** + * Given "FOO-abcdef-0a" returns 10. + */ extractNonceFromExtendedIdentifier(identifier: string): number { const parts = identifier.split("-"); @@ -249,6 +265,9 @@ export class TokenComputer { return decodeUnsignedNumber(Buffer.from(hexNonce, "hex")); } + /** + * Given "FOO-abcdef-0a" returns FOO-abcdef. + */ extractIdentifierFromExtendedIdentifier(identifier: string): string { const parts = identifier.split("-"); const { prefix, ticker, randomSequence } = this.splitIdentifierIntoComponents(parts); @@ -261,6 +280,40 @@ export class TokenComputer { return ticker + "-" + randomSequence; } + /** + * Given "FOO-abcdef-0a" returns FOO. + * Given "FOO-abcdef" returns FOO. + */ + extractTickerFromExtendedIdentifier(identifier: string): string { + const parts = identifier.split("-"); + const { prefix, ticker, randomSequence } = this.splitIdentifierIntoComponents(parts); + + this.validateExtendedIdentifier(prefix, ticker, randomSequence, parts); + if (prefix) { + this.checkLengthOfPrefix(prefix); + return prefix + "-" + ticker + "-" + randomSequence; + } + return ticker; + } + + computeExtendedIdentifier(token: Token): string { + const parts = token.identifier.split("-"); + const { prefix, ticker, randomSequence } = this.splitIdentifierIntoComponents(parts); + + this.validateExtendedIdentifier(prefix, ticker, randomSequence, parts); + + if (token.nonce < 0) { + throw new Error("The token nonce can't be less than 0"); + } + + if (token.nonce === 0n) { + return token.identifier; + } + + const nonceAsHex = numberToPaddedHex(token.nonce); + return `${token.identifier}-${nonceAsHex}`; + } + private validateExtendedIdentifier( prefix: string | null, ticker: string, @@ -294,10 +347,6 @@ export class TokenComputer { } } - private isLowercaseAlphanumeric(str: string): boolean { - return /^[a-z0-9]+$/.test(str); - } - private checkLengthOfRandomSequence(randomSequence: string): void { if (randomSequence.length !== this.TOKEN_RANDOM_SEQUENCE_LENGTH) { throw new ErrInvalidTokenIdentifier( @@ -335,17 +384,3 @@ export class TokenComputer { function decodeUnsignedNumber(arg: Buffer): number { return arg.readUIntBE(0, arg.length); } - -/** - * @deprecated use {@link TokenTransfer} instead. - */ -export class TokenPayment extends TokenTransfer { - constructor(tokenIdentifier: string, nonce: number, amountAsBigInteger: BigNumber.Value, numDecimals: number) { - super({ - tokenIdentifier, - nonce, - amountAsBigInteger, - numDecimals, - }); - } -} diff --git a/src/transaction.local.net.spec.ts b/src/core/transaction.local.net.spec.ts similarity index 54% rename from src/transaction.local.net.spec.ts rename to src/core/transaction.local.net.spec.ts index 15082da7d..1d67876bf 100644 --- a/src/transaction.local.net.spec.ts +++ b/src/core/transaction.local.net.spec.ts @@ -1,18 +1,19 @@ import BigNumber from "bignumber.js"; import { assert } from "chai"; +import { INetworkProvider } from "../networkProviders/interface"; +import { loadTestWallets, stringifyBigIntJSON, TestWallet } from "../testutils"; +import { createLocalnetProvider } from "../testutils/networkProviders"; +import { TransferTransactionsFactory } from "../transfers/transferTransactionsFactory"; import { Logger } from "./logger"; -import { loadTestWallets, TestWallet } from "./testutils"; -import { createLocalnetProvider, INetworkProvider } from "./testutils/networkProviders"; import { TokenTransfer } from "./tokens"; import { Transaction } from "./transaction"; import { TransactionComputer } from "./transactionComputer"; -import { TransactionPayload } from "./transactionPayload"; +import { TransactionsFactoryConfig } from "./transactionsFactoryConfig"; import { TransactionWatcher } from "./transactionWatcher"; -import { TransactionsFactoryConfig } from "./transactionsFactories/transactionsFactoryConfig"; -import { TransferTransactionsFactory } from "./transactionsFactories/transferTransactionsFactory"; describe("test transaction", function () { let alice: TestWallet, bob: TestWallet; + const transactionComputer = new TransactionComputer(); before(async function () { ({ alice, bob } = await loadTestWallets()); @@ -22,7 +23,7 @@ describe("test transaction", function () { return new TransactionWatcher( { getTransaction: async (hash: string) => { - return await provider.getTransaction(hash, true); + return await provider.getTransaction(hash); }, }, { timeoutMilliseconds: 100000 }, @@ -39,40 +40,42 @@ describe("test transaction", function () { let transactionOne = new Transaction({ sender: alice.address, receiver: bob.address, - value: TokenTransfer.egldFromAmount(42), - gasLimit: network.MinGasLimit, - chainID: network.ChainID, + value: TokenTransfer.newFromNativeAmount(42000000000000000000n).amount, + gasLimit: BigInt(network.minGasLimit), + chainID: network.chainID, }); let transactionTwo = new Transaction({ sender: alice.address, receiver: bob.address, - value: TokenTransfer.egldFromAmount(43), - gasLimit: network.MinGasLimit, - chainID: network.ChainID, + value: TokenTransfer.newFromNativeAmount(43000000000000000000n).amount, + gasLimit: BigInt(network.minGasLimit), + chainID: network.chainID, }); await alice.sync(provider); await bob.sync(provider); - let initialBalanceOfBob = new BigNumber(bob.account.balance.toString()); + let initialBalanceOfBob = new BigNumber((await bob.getBalance(provider)).toString()); - transactionOne.setNonce(alice.account.nonce); - alice.account.incrementNonce(); - transactionTwo.setNonce(alice.account.nonce); + transactionOne.nonce = alice.account.getNonceThenIncrement(); + transactionTwo.nonce = alice.account.nonce; await signTransaction({ transaction: transactionOne, wallet: alice }); await signTransaction({ transaction: transactionTwo, wallet: alice }); - await provider.sendTransaction(transactionOne); - await provider.sendTransaction(transactionTwo); + const hashOne = await provider.sendTransaction(transactionOne); + const hashTwo = await provider.sendTransaction(transactionTwo); - await watcher.awaitCompleted(transactionOne.getHash().hex()); - await watcher.awaitCompleted(transactionTwo.getHash().hex()); + await watcher.awaitCompleted(hashOne); + await watcher.awaitCompleted(hashTwo); await bob.sync(provider); - let newBalanceOfBob = new BigNumber(bob.account.balance.toString()); + let newBalanceOfBob = new BigNumber((await bob.getBalance(provider)).toString()); - assert.deepEqual(TokenTransfer.egldFromAmount(85).valueOf(), newBalanceOfBob.minus(initialBalanceOfBob)); + assert.deepEqual( + TokenTransfer.newFromNativeAmount(85000000000000000000n).amount, + BigInt(newBalanceOfBob.minus(initialBalanceOfBob).toString()), + ); }); it("should send transaction and wait for completion using the new proxy provider", async function () { @@ -86,24 +89,27 @@ describe("test transaction", function () { let transactionOne = new Transaction({ sender: alice.address, receiver: bob.address, - value: TokenTransfer.egldFromAmount(42), - gasLimit: network.MinGasLimit, - chainID: network.ChainID, + value: TokenTransfer.newFromNativeAmount(42n).amount, + gasLimit: BigInt(network.minGasLimit), + chainID: network.chainID, }); await alice.sync(provider); await bob.sync(provider); - let initialBalanceOfBob = new BigNumber(bob.account.balance.toString()); + let initialBalanceOfBob = new BigNumber((await bob.getBalance(provider)).toString()); - transactionOne.setNonce(alice.account.nonce); + transactionOne.nonce = alice.account.nonce; await signTransaction({ transaction: transactionOne, wallet: alice }); - await provider.sendTransaction(transactionOne); - await watcher.awaitCompleted(transactionOne.getHash().hex()); + const hashOne = await provider.sendTransaction(transactionOne); + await watcher.awaitCompleted(hashOne); await bob.sync(provider); - let newBalanceOfBob = new BigNumber(bob.account.balance.toString()); + let newBalanceOfBob = new BigNumber((await bob.getBalance(provider)).toString()); - assert.deepEqual(TokenTransfer.egldFromAmount(42).valueOf(), newBalanceOfBob.minus(initialBalanceOfBob)); + assert.deepEqual( + TokenTransfer.newFromNativeAmount(42n).amount, + BigInt(newBalanceOfBob.minus(initialBalanceOfBob).toString()), + ); }); it("should simulate transactions", async function () { @@ -115,30 +121,30 @@ describe("test transaction", function () { let transactionOne = new Transaction({ sender: alice.address, - data: new TransactionPayload("helloWorld"), - gasLimit: 70000, + data: Buffer.from("helloWorld"), + gasLimit: 70000n, receiver: alice.address, - value: TokenTransfer.egldFromAmount(1000), - chainID: network.ChainID, + value: TokenTransfer.newFromNativeAmount(1000n).amount, + chainID: network.chainID, }); let transactionTwo = new Transaction({ sender: alice.address, - data: new TransactionPayload("helloWorld"), - gasLimit: 70000, + data: Buffer.from("helloWorld"), + gasLimit: 70000n, receiver: alice.address, - value: TokenTransfer.egldFromAmount(1000000), - chainID: network.ChainID, + value: TokenTransfer.newFromNativeAmount(1000000n).amount, + chainID: network.chainID, }); - transactionOne.setNonce(alice.account.nonce); - transactionTwo.setNonce(alice.account.nonce); + transactionOne.nonce = alice.account.nonce; + transactionTwo.nonce = alice.account.nonce; await signTransaction({ transaction: transactionOne, wallet: alice }); await signTransaction({ transaction: transactionTwo, wallet: alice }); - Logger.trace(JSON.stringify(await provider.simulateTransaction(transactionOne), null, 4)); - Logger.trace(JSON.stringify(await provider.simulateTransaction(transactionTwo), null, 4)); + Logger.trace(stringifyBigIntJSON(await provider.simulateTransaction(transactionOne))); + Logger.trace(stringifyBigIntJSON(await provider.simulateTransaction(transactionTwo))); }); it("should create transaction using the TokenTransferFactory", async function () { @@ -149,38 +155,39 @@ describe("test transaction", function () { const network = await provider.getNetworkConfig(); - const config = new TransactionsFactoryConfig({ chainID: network.ChainID }); + const config = new TransactionsFactoryConfig({ chainID: network.chainID }); const factory = new TransferTransactionsFactory({ config: config }); await alice.sync(provider); await bob.sync(provider); - const initialBalanceOfBob = new BigNumber(bob.account.balance.toString()); + const initialBalanceOfBob = new BigNumber((await bob.getBalance(provider)).toString()); - const transaction = factory.createTransactionForNativeTokenTransfer({ - sender: alice.address, + const transaction = factory.createTransactionForNativeTokenTransfer(alice.address, { receiver: bob.address, nativeAmount: 42000000000000000000n, }); transaction.nonce = BigInt(alice.account.nonce.valueOf()); - const transactionComputer = new TransactionComputer(); transaction.signature = await alice.signer.sign(transactionComputer.computeBytesForSigning(transaction)); const txHash = await provider.sendTransaction(transaction); await watcher.awaitCompleted(txHash); await bob.sync(provider); - const newBalanceOfBob = new BigNumber(bob.account.balance.toString()); + const newBalanceOfBob = new BigNumber((await bob.getBalance(provider)).toString()); - assert.deepEqual(TokenTransfer.egldFromAmount(42).valueOf(), newBalanceOfBob.minus(initialBalanceOfBob)); + assert.deepEqual( + TokenTransfer.newFromNativeAmount(42000000000000000000n).amount, + BigInt(newBalanceOfBob.minus(initialBalanceOfBob).toString()), + ); }); async function signTransaction(options: { transaction: Transaction; wallet: TestWallet }) { const transaction = options.transaction; const wallet = options.wallet; - const serialized = transaction.serializeForSigning(); + const serialized = transactionComputer.computeBytesForSigning(transaction); const signature = await wallet.signer.sign(serialized); - transaction.applySignature(signature); + transaction.signature = signature; } }); diff --git a/src/transaction.spec.ts b/src/core/transaction.spec.ts similarity index 54% rename from src/transaction.spec.ts rename to src/core/transaction.spec.ts index dfce314ea..ec54350ae 100644 --- a/src/transaction.spec.ts +++ b/src/core/transaction.spec.ts @@ -1,39 +1,41 @@ -import BigNumber from "bignumber.js"; +import { Buffer } from "buffer"; import { assert } from "chai"; +import { Account } from "../accounts"; +import { ProtoSerializer } from "../proto"; +import { getTestWalletsPath } from "../testutils/utils"; import { Address } from "./address"; -import { MIN_TRANSACTION_VERSION_THAT_SUPPORTS_OPTIONS } from "./constants"; -import { TransactionOptions, TransactionVersion } from "./networkParams"; -import { ProtoSerializer } from "./proto"; -import { TestWallet, loadTestWallets } from "./testutils"; -import { TokenTransfer } from "./tokens"; +import { MIN_TRANSACTION_VERSION_THAT_SUPPORTS_OPTIONS, TRANSACTION_OPTIONS_DEFAULT } from "./constants"; +import { INetworkConfig } from "./interfaces"; import { Transaction } from "./transaction"; import { TransactionComputer } from "./transactionComputer"; -import { TransactionPayload } from "./transactionPayload"; -import { UserPublicKey, UserVerifier } from "./wallet"; describe("test transaction", async () => { - let wallets: Record; + let alice: Account; + let bob: Account; + let carol: Account; const minGasLimit = 50000; const minGasPrice = 1000000000; const transactionComputer = new TransactionComputer(); - const networkConfig = { - MinGasLimit: 50000, - GasPerDataByte: 1500, - GasPriceModifier: 0.01, - ChainID: "D", + const networkConfig: INetworkConfig = { + minGasLimit: 50000n, + gasPerDataByte: 1500n, + gasPriceModifier: 0.01, + chainID: "D", }; before(async function () { - wallets = await loadTestWallets(); + alice = await Account.newFromPem(`${getTestWalletsPath()}/alice.pem`); + bob = await Account.newFromPem(`${getTestWalletsPath()}/bob.pem`); + carol = await Account.newFromPem(`${getTestWalletsPath()}/carol.pem`); }); it("should serialize transaction for signing (without data)", async () => { const transaction = new Transaction({ - chainID: networkConfig.ChainID, - sender: wallets.alice.address.bech32(), - receiver: wallets.bob.address.bech32(), + chainID: networkConfig.chainID, + sender: alice.address, + receiver: bob.address, gasLimit: 50000n, value: 0n, version: 2, @@ -51,9 +53,9 @@ describe("test transaction", async () => { it("should serialize transaction for signing (with data)", async () => { const transaction = new Transaction({ - chainID: networkConfig.ChainID, - sender: wallets.alice.address.bech32(), - receiver: wallets.bob.address.bech32(), + chainID: networkConfig.chainID, + sender: alice.address, + receiver: bob.address, gasLimit: 70000n, value: 1000000000000000000n, version: 2, @@ -72,47 +74,47 @@ describe("test transaction", async () => { it("should sign transaction (with no data, no value) (legacy)", async () => { const transaction = new Transaction({ - nonce: 89, - value: "0", - sender: wallets.alice.address, - receiver: wallets.bob.address, - gasPrice: minGasPrice, - gasLimit: minGasLimit, + nonce: 89n, + value: 0n, + sender: alice.address, + receiver: bob.address, + gasPrice: BigInt(minGasPrice), + gasLimit: BigInt(minGasLimit), chainID: "local-testnet", }); - transaction.applySignature(await wallets.alice.signer.sign(transaction.serializeForSigning())); + transaction.signature = await alice.signTransaction(transaction); assert.equal( - transaction.getSignature().toString("hex"), + Buffer.from(transaction.signature).toString("hex"), "3f08a1dd64fbb627d10b048e0b45b1390f29bb0e457762a2ccb710b029f299022a67a4b8e45cf62f4314afec2e56b5574c71e38df96cc41fae757b7ee5062503", ); assert.equal( - transaction.getHash().toString(), + transactionComputer.computeTransactionHash(transaction), "1359fb9d5b0b47ca9f3b4adce6e4a524fa74099dd4732743b9226774a4cb0ad8", ); }); it("should sign transaction (with data, no value) (legacy)", async () => { const transaction = new Transaction({ - nonce: 90, - value: "0", - sender: wallets.alice.address, - receiver: wallets.bob.address, - gasPrice: minGasPrice, - gasLimit: 80000, - data: new TransactionPayload("hello"), + nonce: 90n, + value: 0n, + sender: alice.address, + receiver: bob.address, + gasPrice: BigInt(minGasPrice), + gasLimit: 80000n, + data: Buffer.from("hello"), chainID: "local-testnet", }); - transaction.applySignature(await wallets.alice.signer.sign(transaction.serializeForSigning())); + transaction.signature = await alice.signTransaction(transaction); assert.equal( - transaction.getSignature().toString("hex"), + Buffer.from(transaction.signature).toString("hex"), "f9e8c1caf7f36b99e7e76ee1118bf71b55cde11a2356e2b3adf15f4ad711d2e1982469cbba7eb0afbf74e8a8f78e549b9410cd86eeaa88fcba62611ac9f6e30e", ); assert.equal( - transaction.getHash().toString(), + transactionComputer.computeTransactionHash(transaction), "10a2bd6f9c358d2c9645368081999efd2a4cc7f24bdfdd75e8f57485fd702001", ); }); @@ -120,8 +122,8 @@ describe("test transaction", async () => { it("should sign transaction (with usernames)", async () => { const transaction = new Transaction({ chainID: "T", - sender: wallets.carol.address.bech32(), - receiver: wallets.alice.address.bech32(), + sender: carol.address, + receiver: alice.address, gasLimit: 50000n, value: 1000000000000000000n, version: 2, @@ -130,9 +132,7 @@ describe("test transaction", async () => { receiverUsername: "alice", }); - transaction.signature = await wallets.carol.signer.sign( - transactionComputer.computeBytesForSigning(transaction), - ); + transaction.signature = await carol.signTransaction(transaction); assert.equal( Buffer.from(transaction.signature).toString("hex"), @@ -142,9 +142,9 @@ describe("test transaction", async () => { it("should compute hash", async () => { const transaction = new Transaction({ - chainID: networkConfig.ChainID, - sender: wallets.alice.address.bech32(), - receiver: wallets.alice.address.bech32(), + chainID: networkConfig.chainID, + sender: alice.address, + receiver: alice.address, gasLimit: 100000n, value: 1000000000000n, version: 2, @@ -159,17 +159,14 @@ describe("test transaction", async () => { const hash = transactionComputer.computeTransactionHash(transaction); - assert.equal( - Buffer.from(hash).toString("hex"), - "169b76b752b220a76a93aeebc462a1192db1dc2ec9d17e6b4d7b0dcc91792f03", - ); + assert.equal(hash, "169b76b752b220a76a93aeebc462a1192db1dc2ec9d17e6b4d7b0dcc91792f03"); }); it("should compute hash (with usernames)", async () => { const transaction = new Transaction({ - chainID: networkConfig.ChainID, - sender: wallets.alice.address.bech32(), - receiver: wallets.alice.address.bech32(), + chainID: networkConfig.chainID, + sender: alice.address, + receiver: alice.address, gasLimit: 100000n, value: 1000000000000n, version: 2, @@ -186,187 +183,182 @@ describe("test transaction", async () => { const hash = transactionComputer.computeTransactionHash(transaction); - assert.equal( - Buffer.from(hash).toString("hex"), - "41b5acf7ebaf4a9165a64206b6ebc02021b3adda55ffb2a2698aac2e7004dc29", - ); + assert.equal(hash, "41b5acf7ebaf4a9165a64206b6ebc02021b3adda55ffb2a2698aac2e7004dc29"); }); it("should sign & compute hash (with data, with opaque, unused options) (legacy)", async () => { const transaction = new Transaction({ - nonce: 89, - value: "0", - sender: wallets.alice.address, - receiver: wallets.bob.address, - gasPrice: minGasPrice, - gasLimit: minGasLimit, + nonce: 89n, + value: 0n, + sender: alice.address, + receiver: bob.address, + gasPrice: BigInt(minGasPrice), + gasLimit: BigInt(minGasLimit), chainID: "local-testnet", // The protocol ignores the options when version == 1 - version: new TransactionVersion(1), - options: new TransactionOptions(1), + version: 1, + options: 1, }); assert.throws(() => { - transaction.serializeForSigning(); + transactionComputer.computeBytesForSigning(transaction); }, `Non-empty transaction options requires transaction version >= ${MIN_TRANSACTION_VERSION_THAT_SUPPORTS_OPTIONS}`); }); it("should sign & compute hash (with data, with value) (legacy)", async () => { const transaction = new Transaction({ - nonce: 91, - value: TokenTransfer.egldFromAmount(10), - sender: wallets.alice.address, - receiver: wallets.bob.address, - gasPrice: minGasPrice, - gasLimit: 100000, - data: new TransactionPayload("for the book"), + nonce: 91n, + value: 10000000000000000000n, + sender: alice.address, + receiver: bob.address, + gasPrice: BigInt(minGasPrice), + gasLimit: 100000n, + data: Buffer.from("for the book"), chainID: "local-testnet", }); - transaction.applySignature(await wallets.alice.signer.sign(transaction.serializeForSigning())); + transaction.signature = await alice.signTransaction(transaction); assert.equal( - transaction.getSignature().toString("hex"), + Buffer.from(transaction.signature).toString("hex"), "b45f22e9f57a6df22670fcc3566723a0711a05ac2547456de59fd222a54940e4a1d99bd414897ccbf5c02a842ad86e638989b7f4d30edd26c99a8cd1eb092304", ); assert.equal( - transaction.getHash().toString(), + transactionComputer.computeTransactionHash(transaction).toString(), "84125d7154d81a723642100bdf74e6df99f7c069c016d1e6bbeb408fd4e961bf", ); }); it("should sign & compute hash (with data, with large value) (legacy)", async () => { const transaction = new Transaction({ - nonce: 92, - value: TokenTransfer.egldFromBigInteger("123456789000000000000000000000"), - sender: wallets.alice.address, - receiver: wallets.bob.address, - gasPrice: minGasPrice, - gasLimit: 100000, - data: new TransactionPayload("for the spaceship"), + nonce: 92n, + value: BigInt("123456789000000000000000000000"), + sender: alice.address, + receiver: bob.address, + gasPrice: BigInt(minGasPrice), + gasLimit: 100000n, + data: Buffer.from("for the spaceship"), chainID: "local-testnet", }); - transaction.applySignature(await wallets.alice.signer.sign(transaction.serializeForSigning())); + transaction.signature = await alice.signTransaction(transaction); assert.equal( - transaction.getSignature().toString("hex"), + Buffer.from(transaction.signature).toString("hex"), "01f05aa8cb0614e12a94ab9dcbde5e78370a4e05d23ef25a1fb9d5fcf1cb3b1f33b919cd8dafb1704efb18fa233a8aa0d3344fb6ee9b613a7d7a403786ffbd0a", ); assert.equal( - transaction.getHash().toString(), + transactionComputer.computeTransactionHash(transaction), "321e1f1a0e3d06edade34fd0fdf3b4859e4328a73706a442c2439968a074113c", ); }); it("should sign & compute hash (with nonce = 0) (legacy)", async () => { const transaction = new Transaction({ - nonce: 0, - value: 0, - sender: wallets.alice.address, - receiver: wallets.bob.address, - gasPrice: minGasPrice, - gasLimit: 80000, - data: new TransactionPayload("hello"), + nonce: 0n, + value: 0n, + sender: alice.address, + receiver: bob.address, + gasPrice: BigInt(minGasPrice), + gasLimit: 80000n, + data: Buffer.from("hello"), chainID: "local-testnet", - version: new TransactionVersion(1), + version: 1, }); - transaction.applySignature(await wallets.alice.signer.sign(transaction.serializeForSigning())); + transaction.signature = await alice.signTransaction(transaction); assert.equal( - transaction.getSignature().toString("hex"), + Buffer.from(transaction.signature).toString("hex"), "dfa3e9f2fdec60dcb353bac3b3435b4a2ff251e7e98eaf8620f46c731fc70c8ba5615fd4e208b05e75fe0f7dc44b7a99567e29f94fcd91efac7e67b182cd2a04", ); assert.equal( - transaction.getHash().toString(), + transactionComputer.computeTransactionHash(transaction), "6ffa1a75f98aaf336bfb87ef13b9b5a477a017158285d34ee2a503668767e69e", ); }); it("should sign & compute hash (without options field, should be omitted) (legacy)", async () => { const transaction = new Transaction({ - nonce: 89, - value: 0, - sender: wallets.alice.address, - receiver: wallets.bob.address, - gasPrice: minGasPrice, - gasLimit: minGasLimit, + nonce: 89n, + value: 0n, + sender: alice.address, + receiver: bob.address, + gasPrice: BigInt(minGasPrice), + gasLimit: BigInt(minGasLimit), chainID: "local-testnet", }); - transaction.applySignature(await wallets.alice.signer.sign(transaction.serializeForSigning())); + transaction.signature = await alice.signTransaction(transaction); assert.equal( - transaction.getSignature().toString("hex"), + Buffer.from(transaction.signature).toString("hex"), "3f08a1dd64fbb627d10b048e0b45b1390f29bb0e457762a2ccb710b029f299022a67a4b8e45cf62f4314afec2e56b5574c71e38df96cc41fae757b7ee5062503", ); assert.equal( - transaction.getHash().toString(), + transactionComputer.computeTransactionHash(transaction), "1359fb9d5b0b47ca9f3b4adce6e4a524fa74099dd4732743b9226774a4cb0ad8", ); - const result = transaction.serializeForSigning(); + const result = transactionComputer.computeBytesForSigning(transaction); assert.isFalse(result.toString().includes("options")); }); it("should sign & compute hash (with guardian field, should be omitted) (legacy)", async () => { const transaction = new Transaction({ - nonce: 89, - value: 0, - sender: wallets.alice.address, - receiver: wallets.bob.address, - gasPrice: minGasPrice, - gasLimit: minGasLimit, + nonce: 89n, + value: 0n, + sender: alice.address, + receiver: bob.address, + gasPrice: BigInt(minGasPrice), + gasLimit: BigInt(minGasLimit), chainID: "local-testnet", }); - transaction.applySignature(await wallets.alice.signer.sign(transaction.serializeForSigning())); + transaction.signature = await alice.signTransaction(transaction); assert.equal( - transaction.getSignature().toString("hex"), + Buffer.from(transaction.signature).toString("hex"), "3f08a1dd64fbb627d10b048e0b45b1390f29bb0e457762a2ccb710b029f299022a67a4b8e45cf62f4314afec2e56b5574c71e38df96cc41fae757b7ee5062503", ); assert.equal( - transaction.getHash().toString(), + transactionComputer.computeTransactionHash(transaction), "1359fb9d5b0b47ca9f3b4adce6e4a524fa74099dd4732743b9226774a4cb0ad8", ); - const result = transaction.serializeForSigning(); + const result = transactionComputer.computeBytesForSigning(transaction); assert.isFalse(result.toString().includes("options")); }); it("should sign & compute hash (with usernames) (legacy)", async () => { const transaction = new Transaction({ - nonce: 204, - value: "1000000000000000000", - sender: Address.fromBech32("erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8"), - receiver: Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + nonce: 204n, + value: 1000000000000000000n, + sender: Address.newFromBech32("erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8"), + receiver: Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), senderUsername: "carol", receiverUsername: "alice", - gasLimit: 50000, + gasLimit: 50000n, chainID: "T", }); - transaction.applySignature(await wallets.carol.signer.sign(transaction.serializeForSigning())); + transaction.signature = await carol.signTransaction(transaction); assert.equal( - transaction.getSignature().toString("hex"), + Buffer.from(transaction.signature).toString("hex"), "51e6cd78fb3ab4b53ff7ad6864df27cb4a56d70603332869d47a5cf6ea977c30e696103e41e8dddf2582996ad335229fdf4acb726564dbc1a0bc9e705b511f06", ); assert.equal( - transaction.getHash().toString(), + transactionComputer.computeTransactionHash(transaction), "edc84d776bfd655ddbd6fce24a83e379496ac47890d00be9c8bb2c6666fa3fd8", ); }); it("should sign & compute hash (guarded transaction)", async () => { - const alice = wallets.alice; - const transaction = new Transaction({ chainID: "local-testnet", - sender: alice.address.bech32(), - receiver: wallets.bob.address.bech32(), + sender: alice.address, + receiver: bob.address, gasLimit: 150000n, gasPrice: 1000000000n, data: new Uint8Array(Buffer.from("test data field")), @@ -374,10 +366,10 @@ describe("test transaction", async () => { options: 2, nonce: 92n, value: 123456789000000000000000000000n, - guardian: "erd1x23lzn8483xs2su4fak0r0dqx6w38enpmmqf2yrkylwq7mfnvyhsxqw57y", + guardian: Address.newFromBech32("erd1x23lzn8483xs2su4fak0r0dqx6w38enpmmqf2yrkylwq7mfnvyhsxqw57y"), }); transaction.guardianSignature = new Uint8Array(64); - transaction.signature = await alice.signer.sign(transactionComputer.computeBytesForSigning(transaction)); + transaction.signature = await alice.signTransaction(transaction); const serializer = new ProtoSerializer(); const buffer = serializer.serializeTransaction(transaction); @@ -388,34 +380,31 @@ describe("test transaction", async () => { ); const txHash = transactionComputer.computeTransactionHash(transaction); - assert.equal( - Buffer.from(txHash).toString("hex"), - "242022e9dcfa0ee1d8199b0043314dbda8601619f70069ebc441b9f03349a35c", - ); + assert.equal(txHash, "242022e9dcfa0ee1d8199b0043314dbda8601619f70069ebc441b9f03349a35c"); }); it("computes fee (legacy)", () => { const transaction = new Transaction({ - nonce: 92, - value: TokenTransfer.egldFromBigInteger("123456789000000000000000000000"), - sender: wallets.alice.address, - receiver: wallets.bob.address, - gasPrice: minGasPrice, - gasLimit: minGasLimit, + nonce: 92n, + value: BigInt("123456789000000000000000000000"), + sender: alice.address, + receiver: bob.address, + gasPrice: BigInt(minGasPrice), + gasLimit: BigInt(minGasLimit), chainID: "local-testnet", }); - const fee = transaction.computeFee(networkConfig); + const fee = transactionComputer.computeTransactionFee(transaction, networkConfig); assert.equal(fee.toString(), "50000000000000"); }); it("computes fee", async () => { const transaction = new Transaction({ chainID: "D", - sender: wallets.alice.address.bech32(), - receiver: wallets.alice.address.bech32(), + sender: alice.address, + receiver: alice.address, gasLimit: 50000n, - gasPrice: minGasPrice, + gasPrice: BigInt(minGasPrice), }); const gasLimit = transactionComputer.computeTransactionFee(transaction, networkConfig); @@ -424,9 +413,9 @@ describe("test transaction", async () => { it("computes fee, but should throw `NotEnoughGas` error", async () => { const transaction = new Transaction({ - chainID: networkConfig.ChainID, - sender: wallets.alice.address.bech32(), - receiver: wallets.alice.address.bech32(), + chainID: networkConfig.chainID, + sender: alice.address, + receiver: alice.address, gasLimit: 50000n, data: Buffer.from("toolittlegaslimit"), }); @@ -438,27 +427,27 @@ describe("test transaction", async () => { it("computes fee (with data field) (legacy)", () => { let transaction = new Transaction({ - nonce: 92, - value: TokenTransfer.egldFromBigInteger("123456789000000000000000000000"), - sender: wallets.alice.address, - receiver: wallets.bob.address, - data: new TransactionPayload("testdata"), - gasPrice: minGasPrice, - gasLimit: minGasLimit + 12010, + nonce: 92n, + value: BigInt("123456789000000000000000000000"), + sender: alice.address, + receiver: bob.address, + data: Buffer.from("testdata"), + gasPrice: BigInt(minGasPrice), + gasLimit: BigInt(minGasLimit + 12010), chainID: "local-testnet", }); - let fee = transaction.computeFee(networkConfig); + let fee = transactionComputer.computeTransactionFee(transaction, networkConfig); assert.equal(fee.toString(), "62000100000000"); }); it("computes fee (with data field)", async () => { const transaction = new Transaction({ - chainID: networkConfig.ChainID, - sender: wallets.alice.address.bech32(), - receiver: wallets.alice.address.bech32(), + chainID: networkConfig.chainID, + sender: alice.address, + receiver: alice.address, gasLimit: 50000n + 12010n, - gasPrice: minGasPrice, + gasPrice: BigInt(minGasPrice), data: Buffer.from("testdata"), }); @@ -467,144 +456,141 @@ describe("test transaction", async () => { }); it("should convert transaction to plain object and back", () => { - const sender = wallets.alice.address; + const sender = alice.address; const transaction = new Transaction({ - nonce: 90, - value: "123456789000000000000000000000", + nonce: 90n, + value: 123456789000000000000000000000n, sender: sender, - receiver: wallets.bob.address, + receiver: bob.address, senderUsername: "alice", receiverUsername: "bob", - gasPrice: minGasPrice, - gasLimit: 80000, - data: new TransactionPayload("hello"), + gasPrice: BigInt(minGasPrice), + gasLimit: 80000n, + data: Buffer.from("hello"), chainID: "local-testnet", }); const plainObject = transaction.toPlainObject(); - const restoredTransaction = Transaction.fromPlainObject(plainObject); + const restoredTransaction = Transaction.newFromPlainObject(plainObject); assert.deepEqual(restoredTransaction, transaction); }); it("should handle large values", () => { const tx1 = new Transaction({ - value: "123456789000000000000000000000", - sender: wallets.alice.address, - receiver: wallets.bob.address, - gasLimit: 50000, + value: 123456789000000000000000000000n, + sender: alice.address, + receiver: bob.address, + gasLimit: 50000n, chainID: "local-testnet", }); - assert.equal(tx1.getValue().toString(), "123456789000000000000000000000"); + assert.equal(tx1.value.toString(), "123456789000000000000000000000"); const tx2 = new Transaction({ - value: TokenTransfer.egldFromBigInteger("123456789000000000000000000000"), - sender: wallets.alice.address, - receiver: wallets.bob.address, - gasLimit: 50000, + value: 123456789000000000000000000000n, + sender: alice.address, + receiver: bob.address, + gasLimit: 50000n, chainID: "local-testnet", }); - assert.equal(tx2.getValue().toString(), "123456789000000000000000000000"); + assert.equal(tx2.value.toString(), "123456789000000000000000000000"); const tx3 = new Transaction({ - // Passing a BigNumber is not recommended. - // However, ITransactionValue interface is permissive, and developers may mistakenly pass such objects as values. - // TokenTransfer objects or simple strings (see above) are preferred, instead. - value: new BigNumber("123456789000000000000000000000"), - sender: wallets.alice.address, - receiver: wallets.bob.address, - gasLimit: 50000, + value: BigInt("123456789000000000000000000000"), + sender: alice.address, + receiver: bob.address, + gasLimit: 50000n, chainID: "local-testnet", }); - assert.equal(tx3.getValue().toString(), "123456789000000000000000000000"); + assert.equal(tx3.value.toString(), "123456789000000000000000000000"); }); it("checks correctly the version and options of the transaction", async () => { let transaction = new Transaction({ - nonce: 90, - value: new BigNumber("1000000000000000000"), - sender: wallets.alice.address, - receiver: wallets.bob.address, - gasPrice: minGasPrice, - gasLimit: 80000, - data: new TransactionPayload("hello"), + nonce: 90n, + value: BigInt("1000000000000000000"), + sender: alice.address, + receiver: bob.address, + gasPrice: BigInt(minGasPrice), + gasLimit: 80000n, + data: Buffer.from("hello"), chainID: "local-testnet", - version: new TransactionVersion(1), - options: TransactionOptions.withDefaultOptions(), + version: 1, + options: TRANSACTION_OPTIONS_DEFAULT, }); assert.isFalse(transaction.isGuardedTransaction()); transaction = new Transaction({ - nonce: 90, - value: new BigNumber("1000000000000000000"), - sender: wallets.alice.address, - receiver: wallets.bob.address, - gasPrice: minGasPrice, - gasLimit: 80000, - data: new TransactionPayload("hello"), + nonce: 90n, + value: BigInt("1000000000000000000"), + sender: alice.address, + receiver: bob.address, + gasPrice: BigInt(minGasPrice), + gasLimit: 80000n, + data: Buffer.from("hello"), chainID: "local-testnet", - version: new TransactionVersion(1), - options: TransactionOptions.withOptions({ guarded: true }), + version: 1, + options: 2, }); assert.isFalse(transaction.isGuardedTransaction()); transaction = new Transaction({ - nonce: 90, - value: new BigNumber("1000000000000000000"), - sender: wallets.alice.address, - receiver: wallets.bob.address, - gasPrice: minGasPrice, - gasLimit: 80000, - data: new TransactionPayload("hello"), + nonce: 90n, + value: BigInt("1000000000000000000"), + sender: alice.address, + receiver: bob.address, + gasPrice: BigInt(minGasPrice), + gasLimit: 80000n, + data: Buffer.from("hello"), chainID: "local-testnet", - version: new TransactionVersion(2), - options: TransactionOptions.withOptions({ guarded: true }), + version: 2, + options: 2, }); assert.isFalse(transaction.isGuardedTransaction()); transaction = new Transaction({ - nonce: 90, - value: new BigNumber("1000000000000000000"), - sender: wallets.alice.address, - receiver: wallets.bob.address, - gasPrice: minGasPrice, - gasLimit: 80000, - data: new TransactionPayload("hello"), + nonce: 90n, + value: BigInt("1000000000000000000"), + sender: alice.address, + receiver: bob.address, + gasPrice: BigInt(minGasPrice), + gasLimit: 80000n, + data: Buffer.from("hello"), chainID: "local-testnet", - version: new TransactionVersion(2), - options: TransactionOptions.withOptions({ guarded: true }), + version: 2, + options: 2, }); assert.isFalse(transaction.isGuardedTransaction()); transaction = new Transaction({ - nonce: 90, - value: new BigNumber("1000000000000000000"), - sender: wallets.alice.address, - receiver: wallets.bob.address, - gasPrice: minGasPrice, - guardian: wallets.bob.address, - gasLimit: 80000, - data: new TransactionPayload("hello"), + nonce: 90n, + value: BigInt("1000000000000000000"), + sender: alice.address, + receiver: bob.address, + gasPrice: BigInt(minGasPrice), + guardian: bob.address, + gasLimit: 80000n, + data: Buffer.from("hello"), chainID: "local-testnet", - version: new TransactionVersion(2), - options: TransactionOptions.withOptions({ guarded: true }), + version: 2, + options: 2, }); assert.isFalse(transaction.isGuardedTransaction()); transaction = new Transaction({ - nonce: 90, - value: new BigNumber("1000000000000000000"), - sender: wallets.alice.address, - receiver: wallets.bob.address, - gasPrice: minGasPrice, - guardian: wallets.bob.address, - gasLimit: 80000, - data: new TransactionPayload("hello"), + nonce: 90n, + value: BigInt("1000000000000000000"), + sender: alice.address, + receiver: bob.address, + gasPrice: BigInt(minGasPrice), + guardian: bob.address, + gasLimit: 80000n, + data: Buffer.from("hello"), chainID: "local-testnet", - version: new TransactionVersion(2), - options: TransactionOptions.withOptions({ guarded: true }), + version: 2, + options: 2, }); - transaction.applySignature(await wallets.alice.signer.sign(transaction.serializeForSigning())); - transaction.applyGuardianSignature(transaction.getSignature()); + transaction.signature = await alice.signTransaction(transaction); + transaction.guardianSignature = transaction.signature; assert.isTrue(transaction.isGuardedTransaction()); }); @@ -612,8 +598,8 @@ describe("test transaction", async () => { let transaction = new Transaction({ nonce: 89n, value: 0n, - sender: wallets.alice.address.toBech32(), - receiver: wallets.bob.address.toBech32(), + sender: alice.address, + receiver: bob.address, gasLimit: 50000n, gasPrice: 1000000000n, chainID: "integration tests chain ID", @@ -621,11 +607,11 @@ describe("test transaction", async () => { options: 1, }); - transaction.signature = await wallets.alice.signer.sign(transactionComputer.computeHashForSigning(transaction)); + transaction.signature = await alice.sign(transactionComputer.computeHashForSigning(transaction)); assert.equal( - "f0c81f2393b1ec5972c813f817bae8daa00ade91c6f75ea604ab6a4d2797aca4378d783023ff98f1a02717fe4f24240cdfba0b674ee9abb18042203d713bc70a", Buffer.from(transaction.signature).toString("hex"), + "f0c81f2393b1ec5972c813f817bae8daa00ade91c6f75ea604ab6a4d2797aca4378d783023ff98f1a02717fe4f24240cdfba0b674ee9abb18042203d713bc70a", ); }); @@ -633,25 +619,25 @@ describe("test transaction", async () => { let transaction = new Transaction({ nonce: 89n, value: 0n, - sender: wallets.alice.address.toBech32(), - receiver: wallets.bob.address.toBech32(), + sender: alice.address, + receiver: bob.address, gasLimit: 50000n, chainID: "localnet", }); - transactionComputer.applyGuardian(transaction, wallets.carol.address.toBech32()); + transactionComputer.applyGuardian(transaction, carol.address); assert.equal(transaction.version, 2); assert.equal(transaction.options, 2); - assert.equal(transaction.guardian, wallets.carol.address.toBech32()); + assert.equal(transaction.guardian, carol.address); }); it("should apply guardian with options set for hash signing", async () => { let transaction = new Transaction({ nonce: 89n, value: 0n, - sender: wallets.alice.address.toBech32(), - receiver: wallets.bob.address.toBech32(), + sender: alice.address, + receiver: bob.address, gasLimit: 50000n, chainID: "localnet", version: 1, @@ -661,20 +647,20 @@ describe("test transaction", async () => { assert.equal(transaction.version, 2); assert.equal(transaction.options, 1); - transactionComputer.applyGuardian(transaction, wallets.carol.address.toBech32()); + transactionComputer.applyGuardian(transaction, carol.address); assert.equal(transaction.version, 2); assert.equal(transaction.options, 3); }); it("should ensure transaction is valid", async () => { let transaction = new Transaction({ - sender: "invalidAddress", - receiver: wallets.bob.address.toBech32(), + sender: Address.empty(), + receiver: bob.address, gasLimit: 50000n, chainID: "", }); - transaction.sender = wallets.alice.address.toBech32(); + transaction.sender = alice.address; assert.throws(() => { transactionComputer.computeBytesForSigning(transaction); @@ -696,28 +682,18 @@ describe("test transaction", async () => { it("should compute bytes to verify transaction signature", async () => { let transaction = new Transaction({ - sender: wallets.alice.address.toBech32(), - receiver: wallets.bob.address.toBech32(), + sender: alice.address, + receiver: bob.address, gasLimit: 50000n, chainID: "D", nonce: 7n, }); - transaction.signature = await wallets.alice.signer.sign( - transactionComputer.computeBytesForSigning(transaction), - ); + transaction.signature = await alice.signTransaction(transaction); - const userVerifier = new UserVerifier(new UserPublicKey(wallets.alice.address.getPublicKey())); - const isSignedByAlice = userVerifier.verify( - transactionComputer.computeBytesForVerifying(transaction), - transaction.signature, - ); + const isSignedByAlice = await alice.verifyTransactionSignature(transaction, transaction.signature); - const wrongVerifier = new UserVerifier(new UserPublicKey(wallets.bob.address.getPublicKey())); - const isSignedByBob = wrongVerifier.verify( - transactionComputer.computeBytesForVerifying(transaction), - transaction.signature, - ); + const isSignedByBob = await bob.verifyTransactionSignature(transaction, transaction.signature); assert.equal(isSignedByAlice, true); assert.equal(isSignedByBob, false); @@ -725,8 +701,8 @@ describe("test transaction", async () => { it("should compute bytes to verify transaction signature (signed by hash)", async () => { let transaction = new Transaction({ - sender: wallets.alice.address.toBech32(), - receiver: wallets.bob.address.toBech32(), + sender: alice.address, + receiver: bob.address, gasLimit: 50000n, chainID: "D", nonce: 7n, @@ -734,30 +710,62 @@ describe("test transaction", async () => { transactionComputer.applyOptionsForHashSigning(transaction); - transaction.signature = await wallets.alice.signer.sign(transactionComputer.computeHashForSigning(transaction)); + transaction.signature = await alice.sign(transactionComputer.computeHashForSigning(transaction)); - const userVerifier = new UserVerifier(new UserPublicKey(wallets.alice.address.getPublicKey())); - const isSignedByAlice = userVerifier.verify( - transactionComputer.computeBytesForVerifying(transaction), - transaction.signature, - ); - - const wrongVerifier = new UserVerifier(new UserPublicKey(wallets.bob.address.getPublicKey())); - const isSignedByBob = wrongVerifier.verify( - transactionComputer.computeBytesForVerifying(transaction), - transaction.signature, - ); + const isSignedByAlice = await alice.verifyTransactionSignature(transaction, transaction.signature); + const isSignedByBob = await bob.verifyTransactionSignature(transaction, transaction.signature); assert.equal(isSignedByAlice, true); assert.equal(isSignedByBob, false); }); + it("converts transaction to plain object and back", () => { + const transaction = new Transaction({ + nonce: 90n, + value: BigInt("123456789000000000000000000000"), + sender: Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + receiver: Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"), + senderUsername: "alice", + receiverUsername: "bob", + gasPrice: 1000000000n, + gasLimit: 80000n, + data: Buffer.from("hello"), + chainID: "localnet", + version: 2, + }); + + const plainObject = transaction.toPlainObject(); + const restoredTransaction = Transaction.newFromPlainObject(plainObject); + + assert.deepEqual(plainObject, transaction.toPlainObject()); + assert.deepEqual(restoredTransaction, Transaction.newFromPlainObject(plainObject)); + assert.deepEqual(restoredTransaction, transaction); + assert.deepEqual(plainObject, { + nonce: 90, + value: "123456789000000000000000000000", + sender: "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + receiver: "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx", + senderUsername: "YWxpY2U=", + receiverUsername: "Ym9i", + gasPrice: 1000000000, + gasLimit: 80000, + data: "aGVsbG8=", + chainID: "localnet", + version: 2, + options: undefined, + guardian: undefined, + relayer: undefined, + signature: undefined, + guardianSignature: undefined, + relayerSignature: undefined, + }); + }); it("should serialize transaction with relayer", async () => { const transaction = new Transaction({ - chainID: networkConfig.ChainID, - sender: wallets.alice.address.toBech32(), - receiver: wallets.alice.address.toBech32(), - relayer: wallets.bob.address, + chainID: networkConfig.chainID, + sender: alice.address, + receiver: alice.address, + relayer: bob.address, gasLimit: 50000n, value: 0n, version: 2, @@ -775,9 +783,9 @@ describe("test transaction", async () => { it("should test relayed v3", async () => { const transaction = new Transaction({ - chainID: networkConfig.ChainID, - sender: wallets.alice.address.toBech32(), - receiver: wallets.alice.address.toBech32(), + chainID: networkConfig.chainID, + sender: alice.address, + receiver: alice.address, senderUsername: "alice", receiverUsername: "bob", gasLimit: 80000n, @@ -788,7 +796,7 @@ describe("test transaction", async () => { }); assert.isFalse(transactionComputer.isRelayedV3Transaction(transaction)); - transaction.relayer = wallets.carol.address; + transaction.relayer = carol.address; assert.isTrue(transactionComputer.isRelayedV3Transaction(transaction)); }); }); diff --git a/src/core/transaction.ts b/src/core/transaction.ts new file mode 100644 index 000000000..00de7a681 --- /dev/null +++ b/src/core/transaction.ts @@ -0,0 +1,464 @@ +import { BigNumber } from "bignumber.js"; +import { Address } from "./address"; +import { TRANSACTION_MIN_GAS_PRICE, TRANSACTION_OPTIONS_DEFAULT, TRANSACTION_VERSION_DEFAULT } from "./constants"; +import { INetworkConfig, IPlainTransactionObject } from "./interfaces"; +import { interpretSignatureAsBuffer } from "./signature"; +import { TransactionComputer } from "./transactionComputer"; + +/** + * An abstraction for creating and signing transactions. + */ +export class Transaction { + /** + * The nonce of the transaction (the account sequence number of the sender). + */ + public nonce: bigint; + + /** + * The value to transfer. + */ + public value: bigint; + + /** + * The address of the sender. + */ + public sender: Address; + + /** + * The address of the receiver. + */ + public receiver: Address; + + /** + * The username of the sender. + */ + public senderUsername: string; + + /** + * The username of the receiver. + */ + public receiverUsername: string; + + /** + * The gas price to be used. + */ + public gasPrice: bigint; + + /** + * The maximum amount of gas to be consumed when processing the transaction. + */ + public gasLimit: bigint; + + /** + * The payload of the transaction. + */ + public data: Uint8Array; + + /** + * The chain ID of the Network (e.g. "1" for Mainnet). + */ + public chainID: string; + + /** + * The version, required by the Network in order to correctly interpret the contents of the transaction. + */ + public version: number; + + /** + * The options field, useful for describing different settings available for transactions. + */ + public options: number; + + /** + * The address of the guardian, in bech32 format. + */ + public guardian: Address; + + /** + * The relayer address. + * Note: in the next major version, `sender`, `receiver` and `guardian` will also have the type `Address`, instead of `string`. + */ + public relayer: Address; + + /** + * The signature. + */ + public signature: Uint8Array; + + /** + * The signature of the guardian. + */ + public guardianSignature: Uint8Array; + + /** + * The signature of the relayer. + */ + public relayerSignature: Uint8Array; + + /** + * Creates a new Transaction object. + */ + public constructor(options: { + nonce?: bigint; + value?: bigint; + sender: Address; + receiver: Address; + senderUsername?: string; + receiverUsername?: string; + gasPrice?: bigint; + gasLimit: bigint; + data?: Uint8Array; + chainID: string; + version?: number; + options?: number; + guardian?: Address; + relayer?: Address; + signature?: Uint8Array; + guardianSignature?: Uint8Array; + relayerSignature?: Uint8Array; + }) { + this.nonce = options.nonce ?? 0n; + this.value = options.value ?? 0n; + this.sender = options.sender; + this.receiver = options.receiver; + this.senderUsername = options.senderUsername || ""; + this.receiverUsername = options.receiverUsername || ""; + this.gasPrice = options.gasPrice ?? BigInt(TRANSACTION_MIN_GAS_PRICE); + this.gasLimit = options.gasLimit; + this.data = options.data ?? new Uint8Array(); + this.chainID = options.chainID.valueOf(); + this.version = options.version ?? TRANSACTION_VERSION_DEFAULT; + this.options = options.options ?? TRANSACTION_OPTIONS_DEFAULT; + this.guardian = options.guardian ?? Address.empty(); + this.relayer = options.relayer ? options.relayer : Address.empty(); + + this.signature = options.signature || Buffer.from([]); + this.guardianSignature = options.guardianSignature || Buffer.from([]); + this.relayerSignature = options.relayerSignature || Buffer.from([]); + } + + /** + * @deprecated method, use {@link nonce} property instead. + */ + getNonce(): bigint { + return this.nonce; + } + + /** + * @deprecated method, use {@link nonce} property instead. + * Sets the account sequence number of the sender. Must be done prior signing. + */ + setNonce(nonce: bigint) { + this.nonce = nonce; + } + + /** + * @deprecated method, use {@link value} property instead. + */ + getValue(): bigint { + return this.value; + } + + /** + * @deprecated method, use {@link value} property instead. + */ + setValue(value: bigint) { + this.value = value; + } + + /** + * @deprecated method, use {@link sender} property instead. + */ + getSender(): Address { + return this.sender; + } + + /** + * @deprecated method, use {@link sender} property instead. + */ + setSender(sender: Address) { + this.sender = sender; + } + + /** + * @deprecated method, use {@link receiver} property instead. + */ + getReceiver(): Address { + return this.receiver; + } + + /** + * @deprecated method, use {@link senderUsername} property instead. + */ + getSenderUsername(): string { + return this.senderUsername; + } + + /** + * @deprecated method, use {@link senderUsername} property instead. + */ + setSenderUsername(senderUsername: string) { + this.senderUsername = senderUsername; + } + + /** + * @deprecated method, use {@link receiverUsername} property instead. + */ + getReceiverUsername(): string { + return this.receiverUsername; + } + + /** + * @deprecated method, use {@link receiverUsername} property instead. + */ + setReceiverUsername(receiverUsername: string) { + this.receiverUsername = receiverUsername; + } + + /** + * @deprecated method, use {@link guardian} property instead. + */ + getGuardian(): Address { + return this.guardian; + } + + /** + * @deprecated method, use {@link gasPrice} property instead. + */ + getGasPrice(): bigint { + return this.gasPrice; + } + + /** + * @deprecated method, use {@link gasPrice} property instead. + */ + setGasPrice(gasPrice: bigint) { + this.gasPrice = gasPrice; + } + + /** + * @deprecated method, use {@link gasLimit} property instead. + */ + getGasLimit(): bigint { + return this.gasLimit; + } + + /** + * @deprecated method, use {@link gasLimit} property instead. + */ + setGasLimit(gasLimit: bigint) { + this.gasLimit = gasLimit; + } + + /** + * @deprecated method, use {@link data} property instead. + */ + getData(): Uint8Array { + return this.data; + } + + /** + * @deprecated method, use {@link chainID} property instead. + */ + getChainID(): string { + return this.chainID; + } + + /** + * @deprecated method, use {@link chainID} property instead. + */ + setChainID(chainID: string) { + this.chainID = chainID; + } + + /** + * @deprecated method, use {@link version} property instead. + */ + getVersion(): number { + return this.version; + } + + /** + * @deprecated method, use {@link version} property instead. + */ + setVersion(version: number) { + this.version = version; + } + + /** + * @deprecated method, use {@link options} property instead. + */ + getOptions(): number { + return this.options; + } + + /** + * @deprecated method, use {@link options} property instead. + * + * Question for review: check how the options are set by sdk-dapp, wallet, ledger, extension. + */ + setOptions(options: number) { + this.options = options; + } + + /** + * @deprecated method, use{@link signature} property instead. + */ + getSignature(): Buffer { + return Buffer.from(this.signature); + } + + /** + * @deprecated method, use {@link guardianSignature} property instead. + */ + getGuardianSignature(): Buffer { + return Buffer.from(this.guardianSignature); + } + + /** + * @deprecated method, use {@link guardian} property instead. + */ + setGuardian(guardian: Address) { + this.guardian = guardian; + } + + /** + * @deprecated method, use "TransactionComputer.computeBytesForSigning()" instead. + * Serializes a transaction to a sequence of bytes, ready to be signed. + * This function is called internally by signers. + */ + serializeForSigning(): Buffer { + const computer = new TransactionComputer(); + const bytes = computer.computeBytesForSigning(this); + return Buffer.from(bytes); + } + + /** + * Checks the integrity of the guarded transaction + */ + isGuardedTransaction(): boolean { + const computer = new TransactionComputer(); + const hasGuardian = !this.guardian.isEmpty(); + const hasGuardianSignature = this.guardianSignature.length > 0; + return computer.hasOptionsSetForGuardedTransaction(this) && hasGuardian && hasGuardianSignature; + } + + /** + * Converts the transaction object into a ready-to-serialize, plain JavaScript object. + * This function is called internally within the signing procedure. + */ + toPlainObject(): IPlainTransactionObject { + const plainObject = { + nonce: Number(this.nonce), + value: this.value.toString(), + receiver: this.receiver.toBech32(), + sender: this.sender.toBech32(), + senderUsername: this.toBase64OrUndefined(this.senderUsername), + receiverUsername: this.toBase64OrUndefined(this.receiverUsername), + gasPrice: Number(this.gasPrice), + gasLimit: Number(this.gasLimit), + data: this.toBase64OrUndefined(this.data), + chainID: this.chainID.valueOf(), + version: this.version, + options: this.options == 0 ? undefined : this.options, + guardian: this.guardian.isEmpty() ? undefined : this.guardian.toBech32(), + relayer: this.relayer.isEmpty() ? undefined : this.relayer.toBech32(), + signature: this.toHexOrUndefined(this.signature), + guardianSignature: this.toHexOrUndefined(this.guardianSignature), + relayerSignature: this.toHexOrUndefined(this.relayerSignature), + }; + + return plainObject; + } + + /** + * @deprecated method, use {@link toPlainObject} instead. + * Converts a plain object transaction into a Transaction Object. + * + * @param plainObjectTransaction Raw data of a transaction, usually obtained by calling toPlainObject() + */ + static fromPlainObject(plainObjectTransaction: IPlainTransactionObject): Transaction { + return Transaction.newFromPlainObject(plainObjectTransaction); + } + + /** + * Converts a plain object transaction into a Transaction Object. + * + * @param plainObjectTransaction Raw data of a transaction, usually obtained by calling toPlainObject() + */ + static newFromPlainObject(plainObjectTransaction: IPlainTransactionObject): Transaction { + const transaction = new Transaction({ + nonce: BigInt(plainObjectTransaction.nonce), + value: BigInt(plainObjectTransaction.value || ""), + receiver: Address.newFromBech32(plainObjectTransaction.receiver), + receiverUsername: Buffer.from(plainObjectTransaction.receiverUsername || "", "base64").toString(), + sender: Address.newFromBech32(plainObjectTransaction.sender), + senderUsername: Buffer.from(plainObjectTransaction.senderUsername || "", "base64").toString(), + guardian: plainObjectTransaction.guardian + ? Address.newFromBech32(plainObjectTransaction.guardian) + : Address.empty(), + relayer: plainObjectTransaction.relayer + ? Address.newFromBech32(plainObjectTransaction.relayer) + : Address.empty(), + gasPrice: BigInt(plainObjectTransaction.gasPrice), + gasLimit: BigInt(plainObjectTransaction.gasLimit), + data: Buffer.from(plainObjectTransaction.data || "", "base64"), + chainID: String(plainObjectTransaction.chainID), + version: Number(plainObjectTransaction.version), + options: plainObjectTransaction.options ? Number(plainObjectTransaction.options) : undefined, + signature: Buffer.from(plainObjectTransaction.signature || "", "hex"), + guardianSignature: Buffer.from(plainObjectTransaction.guardianSignature || "", "hex"), + relayerSignature: Buffer.from(plainObjectTransaction.relayerSignature || "", "hex"), + }); + + return transaction; + } + + /** + * @deprecated method, use {@link signature} property instead. + * Applies the signature on the transaction. + * + * @param signature The signature, as computed by a signer. + */ + applySignature(signature: Uint8Array) { + this.signature = interpretSignatureAsBuffer(signature); + } + + /** + * @deprecated method, use {@link guardianSignature} property instead. + * Applies the guardian signature on the transaction. + * + * @param guardianSignature The signature, as computed by a signer. + */ + applyGuardianSignature(guardianSignature: Uint8Array) { + this.guardianSignature = interpretSignatureAsBuffer(guardianSignature); + } + + /** + * Converts a transaction to a ready-to-broadcast object. + * Called internally by the network provider. + */ + toSendable(): any { + return this.toPlainObject(); + } + + /** + * @deprecated method, use "TransactionComputer.computeTransactionFee()" instead. + * + * Computes the current transaction fee based on the {@link NetworkConfig} and transaction properties + * @param networkConfig {@link NetworkConfig} + */ + computeFee(networkConfig: INetworkConfig): BigNumber { + const computer = new TransactionComputer(); + const fee = computer.computeTransactionFee(this, networkConfig); + return new BigNumber(fee.toString()); + } + + private toBase64OrUndefined(value?: string | Uint8Array) { + return value && value.length ? Buffer.from(value).toString("base64") : undefined; + } + + private toHexOrUndefined(value?: Uint8Array) { + return value && value.length ? Buffer.from(value).toString("hex") : undefined; + } +} diff --git a/src/transactionsFactories/transactionBuilder.ts b/src/core/transactionBuilder.ts similarity index 69% rename from src/transactionsFactories/transactionBuilder.ts rename to src/core/transactionBuilder.ts index 6c163e816..d7e46d4bc 100644 --- a/src/transactionsFactories/transactionBuilder.ts +++ b/src/core/transactionBuilder.ts @@ -1,7 +1,6 @@ -import { ARGUMENTS_SEPARATOR } from "../constants"; -import { IAddress, ITransactionPayload } from "../interface"; -import { Transaction } from "../transaction"; -import { TransactionPayload } from "../transactionPayload"; +import { Address } from "./address"; +import { ARGUMENTS_SEPARATOR } from "./constants"; +import { Transaction } from "./transaction"; interface Config { chainID: string; @@ -14,8 +13,8 @@ interface Config { */ export class TransactionBuilder { private config: Config; - private sender: IAddress; - private receiver: IAddress; + private sender: Address; + private receiver: Address; private dataParts: string[]; private providedGasLimit: bigint; private addDataMovementGas: boolean; @@ -23,8 +22,8 @@ export class TransactionBuilder { constructor(options: { config: Config; - sender: IAddress; - receiver: IAddress; + sender: Address; + receiver: Address; dataParts: string[]; gasLimit: bigint; addDataMovementGas: boolean; @@ -39,19 +38,19 @@ export class TransactionBuilder { this.amount = options.amount; } - private computeGasLimit(payload: ITransactionPayload): bigint { + private computeGasLimit(payload: Uint8Array): bigint { if (!this.addDataMovementGas) { return this.providedGasLimit; } - const dataMovementGas = this.config.minGasLimit + this.config.gasLimitPerByte * BigInt(payload.length()); + const dataMovementGas = this.config.minGasLimit + this.config.gasLimitPerByte * BigInt(payload.length); const gasLimit = dataMovementGas + this.providedGasLimit; return gasLimit; } - private buildTransactionPayload(): TransactionPayload { + private buildTransactionPayload(): Uint8Array { const data = this.dataParts.join(ARGUMENTS_SEPARATOR); - return new TransactionPayload(data); + return Buffer.from(data); } build(): Transaction { @@ -59,8 +58,8 @@ export class TransactionBuilder { const gasLimit = this.computeGasLimit(data); return new Transaction({ - sender: this.sender.bech32(), - receiver: this.receiver.bech32(), + sender: this.sender, + receiver: this.receiver, gasLimit: gasLimit, value: this.amount || 0n, data: data.valueOf(), diff --git a/src/transactionComputer.ts b/src/core/transactionComputer.ts similarity index 69% rename from src/transactionComputer.ts rename to src/core/transactionComputer.ts index a9690b2fa..81cbb08f2 100644 --- a/src/transactionComputer.ts +++ b/src/core/transactionComputer.ts @@ -1,13 +1,13 @@ import BigNumber from "bignumber.js"; +import { ProtoSerializer } from "../proto"; +import { Address } from "./address"; import { MIN_TRANSACTION_VERSION_THAT_SUPPORTS_OPTIONS, TRANSACTION_OPTIONS_TX_GUARDED, TRANSACTION_OPTIONS_TX_HASH_SIGN, } from "./constants"; import * as errors from "./errors"; -import { ITransaction } from "./interface"; -import { INetworkConfig } from "./interfaceOfNetwork"; -import { ProtoSerializer } from "./proto"; +import { INetworkConfig } from "./interfaces"; import { Transaction } from "./transaction"; const createTransactionHasher = require("blake2b"); @@ -25,7 +25,7 @@ export class TransactionComputer { networkConfig: INetworkConfig, ): bigint { const moveBalanceGas = BigInt( - networkConfig.MinGasLimit + transaction.data.length * networkConfig.GasPerDataByte, + networkConfig.minGasLimit + BigInt(transaction.data.length) * networkConfig.gasPerDataByte, ); if (moveBalanceGas > transaction.gasLimit) { throw new errors.ErrNotEnoughGas(parseInt(transaction.gasLimit.toString(), 10)); @@ -39,14 +39,17 @@ export class TransactionComputer { const diff = transaction.gasLimit - moveBalanceGas; const modifiedGasPrice = BigInt( - new BigNumber(gasPrice.toString()).multipliedBy(new BigNumber(networkConfig.GasPriceModifier)).toFixed(0), + new BigNumber(gasPrice.toString()).multipliedBy(new BigNumber(networkConfig.gasPriceModifier)).toFixed(0), ); const processingFee = diff * modifiedGasPrice; return feeForMove + processingFee; } - computeBytesForSigning(transaction: ITransaction): Uint8Array { + /** + * Compute bytes for signing the transaction + */ + computeBytesForSigning(transaction: Transaction): Uint8Array { this.ensureValidTransactionFields(transaction); const plainTransaction = this.toPlainObject(transaction); @@ -54,7 +57,10 @@ export class TransactionComputer { return new Uint8Array(Buffer.from(serialized)); } - computeBytesForVerifying(transaction: ITransaction): Uint8Array { + /** + * Compute bytes for verifying the transaction signature + */ + computeBytesForVerifying(transaction: Transaction): Uint8Array { const isTxSignedByHash = this.hasOptionsSetForHashSigning(transaction); if (isTxSignedByHash) { @@ -63,29 +69,41 @@ export class TransactionComputer { return this.computeBytesForSigning(transaction); } - computeHashForSigning(transaction: ITransaction): Uint8Array { + /** + * Serializes the transaction then computes the hash; used for hash signing transactions. + */ + computeHashForSigning(transaction: Transaction): Uint8Array { const plainTransaction = this.toPlainObject(transaction); const signable = Buffer.from(JSON.stringify(plainTransaction)); return createKeccakHash("keccak256").update(signable).digest(); } - computeTransactionHash(transaction: ITransaction): Uint8Array { + computeTransactionHash(transaction: Transaction): string { const serializer = new ProtoSerializer(); - const buffer = serializer.serializeTransaction(new Transaction(transaction)); + const buffer = serializer.serializeTransaction(transaction); const hash = createTransactionHasher(TRANSACTION_HASH_LENGTH).update(buffer).digest("hex"); - return Buffer.from(hash, "hex"); + return Buffer.from(hash, "hex").toString("hex"); } - hasOptionsSetForGuardedTransaction(transaction: ITransaction): boolean { + /** + * Returns true if the second least significant bit is set; returns false otherwise + */ + hasOptionsSetForGuardedTransaction(transaction: Transaction): boolean { return (transaction.options & TRANSACTION_OPTIONS_TX_GUARDED) == TRANSACTION_OPTIONS_TX_GUARDED; } - hasOptionsSetForHashSigning(transaction: ITransaction): boolean { + /** + * Returns true if the least significant bit is set; returns false otherwise; should also have transaction.version >= 2 + */ + hasOptionsSetForHashSigning(transaction: Transaction): boolean { return (transaction.options & TRANSACTION_OPTIONS_TX_HASH_SIGN) == TRANSACTION_OPTIONS_TX_HASH_SIGN; } - applyGuardian(transaction: ITransaction, guardian: string) { + /** + * Sets guardian address, transaction.version = 2, sets transaction.options second least significant bit + */ + applyGuardian(transaction: Transaction, guardian: Address) { if (transaction.version < MIN_TRANSACTION_VERSION_THAT_SUPPORTS_OPTIONS) { transaction.version = MIN_TRANSACTION_VERSION_THAT_SUPPORTS_OPTIONS; } @@ -94,23 +112,29 @@ export class TransactionComputer { transaction.guardian = guardian; } - isRelayedV3Transaction(transaction: ITransaction) { + /** + * Returns true if transaction.relayer is set; returns false otherwise; + */ + isRelayedV3Transaction(transaction: Transaction) { return !transaction.relayer.isEmpty(); } - applyOptionsForHashSigning(transaction: ITransaction) { + /** + * Sets the least significant bit of the `options` field; also ensures that `version` >= 2 + */ + applyOptionsForHashSigning(transaction: Transaction) { if (transaction.version < MIN_TRANSACTION_VERSION_THAT_SUPPORTS_OPTIONS) { transaction.version = MIN_TRANSACTION_VERSION_THAT_SUPPORTS_OPTIONS; } transaction.options = transaction.options | TRANSACTION_OPTIONS_TX_HASH_SIGN; } - private toPlainObject(transaction: ITransaction, withSignature?: boolean) { + private toPlainObject(transaction: Transaction, withSignature?: boolean) { let obj: any = { nonce: Number(transaction.nonce), value: transaction.value.toString(), - receiver: transaction.receiver, - sender: transaction.sender, + receiver: transaction.receiver.toBech32(), + sender: transaction.sender.toBech32(), senderUsername: this.toBase64OrUndefined(transaction.senderUsername), receiverUsername: this.toBase64OrUndefined(transaction.receiverUsername), gasPrice: Number(transaction.gasPrice), @@ -125,7 +149,7 @@ export class TransactionComputer { obj.chainID = transaction.chainID; obj.version = transaction.version; obj.options = transaction.options ? transaction.options : undefined; - obj.guardian = transaction.guardian ? transaction.guardian : undefined; + obj.guardian = transaction.guardian.isEmpty() ? undefined : transaction.guardian.toBech32(); obj.relayer = transaction.relayer?.isEmpty() ? undefined : transaction.relayer?.toBech32(); return obj; @@ -139,7 +163,7 @@ export class TransactionComputer { return value && value.length ? Buffer.from(value).toString("base64") : undefined; } - private ensureValidTransactionFields(transaction: ITransaction) { + private ensureValidTransactionFields(transaction: Transaction) { if (!transaction.chainID.length) { throw new errors.ErrBadUsage("The `chainID` field is not set"); } diff --git a/src/core/transactionEvents.ts b/src/core/transactionEvents.ts new file mode 100644 index 000000000..48df23ccb --- /dev/null +++ b/src/core/transactionEvents.ts @@ -0,0 +1,34 @@ +import { Address } from "./address"; + +export class TransactionEvent { + raw: Record = {}; + address: Address = Address.empty(); + identifier: string = ""; + topics: Uint8Array[] = []; + + data: Uint8Array = new Uint8Array(); + additionalData: Uint8Array[] = []; + + constructor(init?: Partial) { + Object.assign(this, init); + } + + static fromHttpResponse(responsePart: { + address: string; + identifier: string; + topics: string[]; + data: string; + additionalData?: string[]; + }): TransactionEvent { + let result = new TransactionEvent(); + result.address = new Address(responsePart.address); + result.identifier = responsePart.identifier || ""; + result.topics = (responsePart.topics || []).map((topic) => Buffer.from(topic, "base64")); + + result.data = Buffer.from(responsePart.data ?? "", "base64"); + result.additionalData = (responsePart.additionalData || []).map((data) => Buffer.from(data, "base64")); + result.raw = responsePart; + + return result; + } +} diff --git a/src/networkProviders/transactionLogs.ts b/src/core/transactionLogs.ts similarity index 89% rename from src/networkProviders/transactionLogs.ts rename to src/core/transactionLogs.ts index 36c10690d..081770f69 100644 --- a/src/networkProviders/transactionLogs.ts +++ b/src/core/transactionLogs.ts @@ -1,10 +1,9 @@ -import { Address } from "../address"; -import { ErrUnexpectedCondition } from "./../errors"; -import { IAddress } from "./interface"; +import { Address } from "./address"; +import { ErrUnexpectedCondition } from "./errors"; import { TransactionEvent } from "./transactionEvents"; export class TransactionLogs { - address: IAddress = Address.empty(); + address: Address = Address.empty(); events: TransactionEvent[] = []; constructor(init?: Partial) { diff --git a/src/core/transactionOnNetwork.ts b/src/core/transactionOnNetwork.ts new file mode 100644 index 000000000..5d6cbafc6 --- /dev/null +++ b/src/core/transactionOnNetwork.ts @@ -0,0 +1,187 @@ +import { SmartContractResult } from "../transactionsOutcomeParsers"; +import { Address } from "./address"; +import { Transaction } from "./transaction"; +import { TransactionLogs } from "./transactionLogs"; +import { TransactionStatus } from "./transactionStatus"; + +export function prepareTransactionForBroadcasting(transaction: Transaction): any { + return { + nonce: Number(transaction.nonce), + value: transaction.value.toString(), + receiver: transaction.receiver.toBech32(), + sender: transaction.sender.toBech32(), + senderUsername: transaction.senderUsername + ? Buffer.from(transaction.senderUsername).toString("base64") + : undefined, + receiverUsername: transaction.receiverUsername + ? Buffer.from(transaction.receiverUsername).toString("base64") + : undefined, + gasPrice: Number(transaction.gasPrice), + gasLimit: Number(transaction.gasLimit), + data: transaction.data.length === 0 ? undefined : Buffer.from(transaction.data).toString("base64"), + chainID: transaction.chainID, + version: transaction.version, + options: transaction.options, + guardian: transaction.guardian.isEmpty() ? undefined : transaction.guardian.toBech32(), + signature: Buffer.from(transaction.signature).toString("hex"), + guardianSignature: + transaction.guardianSignature.length === 0 + ? undefined + : Buffer.from(transaction.guardianSignature).toString("hex"), + relayer: transaction.relayer.isEmpty() ? undefined : transaction.relayer.toBech32(), + relayerSignature: + transaction.relayerSignature.length === 0 + ? undefined + : Buffer.from(transaction.relayerSignature).toString("hex"), + }; +} + +export class TransactionOnNetwork { + raw: Record = {}; + isCompleted?: boolean; + hash: string = ""; + type: string = ""; + nonce: bigint = 0n; + round: bigint = 0n; + epoch: number = 0; + value: bigint = 0n; + receiver: Address = Address.empty(); + sender: Address = Address.empty(); + senderShard: number = 0; + receiverShard: number = 0; + gasLimit: bigint = 0n; + gasPrice: bigint = 0n; + function: string = ""; + data: Buffer = Buffer.from([]); + version: number = 0; + options: number = 0; + signature: Uint8Array = new Uint8Array(); + status: TransactionStatus = TransactionStatus.createUnknown(); + timestamp: number = 0; + miniblockHash: string = ""; + blockHash: string = ""; + + smartContractResults: SmartContractResult[] = []; + logs: TransactionLogs = new TransactionLogs(); + + constructor(init?: Partial) { + Object.assign(this, init); + } + + static fromProxyHttpResponse( + txHash: string, + response: any, + processStatus?: TransactionStatus | undefined, + ): TransactionOnNetwork { + const result = TransactionOnNetwork.fromHttpResponse(txHash, response); + result.smartContractResults = + response.smartContractResults?.map( + (result: Partial) => + new SmartContractResult({ + ...result, + receiver: result.receiver ? new Address(result.receiver) : undefined, + sender: result.sender ? new Address(result.sender) : undefined, + raw: result, + }), + ) ?? []; + + if (processStatus) { + result.status = processStatus; + result.isCompleted = result.status.isSuccessful() || result.status.isFailed(); + } + + return result; + } + + static fromSimulateResponse(originalTx: Transaction, response: any): TransactionOnNetwork { + const status = new TransactionStatus(response["status"]); + const txHash = response["hash"] ?? ""; + const scResults: SmartContractResult[] = []; + const results = response["scResults"] || {}; + for (const hash in results) { + const result = results[hash]; + + const scResult = new SmartContractResult({ + ...result, + receiver: result.receiver ? new Address(result.receiver) : undefined, + sender: result.sender ? new Address(result.sender) : undefined, + raw: result, + }); + scResults.push(scResult); + } + + let result = new TransactionOnNetwork(); + result.hash = txHash; + result.type = response.type || ""; + result.nonce = BigInt(originalTx.nonce || 0); + result.round = -1n; + result.epoch = -1; + result.value = BigInt((originalTx.value || 0).toString()); + result.sender = new Address(originalTx.sender); + result.receiver = new Address(originalTx.receiver); + result.gasPrice = BigInt(originalTx.gasPrice) || 0n; + result.gasLimit = BigInt(originalTx.gasLimit) || 0n; + result.function = ""; + result.data = originalTx.data ? Buffer.from(originalTx.data?.toString()) : Buffer.from(""); + result.version = originalTx.version || 1; + result.options = originalTx.options || 0; + result.timestamp = 0; + result.miniblockHash = ""; + result.blockHash = ""; + result.logs = TransactionLogs.fromHttpResponse(response.logs || {}); + result.raw = response; + result.smartContractResults = scResults; + + result.status = status; + result.isCompleted = status.isSuccessful() || status.isFailed(); + + return result; + } + + static fromApiHttpResponse(txHash: string, response: any): TransactionOnNetwork { + const result = TransactionOnNetwork.fromHttpResponse(txHash, response); + result.smartContractResults = + response.results?.map( + (result: Partial) => + new SmartContractResult({ + ...result, + receiver: result.receiver ? new Address(result.receiver) : undefined, + sender: result.sender ? new Address(result.sender) : undefined, + raw: result, + }), + ) ?? []; + result.isCompleted = !result.status.isPending(); + return result; + } + + private static fromHttpResponse(txHash: string, response: any): TransactionOnNetwork { + let result = new TransactionOnNetwork(); + result.hash = txHash; + result.type = response.type || ""; + result.nonce = BigInt(response.nonce || 0); + result.round = BigInt(response.round || 0); + result.epoch = response.epoch || 0; + result.value = BigInt((response.value || 0).toString()); + result.sender = new Address(response.sender); + result.receiver = new Address(response.receiver); + result.gasPrice = BigInt(response.gasPrice) || 0n; + result.gasLimit = BigInt(response.gasLimit) || 0n; + result.function = response.function || ""; + result.data = Buffer.from(response.data || "", "base64"); + result.version = response.version || 1; + result.options = response.options || 0; + result.data = Buffer.from(response.data || "", "base64"); + result.status = new TransactionStatus(response.status); + result.timestamp = response.timestamp || 0; + result.miniblockHash = response.miniblockHash || ""; + result.blockHash = response.blockHash || ""; + result.logs = TransactionLogs.fromHttpResponse(response.logs || {}); + result.raw = response; + + return result; + } + + getDateTime(): Date { + return new Date(this.timestamp * 1000); + } +} diff --git a/src/transactionPayload.ts b/src/core/transactionPayload.ts similarity index 100% rename from src/transactionPayload.ts rename to src/core/transactionPayload.ts diff --git a/src/networkProviders/transactionStatus.ts b/src/core/transactionStatus.ts similarity index 72% rename from src/networkProviders/transactionStatus.ts rename to src/core/transactionStatus.ts index bb509cbcf..e13ee7b84 100644 --- a/src/networkProviders/transactionStatus.ts +++ b/src/core/transactionStatus.ts @@ -25,40 +25,36 @@ export class TransactionStatus { * Returns whether the transaction is pending (e.g. in mempool). */ isPending(): boolean { - return ( - this.status == "received" || - this.status == "pending" - ); + return this.status == "received" || this.status == "pending"; } /** * Returns whether the transaction has been executed (not necessarily with success). + * @deprecated This will be remove next version, please use {@link isCompleted} instead. */ isExecuted(): boolean { return this.isSuccessful() || this.isFailed() || this.isInvalid(); } + /** + * Returns whether the transaction has been conpleted (not necessarily with success). + */ + isCompleted(): boolean { + return this.isSuccessful() || this.isFailed() || this.isInvalid(); + } + /** * Returns whether the transaction has been executed successfully. */ isSuccessful(): boolean { - return ( - this.status == "executed" || - this.status == "success" || - this.status == "successful" - ); + return this.status == "executed" || this.status == "success" || this.status == "successful"; } /** * Returns whether the transaction has been executed, but with a failure. */ isFailed(): boolean { - return ( - this.status == "fail" || - this.status == "failed" || - this.status == "unsuccessful" || - this.isInvalid() - ); + return this.status == "fail" || this.status == "failed" || this.status == "unsuccessful" || this.isInvalid(); } /** diff --git a/src/transactionWatcher.spec.ts b/src/core/transactionWatcher.spec.ts similarity index 66% rename from src/transactionWatcher.spec.ts rename to src/core/transactionWatcher.spec.ts index dd223af6d..967be8037 100644 --- a/src/transactionWatcher.spec.ts +++ b/src/core/transactionWatcher.spec.ts @@ -1,20 +1,17 @@ -import { TransactionOnNetwork, TransactionStatus } from "./networkProviders"; import { assert } from "chai"; -import { MarkCompleted, MockNetworkProvider, Wait } from "./testutils"; -import { TransactionHash } from "./transaction"; +import { MarkCompleted, MockNetworkProvider, Wait } from "../testutils"; +import { TransactionOnNetwork } from "./transactionOnNetwork"; +import { TransactionStatus } from "./transactionStatus"; import { TransactionWatcher } from "./transactionWatcher"; describe("test transactionWatcher", () => { it("should await status == executed using hash", async () => { - let hash = new TransactionHash("abbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabba"); + let hash = "abbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabba"; let provider = new MockNetworkProvider(); let watcher = new TransactionWatcher(provider, { pollingIntervalMilliseconds: 42, timeoutMilliseconds: 42 * 42, }); - let dummyTransaction = { - getHash: () => hash, - }; provider.mockPutTransaction( hash, @@ -31,22 +28,19 @@ describe("test transactionWatcher", () => { new TransactionStatus("executed"), new MarkCompleted(), ]), - watcher.awaitCompleted(dummyTransaction.getHash().hex()), + watcher.awaitCompleted(hash), ]); - assert.isTrue((await provider.getTransactionStatus(hash.hex())).isExecuted()); + assert.isTrue((await provider.getTransactionStatus(hash)).isCompleted()); }); it("should await status == executed using transaction", async () => { - let hash = new TransactionHash("abbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabba"); + let hash = "abbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabba"; let provider = new MockNetworkProvider(); let watcher = new TransactionWatcher(provider, { pollingIntervalMilliseconds: 42, timeoutMilliseconds: 42 * 42, }); - let dummyTransaction = { - getHash: () => hash, - }; provider.mockPutTransaction( hash, @@ -63,9 +57,9 @@ describe("test transactionWatcher", () => { new TransactionStatus("executed"), new MarkCompleted(), ]), - watcher.awaitCompleted(dummyTransaction), + watcher.awaitCompleted(hash), ]); - assert.isTrue((await provider.getTransactionStatus(hash.hex())).isExecuted()); + assert.isTrue((await provider.getTransactionStatus(hash)).isCompleted()); }); }); diff --git a/src/transactionWatcher.ts b/src/core/transactionWatcher.ts similarity index 58% rename from src/transactionWatcher.ts rename to src/core/transactionWatcher.ts index ae077b38b..0f1826b83 100644 --- a/src/transactionWatcher.ts +++ b/src/core/transactionWatcher.ts @@ -1,33 +1,27 @@ import { AsyncTimer } from "./asyncTimer"; -import { HEX_TRANSACTION_HASH_LENGTH } from "./constants"; import { Err, ErrExpectedTransactionEventsNotFound, ErrExpectedTransactionStatusNotReached, ErrIsCompletedFieldIsMissingOnTransaction, } from "./errors"; -import { ITransactionFetcher } from "./interface"; -import { ITransactionEvent, ITransactionOnNetwork, ITransactionStatus } from "./interfaceOfNetwork"; +import { ITransactionFetcher } from "./interfaces"; import { Logger } from "./logger"; +import { TransactionEvent } from "./transactionEvents"; +import { TransactionOnNetwork } from "./transactionOnNetwork"; +import { TransactionStatus } from "./transactionStatus"; -export type PredicateIsAwaitedStatus = (status: ITransactionStatus) => boolean; - -/** - * Internal interface: a transaction, as seen from the perspective of a {@link TransactionWatcher}. - */ -interface ITransaction { - getHash(): { hex(): string }; -} +export type PredicateIsAwaitedStatus = (status: TransactionStatus) => boolean; /** * TransactionWatcher allows one to continuously watch (monitor), by means of polling, the status of a given transaction. */ export class TransactionWatcher { - static DefaultPollingInterval: number = 6000; - static DefaultTimeout: number = TransactionWatcher.DefaultPollingInterval * 15; - static DefaultPatience: number = 0; + private static DefaultPollingInterval: number = 6000; + private static DefaultTimeout: number = TransactionWatcher.DefaultPollingInterval * 15; + private static DefaultPatience: number = 0; - static NoopOnStatusReceived = (_: ITransactionStatus) => {}; + static NoopOnStatusReceived = (_: TransactionStatus) => {}; protected readonly fetcher: ITransactionFetcher; protected readonly pollingIntervalMilliseconds: number; @@ -62,43 +56,35 @@ export class TransactionWatcher { * Waits until the transaction reaches the "pending" status. * @param txHash The hex-encoded transaction hash */ - public async awaitPending(transactionOrTxHash: ITransaction | string): Promise { - const isPending = (transaction: ITransactionOnNetwork) => transaction.status.isPending(); + public async awaitPending(txHash: string): Promise { + const isPending = (transaction: TransactionOnNetwork) => transaction.status.isPending(); const doFetch = async () => { - const hash = this.transactionOrTxHashToTxHash(transactionOrTxHash); - return await this.fetcher.getTransaction(hash); + return await this.fetcher.getTransaction(txHash); }; const errorProvider = () => new ErrExpectedTransactionStatusNotReached(); - return this.awaitConditionally(isPending, doFetch, errorProvider); + return this.awaitConditionally(isPending, doFetch, errorProvider); } /** * Waits until the transaction is completely processed. - * @param txHash The hex-encoded transaction hash + * @param txHash The transaction hash */ - public async awaitCompleted(transactionOrTxHash: ITransaction | string): Promise { - const isCompleted = (transactionOnNetwork: ITransactionOnNetwork) => { - if (transactionOnNetwork.isCompleted === undefined) { - throw new ErrIsCompletedFieldIsMissingOnTransaction(); - } - return transactionOnNetwork.isCompleted; + public async awaitCompleted(txHash: string): Promise { + const isCompleted = (transactionOnNetwork: TransactionOnNetwork) => { + return transactionOnNetwork.status.isCompleted(); }; const doFetch = async () => { - const hash = this.transactionOrTxHashToTxHash(transactionOrTxHash); - return await this.fetcher.getTransaction(hash); + return await this.fetcher.getTransaction(txHash); }; const errorProvider = () => new ErrExpectedTransactionStatusNotReached(); - return this.awaitConditionally(isCompleted, doFetch, errorProvider); + return this.awaitConditionally(isCompleted, doFetch, errorProvider); } - public async awaitAllEvents( - transactionOrTxHash: ITransaction | string, - events: string[], - ): Promise { - const foundAllEvents = (transactionOnNetwork: ITransactionOnNetwork) => { + public async awaitAllEvents(txHash: string, events: string[]): Promise { + const foundAllEvents = (transactionOnNetwork: TransactionOnNetwork) => { const allEventIdentifiers = this.getAllTransactionEvents(transactionOnNetwork).map( (event) => event.identifier, ); @@ -107,19 +93,15 @@ export class TransactionWatcher { }; const doFetch = async () => { - const hash = this.transactionOrTxHashToTxHash(transactionOrTxHash); - return await this.fetcher.getTransaction(hash); + return await this.fetcher.getTransaction(txHash); }; const errorProvider = () => new ErrExpectedTransactionEventsNotFound(); - return this.awaitConditionally(foundAllEvents, doFetch, errorProvider); + return this.awaitConditionally(foundAllEvents, doFetch, errorProvider); } - public async awaitAnyEvent( - transactionOrTxHash: ITransaction | string, - events: string[], - ): Promise { - const foundAnyEvent = (transactionOnNetwork: ITransactionOnNetwork) => { + public async awaitAnyEvent(txHash: string, events: string[]): Promise { + const foundAnyEvent = (transactionOnNetwork: TransactionOnNetwork) => { const allEventIdentifiers = this.getAllTransactionEvents(transactionOnNetwork).map( (event) => event.identifier, ); @@ -128,38 +110,23 @@ export class TransactionWatcher { }; const doFetch = async () => { - const hash = this.transactionOrTxHashToTxHash(transactionOrTxHash); - return await this.fetcher.getTransaction(hash); + return await this.fetcher.getTransaction(txHash); }; const errorProvider = () => new ErrExpectedTransactionEventsNotFound(); - return this.awaitConditionally(foundAnyEvent, doFetch, errorProvider); + return this.awaitConditionally(foundAnyEvent, doFetch, errorProvider); } public async awaitOnCondition( - transactionOrTxHash: ITransaction | string, - condition: (data: ITransactionOnNetwork) => boolean, - ): Promise { + txHash: string, + condition: (data: TransactionOnNetwork) => boolean, + ): Promise { const doFetch = async () => { - const hash = this.transactionOrTxHashToTxHash(transactionOrTxHash); - return await this.fetcher.getTransaction(hash); + return await this.fetcher.getTransaction(txHash); }; const errorProvider = () => new ErrExpectedTransactionStatusNotReached(); - return this.awaitConditionally(condition, doFetch, errorProvider); - } - - private transactionOrTxHashToTxHash(transactionOrTxHash: ITransaction | string): string { - const hash = - typeof transactionOrTxHash === "string" ? transactionOrTxHash : transactionOrTxHash.getHash().hex(); - - if (hash.length !== HEX_TRANSACTION_HASH_LENGTH) { - throw new Err( - `Invalid transaction hash length. The length of a hex encoded hash should be ${HEX_TRANSACTION_HASH_LENGTH}.`, - ); - } - - return hash; + return this.awaitConditionally(condition, doFetch, errorProvider); } protected async awaitConditionally( @@ -218,11 +185,11 @@ export class TransactionWatcher { return fetchedData; } - protected getAllTransactionEvents(transaction: ITransactionOnNetwork): ITransactionEvent[] { + protected getAllTransactionEvents(transaction: TransactionOnNetwork): TransactionEvent[] { const result = [...transaction.logs.events]; - for (const resultItem of transaction.contractResults.items) { - result.push(...resultItem.logs.events); + for (const resultItem of transaction.smartContractResults) { + result.push(...resultItem.logs?.events); } return result; @@ -236,7 +203,7 @@ class TransactionFetcherWithTracing implements ITransactionFetcher { this.fetcher = fetcher; } - async getTransaction(txHash: string): Promise { + async getTransaction(txHash: string): Promise { Logger.debug(`transactionWatcher, getTransaction(${txHash})`); return await this.fetcher.getTransaction(txHash); } diff --git a/src/transactionsFactories/transactionsFactoryConfig.ts b/src/core/transactionsFactoryConfig.ts similarity index 95% rename from src/transactionsFactories/transactionsFactoryConfig.ts rename to src/core/transactionsFactoryConfig.ts index c1d21df92..d898fbb00 100644 --- a/src/transactionsFactories/transactionsFactoryConfig.ts +++ b/src/core/transactionsFactoryConfig.ts @@ -1,4 +1,4 @@ -import { LibraryConfig } from "../config"; +import { LibraryConfig } from "./config"; export class TransactionsFactoryConfig { chainID: string; @@ -62,9 +62,9 @@ export class TransactionsFactoryConfig { this.gasLimitFreezing = 60000000n; this.gasLimitWiping = 60000000n; this.gasLimitEsdtNftCreate = 3000000n; - this.gasLimitEsdtNftUpdateAttributes = 1000000n; - this.gasLimitEsdtNftAddQuantity = 1000000n; - this.gasLimitEsdtNftBurn = 1000000n; + this.gasLimitEsdtNftUpdateAttributes = 50000n; + this.gasLimitEsdtNftAddQuantity = 50000n; + this.gasLimitEsdtNftBurn = 50000n; this.gasLimitStorePerByte = 10000n; this.issueCost = 50000000000000000n; this.gasLimitEsdtModifyRoyalties = 60000000n; diff --git a/src/utils.codec.spec.ts b/src/core/utils.codec.spec.ts similarity index 100% rename from src/utils.codec.spec.ts rename to src/core/utils.codec.spec.ts diff --git a/src/utils.codec.ts b/src/core/utils.codec.ts similarity index 81% rename from src/utils.codec.ts rename to src/core/utils.codec.ts index 0e03b658b..299efda1e 100644 --- a/src/utils.codec.ts +++ b/src/core/utils.codec.ts @@ -1,7 +1,5 @@ import BigNumber from "bignumber.js"; -import { Address } from "./address"; -import { IAddress } from "./interface"; -import * as contractsCodecUtils from "./smartcontracts/codec/utils"; +import * as contractsCodecUtils from "../abi/codec/utils"; export function numberToPaddedHex(value: bigint | number | BigNumber.Value) { let hexableNumber: { toString(radix?: number): string }; @@ -53,8 +51,3 @@ export function bigIntToHex(value: BigNumber.Value): string { return contractsCodecUtils.getHexMagnitudeOfBigInt(value); } - -export function addressToHex(address: IAddress): string { - const buffer = Address.fromBech32(address.toString()).pubkey(); - return buffer.toString("hex"); -} diff --git a/src/utils.ts b/src/core/utils.ts similarity index 100% rename from src/utils.ts rename to src/core/utils.ts diff --git a/src/delegation/delegationController.ts b/src/delegation/delegationController.ts new file mode 100644 index 000000000..1f4384caf --- /dev/null +++ b/src/delegation/delegationController.ts @@ -0,0 +1,349 @@ +import { + Address, + BaseController, + BaseControllerInput, + IAccount, + Transaction, + TransactionOnNetwork, + TransactionsFactoryConfig, + TransactionWatcher, +} from "../core"; +import { INetworkProvider } from "../networkProviders/interface"; +import { DelegationTransactionsFactory } from "./delegationTransactionsFactory"; +import { DelegationTransactionsOutcomeParser } from "./delegationTransactionsOutcomeParser"; +import * as resources from "./resources"; + +export class DelegationController extends BaseController { + private transactionAwaiter: TransactionWatcher; + private factory: DelegationTransactionsFactory; + private parser: DelegationTransactionsOutcomeParser; + + constructor(options: { chainID: string; networkProvider: INetworkProvider }) { + super(); + this.transactionAwaiter = new TransactionWatcher(options.networkProvider); + this.factory = new DelegationTransactionsFactory({ + config: new TransactionsFactoryConfig({ chainID: options.chainID }), + }); + this.parser = new DelegationTransactionsOutcomeParser(); + } + + async createTransactionForNewDelegationContract( + sender: IAccount, + nonce: bigint, + options: resources.NewDelegationContractInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForNewDelegationContract(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async awaitCompletedCreateNewDelegationContract(txHash: string): Promise<{ contractAddress: string }[]> { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.parseCreateNewDelegationContract(transaction); + } + + parseCreateNewDelegationContract(transactionOnNetwork: TransactionOnNetwork): { contractAddress: string }[] { + return this.parser.parseCreateNewDelegationContract(transactionOnNetwork); + } + + async createTransactionForAddingNodes( + sender: IAccount, + nonce: bigint, + options: resources.AddNodesInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForAddingNodes(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async createTransactionForRemovingNodes( + sender: IAccount, + nonce: bigint, + options: resources.ManageNodesInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForRemovingNodes(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async createTransactionForStakingNodes( + sender: IAccount, + nonce: bigint, + options: resources.ManageNodesInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForStakingNodes(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async createTransactionForUnbondingNodes( + sender: IAccount, + nonce: bigint, + options: resources.ManageNodesInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForUnbondingNodes(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async createTransactionForUnstakingNodes( + sender: IAccount, + nonce: bigint, + options: resources.ManageNodesInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForUnstakingNodes(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async createTransactionForUnjailingNodes( + sender: IAccount, + nonce: bigint, + options: resources.UnjailingNodesInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForUnjailingNodes(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async createTransactionForChangingServiceFee( + sender: IAccount, + nonce: bigint, + options: resources.ChangeServiceFee & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForChangingServiceFee(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async createTransactionForModifyingDelegationCap( + sender: IAccount, + nonce: bigint, + options: resources.ModifyDelegationCapInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForModifyingDelegationCap(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async createTransactionForSettingAutomaticActivation( + sender: IAccount, + nonce: bigint, + options: resources.ManageDelegationContractInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForSettingAutomaticActivation(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async createTransactionForUnsettingAutomaticActivation( + sender: IAccount, + nonce: bigint, + options: resources.ManageDelegationContractInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForUnsettingAutomaticActivation(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async createTransactionForSettingCapCheckOnRedelegateRewards( + sender: IAccount, + nonce: bigint, + options: resources.ManageDelegationContractInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForSettingCapCheckOnRedelegateRewards( + sender.address, + options, + ); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async createTransactionForUnsettingCapCheckOnRedelegateRewards( + sender: IAccount, + nonce: bigint, + options: resources.ManageDelegationContractInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForUnsettingCapCheckOnRedelegateRewards( + sender.address, + options, + ); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async createTransactionForSettingMetadata( + sender: IAccount, + nonce: bigint, + options: resources.SetContractMetadataInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForSettingMetadata(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async createTransactionForDelegating( + sender: IAccount, + nonce: bigint, + options: resources.DelegateActionsInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForDelegating(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async createTransactionForClaimingRewards( + sender: IAccount, + nonce: bigint, + options: resources.ManageDelegationContractInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForClaimingRewards(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async createTransactionForRedelegatingRewards( + sender: IAccount, + nonce: bigint, + options: resources.ManageDelegationContractInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForRedelegatingRewards(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async createTransactionForUndelegating( + sender: IAccount, + nonce: bigint, + options: resources.DelegateActionsInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForUndelegating(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async createTransactionForWithdrawing( + sender: IAccount, + nonce: bigint, + options: resources.ManageDelegationContractInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForWithdrawing(sender.address, options); + + transaction.nonce = nonce; + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } +} diff --git a/src/delegation/delegationTransactionsFactory.spec.ts b/src/delegation/delegationTransactionsFactory.spec.ts new file mode 100644 index 000000000..2b35d3000 --- /dev/null +++ b/src/delegation/delegationTransactionsFactory.spec.ts @@ -0,0 +1,447 @@ +import { assert } from "chai"; +import { Address, TransactionsFactoryConfig } from "../core"; +import { DELEGATION_MANAGER_SC_ADDRESS_HEX } from "../core/constants"; +import { ValidatorPublicKey } from "../wallet"; +import { DelegationTransactionsFactory } from "./delegationTransactionsFactory"; + +describe("test delegation transactions factory", function () { + const config = new TransactionsFactoryConfig({ chainID: "D" }); + const delegationFactory = new DelegationTransactionsFactory({ config: config }); + + it("should create 'Transaction' for new delegation contract", async function () { + const sender = Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + const delagationCap = 5000000000000000000000n; + const serviceFee = 10n; + const value = 1250000000000000000000n; + + const transaction = delegationFactory.createTransactionForNewDelegationContract(sender, { + totalDelegationCap: delagationCap, + serviceFee: serviceFee, + amount: value, + }); + + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + ); + assert.deepEqual( + transaction.receiver, + Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX, config.addressHrp), + ); + assert.isDefined(transaction.data); + assert.deepEqual(transaction.data, Buffer.from("createNewDelegationContract@010f0cf064dd59200000@0a")); + assert.equal(transaction.gasLimit, 60126500n); + assert.equal(transaction.value, value); + assert.equal(transaction.chainID, config.chainID); + }); + + it("should create 'Transaction' for adding nodes", async function () { + const sender = Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + const delegationContract = Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX); + const publicKey = new ValidatorPublicKey( + Buffer.from( + "e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208", + "hex", + ), + ); + + const mockMessage = { + getSignature: () => + Buffer.from( + "81109fa1c8d3dc7b6c2d6e65206cc0bc1a83c9b2d1eb91a601d66ad32def430827d5eb52917bd2b0d04ce195738db216", + "hex", + ), + }; + + const transaction = delegationFactory.createTransactionForAddingNodes(sender, { + delegationContract: delegationContract, + publicKeys: [publicKey], + signedMessages: [mockMessage.getSignature()], + }); + + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX)); + assert.isDefined(transaction.data); + assert.deepEqual( + transaction.data, + Buffer.from( + "addNodes@e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208@81109fa1c8d3dc7b6c2d6e65206cc0bc1a83c9b2d1eb91a601d66ad32def430827d5eb52917bd2b0d04ce195738db216", + ), + ); + assert.equal(transaction.value, 0n); + }); + + it("should create 'Transaction' for removing nodes", async function () { + const sender = Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + const delegationContract = Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX); + + const publicKey = new ValidatorPublicKey( + Buffer.from( + "be2e593ff10899a2ee8e1d5c8094e36c9f48e04b87e129991ff09475808743e07bb41bf6e7bc1463fa554c4b46594b98", + ), + ); + + const transaction = delegationFactory.createTransactionForRemovingNodes(sender, { + delegationContract: delegationContract, + publicKeys: [publicKey], + }); + + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX)); + assert.isDefined(transaction.data); + assert.deepEqual( + transaction.data, + Buffer.from( + "removeNodes@626532653539336666313038393961326565386531643563383039346533366339663438653034623837653132393939316666303934373538303837343365303762623431626636653762633134363366613535346334623436353934623938", + ), + ); + assert.equal(transaction.value, 0n); + }); + + it("should create 'Transaction' for staking nodes", async function () { + const sender = Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + const delegationContract = Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX); + const publicKey = new ValidatorPublicKey( + Buffer.from( + "be2e593ff10899a2ee8e1d5c8094e36c9f48e04b87e129991ff09475808743e07bb41bf6e7bc1463fa554c4b46594b98", + ), + ); + + const transaction = delegationFactory.createTransactionForStakingNodes(sender, { + delegationContract: delegationContract, + publicKeys: [publicKey], + }); + + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX)); + assert.isDefined(transaction.data); + assert.deepEqual( + transaction.data, + Buffer.from( + "stakeNodes@626532653539336666313038393961326565386531643563383039346533366339663438653034623837653132393939316666303934373538303837343365303762623431626636653762633134363366613535346334623436353934623938", + ), + ); + assert.equal(transaction.value, 0n); + }); + + it("should create 'Transaction' for unbonding nodes", async function () { + const sender = Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + const delegationContract = Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX); + const publicKey = new ValidatorPublicKey( + Buffer.from( + "be2e593ff10899a2ee8e1d5c8094e36c9f48e04b87e129991ff09475808743e07bb41bf6e7bc1463fa554c4b46594b98", + ), + ); + + const transaction = delegationFactory.createTransactionForUnbondingNodes(sender, { + delegationContract: delegationContract, + publicKeys: [publicKey], + }); + + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX)); + assert.isDefined(transaction.data); + assert.deepEqual( + transaction.data, + Buffer.from( + "unBondNodes@626532653539336666313038393961326565386531643563383039346533366339663438653034623837653132393939316666303934373538303837343365303762623431626636653762633134363366613535346334623436353934623938", + ), + ); + assert.equal(transaction.value, 0n); + assert.equal(transaction.gasLimit, 12356000n); + }); + + it("should create 'Transaction' for unstaking nodes", async function () { + const sender = Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + const delegationContract = Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX); + const publicKey = new ValidatorPublicKey( + Buffer.from( + "be2e593ff10899a2ee8e1d5c8094e36c9f48e04b87e129991ff09475808743e07bb41bf6e7bc1463fa554c4b46594b98", + ), + ); + + const transaction = delegationFactory.createTransactionForUnstakingNodes(sender, { + delegationContract: delegationContract, + publicKeys: [publicKey], + }); + + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX)); + assert.isDefined(transaction.data); + assert.deepEqual( + transaction.data, + Buffer.from( + "unStakeNodes@626532653539336666313038393961326565386531643563383039346533366339663438653034623837653132393939316666303934373538303837343365303762623431626636653762633134363366613535346334623436353934623938", + ), + ); + assert.equal(transaction.value, 0n); + assert.equal(transaction.gasLimit, 12357500n); + }); + + it("should create 'Transaction' for unjailing nodes", async function () { + const sender = Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + const delegationContract = Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX); + const publicKey = new ValidatorPublicKey( + Buffer.from( + "be2e593ff10899a2ee8e1d5c8094e36c9f48e04b87e129991ff09475808743e07bb41bf6e7bc1463fa554c4b46594b98", + ), + ); + + const transaction = delegationFactory.createTransactionForUnjailingNodes(sender, { + delegationContract: delegationContract, + publicKeys: [publicKey], + amount: 25000000000000000000n, + }); + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX)); + assert.isDefined(transaction.data); + assert.deepEqual( + transaction.data, + Buffer.from( + "unJailNodes@626532653539336666313038393961326565386531643563383039346533366339663438653034623837653132393939316666303934373538303837343365303762623431626636653762633134363366613535346334623436353934623938", + ), + ); + assert.equal(transaction.value, 25000000000000000000n); + }); + + it("should create 'Transaction' for changing service fee", async function () { + const sender = Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + const delegationContract = Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX); + const serviceFee = 10n; + + const transaction = delegationFactory.createTransactionForChangingServiceFee(sender, { + delegationContract: delegationContract, + serviceFee: serviceFee, + }); + + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX)); + assert.isDefined(transaction.data); + assert.deepEqual(transaction.data, Buffer.from("changeServiceFee@0a")); + assert.equal(transaction.value, 0n); + }); + + it("should create 'Transaction' for changing delegation cap", async function () { + const sender = Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + const delegationContract = Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX); + const delegationCap = 5000000000000000000000n; + + const transaction = delegationFactory.createTransactionForModifyingDelegationCap(sender, { + delegationContract: delegationContract, + delegationCap: delegationCap, + }); + + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX)); + assert.isDefined(transaction.data); + assert.deepEqual(transaction.data, Buffer.from("modifyTotalDelegationCap@010f0cf064dd59200000")); + assert.equal(transaction.value, 0n); + }); + + it("should create 'Transaction' for setting automatic activation", async function () { + const sender = Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + const delegationContract = Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX); + + const transaction = delegationFactory.createTransactionForSettingAutomaticActivation(sender, { + delegationContract: delegationContract, + }); + + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX)); + assert.isDefined(transaction.data); + assert.deepEqual(transaction.data, Buffer.from("setAutomaticActivation@74727565")); + assert.equal(transaction.value, 0n); + }); + + it("should create 'Transaction' for unsetting automatic activation", async function () { + const sender = Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + const delegationContract = Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX); + + const transaction = delegationFactory.createTransactionForUnsettingAutomaticActivation(sender, { + delegationContract: delegationContract, + }); + + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX)); + assert.isDefined(transaction.data); + assert.deepEqual(transaction.data, Buffer.from("setAutomaticActivation@66616c7365")); + assert.equal(transaction.value, 0n); + }); + + it("should create 'Transaction' for setting cap check on redelegate rewards", async function () { + const sender = Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + const delegationContract = Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX); + + const transaction = delegationFactory.createTransactionForSettingCapCheckOnRedelegateRewards(sender, { + delegationContract: delegationContract, + }); + + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX)); + assert.isDefined(transaction.data); + assert.deepEqual(transaction.data, Buffer.from("setCheckCapOnReDelegateRewards@74727565")); + assert.equal(transaction.value, 0n); + }); + + it("should create 'Transaction' for unsetting cap check on redelegate rewards", async function () { + const sender = Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + const delegationContract = Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX); + + const transaction = delegationFactory.createTransactionForUnsettingCapCheckOnRedelegateRewards(sender, { + delegationContract: delegationContract, + }); + + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX)); + assert.isDefined(transaction.data); + assert.deepEqual(transaction.data, Buffer.from("setCheckCapOnReDelegateRewards@66616c7365")); + assert.equal(transaction.value, 0n); + }); + + it("should create 'Transaction' for setting metadata", async function () { + const sender = Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + const delegationContract = Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX); + + const transaction = delegationFactory.createTransactionForSettingMetadata(sender, { + delegationContract: delegationContract, + name: "name", + website: "website", + identifier: "identifier", + }); + + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX)); + assert.isDefined(transaction.data); + assert.deepEqual(transaction.data, Buffer.from("setMetaData@6e616d65@77656273697465@6964656e746966696572")); + assert.equal(transaction.value, 0n); + }); + + it("should create 'Transaction' for delegating", async function () { + const sender = Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + const delegationContract = Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX); + + const transaction = delegationFactory.createTransactionForDelegating(sender, { + delegationContract: delegationContract, + amount: 1000000000000000000n, + }); + + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX)); + assert.isDefined(transaction.data); + assert.deepEqual(transaction.data, Buffer.from("delegate")); + assert.equal(transaction.value, 1000000000000000000n); + }); + + it("should create 'Transaction' for claiming rewards", async function () { + const sender = Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + const delegationContract = Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX); + + const transaction = delegationFactory.createTransactionForClaimingRewards(sender, { + delegationContract: delegationContract, + }); + + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX)); + assert.isDefined(transaction.data); + assert.deepEqual(transaction.data, Buffer.from("claimRewards")); + }); + + it("should create 'Transaction' for redelegating rewards", async function () { + const sender = Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + const delegationContract = Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX); + + const transaction = delegationFactory.createTransactionForRedelegatingRewards(sender, { + delegationContract: delegationContract, + }); + + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX)); + assert.isDefined(transaction.data); + assert.deepEqual(transaction.data, Buffer.from("reDelegateRewards")); + }); + + it("should create 'Transaction' for undelegating", async function () { + const sender = Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + const delegationContract = Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX); + + const transaction = delegationFactory.createTransactionForUndelegating(sender, { + delegationContract: delegationContract, + amount: 1000000000000000000n, + }); + + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX)); + assert.isDefined(transaction.data); + assert.deepEqual(transaction.data, Buffer.from("unDelegate@0de0b6b3a7640000")); + assert.equal(transaction.value, 0n); + }); + + it("should create 'Transaction' for withdrawing", async function () { + const sender = Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + const delegationContract = Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX); + + const transaction = delegationFactory.createTransactionForWithdrawing(sender, { + delegationContract: delegationContract, + }); + + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX)); + assert.isDefined(transaction.data); + assert.deepEqual(transaction.data, Buffer.from("withdraw")); + assert.equal(transaction.value, 0n); + }); +}); diff --git a/src/transactionsFactories/delegationTransactionsFactory.ts b/src/delegation/delegationTransactionsFactory.ts similarity index 63% rename from src/transactionsFactories/delegationTransactionsFactory.ts rename to src/delegation/delegationTransactionsFactory.ts index 0b8062ec2..bfd2d5b3d 100644 --- a/src/transactionsFactories/delegationTransactionsFactory.ts +++ b/src/delegation/delegationTransactionsFactory.ts @@ -1,10 +1,10 @@ -import { Address } from "../address"; -import { DELEGATION_MANAGER_SC_ADDRESS_HEX } from "../constants"; -import { Err } from "../errors"; -import { IAddress } from "../interface"; -import { ArgSerializer, BigUIntValue, BytesValue, StringValue } from "../smartcontracts"; -import { Transaction } from "../transaction"; -import { TransactionBuilder } from "./transactionBuilder"; +import { ArgSerializer, BigUIntValue, BytesValue, StringValue } from "../abi"; +import { Address } from "../core/address"; +import { DELEGATION_MANAGER_SC_ADDRESS_HEX } from "../core/constants"; +import { Err } from "../core/errors"; +import { Transaction } from "../core/transaction"; +import { TransactionBuilder } from "../core/transactionBuilder"; +import * as resources from "./resources"; interface IConfig { chainID: string; @@ -20,10 +20,6 @@ interface IConfig { additionalGasLimitForDelegationOperations: bigint; } -interface IValidatorPublicKey { - hex(): string; -} - /** * Use this class to create delegation related transactions like creating a new delegation contract or adding nodes. */ @@ -35,15 +31,13 @@ export class DelegationTransactionsFactory { constructor(options: { config: IConfig }) { this.config = options.config; this.argSerializer = new ArgSerializer(); - this.delegationManagerAddress = Address.fromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX, this.config.addressHrp); + this.delegationManagerAddress = Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX, this.config.addressHrp); } - createTransactionForNewDelegationContract(options: { - sender: IAddress; - totalDelegationCap: bigint; - serviceFee: bigint; - amount: bigint; - }): Transaction { + createTransactionForNewDelegationContract( + sender: Address, + options: resources.NewDelegationContractInput, + ): Transaction { const dataParts = [ "createNewDelegationContract", ...this.argSerializer.valuesToStrings([ @@ -57,7 +51,7 @@ export class DelegationTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, + sender: sender, receiver: this.delegationManagerAddress, dataParts: dataParts, gasLimit: executionGasLimit, @@ -66,12 +60,7 @@ export class DelegationTransactionsFactory { }).build(); } - createTransactionForAddingNodes(options: { - sender: IAddress; - delegationContract: IAddress; - publicKeys: IValidatorPublicKey[]; - signedMessages: Uint8Array[]; - }): Transaction { + createTransactionForAddingNodes(sender: Address, options: resources.AddNodesInput): Transaction { if (options.publicKeys.length !== options.signedMessages.length) { throw new Err("The number of public keys should match the number of signed messages"); } @@ -90,7 +79,7 @@ export class DelegationTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, + sender: sender, receiver: options.delegationContract, dataParts: dataParts, gasLimit: this.computeExecutionGasLimitForNodesManagement(numNodes), @@ -98,11 +87,7 @@ export class DelegationTransactionsFactory { }).build(); } - createTransactionForRemovingNodes(options: { - sender: IAddress; - delegationContract: IAddress; - publicKeys: IValidatorPublicKey[]; - }): Transaction { + createTransactionForRemovingNodes(sender: Address, options: resources.ManageNodesInput): Transaction { const dataParts = ["removeNodes"]; for (const key of options.publicKeys) { @@ -112,7 +97,7 @@ export class DelegationTransactionsFactory { const numNodes = options.publicKeys.length; return new TransactionBuilder({ config: this.config, - sender: options.sender, + sender: sender, receiver: options.delegationContract, dataParts: dataParts, gasLimit: this.computeExecutionGasLimitForNodesManagement(numNodes), @@ -120,11 +105,7 @@ export class DelegationTransactionsFactory { }).build(); } - createTransactionForStakingNodes(options: { - sender: IAddress; - delegationContract: IAddress; - publicKeys: IValidatorPublicKey[]; - }): Transaction { + createTransactionForStakingNodes(sender: Address, options: resources.ManageNodesInput): Transaction { let dataParts = ["stakeNodes"]; for (const key of options.publicKeys) { @@ -139,7 +120,7 @@ export class DelegationTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, + sender: sender, receiver: options.delegationContract, dataParts: dataParts, gasLimit: executionGasLimit, @@ -147,11 +128,7 @@ export class DelegationTransactionsFactory { }).build(); } - createTransactionForUnbondingNodes(options: { - sender: IAddress; - delegationContract: IAddress; - publicKeys: IValidatorPublicKey[]; - }): Transaction { + createTransactionForUnbondingNodes(sender: Address, options: resources.ManageNodesInput): Transaction { let dataParts = ["unBondNodes"]; for (const key of options.publicKeys) { @@ -166,7 +143,7 @@ export class DelegationTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, + sender: sender, receiver: options.delegationContract, dataParts: dataParts, gasLimit: executionGasLimit, @@ -174,11 +151,7 @@ export class DelegationTransactionsFactory { }).build(); } - createTransactionForUnstakingNodes(options: { - sender: IAddress; - delegationContract: IAddress; - publicKeys: IValidatorPublicKey[]; - }): Transaction { + createTransactionForUnstakingNodes(sender: Address, options: resources.ManageNodesInput): Transaction { let dataParts = ["unStakeNodes"]; for (const key of options.publicKeys) { @@ -193,7 +166,7 @@ export class DelegationTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, + sender: sender, receiver: options.delegationContract, dataParts: dataParts, gasLimit: executionGasLimit, @@ -201,12 +174,7 @@ export class DelegationTransactionsFactory { }).build(); } - createTransactionForUnjailingNodes(options: { - sender: IAddress; - delegationContract: IAddress; - publicKeys: IValidatorPublicKey[]; - amount: bigint; - }): Transaction { + createTransactionForUnjailingNodes(sender: Address, options: resources.UnjailingNodesInput): Transaction { const dataParts = ["unJailNodes"]; for (const key of options.publicKeys) { @@ -216,7 +184,7 @@ export class DelegationTransactionsFactory { const numNodes = options.publicKeys.length; return new TransactionBuilder({ config: this.config, - sender: options.sender, + sender: sender, receiver: options.delegationContract, dataParts: dataParts, gasLimit: this.computeExecutionGasLimitForNodesManagement(numNodes), @@ -225,11 +193,7 @@ export class DelegationTransactionsFactory { }).build(); } - createTransactionForChangingServiceFee(options: { - sender: IAddress; - delegationContract: IAddress; - serviceFee: bigint; - }): Transaction { + createTransactionForChangingServiceFee(sender: Address, options: resources.ChangeServiceFee): Transaction { const dataParts = [ "changeServiceFee", this.argSerializer.valuesToStrings([new BigUIntValue(options.serviceFee)])[0], @@ -239,7 +203,7 @@ export class DelegationTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, + sender: sender, receiver: options.delegationContract, dataParts: dataParts, gasLimit: gasLimit, @@ -247,11 +211,10 @@ export class DelegationTransactionsFactory { }).build(); } - createTransactionForModifyingDelegationCap(options: { - sender: IAddress; - delegationContract: IAddress; - delegationCap: bigint; - }): Transaction { + createTransactionForModifyingDelegationCap( + sender: Address, + options: resources.ModifyDelegationCapInput, + ): Transaction { const dataParts = [ "modifyTotalDelegationCap", this.argSerializer.valuesToStrings([new BigUIntValue(options.delegationCap)])[0], @@ -261,7 +224,7 @@ export class DelegationTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, + sender: sender, receiver: options.delegationContract, dataParts: dataParts, gasLimit: gasLimit, @@ -269,17 +232,17 @@ export class DelegationTransactionsFactory { }).build(); } - createTransactionForSettingAutomaticActivation(options: { - sender: IAddress; - delegationContract: IAddress; - }): Transaction { + createTransactionForSettingAutomaticActivation( + sender: Address, + options: resources.ManageDelegationContractInput, + ): Transaction { const dataParts = ["setAutomaticActivation", this.argSerializer.valuesToStrings([new StringValue("true")])[0]]; const gasLimit = this.config.gasLimitDelegationOperations + this.config.additionalGasLimitForDelegationOperations; return new TransactionBuilder({ config: this.config, - sender: options.sender, + sender: sender, receiver: options.delegationContract, dataParts: dataParts, gasLimit: gasLimit, @@ -287,17 +250,17 @@ export class DelegationTransactionsFactory { }).build(); } - createTransactionForUnsettingAutomaticActivation(options: { - sender: IAddress; - delegationContract: IAddress; - }): Transaction { + createTransactionForUnsettingAutomaticActivation( + sender: Address, + options: resources.ManageDelegationContractInput, + ): Transaction { const dataParts = ["setAutomaticActivation", this.argSerializer.valuesToStrings([new StringValue("false")])[0]]; const gasLimit = this.config.gasLimitDelegationOperations + this.config.additionalGasLimitForDelegationOperations; return new TransactionBuilder({ config: this.config, - sender: options.sender, + sender: sender, receiver: options.delegationContract, dataParts: dataParts, gasLimit: gasLimit, @@ -305,10 +268,10 @@ export class DelegationTransactionsFactory { }).build(); } - createTransactionForSettingCapCheckOnRedelegateRewards(options: { - sender: IAddress; - delegationContract: IAddress; - }): Transaction { + createTransactionForSettingCapCheckOnRedelegateRewards( + sender: Address, + options: resources.ManageDelegationContractInput, + ): Transaction { const dataParts = [ "setCheckCapOnReDelegateRewards", this.argSerializer.valuesToStrings([new StringValue("true")])[0], @@ -318,7 +281,7 @@ export class DelegationTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, + sender: sender, receiver: options.delegationContract, dataParts: dataParts, gasLimit: gasLimit, @@ -326,10 +289,10 @@ export class DelegationTransactionsFactory { }).build(); } - createTransactionForUnsettingCapCheckOnRedelegateRewards(options: { - sender: IAddress; - delegationContract: IAddress; - }): Transaction { + createTransactionForUnsettingCapCheckOnRedelegateRewards( + sender: Address, + options: resources.ManageDelegationContractInput, + ): Transaction { const dataParts = [ "setCheckCapOnReDelegateRewards", this.argSerializer.valuesToStrings([new StringValue("false")])[0], @@ -339,7 +302,7 @@ export class DelegationTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, + sender: sender, receiver: options.delegationContract, dataParts: dataParts, gasLimit: gasLimit, @@ -347,13 +310,7 @@ export class DelegationTransactionsFactory { }).build(); } - createTransactionForSettingMetadata(options: { - sender: IAddress; - delegationContract: IAddress; - name: string; - website: string; - identifier: string; - }): Transaction { + createTransactionForSettingMetadata(sender: Address, options: resources.SetContractMetadataInput): Transaction { const dataParts = [ "setMetaData", ...this.argSerializer.valuesToStrings([ @@ -368,7 +325,7 @@ export class DelegationTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, + sender: sender, receiver: options.delegationContract, dataParts: dataParts, gasLimit: gasLimit, @@ -376,6 +333,88 @@ export class DelegationTransactionsFactory { }).build(); } + createTransactionForDelegating(sender: Address, options: resources.DelegateActionsInput): Transaction { + const dataParts = ["delegate"]; + const gasLimit = + this.config.gasLimitDelegationOperations + this.config.additionalGasLimitForDelegationOperations; + + return new TransactionBuilder({ + config: this.config, + sender: sender, + receiver: options.delegationContract, + dataParts: dataParts, + gasLimit: gasLimit, + amount: options.amount, + addDataMovementGas: false, + }).build(); + } + + createTransactionForClaimingRewards( + sender: Address, + options: resources.ManageDelegationContractInput, + ): Transaction { + const dataParts = ["claimRewards"]; + const gasLimit = + this.config.gasLimitDelegationOperations + this.config.additionalGasLimitForDelegationOperations; + + return new TransactionBuilder({ + config: this.config, + sender: sender, + receiver: options.delegationContract, + dataParts: dataParts, + gasLimit: gasLimit, + addDataMovementGas: false, + }).build(); + } + + createTransactionForRedelegatingRewards( + sender: Address, + options: resources.ManageDelegationContractInput, + ): Transaction { + const dataParts = ["reDelegateRewards"]; + const gasLimit = + this.config.gasLimitDelegationOperations + this.config.additionalGasLimitForDelegationOperations; + + return new TransactionBuilder({ + config: this.config, + sender: sender, + receiver: options.delegationContract, + dataParts: dataParts, + gasLimit: gasLimit, + addDataMovementGas: false, + }).build(); + } + + createTransactionForUndelegating(sender: Address, options: resources.DelegateActionsInput): Transaction { + const dataParts = ["unDelegate", this.argSerializer.valuesToStrings([new BigUIntValue(options.amount)])[0]]; + const gasLimit = + this.config.gasLimitDelegationOperations + this.config.additionalGasLimitForDelegationOperations; + + return new TransactionBuilder({ + config: this.config, + sender: sender, + receiver: options.delegationContract, + dataParts: dataParts, + gasLimit: gasLimit, + addDataMovementGas: false, + }).build(); + } + + createTransactionForWithdrawing(sender: Address, options: resources.ManageDelegationContractInput): Transaction { + const dataParts = ["withdraw"]; + const gasLimit = + this.config.gasLimitDelegationOperations + this.config.additionalGasLimitForDelegationOperations; + + return new TransactionBuilder({ + config: this.config, + sender: sender, + receiver: options.delegationContract, + dataParts: dataParts, + gasLimit: gasLimit, + addDataMovementGas: false, + }).build(); + } + private computeExecutionGasLimitForNodesManagement(numNodes: number): bigint { const additionalGasForAllNodes = this.config.additionalGasLimitPerValidatorNode * BigInt(numNodes); diff --git a/src/transactionsOutcomeParsers/delegationTransactionsOutcomeParser.spec.ts b/src/delegation/delegationTransactionsOutcomeParser.spec.ts similarity index 65% rename from src/transactionsOutcomeParsers/delegationTransactionsOutcomeParser.spec.ts rename to src/delegation/delegationTransactionsOutcomeParser.spec.ts index c57ac28c4..cd88c5edf 100644 --- a/src/transactionsOutcomeParsers/delegationTransactionsOutcomeParser.spec.ts +++ b/src/delegation/delegationTransactionsOutcomeParser.spec.ts @@ -1,14 +1,14 @@ import { assert } from "chai"; -import { Address } from "../address"; +import { Address, TransactionEvent, TransactionLogs, TransactionOnNetwork } from "../core"; import { b64TopicsToBytes } from "../testutils"; +import { SmartContractResult } from "../transactionsOutcomeParsers"; import { DelegationTransactionsOutcomeParser } from "./delegationTransactionsOutcomeParser"; -import { SmartContractResult, TransactionEvent, TransactionLogs, TransactionOutcome } from "./resources"; describe("test delegation transactions outcome parser", () => { const parser = new DelegationTransactionsOutcomeParser(); it("should test parseCreateNewDelegationContract ", () => { - const contractAddress = Address.fromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqy8lllls62y8s5"); + const contractAddress = Address.newFromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqy8lllls62y8s5"); let encodedTopics = [ "Q8M8GTdWSAAA", "Q8M8GTdWSAAA", @@ -18,7 +18,7 @@ describe("test delegation transactions outcome parser", () => { ]; const delegateEvent = new TransactionEvent({ - address: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), identifier: "delegate", topics: b64TopicsToBytes(encodedTopics), }); @@ -28,7 +28,7 @@ describe("test delegation transactions outcome parser", () => { "PDXX6ssamaSgzKpTfvDMCuEJ9B9sK0AiA+Yzv7sHH1w=", ]; const scDeployEvent = new TransactionEvent({ - address: "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqy8lllls62y8s5", + address: new Address("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqy8lllls62y8s5"), identifier: "SCDeploy", topics: b64TopicsToBytes(encodedTopics), }); @@ -37,29 +37,28 @@ describe("test delegation transactions outcome parser", () => { encodedTopics = ["b2g6sUl6beG17FCUIkFwCOTGJjoJJi5SjkP2077e6xA="]; const scResultEvent = new TransactionEvent({ - address: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), identifier: "completedTxEvent", topics: b64TopicsToBytes(encodedTopics), }); const scResultLog = new TransactionLogs({ - address: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), events: [scResultEvent], }); const scResult = new SmartContractResult({ - sender: "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6", - receiver: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", + sender: new Address("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6"), + receiver: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), data: Buffer.from( "QDZmNmJAMDAwMDAwMDAwMDAwMDAwMDAwMDEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAxMGZmZmZmZg==", "base64", ), logs: scResultLog, }); + const transaction = new TransactionOnNetwork({ smartContractResults: [scResult], logs: logs }); - const txOutcome = new TransactionOutcome({ smartContractResults: [scResult], logs: logs }); - - const outcome = parser.parseCreateNewDelegationContract(txOutcome); + const outcome = parser.parseCreateNewDelegationContract(transaction); assert.lengthOf(outcome, 1); assert.equal(outcome[0].contractAddress, contractAddress.toBech32()); diff --git a/src/delegation/delegationTransactionsOutcomeParser.ts b/src/delegation/delegationTransactionsOutcomeParser.ts new file mode 100644 index 000000000..137a28f68 --- /dev/null +++ b/src/delegation/delegationTransactionsOutcomeParser.ts @@ -0,0 +1,42 @@ +import { Address } from "../core/address"; +import { ErrParseTransactionOutcome } from "../core/errors"; +import { TransactionEvent } from "../core/transactionEvents"; +import { TransactionOnNetwork } from "../core/transactionOnNetwork"; +import { findEventsByIdentifier } from "../transactionsOutcomeParsers/resources"; + +export class DelegationTransactionsOutcomeParser { + constructor() {} + + parseCreateNewDelegationContract(transaction: TransactionOnNetwork): { contractAddress: string }[] { + this.ensureNoError(transaction.logs.events); + + const events = findEventsByIdentifier(transaction, "SCDeploy"); + + return events.map((event) => ({ contractAddress: this.extractContractAddress(event) })); + } + + private ensureNoError(transactionEvents: TransactionEvent[]) { + for (const event of transactionEvents) { + if (event.identifier == "signalError") { + const data = Buffer.from(event.additionalData[0]?.toString().slice(1)).toString() || ""; + const message = this.decodeTopicAsString(event.topics[1]); + + throw new ErrParseTransactionOutcome( + `encountered signalError: ${message} (${Buffer.from(data, "hex").toString()})`, + ); + } + } + } + + private extractContractAddress(event: TransactionEvent): string { + if (!event.topics[0]?.length) { + return ""; + } + const address = Buffer.from(event.topics[0]); + return new Address(address).toBech32(); + } + + private decodeTopicAsString(topic: Uint8Array): string { + return Buffer.from(topic).toString(); + } +} diff --git a/src/delegation/index.ts b/src/delegation/index.ts new file mode 100644 index 000000000..648988539 --- /dev/null +++ b/src/delegation/index.ts @@ -0,0 +1,4 @@ +export * from "./delegationController"; +export * from "./delegationTransactionsFactory"; +export * from "./delegationTransactionsOutcomeParser"; +export * from "./resources"; diff --git a/src/delegation/resources.ts b/src/delegation/resources.ts new file mode 100644 index 000000000..b09a9f5a8 --- /dev/null +++ b/src/delegation/resources.ts @@ -0,0 +1,17 @@ +import { Address } from "../core/address"; +import { ValidatorPublicKey } from "../wallet"; + +export type NewDelegationContractInput = { totalDelegationCap: bigint; serviceFee: bigint; amount: bigint }; +export type AddNodesInput = ManageNodesInput & { signedMessages: Uint8Array[] }; +export type UnjailingNodesInput = ManageNodesInput & { amount: bigint }; +export type ManageNodesInput = { delegationContract: Address; publicKeys: ValidatorPublicKey[] }; +export type ChangeServiceFee = { delegationContract: Address; serviceFee: bigint }; +export type ModifyDelegationCapInput = { delegationContract: Address; delegationCap: bigint }; +export type ManageDelegationContractInput = { delegationContract: Address }; +export type DelegateActionsInput = { delegationContract: Address; amount: bigint }; +export type SetContractMetadataInput = { + delegationContract: Address; + name: string; + website: string; + identifier: string; +}; diff --git a/src/entrypoints/config.ts b/src/entrypoints/config.ts new file mode 100644 index 000000000..479cd1e2c --- /dev/null +++ b/src/entrypoints/config.ts @@ -0,0 +1,68 @@ +export interface EntrypointConfig { + networkProviderUrl: string; + networkProviderKind: string; + chainId: string; +} + +export class TestnetEntrypointConfig { + networkProviderUrl: string; + networkProviderKind: string; + chainId: string; + + constructor({ + networkProviderUrl = "https://testnet-api.multiversx.com", + networkProviderKind = "api", + chainId = "T", + }: Partial = {}) { + this.networkProviderUrl = networkProviderUrl; + this.networkProviderKind = networkProviderKind; + this.chainId = chainId; + } +} + +export class DevnetEntrypointConfig { + networkProviderUrl: string; + networkProviderKind: string; + chainId: string; + constructor({ + networkProviderUrl = "https://devnet-api.multiversx.com", + networkProviderKind = "api", + chainId = "D", + }: Partial = {}) { + this.networkProviderUrl = networkProviderUrl; + this.networkProviderKind = networkProviderKind; + this.chainId = chainId; + } +} + +export class MainnetEntrypointConfig { + networkProviderUrl: string; + networkProviderKind: string; + chainId: string; + + constructor({ + networkProviderUrl = "https://api.multiversx.com", + networkProviderKind = "api", + chainId = "1", + }: Partial = {}) { + this.networkProviderUrl = networkProviderUrl; + this.networkProviderKind = networkProviderKind; + this.chainId = chainId; + } +} + +export class LocalnetEntrypointConfig { + networkProviderUrl: string; + networkProviderKind: string; + chainId: string; + + constructor({ + networkProviderUrl = "http://localhost:7950", + networkProviderKind = "proxy", + chainId = "localnet", + }: Partial = {}) { + this.networkProviderUrl = networkProviderUrl; + this.networkProviderKind = networkProviderKind; + this.chainId = chainId; + } +} diff --git a/src/entrypoints/entrypoints.spec.ts b/src/entrypoints/entrypoints.spec.ts new file mode 100644 index 000000000..5febc135a --- /dev/null +++ b/src/entrypoints/entrypoints.spec.ts @@ -0,0 +1,137 @@ +import { assert } from "chai"; +import { readFileSync } from "fs"; +import path from "path"; +import { Account } from "../accounts/account"; +import { loadAbiRegistry } from "../testutils"; +import { DevnetEntrypoint } from "./entrypoints"; + +describe("TestEntrypoint", function () { + const entrypoint = new DevnetEntrypoint(); + + before(async function () {}); + + it("native transfer", async () => { + const controller = entrypoint.createTransfersController(); + const filePath = path.join("src", "testdata", "testwallets", "alice.pem"); + const sender = await Account.newFromPem(filePath); + sender.nonce = 77777n; + + const transaction = await controller.createTransactionForTransfer( + sender, + BigInt(sender.getNonceThenIncrement().valueOf()), + { + receiver: sender.address, + nativeAmount: BigInt(0), + data: Buffer.from("hello"), + }, + ); + assert.equal( + Buffer.from(transaction.signature).toString("hex"), + "69bc7d1777edd0a901e6cf94830475716205c5efdf2fd44d4be31badead59fc8418b34f0aa3b2c80ba14aed5edd30031757d826af58a1abb690a0bee89ba9309", + ); + }); + + it("native transfer with gas options", async () => { + const controller = entrypoint.createTransfersController(); + const filePath = path.join("src", "testdata", "testwallets", "alice.pem"); + const sender = await Account.newFromPem(filePath); + sender.nonce = 77777n; + + const gasLimit = BigInt(50000); + const gasPrice = BigInt(1000); + + const transaction = await controller.createTransactionForTransfer( + sender, + BigInt(sender.getNonceThenIncrement().valueOf()), + { + receiver: sender.address, + nativeAmount: BigInt(0), + data: Buffer.from("hello"), + gasLimit: gasLimit, + gasPrice: gasPrice, + }, + ); + + assert.equal(transaction.gasLimit, gasLimit, "Gas limit should be set correctly"); + assert.equal(transaction.gasPrice, gasPrice, "Gas price should be set correctly"); + }); + + it("native transfer with guardian and relayer", async () => { + const controller = entrypoint.createTransfersController(); + const filePath = path.join("src", "testdata", "testwallets"); + const sender = await Account.newFromPem(path.join(filePath, "alice.pem")); + const grace = await Account.newFromPem(path.join(filePath, "grace.pem")); + sender.nonce = 77777n; + + const transaction = await controller.createTransactionForTransfer( + sender, + BigInt(sender.getNonceThenIncrement().valueOf()), + { + receiver: sender.address, + nativeAmount: BigInt(0), + data: Buffer.from("hello"), + guardian: grace.address, + relayer: grace.address, + }, + ); + assert.deepEqual(transaction.guardian, grace.address); + assert.deepEqual(transaction.relayer, grace.address); + assert.deepEqual(transaction.guardianSignature, new Uint8Array()); + + assert.deepEqual(transaction.relayerSignature, new Uint8Array()); + }); + + it("contract flow", async function () { + this.timeout(30000); + const abi = await loadAbiRegistry("src/testdata/adder.abi.json"); + const filePath = path.join("src", "testdata", "testwallets", "grace.pem"); + const sender = await Account.newFromPem(filePath); + sender.nonce = await entrypoint.recallAccountNonce(sender.address); + + const controller = entrypoint.createSmartContractController(abi); + const bytecode = readFileSync("src/testdata/adder.wasm"); + + const transaction = await controller.createTransactionForDeploy( + sender, + BigInt(sender.getNonceThenIncrement().valueOf()), + { + bytecode, + gasLimit: BigInt(10_000_000), + arguments: [0], + }, + ); + + const txHash = await entrypoint.sendTransaction(transaction); + const outcome = await controller.awaitCompletedDeploy(txHash); + + assert.equal(outcome.contracts.length, 1); + + const contractAddress = outcome.contracts[0].address; + + const executeTransaction = await controller.createTransactionForExecute( + sender, + BigInt(sender.getNonceThenIncrement().valueOf()), + { + contract: contractAddress, + gasLimit: BigInt(10_000_000), + function: "add", + arguments: [7], + }, + ); + + const txHashExecute = await entrypoint.sendTransaction(executeTransaction); + await entrypoint.awaitCompletedTransaction(txHashExecute); + + const queryResult = await controller.query({ contract: contractAddress, function: "getSum", arguments: [] }); + assert.equal(queryResult.length, 1); + assert.equal(queryResult[0], 7); + }); + + it("create account", async () => { + const account = await entrypoint.createAccount(); + assert.isNotNull(account); + assert.isNotNull(account.address); + assert.equal(account.secretKey.valueOf().length, 32); + assert.equal(account.publicKey.valueOf().length, 32); + }); +}); diff --git a/src/entrypoints/entrypoints.ts b/src/entrypoints/entrypoints.ts new file mode 100644 index 000000000..ffdcc7d95 --- /dev/null +++ b/src/entrypoints/entrypoints.ts @@ -0,0 +1,207 @@ +import { Abi } from "../abi"; +import { AccountController, AccountTransactionsFactory } from "../accountManagement"; +import { Account } from "../accounts"; +import { + Address, + ErrInvalidNetworkProviderKind, + IAccount, + Message, + Transaction, + TransactionOnNetwork, + TransactionsFactoryConfig, + TransactionWatcher, +} from "../core"; +import { DelegationController, DelegationTransactionsFactory } from "../delegation"; +import { ApiNetworkProvider, ProxyNetworkProvider } from "../networkProviders"; +import { INetworkProvider } from "../networkProviders/interface"; +import { SmartContractTransactionsFactory } from "../smartContracts"; +import { SmartContractController } from "../smartContracts/smartContractController"; +import { TokenManagementController, TokenManagementTransactionsFactory } from "../tokenManagement"; +import { TransfersController, TransferTransactionsFactory } from "../transfers"; +import { UserSecretKey } from "../wallet"; +import { DevnetEntrypointConfig, MainnetEntrypointConfig, TestnetEntrypointConfig } from "./config"; + +class NetworkEntrypoint { + private networkProvider: INetworkProvider; + private chainId: string; + + constructor(options: { networkProviderUrl: string; networkProviderKind: string; chainId: string }) { + if (options.networkProviderKind === "proxy") { + this.networkProvider = new ProxyNetworkProvider(options.networkProviderUrl); + } else if (options.networkProviderKind === "api") { + this.networkProvider = new ApiNetworkProvider(options.networkProviderUrl); + } else { + throw new ErrInvalidNetworkProviderKind(); + } + + this.chainId = options.chainId; + } + + /** + * Creates a new Account by generating a new secret key and instantiating an UserSigner + */ + async createAccount(): Promise { + const secretKey = UserSecretKey.generate(); + return new Account(secretKey); + } + + /** + * Calls a faucet + */ + async getAirdrop(_address: Address): Promise { + throw new Error("Not implemented"); + } + + async signTransaction(transaction: Transaction, account: IAccount): Promise { + transaction.signature = await account.signTransaction(transaction); + } + + /** + * Verifies if the signature field is valid + * @param transaction + * @param account + */ + async verifyTransactionSignature(transaction: Transaction, account: IAccount): Promise { + return await account.verifyTransactionSignature(transaction, transaction.signature); + } + + /** + * Verifies if message signature is valid + * @param message + * @param account + */ + async verifyMessageSignature(message: Message, account: IAccount): Promise { + if (!message.address) { + throw new Error("`address` property of Message is not set"); + } + + if (!message.signature) { + throw new Error("`signature` property of Message is not set"); + } + + return await account.verifyMessageSignature(message, message.signature); + } + + /** + * Fetches the account nonce from the network. + * @param address + */ + async recallAccountNonce(address: Address): Promise { + return (await this.networkProvider.getAccount(address)).nonce; + } + + /** + * Function of the network provider, promoted to the entrypoint. + * @param transactions + */ + sendTransactions(transactions: Transaction[]): Promise<[number, string[]]> { + return this.networkProvider.sendTransactions(transactions); + } + + /** + * Function of the network provider, promoted to the entrypoint. + * @param transaction + */ + sendTransaction(transaction: Transaction): Promise { + return this.networkProvider.sendTransaction(transaction); + } + + /** + * Generic function to await a transaction on the network. + * @param txHash + */ + async awaitCompletedTransaction(txHash: string): Promise { + const transactionAwaiter = new TransactionWatcher(this.networkProvider); + return transactionAwaiter.awaitCompleted(txHash); + } + + getTransaction(txHash: string): Promise { + return this.networkProvider.getTransaction(txHash); + } + + /** + * Access to the underlying network provider. + */ + createNetworkProvider(): INetworkProvider { + return this.networkProvider; + } + + createDelegationController(): DelegationController { + return new DelegationController({ chainID: this.chainId, networkProvider: this.networkProvider }); + } + + createDelegationTransactionsFactory(): DelegationTransactionsFactory { + return new DelegationTransactionsFactory({ config: new TransactionsFactoryConfig({ chainID: this.chainId }) }); + } + + createAccountController(): AccountController { + return new AccountController({ chainID: this.chainId }); + } + + createAccountTransactionsFactory(): AccountTransactionsFactory { + return new AccountTransactionsFactory({ config: new TransactionsFactoryConfig({ chainID: this.chainId }) }); + } + + createSmartContractController(abi?: Abi): SmartContractController { + return new SmartContractController({ chainID: this.chainId, networkProvider: this.networkProvider, abi }); + } + + createSmartContractTransactionsFactory(): SmartContractTransactionsFactory { + return new SmartContractTransactionsFactory({ + config: new TransactionsFactoryConfig({ chainID: this.chainId }), + }); + } + + createTokenManagementController(): TokenManagementController { + return new TokenManagementController({ chainID: this.chainId, networkProvider: this.networkProvider }); + } + + createTokenManagementTransactionsFactory(): TokenManagementTransactionsFactory { + return new TokenManagementTransactionsFactory({ + config: new TransactionsFactoryConfig({ chainID: this.chainId }), + }); + } + + createTransfersController(): TransfersController { + return new TransfersController({ chainID: this.chainId }); + } + + createTransfersTransactionsFactory(): TransferTransactionsFactory { + return new TransferTransactionsFactory({ + config: new TransactionsFactoryConfig({ chainID: this.chainId }), + }); + } +} + +export class TestnetEntrypoint extends NetworkEntrypoint { + constructor(url?: string, kind?: string) { + const entrypointConfig = new TestnetEntrypointConfig(); + super({ + networkProviderUrl: url || entrypointConfig.networkProviderUrl, + networkProviderKind: kind || entrypointConfig.networkProviderKind, + chainId: entrypointConfig.chainId, + }); + } +} + +export class DevnetEntrypoint extends NetworkEntrypoint { + constructor(url?: string, kind?: string) { + const entrypointConfig = new DevnetEntrypointConfig(); + super({ + networkProviderUrl: url || entrypointConfig.networkProviderUrl, + networkProviderKind: kind || entrypointConfig.networkProviderKind, + chainId: entrypointConfig.chainId, + }); + } +} + +export class MainnetEntrypoint extends NetworkEntrypoint { + constructor(url?: string, kind?: string) { + const entrypointConfig = new MainnetEntrypointConfig(); + super({ + networkProviderUrl: url || entrypointConfig.networkProviderUrl, + networkProviderKind: kind || entrypointConfig.networkProviderKind, + chainId: entrypointConfig.chainId, + }); + } +} diff --git a/src/entrypoints/index.ts b/src/entrypoints/index.ts new file mode 100644 index 000000000..311ba2137 --- /dev/null +++ b/src/entrypoints/index.ts @@ -0,0 +1,2 @@ +export * from "./config"; +export * from "./entrypoints"; diff --git a/src/gasEstimator.spec.ts b/src/gasEstimator.spec.ts deleted file mode 100644 index e1394717f..000000000 --- a/src/gasEstimator.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { assert } from "chai"; -import { GasEstimator } from "./gasEstimator"; - -describe("test gas estimator", () => { - it("should estimate gas limit (default gas configuration)", () => { - const estimator = new GasEstimator(); - - assert.equal(estimator.forEGLDTransfer(0), 50000); - assert.equal(estimator.forEGLDTransfer(3), 50000 + 3 * 1500); - - assert.equal(estimator.forESDTTransfer(80), 50000 + 80 * 1500 + 200000 + 100000); - assert.equal(estimator.forESDTTransfer(100), 50000 + 100 * 1500 + 200000 + 100000); - - assert.equal(estimator.forESDTNFTTransfer(80), 50000 + 80 * 1500 + 200000 + 800000); - assert.equal(estimator.forESDTNFTTransfer(100), 50000 + 100 * 1500 + 200000 + 800000); - - assert.equal(estimator.forMultiESDTNFTTransfer(80, 1), 50000 + 80 * 1500 + (200000 + 800000) * 1); - assert.equal(estimator.forMultiESDTNFTTransfer(80, 3), 50000 + 80 * 1500 + (200000 + 800000) * 3); - }); - - it("should estimate gas limit (custom gas configuration)", () => { - const estimator = new GasEstimator({ - minGasLimit: 10000, - gasPerDataByte: 3000, - gasCostESDTTransfer: 200000, - gasCostESDTNFTTransfer: 300000, - gasCostESDTNFTMultiTransfer: 400000, - }); - - assert.equal(estimator.forEGLDTransfer(0), 10000); - assert.equal(estimator.forEGLDTransfer(3), 10000 + 3 * 3000); - - assert.equal(estimator.forESDTTransfer(80), 10000 + 80 * 3000 + 200000 + 100000); - assert.equal(estimator.forESDTTransfer(100), 10000 + 100 * 3000 + 200000 + 100000); - - assert.equal(estimator.forESDTNFTTransfer(80), 10000 + 80 * 3000 + 300000 + 800000); - assert.equal(estimator.forESDTNFTTransfer(100), 10000 + 100 * 3000 + 300000 + 800000); - - assert.equal(estimator.forMultiESDTNFTTransfer(80, 1), 10000 + 80 * 3000 + (400000 + 800000) * 1); - assert.equal(estimator.forMultiESDTNFTTransfer(80, 3), 10000 + 80 * 3000 + (400000 + 800000) * 3); - }); -}); diff --git a/src/gasEstimator.ts b/src/gasEstimator.ts deleted file mode 100644 index 716ddb27f..000000000 --- a/src/gasEstimator.ts +++ /dev/null @@ -1,74 +0,0 @@ -interface IGasConfiguration { - readonly minGasLimit: number; - readonly gasPerDataByte: number; - readonly gasCostESDTTransfer: number; - readonly gasCostESDTNFTTransfer: number; - readonly gasCostESDTNFTMultiTransfer: number; -} - -/** - * This is mirroring (on a best efforts basis) the network's gas configuration & gas schedule: - * - https://gateway.multiversx.com/network/config - * - https://github.com/multiversx/mx-chain-mainnet-config/tree/master/gasSchedules - * - https://github.com/multiversx/mx-chain-mainnet-config/blob/master/enableEpochs.toml#L200 - */ -export const DefaultGasConfiguration: IGasConfiguration = { - minGasLimit: 50000, - gasPerDataByte: 1500, - gasCostESDTTransfer: 200000, - gasCostESDTNFTTransfer: 200000, - gasCostESDTNFTMultiTransfer: 200000, -}; - -// Additional gas to account for eventual increases in gas requirements (thus avoid fast-breaking changes in clients of the library). -const ADDITIONAL_GAS_FOR_ESDT_TRANSFER = 100000; - -// Additional gas to account for extra blockchain operations (e.g. data movement (between accounts) for NFTs), -// and for eventual increases in gas requirements (thus avoid fast-breaking changes in clients of the library). -const ADDITIONAL_GAS_FOR_ESDT_NFT_TRANSFER = 800000; - -/** - * @deprecated This will be remove with the next release as the only place where it is used is a deprecated constructor. - */ -export class GasEstimator { - private readonly gasConfiguration: IGasConfiguration; - - constructor(gasConfiguration?: IGasConfiguration) { - this.gasConfiguration = gasConfiguration || DefaultGasConfiguration; - } - - forEGLDTransfer(dataLength: number) { - const gasLimit = this.gasConfiguration.minGasLimit + this.gasConfiguration.gasPerDataByte * dataLength; - - return gasLimit; - } - - forESDTTransfer(dataLength: number) { - const gasLimit = - this.gasConfiguration.minGasLimit + - this.gasConfiguration.gasCostESDTTransfer + - this.gasConfiguration.gasPerDataByte * dataLength + - ADDITIONAL_GAS_FOR_ESDT_TRANSFER; - - return gasLimit; - } - - forESDTNFTTransfer(dataLength: number) { - const gasLimit = - this.gasConfiguration.minGasLimit + - this.gasConfiguration.gasCostESDTNFTTransfer + - this.gasConfiguration.gasPerDataByte * dataLength + - ADDITIONAL_GAS_FOR_ESDT_NFT_TRANSFER; - - return gasLimit; - } - - forMultiESDTNFTTransfer(dataLength: number, numTransfers: number) { - const gasLimit = - this.gasConfiguration.minGasLimit + - (this.gasConfiguration.gasCostESDTNFTMultiTransfer + ADDITIONAL_GAS_FOR_ESDT_NFT_TRANSFER) * numTransfers + - this.gasConfiguration.gasPerDataByte * dataLength; - - return gasLimit; - } -} diff --git a/src/hash.ts b/src/hash.ts deleted file mode 100644 index 40efda328..000000000 --- a/src/hash.ts +++ /dev/null @@ -1,48 +0,0 @@ -import * as errors from "./errors"; - -export class Hash { - /** - * The hash, as a hex-encoded string. - */ - readonly hash: Buffer; - - /** - * Creates a new Hash object. - * - * @param hash The hash, as a Buffer or a hex-encoded string. - */ - constructor(hash: Buffer | string) { - if (!hash) { - this.hash = Buffer.from([]); - } else if (hash instanceof Buffer) { - this.hash = hash; - } else if (typeof hash === "string") { - this.hash = Buffer.from(hash, "hex"); - } else { - throw new errors.ErrBadType("hash", "buffer | string", hash); - } - } - - static empty(): Hash { - return new Hash(Buffer.from([])); - } - - /** - * Returns whether the hash is empty (not computed). - */ - isEmpty(): boolean { - return this.hash.length == 0; - } - - toString(): string { - return this.hex(); - } - - hex(): string { - return this.hash.toString("hex"); - } - - valueOf(): Buffer { - return this.hash; - } -} diff --git a/src/index.ts b/src/index.ts index 08c8affee..b9e1ef27e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,34 +4,15 @@ * @packageDocumentation */ -require("./globals"); - -export * from "./account"; -export * from "./adapters"; -export * from "./address"; -export * from "./asyncTimer"; -export * from "./config"; -export * from "./converters"; -export * from "./errors"; -export * from "./gasEstimator"; -export * from "./interface"; -export * from "./interfaceOfNetwork"; -export * from "./logger"; -export * from "./message"; -export * from "./networkParams"; -export * from "./relayedTransactionV1Builder"; -export * from "./relayedTransactionV2Builder"; -export * from "./signableMessage"; -export * from "./smartContractQueriesController"; -export * from "./smartcontracts"; -export * from "./tokenOperations"; -export * from "./tokens"; -export * from "./transaction"; -export * from "./transactionComputer"; -export * from "./transactionPayload"; -export * from "./transactionWatcher"; -export * from "./transactionsFactories"; -export * from "./transactionsOutcomeParsers"; -export * from "./utils"; +export * from "./abi"; +export * from "./accountManagement"; +export * from "./accounts"; +export * from "./core"; +export * from "./delegation"; +export * from "./entrypoints"; export * from "./networkProviders"; +export * from "./smartContracts"; +export * from "./tokenManagement"; +export * from "./transactionsOutcomeParsers"; +export * from "./transfers"; export * from "./wallet"; diff --git a/src/interface.ts b/src/interface.ts deleted file mode 100644 index f91d4b0af..000000000 --- a/src/interface.ts +++ /dev/null @@ -1,112 +0,0 @@ -import BigNumber from "bignumber.js"; -import { Address } from "./address"; -import { ITransactionOnNetwork } from "./interfaceOfNetwork"; - -export interface ITransactionFetcher { - /** - * Fetches the state of a {@link Transaction}. - */ - getTransaction(txHash: string): Promise; -} - -export interface IPlainTransactionObject { - nonce: number; - value: string; - receiver: string; - sender: string; - receiverUsername?: string; - senderUsername?: string; - guardian?: string; - relayer?: string; - gasPrice: number; - gasLimit: number; - data?: string; - chainID: string; - version: number; - options?: number; - signature?: string; - guardianSignature?: string; - relayerSignature?: string; -} - -export interface ISignature { - hex(): string; -} - -export interface IAddress { - bech32(): string; -} - -export interface ITransactionValue { - toString(): string; -} - -export interface IAccountBalance { - toString(): string; -} - -export interface INonce { - valueOf(): number; -} - -export interface IChainID { - valueOf(): string; -} - -export interface IGasLimit { - valueOf(): number; -} - -export interface IGasPrice { - valueOf(): number; -} - -export interface ITransactionVersion { - valueOf(): number; -} - -export interface ITransactionOptions { - valueOf(): number; -} - -export interface ITransactionPayload { - length(): number; - encoded(): string; - toString(): string; - valueOf(): Buffer; -} - -/** - * Legacy interface. The class `TokenTransfer` can be used instead, where necessary. - */ -export interface ITokenTransfer { - readonly tokenIdentifier: string; - readonly nonce: number; - readonly amountAsBigInteger: BigNumber.Value; - valueOf(): BigNumber.Value; -} - -/** - * @deprecated Use {@link ITokenTransfer} instead. - */ -export type ITokenPayment = ITokenTransfer; - -export interface ITransaction { - sender: string; - receiver: string; - gasLimit: bigint; - chainID: string; - nonce: bigint; - value: bigint; - senderUsername: string; - receiverUsername: string; - gasPrice: bigint; - data: Uint8Array; - version: number; - options: number; - guardian: string; - relayer: Address; - signature: Uint8Array; - guardianSignature: Uint8Array; - relayerSignature: Uint8Array; -} diff --git a/src/interfaceOfNetwork.ts b/src/interfaceOfNetwork.ts deleted file mode 100644 index eaee206aa..000000000 --- a/src/interfaceOfNetwork.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { IAccountBalance, IAddress } from "./interface"; - -export interface IAccountOnNetwork { - nonce: number; - balance: IAccountBalance; -} - -export interface INetworkConfig { - MinGasLimit: number; - GasPerDataByte: number; - GasPriceModifier: number; - ChainID: string; -} - -export interface ITransactionOnNetwork { - isCompleted?: boolean; - - hash: string; - type: string; - value: string; - receiver: IAddress; - sender: IAddress; - function?: string; - data: Buffer; - status: ITransactionStatus; - receipt: ITransactionReceipt; - contractResults: IContractResults; - logs: ITransactionLogs; -} - -export interface ITransactionStatus { - isPending(): boolean; - isFailed(): boolean; - isInvalid(): boolean; - isExecuted(): boolean; - isSuccessful(): boolean; - valueOf(): string; -} - -export interface ITransactionReceipt { - data: string; -} - -export interface IContractResults { - items: IContractResultItem[]; -} - -export interface IContractResultItem { - hash: string; - nonce: number; - receiver: IAddress; - sender: IAddress; - data: string; - returnMessage: string; - logs: ITransactionLogs; - previousHash?: string; -} - -export interface IContractQueryResponse { - returnCode: IContractReturnCode; - returnMessage: string; - getReturnDataParts(): Buffer[]; -} - -export interface IContractReturnCode { - toString(): string; -} - -export interface ITransactionLogs { - address: IAddress; - events: ITransactionEvent[]; - - findSingleOrNoneEvent( - identifier: string, - predicate?: (event: ITransactionEvent) => boolean, - ): ITransactionEvent | undefined; -} - -export interface ITransactionEvent { - readonly address: IAddress; - readonly identifier: string; - readonly topics: ITransactionEventTopic[]; - readonly data: string; - // See https://github.com/multiversx/mx-sdk-js-network-providers/blob/v2.4.0/src/transactionEvents.ts#L13 - readonly dataPayload?: { valueOf(): Uint8Array }; - readonly additionalData?: { valueOf(): Uint8Array }[]; - - findFirstOrNoneTopic(predicate: (topic: ITransactionEventTopic) => boolean): ITransactionEventTopic | undefined; - getLastTopic(): ITransactionEventTopic; -} - -export interface ITransactionEventTopic { - toString(): string; - hex(): string; -} diff --git a/src/networkProviders/accountAwaiter.dev.net.spec.ts b/src/networkProviders/accountAwaiter.dev.net.spec.ts new file mode 100644 index 000000000..f382b2527 --- /dev/null +++ b/src/networkProviders/accountAwaiter.dev.net.spec.ts @@ -0,0 +1,75 @@ +import { assert } from "chai"; +import { Account } from "../accounts"; +import { Address, Transaction } from "../core"; +import { MarkCompleted, MockNetworkProvider, Wait } from "../testutils/mockNetworkProvider"; +import { createAccountBalance, getTestWalletsPath } from "../testutils/utils"; +import { AccountAwaiter } from "./accountAwaiter"; +import { AccountOnNetwork } from "./accounts"; +import { ApiNetworkProvider } from "./apiNetworkProvider"; + +describe("AccountAwaiter Tests", () => { + const provider = new MockNetworkProvider(); + + const watcher = new AccountAwaiter({ + fetcher: provider, + pollingIntervalInMilliseconds: 42, + timeoutIntervalInMilliseconds: 42 * 42, + patienceTimeInMilliseconds: 0, + }); + + it("should await on balance increase", async () => { + const alice = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + // alice account is created with 1000 EGLD + const initialBalance = (await provider.getAccount(alice)).balance; + + // Mock balance timeline + provider.mockAccountBalanceTimelineByAddress(alice, [ + new Wait(40), + new Wait(40), + new Wait(45), + new MarkCompleted(), + ]); + + const condition = (account: AccountOnNetwork) => { + return account.balance === initialBalance + createAccountBalance(7); + }; + + const account = await watcher.awaitOnCondition(alice, condition); + + assert.equal(account.balance, createAccountBalance(1007)); + }); + + it("should await for account balance increase on the network", async function () { + this.timeout(20000); + const alice = await Account.newFromPem(`${getTestWalletsPath()}/alice.pem`); + const aliceAddress = alice.address; + const frank = Address.newFromBech32("erd1kdl46yctawygtwg2k462307dmz2v55c605737dp3zkxh04sct7asqylhyv"); + + const api = new ApiNetworkProvider("https://devnet-api.multiversx.com"); + const watcher = new AccountAwaiter({ fetcher: api }); + const value = 100_000n; + + // Create and sign the transaction + const transaction = new Transaction({ + sender: aliceAddress, + receiver: frank, + gasLimit: 50000n, + chainID: "D", + value, + }); + transaction.nonce = (await api.getAccount(aliceAddress)).nonce; + transaction.signature = await alice.signTransaction(transaction); + + const initialBalance = (await api.getAccount(frank)).balance; + + const condition = (account: AccountOnNetwork) => { + return account.balance === initialBalance + value; + }; + + await api.sendTransaction(transaction); + + const accountOnNetwork = await watcher.awaitOnCondition(frank, condition); + + assert.equal(accountOnNetwork.balance, initialBalance + value); + }); +}); diff --git a/src/networkProviders/accountAwaiter.ts b/src/networkProviders/accountAwaiter.ts new file mode 100644 index 000000000..b83690587 --- /dev/null +++ b/src/networkProviders/accountAwaiter.ts @@ -0,0 +1,104 @@ +import { Address } from "../core/address"; +import { ExpectedAccountConditionNotReachedError } from "../core/errors"; +import { AccountOnNetwork } from "./accounts"; +import { + DEFAULT_ACCOUNT_AWAITING_PATIENCE_IN_MILLISECONDS, + DEFAULT_ACCOUNT_AWAITING_POLLING_TIMEOUT_IN_MILLISECONDS, + DEFAULT_ACCOUNT_AWAITING_TIMEOUT_IN_MILLISECONDS, +} from "./constants"; + +interface IAccountFetcher { + getAccount(address: Address): Promise; +} + +export class AccountAwaiter { + private readonly fetcher: IAccountFetcher; + private readonly pollingIntervalInMilliseconds: number; + private readonly timeoutIntervalInMilliseconds: number; + private readonly patienceTimeInMilliseconds: number; + + /** + * AccountAwaiter allows one to await until a specific event occurs on a given address. + * + * @param fetcher - Used to fetch the account of the network. + * @param pollingIntervalInMilliseconds - The polling interval, in milliseconds. + * @param timeoutIntervalInMilliseconds - The timeout, in milliseconds. + * @param patienceTimeInMilliseconds - The patience, an extra time (in milliseconds) to wait, after the account has reached its desired condition. + */ + constructor(options: { + fetcher: IAccountFetcher; + pollingIntervalInMilliseconds?: number; + timeoutIntervalInMilliseconds?: number; + patienceTimeInMilliseconds?: number; + }) { + this.fetcher = options.fetcher; + + this.pollingIntervalInMilliseconds = + options.pollingIntervalInMilliseconds ?? DEFAULT_ACCOUNT_AWAITING_POLLING_TIMEOUT_IN_MILLISECONDS; + + this.timeoutIntervalInMilliseconds = + options.timeoutIntervalInMilliseconds ?? DEFAULT_ACCOUNT_AWAITING_TIMEOUT_IN_MILLISECONDS; + + this.patienceTimeInMilliseconds = + options.patienceTimeInMilliseconds ?? DEFAULT_ACCOUNT_AWAITING_PATIENCE_IN_MILLISECONDS; + } + + /** + * Waits until the condition is satisfied. + * + * @param address - The address to monitor. + * @param condition - A callable that evaluates the desired condition. + */ + async awaitOnCondition( + address: Address, + condition: (account: AccountOnNetwork) => boolean, + ): Promise { + const doFetch = async () => await this.fetcher.getAccount(address); + + return this.awaitConditionally(condition, doFetch, new ExpectedAccountConditionNotReachedError()); + } + + private async awaitConditionally( + isSatisfied: (account: AccountOnNetwork) => boolean, + doFetch: () => Promise, + error: Error, + ): Promise { + let isConditionSatisfied = false; + let fetchedData: AccountOnNetwork | null = null; + + const maxNumberOfRetries = Math.floor(this.timeoutIntervalInMilliseconds / this.pollingIntervalInMilliseconds); + + let numberOfRetries = 0; + + while (numberOfRetries < maxNumberOfRetries) { + try { + fetchedData = await doFetch(); + isConditionSatisfied = isSatisfied(fetchedData); + + if (isConditionSatisfied) { + break; + } + } catch (ex) { + throw ex; + } + + numberOfRetries += 1; + await this._sleep(this.pollingIntervalInMilliseconds); + } + + if (!fetchedData || !isConditionSatisfied) { + throw error; + } + + if (this.patienceTimeInMilliseconds) { + await this._sleep(this.patienceTimeInMilliseconds); + return doFetch(); + } + + return fetchedData; + } + + private async _sleep(milliseconds: number): Promise { + return new Promise((resolve) => setTimeout(resolve, milliseconds)); + } +} diff --git a/src/networkProviders/accounts.ts b/src/networkProviders/accounts.ts index cee89aea6..1be9ce39f 100644 --- a/src/networkProviders/accounts.ts +++ b/src/networkProviders/accounts.ts @@ -1,30 +1,78 @@ -import BigNumber from "bignumber.js"; -import { Address } from "../address"; -import { IAddress } from "./interface"; +import { BytesValue } from "../abi"; +import { Address, CodeMetadata } from "../core"; +import { BlockCoordinates } from "./blocks"; /** * A plain view of an account, as queried from the Network. */ export class AccountOnNetwork { - address: IAddress = Address.empty(); - nonce: number = 0; - balance: BigNumber = new BigNumber(0); - code: string = ""; + address: Address = Address.empty(); + nonce: bigint = 0n; + balance: bigint = 0n; userName: string = ""; + contractCodeHash?: string; + contractCode?: Uint8Array; + contractDeveloperReward?: bigint; + contractOwnerAddress?: Address; + + isContractUpgradable?: boolean; + isContractReadable?: boolean; + isContractPayable?: boolean; + isContractPayableByContract?: boolean; + + isGuarded: boolean = false; + constructor(init?: Partial) { Object.assign(this, init); } - static fromHttpResponse(payload: any): AccountOnNetwork { - let result = new AccountOnNetwork(); - - result.address = new Address(payload["address"] || ""); - result.nonce = Number(payload["nonce"] || 0); - result.balance = new BigNumber(payload["balance"] || 0); - result.code = payload["code"] || ""; - result.userName = payload["username"] || ""; + static fromApiHttpResponse(payload: any): AccountOnNetwork { + const result = new AccountOnNetwork(); + + result.address = payload["address"] ? new Address(payload["address"]) : Address.empty(); + result.nonce = BigInt(payload["nonce"] || 0); + result.balance = BigInt(payload["balance"] || 0); + result.userName = payload["username"] || undefined; + + result.contractCodeHash = payload["codeHash"] || ""; + result.contractCode = Buffer.from(payload["code"] || ""); + result.contractDeveloperReward = payload["developerReward"] || 0n; + result.contractOwnerAddress = payload["ownerAddress"] ? new Address(payload["ownerAddress"]) : undefined; + result.isContractUpgradable = Boolean(payload["isUpgradeable"]); + result.isContractReadable = Boolean(payload["isReadable"]); + result.isContractPayable = Boolean(payload["isPayable"]); + result.isContractPayableByContract = Boolean(payload["isPayableBySmartContract"]); + result.isGuarded = Boolean(payload["isGuarded"]); + return result; + } + static fromProxyHttpResponse(payload: any): AccountOnNetwork { + const result = new AccountOnNetwork(); + + result.address = payload["address"] ? new Address(payload["address"]) : Address.empty(); + result.nonce = BigInt(payload["nonce"] || 0); + result.balance = BigInt(payload["balance"] || 0); + result.userName = payload["username"] || undefined; + + const codeMetadata = payload["codeMetadata"] ?? null; + result.isContractUpgradable = false; + result.isContractReadable = false; + result.isContractPayable = false; + result.isContractPayableByContract = false; + if (codeMetadata) { + const metadataBuffer = Buffer.from(codeMetadata, "base64"); + const metadata = CodeMetadata.newFromBytes(metadataBuffer); + result.isContractUpgradable = metadata.upgradeable; + result.isContractReadable = metadata.readable; + result.isContractPayable = metadata.payable; + result.isContractPayableByContract = metadata.payableBySc; + } + result.contractCodeHash = payload["codeHash"] || ""; + result.contractCode = Buffer.from(payload["code"] || ""); + result.contractDeveloperReward = payload["developerReward"] || 0n; + result.contractOwnerAddress = payload["ownerAddress"] ? new Address(payload["ownerAddress"]) : undefined; + result.isGuarded = Boolean(payload["isGuarded"]); return result; } } @@ -54,7 +102,7 @@ export class GuardianData { return result; } - getCurrentGuardianAddress(): IAddress | undefined { + getCurrentGuardianAddress(): Address | undefined { if (!this.guarded) { return undefined; } @@ -63,9 +111,9 @@ export class GuardianData { } } -class Guardian { +export class Guardian { activationEpoch: number = 0; - address: IAddress = Address.empty(); + address: Address = Address.empty(); serviceUID: string = ""; static fromHttpResponse(responsePart: any): Guardian { @@ -78,3 +126,59 @@ class Guardian { return result; } } +export class AccountStorageEntry { + raw: Record = {}; + address: Address = Address.empty(); + key: string = ""; + value: string = ""; + + constructor(init?: Partial) { + Object.assign(this, init); + } + + static fromHttpResponse(payload: any, key: string): AccountStorageEntry { + const result = new AccountStorageEntry(); + const value = payload["value"] || ""; + + result.raw = payload; + result.key = key; + result.value = BytesValue.fromHex(value).toString(); + + return result; + } +} + +/** + * A plain view of an account storage. + */ + +export class AccountStorage { + raw: Record = {}; + blockCoordinates!: BlockCoordinates; + entries: AccountStorageEntry[] = []; + constructor(init?: Partial) { + Object.assign(this, init); + } + + static fromHttpResponse(payload: any): AccountStorage { + let result = new AccountStorage(); + + const pairs = payload["pairs"] || {}; + const entries: AccountStorageEntry[] = Object.entries(pairs).map(([key, value]) => { + const decodedKey = Buffer.from(key, "hex").toString("utf8"); + const val: string = value ? (value as string).toString() : ""; + const decodedValue = Buffer.from(val).toString(); + + return new AccountStorageEntry({ + raw: { [key]: value }, + key: decodedKey, + value: decodedValue, + }); + }); + result.raw = payload; + result.entries = entries; + result.blockCoordinates = BlockCoordinates.fromHttpResponse(payload); + + return result; + } +} diff --git a/src/networkProviders/apiNetworkProvider.dev.net.spec.ts b/src/networkProviders/apiNetworkProvider.dev.net.spec.ts new file mode 100644 index 000000000..4e7183a82 --- /dev/null +++ b/src/networkProviders/apiNetworkProvider.dev.net.spec.ts @@ -0,0 +1,385 @@ +import { assert, expect } from "chai"; +import { Account } from "../accounts"; +import { Address, SmartContractQuery, Token, Transaction, TransactionOnNetwork, TransactionStatus } from "../core"; +import { getTestWalletsPath } from "../testutils/utils"; +import { ApiNetworkProvider } from "./apiNetworkProvider"; + +describe("ApiNetworkProvider Tests", function () { + const apiProvider = new ApiNetworkProvider("https://devnet-api.multiversx.com"); + + it("should fetch network configuration", async () => { + const result = await apiProvider.getNetworkConfig(); + assert.equal(result.chainID, "D"); + assert.equal(result.gasPerDataByte, 1500n); + assert.equal(result.roundDuration, 6000); + assert.equal(result.minGasLimit, 50000n); + assert.equal(result.minGasPrice, 1_000_000_000n); + assert.exists(result.raw); + }); + + it("should fetch network status", async () => { + const result = await apiProvider.getNetworkStatus(); + assert.exists(result.blockNonce); + assert.exists(result.currentRound); + assert.exists(result.blockTimestamp); + assert.exists(result.currentEpoch); + assert.exists(result.highestFinalNonce); + assert.exists(result.raw); + }); + + it("should fetch block details by hash and nonce", async () => { + const blockHash = "ded535cc0afb2dc5f9787e9560dc48d0b83564a3f994a390b228d894d854699f"; + const resultByHash = await apiProvider.getBlock(blockHash); + + assert.equal(resultByHash.hash, blockHash); + assert.equal(resultByHash.nonce, 5949242n); + assert.equal(resultByHash.shard, 1); + assert.equal(resultByHash.timestamp, 1730112578); + }); + + it("should fetch the latest block", async () => { + const result = await apiProvider.getLatestBlock(); + expect(result).to.exist; + }); + + it("should fetch account details", async () => { + const address1 = Address.newFromBech32("erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"); + const result1 = await apiProvider.getAccount(address1); + + assert.equal(result1.address.toBech32(), "erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"); + assert.isUndefined(result1.userName); + assert.isUndefined(result1.contractOwnerAddress); + + const address2 = Address.newFromBech32("erd1qqqqqqqqqqqqqpgq076flgeualrdu5jyyj60snvrh7zu4qrg05vqez5jen"); + const result2 = await apiProvider.getAccount(address2); + + assert.equal(result2.address.toBech32(), "erd1qqqqqqqqqqqqqpgq076flgeualrdu5jyyj60snvrh7zu4qrg05vqez5jen"); + assert.isUndefined(result2.userName); + assert.equal( + result2.contractOwnerAddress?.toBech32(), + "erd1wzx0tak22f2me4g7wpxfae2w3htfue7khrg28fy6wu8x9hzq05vqm8qhnm", + ); + assert.isFalse(result2.isContractPayable); + assert.isTrue(result2.isContractReadable); + }); + + it("should fetch account storage", async () => { + const address = Address.newFromBech32("erd1qqqqqqqqqqqqqpgq076flgeualrdu5jyyj60snvrh7zu4qrg05vqez5jen"); + const result = await apiProvider.getAccountStorage(address); + + assert.equal(result.entries.length, 1); + assert.equal(result.entries[0].key, "sum"); + assert.exists(result.entries[0].value); + }); + + it("should fetch a storage entry for an account", async () => { + const address = Address.newFromBech32("erd1qqqqqqqqqqqqqpgq076flgeualrdu5jyyj60snvrh7zu4qrg05vqez5jen"); + const result = await apiProvider.getAccountStorageEntry(address, "sum"); + + assert.equal(result.key, "sum"); + assert.exists(result.value); + }); + + it("should fetch token of an account", async () => { + const address = Address.newFromBech32("erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"); + let token = await apiProvider.getTokenOfAccount(address, new Token({ identifier: "TEST-ff155e" })); + + assert.equal(token.token.identifier, "TEST-ff155e"); + assert.equal(token.amount, 99999999999980000n); + + token = await apiProvider.getTokenOfAccount(address, new Token({ identifier: "NFTEST-ec88b8", nonce: 1n })); + + assert.equal(token.token.identifier, "NFTEST-ec88b8-01"); + assert.equal(token.amount, 1n); + assert.equal(token.token.nonce, 1n); + }); + + it("should fetch fungible tokens of an account", async () => { + const address = Address.newFromBech32("erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"); + const tokens = await apiProvider.getFungibleTokensOfAccount(address); + assert.isTrue(tokens.length > 0); + + const filtered = tokens.filter((token) => token.token.identifier === "TEST-ff155e"); + assert.equal(filtered.length, 1); + assert.equal(filtered[0].token.identifier, "TEST-ff155e"); + assert.equal(filtered[0].amount.toString(), "99999999999980000"); + }); + + it("should fetch non-fungible tokens of an account", async () => { + const address = Address.newFromBech32("erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"); + const tokens = await apiProvider.getNonFungibleTokensOfAccount(address); + assert.isTrue(tokens.length > 0); + + const filtered = tokens.filter((token) => token.token.identifier === "NFTEST-ec88b8-01"); + assert.equal(filtered.length, 1); + assert.equal(filtered[0].token.identifier, "NFTEST-ec88b8-01"); + assert.equal(filtered[0].token.nonce, 1n); + assert.equal(filtered[0].amount, 1n); + }); + + it("should fetch definition of fungible token", async () => { + const token = await apiProvider.getDefinitionOfFungibleToken("TEST-ff155e"); + + assert.equal(token.identifier, "TEST-ff155e"); + assert.equal(token.owner.toBech32(), "erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"); + assert.equal(token.decimals, 6); + }); + + it("should fetch definition of token collection", async () => { + const token = await apiProvider.getDefinitionOfTokenCollection("NFTEST-ec88b8"); + + assert.equal(token.collection, "NFTEST-ec88b8"); + assert.equal(token.owner.toBech32(), "erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"); + assert.equal(token.type, "NonFungibleESDT"); + assert.equal(token.decimals, 0); + }); + + it("should fetch transaction", async () => { + const transaction = await apiProvider.getTransaction( + "9d47c4b4669cbcaa26f5dec79902dd20e55a0aa5f4b92454a74e7dbd0183ad6c", + ); + + assert.equal(transaction.nonce, 0n); + assert.equal(transaction.hash, "9d47c4b4669cbcaa26f5dec79902dd20e55a0aa5f4b92454a74e7dbd0183ad6c"); + assert.isTrue(transaction.status.isCompleted()); + assert.equal(transaction.sender.toBech32(), "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + assert.deepEqual(transaction.smartContractResults, []); + }); + + it("should fetch transaction with events", async () => { + const transaction = await apiProvider.getTransaction( + "6fe05e4ca01d42c96ae5182978a77fe49f26bcc14aac95ad4f19618173f86ddb", + ); + + assert.exists(transaction.logs); + assert.exists(transaction.logs.events); + assert.equal(transaction.logs.events.length, 2); + assert.equal(transaction.logs.events[0].topics.length, 8); + assert.equal(Buffer.from(transaction.logs.events[0].topics[0]).toString("hex"), "544553542d666631353565"); + assert.equal(Buffer.from(transaction.logs.events[0].topics[1]).toString("hex"), ""); + assert.equal(Buffer.from(transaction.logs.events[0].topics[2]).toString("hex"), "63616e4368616e67654f776e6572"); + assert.equal(transaction.hash, "6fe05e4ca01d42c96ae5182978a77fe49f26bcc14aac95ad4f19618173f86ddb"); + assert.isTrue(transaction.status.isCompleted()); + }); + + it("should fetch smart contract invoking transaction", async () => { + const transaction = await apiProvider.getTransaction( + "6fe05e4ca01d42c96ae5182978a77fe49f26bcc14aac95ad4f19618173f86ddb", + ); + + assert.isTrue(transaction.status.isCompleted()); + assert.isTrue(transaction.smartContractResults.length > 2); + assert.deepEqual( + transaction.data, + Buffer.from( + "issue@54455354546f6b656e@54455354@016345785d8a0000@06@63616e4368616e67654f776e6572@74727565@63616e55706772616465@74727565@63616e4164645370656369616c526f6c6573@74727565", + ), + ); + assert.equal(Buffer.from(transaction.logs.events[0].topics[0]).toString("hex"), "544553542d666631353565"); + assert.equal(Buffer.from(transaction.logs.events[0].topics[1]).toString("hex"), ""); + assert.equal(Buffer.from(transaction.logs.events[0].topics[2]).toString("hex"), "63616e4368616e67654f776e6572"); + assert.equal(transaction.hash, "6fe05e4ca01d42c96ae5182978a77fe49f26bcc14aac95ad4f19618173f86ddb"); + }); + + it("should fetch transaction status", async () => { + const txHash = "9d47c4b4669cbcaa26f5dec79902dd20e55a0aa5f4b92454a74e7dbd0183ad6c"; + const result = await apiProvider.getTransactionStatus(txHash); + assert.equal(result.status, "success"); + }); + + it("should send transaction", async () => { + const transaction = new Transaction({ + sender: Address.newFromBech32("erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"), + receiver: Address.newFromBech32("erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"), + gasLimit: 50000n, + chainID: "D", + value: 5000000000000000000n, + nonce: 100n, + gasPrice: 1000000000n, + version: 2, + signature: Buffer.from( + "faf50b8368cb2c20597dad671a14aa76d4c65937d6e522c64946f16ad6a250262463e444596fa7ee2af1273f6ad0329d43af48d1ae5f3b295bc8f48fdba41a05", + "hex", + ), + }); + + const expectedHash = "fc914860c1d137ed8baa602e561381f97c7bad80d150c5bf90760d3cfd3a4cea"; + assert.equal(await apiProvider.sendTransaction(transaction), expectedHash); + }); + + it("should send transaction with data", async () => { + const transaction = new Transaction({ + sender: Address.newFromBech32("erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"), + receiver: Address.newFromBech32("erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"), + gasLimit: 70000n, + chainID: "D", + nonce: 105n, + gasPrice: 1000000000n, + version: 2, + data: new Uint8Array(Buffer.from("foo")), + signature: Buffer.from( + "7a8bd08351bac6b1113545f5a896cb0b63806abd93d639bc4d16bfbc82c7b514f68ed7b36c743f4c3d2d1e1d3cb356824041d51dfe587a149f6fc9ab0dd9c408", + "hex", + ), + }); + + const expectedHash = "4dc7d4e18c0cf9ca7f17677ef0ac3d1363528e892996b518bee909bb17cf7929"; + assert.equal(await apiProvider.sendTransaction(transaction), expectedHash); + }); + + it("should send transactions", async () => { + const txs = [ + new Transaction({ + nonce: 103n, + receiver: Address.newFromBech32("erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"), + sender: Address.newFromBech32("erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"), + gasPrice: 1000000000n, + gasLimit: 50000n, + chainID: "D", + version: 2, + signature: Buffer.from( + "498d5abb9f8eb69cc75f24320e8929dadbfa855ffac220d5e92175a83be68e0437801af3a1411e3d839738230097a1c38da5c8c4df3f345defc5d40300675900", + "hex", + ), + }), + new Transaction({ + nonce: 77n, + chainID: "D", + receiver: Address.newFromBech32("erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"), + sender: Address.newFromBech32("erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"), + gasLimit: 50000n, + gasPrice: 1000000000n, + }), + new Transaction({ + nonce: 104n, + receiver: Address.newFromBech32("erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"), + sender: Address.newFromBech32("erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"), + gasPrice: 1000000000n, + gasLimit: 50000n, + chainID: "D", + version: 2, + signature: Buffer.from( + "341a2f3b738fbd20692e3bbd1cb36cb5f4ce9c0a9acc0cf4322269c0fcf34fd6bb59cd94062a9a4730e47f41b1ef3e29b69c6ab2a2a4dca9c9a7724681bc1708", + "hex", + ), + }), + ]; + + const expectedHashes = [ + "61b4f2561fc57bfb8b8971ed23cd64259b664bc0404ea7a0449def8ceef24b08", + null, + "30274b60b5635f981fa89ccfe726a34ca7121caa5d34123021c77a5c64cc9163", + ]; + const [numOfSentTxs, hashes] = await apiProvider.sendTransactions(txs); + assert.equal(numOfSentTxs, 2); + assert.deepEqual(hashes, expectedHashes); + }); + + it("should simulate transaction", async () => { + const bob = await Account.newFromPem(`${getTestWalletsPath()}/bob.pem`); + let transaction = new Transaction({ + sender: bob.address, + receiver: bob.address, + gasLimit: 50000n, + chainID: "D", + signature: Buffer.from(Array(128).fill("0").join(""), "hex"), + }); + const nonce = (await apiProvider.getAccount(bob.address)).nonce; + transaction.nonce = nonce; + let txOnNetwork = await apiProvider.simulateTransaction(transaction); + assert.deepEqual(txOnNetwork.status, new TransactionStatus("success")); + + transaction.signature = await bob.signTransaction(transaction); + txOnNetwork = await apiProvider.simulateTransaction(transaction, true); + + transaction = new Transaction({ + sender: bob.address, + receiver: Address.newFromBech32("erd1qqqqqqqqqqqqqpgq076flgeualrdu5jyyj60snvrh7zu4qrg05vqez5jen"), + gasLimit: 10000000n, + chainID: "D", + gasPrice: 1000000000n, + version: 2, + data: new Uint8Array(Buffer.from("add@07")), + nonce: nonce, + signature: Buffer.from(Array(128).fill("0").join(""), "hex"), + }); + + txOnNetwork = await apiProvider.simulateTransaction(transaction); + assert.equal(txOnNetwork.smartContractResults.length, 1); + assert.equal( + txOnNetwork.smartContractResults[0].sender.toBech32(), + "erd1qqqqqqqqqqqqqpgq076flgeualrdu5jyyj60snvrh7zu4qrg05vqez5jen", + ); + assert.equal(txOnNetwork.smartContractResults[0].receiver.toBech32(), bob.address.toBech32()); + assert.deepEqual(txOnNetwork.smartContractResults[0].data, "@6f6b"); + + transaction.signature = await bob.signTransaction(transaction); + txOnNetwork = await apiProvider.simulateTransaction(transaction, true); + + assert.deepEqual(txOnNetwork.status, new TransactionStatus("success")); + assert.equal(txOnNetwork.smartContractResults.length, 1); + assert.equal( + txOnNetwork.smartContractResults[0].sender.toBech32(), + "erd1qqqqqqqqqqqqqpgq076flgeualrdu5jyyj60snvrh7zu4qrg05vqez5jen", + ); + assert.equal(txOnNetwork.smartContractResults[0].receiver.toBech32(), bob.address.toBech32()); + assert.equal(txOnNetwork.smartContractResults[0].data, "@6f6b"); + }); + + it("should estimate transaction cost", async function () { + const bob = await Account.newFromPem(`${getTestWalletsPath()}/bob.pem`); + const transaction = new Transaction({ + sender: bob.address, + receiver: bob.address, + gasLimit: 50000n, + chainID: "D", + data: new Uint8Array(Buffer.from("test transaction")), + }); + transaction.nonce = (await apiProvider.getAccount(bob.address)).nonce; + transaction.signature = await bob.signTransaction(transaction); + const response = await apiProvider.estimateTransactionCost(transaction); + assert.equal(response.gasLimit, 74000); + }); + + it("should send and await for completed transaction", async function () { + this.timeout(30000); + const bob = await Account.newFromPem(`${getTestWalletsPath()}/bob.pem`); + let transaction = new Transaction({ + sender: bob.address, + receiver: bob.address, + gasLimit: 50000n, + chainID: "D", + }); + const nonce = (await apiProvider.getAccount(bob.address)).nonce; + transaction.nonce = nonce; + transaction.signature = await bob.signTransaction(transaction); + let hash = await apiProvider.sendTransaction(transaction); + let transactionOnNetwork = await apiProvider.awaitTransactionCompleted(hash); + assert.isTrue(transactionOnNetwork.status.isCompleted()); + + transaction = new Transaction({ + sender: bob.address, + receiver: Address.newFromBech32("erd1qqqqqqqqqqqqqpgqhdqz9j3zgpl8fg2z0jzx9n605gwxx4djd8ssruw094"), + gasLimit: 5000000n, + chainID: "D", + data: new Uint8Array(Buffer.from("dummy@05")), + }); + transaction.nonce = nonce + 1n; + transaction.signature = await bob.signTransaction(transaction); + const condition = (txOnNetwork: TransactionOnNetwork) => !txOnNetwork.status.isSuccessful(); + + hash = await apiProvider.sendTransaction(transaction); + transactionOnNetwork = await apiProvider.awaitTransactionOnCondition(hash, condition); + assert.isFalse(transactionOnNetwork.status.isSuccessful()); + }); + + it("should query contract", async () => { + const query = new SmartContractQuery({ + contract: Address.newFromBech32("erd1qqqqqqqqqqqqqpgqqy34h7he2ya6qcagqre7ur7cc65vt0mxrc8qnudkr4"), + function: "getSum", + arguments: [], + }); + const result = await apiProvider.queryContract(query); + assert.equal(result.returnDataParts.length, 1); + }); +}); diff --git a/src/networkProviders/apiNetworkProvider.ts b/src/networkProviders/apiNetworkProvider.ts index bf9cf6858..bedf6dcd3 100644 --- a/src/networkProviders/apiNetworkProvider.ts +++ b/src/networkProviders/apiNetworkProvider.ts @@ -1,23 +1,33 @@ -import { ErrContractQuery, ErrNetworkProvider } from "../errors"; -import { getAxios } from "../utils"; -import { numberToPaddedHex } from "../utils.codec"; -import { AccountOnNetwork, GuardianData } from "./accounts"; +import { + Address, + ErrContractQuery, + ErrNetworkProvider, + getAxios, + prepareTransactionForBroadcasting, + SmartContractQuery, + SmartContractQueryResponse, + Token, + TokenComputer, + Transaction, + TransactionOnNetwork, + TransactionStatus, + TransactionWatcher, +} from "../core"; +import { METACHAIN_ID } from "../core/constants"; +import { AccountAwaiter } from "./accountAwaiter"; +import { AccountOnNetwork, AccountStorage, AccountStorageEntry } from "./accounts"; +import { BlockOnNetwork } from "./blocks"; import { defaultAxiosConfig, defaultPagination } from "./config"; import { BaseUserAgent } from "./constants"; import { ContractQueryRequest } from "./contractQueryRequest"; -import { ContractQueryResponse } from "./contractQueryResponse"; -import { IAddress, IContractQuery, INetworkProvider, IPagination, ITransaction, ITransactionNext } from "./interface"; +import { INetworkProvider, IPagination } from "./interface"; import { NetworkConfig } from "./networkConfig"; -import { NetworkGeneralStatistics } from "./networkGeneralStatistics"; import { NetworkProviderConfig } from "./networkProviderConfig"; -import { NetworkStake } from "./networkStake"; import { NetworkStatus } from "./networkStatus"; -import { PairOnNetwork } from "./pairs"; import { ProxyNetworkProvider } from "./proxyNetworkProvider"; +import { AwaitingOptions, TransactionCostResponse } from "./resources"; import { DefinitionOfFungibleTokenOnNetwork, DefinitionOfTokenCollectionOnNetwork } from "./tokenDefinitions"; -import { FungibleTokenOfAccountOnNetwork, NonFungibleTokenOfAccountOnNetwork } from "./tokens"; -import { TransactionOnNetwork, prepareTransactionForBroadcasting } from "./transactions"; -import { TransactionStatus } from "./transactionStatus"; +import { TokenAmountOnNetwork } from "./tokens"; import { extendUserAgentIfBackend } from "./userAgent"; // TODO: Find & remove duplicate code between "ProxyNetworkProvider" and "ApiNetworkProvider". @@ -47,91 +57,90 @@ export class ApiNetworkProvider implements INetworkProvider { return await this.backingProxyNetworkProvider.getNetworkConfig(); } - async getNetworkStatus(): Promise { - return await this.backingProxyNetworkProvider.getNetworkStatus(); + async getNetworkStatus(shard: number = METACHAIN_ID): Promise { + return await this.backingProxyNetworkProvider.getNetworkStatus(shard); } - async getNetworkStakeStatistics(): Promise { - const response = await this.doGetGeneric("stake"); - const networkStake = NetworkStake.fromHttpResponse(response); - return networkStake; + async getBlock(blockHash: string): Promise { + const response = await this.doGetGeneric(`blocks/${blockHash}`); + return BlockOnNetwork.fromHttpResponse(response); } - async getNetworkGeneralStatistics(): Promise { - const response = await this.doGetGeneric("stats"); - const stats = NetworkGeneralStatistics.fromHttpResponse(response); - return stats; + async getLatestBlock(): Promise { + const response = await this.doGetGeneric("blocks/latest"); + return BlockOnNetwork.fromHttpResponse(response); } - async getAccount(address: IAddress): Promise { - const response = await this.doGetGeneric(`accounts/${address.bech32()}`); - const account = AccountOnNetwork.fromHttpResponse(response); + async getAccount(address: Address): Promise { + const response = await this.doGetGeneric(`accounts/${address.toBech32()}`); + const account = AccountOnNetwork.fromApiHttpResponse(response); return account; } - async getGuardianData(address: IAddress): Promise { - return await this.backingProxyNetworkProvider.getGuardianData(address); + async getAccountStorage(address: Address): Promise { + const response = await this.doGetGeneric(`address/${address.toBech32()}/keys`); + const account = AccountStorage.fromHttpResponse(response.data); + return account; } - async getFungibleTokensOfAccount( - address: IAddress, - pagination?: IPagination, - ): Promise { - pagination = pagination || defaultPagination; - - const url = `accounts/${address.bech32()}/tokens?${this.buildPaginationParams(pagination)}`; - const response: any[] = await this.doGetGeneric(url); - const tokens = response.map((item) => FungibleTokenOfAccountOnNetwork.fromHttpResponse(item)); - - // TODO: Fix sorting - tokens.sort((a, b) => a.identifier.localeCompare(b.identifier)); - return tokens; + async getAccountStorageEntry(address: Address, entryKey: string): Promise { + const keyAsHex = Buffer.from(entryKey).toString("hex"); + const response = await this.doGetGeneric(`address/${address.toBech32()}/key/${keyAsHex}`); + const account = AccountStorageEntry.fromHttpResponse(response.data, entryKey); + return account; } - async getNonFungibleTokensOfAccount( - address: IAddress, - pagination?: IPagination, - ): Promise { - pagination = pagination || defaultPagination; - - const url = `accounts/${address.bech32()}/nfts?${this.buildPaginationParams(pagination)}`; - const response: any[] = await this.doGetGeneric(url); - const tokens = response.map((item) => NonFungibleTokenOfAccountOnNetwork.fromApiHttpResponse(item)); + async awaitAccountOnCondition( + address: Address, + condition: (account: AccountOnNetwork) => boolean, + options?: AwaitingOptions, + ): Promise { + if (!options) { + options = new AwaitingOptions(); + } + const awaiter = new AccountAwaiter({ + fetcher: this, + patienceTimeInMilliseconds: options.patienceInMilliseconds, + pollingIntervalInMilliseconds: options.pollingIntervalInMilliseconds, + timeoutIntervalInMilliseconds: options.timeoutInMilliseconds, + }); + return await awaiter.awaitOnCondition(address, condition); + } - // TODO: Fix sorting - tokens.sort((a, b) => a.identifier.localeCompare(b.identifier)); - return tokens; + async sendTransaction(tx: Transaction): Promise { + const transaction = prepareTransactionForBroadcasting(tx); + const response = await this.doPostGeneric("transactions", transaction); + return response.txHash; } - async getFungibleTokenOfAccount( - address: IAddress, - tokenIdentifier: string, - ): Promise { - const response = await this.doGetGeneric(`accounts/${address.bech32()}/tokens/${tokenIdentifier}`); - const tokenData = FungibleTokenOfAccountOnNetwork.fromHttpResponse(response); - return tokenData; + async simulateTransaction(tx: Transaction, checkSignature: boolean = false): Promise { + const transaction = prepareTransactionForBroadcasting(tx); + let url = "transaction/simulate?checkSignature=false"; + if (checkSignature) { + url = "transaction/simulate"; + } + const response = await this.doPostGeneric(url, transaction); + const data = response["data"] ?? {}; + return TransactionOnNetwork.fromSimulateResponse(transaction, data["result"] ?? {}); } - async getNonFungibleTokenOfAccount( - address: IAddress, - collection: string, - nonce: number, - ): Promise { - const nonceAsHex = numberToPaddedHex(nonce); - const response = await this.doGetGeneric(`accounts/${address.bech32()}/nfts/${collection}-${nonceAsHex}`); - const tokenData = NonFungibleTokenOfAccountOnNetwork.fromApiHttpResponse(response); - return tokenData; + async estimateTransactionCost(tx: Transaction): Promise { + const transaction = prepareTransactionForBroadcasting(tx); + const response = await this.doPostGeneric("transaction/cost", transaction); + return TransactionCostResponse.fromHttpResponse(response.data); } - async getMexPairs(pagination?: IPagination): Promise { - let url = `mex/pairs`; - if (pagination) { - url = `${url}?from=${pagination.from}&size=${pagination.size}`; - } + async sendTransactions(txs: Transaction[]): Promise<[number, string[]]> { + const data = txs.map((tx) => prepareTransactionForBroadcasting(tx)); - const response: any[] = await this.doGetGeneric(url); + const response = await this.doPostGeneric("transaction/send-multiple", data); + const numSent = Number(response.data["numOfSentTxs"] ?? 0); + const hashes = Array(txs.length).fill(null); - return response.map((item) => PairOnNetwork.fromApiHttpResponse(item)); + for (let i = 0; i < txs.length; i++) { + hashes[i] = response.data.txsHashes[i.toString()] || null; + } + return [numSent, hashes]; } async getTransaction(txHash: string): Promise { @@ -146,28 +155,63 @@ export class ApiNetworkProvider implements INetworkProvider { return status; } - async sendTransaction(tx: ITransaction | ITransactionNext): Promise { - const transaction = prepareTransactionForBroadcasting(tx); - const response = await this.doPostGeneric("transactions", transaction); - return response.txHash; - } + async awaitTransactionOnCondition( + transactionHash: string, + condition: (account: TransactionOnNetwork) => boolean, + options?: AwaitingOptions, + ): Promise { + if (!options) { + options = new AwaitingOptions(); + } - async sendTransactions(txs: (ITransaction | ITransactionNext)[]): Promise { - return await this.backingProxyNetworkProvider.sendTransactions(txs); + const awaiter = new TransactionWatcher(this, { + patienceMilliseconds: options.patienceInMilliseconds, + pollingIntervalMilliseconds: options.pollingIntervalInMilliseconds, + timeoutMilliseconds: options.timeoutInMilliseconds, + }); + return await awaiter.awaitOnCondition(transactionHash, condition); } - async simulateTransaction(tx: ITransaction | ITransactionNext): Promise { - return await this.backingProxyNetworkProvider.simulateTransaction(tx); + async awaitTransactionCompleted(transactionHash: string, options?: AwaitingOptions): Promise { + if (!options) { + options = new AwaitingOptions(); + } + + const awaiter = new TransactionWatcher(this, { + patienceMilliseconds: options.patienceInMilliseconds, + pollingIntervalMilliseconds: options.pollingIntervalInMilliseconds, + timeoutMilliseconds: options.timeoutInMilliseconds, + }); + return await awaiter.awaitCompleted(transactionHash); } - async queryContract(query: IContractQuery): Promise { - try { - const request = new ContractQueryRequest(query).toHttpRequest(); - const response = await this.doPostGeneric("query", request); - return ContractQueryResponse.fromHttpResponse(response); - } catch (error: any) { - throw new ErrContractQuery(error); + async getTokenOfAccount(address: Address, token: Token): Promise { + let response; + if (token.nonce === 0n) { + response = await this.doGetGeneric(`accounts/${address.toBech32()}/tokens/${token.identifier}`); + } else { + const identifier = new TokenComputer().computeExtendedIdentifier(token); + response = await this.doGetGeneric(`accounts/${address.toBech32()}/nfts/${identifier}`); } + return TokenAmountOnNetwork.fromApiResponse(response); + } + + async getFungibleTokensOfAccount(address: Address, pagination?: IPagination): Promise { + pagination = pagination || defaultPagination; + + const url = `accounts/${address.toBech32()}/tokens?${this.buildPaginationParams(pagination)}`; + const response: any[] = await this.doGetGeneric(url); + const tokens = response.map((item) => TokenAmountOnNetwork.fromApiResponse(item)); + return tokens; + } + + async getNonFungibleTokensOfAccount(address: Address, pagination?: IPagination): Promise { + pagination = pagination || defaultPagination; + + const url = `accounts/${address.toBech32()}/nfts?${this.buildPaginationParams(pagination)}`; + const response: any[] = await this.doGetGeneric(url); + const tokens = response.map((item) => TokenAmountOnNetwork.fromApiResponse(item)); + return tokens; } async getDefinitionOfFungibleToken(tokenIdentifier: string): Promise { @@ -182,11 +226,14 @@ export class ApiNetworkProvider implements INetworkProvider { return definition; } - async getNonFungibleToken(collection: string, nonce: number): Promise { - const nonceAsHex = numberToPaddedHex(nonce); - const response = await this.doGetGeneric(`nfts/${collection}-${nonceAsHex}`); - const token = NonFungibleTokenOfAccountOnNetwork.fromApiHttpResponse(response); - return token; + async queryContract(query: SmartContractQuery): Promise { + try { + const request = new ContractQueryRequest(query).toHttpRequest(); + const response = await this.doPostGeneric("query", request); + return SmartContractQueryResponse.fromHttpResponse(response, query.function); + } catch (error: any) { + throw new ErrContractQuery(error); + } } async doGetGeneric(resourceUrl: string): Promise { diff --git a/src/networkProviders/blocks.ts b/src/networkProviders/blocks.ts new file mode 100644 index 000000000..bab275760 --- /dev/null +++ b/src/networkProviders/blocks.ts @@ -0,0 +1,82 @@ +/** + * An object holding network status configuration parameters. + */ + +export class BlockOnNetwork { + /** + * The raw data return by provider. + */ + public raw: Record = {}; + + /** + * The shard number. + */ + public shard: number = 0; + + /** + * The shard nonce. + */ + public nonce: bigint = 0n; + + /** + * The block hash. + */ + public hash: string = ""; + + /** + * The block previous hash. + */ + public previousHash: string = ""; + + /** + * The block timestamp. + */ + public timestamp: number = 0; + + /** + * The block timestamp. + */ + public round: number = 0; + + /** + * The block timestamp. + */ + public epoch: number = 0; + + /** + * Constructs a configuration object from a HTTP response (as returned by the provider). + */ + static fromHttpResponse(payload: any): BlockOnNetwork { + let blockOnNetwork = new BlockOnNetwork(); + + blockOnNetwork.raw = payload; + blockOnNetwork.shard = Number(payload["shard"]) ?? 0; + blockOnNetwork.nonce = BigInt(payload["nonce"] ?? 0); + blockOnNetwork.hash = payload["hash"] ?? ""; + blockOnNetwork.previousHash = payload["prevBlockHash"] ?? payload["prevHash"] ?? ""; + blockOnNetwork.timestamp = Number(payload["timestamp"] ?? 0); + blockOnNetwork.round = Number(payload["round"] ?? 0); + blockOnNetwork.epoch = Number(payload["epoch"] ?? 0); + + return blockOnNetwork; + } +} + +export class BlockCoordinates { + nonce: bigint = 0n; + hash: string = ""; + rootHash: string = ""; + constructor(init?: Partial) { + Object.assign(this, init); + } + static fromHttpResponse(payload: any): BlockCoordinates { + const result = new BlockCoordinates(); + const value = payload["blockInfo"] || {}; + + result.nonce = value["nonce"] || 0n; + result.hash = value["hash"] || ""; + result.rootHash = value["rootHash"] || ""; + + return result; + } +} diff --git a/src/networkProviders/config.ts b/src/networkProviders/config.ts index 8239bfda9..c43ac48bd 100644 --- a/src/networkProviders/config.ts +++ b/src/networkProviders/config.ts @@ -1,6 +1,6 @@ import { IPagination } from "./interface"; -const JSONbig = require("json-bigint")({ constructorAction: 'ignore' }); +const JSONbig = require("json-bigint")({ constructorAction: "ignore" }); export const defaultAxiosConfig = { timeout: 5000, @@ -8,11 +8,11 @@ export const defaultAxiosConfig = { transformResponse: [ function (data: any) { return JSONbig.parse(data); - } - ] + }, + ], }; export const defaultPagination: IPagination = { from: 0, - size: 100 + size: 100, }; diff --git a/src/networkProviders/constants.ts b/src/networkProviders/constants.ts index d85d175b4..f31aee782 100644 --- a/src/networkProviders/constants.ts +++ b/src/networkProviders/constants.ts @@ -1,7 +1,12 @@ import BigNumber from "bignumber.js"; -import { Address } from "../address"; +import { Address } from "../core/address"; export const MaxUint64AsBigNumber = new BigNumber("18446744073709551615"); export const EsdtContractAddress = new Address("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u"); export const BaseUserAgent = "multiversx-sdk"; export const UnknownClientName = "unknown"; + +export const DEFAULT_ACCOUNT_AWAITING_POLLING_TIMEOUT_IN_MILLISECONDS = 6000; +export const DEFAULT_ACCOUNT_AWAITING_TIMEOUT_IN_MILLISECONDS = + 15 * DEFAULT_ACCOUNT_AWAITING_POLLING_TIMEOUT_IN_MILLISECONDS; +export const DEFAULT_ACCOUNT_AWAITING_PATIENCE_IN_MILLISECONDS = 0; diff --git a/src/networkProviders/contractQueryRequest.ts b/src/networkProviders/contractQueryRequest.ts index d64183c9a..3bafd74f9 100644 --- a/src/networkProviders/contractQueryRequest.ts +++ b/src/networkProviders/contractQueryRequest.ts @@ -1,21 +1,20 @@ -import { IContractQuery } from "./interface"; +import { SmartContractQuery } from "../core/smartContractQuery"; export class ContractQueryRequest { - private readonly query: IContractQuery; + private readonly query: SmartContractQuery; - constructor(query: IContractQuery) { + constructor(query: SmartContractQuery) { this.query = query; } toHttpRequest() { let request: any = {}; let query = this.query; - request.scAddress = query.address.bech32(); - request.caller = query.caller?.bech32() ? query.caller.bech32() : undefined; - request.funcName = query.func.toString(); + request.scAddress = query.contract.toBech32(); + request.caller = query.caller?.toBech32() ? query.caller.toBech32() : undefined; + request.funcName = query.function; request.value = query.value ? query.value.toString() : undefined; - request.args = query.getEncodedArguments(); - + request.args = query.arguments?.map((x) => Buffer.from(x).toString("hex")); return request; } } diff --git a/src/networkProviders/contractResults.ts b/src/networkProviders/contractResults.ts index a7f2a3674..38c057f75 100644 --- a/src/networkProviders/contractResults.ts +++ b/src/networkProviders/contractResults.ts @@ -1,6 +1,5 @@ -import { Address } from "../address"; -import { IAddress } from "./interface"; -import { TransactionLogs } from "./transactionLogs"; +import { Address } from "../core/address"; +import { TransactionLogs } from "../core/transactionLogs"; export class ContractResults { readonly items: ContractResultItem[]; @@ -14,7 +13,7 @@ export class ContractResults { } static fromProxyHttpResponse(results: any[]): ContractResults { - let items = results.map((item) => ContractResultItem.fromProxyHttpResponse(item)); + const items = results.map((item) => ContractResultItem.fromProxyHttpResponse(item)); return new ContractResults(items); } @@ -28,8 +27,8 @@ export class ContractResultItem { hash: string = ""; nonce: number = 0; value: string = ""; - receiver: IAddress = Address.empty(); - sender: IAddress = Address.empty(); + receiver: Address = Address.empty(); + sender: Address = Address.empty(); data: string = ""; previousHash: string = ""; originalHash: string = ""; @@ -44,7 +43,7 @@ export class ContractResultItem { } static fromProxyHttpResponse(response: any): ContractResultItem { - let item = ContractResultItem.fromHttpResponse(response); + const item = ContractResultItem.fromHttpResponse(response); return item; } diff --git a/src/networkProviders/index.ts b/src/networkProviders/index.ts index 99c22853f..97f5dffa9 100644 --- a/src/networkProviders/index.ts +++ b/src/networkProviders/index.ts @@ -1,23 +1,15 @@ export { ApiNetworkProvider } from "./apiNetworkProvider"; +export * from "./interface"; export { ProxyNetworkProvider } from "./proxyNetworkProvider"; -export { AccountOnNetwork } from "./accounts"; export { ContractQueryResponse } from "./contractQueryResponse"; export { ContractResultItem, ContractResults } from "./contractResults"; -export { - TransactionEventData, - TransactionEvent as TransactionEventOnNetwork, - TransactionEventTopic, -} from "./transactionEvents"; -export { TransactionLogs as TransactionLogsOnNetwork } from "./transactionLogs"; -export { TransactionReceipt } from "./transactionReceipt"; -export { TransactionOnNetwork } from "./transactions"; -export { TransactionStatus } from "./transactionStatus"; - -export { DefinitionOfFungibleTokenOnNetwork, DefinitionOfTokenCollectionOnNetwork } from "./tokenDefinitions"; -export { FungibleTokenOfAccountOnNetwork, NonFungibleTokenOfAccountOnNetwork } from "./tokens"; +export * from "./accounts"; +export * from "./blocks"; export { NetworkConfig } from "./networkConfig"; -export { NetworkGeneralStatistics } from "./networkGeneralStatistics"; -export { NetworkStake } from "./networkStake"; +export { NetworkProviderConfig } from "./networkProviderConfig"; export { NetworkStatus } from "./networkStatus"; +export * from "./resources"; +export * from "./tokenDefinitions"; +export * from "./tokens"; diff --git a/src/networkProviders/interface.ts b/src/networkProviders/interface.ts index eb73db286..0920d8474 100644 --- a/src/networkProviders/interface.ts +++ b/src/networkProviders/interface.ts @@ -1,14 +1,18 @@ -import { ITransaction as ITransactionAsInSpecs } from "../interface"; -import { AccountOnNetwork } from "./accounts"; -import { ContractQueryResponse } from "./contractQueryResponse"; +import { + Address, + SmartContractQuery, + SmartContractQueryResponse, + Token, + Transaction, + TransactionOnNetwork, + TransactionStatus, +} from "../core"; +import { AccountOnNetwork, AccountStorage, AccountStorageEntry } from "./accounts"; import { NetworkConfig } from "./networkConfig"; -import { NetworkGeneralStatistics } from "./networkGeneralStatistics"; -import { NetworkStake } from "./networkStake"; import { NetworkStatus } from "./networkStatus"; +import { AwaitingOptions, TransactionCostResponse } from "./resources"; import { DefinitionOfFungibleTokenOnNetwork, DefinitionOfTokenCollectionOnNetwork } from "./tokenDefinitions"; -import { FungibleTokenOfAccountOnNetwork, NonFungibleTokenOfAccountOnNetwork } from "./tokens"; -import { TransactionOnNetwork } from "./transactions"; -import { TransactionStatus } from "./transactionStatus"; +import { TokenAmountOnNetwork } from "./tokens"; /** * An interface that defines the endpoints of an HTTP API Provider. @@ -25,46 +29,49 @@ export interface INetworkProvider { getNetworkStatus(): Promise; /** - * Fetches stake statistics. + * Fetches the state of an account. */ - getNetworkStakeStatistics(): Promise; + getAccount(address: Address): Promise; /** - * Fetches general statistics. + * Fetches the storage (key-value pairs) of an account. */ - getNetworkGeneralStatistics(): Promise; + getAccountStorage(address: Address): Promise; /** - * Fetches the state of an account. + * Fetches a specific storage entry of an account. */ - getAccount(address: IAddress): Promise; + getAccountStorageEntry(address: Address, entryKey: string): Promise; /** - * Fetches data about the fungible tokens held by an account. + * Waits until an account satisfies a given condition. + * Can throw: + * - ErrAwaitConditionNotReached */ - getFungibleTokensOfAccount(address: IAddress, pagination?: IPagination): Promise; + awaitAccountOnCondition( + address: Address, + condition: (account: AccountOnNetwork) => boolean, + options?: AwaitingOptions, + ): Promise; /** - * Fetches data about the non-fungible tokens held by account. + * Broadcasts an already-signed transaction. */ - getNonFungibleTokensOfAccount( - address: IAddress, - pagination?: IPagination, - ): Promise; + sendTransaction(tx: Transaction): Promise; /** - * Fetches data about a specific fungible token held by an account. + * Simulates the processing of an already-signed transaction. * + */ + simulateTransaction(tx: Transaction): Promise; + /** + * Estimates the cost of a transaction. * */ - getFungibleTokenOfAccount(address: IAddress, tokenIdentifier: string): Promise; + estimateTransactionCost(tx: Transaction): Promise; /** - * Fetches data about a specific non-fungible token (instance) held by an account. + * Broadcasts a list of already-signed transactions. */ - getNonFungibleTokenOfAccount( - address: IAddress, - collection: string, - nonce: number, - ): Promise; + sendTransactions(txs: Transaction[]): Promise<[number, string[]]>; /** * Fetches the state of a transaction. @@ -72,30 +79,42 @@ export interface INetworkProvider { getTransaction(txHash: string, withProcessStatus?: boolean): Promise; /** - * Queries the status of a transaction. + * Fetches the status of a transaction. */ getTransactionStatus(txHash: string): Promise; /** - * Broadcasts an already-signed transaction. + * Waits until the transaction is completely processed. + * Can throw: + * - ErrAwaitConditionNotReached */ - sendTransaction(tx: ITransaction | ITransactionNext): Promise; + awaitTransactionCompleted(transactionHash: string, options?: AwaitingOptions): Promise; /** - * Broadcasts a list of already-signed transactions. + * Waits until the transaction satisfies a given condition. + * Can throw: + * - ErrAwaitConditionNotReached */ - sendTransactions(txs: (ITransaction | ITransactionNext)[]): Promise; + awaitTransactionOnCondition( + transactionHash: string, + condition: (account: TransactionOnNetwork) => boolean, + options?: AwaitingOptions, + ): Promise; /** - * Simulates the processing of an already-signed transaction. - * + * Fetches the balance of an account, for a given token. */ - simulateTransaction(tx: ITransaction): Promise; + getTokenOfAccount(address: Address, token: Token): Promise; /** - * Queries a Smart Contract - runs a pure function defined by the contract and returns its results. + * Fetches data about the fungible tokens held by an account. + */ + getFungibleTokensOfAccount(address: Address, pagination?: IPagination): Promise; + + /** + * Fetches data about the non-fungible tokens held by account. */ - queryContract(query: IContractQuery): Promise; + getNonFungibleTokensOfAccount(address: Address, pagination?: IPagination): Promise; /** * Fetches the definition of a fungible token. @@ -108,9 +127,9 @@ export interface INetworkProvider { getDefinitionOfTokenCollection(collection: string): Promise; /** - * Fetches data about a specific non-fungible token (instance). + * Queries a Smart Contract - runs a pure function defined by the contract and returns its results. */ - getNonFungibleToken(collection: string, nonce: number): Promise; + queryContract(query: SmartContractQuery): Promise; /** * Performs a generic GET action against the provider (useful for new HTTP endpoints). @@ -123,28 +142,7 @@ export interface INetworkProvider { doPostGeneric(resourceUrl: string, payload: any): Promise; } -export interface IContractQuery { - address: IAddress; - caller?: IAddress; - func: { toString(): string }; - value?: { toString(): string }; - getEncodedArguments(): string[]; -} - export interface IPagination { from: number; size: number; } - -export interface ITransaction { - toSendable(): any; -} - -export interface IAddress { - bech32(): string; -} - -/** - * @deprecated This will be removed with the next release (replaced by the `ITransaction` interface from "src/interface.ts"). - */ -export type ITransactionNext = ITransactionAsInSpecs; diff --git a/src/networkProviders/networkConfig.ts b/src/networkProviders/networkConfig.ts index be931e3c4..0efbff2c4 100644 --- a/src/networkProviders/networkConfig.ts +++ b/src/networkProviders/networkConfig.ts @@ -1,65 +1,65 @@ -import BigNumber from "bignumber.js"; - /** * An object holding Network configuration parameters. */ export class NetworkConfig { + raw: Record = {}; /** * The chain ID. E.g. "1" for the Mainnet. */ - public ChainID: string; + public chainID: string; /** * The gas required by the Network to process a byte of the transaction data. */ - public GasPerDataByte: number; + public gasPerDataByte: bigint; + + public gasPriceModifier: number; + /** - * The round duration. + * The minimum gas limit required to be set when broadcasting a transaction. */ - public RoundDuration: number; + public minGasLimit: bigint; + /** - * The number of rounds per epoch. + * The minimum gas price required to be set when broadcasting a transaction. */ - public RoundsPerEpoch: number; + public minGasPrice: bigint; /** - * The Top Up Factor for APR calculation + * The extra gas needed for guarded transactions. */ - public TopUpFactor: number; + public extraGasLimitForGuardedTransactions: bigint; /** - * The Top Up Factor for APR calculation + * The number of shards. */ - public TopUpRewardsGradientPoint: BigNumber; - - public GasPriceModifier: number; + public numShards: number; /** - * The minimum gas limit required to be set when broadcasting a transaction. + * The round duration. */ - public MinGasLimit: number; - + public roundDuration: number; /** - * The minimum gas price required to be set when broadcasting a transaction. + * The number of rounds per epoch. */ - public MinGasPrice: number; + public numRoundsPerEpoch: number; /** - * The oldest transaction version accepted by the Network. + * The genesis timestamp */ - public MinTransactionVersion: number; + public genesisTimestamp: number; constructor() { - this.ChainID = "T"; - this.GasPerDataByte = 1500; - this.TopUpFactor = 0; - this.RoundDuration = 0; - this.RoundsPerEpoch = 0; - this.TopUpRewardsGradientPoint = new BigNumber(0); - this.MinGasLimit = 50000; - this.MinGasPrice = 1000000000; - this.GasPriceModifier = 1; - this.MinTransactionVersion = 1; + this.chainID = ""; + this.gasPerDataByte = 0n; + this.genesisTimestamp = 0; + this.roundDuration = 0; + this.numRoundsPerEpoch = 0; + this.minGasLimit = 0n; + this.minGasPrice = 0n; + this.extraGasLimitForGuardedTransactions = 0n; + this.gasPriceModifier = 1; + this.numShards = 0; } /** @@ -68,16 +68,17 @@ export class NetworkConfig { static fromHttpResponse(payload: any): NetworkConfig { let networkConfig = new NetworkConfig(); - networkConfig.ChainID = String(payload["erd_chain_id"]); - networkConfig.GasPerDataByte = Number(payload["erd_gas_per_data_byte"]); - networkConfig.TopUpFactor = Number(payload["erd_top_up_factor"]); - networkConfig.RoundDuration = Number(payload["erd_round_duration"]); - networkConfig.RoundsPerEpoch = Number(payload["erd_rounds_per_epoch"]); - networkConfig.TopUpRewardsGradientPoint = new BigNumber(payload["erd_rewards_top_up_gradient_point"]); - networkConfig.MinGasLimit = Number(payload["erd_min_gas_limit"]); - networkConfig.MinGasPrice = Number(payload["erd_min_gas_price"]); - networkConfig.MinTransactionVersion = Number(payload["erd_min_transaction_version"]); - networkConfig.GasPriceModifier = Number(payload["erd_gas_price_modifier"]); + networkConfig.raw = payload; + networkConfig.chainID = String(payload["erd_chain_id"]); + networkConfig.gasPerDataByte = BigInt(payload["erd_gas_per_data_byte"]); + networkConfig.gasPriceModifier = Number(payload["erd_top_up_factor"]); + networkConfig.minGasLimit = BigInt(payload["erd_min_gas_limit"]); + networkConfig.minGasPrice = BigInt(payload["erd_min_gas_price"]); + networkConfig.extraGasLimitForGuardedTransactions = BigInt(payload["erd_extra_gas_limit_guarded_tx"]); + networkConfig.numShards = Number(payload["erd_num_shards_without_meta"]); + networkConfig.roundDuration = Number(payload["erd_round_duration"]); + networkConfig.numRoundsPerEpoch = Number(payload["erd_rounds_per_epoch"]); + networkConfig.genesisTimestamp = Number(payload["erd_start_time"]); return networkConfig; } diff --git a/src/networkProviders/networkGeneralStatistics.ts b/src/networkProviders/networkGeneralStatistics.ts deleted file mode 100644 index 2935b1cbc..000000000 --- a/src/networkProviders/networkGeneralStatistics.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * An object holding general Network statistics and parameters. - */ -export class NetworkGeneralStatistics { - /** - * The number of Shards. - */ - public Shards: number; - - /** - * The Number of Blocks. - */ - public Blocks: number; - - /** - * The Number of Accounts. - */ - public Accounts: number; - - /** - * The Number of transactions. - */ - public Transactions: number; - - /** - * The Refresh rate. - */ - public RefreshRate: number; - - /** - * The Number of the current Epoch. - */ - public Epoch: number; - - /** - * The Number of rounds passed. - */ - public RoundsPassed: number; - - /** - * The Number of Rounds per epoch. - */ - public RoundsPerEpoch: number; - - constructor() { - this.Shards = 0; - this.Blocks = 0; - this.Accounts = 0; - this.Transactions = 0; - this.RefreshRate = 0; - this.Epoch = 0; - this.RoundsPassed = 0; - this.RoundsPerEpoch = 0; - } - - /** - * Constructs a stats object from a HTTP response (as returned by the provider). - */ - static fromHttpResponse(payload: any): NetworkGeneralStatistics { - let stats = new NetworkGeneralStatistics(); - - stats.Shards = Number(payload["shards"]); - stats.Blocks = Number(payload["blocks"]); - stats.Accounts = Number(payload["accounts"]); - stats.Transactions = Number(payload["transactions"]); - stats.RefreshRate = Number(payload["refreshRate"]); - stats.Epoch = Number(payload["epoch"]); - stats.RoundsPassed = Number(payload["roundsPassed"]); - stats.RoundsPerEpoch = Number(payload["roundsPerEpoch"]); - - return stats; - } -} diff --git a/src/networkProviders/networkProviderConfig.ts b/src/networkProviders/networkProviderConfig.ts index b66bffbcc..6d1260e43 100644 --- a/src/networkProviders/networkProviderConfig.ts +++ b/src/networkProviders/networkProviderConfig.ts @@ -1,4 +1,4 @@ -import { AxiosRequestConfig } from 'axios'; +import { AxiosRequestConfig } from "axios"; export interface NetworkProviderConfig extends AxiosRequestConfig { clientName?: string; diff --git a/src/networkProviders/networkStake.ts b/src/networkProviders/networkStake.ts deleted file mode 100644 index 5806f9eb2..000000000 --- a/src/networkProviders/networkStake.ts +++ /dev/null @@ -1,47 +0,0 @@ -import BigNumber from "bignumber.js"; - -/** - * An object holding Network stake parameters. - */ -export class NetworkStake { - private static default: NetworkStake; - - /** - * The Total Validators Number. - */ - public TotalValidators: number; - - /** - * The Active Validators Number. - */ - public ActiveValidators: number; - /** - * The Queue Size. - */ - public QueueSize: number; - /** - * The Total Validators Number. - */ - public TotalStaked: BigNumber; - - constructor() { - this.TotalValidators = 0; - this.ActiveValidators = 0; - this.QueueSize = 0; - this.TotalStaked = new BigNumber(0); - } - - /** - * Constructs a configuration object from a HTTP response (as returned by the provider). - */ - static fromHttpResponse(payload: any): NetworkStake { - let networkStake = new NetworkStake(); - - networkStake.TotalValidators = Number(payload["totalValidators"]); - networkStake.ActiveValidators = Number(payload["activeValidators"]); - networkStake.QueueSize = Number(payload["queueSize"]); - networkStake.TotalStaked = new BigNumber(payload["totalStaked"]); - - return networkStake; - } -} diff --git a/src/networkProviders/networkStatus.ts b/src/networkProviders/networkStatus.ts index ec42a479c..0988a1707 100644 --- a/src/networkProviders/networkStatus.ts +++ b/src/networkProviders/networkStatus.ts @@ -2,63 +2,39 @@ * An object holding network status configuration parameters. */ export class NetworkStatus { - private static default: NetworkStatus; + raw: Record = {}; /** - * The current round. - */ - public CurrentRound: number; - - /** - * The epoch number. - */ - public EpochNumber: number; - - /** - * The Highest final nonce. + * The block timestamp. */ - public HighestFinalNonce: number; + public blockTimestamp: number; /** - * The erd nonce. + * The block nonce. */ - public Nonce: number; + public blockNonce: bigint; /** - * The nonce at epoch start. + * The highest final nonce. */ - public NonceAtEpochStart: number; + public highestFinalNonce: bigint; /** - * The nonces passed in current epoch. - */ - public NoncesPassedInCurrentEpoch: number; - - /** - * The round at epoch start - */ - public RoundAtEpochStart: number; - - /** - * The rounds passed in current epoch + * The current round. */ - public RoundsPassedInCurrentEpoch: number; + public currentRound: bigint; /** - * The rounds per epoch + * The current epoch. */ - public RoundsPerEpoch: number; + public currentEpoch: number; constructor() { - this.CurrentRound = 0; - this.EpochNumber = 0; - this.HighestFinalNonce = 0; - this.Nonce = 0; - this.NonceAtEpochStart = 0; - this.NoncesPassedInCurrentEpoch = 0; - this.RoundAtEpochStart = 0; - this.RoundsPassedInCurrentEpoch = 0; - this.RoundsPerEpoch = 0; + this.currentRound = 0n; + this.currentEpoch = 0; + this.highestFinalNonce = 0n; + this.blockNonce = 0n; + this.blockTimestamp = 0; } /** @@ -67,15 +43,12 @@ export class NetworkStatus { static fromHttpResponse(payload: any): NetworkStatus { let networkStatus = new NetworkStatus(); - networkStatus.CurrentRound = Number(payload["erd_current_round"]); - networkStatus.EpochNumber = Number(payload["erd_epoch_number"]); - networkStatus.HighestFinalNonce = Number(payload["erd_highest_final_nonce"]); - networkStatus.Nonce = Number(payload["erd_nonce"]); - networkStatus.NonceAtEpochStart = Number(payload["erd_nonce_at_epoch_start"]); - networkStatus.NoncesPassedInCurrentEpoch = Number(payload["erd_nonces_passed_in_current_epoch"]); - networkStatus.RoundAtEpochStart = Number(payload["erd_round_at_epoch_start"]); - networkStatus.RoundsPassedInCurrentEpoch = Number(payload["erd_rounds_passed_in_current_epoch"]); - networkStatus.RoundsPerEpoch = Number(payload["erd_rounds_per_epoch"]); + networkStatus.raw = payload; + networkStatus.currentRound = BigInt(payload["erd_current_round"]); + networkStatus.currentEpoch = Number(payload["erd_epoch_number"]); + networkStatus.highestFinalNonce = BigInt(payload["erd_highest_final_nonce"]); + networkStatus.blockNonce = BigInt(payload["erd_nonce"]); + networkStatus.blockTimestamp = Number(payload["erd_block_timestamp"]); return networkStatus; } diff --git a/src/networkProviders/pairs.ts b/src/networkProviders/pairs.ts deleted file mode 100644 index 31a92bf12..000000000 --- a/src/networkProviders/pairs.ts +++ /dev/null @@ -1,55 +0,0 @@ -import BigNumber from "bignumber.js"; -import { Address } from "../address"; -import { IAddress } from "./interface"; - -export class PairOnNetwork { - address: IAddress = Address.empty(); - id: string = ""; - symbol: string = ""; - name: string = ""; - price: BigNumber = new BigNumber(0); - baseId: string = ""; - basePrice: BigNumber = new BigNumber(0); - baseSymbol: string = ""; - baseName: string = ""; - quoteId: string = ""; - quotePrice: BigNumber = new BigNumber(0); - quoteSymbol: string = ""; - quoteName: string = ""; - totalValue: BigNumber = new BigNumber(0); - volume24h: BigNumber = new BigNumber(0); - state: string = ""; - type: string = ""; - - rawResponse: any = {}; - - constructor(init?: Partial) { - Object.assign(this, init); - } - - static fromApiHttpResponse(payload: any): PairOnNetwork { - let result = new PairOnNetwork(); - - result.address = new Address(payload.address || ""); - result.id = payload.id || ""; - result.symbol = payload.symbol || ""; - result.name = payload.name || ""; - result.price = new BigNumber(payload.price || 0); - result.baseId = payload.baseId || ""; - result.basePrice = new BigNumber(payload.basePrice || 0); - result.baseSymbol = payload.baseSymbol || ""; - result.baseName = payload.baseName || ""; - result.quoteId = payload.quoteId || ""; - result.quotePrice = new BigNumber(payload.quotePrice || 0); - result.quoteSymbol = payload.quoteSymbol || ""; - result.quoteName = payload.quoteName || ""; - result.totalValue = new BigNumber(payload.totalValue || 0); - result.volume24h = new BigNumber(payload.volume24h || 0); - result.state = payload.state || ""; - result.type = payload.type || ""; - - result.rawResponse = payload; - - return result; - } -} diff --git a/src/networkProviders/providers.dev.net.spec.ts b/src/networkProviders/providers.dev.net.spec.ts index 428a5d692..e2555f4f6 100644 --- a/src/networkProviders/providers.dev.net.spec.ts +++ b/src/networkProviders/providers.dev.net.spec.ts @@ -1,21 +1,12 @@ import { AxiosHeaders } from "axios"; import { assert } from "chai"; -import { Address } from "../address"; -import { loadTestWallet } from "../testutils"; -import { MockQuery } from "../testutils/dummyQuery"; -import { TransactionComputer } from "../transactionComputer"; +import { Address, SmartContractQuery, Transaction, TransactionOnNetwork } from "../core"; import { ApiNetworkProvider } from "./apiNetworkProvider"; -import { INetworkProvider, ITransactionNext } from "./interface"; +import { INetworkProvider } from "./interface"; import { ProxyNetworkProvider } from "./proxyNetworkProvider"; -import { NonFungibleTokenOfAccountOnNetwork } from "./tokens"; -import { TransactionEventData } from "./transactionEvents"; -import { TransactionOnNetwork } from "./transactions"; describe("test network providers on devnet: Proxy and API", function () { let alice = new Address("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - let carol = new Address("erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8"); - let dan = new Address("erd1kyaqzaprcdnv4luvanah0gfxzzsnpaygsy6pytrexll2urtd05ts9vegu7"); - const MAX_NUMBER_OF_ITEMS_BY_DEFAULT = 20; let apiProvider: INetworkProvider = new ApiNetworkProvider("https://devnet-api.multiversx.com", { timeout: 10000, @@ -33,8 +24,8 @@ describe("test network providers on devnet: Proxy and API", function () { const apiResponse = await apiProviderWithoutConfig.getNetworkConfig(); const proxyResponse = await proxyProviderWithoutConfig.getNetworkConfig(); - assert.equal(apiResponse.ChainID, "D"); - assert.equal(proxyResponse.ChainID, "D"); + assert.equal(apiResponse.chainID, "D"); + assert.equal(proxyResponse.chainID, "D"); }); it("should have same response for getNetworkConfig()", async function () { @@ -93,37 +84,6 @@ describe("test network providers on devnet: Proxy and API", function () { assert.equal(localProxyProvider.config.headers.getUserAgent(), expectedProxyUserAgent); }); - it("should have same response for getNetworkStatus()", async function () { - let apiResponse = await apiProvider.getNetworkStatus(); - let proxyResponse = await proxyProvider.getNetworkStatus(); - - assert.equal(apiResponse.EpochNumber, proxyResponse.EpochNumber); - assert.equal(apiResponse.NonceAtEpochStart, proxyResponse.NonceAtEpochStart); - assert.equal(apiResponse.RoundAtEpochStart, proxyResponse.RoundAtEpochStart); - assert.equal(apiResponse.RoundsPerEpoch, proxyResponse.RoundsPerEpoch); - // done this way because the nonces may change until both requests are executed - assert.approximately(apiResponse.CurrentRound, proxyResponse.CurrentRound, 1); - assert.approximately(apiResponse.HighestFinalNonce, proxyResponse.HighestFinalNonce, 1); - assert.approximately(apiResponse.Nonce, proxyResponse.Nonce, 1); - assert.approximately(apiResponse.NoncesPassedInCurrentEpoch, proxyResponse.NoncesPassedInCurrentEpoch, 1); - }); - - // TODO: Enable test after implementing ProxyNetworkProvider.getNetworkStakeStatistics(). - it.skip("should have same response for getNetworkStakeStatistics()", async function () { - let apiResponse = await apiProvider.getNetworkStakeStatistics(); - let proxyResponse = await proxyProvider.getNetworkStakeStatistics(); - - assert.deepEqual(apiResponse, proxyResponse); - }); - - // TODO: Enable test after implementing ProxyNetworkProvider.getNetworkGeneralStatistics(). - it.skip("should have same response for getNetworkGeneralStatistics()", async function () { - let apiResponse = await apiProvider.getNetworkGeneralStatistics(); - let proxyResponse = await proxyProvider.getNetworkGeneralStatistics(); - - assert.deepEqual(apiResponse, proxyResponse); - }); - it("should have same response for getAccount()", async function () { let apiResponse = await apiProvider.getAccount(alice); let proxyResponse = await proxyProvider.getAccount(alice); @@ -131,161 +91,59 @@ describe("test network providers on devnet: Proxy and API", function () { assert.deepEqual(apiResponse, proxyResponse); }); - it("should have same response for getFungibleTokensOfAccount(), getFungibleTokenOfAccount()", async function () { - this.timeout(30000); - - for (const user of [carol, dan]) { - let apiResponse = (await apiProvider.getFungibleTokensOfAccount(user)).slice( - 0, - MAX_NUMBER_OF_ITEMS_BY_DEFAULT, - ); - let proxyResponse = (await proxyProvider.getFungibleTokensOfAccount(user)).slice( - 0, - MAX_NUMBER_OF_ITEMS_BY_DEFAULT, - ); - - for (let i = 0; i < apiResponse.length; i++) { - assert.equal(apiResponse[i].identifier, proxyResponse[i].identifier); - assert.equal(apiResponse[i].balance.valueOf, proxyResponse[i].balance.valueOf); - } - } - }); - - it("should have same response for getNonFungibleTokensOfAccount(), getNonFungibleTokenOfAccount", async function () { - this.timeout(30000); - - let apiResponse = (await apiProvider.getNonFungibleTokensOfAccount(dan)).slice( - 0, - MAX_NUMBER_OF_ITEMS_BY_DEFAULT, - ); - let proxyResponse = (await proxyProvider.getNonFungibleTokensOfAccount(dan)).slice( - 0, - MAX_NUMBER_OF_ITEMS_BY_DEFAULT, - ); - - assert.isTrue(apiResponse.length > 0, "For the sake of the test, there should be at least one item."); - assert.equal(apiResponse.length, proxyResponse.length); - - for (let i = 0; i < apiResponse.length; i++) { - removeInconsistencyForNonFungibleTokenOfAccount(apiResponse[i], proxyResponse[i]); - } - - assert.deepEqual(apiResponse, proxyResponse); - - const item = apiResponse[0]; - let apiItemResponse = await apiProvider.getNonFungibleTokenOfAccount(dan, item.collection, item.nonce); - let proxyItemResponse = await proxyProvider.getNonFungibleTokenOfAccount(dan, item.collection, item.nonce); - - removeInconsistencyForNonFungibleTokenOfAccount(apiItemResponse, proxyItemResponse); - assert.deepEqual(apiResponse, proxyResponse, `user: ${dan.bech32()}, token: ${item.identifier}`); - }); - - // TODO: Strive to have as little differences as possible between Proxy and API. - function removeInconsistencyForNonFungibleTokenOfAccount( - apiResponse: NonFungibleTokenOfAccountOnNetwork, - proxyResponse: NonFungibleTokenOfAccountOnNetwork, - ) { - // unset unconsistent fields - apiResponse.type = ""; - proxyResponse.type = ""; - apiResponse.name = ""; - proxyResponse.name = ""; - apiResponse.decimals = 0; - proxyResponse.decimals = 0; - } - - it("should be able to send transaction with relayer", async function () { - this.timeout(5000); - const grace = await loadTestWallet("grace"); - const relayer = await loadTestWallet("alice"); - const transactionComputer = new TransactionComputer(); - const nonce = (await apiProvider.getAccount(grace.getAddress())).nonce; - const transaction: ITransactionNext = { - receiver: grace.getAddress().bech32(), - sender: grace.getAddress().bech32(), - gasPrice: BigInt(1000000000), - gasLimit: BigInt(150000), - chainID: "D", - version: 1, - nonce: BigInt(nonce), - relayer: relayer.getAddress(), - value: BigInt(1), - senderUsername: "", - receiverUsername: "", - guardian: "", - guardianSignature: new Uint8Array(), - options: 0, - data: new Uint8Array(), - signature: new Uint8Array(), - relayerSignature: new Uint8Array(), - }; - transaction.signature = await grace.signer.sign(transactionComputer.computeBytesForSigning(transaction)); - - const buffer = transactionComputer.computeBytesForSigning(transaction); - - const signature = await relayer.signer.sign(Buffer.from(buffer)); - transaction.relayerSignature = signature; - - const hash = await proxyProvider.sendTransaction(transaction); - assert.isNotNull(hash); - }); - it("should be able to send transaction(s)", async function () { this.timeout(5000); const txs = [ - { - toSendable: function () { - return { - nonce: 42, - value: "1", - receiver: "erd1testnlersh4z0wsv8kjx39me4rmnvjkwu8dsaea7ukdvvc9z396qykv7z7", - sender: "erd15x2panzqvfxul2lvstfrmdcl5t4frnsylfrhng8uunwdssxw4y9succ9sq", - gasPrice: 1000000000, - gasLimit: 50000, - chainID: "D", - version: 1, - signature: - "c8eb539e486db7d703d8c70cab3b7679113f77c4685d8fcc94db027ceacc6b8605115034355386dffd7aa12e63dbefa03251a2f1b1d971f52250187298d12900", - }; - }, - }, - { - toSendable: function () { - return { - nonce: 43, - value: "1", - receiver: "erd1testnlersh4z0wsv8kjx39me4rmnvjkwu8dsaea7ukdvvc9z396qykv7z7", - sender: "erd15x2panzqvfxul2lvstfrmdcl5t4frnsylfrhng8uunwdssxw4y9succ9sq", - gasPrice: 1000000000, - gasLimit: 50000, - chainID: "D", - version: 1, - signature: - "9c4c22d0ae1b5a10c39583a5ab9020b00b27aa69d4ac8ab4922620dbf0df4036ed890f9946d38a9d0c85d6ac485c0d9b2eac0005e752f249fd0ad863b0471d02", - }; - }, - }, - { - toSendable: function () { - return { - nonce: 44, - }; - }, - }, + new Transaction({ + nonce: 103n, + receiver: Address.newFromBech32("erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"), + sender: Address.newFromBech32("erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"), + gasPrice: 1000000000n, + gasLimit: 50000n, + chainID: "D", + version: 2, + signature: Buffer.from( + "498d5abb9f8eb69cc75f24320e8929dadbfa855ffac220d5e92175a83be68e0437801af3a1411e3d839738230097a1c38da5c8c4df3f345defc5d40300675900", + "hex", + ), + }), + + new Transaction({ + nonce: 104n, + receiver: Address.newFromBech32("erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"), + sender: Address.newFromBech32("erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"), + gasPrice: 1000000000n, + gasLimit: 50000n, + chainID: "D", + version: 2, + signature: Buffer.from( + "341a2f3b738fbd20692e3bbd1cb36cb5f4ce9c0a9acc0cf4322269c0fcf34fd6bb59cd94062a9a4730e47f41b1ef3e29b69c6ab2a2a4dca9c9a7724681bc1708", + "hex", + ), + }), + new Transaction({ + nonce: 77n, + chainID: "D", + receiver: Address.newFromBech32("erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"), + sender: Address.newFromBech32("erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"), + gasLimit: 50000n, + gasPrice: 1000000000n, + }), ]; const expectedHashes = [ - "6e2fa63ea02937f00d7549f3e4eb9af241e4ac13027aa65a5300816163626c01", - "37d7e84313a5baea2a61c6ab10bb29b52bc54f7ac9e3918a9faeb1e08f42081c", + "61b4f2561fc57bfb8b8971ed23cd64259b664bc0404ea7a0449def8ceef24b08", + "30274b60b5635f981fa89ccfe726a34ca7121caa5d34123021c77a5c64cc9163", null, ]; assert.equal(await apiProvider.sendTransaction(txs[0]), expectedHashes[0]); assert.equal(await proxyProvider.sendTransaction(txs[1]), expectedHashes[1]); - - assert.deepEqual(await apiProvider.sendTransactions(txs), expectedHashes); - assert.deepEqual(await proxyProvider.sendTransactions(txs), expectedHashes); + const [, apiHashes] = await apiProvider.sendTransactions(txs); + const [, proxyHashes] = await proxyProvider.sendTransactions(txs); + assert.deepEqual(apiHashes, expectedHashes); + assert.deepEqual(proxyHashes, expectedHashes); }); it("should have same response for getTransaction()", async function () { @@ -309,6 +167,22 @@ describe("test network providers on devnet: Proxy and API", function () { } }); + it("should have same response for getTransactionStatus()", async function () { + this.timeout(20000); + + let hashes = [ + "08acf8cbd71306a56eb58f9593cb2e23f109c94e27acdd906c82a5c3a5f84d9d", + "410efb1db2ab86678b8dbc503beb695b5b7d52754fb0de86c09cbb433de5f6a8", + ]; + + for (const hash of hashes) { + let apiResponse = await apiProvider.getTransactionStatus(hash); + let proxyResponse = await proxyProvider.getTransactionStatus(hash); + + assert.deepEqual(apiResponse, proxyResponse, `transaction: ${hash}`); + } + }); + // TODO: Strive to have as little differences as possible between Proxy and API. function ignoreKnownTransactionDifferencesBetweenProviders( apiResponse: TransactionOnNetwork, @@ -316,12 +190,18 @@ describe("test network providers on devnet: Proxy and API", function () { ) { // Proxy and API exhibit differences in the "function" field, in case of move-balance transactions. apiResponse.function = proxyResponse.function; - + apiResponse.raw = {}; + apiResponse.smartContractResults.map((x) => (x.raw = {})); + apiResponse.smartContractResults.map((x) => x.logs.events.map((e) => (e.raw = {}))); + apiResponse.logs.events.map((e) => (e.raw = {})); // Ignore fields which are not present on API response: proxyResponse.epoch = 0; - proxyResponse.blockNonce = 0; - proxyResponse.hyperblockNonce = 0; - proxyResponse.hyperblockHash = ""; + proxyResponse.blockHash = ""; + proxyResponse.miniblockHash = ""; + proxyResponse.raw = {}; + proxyResponse.smartContractResults.map((x) => (x.raw = {})); + proxyResponse.smartContractResults.map((x) => x.logs.events.map((e) => (e.raw = {}))); + proxyResponse.logs.events.map((e) => (e.raw = {})); } it("should have the same response for transactions with events", async function () { @@ -334,38 +214,25 @@ describe("test network providers on devnet: Proxy and API", function () { assert.exists(proxyResponse.logs); assert.exists(apiResponse.logs.events); assert.exists(proxyResponse.logs.events); - assert.equal(apiResponse.logs.events[0].topics[0].hex(), "414c4943452d353632376631"); - assert.equal(apiResponse.logs.events[0].topics[1].hex(), ""); - assert.equal(apiResponse.logs.events[0].topics[2].hex(), "01"); + assert.equal(Buffer.from(apiResponse.logs.events[0].topics[0]).toString("hex"), "414c4943452d353632376631"); + assert.equal(Buffer.from(apiResponse.logs.events[0].topics[1]).toString("hex"), ""); + assert.equal(Buffer.from(apiResponse.logs.events[0].topics[2]).toString("hex"), "01"); assert.equal( - apiResponse.logs.events[0].topics[3].hex(), + Buffer.from(apiResponse.logs.events[0].topics[3]).toString("hex"), "0000000000000000050032e141d21536e2dfc3d64b9e7dd0c2c53f201dc469e1", ); - assert.equal(proxyResponse.logs.events[0].topics[0].hex(), "414c4943452d353632376631"); - assert.equal(proxyResponse.logs.events[0].topics[1].hex(), ""); - assert.equal(proxyResponse.logs.events[0].topics[2].hex(), "01"); assert.equal( - proxyResponse.logs.events[0].topics[3].hex(), + Buffer.from(proxyResponse.logs.events[0].topics[0].toString()).toString("hex"), + "414c4943452d353632376631", + ); + assert.equal(Buffer.from(proxyResponse.logs.events[0].topics[1].toString()).toString("hex"), ""); + assert.equal(Buffer.from(proxyResponse.logs.events[0].topics[2].toString()).toString("hex"), "01"); + assert.equal( + Buffer.from(proxyResponse.logs.events[0].topics[3]).toString("hex"), "0000000000000000050032e141d21536e2dfc3d64b9e7dd0c2c53f201dc469e1", ); }); - it("should have same response for getTransactionStatus()", async function () { - this.timeout(20000); - - let hashes = [ - "08acf8cbd71306a56eb58f9593cb2e23f109c94e27acdd906c82a5c3a5f84d9d", - "410efb1db2ab86678b8dbc503beb695b5b7d52754fb0de86c09cbb433de5f6a8", - ]; - - for (const hash of hashes) { - let apiResponse = await apiProvider.getTransactionStatus(hash); - let proxyResponse = await proxyProvider.getTransactionStatus(hash); - - assert.deepEqual(apiResponse, proxyResponse, `transaction: ${hash}`); - } - }); - it("should have same response for getDefinitionOfFungibleToken()", async function () { this.timeout(10000); @@ -395,40 +262,20 @@ describe("test network providers on devnet: Proxy and API", function () { } }); - it("should have same response for getNonFungibleToken()", async function () { - this.timeout(10000); - - let tokens = [{ id: "TEST-37adcf", nonce: 1 }]; - - for (const token of tokens) { - let apiResponse = await apiProvider.getNonFungibleToken(token.id, token.nonce); - - assert.equal(apiResponse.collection, token.id); - - // TODO: Uncomment after implementing the function in the proxy provider. - // let proxyResponse = await proxyProvider.getNonFungibleToken(token.id, token.nonce); - // assert.deepEqual(apiResponse, proxyResponse); - } - }); - it("should have same response for queryContract()", async function () { this.timeout(10000); // Query: get sum (of adder contract) - let query = new MockQuery({ - address: new Address("erd1qqqqqqqqqqqqqpgqfzydqmdw7m2vazsp6u5p95yxz76t2p9rd8ss0zp9ts"), - func: "getSum", + let query = new SmartContractQuery({ + contract: new Address("erd1qqqqqqqqqqqqqpgqfzydqmdw7m2vazsp6u5p95yxz76t2p9rd8ss0zp9ts"), + function: "getSum", }); let apiResponse = await apiProvider.queryContract(query); let proxyResponse = await proxyProvider.queryContract(query); - // Ignore "gasUsed" due to numerical imprecision (API). - apiResponse.gasUsed = 0; - proxyResponse.gasUsed = 0; - assert.deepEqual(apiResponse, proxyResponse); - assert.deepEqual(apiResponse.getReturnDataParts(), proxyResponse.getReturnDataParts()); + assert.deepEqual(apiResponse.returnDataParts, proxyResponse.returnDataParts); }); it("should handle events 'data' and 'additionalData'", async function () { @@ -441,41 +288,21 @@ describe("test network providers on devnet: Proxy and API", function () { "a419271407a2ec217739811805e3a751e30dbc72ae0777e3b4c825f036995184", ); - assert.equal(apiResponse.logs.events[0].data, Buffer.from("test").toString()); - assert.equal(proxyResponse.logs.events[0].data, Buffer.from("test").toString()); + assert.deepEqual(apiResponse.logs.events[0].data, Buffer.from("dGVzdA==", "base64")); + assert.deepEqual(proxyResponse.logs.events[0].data, Buffer.from("dGVzdA==", "base64")); - assert.deepEqual(apiResponse.logs.events[0].dataPayload, TransactionEventData.fromBase64("dGVzdA==")); - assert.deepEqual(proxyResponse.logs.events[0].dataPayload, TransactionEventData.fromBase64("dGVzdA==")); - - assert.deepEqual(apiResponse.logs.events[0].additionalData, [TransactionEventData.fromBase64("dGVzdA==")]); - assert.deepEqual(proxyResponse.logs.events[0].additionalData, [TransactionEventData.fromBase64("dGVzdA==")]); + assert.deepEqual(apiResponse.logs.events[0].additionalData, [Buffer.from("dGVzdA==", "base64")]); + assert.deepEqual(proxyResponse.logs.events[0].additionalData, [Buffer.from("dGVzdA==", "base64")]); }); - it("should send both `Transaction` and `TransactionNext`", async function () { + it("should send both `Transaction` ", async function () { this.timeout(50000); - const transaction = { - toSendable: function () { - return { - nonce: 7, - value: "0", - receiver: "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", - sender: "erd1zztjf9fhwvuvquzsllknq4qcmffwad6n0hjtn5dyzytr5tgz7uas0mkgrq", - gasPrice: 1000000000, - gasLimit: 50000, - chainID: "D", - version: 2, - signature: - "149f1d8296efcb9489c5b3142ae659aacfa3a7daef3645f1d3747a96dc9cee377070dd8b83b322997c15ba3c305ac18daaee0fd25760eba334b14a9272b34802", - }; - }, - }; - - const transactionNext: ITransactionNext = { + const transaction = new Transaction({ nonce: BigInt(8), value: BigInt(0), - receiver: "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", - sender: "erd1zztjf9fhwvuvquzsllknq4qcmffwad6n0hjtn5dyzytr5tgz7uas0mkgrq", + receiver: Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + sender: Address.newFromBech32("erd1zztjf9fhwvuvquzsllknq4qcmffwad6n0hjtn5dyzytr5tgz7uas0mkgrq"), data: new Uint8Array(Buffer.from("test")), gasPrice: BigInt(1000000000), gasLimit: BigInt(80000), @@ -485,22 +312,12 @@ describe("test network providers on devnet: Proxy and API", function () { "3fa42d97b4f85442850340a11411a3cbd63885e06ff3f84c7a75d0ef59c780f7a18aa4f331cf460300bc8bd99352aea10b7c3bc17e40287337ae9f9842470205", "hex", ), - senderUsername: "", - receiverUsername: "", - guardian: "", - guardianSignature: new Uint8Array(), - options: 0, - relayer: Address.empty(), - relayerSignature: new Uint8Array(), - }; - - const apiLegacyTxHash = await apiProvider.sendTransaction(transaction); - const apiTxNextHash = await apiProvider.sendTransaction(transactionNext); - - const proxyLegacyTxHash = await proxyProvider.sendTransaction(transaction); - const proxyTxNextHash = await proxyProvider.sendTransaction(transactionNext); - - assert.equal(apiLegacyTxHash, proxyLegacyTxHash); + }); + + const apiTxNextHash = await apiProvider.sendTransaction(transaction); + + const proxyTxNextHash = await proxyProvider.sendTransaction(transaction); + assert.equal(apiTxNextHash, proxyTxNextHash); }); }); diff --git a/src/networkProviders/proxyNetworkProvider.dev.net.spec.ts b/src/networkProviders/proxyNetworkProvider.dev.net.spec.ts new file mode 100644 index 000000000..39322cb17 --- /dev/null +++ b/src/networkProviders/proxyNetworkProvider.dev.net.spec.ts @@ -0,0 +1,391 @@ +import { assert, expect } from "chai"; +import { Account } from "../accounts"; +import { Address, SmartContractQuery, Token, Transaction, TransactionOnNetwork, TransactionStatus } from "../core"; +import { getTestWalletsPath } from "../testutils/utils"; +import { ProxyNetworkProvider } from "./proxyNetworkProvider"; + +describe("ProxyNetworkProvider Tests", function () { + const proxy = new ProxyNetworkProvider("https://devnet-gateway.multiversx.com"); + + it("should fetch network configuration", async () => { + const result = await proxy.getNetworkConfig(); + assert.equal(result.chainID, "D"); + assert.equal(result.gasPerDataByte, 1500n); + assert.equal(result.roundDuration, 6000); + assert.equal(result.minGasLimit, 50000n); + assert.equal(result.minGasPrice, 1_000_000_000n); + assert.exists(result.raw); + }); + + it("should fetch network status", async () => { + const result = await proxy.getNetworkStatus(); + assert.exists(result.blockNonce); + assert.exists(result.currentRound); + assert.exists(result.blockTimestamp); + assert.exists(result.currentEpoch); + assert.exists(result.highestFinalNonce); + assert.exists(result.raw); + }); + + it("should fetch block details by hash and nonce", async () => { + const shard = 1; + const blockHash = "ded535cc0afb2dc5f9787e9560dc48d0b83564a3f994a390b228d894d854699f"; + const resultByHash = await proxy.getBlock({ shard, blockHash }); + + const blockNonce = 5949242n; + const resultByNonce = await proxy.getBlock({ shard, blockNonce }); + + assert.equal(resultByHash.hash, blockHash); + assert.equal(resultByHash.nonce, 5949242n); + assert.equal(resultByHash.shard, 1); + assert.equal(resultByHash.timestamp, 1730112578); + assert.deepEqual(resultByHash, resultByNonce); + }); + + it("should fetch the latest block", async () => { + const result = await proxy.getLatestBlock(); + expect(result).to.exist; + }); + + it("should fetch account details", async () => { + const address1 = Address.newFromBech32("erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"); + const result1 = await proxy.getAccount(address1); + + assert.equal(result1.address.toBech32(), "erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"); + assert.isUndefined(result1.userName); + assert.isUndefined(result1.contractOwnerAddress); + + const address2 = Address.newFromBech32("erd1qqqqqqqqqqqqqpgq076flgeualrdu5jyyj60snvrh7zu4qrg05vqez5jen"); + const result2 = await proxy.getAccount(address2); + + assert.equal(result2.address.toBech32(), "erd1qqqqqqqqqqqqqpgq076flgeualrdu5jyyj60snvrh7zu4qrg05vqez5jen"); + assert.isUndefined(result2.userName); + assert.equal( + result2.contractOwnerAddress?.toBech32(), + "erd1wzx0tak22f2me4g7wpxfae2w3htfue7khrg28fy6wu8x9hzq05vqm8qhnm", + ); + assert.isFalse(result2.isContractPayable); + assert.isTrue(result2.isContractReadable); + }); + + it("should fetch account storage", async () => { + const address = Address.newFromBech32("erd1qqqqqqqqqqqqqpgq076flgeualrdu5jyyj60snvrh7zu4qrg05vqez5jen"); + const result = await proxy.getAccountStorage(address); + + assert.equal(result.entries.length, 1); + assert.equal(result.entries[0].key, "sum"); + assert.exists(result.entries[0].value); + }); + + it("should fetch a storage entry for an account", async () => { + const address = Address.newFromBech32("erd1qqqqqqqqqqqqqpgq076flgeualrdu5jyyj60snvrh7zu4qrg05vqez5jen"); + const result = await proxy.getAccountStorageEntry(address, "sum"); + + assert.equal(result.key, "sum"); + assert.exists(result.value); + }); + + it("should fetch token of an account", async () => { + const address = Address.newFromBech32("erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"); + let token = await proxy.getTokenOfAccount(address, new Token({ identifier: "TEST-ff155e" })); + + assert.equal(token.token.identifier, "TEST-ff155e"); + assert.equal(token.amount, 99999999999980000n); + + token = await proxy.getTokenOfAccount(address, new Token({ identifier: "NFTEST-ec88b8", nonce: 1n })); + + assert.equal(token.token.identifier, "NFTEST-ec88b8"); + assert.equal(token.amount, 1n); + assert.equal(token.token.nonce, 1n); + }); + + it("should fetch fungible tokens of an account", async () => { + const address = Address.newFromBech32("erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"); + const tokens = await proxy.getFungibleTokensOfAccount(address); + assert.isTrue(tokens.length > 0); + + const filtered = tokens.filter((token) => token.token.identifier === "TEST-ff155e"); + assert.equal(filtered.length, 1); + assert.equal(filtered[0].token.identifier, "TEST-ff155e"); + assert.equal(filtered[0].amount.toString(), "99999999999980000"); + }); + + it("should fetch non-fungible tokens of an account", async () => { + const address = Address.newFromBech32("erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"); + const tokens = await proxy.getNonFungibleTokensOfAccount(address); + assert.isTrue(tokens.length > 0); + + const filtered = tokens.filter((token) => token.token.identifier === "NFTEST-ec88b8-01"); + assert.equal(filtered.length, 1); + assert.equal(filtered[0].token.identifier, "NFTEST-ec88b8-01"); + assert.equal(filtered[0].token.nonce, 1n); + assert.equal(filtered[0].amount, 1n); + }); + + it("should fetch definition of fungible token", async () => { + const token = await proxy.getDefinitionOfFungibleToken("TEST-ff155e"); + + assert.equal(token.identifier, "TEST-ff155e"); + assert.equal(token.owner.toBech32(), "erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"); + assert.equal(token.decimals, 6); + }); + + it("should fetch definition of token collection", async () => { + const token = await proxy.getDefinitionOfTokenCollection("NFTEST-ec88b8"); + + assert.equal(token.collection, "NFTEST-ec88b8"); + assert.equal(token.owner.toBech32(), "erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"); + assert.equal(token.type, "NonFungibleESDT"); + assert.equal(token.decimals, 0); + }); + + it("should fetch transaction", async () => { + const transaction = await proxy.getTransaction( + "9d47c4b4669cbcaa26f5dec79902dd20e55a0aa5f4b92454a74e7dbd0183ad6c", + ); + + assert.equal(transaction.nonce, 0n); + assert.equal(transaction.epoch, 348); + assert.equal(transaction.hash, "9d47c4b4669cbcaa26f5dec79902dd20e55a0aa5f4b92454a74e7dbd0183ad6c"); + assert.isTrue(transaction.status.isCompleted()); + assert.equal(transaction.sender.toBech32(), "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); + assert.deepEqual(transaction.smartContractResults, []); + }); + + it("should fetch transaction with events", async () => { + const transaction = await proxy.getTransaction( + "6fe05e4ca01d42c96ae5182978a77fe49f26bcc14aac95ad4f19618173f86ddb", + ); + + assert.exists(transaction.logs); + assert.exists(transaction.logs.events); + assert.equal(transaction.logs.events.length, 2); + assert.equal(transaction.logs.events[0].topics.length, 8); + assert.equal(Buffer.from(transaction.logs.events[0].topics[0]).toString("hex"), "544553542d666631353565"); + assert.equal(Buffer.from(transaction.logs.events[0].topics[1]).toString("hex"), ""); + assert.equal(Buffer.from(transaction.logs.events[0].topics[2]).toString("hex"), "63616e4368616e67654f776e6572"); + assert.equal(transaction.hash, "6fe05e4ca01d42c96ae5182978a77fe49f26bcc14aac95ad4f19618173f86ddb"); + assert.isTrue(transaction.status.isCompleted()); + }); + + it("should fetch smart contract invoking transaction", async () => { + const transaction = await proxy.getTransaction( + "6fe05e4ca01d42c96ae5182978a77fe49f26bcc14aac95ad4f19618173f86ddb", + ); + + assert.isTrue(transaction.status.isCompleted()); + assert.isTrue(transaction.smartContractResults.length > 2); + assert.deepEqual( + transaction.data, + Buffer.from( + "issue@54455354546f6b656e@54455354@016345785d8a0000@06@63616e4368616e67654f776e6572@74727565@63616e55706772616465@74727565@63616e4164645370656369616c526f6c6573@74727565", + ), + ); + assert.equal(Buffer.from(transaction.logs.events[0].topics[0]).toString("hex"), "544553542d666631353565"); + assert.equal(Buffer.from(transaction.logs.events[0].topics[1]).toString("hex"), ""); + assert.equal(Buffer.from(transaction.logs.events[0].topics[2]).toString("hex"), "63616e4368616e67654f776e6572"); + assert.equal(transaction.hash, "6fe05e4ca01d42c96ae5182978a77fe49f26bcc14aac95ad4f19618173f86ddb"); + }); + + it("should send transaction", async () => { + const transaction = new Transaction({ + sender: Address.newFromBech32("erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"), + receiver: Address.newFromBech32("erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"), + gasLimit: 50000n, + chainID: "D", + value: 5000000000000000000n, + nonce: 100n, + gasPrice: 1000000000n, + version: 2, + signature: Buffer.from( + "faf50b8368cb2c20597dad671a14aa76d4c65937d6e522c64946f16ad6a250262463e444596fa7ee2af1273f6ad0329d43af48d1ae5f3b295bc8f48fdba41a05", + "hex", + ), + }); + + const expectedHash = "fc914860c1d137ed8baa602e561381f97c7bad80d150c5bf90760d3cfd3a4cea"; + assert.equal(await proxy.sendTransaction(transaction), expectedHash); + }); + + it("should send transaction with data", async () => { + const transaction = new Transaction({ + sender: Address.newFromBech32("erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"), + receiver: Address.newFromBech32("erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"), + gasLimit: 70000n, + chainID: "D", + nonce: 105n, + gasPrice: 1000000000n, + version: 2, + data: new Uint8Array(Buffer.from("foo")), + signature: Buffer.from( + "7a8bd08351bac6b1113545f5a896cb0b63806abd93d639bc4d16bfbc82c7b514f68ed7b36c743f4c3d2d1e1d3cb356824041d51dfe587a149f6fc9ab0dd9c408", + "hex", + ), + }); + + const expectedHash = "4dc7d4e18c0cf9ca7f17677ef0ac3d1363528e892996b518bee909bb17cf7929"; + assert.equal(await proxy.sendTransaction(transaction), expectedHash); + }); + + it("should send transactions", async () => { + const txs = [ + new Transaction({ + nonce: 103n, + receiver: Address.newFromBech32("erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"), + sender: Address.newFromBech32("erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"), + gasPrice: 1000000000n, + gasLimit: 50000n, + chainID: "D", + version: 2, + signature: Buffer.from( + "498d5abb9f8eb69cc75f24320e8929dadbfa855ffac220d5e92175a83be68e0437801af3a1411e3d839738230097a1c38da5c8c4df3f345defc5d40300675900", + "hex", + ), + }), + new Transaction({ + nonce: 77n, + chainID: "D", + receiver: Address.newFromBech32("erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"), + sender: Address.newFromBech32("erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"), + gasLimit: 50000n, + gasPrice: 1000000000n, + }), + new Transaction({ + nonce: 104n, + receiver: Address.newFromBech32("erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"), + sender: Address.newFromBech32("erd1487vz5m4zpxjyqw4flwa3xhnkzg4yrr3mkzf5sf0zgt94hjprc8qazcccl"), + gasPrice: 1000000000n, + gasLimit: 50000n, + chainID: "D", + version: 2, + signature: Buffer.from( + "341a2f3b738fbd20692e3bbd1cb36cb5f4ce9c0a9acc0cf4322269c0fcf34fd6bb59cd94062a9a4730e47f41b1ef3e29b69c6ab2a2a4dca9c9a7724681bc1708", + "hex", + ), + }), + ]; + + const expectedHashes = [ + "61b4f2561fc57bfb8b8971ed23cd64259b664bc0404ea7a0449def8ceef24b08", + null, + "30274b60b5635f981fa89ccfe726a34ca7121caa5d34123021c77a5c64cc9163", + ]; + const [numOfSentTxs, hashes] = await proxy.sendTransactions(txs); + assert.equal(numOfSentTxs, 2); + assert.deepEqual(hashes, expectedHashes); + }); + + it("should simulate transaction", async () => { + const bob = await Account.newFromPem(`${getTestWalletsPath()}/bob.pem`); + let transaction = new Transaction({ + sender: bob.address, + receiver: bob.address, + gasLimit: 50000n, + chainID: "D", + signature: Buffer.from(Array(128).fill("0").join(""), "hex"), + }); + const nonce = (await proxy.getAccount(bob.address)).nonce; + transaction.nonce = nonce; + let txOnNetwork = await proxy.simulateTransaction(transaction); + assert.deepEqual(txOnNetwork.status, new TransactionStatus("success")); + + transaction.signature = await bob.signTransaction(transaction); + txOnNetwork = await proxy.simulateTransaction(transaction); + + transaction = new Transaction({ + sender: bob.address, + receiver: Address.newFromBech32("erd1qqqqqqqqqqqqqpgq076flgeualrdu5jyyj60snvrh7zu4qrg05vqez5jen"), + gasLimit: 10000000n, + chainID: "D", + gasPrice: 1000000000n, + version: 2, + data: new Uint8Array(Buffer.from("add@07")), + nonce: nonce, + signature: Buffer.from(Array(128).fill("0").join(""), "hex"), + }); + + txOnNetwork = await proxy.simulateTransaction(transaction); + assert.equal(txOnNetwork.smartContractResults.length, 1); + assert.equal( + txOnNetwork.smartContractResults[0].sender.toBech32(), + "erd1qqqqqqqqqqqqqpgq076flgeualrdu5jyyj60snvrh7zu4qrg05vqez5jen", + ); + assert.equal(txOnNetwork.smartContractResults[0].receiver.toBech32(), bob.address.toBech32()); + assert.equal(txOnNetwork.smartContractResults[0].data, "@6f6b"); + + transaction.signature = await bob.signTransaction(transaction); + txOnNetwork = await proxy.simulateTransaction(transaction); + + assert.deepEqual(txOnNetwork.status, new TransactionStatus("success")); + assert.equal(txOnNetwork.smartContractResults.length, 1); + assert.equal( + txOnNetwork.smartContractResults[0].sender.toBech32(), + "erd1qqqqqqqqqqqqqpgq076flgeualrdu5jyyj60snvrh7zu4qrg05vqez5jen", + ); + assert.equal(txOnNetwork.smartContractResults[0].receiver.toBech32(), bob.address.toBech32()); + assert.equal(txOnNetwork.smartContractResults[0].data, "@6f6b", "base64"); + }); + + it("should estimate transaction cost", async function () { + const bob = await Account.newFromPem(`${getTestWalletsPath()}/bob.pem`); + const transaction = new Transaction({ + sender: bob.address, + receiver: bob.address, + gasLimit: 50000n, + chainID: "D", + data: new Uint8Array(Buffer.from("test transaction")), + }); + transaction.nonce = (await proxy.getAccount(bob.address)).nonce; + transaction.signature = await bob.signTransaction(transaction); + const response = await proxy.estimateTransactionCost(transaction); + assert.equal(response.gasLimit, 74000); + }); + + it("should send and await for completed transaction", async function () { + this.timeout(50000); + const bob = await Account.newFromPem(`${getTestWalletsPath()}/bob.pem`); + let transaction = new Transaction({ + sender: bob.address, + receiver: bob.address, + gasLimit: 50000n, + chainID: "D", + }); + const nonce = (await proxy.getAccount(bob.address)).nonce; + transaction.nonce = nonce; + transaction.signature = await bob.signTransaction(transaction); + let hash = await proxy.sendTransaction(transaction); + let transactionOnNetwork = await proxy.awaitTransactionCompleted(hash); + assert.isTrue(transactionOnNetwork.status.isCompleted()); + + transaction = new Transaction({ + sender: bob.address, + receiver: Address.newFromBech32("erd1qqqqqqqqqqqqqpgqhdqz9j3zgpl8fg2z0jzx9n605gwxx4djd8ssruw094"), + gasLimit: 5000000n, + chainID: "D", + data: new Uint8Array(Buffer.from("dummy@05")), + }); + transaction.nonce = nonce + 1n; + transaction.signature = await bob.signTransaction(transaction); + const condition = (txOnNetwork: TransactionOnNetwork) => !txOnNetwork.status.isSuccessful(); + + hash = await proxy.sendTransaction(transaction); + transactionOnNetwork = await proxy.awaitTransactionOnCondition(hash, condition); + assert.isFalse(transactionOnNetwork.status.isSuccessful()); + }); + + it("should fetch transaction status", async () => { + const txHash = "9d47c4b4669cbcaa26f5dec79902dd20e55a0aa5f4b92454a74e7dbd0183ad6c"; + const result = await proxy.getTransactionStatus(txHash); + assert.equal(result.status, "success"); + }); + + it("should query contract", async () => { + const query = new SmartContractQuery({ + contract: Address.newFromBech32("erd1qqqqqqqqqqqqqpgqqy34h7he2ya6qcagqre7ur7cc65vt0mxrc8qnudkr4"), + function: "getSum", + arguments: [], + }); + const result = await proxy.queryContract(query); + assert.equal(result.returnDataParts.length, 1); + }); +}); diff --git a/src/networkProviders/proxyNetworkProvider.ts b/src/networkProviders/proxyNetworkProvider.ts index 5b2059752..f62f632bb 100644 --- a/src/networkProviders/proxyNetworkProvider.ts +++ b/src/networkProviders/proxyNetworkProvider.ts @@ -1,20 +1,31 @@ -import { ErrContractQuery, ErrNetworkProvider } from "../errors"; -import { getAxios } from "../utils"; -import { AccountOnNetwork, GuardianData } from "./accounts"; +import { + Address, + ErrContractQuery, + ErrNetworkProvider, + getAxios, + prepareTransactionForBroadcasting, + SmartContractQuery, + SmartContractQueryResponse, + Token, + Transaction, + TransactionOnNetwork, + TransactionStatus, + TransactionWatcher, +} from "../core"; +import { ESDT_CONTRACT_ADDRESS_HEX, METACHAIN_ID } from "../core/constants"; +import { AccountAwaiter } from "./accountAwaiter"; +import { AccountOnNetwork, AccountStorage, AccountStorageEntry, GuardianData } from "./accounts"; +import { BlockOnNetwork } from "./blocks"; import { defaultAxiosConfig } from "./config"; -import { BaseUserAgent, EsdtContractAddress } from "./constants"; +import { BaseUserAgent } from "./constants"; import { ContractQueryRequest } from "./contractQueryRequest"; -import { ContractQueryResponse } from "./contractQueryResponse"; -import { IAddress, IContractQuery, INetworkProvider, IPagination, ITransaction, ITransactionNext } from "./interface"; +import { INetworkProvider } from "./interface"; import { NetworkConfig } from "./networkConfig"; -import { NetworkGeneralStatistics } from "./networkGeneralStatistics"; import { NetworkProviderConfig } from "./networkProviderConfig"; -import { NetworkStake } from "./networkStake"; import { NetworkStatus } from "./networkStatus"; +import { AwaitingOptions, TransactionCostResponse } from "./resources"; import { DefinitionOfFungibleTokenOnNetwork, DefinitionOfTokenCollectionOnNetwork } from "./tokenDefinitions"; -import { FungibleTokenOfAccountOnNetwork, NonFungibleTokenOfAccountOnNetwork } from "./tokens"; -import { TransactionOnNetwork, prepareTransactionForBroadcasting } from "./transactions"; -import { TransactionStatus } from "./transactionStatus"; +import { TokenAmountOnNetwork } from "./tokens"; import { extendUserAgentIfBackend } from "./userAgent"; // TODO: Find & remove duplicate code between "ProxyNetworkProvider" and "ApiNetworkProvider". @@ -37,136 +48,179 @@ export class ProxyNetworkProvider implements INetworkProvider { return networkConfig; } - async getNetworkStatus(): Promise { - const response = await this.doGetGeneric("network/status/4294967295"); + async getNetworkStatus(shard: number = METACHAIN_ID): Promise { + const response = await this.doGetGeneric(`network/status/${shard}`); const networkStatus = NetworkStatus.fromHttpResponse(response.status); return networkStatus; } - async getNetworkStakeStatistics(): Promise { - // TODO: Implement wrt.: - // https://github.com/multiversx/mx-api-service/blob/main/src/endpoints/stake/stake.service.ts - throw new Error("Method not implemented."); + async getBlock(args: { shard: number; blockHash?: string; blockNonce?: bigint }): Promise { + let response; + if (args.blockHash) { + response = await this.doGetGeneric(`block/${args.shard}/by-hash/${args.blockHash}`); + } else if (args.blockNonce) { + response = await this.doGetGeneric(`block/${args.shard}/by-nonce/${args.blockNonce}`); + } else throw new Error("Block hash or block nonce not provided."); + return BlockOnNetwork.fromHttpResponse(response.block); } - async getNetworkGeneralStatistics(): Promise { - // TODO: Implement wrt. (full implementation may not be possible): - // https://github.com/multiversx/mx-api-service/blob/main/src/endpoints/network/network.service.ts - throw new Error("Method not implemented."); + async getLatestBlock(shard: number = METACHAIN_ID): Promise { + const blockNonce = (await this.getNetworkStatus(shard)).blockNonce; + const response = await this.doGetGeneric(`block/${shard}/by-nonce/${blockNonce}`); + return BlockOnNetwork.fromHttpResponse(response); } - async getAccount(address: IAddress): Promise { - const response = await this.doGetGeneric(`address/${address.bech32()}`); - const account = AccountOnNetwork.fromHttpResponse(response.account); + async getAccount(address: Address): Promise { + const response = await this.doGetGeneric(`address/${address.toBech32()}`); + const account = AccountOnNetwork.fromProxyHttpResponse(response.account); return account; } - async getGuardianData(address: IAddress): Promise { - const response = await this.doGetGeneric(`address/${address.bech32()}/guardian-data`); + async getGuardianData(address: Address): Promise { + const response = await this.doGetGeneric(`address/${address.toBech32()}/guardian-data`); const accountGuardian = GuardianData.fromHttpResponse(response.guardianData); return accountGuardian; } - async getFungibleTokensOfAccount( - address: IAddress, - _pagination?: IPagination, - ): Promise { - const url = `address/${address.bech32()}/esdt`; - const response = await this.doGetGeneric(url); - const responseItems: any[] = Object.values(response.esdts); - // Skip NFTs / SFTs. - const responseItemsFiltered = responseItems.filter((item) => !item.nonce); - const tokens = responseItemsFiltered.map((item) => FungibleTokenOfAccountOnNetwork.fromHttpResponse(item)); - - // TODO: Fix sorting - tokens.sort((a, b) => a.identifier.localeCompare(b.identifier)); - return tokens; - } - - async getNonFungibleTokensOfAccount( - address: IAddress, - _pagination?: IPagination, - ): Promise { - const url = `address/${address.bech32()}/esdt`; - const response = await this.doGetGeneric(url); - const responseItems: any[] = Object.values(response.esdts); - // Skip fungible tokens. - const responseItemsFiltered = responseItems.filter((item) => item.nonce >= 0); - const tokens = responseItemsFiltered.map((item) => - NonFungibleTokenOfAccountOnNetwork.fromProxyHttpResponse(item), - ); - - // TODO: Fix sorting - tokens.sort((a, b) => a.identifier.localeCompare(b.identifier)); - return tokens; + async getAccountStorage(address: Address): Promise { + const response = await this.doGetGeneric(`address/${address.toBech32()}/keys`); + const account = AccountStorage.fromHttpResponse(response); + return account; } - async getFungibleTokenOfAccount( - address: IAddress, - tokenIdentifier: string, - ): Promise { - const response = await this.doGetGeneric(`address/${address.bech32()}/esdt/${tokenIdentifier}`); - const tokenData = FungibleTokenOfAccountOnNetwork.fromHttpResponse(response.tokenData); - return tokenData; + async getAccountStorageEntry(address: Address, entryKey: string): Promise { + const keyAsHex = Buffer.from(entryKey).toString("hex"); + const response = await this.doGetGeneric(`address/${address.toBech32()}/key/${keyAsHex}`); + const account = AccountStorageEntry.fromHttpResponse(response, entryKey); + return account; } - async getNonFungibleTokenOfAccount( - address: IAddress, - collection: string, - nonce: number, - ): Promise { - const response = await this.doGetGeneric( - `address/${address.bech32()}/nft/${collection}/nonce/${nonce.valueOf()}`, - ); - const tokenData = NonFungibleTokenOfAccountOnNetwork.fromProxyHttpResponseByNonce(response.tokenData); - return tokenData; + async awaitAccountOnCondition( + address: Address, + condition: (account: AccountOnNetwork) => boolean, + options?: AwaitingOptions, + ): Promise { + if (!options) { + options = new AwaitingOptions(); + } + const awaiter = new AccountAwaiter({ + fetcher: this, + patienceTimeInMilliseconds: options.patienceInMilliseconds, + pollingIntervalInMilliseconds: options.pollingIntervalInMilliseconds, + timeoutIntervalInMilliseconds: options.timeoutInMilliseconds, + }); + return await awaiter.awaitOnCondition(address, condition); } - async getTransaction(txHash: string, _?: boolean): Promise { - const url = this.buildUrlWithQueryParameters(`transaction/${txHash}`, { withResults: "true" }); - const [data, status] = await Promise.all([this.doGetGeneric(url), this.getTransactionStatus(txHash)]); - return TransactionOnNetwork.fromProxyHttpResponse(txHash, data.transaction, status); + async sendTransaction(tx: Transaction): Promise { + const transaction = prepareTransactionForBroadcasting(tx); + const response = await this.doPostGeneric("transaction/send", transaction); + return response.txHash; } - async getTransactionStatus(txHash: string): Promise { - const response = await this.doGetGeneric(`transaction/${txHash}/process-status`); - const status = new TransactionStatus(response.status); - return status; + async simulateTransaction(tx: Transaction, checkSignature: boolean = false): Promise { + const transaction = prepareTransactionForBroadcasting(tx); + let url = "transaction/simulate?checkSignature=false"; + if (checkSignature) { + url = "transaction/simulate"; + } + const response = await this.doPostGeneric(url, transaction); + return TransactionOnNetwork.fromSimulateResponse(transaction, response["result"] ?? {}); } - async sendTransaction(tx: ITransaction | ITransactionNext): Promise { + async estimateTransactionCost(tx: Transaction): Promise { const transaction = prepareTransactionForBroadcasting(tx); - const response = await this.doPostGeneric("transaction/send", transaction); - return response.txHash; + const response = await this.doPostGeneric("transaction/cost", transaction); + return TransactionCostResponse.fromHttpResponse(response); } - async sendTransactions(txs: (ITransaction | ITransactionNext)[]): Promise { + async sendTransactions(txs: Transaction[]): Promise<[number, string[]]> { const data = txs.map((tx) => prepareTransactionForBroadcasting(tx)); const response = await this.doPostGeneric("transaction/send-multiple", data); + const numSent = Number(response["numOfSentTxs"] ?? 0); const hashes = Array(txs.length).fill(null); for (let i = 0; i < txs.length; i++) { hashes[i] = response.txsHashes[i.toString()] || null; } + return [numSent, hashes]; + } - return hashes; + async getTransaction(txHash: string): Promise { + const url = this.buildUrlWithQueryParameters(`transaction/${txHash}`, { withResults: "true" }); + const [data, status] = await Promise.all([this.doGetGeneric(url), this.getTransactionStatus(txHash)]); + return TransactionOnNetwork.fromProxyHttpResponse(txHash, data.transaction, status); } - async simulateTransaction(tx: ITransaction | ITransactionNext): Promise { - const transaction = prepareTransactionForBroadcasting(tx); - const response = await this.doPostGeneric("transaction/simulate", transaction); - return response; + async awaitTransactionOnCondition( + transactionHash: string, + condition: (account: TransactionOnNetwork) => boolean, + options?: AwaitingOptions, + ): Promise { + if (!options) { + options = new AwaitingOptions(); + } + + const awaiter = new TransactionWatcher(this, { + patienceMilliseconds: options.patienceInMilliseconds, + pollingIntervalMilliseconds: options.pollingIntervalInMilliseconds, + timeoutMilliseconds: options.timeoutInMilliseconds, + }); + return await awaiter.awaitOnCondition(transactionHash, condition); } - async queryContract(query: IContractQuery): Promise { - try { - const request = new ContractQueryRequest(query).toHttpRequest(); - const response = await this.doPostGeneric("vm-values/query", request); - return ContractQueryResponse.fromHttpResponse(response.data); - } catch (error: any) { - throw new ErrContractQuery(error); + async awaitTransactionCompleted(transactionHash: string, options?: AwaitingOptions): Promise { + if (!options) { + options = new AwaitingOptions(); } + + const awaiter = new TransactionWatcher(this, { + patienceMilliseconds: options.patienceInMilliseconds, + pollingIntervalMilliseconds: options.pollingIntervalInMilliseconds, + timeoutMilliseconds: options.timeoutInMilliseconds, + }); + return await awaiter.awaitCompleted(transactionHash); + } + + async getTokenOfAccount(address: Address, token: Token): Promise { + let response; + if (token.nonce === 0n) { + response = await this.doGetGeneric(`address/${address.toBech32()}/esdt/${token.identifier}`); + } else { + response = await this.doGetGeneric( + `address/${address.toBech32()}/nft/${token.identifier}/nonce/${token.nonce}`, + ); + } + return TokenAmountOnNetwork.fromProxyResponse(response["tokenData"]); + } + + async getFungibleTokensOfAccount(address: Address): Promise { + const url = `address/${address.toBech32()}/esdt`; + const response = await this.doGetGeneric(url); + const responseItems: any[] = Object.values(response.esdts); + // Skip NFTs / SFTs. + const responseItemsFiltered = responseItems.filter((item) => !item.nonce); + const tokens = responseItemsFiltered.map((item) => TokenAmountOnNetwork.fromProxyResponse(item)); + + return tokens; + } + + async getNonFungibleTokensOfAccount(address: Address): Promise { + const url = `address/${address.toBech32()}/esdt`; + const response = await this.doGetGeneric(url); + const responseItems: any[] = Object.values(response.esdts); + // Skip fungible tokens. + const responseItemsFiltered = responseItems.filter((item) => item.nonce >= 0); + const tokens = responseItemsFiltered.map((item) => TokenAmountOnNetwork.fromProxyResponse(item)); + + return tokens; + } + + async getTransactionStatus(txHash: string): Promise { + const response = await this.doGetGeneric(`transaction/${txHash}/process-status`); + const status = new TransactionStatus(response.status); + return status; } async getDefinitionOfFungibleToken(tokenIdentifier: string): Promise { @@ -178,17 +232,27 @@ export class ProxyNetworkProvider implements INetworkProvider { return definition; } + async queryContract(query: SmartContractQuery): Promise { + try { + const request = new ContractQueryRequest(query).toHttpRequest(); + const response = await this.doPostGeneric("vm-values/query", request); + return SmartContractQueryResponse.fromHttpResponse(response.data, query.function); + } catch (error: any) { + throw new ErrContractQuery(error); + } + } + private async getTokenProperties(identifier: string): Promise { - const encodedIdentifier = Buffer.from(identifier).toString("hex"); + const encodedIdentifier = Buffer.from(identifier); const queryResponse = await this.queryContract({ - address: EsdtContractAddress, - func: "getTokenProperties", - getEncodedArguments: () => [encodedIdentifier], + contract: Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX), + function: "getTokenProperties", + arguments: [new Uint8Array(encodedIdentifier)], }); - const properties = queryResponse.getReturnDataParts(); - return properties; + const properties = queryResponse.returnDataParts; + return properties?.map((prop) => Buffer.from(prop)); } async getDefinitionOfTokenCollection(collection: string): Promise { @@ -200,10 +264,6 @@ export class ProxyNetworkProvider implements INetworkProvider { return definition; } - async getNonFungibleToken(_collection: string, _nonce: number): Promise { - throw new Error("Method not implemented."); - } - async doGetGeneric(resourceUrl: string): Promise { const response = await this.doGet(resourceUrl); return response; diff --git a/src/networkProviders/resources.ts b/src/networkProviders/resources.ts new file mode 100644 index 000000000..a4f9f1484 --- /dev/null +++ b/src/networkProviders/resources.ts @@ -0,0 +1,27 @@ +import { TransactionStatus } from "../core/transactionStatus"; +import { + DEFAULT_ACCOUNT_AWAITING_PATIENCE_IN_MILLISECONDS, + DEFAULT_ACCOUNT_AWAITING_POLLING_TIMEOUT_IN_MILLISECONDS, + DEFAULT_ACCOUNT_AWAITING_TIMEOUT_IN_MILLISECONDS, +} from "./constants"; + +export class AwaitingOptions { + pollingIntervalInMilliseconds: number = DEFAULT_ACCOUNT_AWAITING_POLLING_TIMEOUT_IN_MILLISECONDS; + timeoutInMilliseconds: number = DEFAULT_ACCOUNT_AWAITING_TIMEOUT_IN_MILLISECONDS; + patienceInMilliseconds: number = DEFAULT_ACCOUNT_AWAITING_PATIENCE_IN_MILLISECONDS; +} + +export class TransactionCostResponse { + raw: Record = {}; + gasLimit: number = 0; + status: TransactionStatus = TransactionStatus.createUnknown(); + + static fromHttpResponse(payload: any): TransactionCostResponse { + const result = new TransactionCostResponse(); + result.raw = payload; + result.gasLimit = payload["txGasUnits"] ?? 0; + result.status = new TransactionStatus(""); + + return result; + } +} diff --git a/src/networkProviders/serialization.spec.ts b/src/networkProviders/serialization.spec.ts index 6ffc83096..217a0e071 100644 --- a/src/networkProviders/serialization.spec.ts +++ b/src/networkProviders/serialization.spec.ts @@ -8,9 +8,8 @@ describe("test JSON serialization", function () { }); it("should deserialize", async function () { - const JSONbig = require("json-bigint")({ constructorAction: 'ignore' }); + const JSONbig = require("json-bigint")({ constructorAction: "ignore" }); const data = `{"Costum":{"foo_constructor":1}}`; JSONbig.parse(data); }); }); - diff --git a/src/networkProviders/tokenDefinitions.ts b/src/networkProviders/tokenDefinitions.ts index fbaccf2ac..e5ff77442 100644 --- a/src/networkProviders/tokenDefinitions.ts +++ b/src/networkProviders/tokenDefinitions.ts @@ -1,12 +1,11 @@ import BigNumber from "bignumber.js"; -import { Address } from "../address"; -import { IAddress } from "./interface"; +import { Address } from "../core/address"; export class DefinitionOfFungibleTokenOnNetwork { identifier: string = ""; name: string = ""; ticker: string = ""; - owner: IAddress = Address.empty(); + owner: Address = Address.empty(); decimals: number = 0; supply: BigNumber = new BigNumber(0); isPaused: boolean = false; @@ -21,7 +20,7 @@ export class DefinitionOfFungibleTokenOnNetwork { assets: Record = {}; static fromApiHttpResponse(payload: any): DefinitionOfFungibleTokenOnNetwork { - let result = new DefinitionOfFungibleTokenOnNetwork(); + const result = new DefinitionOfFungibleTokenOnNetwork(); result.identifier = payload.identifier || ""; result.name = payload.name || ""; @@ -76,7 +75,7 @@ export class DefinitionOfTokenCollectionOnNetwork { type: string = ""; name: string = ""; ticker: string = ""; - owner: IAddress = Address.empty(); + owner: Address = Address.empty(); decimals: number = 0; canPause: boolean = false; canFreeze: boolean = false; @@ -88,7 +87,7 @@ export class DefinitionOfTokenCollectionOnNetwork { canCreateMultiShard: boolean = false; static fromApiHttpResponse(payload: any): DefinitionOfTokenCollectionOnNetwork { - let result = new DefinitionOfTokenCollectionOnNetwork(); + const result = new DefinitionOfTokenCollectionOnNetwork(); result.collection = payload.collection || ""; result.type = payload.type || ""; diff --git a/src/networkProviders/tokens.ts b/src/networkProviders/tokens.ts index 1c838f070..92a0aafb2 100644 --- a/src/networkProviders/tokens.ts +++ b/src/networkProviders/tokens.ts @@ -1,7 +1,7 @@ import { BigNumber } from "bignumber.js"; -import { Address } from "../address"; -import { numberToPaddedHex } from "../utils.codec"; -import { IAddress } from "./interface"; +import { Address, Token } from "../core"; +import { numberToPaddedHex } from "../core/utils.codec"; +import { BlockCoordinates } from "./blocks"; export class FungibleTokenOfAccountOnNetwork { identifier: string = ""; @@ -27,7 +27,7 @@ export class NonFungibleTokenOfAccountOnNetwork { nonce: number = 0; type: string = ""; name: string = ""; - creator: IAddress = Address.empty(); + creator: Address = Address.empty(); supply: BigNumber = new BigNumber(0); decimals: number = 0; royalties: BigNumber = new BigNumber(0); @@ -39,7 +39,7 @@ export class NonFungibleTokenOfAccountOnNetwork { } static fromProxyHttpResponse(payload: any): NonFungibleTokenOfAccountOnNetwork { - let result = NonFungibleTokenOfAccountOnNetwork.fromHttpResponse(payload); + const result = NonFungibleTokenOfAccountOnNetwork.fromHttpResponse(payload); result.identifier = payload.tokenIdentifier || ""; result.collection = NonFungibleTokenOfAccountOnNetwork.parseCollectionFromIdentifier(result.identifier); @@ -49,7 +49,7 @@ export class NonFungibleTokenOfAccountOnNetwork { } static fromProxyHttpResponseByNonce(payload: any): NonFungibleTokenOfAccountOnNetwork { - let result = NonFungibleTokenOfAccountOnNetwork.fromHttpResponse(payload); + const result = NonFungibleTokenOfAccountOnNetwork.fromHttpResponse(payload); let nonceAsHex = numberToPaddedHex(result.nonce); result.identifier = `${payload.tokenIdentifier}-${nonceAsHex}`; @@ -60,7 +60,7 @@ export class NonFungibleTokenOfAccountOnNetwork { } static fromApiHttpResponse(payload: any): NonFungibleTokenOfAccountOnNetwork { - let result = NonFungibleTokenOfAccountOnNetwork.fromHttpResponse(payload); + const result = NonFungibleTokenOfAccountOnNetwork.fromHttpResponse(payload); result.identifier = payload.identifier || ""; result.collection = payload.collection || ""; @@ -93,3 +93,34 @@ export class NonFungibleTokenOfAccountOnNetwork { return collection; } } + +export class TokenAmountOnNetwork { + raw: Record = {}; + token: Token = new Token({ identifier: "" }); + amount: bigint = 0n; + block_coordinates?: BlockCoordinates; + + constructor(init?: Partial) { + Object.assign(this, init); + } + + static fromProxyResponse(payload: any): TokenAmountOnNetwork { + const result = new TokenAmountOnNetwork(); + + result.raw = payload; + result.amount = BigInt(payload["balance"] ?? 0); + result.token = new Token({ identifier: payload["tokenIdentifier"] ?? "", nonce: payload["nonce"] ?? 0 }); + + return result; + } + + static fromApiResponse(payload: any): TokenAmountOnNetwork { + const result = new TokenAmountOnNetwork(); + + result.raw = payload; + result.amount = BigInt(payload["balance"] ?? 0); + result.token = new Token({ identifier: payload["identifier"] ?? "", nonce: payload["nonce"] ?? 0 }); + + return result; + } +} diff --git a/src/networkProviders/transactionEvents.ts b/src/networkProviders/transactionEvents.ts deleted file mode 100644 index ab11c5892..000000000 --- a/src/networkProviders/transactionEvents.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Address } from "../address"; -import { IAddress } from "./interface"; - -export class TransactionEvent { - address: IAddress = Address.empty(); - identifier: string = ""; - topics: TransactionEventTopic[] = []; - - /** - * @deprecated Use "dataPayload" instead. - */ - data: string = ""; - dataPayload: TransactionEventData = new TransactionEventData(Buffer.from("", "utf8")); - additionalData: TransactionEventData[] = []; - - constructor(init?: Partial) { - Object.assign(this, init); - } - - static fromHttpResponse(responsePart: { - address: string; - identifier: string; - topics: string[]; - data: string; - additionalData?: string[]; - }): TransactionEvent { - let result = new TransactionEvent(); - result.address = new Address(responsePart.address); - result.identifier = responsePart.identifier || ""; - result.topics = (responsePart.topics || []).map((topic) => new TransactionEventTopic(topic)); - - result.dataPayload = TransactionEventData.fromBase64(responsePart.data); - result.additionalData = (responsePart.additionalData || []).map(TransactionEventData.fromBase64); - result.data = result.dataPayload.toString(); - - return result; - } - - findFirstOrNoneTopic(predicate: (topic: TransactionEventTopic) => boolean): TransactionEventTopic | undefined { - return this.topics.filter((topic) => predicate(topic))[0]; - } - - getLastTopic(): TransactionEventTopic { - return this.topics[this.topics.length - 1]; - } -} - -export class TransactionEventData { - private readonly raw: Buffer; - - constructor(data: Buffer) { - this.raw = data; - } - - static fromBase64(str: string): TransactionEventData { - return new TransactionEventData(Buffer.from(str || "", "base64")); - } - - toString(): string { - return this.raw.toString("utf8"); - } - - hex(): string { - return this.raw.toString("hex"); - } - - valueOf(): Buffer { - return this.raw; - } -} - -export class TransactionEventTopic { - private readonly raw: Buffer; - - constructor(topic: string) { - this.raw = Buffer.from(topic || "", "base64"); - } - - toString(): string { - return this.raw.toString("utf8"); - } - - hex(): string { - return this.raw.toString("hex"); - } - - valueOf(): Buffer { - return this.raw; - } -} diff --git a/src/networkProviders/transactionReceipt.ts b/src/networkProviders/transactionReceipt.ts deleted file mode 100644 index 49d14d62e..000000000 --- a/src/networkProviders/transactionReceipt.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Address } from "../address"; -import { IAddress } from "./interface"; - -export class TransactionReceipt { - value: string = ""; - sender: IAddress = Address.empty(); - data: string = ""; - hash: string = ""; - - static fromHttpResponse(response: { - value: string; - sender: string; - data: string; - txHash: string; - }): TransactionReceipt { - let receipt = new TransactionReceipt(); - - receipt.value = (response.value || 0).toString(); - receipt.sender = new Address(response.sender); - receipt.data = response.data; - receipt.hash = response.txHash; - - return receipt; - } -} diff --git a/src/networkProviders/transactions.ts b/src/networkProviders/transactions.ts deleted file mode 100644 index a31faa1af..000000000 --- a/src/networkProviders/transactions.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { Address } from "../address"; -import { ContractResults } from "./contractResults"; -import { IAddress, ITransaction, ITransactionNext } from "./interface"; -import { TransactionLogs } from "./transactionLogs"; -import { TransactionReceipt } from "./transactionReceipt"; -import { TransactionStatus } from "./transactionStatus"; - -export function prepareTransactionForBroadcasting(transaction: ITransaction | ITransactionNext): any { - if ("toSendable" in transaction) { - return transaction.toSendable(); - } - - return { - nonce: Number(transaction.nonce), - value: transaction.value.toString(), - receiver: transaction.receiver, - sender: transaction.sender, - senderUsername: transaction.senderUsername - ? Buffer.from(transaction.senderUsername).toString("base64") - : undefined, - receiverUsername: transaction.receiverUsername - ? Buffer.from(transaction.receiverUsername).toString("base64") - : undefined, - gasPrice: Number(transaction.gasPrice), - gasLimit: Number(transaction.gasLimit), - data: transaction.data.length === 0 ? undefined : Buffer.from(transaction.data).toString("base64"), - chainID: transaction.chainID, - version: transaction.version, - options: transaction.options, - guardian: transaction.guardian || undefined, - relayer: transaction.relayer.toBech32() || undefined, - relayerSignature: - transaction.relayerSignature.length === 0 - ? undefined - : Buffer.from(transaction.relayerSignature).toString("hex"), - signature: Buffer.from(transaction.signature).toString("hex"), - guardianSignature: - transaction.guardianSignature.length === 0 - ? undefined - : Buffer.from(transaction.guardianSignature).toString("hex"), - }; -} - -export class TransactionOnNetwork { - isCompleted?: boolean; - hash: string = ""; - type: string = ""; - nonce: number = 0; - round: number = 0; - epoch: number = 0; - value: string = ""; - receiver: IAddress = Address.empty(); - sender: IAddress = Address.empty(); - gasLimit: number = 0; - gasPrice: number = 0; - function: string = ""; - data: Buffer = Buffer.from([]); - signature: string = ""; - status: TransactionStatus = TransactionStatus.createUnknown(); - timestamp: number = 0; - - blockNonce: number = 0; - hyperblockNonce: number = 0; - hyperblockHash: string = ""; - - receipt: TransactionReceipt = new TransactionReceipt(); - contractResults: ContractResults = new ContractResults([]); - logs: TransactionLogs = new TransactionLogs(); - - constructor(init?: Partial) { - Object.assign(this, init); - } - - static fromProxyHttpResponse( - txHash: string, - response: any, - processStatus?: TransactionStatus | undefined, - ): TransactionOnNetwork { - let result = TransactionOnNetwork.fromHttpResponse(txHash, response); - result.contractResults = ContractResults.fromProxyHttpResponse(response.smartContractResults || []); - - if (processStatus) { - result.status = processStatus; - result.isCompleted = result.status.isSuccessful() || result.status.isFailed(); - } - - return result; - } - - static fromApiHttpResponse(txHash: string, response: any): TransactionOnNetwork { - let result = TransactionOnNetwork.fromHttpResponse(txHash, response); - result.contractResults = ContractResults.fromApiHttpResponse(response.results || []); - result.isCompleted = !result.status.isPending(); - return result; - } - - private static fromHttpResponse(txHash: string, response: any): TransactionOnNetwork { - let result = new TransactionOnNetwork(); - - result.hash = txHash; - result.type = response.type || ""; - result.nonce = response.nonce || 0; - result.round = response.round; - result.epoch = response.epoch || 0; - result.value = (response.value || 0).toString(); - result.sender = new Address(response.sender); - result.receiver = new Address(response.receiver); - result.gasPrice = response.gasPrice || 0; - result.gasLimit = response.gasLimit || 0; - result.function = response.function || ""; - result.data = Buffer.from(response.data || "", "base64"); - result.status = new TransactionStatus(response.status); - result.timestamp = response.timestamp || 0; - - result.blockNonce = response.blockNonce || 0; - result.hyperblockNonce = response.hyperblockNonce || 0; - result.hyperblockHash = response.hyperblockHash || ""; - - result.receipt = TransactionReceipt.fromHttpResponse(response.receipt || {}); - result.logs = TransactionLogs.fromHttpResponse(response.logs || {}); - - return result; - } - - getDateTime(): Date { - return new Date(this.timestamp * 1000); - } -} diff --git a/src/proto/serializer.spec.ts b/src/proto/serializer.spec.ts index 97a88b5ca..5f28d2c58 100644 --- a/src/proto/serializer.spec.ts +++ b/src/proto/serializer.spec.ts @@ -1,33 +1,31 @@ import { assert } from "chai"; -import { Address } from "../address"; -import { TransactionVersion } from "../networkParams"; -import { Signature } from "../signature"; -import { loadTestWallets, TestWallet } from "../testutils"; -import { TokenTransfer } from "../tokens"; -import { Transaction } from "../transaction"; -import { TransactionPayload } from "../transactionPayload"; +import { Account } from "../accounts"; +import { Address, TokenTransfer, Transaction } from "../core"; +import { getTestWalletsPath } from "../testutils"; import { ProtoSerializer } from "./serializer"; describe("serialize transactions", () => { - let wallets: Record; let serializer = new ProtoSerializer(); - + let alice: Account; + let bob: Account; + let carol: Account; before(async function () { - wallets = await loadTestWallets(); + alice = await Account.newFromPem(`${getTestWalletsPath()}/alice.pem`); + bob = await Account.newFromPem(`${getTestWalletsPath()}/bob.pem`); + carol = await Account.newFromPem(`${getTestWalletsPath()}/carol.pem`); }); it("with no data, no value", async () => { let transaction = new Transaction({ - nonce: 89, - value: 0, - sender: wallets.alice.address, - receiver: wallets.bob.address, - gasLimit: 50000, + nonce: 89n, + value: 0n, + sender: alice.address, + receiver: bob.address, + gasLimit: 50000n, chainID: "local-testnet", }); - const signer = wallets.alice.signer; - transaction.applySignature(await signer.sign(transaction.serializeForSigning())); + transaction.signature = await alice.signTransaction(transaction); let buffer = serializer.serializeTransaction(transaction); assert.equal( @@ -38,17 +36,16 @@ describe("serialize transactions", () => { it("with data, no value", async () => { let transaction = new Transaction({ - nonce: 90, - value: 0, - sender: wallets.alice.address, - receiver: wallets.bob.address, - gasLimit: 80000, - data: new TransactionPayload("hello"), + nonce: 90n, + value: 0n, + sender: alice.address, + receiver: bob.address, + gasLimit: 80000n, + data: Buffer.from("hello"), chainID: "local-testnet", }); - const signer = wallets.alice.signer; - transaction.applySignature(await signer.sign(transaction.serializeForSigning())); + transaction.signature = await alice.signTransaction(transaction); let buffer = serializer.serializeTransaction(transaction); assert.equal( @@ -59,17 +56,16 @@ describe("serialize transactions", () => { it("with data, with value", async () => { let transaction = new Transaction({ - nonce: 91, - value: TokenTransfer.egldFromAmount(10), - sender: wallets.alice.address, - receiver: wallets.bob.address, - gasLimit: 100000, - data: new TransactionPayload("for the book"), + nonce: 91n, + value: TokenTransfer.newFromNativeAmount(10000000000000000000n).amount, + sender: alice.address, + receiver: bob.address, + gasLimit: 100000n, + data: Buffer.from("for the book"), chainID: "local-testnet", }); - const signer = wallets.alice.signer; - transaction.applySignature(await signer.sign(transaction.serializeForSigning())); + transaction.signature = await alice.signTransaction(transaction); let buffer = serializer.serializeTransaction(transaction); assert.equal( @@ -80,17 +76,16 @@ describe("serialize transactions", () => { it("with data, with large value", async () => { let transaction = new Transaction({ - nonce: 92, - value: "123456789000000000000000000000", - sender: wallets.alice.address, - receiver: wallets.bob.address, - gasLimit: 100000, - data: new TransactionPayload("for the spaceship"), + nonce: 92n, + value: 123456789000000000000000000000n, + sender: alice.address, + receiver: bob.address, + gasLimit: 100000n, + data: Buffer.from("for the spaceship"), chainID: "local-testnet", }); - const signer = wallets.alice.signer; - transaction.applySignature(await signer.sign(transaction.serializeForSigning())); + transaction.signature = await alice.signTransaction(transaction); let buffer = serializer.serializeTransaction(transaction); assert.equal( @@ -101,21 +96,17 @@ describe("serialize transactions", () => { it("with nonce = 0", async () => { let transaction = new Transaction({ - nonce: 0, - value: "0", - sender: wallets.alice.address, - receiver: wallets.bob.address, - gasLimit: 80000, - data: new TransactionPayload("hello"), + nonce: 0n, + value: 0n, + sender: alice.address, + receiver: bob.address, + gasLimit: 80000n, + data: Buffer.from("hello"), chainID: "local-testnet", - version: new TransactionVersion(1), + version: 1, }); - transaction.applySignature( - new Signature( - "dfa3e9f2fdec60dcb353bac3b3435b4a2ff251e7e98eaf8620f46c731fc70c8ba5615fd4e208b05e75fe0f7dc44b7a99567e29f94fcd91efac7e67b182cd2a04", - ), - ); + transaction.signature = await alice.signTransaction(transaction); let buffer = serializer.serializeTransaction(transaction); assert.equal( @@ -126,18 +117,17 @@ describe("serialize transactions", () => { it("with usernames", async () => { const transaction = new Transaction({ - nonce: 204, - value: "1000000000000000000", - sender: Address.fromBech32("erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8"), - receiver: Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + nonce: 204n, + value: 1000000000000000000n, + sender: Address.newFromBech32("erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8"), + receiver: Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), senderUsername: "carol", receiverUsername: "alice", - gasLimit: 50000, + gasLimit: 50000n, chainID: "T", }); - const signer = wallets.carol.signer; - transaction.applySignature(await signer.sign(transaction.serializeForSigning())); + transaction.signature = await carol.signTransaction(transaction); const buffer = serializer.serializeTransaction(transaction); assert.equal( diff --git a/src/proto/serializer.ts b/src/proto/serializer.ts index f1693c848..ed1bd5f89 100644 --- a/src/proto/serializer.ts +++ b/src/proto/serializer.ts @@ -1,10 +1,9 @@ import BigNumber from "bignumber.js"; -import { Address } from "../address"; -import { TRANSACTION_OPTIONS_DEFAULT, TRANSACTION_OPTIONS_TX_GUARDED } from "../constants"; -import * as errors from "../errors"; -import { ITransaction, ITransactionValue } from "../interface"; -import { bigIntToBuffer } from "../smartcontracts/codec/utils"; -import { Transaction } from "../transaction"; +import { bigIntToBuffer } from "../abi/codec/utils"; +import { Address } from "../core/address"; +import { TRANSACTION_OPTIONS_DEFAULT, TRANSACTION_OPTIONS_TX_GUARDED } from "../core/constants"; +import * as errors from "../core/errors"; +import { Transaction } from "../core/transaction"; /** * Hides away the serialization complexity, for each type of object (e.g. transactions). @@ -25,11 +24,11 @@ export class ProtoSerializer { return buffer; } - private convertToProtoMessage(transaction: ITransaction) { + private convertToProtoMessage(transaction: Transaction) { const proto = require("./compiled").proto; - const receiverPubkey = new Address(transaction.receiver).getPublicKey(); - const senderPubkey = new Address(transaction.sender).getPublicKey(); + const receiverPubkey = transaction.receiver.getPublicKey(); + const senderPubkey = transaction.sender.getPublicKey(); let protoTransaction = new proto.Transaction({ // mx-chain-go's serializer handles nonce == 0 differently, thus we treat 0 as "undefined". @@ -68,14 +67,14 @@ export class ProtoSerializer { return protoTransaction; } - private isRelayedTransaction(transaction: ITransaction) { + private isRelayedTransaction(transaction: Transaction) { return !transaction.relayer.isEmpty(); } /** * Custom serialization, compatible with mx-chain-go. */ - private serializeTransactionValue(transactionValue: ITransactionValue): Buffer { + private serializeTransactionValue(transactionValue: bigint): Buffer { let value = new BigNumber(transactionValue.toString()); if (value.isZero()) { return Buffer.from([0, 0]); @@ -88,13 +87,13 @@ export class ProtoSerializer { return buffer; } - private isGuardedTransaction(transaction: ITransaction): boolean { - const hasGuardian = transaction.guardian.length > 0; + private isGuardedTransaction(transaction: Transaction): boolean { + const hasGuardian = !transaction.guardian.isEmpty(); const hasGuardianSignature = transaction.guardianSignature.length > 0; return this.isWithGuardian(transaction) && hasGuardian && hasGuardianSignature; } - private isWithGuardian(transaction: ITransaction): boolean { + private isWithGuardian(transaction: Transaction): boolean { return (transaction.options & TRANSACTION_OPTIONS_TX_GUARDED) == TRANSACTION_OPTIONS_TX_GUARDED; } diff --git a/src/relayedTransactionV1Builder.spec.ts b/src/relayedTransactionV1Builder.spec.ts deleted file mode 100644 index 018ba6b04..000000000 --- a/src/relayedTransactionV1Builder.spec.ts +++ /dev/null @@ -1,260 +0,0 @@ -import { assert } from "chai"; -import { Address } from "./address"; -import * as errors from "./errors"; -import { TransactionOptions, TransactionVersion } from "./networkParams"; -import { RelayedTransactionV1Builder } from "./relayedTransactionV1Builder"; -import { TestWallet, loadTestWallets } from "./testutils"; -import { TokenTransfer } from "./tokens"; -import { Transaction } from "./transaction"; -import { TransactionPayload } from "./transactionPayload"; - -describe("test relayed v1 transaction builder", function () { - let alice: TestWallet, bob: TestWallet, carol: TestWallet, grace: TestWallet, frank: TestWallet; - - before(async function () { - ({ alice, bob, carol, grace, frank } = await loadTestWallets()); - }); - - it("should throw exception if args were not set", async function () { - const builder = new RelayedTransactionV1Builder(); - assert.throw(() => builder.build(), errors.ErrInvalidRelayedV1BuilderArguments); - - const innerTx = new Transaction({ - nonce: 15, - sender: alice.address, - receiver: Address.fromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u"), - gasLimit: 10000000, - chainID: "1", - data: new TransactionPayload("getContractConfig"), - }); - builder.setInnerTransaction(innerTx); - assert.throw(() => builder.build(), errors.ErrInvalidRelayedV1BuilderArguments); - - const networkConfig = { - MinGasLimit: 50_000, - GasPerDataByte: 1_500, - GasPriceModifier: 0.01, - ChainID: "T", - }; - builder.setNetworkConfig(networkConfig); - assert.throw(() => builder.build(), errors.ErrInvalidRelayedV1BuilderArguments); - - builder.setRelayerAddress(alice.getAddress()); - assert.doesNotThrow(() => builder.build()); - }); - - it("should compute relayed v1 transaction", async function () { - const networkConfig = { - MinGasLimit: 50_000, - GasPerDataByte: 1_500, - GasPriceModifier: 0.01, - ChainID: "T", - }; - - const innerTx = new Transaction({ - nonce: 198, - sender: bob.address, - receiver: Address.fromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u"), - gasLimit: 60000000, - chainID: networkConfig.ChainID, - data: new TransactionPayload("getContractConfig"), - }); - - innerTx.applySignature(await bob.signer.sign(innerTx.serializeForSigning())); - - const builder = new RelayedTransactionV1Builder(); - const relayedTxV1 = builder - .setInnerTransaction(innerTx) - .setRelayerNonce(2627) - .setNetworkConfig(networkConfig) - .setRelayerAddress(alice.address) - .build(); - - relayedTxV1.applySignature(await alice.signer.sign(relayedTxV1.serializeForSigning())); - - assert.equal(relayedTxV1.getNonce().valueOf(), 2627); - assert.equal( - relayedTxV1.getData().toString(), - "relayedTx@7b226e6f6e6365223a3139382c2273656e646572223a2267456e574f65576d6d413063306a6b71764d354241707a61644b46574e534f69417643575163776d4750673d222c227265636569766572223a22414141414141414141414141415141414141414141414141414141414141414141414141414141432f2f383d222c2276616c7565223a302c226761735072696365223a313030303030303030302c226761734c696d6974223a36303030303030302c2264617461223a225a3256305132397564484a68593352446232356d6157633d222c227369676e6174757265223a2272525455544858677a4273496e4f6e454b6b7869642b354e66524d486e33534948314673746f577352434c434b3258514c41614f4e704449346531476173624c5150616130566f364144516d4f2b52446b6f364a43413d3d222c22636861696e4944223a2256413d3d222c2276657273696f6e223a327d", - ); - assert.equal( - relayedTxV1.getSignature().toString("hex"), - "128e7cdc14c2b9beee2f3ff7a7fa5d1f5ef31a654a0c92e223c90ab28265fa277d306f23a06536248cf9573e828017004fb639617fade4d68a37524aafca710d", - ); - }); - - it("should compute relayed v1 transaction (with usernames)", async function () { - const networkConfig = { - MinGasLimit: 50_000, - GasPerDataByte: 1_500, - GasPriceModifier: 0.01, - ChainID: "T", - }; - - const innerTx = new Transaction({ - nonce: 208, - value: TokenTransfer.egldFromAmount(1), - sender: carol.address, - receiver: alice.address, - senderUsername: "carol", - receiverUsername: "alice", - gasLimit: 50000, - chainID: networkConfig.ChainID, - }); - - innerTx.applySignature(await carol.signer.sign(innerTx.serializeForSigning())); - - const builder = new RelayedTransactionV1Builder(); - const relayedTxV1 = builder - .setInnerTransaction(innerTx) - .setRelayerNonce(715) - .setNetworkConfig(networkConfig) - .setRelayerAddress(frank.address) - .build(); - - relayedTxV1.applySignature(await frank.signer.sign(relayedTxV1.serializeForSigning())); - - assert.equal(relayedTxV1.getNonce().valueOf(), 715); - assert.equal( - relayedTxV1.getData().toString(), - "relayedTx@7b226e6f6e6365223a3230382c2273656e646572223a227371455656633553486b6c45344a717864556e59573068397a536249533141586f3534786f32634969626f3d222c227265636569766572223a2241546c484c76396f686e63616d433877673970645168386b77704742356a6949496f3349484b594e6165453d222c2276616c7565223a313030303030303030303030303030303030302c226761735072696365223a313030303030303030302c226761734c696d6974223a35303030302c2264617461223a22222c227369676e6174757265223a226a33427a6469554144325963517473576c65707663664a6f75657a48573063316b735a424a4d6339573167435450512b6870636759457858326f6f367a4b5654347464314b4b6f79783841526a346e336474576c44413d3d222c22636861696e4944223a2256413d3d222c2276657273696f6e223a322c22736e64557365724e616d65223a22593246796232773d222c22726376557365724e616d65223a22595778705932553d227d", - ); - assert.equal( - relayedTxV1.getSignature().toString("hex"), - "3787d640e5a579e7977a4a1bcdd435ad11855632fa4a414a06fbf8355692d1a58d76ef0adbdd6ccd6bd3c329f36bd53c180d4873ec1a6c558e659aeb9ab92d00", - ); - }); - - it("should compute relayed v1 transaction with big value", async function () { - const networkConfig = { - MinGasLimit: 50_000, - GasPerDataByte: 1_500, - GasPriceModifier: 0.01, - ChainID: "T", - }; - - const innerTx = new Transaction({ - nonce: 208, - value: TokenTransfer.egldFromAmount(1999999), - sender: carol.address, - receiver: alice.address, - senderUsername: "carol", - receiverUsername: "alice", - gasLimit: 50000, - chainID: networkConfig.ChainID, - }); - - innerTx.applySignature(await carol.signer.sign(innerTx.serializeForSigning())); - - const builder = new RelayedTransactionV1Builder(); - const relayedTxV1 = builder - .setInnerTransaction(innerTx) - .setRelayerNonce(715) - .setNetworkConfig(networkConfig) - .setRelayerAddress(frank.address) - .build(); - - relayedTxV1.applySignature(await frank.signer.sign(relayedTxV1.serializeForSigning())); - - assert.equal(relayedTxV1.getNonce().valueOf(), 715); - assert.equal( - relayedTxV1.getData().toString(), - "relayedTx@7b226e6f6e6365223a3230382c2273656e646572223a227371455656633553486b6c45344a717864556e59573068397a536249533141586f3534786f32634969626f3d222c227265636569766572223a2241546c484c76396f686e63616d433877673970645168386b77704742356a6949496f3349484b594e6165453d222c2276616c7565223a313939393939393030303030303030303030303030303030302c226761735072696365223a313030303030303030302c226761734c696d6974223a35303030302c2264617461223a22222c227369676e6174757265223a22594661677972512f726d614c7333766e7159307657553858415a7939354b4e31725738347a4f764b62376c7a3773576e2f566a546d68704378774d682b7261314e444832574d6f3965507648304f79427453776a44773d3d222c22636861696e4944223a2256413d3d222c2276657273696f6e223a322c22736e64557365724e616d65223a22593246796232773d222c22726376557365724e616d65223a22595778705932553d227d", - ); - assert.equal( - relayedTxV1.getSignature().toString("hex"), - "c0fb5cf8c0a413d6988ba35dc279c63f8849572c5f23b1cab36dcc50952dc3ed9da01068d6ac0cbde7e14167bfc2eca5164d5c2154c89eb313c9c596e3f8b801", - ); - }); - - it("should compute guarded inner Tx - relayed v1 transaction", async function () { - const networkConfig = { - MinGasLimit: 50_000, - GasPerDataByte: 1_500, - GasPriceModifier: 0.01, - ChainID: "T", - }; - - const innerTx = new Transaction({ - nonce: 198, - sender: bob.address, - receiver: Address.fromBech32("erd1qqqqqqqqqqqqqpgq54tsxmej537z9leghvp69hfu4f8gg5eu396q83gnnz"), - gasLimit: 60000000, - chainID: networkConfig.ChainID, - data: new TransactionPayload("getContractConfig"), - guardian: grace.address, - version: TransactionVersion.withTxOptions(), - options: TransactionOptions.withOptions({ guarded: true }), - }); - - innerTx.applySignature(await bob.signer.sign(innerTx.serializeForSigning())); - innerTx.applyGuardianSignature(await grace.signer.sign(innerTx.serializeForSigning())); - - const builder = new RelayedTransactionV1Builder(); - const relayedTxV1 = builder - .setInnerTransaction(innerTx) - .setRelayerNonce(2627) - .setNetworkConfig(networkConfig) - .setRelayerAddress(alice.address) - .build(); - - relayedTxV1.applySignature(await alice.signer.sign(relayedTxV1.serializeForSigning())); - - assert.equal(relayedTxV1.getNonce().valueOf(), 2627); - assert.equal( - relayedTxV1.getData().toString(), - "relayedTx@7b226e6f6e6365223a3139382c2273656e646572223a2267456e574f65576d6d413063306a6b71764d354241707a61644b46574e534f69417643575163776d4750673d222c227265636569766572223a22414141414141414141414146414b565841323879704877692f79693741364c64504b704f68464d386958513d222c2276616c7565223a302c226761735072696365223a313030303030303030302c226761734c696d6974223a36303030303030302c2264617461223a225a3256305132397564484a68593352446232356d6157633d222c227369676e6174757265223a224b4b78324f33383655725135416b4f465258307578327933446a384853334b373038487174344668377161557669424550716c45614e746e6158706a6f2f333651476d4a456934784435457a6c6f4f677a634d4442773d3d222c22636861696e4944223a2256413d3d222c2276657273696f6e223a322c226f7074696f6e73223a322c22677561726469616e223a22486f714c61306e655733766843716f56696c70715372744c5673774939535337586d7a563868477450684d3d222c22677561726469616e5369676e6174757265223a222b5431526f4833625a792f54423177342b6a365155477258645637457577553073753948646551626453515269463953757a686d634b705463526d58595252366c534c6652394931624d7134674730436538363741513d3d227d", - ); - assert.equal( - relayedTxV1.getSignature().toString("hex"), - "39cff9d5100e290fbc7361cb6e2402261caf864257b4116f150e0c61e7869155dff8361fa5449431eb7a8ed847c01ba9b3b5ebafe5fac1a3d40c64829d827e00", - ); - }); - - it("should compute guarded inner tx and guarded relayed v1 transaction", async function () { - const networkConfig = { - MinGasLimit: 50_000, - GasPerDataByte: 1_500, - GasPriceModifier: 0.01, - ChainID: "T", - }; - - const innerTx = new Transaction({ - nonce: 198, - sender: bob.address, - receiver: Address.fromBech32("erd1qqqqqqqqqqqqqpgq54tsxmej537z9leghvp69hfu4f8gg5eu396q83gnnz"), - gasLimit: 60000000, - chainID: networkConfig.ChainID, - data: new TransactionPayload("addNumber"), - guardian: grace.address, - version: TransactionVersion.withTxOptions(), - options: TransactionOptions.withOptions({ guarded: true }), - }); - - innerTx.applySignature(await bob.signer.sign(innerTx.serializeForSigning())); - innerTx.applyGuardianSignature(await grace.signer.sign(innerTx.serializeForSigning())); - const builder = new RelayedTransactionV1Builder(); - const relayedTxV1 = builder - .setInnerTransaction(innerTx) - .setRelayerNonce(2627) - .setNetworkConfig(networkConfig) - .setRelayerAddress(alice.address) - .setRelayedTransactionVersion(TransactionVersion.withTxOptions()) - .setRelayedTransactionOptions(TransactionOptions.withOptions({ guarded: true })) - .setRelayedTransactionGuardian(frank.address) - .build(); - - relayedTxV1.applySignature(await alice.signer.sign(relayedTxV1.serializeForSigning())); - relayedTxV1.applyGuardianSignature(await frank.signer.sign(relayedTxV1.serializeForSigning())); - - assert.equal(relayedTxV1.getNonce().valueOf(), 2627); - assert.equal( - relayedTxV1.getData().toString(), - "relayedTx@7b226e6f6e6365223a3139382c2273656e646572223a2267456e574f65576d6d413063306a6b71764d354241707a61644b46574e534f69417643575163776d4750673d222c227265636569766572223a22414141414141414141414146414b565841323879704877692f79693741364c64504b704f68464d386958513d222c2276616c7565223a302c226761735072696365223a313030303030303030302c226761734c696d6974223a36303030303030302c2264617461223a225957526b546e5674596d5679222c227369676e6174757265223a223469724d4b4a656d724d375174344e7635487633544c44683775654779487045564c4371674a3677652f7a662b746a4933354975573452633458543451533433475333356158386c6a533834324a38426854645043673d3d222c22636861696e4944223a2256413d3d222c2276657273696f6e223a322c226f7074696f6e73223a322c22677561726469616e223a22486f714c61306e655733766843716f56696c70715372744c5673774939535337586d7a563868477450684d3d222c22677561726469616e5369676e6174757265223a2270424754394e674a78307539624c56796b654d78786a454865374269696c37764932324a46676f32787a6e2f496e3032463769546563356b44395045324f747065386c475335412b532f4a36417762576834446744673d3d227d", - ); - assert.equal( - relayedTxV1.getSignature().toString("hex"), - "8ede1bbeed96b102344dffeac12c2592c62b7313cdeb132e8c8bf11d2b1d3bb8189d257a6dbcc99e222393d9b9ec77656c349dae97a32e68bdebd636066bf706", - ); - }); -}); diff --git a/src/relayedTransactionV1Builder.ts b/src/relayedTransactionV1Builder.ts deleted file mode 100644 index 52a48262d..000000000 --- a/src/relayedTransactionV1Builder.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { Address } from "./address"; -import { ErrInvalidRelayedV1BuilderArguments } from "./errors"; -import { IAddress, INonce } from "./interface"; -import { INetworkConfig } from "./interfaceOfNetwork"; -import { TransactionOptions, TransactionVersion } from "./networkParams"; -import { Transaction } from "./transaction"; -import { TransactionPayload } from "./transactionPayload"; - -const JSONbig = require("json-bigint"); - -/** - * @deprecated Use {@link RelayedTransactionsFactory} instead. - */ -export class RelayedTransactionV1Builder { - innerTransaction: Transaction | undefined; - relayerAddress: IAddress | undefined; - relayerNonce: INonce | undefined; - netConfig: INetworkConfig | undefined; - relayedTransactionOptions: TransactionOptions | undefined; - relayedTransactionVersion: TransactionVersion | undefined; - relayedTransactionGuardian: IAddress | undefined; - - /** - * Sets the inner transaction to be used. It has to be already signed. - * - * @param {Transaction} transaction The inner transaction to be used - */ - setInnerTransaction(transaction: Transaction): RelayedTransactionV1Builder { - this.innerTransaction = transaction; - return this; - } - - /** - * Sets the network config to be used for building the relayed v1 transaction - * - * @param {INetworkConfig} netConfig The network configuration to be used - */ - setNetworkConfig(netConfig: INetworkConfig): RelayedTransactionV1Builder { - this.netConfig = netConfig; - return this; - } - - /** - * Sets the address of the relayer (the one that will actually pay the fee) - * - * @param relayerAddress - */ - setRelayerAddress(relayerAddress: IAddress): RelayedTransactionV1Builder { - this.relayerAddress = relayerAddress; - return this; - } - - /** - * (optional) Sets the nonce of the relayer - * - * @param relayerNonce - */ - setRelayerNonce(relayerNonce: INonce): RelayedTransactionV1Builder { - this.relayerNonce = relayerNonce; - return this; - } - - /** - * (optional) Sets the version of the relayed transaction - * - * @param relayedTxVersion - */ - setRelayedTransactionVersion(relayedTxVersion: TransactionVersion): RelayedTransactionV1Builder { - this.relayedTransactionVersion = relayedTxVersion; - return this; - } - - /** - * (optional) Sets the options of the relayed transaction - * - * @param relayedTxOptions - */ - setRelayedTransactionOptions(relayedTxOptions: TransactionOptions): RelayedTransactionV1Builder { - this.relayedTransactionOptions = relayedTxOptions; - return this; - } - - /** - * (optional) Sets the guardian of the relayed transaction - * - * @param relayedTxGuardian - */ - setRelayedTransactionGuardian(relayedTxGuardian: IAddress): RelayedTransactionV1Builder { - this.relayedTransactionGuardian = relayedTxGuardian; - return this; - } - - /** - * Tries to build the relayed v1 transaction based on the previously set fields - * - * @throws ErrInvalidRelayedV1BuilderArguments - * @return Transaction - */ - build(): Transaction { - if ( - !this.innerTransaction || - !this.netConfig || - !this.relayerAddress || - !this.innerTransaction.getSignature() - ) { - throw new ErrInvalidRelayedV1BuilderArguments(); - } - - const serializedTransaction = this.prepareInnerTransaction(); - const data = `relayedTx@${Buffer.from(serializedTransaction).toString("hex")}`; - const payload = new TransactionPayload(data); - - const gasLimit = - this.netConfig.MinGasLimit + - this.netConfig.GasPerDataByte * payload.length() + - this.innerTransaction.getGasLimit().valueOf(); - let relayedTransaction = new Transaction({ - nonce: this.relayerNonce, - sender: this.relayerAddress, - receiver: this.innerTransaction.getSender(), - value: 0, - gasLimit: gasLimit, - data: payload, - chainID: this.netConfig.ChainID, - version: this.relayedTransactionVersion, - options: this.relayedTransactionOptions, - guardian: this.relayedTransactionGuardian, - }); - - if (this.relayerNonce) { - relayedTransaction.setNonce(this.relayerNonce); - } - - return relayedTransaction; - } - - private prepareInnerTransaction(): string { - if (!this.innerTransaction) { - return ""; - } - - const txObject = { - nonce: this.innerTransaction.getNonce().valueOf(), - sender: new Address(this.innerTransaction.getSender().bech32()).pubkey().toString("base64"), - receiver: new Address(this.innerTransaction.getReceiver().bech32()).pubkey().toString("base64"), - value: BigInt(this.innerTransaction.getValue().toString()), - gasPrice: this.innerTransaction.getGasPrice().valueOf(), - gasLimit: this.innerTransaction.getGasLimit().valueOf(), - data: this.innerTransaction.getData().valueOf().toString("base64"), - signature: this.innerTransaction.getSignature().toString("base64"), - chainID: Buffer.from(this.innerTransaction.getChainID().valueOf()).toString("base64"), - version: this.innerTransaction.getVersion().valueOf(), - options: - this.innerTransaction.getOptions().valueOf() == 0 - ? undefined - : this.innerTransaction.getOptions().valueOf(), - guardian: this.innerTransaction.getGuardian().bech32() - ? new Address(this.innerTransaction.getGuardian().bech32()).pubkey().toString("base64") - : undefined, - guardianSignature: this.innerTransaction.getGuardianSignature().toString("hex") - ? this.innerTransaction.getGuardianSignature().toString("base64") - : undefined, - sndUserName: this.innerTransaction.getSenderUsername() - ? Buffer.from(this.innerTransaction.getSenderUsername()).toString("base64") - : undefined, - rcvUserName: this.innerTransaction.getReceiverUsername() - ? Buffer.from(this.innerTransaction.getReceiverUsername()).toString("base64") - : undefined, - }; - - return JSONbig.stringify(txObject); - } -} diff --git a/src/relayedTransactionV2Builder.spec.ts b/src/relayedTransactionV2Builder.spec.ts deleted file mode 100644 index cb4cd530c..000000000 --- a/src/relayedTransactionV2Builder.spec.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { loadTestWallets, TestWallet } from "./testutils"; -import { RelayedTransactionV2Builder } from "./relayedTransactionV2Builder"; -import { Address } from "./address"; -import { TransactionPayload } from "./transactionPayload"; -import { assert } from "chai"; -import { Transaction } from "./transaction"; -import * as errors from "./errors"; - -describe("test relayed v2 transaction builder", function () { - let alice: TestWallet, bob: TestWallet; - - before(async function () { - ({ alice, bob } = await loadTestWallets()); - }); - - it("should throw exception if args were not set", async function () { - const builder = new RelayedTransactionV2Builder(); - assert.throw(() => builder.build(), errors.ErrInvalidRelayedV2BuilderArguments); - }); - - it("should throw exception if gas limit of the inner tx is not 0", async function () { - let builder = new RelayedTransactionV2Builder(); - - let networkConfig = { - MinGasLimit: 50_000, - GasPerDataByte: 1_500, - GasPriceModifier: 0.01, - ChainID: "T", - }; - - const innerTx = new Transaction({ - nonce: 15, - sender: alice.address, - receiver: Address.fromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u"), - gasLimit: 10, - chainID: networkConfig.ChainID, - data: new TransactionPayload("getContractConfig"), - }); - builder = builder - .setNetworkConfig(networkConfig) - .setInnerTransactionGasLimit(10) - .setInnerTransaction(innerTx) - .setRelayerAddress(alice.address); - assert.throw(() => builder.build(), errors.ErrGasLimitShouldBe0ForInnerTransaction); - - innerTx.setGasLimit({ - valueOf: function () { - return 10; - }, - }); - builder = builder.setNetworkConfig(networkConfig).setInnerTransactionGasLimit(10).setInnerTransaction(innerTx); - assert.throw(() => builder.build(), errors.ErrGasLimitShouldBe0ForInnerTransaction); - }); - - it("should compute relayed v2 tx", async function () { - let networkConfig = { - MinGasLimit: 50_000, - GasPerDataByte: 1_500, - GasPriceModifier: 0.01, - ChainID: "T", - }; - - const innerTx = new Transaction({ - nonce: 15, - sender: bob.address, - receiver: Address.fromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u"), - gasLimit: 0, - chainID: networkConfig.ChainID, - data: new TransactionPayload("getContractConfig"), - version: 2, - }); - - innerTx.applySignature(await bob.signer.sign(innerTx.serializeForSigning())); - - const builder = new RelayedTransactionV2Builder(); - const relayedTxV2 = builder - .setInnerTransaction(innerTx) - .setInnerTransactionGasLimit(60_000_000) - .setRelayerNonce(37) - .setNetworkConfig(networkConfig) - .setRelayerAddress(alice.getAddress()) - .build(); - - relayedTxV2.setSender(alice.address); - relayedTxV2.applySignature(await alice.signer.sign(relayedTxV2.serializeForSigning())); - - assert.equal(relayedTxV2.getNonce().valueOf(), 37); - assert.equal(relayedTxV2.getVersion().valueOf(), 2); - assert.equal( - relayedTxV2.getData().toString(), - "relayedTxV2@000000000000000000010000000000000000000000000000000000000002ffff@0f@676574436f6e7472616374436f6e666967@fc3ed87a51ee659f937c1a1ed11c1ae677e99629fae9cc289461f033e6514d1a8cfad1144ae9c1b70f28554d196bd6ba1604240c1c1dc19c959e96c1c3b62d0c", - ); - }); -}); diff --git a/src/relayedTransactionV2Builder.ts b/src/relayedTransactionV2Builder.ts deleted file mode 100644 index b5b01b1a6..000000000 --- a/src/relayedTransactionV2Builder.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { ErrGasLimitShouldBe0ForInnerTransaction, ErrInvalidRelayedV2BuilderArguments } from "./errors"; -import { IAddress, IGasLimit, INonce } from "./interface"; -import { INetworkConfig } from "./interfaceOfNetwork"; -import { AddressValue, ArgSerializer, BytesValue, U64Value } from "./smartcontracts"; -import { Transaction } from "./transaction"; -import { TransactionPayload } from "./transactionPayload"; - -/** - * @deprecated Use {@link RelayedTransactionsFactory} instead. - */ -export class RelayedTransactionV2Builder { - innerTransaction: Transaction | undefined; - innerTransactionGasLimit: IGasLimit | undefined; - relayerAddress: IAddress | undefined; - relayerNonce: INonce | undefined; - netConfig: INetworkConfig | undefined; - - /** - * Sets the inner transaction to be used. It has to be already signed and with gasLimit set to 0. These checks - * are performed on the build() method - * - * @param {Transaction} transaction The inner transaction to be used - */ - setInnerTransaction(transaction: Transaction): RelayedTransactionV2Builder { - this.innerTransaction = transaction; - return this; - } - - /** - * Sets the gas limit to be used for the SC Call inside the inner transaction - * - * @param {IGasLimit} gasLimit The gas limit to be used. The inner transaction needs to have the gas limit set to 0, - * so this field will specify the gas to be used for the SC call of the inner transaction - */ - setInnerTransactionGasLimit(gasLimit: IGasLimit): RelayedTransactionV2Builder { - this.innerTransactionGasLimit = gasLimit; - return this; - } - - /** - * Sets the network config to be used for building the relayed v2 transaction - * - * @param {INetworkConfig} netConfig The network configuration to be used - */ - setNetworkConfig(netConfig: INetworkConfig): RelayedTransactionV2Builder { - this.netConfig = netConfig; - return this; - } - - /** - * Sets the address of the relayer (the one that will actually pay the fee) - * - * @param relayerAddress - */ - setRelayerAddress(relayerAddress: IAddress): RelayedTransactionV2Builder { - this.relayerAddress = relayerAddress; - return this; - } - - /** - * (optional) Sets the nonce of the relayer - * - * @param relayerNonce - */ - setRelayerNonce(relayerNonce: INonce): RelayedTransactionV2Builder { - this.relayerNonce = relayerNonce; - return this; - } - - /** - * Tries to build the relayed v2 transaction based on the previously set fields. - * It returns a transaction that isn't signed - * - * @throws ErrInvalidRelayedV2BuilderArguments - * @throws ErrGasLimitShouldBe0ForInnerTransaction - * @return Transaction - */ - build(): Transaction { - if ( - !this.innerTransaction || - !this.innerTransactionGasLimit || - !this.relayerAddress || - !this.netConfig || - !this.innerTransaction.getSignature() - ) { - throw new ErrInvalidRelayedV2BuilderArguments(); - } - if (this.innerTransaction.getGasLimit() != 0) { - throw new ErrGasLimitShouldBe0ForInnerTransaction(); - } - - const { argumentsString } = new ArgSerializer().valuesToString([ - new AddressValue(this.innerTransaction.getReceiver()), - new U64Value(this.innerTransaction.getNonce().valueOf()), - new BytesValue(this.innerTransaction.getData().valueOf()), - new BytesValue(this.innerTransaction.getSignature()), - ]); - - const data = `relayedTxV2@${argumentsString}`; - const payload = new TransactionPayload(data); - - let relayedTransaction = new Transaction({ - sender: this.relayerAddress, - receiver: this.innerTransaction.getSender(), - value: 0, - gasLimit: - this.innerTransactionGasLimit.valueOf() + - this.netConfig.MinGasLimit + - this.netConfig.GasPerDataByte * payload.length(), - data: payload, - chainID: this.netConfig.ChainID, - version: this.innerTransaction.getVersion(), - options: this.innerTransaction.getOptions(), - }); - - if (this.relayerNonce) { - relayedTransaction.setNonce(this.relayerNonce); - } - - return relayedTransaction; - } -} diff --git a/src/signableMessage.spec.ts b/src/signableMessage.spec.ts deleted file mode 100644 index 4480b66dc..000000000 --- a/src/signableMessage.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { assert } from "chai"; -import { SignableMessage } from "./signableMessage"; -import { loadTestWallets, TestWallet } from "./testutils"; - -describe("test signable message", () => { - let alice: TestWallet; - before(async function () { - ({ alice } = await loadTestWallets()); - }); - it("should create signableMessage", async () => { - const sm = new SignableMessage({ - address: alice.address, - message: Buffer.from("test message", "ascii"), - signature: Buffer.from("a".repeat(128), "hex"), - signer: "ElrondWallet", - }); - - const jsonSM = sm.toJSON(); - - // We just test that the returned object contains what was passed and the hex values are prefixed with 0x - assert.deepEqual( - jsonSM, - { - address: "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", - message: "0x74657374206d657373616765", - signature: - "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - version: 1, - signer: "ElrondWallet", - }, - "invalid signable message returned", - ); - }); -}); diff --git a/src/signableMessage.ts b/src/signableMessage.ts deleted file mode 100644 index 872407cf0..000000000 --- a/src/signableMessage.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { Address } from "./address"; -import { ISignature } from "./interface"; -import { interpretSignatureAsBuffer } from "./signature"; -import { MESSAGE_PREFIX } from "./constants"; -const createKeccakHash = require("keccak"); - -/** - * @deprecated Use {@link Message} instead. - */ -export class SignableMessage { - /** - * Actual message being signed. - */ - message: Buffer; - /** - * Signature obtained by a signer of type @param signer . - */ - signature: Buffer; - - /** - * Address of the wallet that performed the signing operation - */ - address: Address; - - /** - * Text representing the identifer for the application that signed the message - */ - signer: string; - - /** - * Number representing the signable message version - */ - version: number; - - public constructor(init?: Partial) { - this.message = Buffer.from([]); - this.signature = Buffer.from([]); - this.version = 1; - this.signer = "ErdJS"; - this.address = Address.empty(); - - Object.assign(this, init); - } - - serializeForSigning(): Buffer { - const messageSize = Buffer.from(this.message.length.toString()); - const signableMessage = Buffer.concat([messageSize, this.message]); - let bytesToHash = Buffer.concat([Buffer.from(MESSAGE_PREFIX), signableMessage]); - - return createKeccakHash("keccak256").update(bytesToHash).digest(); - } - - serializeForSigningRaw(): Buffer { - return Buffer.concat([this.getMessageSize(), this.message]); - } - - getSignature(): Buffer { - return this.signature; - } - - applySignature(signature: ISignature | Uint8Array) { - this.signature = interpretSignatureAsBuffer(signature); - } - - getMessageSize(): Buffer { - const messageSize = Buffer.alloc(4); - messageSize.writeUInt32BE(this.message.length, 0); - - return messageSize; - } - - toJSON(): object { - return { - address: this.address.bech32(), - message: "0x" + this.message.toString("hex"), - signature: "0x" + this.signature.toString("hex"), - version: this.version, - signer: this.signer, - }; - } -} diff --git a/src/smartContractQueriesController.ts b/src/smartContractQueriesController.ts deleted file mode 100644 index d156742fc..000000000 --- a/src/smartContractQueriesController.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { Err, ErrSmartContractQuery } from "./errors"; -import { IContractQueryResponse } from "./interfaceOfNetwork"; -import { SmartContractQuery, SmartContractQueryResponse } from "./smartContractQuery"; -import { ArgSerializer, ContractFunction, EndpointDefinition, NativeSerializer, ResultsParser } from "./smartcontracts"; -import { isTyped } from "./smartcontracts/typesystem"; - -interface IAbi { - getEndpoint(name: string | ContractFunction): EndpointDefinition; -} - -interface IQueryRunner { - runQuery(query: SmartContractQuery): Promise; -} - -export class SmartContractQueriesController { - private readonly abi?: IAbi; - private readonly queryRunner: IQueryRunner; - private readonly legacyResultsParser: ResultsParser; - - constructor(options: { abi?: IAbi; queryRunner: IQueryRunner }) { - this.abi = options.abi; - this.queryRunner = options.queryRunner; - this.legacyResultsParser = new ResultsParser(); - } - - async query(options: { - contract: string; - caller?: string; - value?: bigint; - function: string; - arguments: any[]; - }): Promise { - const query = this.createQuery(options); - const queryResponse = await this.runQuery(query); - this.raiseForStatus(queryResponse); - return this.parseQueryResponse(queryResponse); - } - - private raiseForStatus(queryResponse: SmartContractQueryResponse): void { - const isOk = queryResponse.returnCode === "ok"; - if (!isOk) { - throw new ErrSmartContractQuery(queryResponse.returnCode, queryResponse.returnMessage); - } - } - - createQuery(options: { - contract: string; - caller?: string; - value?: bigint; - function: string; - arguments: any[]; - }): SmartContractQuery { - const preparedArguments = this.encodeArguments(options.function, options.arguments); - - return new SmartContractQuery({ - contract: options.contract, - caller: options.caller, - function: options.function, - arguments: preparedArguments, - value: options.value, - }); - } - - private encodeArguments(functionName: string, args: any[]): Uint8Array[] { - const endpoint = this.abi?.getEndpoint(functionName); - - if (endpoint) { - const typedArgs = NativeSerializer.nativeToTypedValues(args, endpoint); - return new ArgSerializer().valuesToBuffers(typedArgs); - } - - if (this.areArgsOfTypedValue(args)) { - return new ArgSerializer().valuesToBuffers(args); - } - - if (this.areArgsBuffers(args)) { - return args.map((arg) => Buffer.from(arg)); - } - - throw new Err( - "cannot encode arguments: when ABI is not available, they must be either typed values or buffers", - ); - } - - private areArgsOfTypedValue(args: any[]): boolean { - return args.every((arg) => isTyped(arg)); - } - - private areArgsBuffers(args: any[]): boolean { - for (const arg of args) { - if (!ArrayBuffer.isView(arg)) { - return false; - } - } - - return true; - } - - async runQuery(query: SmartContractQuery): Promise { - const queryResponse = await this.queryRunner.runQuery(query); - return queryResponse; - } - - parseQueryResponse(response: SmartContractQueryResponse): any[] { - if (!this.abi) { - return response.returnDataParts; - } - - const legacyQueryResponse: IContractQueryResponse = { - returnCode: response.returnCode, - returnMessage: response.returnMessage, - getReturnDataParts: () => response.returnDataParts.map((part) => Buffer.from(part)), - }; - - const functionName = response.function; - const endpoint = this.abi.getEndpoint(functionName); - const legacyBundle = this.legacyResultsParser.parseQueryResponse(legacyQueryResponse, endpoint); - const nativeValues = legacyBundle.values.map((value) => value.valueOf()); - return nativeValues; - } -} diff --git a/src/smartContractQuery.ts b/src/smartContractQuery.ts deleted file mode 100644 index 275d16bc8..000000000 --- a/src/smartContractQuery.ts +++ /dev/null @@ -1,35 +0,0 @@ -export class SmartContractQuery { - contract: string; - caller?: string; - value?: bigint; - function: string; - arguments: Uint8Array[]; - - constructor(options: { - contract: string; - caller?: string; - value?: bigint; - function: string; - arguments: Uint8Array[]; - }) { - this.contract = options.contract; - this.caller = options.caller; - this.value = options.value; - this.function = options.function; - this.arguments = options.arguments; - } -} - -export class SmartContractQueryResponse { - function: string; - returnCode: string; - returnMessage: string; - returnDataParts: Uint8Array[]; - - constructor(obj: { function: string; returnCode: string; returnMessage: string; returnDataParts: Uint8Array[] }) { - this.function = obj.function; - this.returnCode = obj.returnCode; - this.returnMessage = obj.returnMessage; - this.returnDataParts = obj.returnDataParts; - } -} diff --git a/src/smartContracts/index.ts b/src/smartContracts/index.ts new file mode 100644 index 000000000..9a036eb4d --- /dev/null +++ b/src/smartContracts/index.ts @@ -0,0 +1,4 @@ +export * from "./resources"; +export * from "./smartContractController"; +export * from "./smartContractTransactionsFactory"; +export * from "./smartContractTransactionsOutcomeParser"; diff --git a/src/smartContracts/resources.ts b/src/smartContracts/resources.ts new file mode 100644 index 000000000..508aab388 --- /dev/null +++ b/src/smartContracts/resources.ts @@ -0,0 +1,41 @@ +import { Address } from "../core/address"; +import { TokenTransfer } from "../core/tokens"; + +export type ContractDeployInput = { + bytecode: Uint8Array; + gasLimit: bigint; + arguments?: any[]; + nativeTransferAmount?: bigint; + isUpgradeable?: boolean; + isReadable?: boolean; + isPayable?: boolean; + isPayableBySmartContract?: boolean; +}; + +export type ContractExecuteInput = { + contract: Address; + gasLimit: bigint; + function: string; + arguments?: any[]; + nativeTransferAmount?: bigint; + tokenTransfers?: TokenTransfer[]; +}; + +export type ContractUpgradeInput = ContractDeployInput & { contract: Address }; + +export interface SmartContractDeployOutcome { + returnCode: string; + returnMessage: string; + contracts: DeployedSmartContract[]; +} +export type ParsedSmartContractCallOutcome = { + values: any[]; + returnCode: string; + returnMessage: string; +}; + +export type DeployedSmartContract = { + address: Address; + ownerAddress: Address; + codeHash: Uint8Array; +}; diff --git a/src/smartContractQueriesController.spec.ts b/src/smartContracts/smartContractController.spec.ts similarity index 66% rename from src/smartContractQueriesController.spec.ts rename to src/smartContracts/smartContractController.spec.ts index 07bc570d8..a6b36a0e7 100644 --- a/src/smartContractQueriesController.spec.ts +++ b/src/smartContracts/smartContractController.spec.ts @@ -1,58 +1,56 @@ -import { ContractQueryResponse } from "./networkProviders"; import BigNumber from "bignumber.js"; import { assert } from "chai"; -import { QueryRunnerAdapter } from "./adapters/queryRunnerAdapter"; -import { SmartContractQueriesController } from "./smartContractQueriesController"; -import { SmartContractQueryResponse } from "./smartContractQuery"; -import { AbiRegistry, BigUIntValue, BooleanValue, BytesValue, Tuple, U16Value, U64Value } from "./smartcontracts"; -import { bigIntToBuffer } from "./smartcontracts/codec/utils"; -import { MockNetworkProvider, loadAbiRegistry } from "./testutils"; +import { Abi, BigUIntValue, BooleanValue, BytesValue, Tuple, U16Value, U64Value } from "../abi"; +import { Address, SmartContractQueryResponse } from "../core"; +import { MockNetworkProvider, loadAbiRegistry } from "../testutils"; +import { bigIntToBuffer } from "../tokenOperations/codec"; +import { SmartContractController } from "./smartContractController"; describe("test smart contract queries controller", () => { describe("createQuery", () => { it("works without ABI, when arguments are buffers", function () { - const adapter = new QueryRunnerAdapter({ networkProvider: new MockNetworkProvider() }); - const controller = new SmartContractQueriesController({ - queryRunner: adapter, + const controller = new SmartContractController({ + chainID: this.chainId, + networkProvider: this.networkProvider, }); const query = controller.createQuery({ - contract: "erd1foo", + contract: Address.empty(), function: "bar", arguments: [bigIntToBuffer(42), Buffer.from("abba")], }); - assert.equal(query.contract, "erd1foo"); + assert.deepEqual(query.contract, Address.empty()); assert.equal(query.function, "bar"); assert.deepEqual(query.arguments, [bigIntToBuffer(42), Buffer.from("abba")]); }); it("works without ABI, when arguments are typed values", function () { - const adapter = new QueryRunnerAdapter({ networkProvider: new MockNetworkProvider() }); - const controller = new SmartContractQueriesController({ - queryRunner: adapter, + const controller = new SmartContractController({ + chainID: this.chainId, + networkProvider: this.networkProvider, }); const query = controller.createQuery({ - contract: "erd1foo", + contract: Address.empty(), function: "bar", arguments: [new BigUIntValue(42), BytesValue.fromUTF8("abba")], }); - assert.equal(query.contract, "erd1foo"); + assert.deepEqual(query.contract, Address.empty()); assert.equal(query.function, "bar"); assert.deepEqual(query.arguments, [bigIntToBuffer(42), Buffer.from("abba")]); }); it("fails without ABI, when arguments aren't buffers, nor typed values", function () { - const adapter = new QueryRunnerAdapter({ networkProvider: new MockNetworkProvider() }); - const controller = new SmartContractQueriesController({ - queryRunner: adapter, + const controller = new SmartContractController({ + chainID: this.chainId, + networkProvider: this.networkProvider, }); assert.throws(() => { controller.createQuery({ - contract: "erd1foo", + contract: Address.empty(), function: "bar", arguments: [42, "abba"], }); @@ -60,47 +58,43 @@ describe("test smart contract queries controller", () => { }); it("works with ABI, when arguments are native JS objects", async function () { - const adapter = new QueryRunnerAdapter({ - networkProvider: new MockNetworkProvider(), - }); - const controller = new SmartContractQueriesController({ + const controller = new SmartContractController({ + chainID: this.chainId, + networkProvider: this.networkProvider, abi: await loadAbiRegistry("src/testdata/lottery-esdt.abi.json"), - queryRunner: adapter, }); const query = controller.createQuery({ - contract: "erd1foo", + contract: Address.empty(), function: "getLotteryInfo", arguments: ["myLottery"], }); - assert.equal(query.contract, "erd1foo"); + assert.deepEqual(query.contract, Address.empty()); assert.equal(query.function, "getLotteryInfo"); assert.deepEqual(query.arguments, [Buffer.from("myLottery")]); }); it("works with ABI, when arguments typed values", async function () { - const adapter = new QueryRunnerAdapter({ - networkProvider: new MockNetworkProvider(), - }); - const controller = new SmartContractQueriesController({ + const controller = new SmartContractController({ + chainID: this.chainId, + networkProvider: this.networkProvider, abi: await loadAbiRegistry("src/testdata/lottery-esdt.abi.json"), - queryRunner: adapter, }); const query = controller.createQuery({ - contract: "erd1foo", + contract: Address.empty(), function: "getLotteryInfo", arguments: [BytesValue.fromUTF8("myLottery")], }); - assert.equal(query.contract, "erd1foo"); + assert.deepEqual(query.contract, Address.empty()); assert.equal(query.function, "getLotteryInfo"); assert.deepEqual(query.arguments, [Buffer.from("myLottery")]); }); it("works with ABI, with mixed arguments", async function () { - const abi = AbiRegistry.create({ + const abi = Abi.create({ endpoints: [ { name: "bar", @@ -123,16 +117,14 @@ describe("test smart contract queries controller", () => { ], }); - const adapter = new QueryRunnerAdapter({ - networkProvider: new MockNetworkProvider(), - }); - const controller = new SmartContractQueriesController({ + const controller = new SmartContractController({ + chainID: this.chainId, + networkProvider: this.networkProvider, abi: abi, - queryRunner: adapter, }); const query = controller.createQuery({ - contract: "erd1foo", + contract: Address.empty(), function: "bar", arguments: [ // Typed value @@ -148,7 +140,7 @@ describe("test smart contract queries controller", () => { ], }); - assert.equal(query.contract, "erd1foo"); + assert.deepEqual(query.contract, Address.empty()); assert.equal(query.function, "bar"); assert.deepEqual(query.arguments, [ Buffer.from("002a01", "hex"), @@ -162,23 +154,24 @@ describe("test smart contract queries controller", () => { describe("runQuery", () => { it("calls queryContract on the network provider", async function () { const networkProvider = new MockNetworkProvider(); - const adapter = new QueryRunnerAdapter({ + + const controller = new SmartContractController({ + chainID: this.chainId, networkProvider: networkProvider, }); - const controller = new SmartContractQueriesController({ - queryRunner: adapter, - }); networkProvider.mockQueryContractOnFunction( "bar", - new ContractQueryResponse({ - returnData: [Buffer.from("abba").toString("base64")], + new SmartContractQueryResponse({ + function: "bar", + returnDataParts: [Buffer.from("YWJiYQ==", "base64")], returnCode: "ok", + returnMessage: "msg", }), ); const query = { - contract: "erd1qqqqqqqqqqqqqpgqvc7gdl0p4s97guh498wgz75k8sav6sjfjlwqh679jy", + contract: Address.newFromBech32("erd1qqqqqqqqqqqqqpgqvc7gdl0p4s97guh498wgz75k8sav6sjfjlwqh679jy"), function: "bar", arguments: [], }; @@ -192,11 +185,9 @@ describe("test smart contract queries controller", () => { describe("parseQueryResponse", () => { it("works without ABI", function () { - const adapter = new QueryRunnerAdapter({ - networkProvider: new MockNetworkProvider(), - }); - const controller = new SmartContractQueriesController({ - queryRunner: adapter, + const controller = new SmartContractController({ + chainID: this.chainId, + networkProvider: this.networkProvider, }); const response = new SmartContractQueryResponse({ @@ -212,12 +203,10 @@ describe("test smart contract queries controller", () => { }); it("works with ABI", async function () { - const adapter = new QueryRunnerAdapter({ - networkProvider: new MockNetworkProvider(), - }); - const controller = new SmartContractQueriesController({ + const controller = new SmartContractController({ + chainID: this.chainId, + networkProvider: this.networkProvider, abi: await loadAbiRegistry("src/testdata/lottery-esdt.abi.json"), - queryRunner: adapter, }); const response = new SmartContractQueryResponse({ diff --git a/src/smartContracts/smartContractController.ts b/src/smartContracts/smartContractController.ts new file mode 100644 index 000000000..cd370bbcc --- /dev/null +++ b/src/smartContracts/smartContractController.ts @@ -0,0 +1,187 @@ +import { Abi, ArgSerializer, isTyped, NativeSerializer } from "../abi"; +import { + Address, + BaseController, + BaseControllerInput, + Err, + ErrSmartContractQuery, + IAccount, + SmartContractQuery, + SmartContractQueryInput, + SmartContractQueryResponse, + Transaction, + TransactionOnNetwork, + TransactionsFactoryConfig, + TransactionWatcher, +} from "../core"; +import { INetworkProvider } from "../networkProviders/interface"; +import { SmartContractTransactionsOutcomeParser } from "../transactionsOutcomeParsers"; +import * as resources from "./resources"; +import { SmartContractTransactionsFactory } from "./smartContractTransactionsFactory"; + +export class SmartContractController extends BaseController { + private factory: SmartContractTransactionsFactory; + private parser: SmartContractTransactionsOutcomeParser; + private transactionWatcher: TransactionWatcher; + private networkProvider: INetworkProvider; + private abi?: Abi; + + constructor(options: { chainID: string; networkProvider: INetworkProvider; abi?: Abi }) { + super(); + this.factory = new SmartContractTransactionsFactory({ + config: new TransactionsFactoryConfig({ chainID: options.chainID }), + abi: options.abi, + }); + this.parser = new SmartContractTransactionsOutcomeParser(options); + this.transactionWatcher = new TransactionWatcher(options.networkProvider); + this.networkProvider = options.networkProvider; + this.abi = options.abi; + } + + async createTransactionForDeploy( + sender: IAccount, + nonce: bigint, + options: resources.ContractDeployInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForDeploy(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + parseDeploy(transactionOnNetwork: TransactionOnNetwork): resources.SmartContractDeployOutcome { + return this.parser.parseDeploy({ transactionOnNetwork }); + } + + async awaitCompletedDeploy(txHash: string): Promise { + const transaction = await this.transactionWatcher.awaitCompleted(txHash); + return this.parseDeploy(transaction); + } + + async createTransactionForUpgrade( + sender: IAccount, + nonce: bigint, + options: resources.ContractUpgradeInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForUpgrade(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async createTransactionForExecute( + sender: IAccount, + nonce: bigint, + options: resources.ContractExecuteInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForExecute(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + parseExecute(transactionOnNetwork: TransactionOnNetwork): resources.ParsedSmartContractCallOutcome { + return this.parser.parseExecute({ transactionOnNetwork }); + } + + async awaitCompletedExecute(txHash: string): Promise { + const transaction = await this.transactionWatcher.awaitCompleted(txHash); + return this.parseExecute(transaction); + } + + async query(options: SmartContractQueryInput): Promise { + const query = this.createQuery(options); + const queryResponse = await this.runQuery(query); + this.raiseForStatus(queryResponse); + return this.parseQueryResponse(queryResponse); + } + + async runQuery(query: SmartContractQuery): Promise { + const queryResponse = await this.networkProvider.queryContract(query); + return queryResponse; + } + + createQuery(options: SmartContractQueryInput): SmartContractQuery { + const preparedArguments = this.encodeArguments(options.function, options.arguments); + + return new SmartContractQuery({ + contract: options.contract, + caller: options.caller, + function: options.function, + arguments: preparedArguments, + value: options.value, + }); + } + + private raiseForStatus(queryResponse: SmartContractQueryResponse): void { + const isOk = queryResponse.returnCode === "ok"; + if (!isOk) { + throw new ErrSmartContractQuery(queryResponse.returnCode, queryResponse.returnMessage); + } + } + + parseQueryResponse(response: SmartContractQueryResponse): any[] { + if (!this.abi) { + return response.returnDataParts; + } + + const argsSerializer = new ArgSerializer(); + const functionName = response.function; + const endpoint = this.abi.getEndpoint(functionName); + const parts = response.returnDataParts.map((part) => Buffer.from(part)); + + let values = argsSerializer.buffersToValues(parts, endpoint.output); + + return values.map((value) => value.valueOf()); + } + + private encodeArguments(functionName: string, args: any[]): Uint8Array[] { + const endpoint = this.abi?.getEndpoint(functionName); + + if (endpoint) { + const typedArgs = NativeSerializer.nativeToTypedValues(args, endpoint); + return new ArgSerializer().valuesToBuffers(typedArgs); + } + + if (this.areArgsOfTypedValue(args)) { + return new ArgSerializer().valuesToBuffers(args); + } + + if (this.areArgsBuffers(args)) { + return args.map((arg) => Buffer.from(arg)); + } + + throw new Err( + "cannot encode arguments: when ABI is not available, they must be either typed values or buffers", + ); + } + + private areArgsOfTypedValue(args: any[]): boolean { + return args.every((arg) => isTyped(arg)); + } + + private areArgsBuffers(args: any[]): boolean { + for (const arg of args) { + if (!ArrayBuffer.isView(arg)) { + return false; + } + } + + return true; + } +} diff --git a/src/transactionsFactories/smartContractTransactionsFactory.spec.ts b/src/smartContracts/smartContractTransactionsFactory.spec.ts similarity index 67% rename from src/transactionsFactories/smartContractTransactionsFactory.spec.ts rename to src/smartContracts/smartContractTransactionsFactory.spec.ts index 096c37ea7..217d2b511 100644 --- a/src/transactionsFactories/smartContractTransactionsFactory.spec.ts +++ b/src/smartContracts/smartContractTransactionsFactory.spec.ts @@ -1,20 +1,15 @@ import { assert } from "chai"; -import { Address } from "../address"; -import { Err } from "../errors"; -import { U32Value } from "../smartcontracts"; -import { Code } from "../smartcontracts/code"; -import { AbiRegistry } from "../smartcontracts/typesystem/abiRegistry"; +import { Abi, Code, U32Value } from "../abi"; +import { Address, Err, Token, TokenTransfer, TransactionsFactoryConfig } from "../core"; import { loadAbiRegistry, loadContractCode } from "../testutils/utils"; -import { Token, TokenTransfer } from "../tokens"; import { SmartContractTransactionsFactory } from "./smartContractTransactionsFactory"; -import { TransactionsFactoryConfig } from "./transactionsFactoryConfig"; describe("test smart contract transactions factory", function () { const config = new TransactionsFactoryConfig({ chainID: "D" }); let factory: SmartContractTransactionsFactory; let abiAwareFactory: SmartContractTransactionsFactory; let bytecode: Code; - let abi: AbiRegistry; + let abi: Abi; before(async function () { factory = new SmartContractTransactionsFactory({ @@ -31,14 +26,13 @@ describe("test smart contract transactions factory", function () { }); it("should throw error when args are not of type 'TypedValue'", async function () { - const sender = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + const sender = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); const gasLimit = 6000000n; const args = [0]; assert.throws( () => - factory.createTransactionForDeploy({ - sender: sender, + factory.createTransactionForDeploy(sender, { bytecode: bytecode.valueOf(), gasLimit: gasLimit, arguments: args, @@ -49,26 +43,30 @@ describe("test smart contract transactions factory", function () { }); it("should create 'Transaction' for deploy", async function () { - const sender = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + const sender = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); const gasLimit = 6000000n; const args = [new U32Value(1)]; - const transaction = factory.createTransactionForDeploy({ - sender: sender, + const transaction = factory.createTransactionForDeploy(sender, { bytecode: bytecode.valueOf(), gasLimit: gasLimit, arguments: args, }); - const transactionAbiAware = abiAwareFactory.createTransactionForDeploy({ - sender: sender, + const transactionAbiAware = abiAwareFactory.createTransactionForDeploy(sender, { bytecode: bytecode.valueOf(), gasLimit: gasLimit, arguments: args, }); - assert.equal(transaction.sender, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - assert.equal(transaction.receiver, "erd1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6gq4hu"); + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + ); + assert.deepEqual( + transaction.receiver, + Address.newFromBech32("erd1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6gq4hu"), + ); assert.deepEqual(transaction.data, Buffer.from(`${bytecode}@0500@0504@01`)); assert.equal(transaction.gasLimit.valueOf(), gasLimit); assert.equal(transaction.value, 0n); @@ -77,30 +75,34 @@ describe("test smart contract transactions factory", function () { }); it("should create 'Transaction' for execute without transfer", async function () { - const sender = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - const contract = Address.fromBech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); + const sender = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + const contract = Address.newFromBech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); const func = "add"; const gasLimit = 6000000n; const args = [new U32Value(7)]; - const transaction = factory.createTransactionForExecute({ - sender: sender, + const transaction = factory.createTransactionForExecute(sender, { contract: contract, function: func, gasLimit: gasLimit, arguments: args, }); - const transactionAbiAware = abiAwareFactory.createTransactionForExecute({ - sender: sender, + const transactionAbiAware = abiAwareFactory.createTransactionForExecute(sender, { contract: contract, function: func, gasLimit: gasLimit, arguments: args, }); - assert.equal(transaction.sender, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - assert.equal(transaction.receiver, "erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + ); + assert.deepEqual( + transaction.receiver, + Address.newFromBech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"), + ); assert.deepEqual(transaction.data, Buffer.from("add@07")); assert.equal(transaction.gasLimit, gasLimit); assert.equal(transaction.value, 0n); @@ -109,14 +111,13 @@ describe("test smart contract transactions factory", function () { }); it("should create 'Transaction' for execute and transfer native token", async function () { - const sender = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - const contract = Address.fromBech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); + const sender = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + const contract = Address.newFromBech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); const func = "add"; const gasLimit = 6000000n; const egldAmount = 1000000000000000000n; - const transaction = factory.createTransactionForExecute({ - sender: sender, + const transaction = factory.createTransactionForExecute(sender, { contract: contract, function: func, gasLimit: gasLimit, @@ -124,8 +125,7 @@ describe("test smart contract transactions factory", function () { nativeTransferAmount: egldAmount, }); - const transactionAbiAware = abiAwareFactory.createTransactionForExecute({ - sender: sender, + const transactionAbiAware = abiAwareFactory.createTransactionForExecute(sender, { contract: contract, function: func, gasLimit: gasLimit, @@ -133,8 +133,14 @@ describe("test smart contract transactions factory", function () { nativeTransferAmount: egldAmount, }); - assert.equal(transaction.sender, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - assert.equal(transaction.receiver, "erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + ); + assert.deepEqual( + transaction.receiver, + Address.newFromBech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"), + ); assert.deepEqual(transaction.data, Buffer.from("add@07")); assert.equal(transaction.gasLimit, gasLimit); assert.equal(transaction.value, 1000000000000000000n); @@ -143,16 +149,15 @@ describe("test smart contract transactions factory", function () { }); it("should create 'Transaction' for execute and transfer single esdt", async function () { - const sender = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - const contract = Address.fromBech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); + const sender = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + const contract = Address.newFromBech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); const func = "add"; const gasLimit = 6000000n; const args = [new U32Value(7)]; const token = new Token({ identifier: "FOO-6ce17b", nonce: 0n }); const transfer = new TokenTransfer({ token, amount: 10n }); - const transaction = factory.createTransactionForExecute({ - sender: sender, + const transaction = factory.createTransactionForExecute(sender, { contract: contract, function: func, gasLimit: gasLimit, @@ -160,8 +165,7 @@ describe("test smart contract transactions factory", function () { tokenTransfers: [transfer], }); - const transactionAbiAware = abiAwareFactory.createTransactionForExecute({ - sender: sender, + const transactionAbiAware = abiAwareFactory.createTransactionForExecute(sender, { contract: contract, function: func, gasLimit: gasLimit, @@ -169,8 +173,14 @@ describe("test smart contract transactions factory", function () { tokenTransfers: [transfer], }); - assert.equal(transaction.sender, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - assert.equal(transaction.receiver, "erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + ); + assert.deepEqual( + transaction.receiver, + Address.newFromBech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"), + ); assert.deepEqual(transaction.data, Buffer.from("ESDTTransfer@464f4f2d366365313762@0a@616464@07")); assert.equal(transaction.gasLimit, gasLimit); assert.equal(transaction.value, 0n); @@ -179,8 +189,8 @@ describe("test smart contract transactions factory", function () { }); it("should create 'Transaction' for execute and transfer multiple esdts", async function () { - const sender = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - const contract = Address.fromBech32("erd1qqqqqqqqqqqqqpgqak8zt22wl2ph4tswtyc39namqx6ysa2sd8ss4xmlj3"); + const sender = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + const contract = Address.newFromBech32("erd1qqqqqqqqqqqqqpgqak8zt22wl2ph4tswtyc39namqx6ysa2sd8ss4xmlj3"); const func = "add"; const gasLimit = 6000000n; const args = [new U32Value(7)]; @@ -190,8 +200,7 @@ describe("test smart contract transactions factory", function () { const barToken = new Token({ identifier: "BAR-5bc08f", nonce: 0n }); const barTransfer = new TokenTransfer({ token: barToken, amount: 3140n }); - const transaction = factory.createTransactionForExecute({ - sender: sender, + const transaction = factory.createTransactionForExecute(sender, { contract: contract, function: func, gasLimit: gasLimit, @@ -199,8 +208,7 @@ describe("test smart contract transactions factory", function () { tokenTransfers: [fooTransfer, barTransfer], }); - const transactionAbiAware = abiAwareFactory.createTransactionForExecute({ - sender: sender, + const transactionAbiAware = abiAwareFactory.createTransactionForExecute(sender, { contract: contract, function: func, gasLimit: gasLimit, @@ -208,8 +216,14 @@ describe("test smart contract transactions factory", function () { tokenTransfers: [fooTransfer, barTransfer], }); - assert.equal(transaction.sender, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - assert.equal(transaction.receiver, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + ); + assert.deepEqual( + transaction.receiver, + Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + ); assert.deepEqual( transaction.data, @@ -225,8 +239,8 @@ describe("test smart contract transactions factory", function () { }); it("should create 'Transaction' for execute and transfer single nft", async function () { - const sender = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - const contract = Address.fromBech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); + const sender = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + const contract = Address.newFromBech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); const func = "add"; const gasLimit = 6000000n; const args = [new U32Value(7)]; @@ -234,8 +248,7 @@ describe("test smart contract transactions factory", function () { const token = new Token({ identifier: "NFT-123456", nonce: 1n }); const transfer = new TokenTransfer({ token, amount: 1n }); - const transaction = factory.createTransactionForExecute({ - sender: sender, + const transaction = factory.createTransactionForExecute(sender, { contract: contract, function: func, gasLimit: gasLimit, @@ -243,8 +256,7 @@ describe("test smart contract transactions factory", function () { tokenTransfers: [transfer], }); - const transactionAbiAware = abiAwareFactory.createTransactionForExecute({ - sender: sender, + const transactionAbiAware = abiAwareFactory.createTransactionForExecute(sender, { contract: contract, function: func, gasLimit: gasLimit, @@ -252,8 +264,14 @@ describe("test smart contract transactions factory", function () { tokenTransfers: [transfer], }); - assert.equal(transaction.sender, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - assert.equal(transaction.receiver, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + ); + assert.deepEqual( + transaction.receiver, + Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + ); assert.isDefined(transaction.data); assert.deepEqual( @@ -270,8 +288,8 @@ describe("test smart contract transactions factory", function () { }); it("should create 'Transaction' for execute and transfer multiple nfts", async function () { - const sender = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - const contract = Address.fromBech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); + const sender = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + const contract = Address.newFromBech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); const func = "add"; const gasLimit = 6000000n; const args = [new U32Value(7)]; @@ -281,8 +299,7 @@ describe("test smart contract transactions factory", function () { const secondToken = new Token({ identifier: "NFT-123456", nonce: 42n }); const secondTransfer = new TokenTransfer({ token: secondToken, amount: 1n }); - const transaction = factory.createTransactionForExecute({ - sender: sender, + const transaction = factory.createTransactionForExecute(sender, { contract: contract, function: func, gasLimit: gasLimit, @@ -290,8 +307,7 @@ describe("test smart contract transactions factory", function () { tokenTransfers: [firstTransfer, secondTransfer], }); - const transactionAbiAware = abiAwareFactory.createTransactionForExecute({ - sender: sender, + const transactionAbiAware = abiAwareFactory.createTransactionForExecute(sender, { contract: contract, function: func, gasLimit: gasLimit, @@ -299,8 +315,14 @@ describe("test smart contract transactions factory", function () { tokenTransfers: [firstTransfer, secondTransfer], }); - assert.equal(transaction.sender, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - assert.equal(transaction.receiver, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + ); + assert.deepEqual( + transaction.receiver, + Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + ); assert.isDefined(transaction.data); assert.deepEqual( @@ -317,8 +339,8 @@ describe("test smart contract transactions factory", function () { }); it("should create 'Transaction' for execute and transfer native and nfts", async function () { - const sender = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - const contract = Address.fromBech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); + const sender = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + const contract = Address.newFromBech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); const func = "add"; const gasLimit = 6000000n; const args = [new U32Value(7)]; @@ -328,8 +350,7 @@ describe("test smart contract transactions factory", function () { const secondToken = new Token({ identifier: "NFT-123456", nonce: 42n }); const secondTransfer = new TokenTransfer({ token: secondToken, amount: 1n }); - const transaction = factory.createTransactionForExecute({ - sender: sender, + const transaction = factory.createTransactionForExecute(sender, { contract: contract, function: func, gasLimit: gasLimit, @@ -338,8 +359,7 @@ describe("test smart contract transactions factory", function () { tokenTransfers: [firstTransfer, secondTransfer], }); - const transactionAbiAware = abiAwareFactory.createTransactionForExecute({ - sender: sender, + const transactionAbiAware = abiAwareFactory.createTransactionForExecute(sender, { contract: contract, function: func, gasLimit: gasLimit, @@ -348,8 +368,14 @@ describe("test smart contract transactions factory", function () { tokenTransfers: [firstTransfer, secondTransfer], }); - assert.equal(transaction.sender, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - assert.equal(transaction.receiver, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + ); + assert.deepEqual( + transaction.receiver, + Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + ); assert.isDefined(transaction.data); assert.deepEqual( @@ -366,29 +392,33 @@ describe("test smart contract transactions factory", function () { }); it("should create 'Transaction' for upgrade", async function () { - const sender = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - const contract = Address.fromBech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); + const sender = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + const contract = Address.newFromBech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); const gasLimit = 6000000n; const args = [new U32Value(7)]; - const transaction = factory.createTransactionForUpgrade({ - sender: sender, + const transaction = factory.createTransactionForUpgrade(sender, { contract: contract, bytecode: bytecode.valueOf(), gasLimit: gasLimit, arguments: args, }); - const transactionAbiAware = abiAwareFactory.createTransactionForUpgrade({ - sender: sender, + const transactionAbiAware = abiAwareFactory.createTransactionForUpgrade(sender, { contract: contract, bytecode: bytecode.valueOf(), gasLimit: gasLimit, arguments: args, }); - assert.equal(transaction.sender, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - assert.equal(transaction.receiver, "erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + ); + assert.deepEqual( + transaction.receiver, + Address.newFromBech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"), + ); assert.deepEqual(transaction.data!, Buffer.from(`upgradeContract@${bytecode}@0504@07`)); assert.equal(transaction.gasLimit, gasLimit); assert.equal(transaction.value, 0n); @@ -397,7 +427,7 @@ describe("test smart contract transactions factory", function () { }); it("should create 'Transaction' for upgrade, when ABI is available (with fallbacks)", async function () { - const abi = AbiRegistry.create({ + const abi = Abi.create({ upgradeConstructor: { inputs: [ { @@ -439,13 +469,12 @@ describe("test smart contract transactions factory", function () { }); const bytecode = Buffer.from("abba", "hex"); - const sender = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - const receiver = Address.fromBech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); + const sender = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + const receiver = Address.newFromBech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); const gasLimit = 6000000n; // By default, use the upgrade constructor. - const tx1 = factory.createTransactionForUpgrade({ - sender: sender, + const tx1 = factory.createTransactionForUpgrade(sender, { contract: receiver, bytecode: bytecode, gasLimit: gasLimit, @@ -457,8 +486,7 @@ describe("test smart contract transactions factory", function () { // Fallback to the "upgrade" endpoint. (abi).upgradeConstructorDefinition = undefined; - const tx2 = factory.createTransactionForUpgrade({ - sender: sender, + const tx2 = factory.createTransactionForUpgrade(sender, { contract: receiver, bytecode: bytecode, gasLimit: gasLimit, @@ -470,8 +498,7 @@ describe("test smart contract transactions factory", function () { // Fallback to the constructor. (abi).endpoints.length = 0; - const tx3 = factory.createTransactionForUpgrade({ - sender: sender, + const tx3 = factory.createTransactionForUpgrade(sender, { contract: receiver, bytecode: bytecode, gasLimit: gasLimit, @@ -485,8 +512,7 @@ describe("test smart contract transactions factory", function () { assert.throws( () => - factory.createTransactionForUpgrade({ - sender: sender, + factory.createTransactionForUpgrade(sender, { contract: receiver, bytecode: bytecode, gasLimit: gasLimit, @@ -497,25 +523,31 @@ describe("test smart contract transactions factory", function () { }); it("should create 'Transaction' for claiming developer rewards", async function () { - const sender = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - const contract = Address.fromBech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); + const sender = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + const contract = Address.newFromBech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); const transaction = factory.createTransactionForClaimingDeveloperRewards({ sender: sender, contract: contract, }); - assert.equal(transaction.sender, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - assert.equal(transaction.receiver, "erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + ); + assert.deepEqual( + transaction.receiver, + Address.newFromBech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"), + ); assert.equal(Buffer.from(transaction.data).toString(), "ClaimDeveloperRewards"); assert.equal(transaction.gasLimit, 6000000n); assert.equal(transaction.value, 0n); }); it("should create 'Transaction' for changing owner address", async function () { - const sender = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - const contract = Address.fromBech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); - const newOwner = Address.fromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + const sender = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + const contract = Address.newFromBech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); + const newOwner = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); const transaction = factory.createTransactionForChangingOwnerAddress({ sender: sender, @@ -523,8 +555,14 @@ describe("test smart contract transactions factory", function () { newOwner: newOwner, }); - assert.equal(transaction.sender, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - assert.equal(transaction.receiver, "erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"); + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + ); + assert.deepEqual( + transaction.receiver, + Address.newFromBech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4"), + ); assert.equal( Buffer.from(transaction.data).toString(), "ChangeOwnerAddress@8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8", diff --git a/src/transactionsFactories/smartContractTransactionsFactory.ts b/src/smartContracts/smartContractTransactionsFactory.ts similarity index 74% rename from src/transactionsFactories/smartContractTransactionsFactory.ts rename to src/smartContracts/smartContractTransactionsFactory.ts index 6102ebac5..521a8f600 100644 --- a/src/transactionsFactories/smartContractTransactionsFactory.ts +++ b/src/smartContracts/smartContractTransactionsFactory.ts @@ -1,16 +1,14 @@ -import { Address } from "../address"; -import { CONTRACT_DEPLOY_ADDRESS_HEX, VM_TYPE_WASM_VM } from "../constants"; -import { Err } from "../errors"; -import { IAddress } from "../interface"; -import { Logger } from "../logger"; -import { ArgSerializer, CodeMetadata, ContractFunction, EndpointDefinition } from "../smartcontracts"; -import { NativeSerializer } from "../smartcontracts/nativeSerializer"; -import { isTyped } from "../smartcontracts/typesystem"; -import { TokenComputer, TokenTransfer } from "../tokens"; -import { Transaction } from "../transaction"; -import { byteArrayToHex, utf8ToHex } from "../utils.codec"; -import { TokenTransfersDataBuilder } from "./tokenTransfersDataBuilder"; -import { TransactionBuilder } from "./transactionBuilder"; +import { ArgSerializer, ContractFunction, EndpointDefinition, isTyped, NativeSerializer } from "../abi"; +import { Address, CodeMetadata } from "../core"; +import { CONTRACT_DEPLOY_ADDRESS_HEX, VM_TYPE_WASM_VM } from "../core/constants"; +import { Err } from "../core/errors"; +import { Logger } from "../core/logger"; +import { TokenComputer, TokenTransfer } from "../core/tokens"; +import { TokenTransfersDataBuilder } from "../core/tokenTransfersDataBuilder"; +import { Transaction } from "../core/transaction"; +import { TransactionBuilder } from "../core/transactionBuilder"; +import { byteArrayToHex, utf8ToHex } from "../core/utils.codec"; +import * as resources from "./resources"; interface IConfig { chainID: string; @@ -43,20 +41,10 @@ export class SmartContractTransactionsFactory { this.abi = options.abi; this.tokenComputer = new TokenComputer(); this.dataArgsBuilder = new TokenTransfersDataBuilder(); - this.contractDeployAddress = Address.fromHex(CONTRACT_DEPLOY_ADDRESS_HEX, this.config.addressHrp); + this.contractDeployAddress = Address.newFromHex(CONTRACT_DEPLOY_ADDRESS_HEX, this.config.addressHrp); } - createTransactionForDeploy(options: { - sender: IAddress; - bytecode: Uint8Array; - gasLimit: bigint; - arguments?: any[]; - nativeTransferAmount?: bigint; - isUpgradeable?: boolean; - isReadable?: boolean; - isPayable?: boolean; - isPayableBySmartContract?: boolean; - }): Transaction { + createTransactionForDeploy(sender: Address, options: resources.ContractDeployInput): Transaction { const nativeTransferAmount = options.nativeTransferAmount ?? 0n; const isUpgradeable = options.isUpgradeable ?? true; const isReadable = options.isReadable ?? true; @@ -72,7 +60,7 @@ export class SmartContractTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, + sender: sender, receiver: this.contractDeployAddress, dataParts: dataParts, gasLimit: options.gasLimit, @@ -81,22 +69,14 @@ export class SmartContractTransactionsFactory { }).build(); } - createTransactionForExecute(options: { - sender: IAddress; - contract: IAddress; - function: string; - gasLimit: bigint; - arguments?: any[]; - nativeTransferAmount?: bigint; - tokenTransfers?: TokenTransfer[]; - }): Transaction { + createTransactionForExecute(sender: Address, options: resources.ContractExecuteInput): Transaction { const args = options.arguments || []; let tokenTransfers = options.tokenTransfers ? [...options.tokenTransfers] : []; let nativeTransferAmount = options.nativeTransferAmount ?? 0n; let numberOfTokens = tokenTransfers.length; if (nativeTransferAmount && numberOfTokens) { - tokenTransfers.push(TokenTransfer.newFromEgldAmount(nativeTransferAmount)); + tokenTransfers.push(TokenTransfer.newFromNativeAmount(nativeTransferAmount)); nativeTransferAmount = 0n; numberOfTokens++; } @@ -111,22 +91,23 @@ export class SmartContractTransactionsFactory { dataParts = this.dataArgsBuilder.buildDataPartsForESDTTransfer(transfer); } else { dataParts = this.dataArgsBuilder.buildDataPartsForSingleESDTNFTTransfer(transfer, receiver); - receiver = options.sender; + receiver = sender; } } else if (numberOfTokens > 1) { dataParts = this.dataArgsBuilder.buildDataPartsForMultiESDTNFTTransfer(receiver, tokenTransfers); - receiver = options.sender; + receiver = sender; } dataParts.push(dataParts.length ? utf8ToHex(options.function) : options.function); const endpoint = this.abi?.getEndpoint(options.function); + const preparedArgs = this.argsToDataParts(args, endpoint); dataParts.push(...preparedArgs); return new TransactionBuilder({ config: this.config, - sender: options.sender, + sender: sender, receiver: receiver, dataParts: dataParts, gasLimit: options.gasLimit, @@ -135,18 +116,7 @@ export class SmartContractTransactionsFactory { }).build(); } - createTransactionForUpgrade(options: { - sender: IAddress; - contract: IAddress; - bytecode: Uint8Array; - gasLimit: bigint; - arguments?: any[]; - nativeTransferAmount?: bigint; - isUpgradeable?: boolean; - isReadable?: boolean; - isPayable?: boolean; - isPayableBySmartContract?: boolean; - }): Transaction { + createTransactionForUpgrade(sender: Address, options: resources.ContractUpgradeInput): Transaction { const nativeTransferAmount = options.nativeTransferAmount ?? 0n; const isUpgradeable = options.isUpgradeable ?? true; @@ -164,7 +134,7 @@ export class SmartContractTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, + sender: sender, receiver: options.contract, dataParts: dataParts, gasLimit: options.gasLimit, @@ -195,7 +165,7 @@ export class SmartContractTransactionsFactory { } } - createTransactionForClaimingDeveloperRewards(options: { sender: IAddress; contract: IAddress }): Transaction { + createTransactionForClaimingDeveloperRewards(options: { sender: Address; contract: Address }): Transaction { const dataParts = ["ClaimDeveloperRewards"]; return new TransactionBuilder({ @@ -209,11 +179,11 @@ export class SmartContractTransactionsFactory { } createTransactionForChangingOwnerAddress(options: { - sender: IAddress; - contract: IAddress; - newOwner: IAddress; + sender: Address; + contract: Address; + newOwner: Address; }): Transaction { - const dataParts = ["ChangeOwnerAddress", Address.fromBech32(options.newOwner.bech32()).toHex()]; + const dataParts = ["ChangeOwnerAddress", options.newOwner.toHex()]; return new TransactionBuilder({ config: this.config, diff --git a/src/transactionsOutcomeParsers/smartContractTransactionsOutcomeParser.dev.net.spec.ts b/src/smartContracts/smartContractTransactionsOutcomeParser.dev.net.spec.ts similarity index 63% rename from src/transactionsOutcomeParsers/smartContractTransactionsOutcomeParser.dev.net.spec.ts rename to src/smartContracts/smartContractTransactionsOutcomeParser.dev.net.spec.ts index 6eb10aa4f..a76fe6b2c 100644 --- a/src/transactionsOutcomeParsers/smartContractTransactionsOutcomeParser.dev.net.spec.ts +++ b/src/smartContracts/smartContractTransactionsOutcomeParser.dev.net.spec.ts @@ -1,26 +1,22 @@ import { assert } from "chai"; -import { TransactionsConverter } from "../converters/transactionsConverter"; +import { Address } from "../core"; import { createDevnetProvider } from "../testutils/networkProviders"; import { SmartContractTransactionsOutcomeParser } from "./smartContractTransactionsOutcomeParser"; describe("test smart contract transactions outcome parser on devnet", () => { const networkProvider = createDevnetProvider(); const parser = new SmartContractTransactionsOutcomeParser(); - const transactionsConverter = new TransactionsConverter(); it("should parse outcome of deploy transactions (1)", async () => { const transactionHash = "5d2ff2af8eb3fe7f2acb7e29c0436854b4c6c44de02878b6afff582888024a55"; const transactionOnNetwork = await networkProvider.getTransaction(transactionHash); - const transactionOutcome = transactionsConverter.transactionOnNetworkToOutcome(transactionOnNetwork); const parsedGivenTransactionOnNetwork = parser.parseDeploy({ transactionOnNetwork }); - const parsedGivenTransactionOutcome = parser.parseDeploy({ transactionOutcome }); - assert.deepEqual(parsedGivenTransactionOnNetwork, parsedGivenTransactionOutcome); assert.equal(parsedGivenTransactionOnNetwork.returnCode, "ok"); assert.deepEqual(parsedGivenTransactionOnNetwork.contracts, [ { - address: "erd1qqqqqqqqqqqqqpgqpayq2es08gq8798xhnpr0kzgn7495qt5q6uqd7lpwf", - ownerAddress: "erd1tn62hjp72rznp8vq0lplva5csav6rccpqqdungpxtqz0g2hcq6uq9k4cc6", + address: Address.newFromBech32("erd1qqqqqqqqqqqqqpgqpayq2es08gq8798xhnpr0kzgn7495qt5q6uqd7lpwf"), + ownerAddress: Address.newFromBech32("erd1tn62hjp72rznp8vq0lplva5csav6rccpqqdungpxtqz0g2hcq6uq9k4cc6"), codeHash: Buffer.from("c876625ec34a04445cfd99067777ebe488afdbc6899cd958f4c1d36107ca02d9", "hex"), }, ]); @@ -29,11 +25,8 @@ describe("test smart contract transactions outcome parser on devnet", () => { it("should parse outcome of deploy transactions (2)", async () => { const transactionHash = "76683e926dad142fc9651afca208487f2a80d327fc87e5c876eec9d028196352"; const transactionOnNetwork = await networkProvider.getTransaction(transactionHash); - const transactionOutcome = transactionsConverter.transactionOnNetworkToOutcome(transactionOnNetwork); const parsedGivenTransactionOnNetwork = parser.parseDeploy({ transactionOnNetwork }); - const parsedGivenTransactionOutcome = parser.parseDeploy({ transactionOutcome }); - assert.deepEqual(parsedGivenTransactionOnNetwork, parsedGivenTransactionOutcome); assert.equal(parsedGivenTransactionOnNetwork.returnCode, "execution failed"); assert.lengthOf(parsedGivenTransactionOnNetwork.contracts, 0); }); diff --git a/src/transactionsOutcomeParsers/smartContractTransactionsOutcomeParser.main.net.spec.ts b/src/smartContracts/smartContractTransactionsOutcomeParser.main.net.spec.ts similarity index 77% rename from src/transactionsOutcomeParsers/smartContractTransactionsOutcomeParser.main.net.spec.ts rename to src/smartContracts/smartContractTransactionsOutcomeParser.main.net.spec.ts index 00ee27737..81bfe6b4e 100644 --- a/src/transactionsOutcomeParsers/smartContractTransactionsOutcomeParser.main.net.spec.ts +++ b/src/smartContracts/smartContractTransactionsOutcomeParser.main.net.spec.ts @@ -1,13 +1,11 @@ import { assert } from "chai"; import { promises } from "fs"; -import { TransactionsConverter } from "../converters/transactionsConverter"; import { createMainnetProvider } from "../testutils/networkProviders"; import { SmartContractTransactionsOutcomeParser } from "./smartContractTransactionsOutcomeParser"; describe("test smart contract transactions outcome parser on mainnet", () => { const networkProvider = createMainnetProvider(); const parser = new SmartContractTransactionsOutcomeParser(); - const converter = new TransactionsConverter(); it("should parse (execute_success)", async function () { this.timeout(3600000); @@ -19,11 +17,8 @@ describe("test smart contract transactions outcome parser on mainnet", () => { console.log(i, hash); const transactionOnNetwork = await networkProvider.getTransaction(hash); - const transactionOutcome = converter.transactionOnNetworkToOutcome(transactionOnNetwork); - const parsedOutcomeGivenTransactionOutcome = parser.parseExecute({ transactionOutcome }); const parsedOutcomeGivenTransactionOnNetwork = parser.parseExecute({ transactionOnNetwork }); - assert.deepEqual(parsedOutcomeGivenTransactionOutcome, parsedOutcomeGivenTransactionOnNetwork); assert.equal(parsedOutcomeGivenTransactionOnNetwork.returnCode, "ok"); assert.equal(parsedOutcomeGivenTransactionOnNetwork.returnMessage, "ok"); } @@ -39,11 +34,8 @@ describe("test smart contract transactions outcome parser on mainnet", () => { console.log(i, hash); const transactionOnNetwork = await networkProvider.getTransaction(hash); - const transactionOutcome = converter.transactionOnNetworkToOutcome(transactionOnNetwork); - const parsedOutcomeGivenTransactionOutcome = parser.parseExecute({ transactionOutcome }); const parsedOutcomeGivenTransactionOnNetwork = parser.parseExecute({ transactionOnNetwork }); - assert.deepEqual(parsedOutcomeGivenTransactionOutcome, parsedOutcomeGivenTransactionOnNetwork); assert.isTrue(parsedOutcomeGivenTransactionOnNetwork.returnCode.length > 0); assert.isTrue(parsedOutcomeGivenTransactionOnNetwork.returnMessage.length > 0); assert.lengthOf(parsedOutcomeGivenTransactionOnNetwork.values, 0); @@ -60,11 +52,8 @@ describe("test smart contract transactions outcome parser on mainnet", () => { console.log(i, hash); const transactionOnNetwork = await networkProvider.getTransaction(hash); - const transactionOutcome = converter.transactionOnNetworkToOutcome(transactionOnNetwork); - const parsedOutcomeGivenTransactionOutcome = parser.parseExecute({ transactionOutcome }); const parsedOutcomeGivenTransactionOnNetwork = parser.parseExecute({ transactionOnNetwork }); - assert.deepEqual(parsedOutcomeGivenTransactionOutcome, parsedOutcomeGivenTransactionOnNetwork); assert.equal(parsedOutcomeGivenTransactionOnNetwork.returnCode, "ok"); assert.equal(parsedOutcomeGivenTransactionOnNetwork.returnMessage, "ok"); } @@ -80,11 +69,8 @@ describe("test smart contract transactions outcome parser on mainnet", () => { console.log(i, hash); const transactionOnNetwork = await networkProvider.getTransaction(hash); - const transactionOutcome = converter.transactionOnNetworkToOutcome(transactionOnNetwork); - const parsedOutcomeGivenTransactionOutcome = parser.parseExecute({ transactionOutcome }); const parsedOutcomeGivenTransactionOnNetwork = parser.parseExecute({ transactionOnNetwork }); - assert.deepEqual(parsedOutcomeGivenTransactionOutcome, parsedOutcomeGivenTransactionOnNetwork); assert.isTrue(parsedOutcomeGivenTransactionOnNetwork.returnCode.length > 0); assert.isTrue(parsedOutcomeGivenTransactionOnNetwork.returnMessage.length > 0); assert.lengthOf(parsedOutcomeGivenTransactionOnNetwork.values, 0); @@ -136,11 +122,8 @@ describe("test smart contract transactions outcome parser on mainnet", () => { console.log(i, hash); const transactionOnNetwork = await networkProvider.getTransaction(hash); - const transactionOutcome = converter.transactionOnNetworkToOutcome(transactionOnNetwork); - const parsedOutcomeGivenTransactionOutcome = parser.parseExecute({ transactionOutcome }); const parsedOutcomeGivenTransactionOnNetwork = parser.parseExecute({ transactionOnNetwork }); - assert.deepEqual(parsedOutcomeGivenTransactionOutcome, parsedOutcomeGivenTransactionOnNetwork); assert.equal(parsedOutcomeGivenTransactionOnNetwork.returnCode, "ok"); assert.equal(parsedOutcomeGivenTransactionOnNetwork.returnMessage, "ok"); assert.lengthOf(parsedOutcomeGivenTransactionOnNetwork.values, 0); diff --git a/src/smartContracts/smartContractTransactionsOutcomeParser.spec.ts b/src/smartContracts/smartContractTransactionsOutcomeParser.spec.ts new file mode 100644 index 000000000..649d4b2a9 --- /dev/null +++ b/src/smartContracts/smartContractTransactionsOutcomeParser.spec.ts @@ -0,0 +1,121 @@ +import BigNumber from "bignumber.js"; +import { assert } from "chai"; +import { Address, TransactionEvent, TransactionLogs, TransactionOnNetwork } from "../core"; +import { b64TopicsToBytes, loadAbiRegistry } from "../testutils"; +import { SmartContractResult } from "../transactionsOutcomeParsers"; +import { SmartContractTransactionsOutcomeParser } from "./smartContractTransactionsOutcomeParser"; + +describe("test smart contract transactions outcome parser", () => { + it("parses deploy outcome", async function () { + const contract = Address.newFromBech32("erd1qqqqqqqqqqqqqpgqqacl85rd0gl2q8wggl8pwcyzcr4fflc5d8ssve45cj"); + const deployer = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + const codeHash = Buffer.from("abba", "hex"); + + const parser = new SmartContractTransactionsOutcomeParser(); + + const transactionOnNetwork = new TransactionOnNetwork({ + nonce: 7n, + logs: new TransactionLogs({ + events: [ + new TransactionEvent({ + identifier: "SCDeploy", + topics: [ + new Uint8Array(Buffer.from(contract.getPublicKey().toString("base64"), "base64")), + new Uint8Array(Buffer.from(deployer.getPublicKey().toString("base64"), "base64")), + new Uint8Array(Buffer.from(codeHash.toString("base64"), "base64")), + ], + }), + ], + }), + smartContractResults: [new SmartContractResult({ data: Buffer.from("@6f6b") })], + }); + + const parsed = parser.parseDeploy({ transactionOnNetwork }); + + assert.equal(parsed.returnCode, "ok"); + assert.equal(parsed.returnMessage, "ok"); + assert.deepEqual(parsed.contracts, [ + { + address: contract, + ownerAddress: deployer, + codeHash: codeHash, + }, + ]); + }); + + it("parses deploy outcome (with error)", async function () { + const deployer = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + + const parser = new SmartContractTransactionsOutcomeParser(); + + const transactionOnNetwork = new TransactionOnNetwork({ + nonce: 7n, + logs: new TransactionLogs({ + events: [ + new TransactionEvent({ + identifier: "signalError", + topics: b64TopicsToBytes([ + deployer.getPublicKey().toString("base64"), + Buffer.from("wrong number of arguments").toString("base64"), + ]), + data: Buffer.from("QDc1NzM2NTcyMjA2NTcyNzI2Zjcy", "base64"), + }), + ], + }), + }); + + const parsed = parser.parseDeploy({ transactionOnNetwork }); + + assert.equal(parsed.returnCode, "user error"); + assert.equal(parsed.returnMessage, "wrong number of arguments"); + assert.deepEqual(parsed.contracts, []); + }); + + it("parses execute outcome, without ABI", function () { + const parser = new SmartContractTransactionsOutcomeParser(); + const transactionOnNetwork = new TransactionOnNetwork({ + nonce: 7n, + smartContractResults: [new SmartContractResult({ data: Buffer.from("@6f6b@2a") })], + }); + + const parsed = parser.parseExecute({ transactionOnNetwork }); + + assert.deepEqual(parsed.values, [Buffer.from([42])]); + assert.equal(parsed.returnCode, "ok"); + assert.equal(parsed.returnMessage, "ok"); + }); + + it("parses execute outcome, with ABI", async function () { + const parser = new SmartContractTransactionsOutcomeParser({ + abi: await loadAbiRegistry("src/testdata/answer.abi.json"), + }); + + const transactionOnNetwork = new TransactionOnNetwork({ + nonce: 7n, + function: "getUltimateAnswer", + smartContractResults: [new SmartContractResult({ data: Buffer.from("@6f6b@2a") })], + }); + + const parsed = parser.parseExecute({ transactionOnNetwork }); + + // At this moment, U64Value.valueOf() returns a BigNumber. This might change in the future. + assert.deepEqual(parsed.values, [new BigNumber("42")]); + assert.equal(parsed.returnCode, "ok"); + assert.equal(parsed.returnMessage, "ok"); + }); + + it("cannot parse execute outcome, with ABI, when function name is missing", async function () { + const parser = new SmartContractTransactionsOutcomeParser({ + abi: await loadAbiRegistry("src/testdata/answer.abi.json"), + }); + + const transactionOnNetwork = new TransactionOnNetwork({ + nonce: 7n, + smartContractResults: [new SmartContractResult({ data: Buffer.from("@6f6b@2a") })], + }); + + assert.throws(() => { + parser.parseExecute({ transactionOnNetwork }); + }, 'Function name is not available in the transaction, thus endpoint definition (ABI) cannot be picked (for parsing). Maybe provide the "function" parameter explicitly?'); + }); +}); diff --git a/src/transactionsOutcomeParsers/smartContractTransactionsOutcomeParser.ts b/src/smartContracts/smartContractTransactionsOutcomeParser.ts similarity index 50% rename from src/transactionsOutcomeParsers/smartContractTransactionsOutcomeParser.ts rename to src/smartContracts/smartContractTransactionsOutcomeParser.ts index 9f57b4782..75ba1e3dd 100644 --- a/src/transactionsOutcomeParsers/smartContractTransactionsOutcomeParser.ts +++ b/src/smartContracts/smartContractTransactionsOutcomeParser.ts @@ -1,16 +1,11 @@ -import { Address } from "../address"; -import { ARGUMENTS_SEPARATOR } from "../constants"; -import { Err } from "../errors"; -import { IContractResultItem, ITransactionEvent, ITransactionOnNetwork } from "../interfaceOfNetwork"; -import { - ArgSerializer, - EndpointDefinition, - ResultsParser, - ReturnCode, - Type, - UntypedOutcomeBundle, -} from "../smartcontracts"; -import { SmartContractCallOutcome, TransactionOutcome, findEventsByIdentifier } from "./resources"; +import { Abi, ArgSerializer } from "../abi"; +import { Address } from "../core/address"; +import { ARGUMENTS_SEPARATOR } from "../core/constants"; +import { Err } from "../core/errors"; +import { TransactionEvent } from "../core/transactionEvents"; +import { TransactionOnNetwork } from "../core/transactionOnNetwork"; +import { SmartContractCallOutcome, SmartContractResult } from "../transactionsOutcomeParsers/resources"; +import * as resources from "./resources"; enum Events { SCDeploy = "SCDeploy", @@ -18,93 +13,33 @@ enum Events { WriteLog = "writeLog", } -interface IAbi { - getEndpoint(name: string): EndpointDefinition; -} - -interface IParameterDefinition { - type: Type; -} - -interface ILegacyResultsParser { - parseOutcomeFromUntypedBundle( - bundle: UntypedOutcomeBundle, - endpoint: { output: IParameterDefinition[] }, - ): { - values: any[]; - returnCode: { valueOf(): string }; - returnMessage: string; - }; -} - export class SmartContractTransactionsOutcomeParser { - private readonly abi?: IAbi; - private readonly legacyResultsParser: ILegacyResultsParser; + private readonly abi?: Abi; - constructor(options?: { abi?: IAbi; legacyResultsParser?: ILegacyResultsParser }) { + constructor(options?: { abi?: Abi }) { this.abi = options?.abi; - this.legacyResultsParser = options?.legacyResultsParser || new ResultsParser(); - } - - parseDeploy( - options: { transactionOutcome: TransactionOutcome } | { transactionOnNetwork: ITransactionOnNetwork }, - ): { - returnCode: string; - returnMessage: string; - contracts: { - address: string; - ownerAddress: string; - codeHash: Uint8Array; - }[]; - } { - if ("transactionOutcome" in options) { - return this.parseDeployGivenTransactionOutcome(options.transactionOutcome); - } - - return this.parseDeployGivenTransactionOnNetwork(options.transactionOnNetwork); } /** - * Legacy approach. + * Parses the transaction and recovers basic information about the contract(s) deployed by the transaction. + * @param options */ - protected parseDeployGivenTransactionOutcome(transactionOutcome: TransactionOutcome): { - returnCode: string; - returnMessage: string; - contracts: { - address: string; - ownerAddress: string; - codeHash: Uint8Array; - }[]; - } { - const directCallOutcome = transactionOutcome.directSmartContractCallOutcome; - const events = findEventsByIdentifier(transactionOutcome, Events.SCDeploy); - const contracts = events.map((event) => this.parseScDeployEvent(event)); - - return { - returnCode: directCallOutcome.returnCode, - returnMessage: directCallOutcome.returnMessage, - contracts: contracts, - }; + parseDeploy(options: { transactionOnNetwork: TransactionOnNetwork }): resources.SmartContractDeployOutcome { + return this.parseDeployGivenTransactionOnNetwork(options.transactionOnNetwork); } - protected parseDeployGivenTransactionOnNetwork(transactionOnNetwork: ITransactionOnNetwork): { - returnCode: string; - returnMessage: string; - contracts: { - address: string; - ownerAddress: string; - codeHash: Uint8Array; - }[]; - } { + protected parseDeployGivenTransactionOnNetwork( + transactionOnNetwork: TransactionOnNetwork, + ): resources.SmartContractDeployOutcome { const directCallOutcome = this.findDirectSmartContractCallOutcome(transactionOnNetwork); const events = transactionOnNetwork.logs.events - .concat(transactionOnNetwork.contractResults.items.flatMap((result) => result.logs.events)) + .concat(transactionOnNetwork.smartContractResults.flatMap((result) => result.logs.events)) .filter((event) => event.identifier === Events.SCDeploy); const contracts = events.map((event) => this.parseScDeployEvent({ - topics: event.topics.map((topic) => Buffer.from(topic.hex(), "hex")), + topics: event.topics.map((topic) => Buffer.from(topic)), }), ); @@ -116,16 +51,15 @@ export class SmartContractTransactionsOutcomeParser { } private parseScDeployEvent(event: { topics: Uint8Array[] }): { - address: string; - ownerAddress: string; + address: Address; + ownerAddress: Address; codeHash: Uint8Array; } { - const topicForAddress = event.topics[0]; - const topicForOwnerAddress = event.topics[1]; - const topicForCodeHash = event.topics[2]; - - const address = topicForAddress?.length ? new Address(topicForAddress).toBech32() : ""; - const ownerAddress = topicForOwnerAddress?.length ? new Address(topicForOwnerAddress).toBech32() : ""; + const topicForAddress = Buffer.from(event.topics[0]).toString("hex"); + const topicForOwnerAddress = Buffer.from(event.topics[1]).toString("hex"); + const topicForCodeHash = Buffer.from(event.topics[2]); + const address = topicForAddress?.length ? new Address(topicForAddress) : Address.empty(); + const ownerAddress = topicForOwnerAddress?.length ? new Address(topicForOwnerAddress) : Address.empty(); const codeHash = topicForCodeHash; return { @@ -135,78 +69,18 @@ export class SmartContractTransactionsOutcomeParser { }; } - parseExecute( - options: - | { transactionOutcome: TransactionOutcome; function?: string } - | { transactionOnNetwork: ITransactionOnNetwork; function?: string }, - ): { - values: any[]; - returnCode: string; - returnMessage: string; - } { - if ("transactionOutcome" in options) { - return this.parseExecuteGivenTransactionOutcome(options.transactionOutcome, options.function); - } - + parseExecute(options: { + transactionOnNetwork: TransactionOnNetwork; + function?: string; + }): resources.ParsedSmartContractCallOutcome { return this.parseExecuteGivenTransactionOnNetwork(options.transactionOnNetwork, options.function); } - /** - * Legacy approach. - */ - protected parseExecuteGivenTransactionOutcome( - transactionOutcome: TransactionOutcome, - functionName?: string, - ): { - values: any[]; - returnCode: string; - returnMessage: string; - } { - const directCallOutcome = transactionOutcome.directSmartContractCallOutcome; - - if (!this.abi) { - return { - values: directCallOutcome.returnDataParts, - returnCode: directCallOutcome.returnCode, - returnMessage: directCallOutcome.returnMessage, - }; - } - - functionName = functionName || directCallOutcome.function; - - if (!functionName) { - throw new Err( - `Function name is not available in the transaction outcome, thus endpoint definition (ABI) cannot be picked (for parsing). Maybe provide the "function" parameter explicitly?`, - ); - } - - const endpoint = this.abi.getEndpoint(functionName); - - const legacyUntypedBundle = { - returnCode: new ReturnCode(directCallOutcome.returnCode), - returnMessage: directCallOutcome.returnMessage, - values: directCallOutcome.returnDataParts.map((part) => Buffer.from(part)), - }; - - const legacyTypedBundle = this.legacyResultsParser.parseOutcomeFromUntypedBundle(legacyUntypedBundle, endpoint); - - return { - values: legacyTypedBundle.values.map((value) => value.valueOf()), - returnCode: legacyTypedBundle.returnCode.toString(), - returnMessage: legacyTypedBundle.returnMessage, - }; - } - protected parseExecuteGivenTransactionOnNetwork( - transactionOnNetwork: ITransactionOnNetwork, + transactionOnNetwork: TransactionOnNetwork, functionName?: string, - ): { - values: any[]; - returnCode: string; - returnMessage: string; - } { + ): resources.ParsedSmartContractCallOutcome { const directCallOutcome = this.findDirectSmartContractCallOutcome(transactionOnNetwork); - if (!this.abi) { return { values: directCallOutcome.returnDataParts, @@ -216,7 +90,6 @@ export class SmartContractTransactionsOutcomeParser { } functionName = functionName || directCallOutcome.function; - if (!functionName) { throw new Err( `Function name is not available in the transaction, thus endpoint definition (ABI) cannot be picked (for parsing). Maybe provide the "function" parameter explicitly?`, @@ -231,24 +104,25 @@ export class SmartContractTransactionsOutcomeParser { return { returnCode: directCallOutcome.returnCode, returnMessage: directCallOutcome.returnMessage, - values: values, + values: values.map((value) => value.valueOf()), }; } - protected findDirectSmartContractCallOutcome( - transactionOnNetwork: ITransactionOnNetwork, - ): SmartContractCallOutcome { + protected findDirectSmartContractCallOutcome(transactionOnNetwork: TransactionOnNetwork): SmartContractCallOutcome { let outcome = this.findDirectSmartContractCallOutcomeWithinSmartContractResults(transactionOnNetwork); + if (outcome) { return outcome; } outcome = this.findDirectSmartContractCallOutcomeIfError(transactionOnNetwork); + if (outcome) { return outcome; } outcome = this.findDirectSmartContractCallOutcomeWithinWriteLogEvents(transactionOnNetwork); + if (outcome) { return outcome; } @@ -262,15 +136,15 @@ export class SmartContractTransactionsOutcomeParser { } protected findDirectSmartContractCallOutcomeWithinSmartContractResults( - transactionOnNetwork: ITransactionOnNetwork, + transactionOnNetwork: TransactionOnNetwork, ): SmartContractCallOutcome | null { const argSerializer = new ArgSerializer(); - const eligibleResults: IContractResultItem[] = []; + const eligibleResults: SmartContractResult[] = []; - for (const result of transactionOnNetwork.contractResults.items) { - const matchesCriteriaOnData = result.data.startsWith(ARGUMENTS_SEPARATOR); - const matchesCriteriaOnReceiver = result.receiver.bech32() === transactionOnNetwork.sender.bech32(); - const matchesCriteriaOnPreviousHash = result.previousHash === transactionOnNetwork.hash; + for (const result of transactionOnNetwork.smartContractResults) { + const matchesCriteriaOnData = result.data.toString().startsWith(ARGUMENTS_SEPARATOR); + const matchesCriteriaOnReceiver = result.receiver.toBech32() === transactionOnNetwork.sender.toBech32(); + const matchesCriteriaOnPreviousHash = result; const matchesCriteria = matchesCriteriaOnData && matchesCriteriaOnReceiver && matchesCriteriaOnPreviousHash; if (matchesCriteria) { @@ -289,22 +163,21 @@ export class SmartContractTransactionsOutcomeParser { } const [result] = eligibleResults; - const [_ignored, returnCode, ...returnDataParts] = argSerializer.stringToBuffers(result.data); - + const [_ignored, returnCode, ...returnDataParts] = argSerializer.stringToBuffers(result.data.toString()); return new SmartContractCallOutcome({ function: transactionOnNetwork.function, returnCode: returnCode?.toString(), - returnMessage: result.returnMessage || returnCode?.toString(), + returnMessage: result.raw["returnMessage"] || returnCode?.toString(), returnDataParts: returnDataParts, }); } protected findDirectSmartContractCallOutcomeIfError( - transactionOnNetwork: ITransactionOnNetwork, + transactionOnNetwork: TransactionOnNetwork, ): SmartContractCallOutcome | null { const argSerializer = new ArgSerializer(); const eventIdentifier = Events.SignalError; - const eligibleEvents: ITransactionEvent[] = []; + const eligibleEvents: TransactionEvent[] = []; // First, look in "logs": eligibleEvents.push( @@ -312,8 +185,8 @@ export class SmartContractTransactionsOutcomeParser { ); // Then, look in "logs" of "contractResults": - for (const result of transactionOnNetwork.contractResults.items) { - if (result.previousHash != transactionOnNetwork.hash) { + for (const result of transactionOnNetwork.smartContractResults) { + if (result.raw["prevTxHash"] != transactionOnNetwork.hash) { continue; } @@ -331,12 +204,11 @@ export class SmartContractTransactionsOutcomeParser { } const [event] = eligibleEvents; - const data = event.dataPayload?.valueOf().toString() || ""; - const lastTopic = event.getLastTopic()?.toString(); + const data = Buffer.from(event.data).toString(); + const lastTopic = event.topics[event.topics.length - 1]?.toString(); const parts = argSerializer.stringToBuffers(data); // Assumption: the last part is the return code. const returnCode = parts[parts.length - 1]; - return new SmartContractCallOutcome({ function: transactionOnNetwork.function, returnCode: returnCode?.toString() || eventIdentifier, @@ -346,11 +218,11 @@ export class SmartContractTransactionsOutcomeParser { } protected findDirectSmartContractCallOutcomeWithinWriteLogEvents( - transactionOnNetwork: ITransactionOnNetwork, + transactionOnNetwork: TransactionOnNetwork, ): SmartContractCallOutcome | null { const argSerializer = new ArgSerializer(); const eventIdentifier = Events.WriteLog; - const eligibleEvents: ITransactionEvent[] = []; + const eligibleEvents: TransactionEvent[] = []; // First, look in "logs": eligibleEvents.push( @@ -358,8 +230,8 @@ export class SmartContractTransactionsOutcomeParser { ); // Then, look in "logs" of "contractResults": - for (const result of transactionOnNetwork.contractResults.items) { - if (result.previousHash != transactionOnNetwork.hash) { + for (const result of transactionOnNetwork.smartContractResults) { + if (result.raw["prevTxHash"] != transactionOnNetwork.hash) { continue; } @@ -377,7 +249,7 @@ export class SmartContractTransactionsOutcomeParser { } const [event] = eligibleEvents; - const data = event.dataPayload?.valueOf().toString() || ""; + const data = Buffer.from(event.data).toString(); const [_ignored, returnCode, ...returnDataParts] = argSerializer.stringToBuffers(data); return new SmartContractCallOutcome({ diff --git a/src/smartcontracts/interaction.local.net.spec.ts b/src/smartcontracts/interaction.local.net.spec.ts deleted file mode 100644 index 05b8a8106..000000000 --- a/src/smartcontracts/interaction.local.net.spec.ts +++ /dev/null @@ -1,705 +0,0 @@ -import BigNumber from "bignumber.js"; -import { assert } from "chai"; -import { loadAbiRegistry, loadTestWallets, prepareDeployment, TestWallet } from "../testutils"; -import { ContractController } from "../testutils/contractController"; -import { createLocalnetProvider } from "../testutils/networkProviders"; -import { Transaction } from "../transaction"; -import { TransactionComputer } from "../transactionComputer"; -import { Interaction } from "./interaction"; -import { ReturnCode } from "./returnCode"; -import { SmartContract } from "./smartContract"; -import { TransactionsFactoryConfig } from "../transactionsFactories/transactionsFactoryConfig"; -import { SmartContractTransactionsFactory } from "../transactionsFactories/smartContractTransactionsFactory"; -import { promises } from "fs"; -import { ResultsParser } from "./resultsParser"; -import { TransactionWatcher } from "../transactionWatcher"; -import { SmartContractQueriesController } from "../smartContractQueriesController"; -import { QueryRunnerAdapter } from "../adapters/queryRunnerAdapter"; -import { ManagedDecimalSignedValue, ManagedDecimalValue } from "./typesystem"; - -describe("test smart contract interactor", function () { - let provider = createLocalnetProvider(); - let alice: TestWallet; - let resultsParser: ResultsParser; - - before(async function () { - ({ alice } = await loadTestWallets()); - resultsParser = new ResultsParser(); - }); - - it("should interact with 'answer' (local testnet)", async function () { - this.timeout(80000); - - let abiRegistry = await loadAbiRegistry("src/testdata/answer.abi.json"); - let contract = new SmartContract({ abi: abiRegistry }); - let controller = new ContractController(provider); - - let network = await provider.getNetworkConfig(); - await alice.sync(provider); - - // Deploy the contract - let deployTransaction = await prepareDeployment({ - contract: contract, - deployer: alice, - codePath: "src/testdata/answer.wasm", - gasLimit: 3000000, - initArguments: [], - chainID: network.ChainID, - }); - - let { - bundle: { returnCode }, - } = await controller.deploy(deployTransaction); - assert.isTrue(returnCode.isSuccess()); - - const interaction = ( - contract.methods - .getUltimateAnswer() - .withGasLimit(3000000) - .withChainID(network.ChainID) - .withSender(alice.address) - ); - - // Query - let queryResponseBundle = await controller.query(interaction); - assert.lengthOf(queryResponseBundle.values, 1); - assert.deepEqual(queryResponseBundle.firstValue!.valueOf(), new BigNumber(42)); - assert.isTrue(queryResponseBundle.returnCode.equals(ReturnCode.Ok)); - - // Execute, do not wait for execution - let transaction = interaction - .withSender(alice.address) - .useThenIncrementNonceOf(alice.account) - .buildTransaction(); - - await signTransaction({ transaction: transaction, wallet: alice }); - await provider.sendTransaction(transaction); - - // Execute, and wait for execution - transaction = interaction.withSender(alice.address).useThenIncrementNonceOf(alice.account).buildTransaction(); - - await signTransaction({ transaction: transaction, wallet: alice }); - let { bundle: executionResultsBundle } = await controller.execute(interaction, transaction); - - assert.lengthOf(executionResultsBundle.values, 1); - assert.deepEqual(executionResultsBundle.firstValue!.valueOf(), new BigNumber(42)); - assert.isTrue(executionResultsBundle.returnCode.equals(ReturnCode.Ok)); - }); - - it("should interact with 'answer' (local testnet) using the SmartContractTransactionsFactory", async function () { - this.timeout(80000); - - let abiRegistry = await loadAbiRegistry("src/testdata/answer.abi.json"); - - let network = await provider.getNetworkConfig(); - await alice.sync(provider); - - const config = new TransactionsFactoryConfig({ chainID: network.ChainID }); - const factory = new SmartContractTransactionsFactory({ - config: config, - abi: abiRegistry, - }); - - const bytecode = await promises.readFile("src/testdata/answer.wasm"); - - const deployTransaction = factory.createTransactionForDeploy({ - sender: alice.address, - bytecode: bytecode, - gasLimit: 3000000n, - }); - deployTransaction.nonce = BigInt(alice.account.nonce.valueOf()); - - const transactionComputer = new TransactionComputer(); - deployTransaction.signature = await alice.signer.sign( - Buffer.from(transactionComputer.computeBytesForSigning(deployTransaction)), - ); - - const contractAddress = SmartContract.computeAddress(alice.address, alice.account.nonce); - alice.account.incrementNonce(); - - const transactionCompletionAwaiter = new TransactionWatcher({ - getTransaction: async (hash: string) => { - return await provider.getTransaction(hash, true); - }, - }); - - const deployTxHash = await provider.sendTransaction(deployTransaction); - let transactionOnNetwork = await transactionCompletionAwaiter.awaitCompleted(deployTxHash); - const untypedBundle = resultsParser.parseUntypedOutcome(transactionOnNetwork); - assert.isTrue(untypedBundle.returnCode.isSuccess()); - - const queryRunner = new QueryRunnerAdapter({ networkProvider: provider }); - const queryController = new SmartContractQueriesController({ abi: abiRegistry, queryRunner: queryRunner }); - - const query = queryController.createQuery({ - contract: contractAddress.bech32(), - caller: alice.address.bech32(), - function: "getUltimateAnswer", - arguments: [], - }); - - const queryResponse = await queryController.runQuery(query); - const parsed = queryController.parseQueryResponse(queryResponse); - assert.lengthOf(parsed, 1); - assert.deepEqual(parsed[0], new BigNumber(42)); - - // Query - let transaction = factory.createTransactionForExecute({ - sender: alice.address, - contract: contractAddress, - function: "getUltimateAnswer", - gasLimit: 3000000n, - }); - transaction.nonce = BigInt(alice.account.nonce.valueOf()); - transaction.signature = await alice.signer.sign( - Buffer.from(transactionComputer.computeBytesForSigning(transaction)), - ); - - alice.account.incrementNonce(); - - await provider.sendTransaction(transaction); - - // Execute, and wait for execution - transaction = factory.createTransactionForExecute({ - sender: alice.address, - contract: contractAddress, - function: "getUltimateAnswer", - gasLimit: 3000000n, - }); - transaction.nonce = BigInt(alice.account.nonce.valueOf()); - transaction.signature = await alice.signer.sign( - Buffer.from(transactionComputer.computeBytesForSigning(transaction)), - ); - - alice.account.incrementNonce(); - - const executeTxHash = await provider.sendTransaction(transaction); - transactionOnNetwork = await transactionCompletionAwaiter.awaitCompleted(executeTxHash); - const typedBundle = resultsParser.parseOutcome( - transactionOnNetwork, - abiRegistry.getEndpoint("getUltimateAnswer"), - ); - - assert.lengthOf(typedBundle.values, 1); - assert.deepEqual(typedBundle.firstValue!.valueOf(), new BigNumber(42)); - assert.isTrue(typedBundle.returnCode.equals(ReturnCode.Ok)); - }); - - it("should interact with 'basic-features' (local testnet)", async function () { - this.timeout(140000); - - let abiRegistry = await loadAbiRegistry("src/testdata/basic-features.abi.json"); - let contract = new SmartContract({ abi: abiRegistry }); - let controller = new ContractController(provider); - - let network = await provider.getNetworkConfig(); - await alice.sync(provider); - - // Deploy the contract - let deployTransaction = await prepareDeployment({ - contract: contract, - deployer: alice, - codePath: "src/testdata/basic-features.wasm", - gasLimit: 600000000, - initArguments: [], - chainID: network.ChainID, - }); - - let { - bundle: { returnCode }, - } = await controller.deploy(deployTransaction); - assert.isTrue(returnCode.isSuccess()); - - let returnEgldInteraction = ( - contract.methods - .returns_egld_decimal([]) - .withGasLimit(10000000) - .withChainID(network.ChainID) - .withSender(alice.address) - .withValue(1) - ); - - // returnEgld() - let returnEgldTransaction = returnEgldInteraction - .withSender(alice.address) - .useThenIncrementNonceOf(alice.account) - .buildTransaction(); - - let additionInteraction = contract.methods - .managed_decimal_addition([new ManagedDecimalValue("2.5", 2), new ManagedDecimalValue("2.7", 2)]) - .withGasLimit(10000000) - .withChainID(network.ChainID) - .withSender(alice.address) - .withValue(0); - - // addition() - let additionTransaction = additionInteraction - .withSender(alice.address) - .useThenIncrementNonceOf(alice.account) - .buildTransaction(); - - // log - let mdLnInteraction = contract.methods - .managed_decimal_ln([new ManagedDecimalValue("23", 9)]) - .withGasLimit(10000000) - .withChainID(network.ChainID) - .withSender(alice.address) - .withValue(0); - - // mdLn() - let mdLnTransaction = mdLnInteraction - .withSender(alice.address) - .useThenIncrementNonceOf(alice.account) - .buildTransaction(); - - let additionVarInteraction = contract.methods - .managed_decimal_addition_var([ - new ManagedDecimalValue("4", 2, true), - new ManagedDecimalValue("5", 2, true), - ]) - .withGasLimit(50000000) - .withChainID(network.ChainID) - .withSender(alice.address) - .withValue(0); - - // addition() - let additionVarTransaction = additionVarInteraction - .withSender(alice.address) - .useThenIncrementNonceOf(alice.account) - .buildTransaction(); - - let lnVarInteraction = contract.methods - .managed_decimal_ln_var([new ManagedDecimalValue("23", 9, true)]) - .withGasLimit(50000000) - .withChainID(network.ChainID) - .withSender(alice.address) - .withValue(0); - - // managed_decimal_ln_var() - let lnVarTransaction = lnVarInteraction - .withSender(alice.address) - .useThenIncrementNonceOf(alice.account) - .buildTransaction(); - - // returnEgld() - await signTransaction({ transaction: returnEgldTransaction, wallet: alice }); - let { bundle: bundleEgld } = await controller.execute(returnEgldInteraction, returnEgldTransaction); - assert.isTrue(bundleEgld.returnCode.equals(ReturnCode.Ok)); - assert.lengthOf(bundleEgld.values, 1); - assert.deepEqual(bundleEgld.values[0], new ManagedDecimalValue("0.000000000000000001", 18)); - - // addition with const decimals() - await signTransaction({ transaction: additionTransaction, wallet: alice }); - let { bundle: bundleAdditionConst } = await controller.execute(additionInteraction, additionTransaction); - assert.isTrue(bundleAdditionConst.returnCode.equals(ReturnCode.Ok)); - assert.lengthOf(bundleAdditionConst.values, 1); - assert.deepEqual(bundleAdditionConst.values[0], new ManagedDecimalValue("5.2", 2)); - - // log - await signTransaction({ transaction: mdLnTransaction, wallet: alice }); - let { bundle: bundleMDLn } = await controller.execute(mdLnInteraction, mdLnTransaction); - assert.isTrue(bundleMDLn.returnCode.equals(ReturnCode.Ok)); - assert.lengthOf(bundleMDLn.values, 1); - assert.deepEqual(bundleMDLn.values[0], new ManagedDecimalSignedValue("3.135553845", 9)); - - // addition with var decimals - await signTransaction({ transaction: additionVarTransaction, wallet: alice }); - let { bundle: bundleAddition } = await controller.execute(additionVarInteraction, additionVarTransaction); - assert.isTrue(bundleAddition.returnCode.equals(ReturnCode.Ok)); - assert.lengthOf(bundleAddition.values, 1); - assert.deepEqual(bundleAddition.values[0], new ManagedDecimalValue("9", 2)); - - // log - await signTransaction({ transaction: lnVarTransaction, wallet: alice }); - let { bundle: bundleLnVar } = await controller.execute(lnVarInteraction, lnVarTransaction); - assert.isTrue(bundleLnVar.returnCode.equals(ReturnCode.Ok)); - assert.lengthOf(bundleLnVar.values, 1); - assert.deepEqual(bundleLnVar.values[0], new ManagedDecimalSignedValue("3.135553845", 9)); - }); - - it("should interact with 'counter' (local testnet)", async function () { - this.timeout(120000); - - let abiRegistry = await loadAbiRegistry("src/testdata/counter.abi.json"); - let contract = new SmartContract({ abi: abiRegistry }); - let controller = new ContractController(provider); - - let network = await provider.getNetworkConfig(); - await alice.sync(provider); - - // Deploy the contract - let deployTransaction = await prepareDeployment({ - contract: contract, - deployer: alice, - codePath: "src/testdata/counter.wasm", - gasLimit: 3000000, - initArguments: [], - chainID: network.ChainID, - }); - - let { - bundle: { returnCode }, - } = await controller.deploy(deployTransaction); - assert.isTrue(returnCode.isSuccess()); - - let getInteraction = contract.methods.get(); - let incrementInteraction = (contract.methods.increment()) - .withGasLimit(3000000) - .withChainID(network.ChainID) - .withSender(alice.address); - let decrementInteraction = (contract.methods.decrement()) - .withGasLimit(3000000) - .withChainID(network.ChainID) - .withSender(alice.address); - - // Query "get()" - let { firstValue: counterValue } = await controller.query(getInteraction); - assert.deepEqual(counterValue!.valueOf(), new BigNumber(1)); - - // Increment, wait for execution. - let incrementTransaction = incrementInteraction.useThenIncrementNonceOf(alice.account).buildTransaction(); - await signTransaction({ transaction: incrementTransaction, wallet: alice }); - let { - bundle: { firstValue: valueAfterIncrement }, - } = await controller.execute(incrementInteraction, incrementTransaction); - assert.deepEqual(valueAfterIncrement!.valueOf(), new BigNumber(2)); - - // Decrement twice. Wait for execution of the second transaction. - let decrementTransaction = decrementInteraction.useThenIncrementNonceOf(alice.account).buildTransaction(); - await signTransaction({ transaction: decrementTransaction, wallet: alice }); - await provider.sendTransaction(decrementTransaction); - - decrementTransaction = decrementInteraction.useThenIncrementNonceOf(alice.account).buildTransaction(); - await signTransaction({ transaction: decrementTransaction, wallet: alice }); - let { - bundle: { firstValue: valueAfterDecrement }, - } = await controller.execute(decrementInteraction, decrementTransaction); - assert.deepEqual(valueAfterDecrement!.valueOf(), new BigNumber(0)); - }); - - it("should interact with 'counter' (local testnet) using the SmartContractTransactionsFactory", async function () { - this.timeout(120000); - - let abiRegistry = await loadAbiRegistry("src/testdata/counter.abi.json"); - - let network = await provider.getNetworkConfig(); - await alice.sync(provider); - - const config = new TransactionsFactoryConfig({ chainID: network.ChainID }); - const factory = new SmartContractTransactionsFactory({ - config: config, - abi: abiRegistry, - }); - - const bytecode = await promises.readFile("src/testdata/counter.wasm"); - - const deployTransaction = factory.createTransactionForDeploy({ - sender: alice.address, - bytecode: bytecode, - gasLimit: 3000000n, - }); - deployTransaction.nonce = BigInt(alice.account.nonce.valueOf()); - - const transactionComputer = new TransactionComputer(); - deployTransaction.signature = await alice.signer.sign( - Buffer.from(transactionComputer.computeBytesForSigning(deployTransaction)), - ); - - const contractAddress = SmartContract.computeAddress(alice.address, alice.account.nonce); - alice.account.incrementNonce(); - - const transactionCompletionAwaiter = new TransactionWatcher({ - getTransaction: async (hash: string) => { - return await provider.getTransaction(hash, true); - }, - }); - - const deployTxHash = await provider.sendTransaction(deployTransaction); - let transactionOnNetwork = await transactionCompletionAwaiter.awaitCompleted(deployTxHash); - const untypedBundle = resultsParser.parseUntypedOutcome(transactionOnNetwork); - assert.isTrue(untypedBundle.returnCode.isSuccess()); - - const queryRunner = new QueryRunnerAdapter({ networkProvider: provider }); - const queryController = new SmartContractQueriesController({ abi: abiRegistry, queryRunner: queryRunner }); - - let incrementTransaction = factory.createTransactionForExecute({ - sender: alice.address, - contract: contractAddress, - function: "increment", - gasLimit: 3000000n, - }); - incrementTransaction.nonce = BigInt(alice.account.nonce.valueOf()); - - incrementTransaction.signature = await alice.signer.sign( - Buffer.from(transactionComputer.computeBytesForSigning(incrementTransaction)), - ); - - alice.account.incrementNonce(); - - // Query "get()" - const query = queryController.createQuery({ - contract: contractAddress.bech32(), - function: "get", - arguments: [], - }); - const queryResponse = await queryController.runQuery(query); - const parsed = queryController.parseQueryResponse(queryResponse); - assert.deepEqual(parsed[0], new BigNumber(1)); - - const incrementTxHash = await provider.sendTransaction(incrementTransaction); - transactionOnNetwork = await transactionCompletionAwaiter.awaitCompleted(incrementTxHash); - let typedBundle = resultsParser.parseOutcome(transactionOnNetwork, abiRegistry.getEndpoint("increment")); - assert.deepEqual(typedBundle.firstValue!.valueOf(), new BigNumber(2)); - - let decrementTransaction = factory.createTransactionForExecute({ - sender: alice.address, - contract: contractAddress, - function: "decrement", - gasLimit: 3000000n, - }); - decrementTransaction.nonce = BigInt(alice.account.nonce.valueOf()); - decrementTransaction.signature = await alice.signer.sign( - Buffer.from(transactionComputer.computeBytesForSigning(decrementTransaction)), - ); - - alice.account.incrementNonce(); - - await provider.sendTransaction(decrementTransaction); - - decrementTransaction.nonce = BigInt(alice.account.nonce.valueOf()); - decrementTransaction.signature = await alice.signer.sign( - Buffer.from(transactionComputer.computeBytesForSigning(decrementTransaction)), - ); - - const decrementTxHash = await provider.sendTransaction(decrementTransaction); - transactionOnNetwork = await transactionCompletionAwaiter.awaitCompleted(decrementTxHash); - typedBundle = resultsParser.parseOutcome(transactionOnNetwork, abiRegistry.getEndpoint("decrement")); - }); - - it("should interact with 'lottery-esdt' (local testnet)", async function () { - this.timeout(140000); - - let abiRegistry = await loadAbiRegistry("src/testdata/lottery-esdt.abi.json"); - let contract = new SmartContract({ abi: abiRegistry }); - let controller = new ContractController(provider); - - let network = await provider.getNetworkConfig(); - await alice.sync(provider); - - // Deploy the contract - let deployTransaction = await prepareDeployment({ - contract: contract, - deployer: alice, - codePath: "src/testdata/lottery-esdt.wasm", - gasLimit: 100000000, - initArguments: [], - chainID: network.ChainID, - }); - - let { - bundle: { returnCode }, - } = await controller.deploy(deployTransaction); - assert.isTrue(returnCode.isSuccess()); - - let startInteraction = ( - contract.methods - .start(["lucky", "EGLD", 1, null, null, 1, null, null]) - .withGasLimit(30000000) - .withChainID(network.ChainID) - .withSender(alice.address) - ); - - let lotteryStatusInteraction = ( - contract.methods - .status(["lucky"]) - .withGasLimit(5000000) - .withChainID(network.ChainID) - .withSender(alice.address) - ); - - let getLotteryInfoInteraction = ( - contract.methods - .getLotteryInfo(["lucky"]) - .withGasLimit(5000000) - .withChainID(network.ChainID) - .withSender(alice.address) - ); - - // start() - let startTransaction = startInteraction - .withSender(alice.address) - .useThenIncrementNonceOf(alice.account) - .buildTransaction(); - - await signTransaction({ transaction: startTransaction, wallet: alice }); - let { bundle: bundleStart } = await controller.execute(startInteraction, startTransaction); - assert.isTrue(bundleStart.returnCode.equals(ReturnCode.Ok)); - assert.lengthOf(bundleStart.values, 0); - - // status() - let lotteryStatusTransaction = lotteryStatusInteraction - .withSender(alice.address) - .useThenIncrementNonceOf(alice.account) - .buildTransaction(); - - await signTransaction({ transaction: lotteryStatusTransaction, wallet: alice }); - let { bundle: bundleStatus } = await controller.execute(lotteryStatusInteraction, lotteryStatusTransaction); - assert.isTrue(bundleStatus.returnCode.equals(ReturnCode.Ok)); - assert.lengthOf(bundleStatus.values, 1); - assert.equal(bundleStatus.firstValue!.valueOf().name, "Running"); - - // lotteryInfo() (this is a view function, but for the sake of the test, we'll execute it) - let lotteryInfoTransaction = getLotteryInfoInteraction - .withSender(alice.address) - .useThenIncrementNonceOf(alice.account) - .buildTransaction(); - - await signTransaction({ transaction: lotteryInfoTransaction, wallet: alice }); - let { bundle: bundleLotteryInfo } = await controller.execute(getLotteryInfoInteraction, lotteryInfoTransaction); - assert.isTrue(bundleLotteryInfo.returnCode.equals(ReturnCode.Ok)); - assert.lengthOf(bundleLotteryInfo.values, 1); - - // Ignore "deadline" field in our test - let info = bundleLotteryInfo.firstValue!.valueOf(); - delete info.deadline; - - assert.deepEqual(info, { - token_identifier: "EGLD", - ticket_price: new BigNumber("1"), - tickets_left: new BigNumber(800), - max_entries_per_user: new BigNumber(1), - prize_distribution: Buffer.from([0x64]), - prize_pool: new BigNumber("0"), - }); - }); - - it("should interact with 'lottery-esdt' (local testnet) using the SmartContractTransactionsFactory", async function () { - this.timeout(140000); - - let abiRegistry = await loadAbiRegistry("src/testdata/lottery-esdt.abi.json"); - - let network = await provider.getNetworkConfig(); - await alice.sync(provider); - - const config = new TransactionsFactoryConfig({ chainID: network.ChainID }); - const factory = new SmartContractTransactionsFactory({ - config: config, - abi: abiRegistry, - }); - - const bytecode = await promises.readFile("src/testdata/lottery-esdt.wasm"); - - // Deploy the contract - const deployTransaction = factory.createTransactionForDeploy({ - sender: alice.address, - bytecode: bytecode, - gasLimit: 100000000n, - }); - deployTransaction.nonce = BigInt(alice.account.nonce.valueOf()); - - const transactionComputer = new TransactionComputer(); - deployTransaction.signature = await alice.signer.sign( - Buffer.from(transactionComputer.computeBytesForSigning(deployTransaction)), - ); - - const contractAddress = SmartContract.computeAddress(alice.address, alice.account.nonce); - alice.account.incrementNonce(); - - const transactionCompletionAwaiter = new TransactionWatcher({ - getTransaction: async (hash: string) => { - return await provider.getTransaction(hash, true); - }, - }); - - const deployTxHash = await provider.sendTransaction(deployTransaction); - let transactionOnNetwork = await transactionCompletionAwaiter.awaitCompleted(deployTxHash); - const untypedBundle = resultsParser.parseUntypedOutcome(transactionOnNetwork); - assert.isTrue(untypedBundle.returnCode.isSuccess()); - - // start() - let startTransaction = factory.createTransactionForExecute({ - sender: alice.address, - contract: contractAddress, - function: "start", - arguments: ["lucky", "EGLD", 1, null, null, 1, null, null], - gasLimit: 30000000n, - }); - startTransaction.nonce = BigInt(alice.account.nonce.valueOf()); - startTransaction.signature = await alice.signer.sign( - Buffer.from(transactionComputer.computeBytesForSigning(startTransaction)), - ); - - alice.account.incrementNonce(); - - const startTxHash = await provider.sendTransaction(startTransaction); - transactionOnNetwork = await transactionCompletionAwaiter.awaitCompleted(startTxHash); - let typedBundle = resultsParser.parseOutcome(transactionOnNetwork, abiRegistry.getEndpoint("start")); - assert.equal(typedBundle.returnCode.valueOf(), "ok"); - assert.lengthOf(typedBundle.values, 0); - - // status() - let lotteryStatusTransaction = factory.createTransactionForExecute({ - sender: alice.address, - contract: contractAddress, - function: "status", - arguments: ["lucky"], - gasLimit: 5000000n, - }); - lotteryStatusTransaction.nonce = BigInt(alice.account.nonce.valueOf()); - lotteryStatusTransaction.signature = await alice.signer.sign( - Buffer.from(transactionComputer.computeBytesForSigning(lotteryStatusTransaction)), - ); - - alice.account.incrementNonce(); - - const statusTxHash = await provider.sendTransaction(lotteryStatusTransaction); - transactionOnNetwork = await transactionCompletionAwaiter.awaitCompleted(statusTxHash); - typedBundle = resultsParser.parseOutcome(transactionOnNetwork, abiRegistry.getEndpoint("status")); - assert.equal(typedBundle.returnCode.valueOf(), "ok"); - assert.lengthOf(typedBundle.values, 1); - assert.equal(typedBundle.firstValue!.valueOf().name, "Running"); - - // getlotteryInfo() (this is a view function, but for the sake of the test, we'll execute it) - let lotteryInfoTransaction = factory.createTransactionForExecute({ - sender: alice.address, - contract: contractAddress, - function: "getLotteryInfo", - arguments: ["lucky"], - gasLimit: 5000000n, - }); - lotteryInfoTransaction.nonce = BigInt(alice.account.nonce.valueOf()); - lotteryInfoTransaction.signature = await alice.signer.sign( - Buffer.from(transactionComputer.computeBytesForSigning(lotteryInfoTransaction)), - ); - - alice.account.incrementNonce(); - - const infoTxHash = await provider.sendTransaction(lotteryInfoTransaction); - transactionOnNetwork = await transactionCompletionAwaiter.awaitCompleted(infoTxHash); - typedBundle = resultsParser.parseOutcome(transactionOnNetwork, abiRegistry.getEndpoint("getLotteryInfo")); - assert.equal(typedBundle.returnCode.valueOf(), "ok"); - assert.lengthOf(typedBundle.values, 1); - - // Ignore "deadline" field in our test - let info = typedBundle.firstValue!.valueOf(); - delete info.deadline; - - assert.deepEqual(info, { - token_identifier: "EGLD", - ticket_price: new BigNumber("1"), - tickets_left: new BigNumber(800), - max_entries_per_user: new BigNumber(1), - prize_distribution: Buffer.from([0x64]), - prize_pool: new BigNumber("0"), - }); - }); - - async function signTransaction(options: { transaction: Transaction; wallet: TestWallet }) { - const transaction = options.transaction; - const wallet = options.wallet; - - const serialized = transaction.serializeForSigning(); - const signature = await wallet.signer.sign(serialized); - transaction.applySignature(signature); - } -}); diff --git a/src/smartcontracts/interaction.spec.ts b/src/smartcontracts/interaction.spec.ts deleted file mode 100644 index e88789a90..000000000 --- a/src/smartcontracts/interaction.spec.ts +++ /dev/null @@ -1,424 +0,0 @@ -import { ContractQueryResponse } from "../networkProviders"; -import BigNumber from "bignumber.js"; -import { assert } from "chai"; -import { Address } from "../address"; -import { - loadAbiRegistry, - loadTestWallets, - MockNetworkProvider, - setupUnitTestWatcherTimeouts, - TestWallet, -} from "../testutils"; -import { ContractController } from "../testutils/contractController"; -import { Token, TokenTransfer } from "../tokens"; -import { Transaction } from "../transaction"; -import { ContractFunction } from "./function"; -import { Interaction } from "./interaction"; -import { ReturnCode } from "./returnCode"; -import { SmartContract } from "./smartContract"; -import { BigUIntValue, OptionalValue, OptionValue, TokenIdentifierValue, U32Value } from "./typesystem"; -import { BytesValue } from "./typesystem/bytes"; - -describe("test smart contract interactor", function () { - let dummyAddress = new Address("erd1qqqqqqqqqqqqqpgqak8zt22wl2ph4tswtyc39namqx6ysa2sd8ss4xmlj3"); - let provider = new MockNetworkProvider(); - let alice: TestWallet; - - before(async function () { - ({ alice } = await loadTestWallets()); - }); - - it("should set transaction fields", async function () { - let contract = new SmartContract({ address: dummyAddress }); - let dummyFunction = new ContractFunction("dummy"); - let interaction = new Interaction(contract, dummyFunction, []); - - let transaction = interaction - .withSender(alice.address) - .withNonce(7) - .withValue(TokenTransfer.egldFromAmount(1)) - .withGasLimit(20000000) - .buildTransaction(); - - assert.deepEqual(transaction.getReceiver(), dummyAddress); - assert.equal(transaction.getValue().toString(), "1000000000000000000"); - assert.equal(transaction.getNonce(), 7); - assert.equal(transaction.getGasLimit().valueOf(), 20000000); - }); - - it("should set transfers (payments) on contract calls (transfer and execute)", async function () { - let contract = new SmartContract({ address: dummyAddress }); - let dummyFunction = new ContractFunction("dummy"); - let alice = new Address("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - - const TokenFoo = (amount: BigNumber.Value) => TokenTransfer.fungibleFromAmount("FOO-6ce17b", amount, 0); - const TokenBar = (amount: BigNumber.Value) => TokenTransfer.fungibleFromAmount("BAR-5bc08f", amount, 3); - const LKMEX = (nonce: number, amount: BigNumber.Value) => - TokenTransfer.metaEsdtFromAmount("LKMEX-aab910", nonce, amount, 18); - const nonFungibleToken = (nonce: number) => TokenTransfer.nonFungible("MOS-b9b4b2", nonce); - - const hexFoo = "464f4f2d366365313762"; - const hexBar = "4241522d356263303866"; - const hexLKMEX = "4c4b4d45582d616162393130"; - const hexNFT = "4d4f532d623962346232"; - const hexContractAddress = new Address(contract.getAddress().bech32()).hex(); - const hexDummyFunction = "64756d6d79"; - - // ESDT, single - let transaction = new Interaction(contract, dummyFunction, []) - .withSender(alice) - .withSingleESDTTransfer(TokenFoo(10)) - .buildTransaction(); - - assert.equal(transaction.getData().toString(), `ESDTTransfer@${hexFoo}@0a@${hexDummyFunction}`); - - // Meta ESDT (special SFT), single - transaction = new Interaction(contract, dummyFunction, []) - .withSender(alice) - .withSingleESDTNFTTransfer(LKMEX(123456, 123.456)) - .buildTransaction(); - - assert.equal(transaction.getSender().bech32(), alice.bech32()); - assert.equal(transaction.getReceiver().bech32(), alice.bech32()); - assert.equal( - transaction.getData().toString(), - `ESDTNFTTransfer@${hexLKMEX}@01e240@06b14bd1e6eea00000@${hexContractAddress}@${hexDummyFunction}`, - ); - - // Meta ESDT (special SFT), single, but using "withSender()" (recommended) - transaction = new Interaction(contract, dummyFunction, []) - .withSingleESDTNFTTransfer(LKMEX(123456, 123.456)) - .withSender(alice) - .buildTransaction(); - - assert.equal(transaction.getSender().bech32(), alice.bech32()); - assert.equal(transaction.getReceiver().bech32(), alice.bech32()); - assert.equal( - transaction.getData().toString(), - `ESDTNFTTransfer@${hexLKMEX}@01e240@06b14bd1e6eea00000@${hexContractAddress}@${hexDummyFunction}`, - ); - - // NFT, single - transaction = new Interaction(contract, dummyFunction, []) - .withSender(alice) - .withSingleESDTNFTTransfer(nonFungibleToken(1)) - .buildTransaction(); - - assert.equal(transaction.getSender().bech32(), alice.bech32()); - assert.equal(transaction.getReceiver().bech32(), alice.bech32()); - assert.equal( - transaction.getData().toString(), - `ESDTNFTTransfer@${hexNFT}@01@01@${hexContractAddress}@${hexDummyFunction}`, - ); - - // NFT, single, but using "withSender()" (recommended) - transaction = new Interaction(contract, dummyFunction, []) - .withSingleESDTNFTTransfer(nonFungibleToken(1)) - .withSender(alice) - .buildTransaction(); - - assert.equal(transaction.getSender().bech32(), alice.bech32()); - assert.equal(transaction.getReceiver().bech32(), alice.bech32()); - assert.equal( - transaction.getData().toString(), - `ESDTNFTTransfer@${hexNFT}@01@01@${hexContractAddress}@${hexDummyFunction}`, - ); - - // ESDT, multiple - transaction = new Interaction(contract, dummyFunction, []) - .withSender(alice) - .withMultiESDTNFTTransfer([TokenFoo(3), TokenBar(3.14)]) - .buildTransaction(); - - assert.equal(transaction.getSender().bech32(), alice.bech32()); - assert.equal(transaction.getReceiver().bech32(), alice.bech32()); - assert.equal( - transaction.getData().toString(), - `MultiESDTNFTTransfer@${hexContractAddress}@02@${hexFoo}@@03@${hexBar}@@0c44@${hexDummyFunction}`, - ); - - // ESDT, multiple, but using "withSender()" (recommended) - transaction = new Interaction(contract, dummyFunction, []) - .withMultiESDTNFTTransfer([TokenFoo(3), TokenBar(3.14)]) - .withSender(alice) - .buildTransaction(); - - assert.equal(transaction.getSender().bech32(), alice.bech32()); - assert.equal(transaction.getReceiver().bech32(), alice.bech32()); - assert.equal( - transaction.getData().toString(), - `MultiESDTNFTTransfer@${hexContractAddress}@02@${hexFoo}@@03@${hexBar}@@0c44@${hexDummyFunction}`, - ); - - // NFT, multiple - transaction = new Interaction(contract, dummyFunction, []) - .withSender(alice) - .withMultiESDTNFTTransfer([nonFungibleToken(1), nonFungibleToken(42)]) - .buildTransaction(); - - assert.equal(transaction.getSender().bech32(), alice.bech32()); - assert.equal(transaction.getReceiver().bech32(), alice.bech32()); - assert.equal( - transaction.getData().toString(), - `MultiESDTNFTTransfer@${hexContractAddress}@02@${hexNFT}@01@01@${hexNFT}@2a@01@${hexDummyFunction}`, - ); - - // NFT, multiple, but using "withSender()" (recommended) - transaction = new Interaction(contract, dummyFunction, []) - .withMultiESDTNFTTransfer([nonFungibleToken(1), nonFungibleToken(42)]) - .withSender(alice) - .buildTransaction(); - - assert.equal(transaction.getSender().bech32(), alice.bech32()); - assert.equal(transaction.getReceiver().bech32(), alice.bech32()); - }); - - it("should create transaction, with ABI, with transfer & execute", async function () { - const abiRegistry = await loadAbiRegistry("src/testdata/answer.abi.json"); - const contract = new SmartContract({ address: dummyAddress, abi: abiRegistry }); - const alice = new Address("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - const token = new Token({ identifier: "FOO-abcdef", nonce: 0n }); - - const transaction = contract.methods - .getUltimateAnswer() - .withChainID("T") - .withSender(alice) - .withGasLimit(543210) - .withSingleESDTTransfer(new TokenTransfer({ token, amount: 100n })) - .withNonce(42) - .buildTransaction(); - - assert.deepEqual( - transaction, - new Transaction({ - chainID: "T", - sender: alice.toBech32(), - receiver: dummyAddress.toBech32(), - data: Buffer.from("ESDTTransfer@464f4f2d616263646566@64@676574556c74696d617465416e73776572"), - gasLimit: 543210n, - value: 0n, - version: 2, - nonce: 42n, - }), - ); - }); - - it("should interact with 'answer'", async function () { - setupUnitTestWatcherTimeouts(); - - let abiRegistry = await loadAbiRegistry("src/testdata/answer.abi.json"); - let contract = new SmartContract({ address: dummyAddress, abi: abiRegistry }); - let controller = new ContractController(provider); - - let interaction = contract.methods.getUltimateAnswer().withGasLimit(543210).withChainID("T"); - - assert.equal(contract.getAddress(), dummyAddress); - assert.deepEqual(interaction.getFunction(), new ContractFunction("getUltimateAnswer")); - assert.lengthOf(interaction.getArguments(), 0); - assert.deepEqual(interaction.getGasLimit(), 543210); - - provider.mockQueryContractOnFunction( - "getUltimateAnswer", - new ContractQueryResponse({ returnData: [Buffer.from([42]).toString("base64")], returnCode: "ok" }), - ); - - // Query - let { - values: queryValues, - firstValue: queryAnwser, - returnCode: queryCode, - } = await controller.query(interaction); - assert.lengthOf(queryValues, 1); - assert.deepEqual(queryAnwser!.valueOf(), new BigNumber(42)); - assert.isTrue(queryCode.equals(ReturnCode.Ok)); - - // Execute, do not wait for execution - let transaction = interaction.withSender(alice.address).withNonce(0).buildTransaction(); - transaction.setSender(alice.address); - transaction.applySignature(await alice.signer.sign(transaction.serializeForSigning())); - await provider.sendTransaction(transaction); - assert.equal(transaction.getNonce().valueOf(), 0); - assert.equal(transaction.getData().toString(), "getUltimateAnswer"); - assert.equal( - transaction.getHash().toString(), - "3579ad09099feb9755c860ddd225251170806d833342e912fccdfe2ed5c3a364", - ); - - transaction = interaction.withNonce(1).buildTransaction(); - transaction.setSender(alice.address); - transaction.applySignature(await alice.signer.sign(transaction.serializeForSigning())); - await provider.sendTransaction(transaction); - assert.equal(transaction.getNonce().valueOf(), 1); - assert.equal( - transaction.getHash().toString(), - "ad513ce7c5d371d30e48f073326899766736eac1ac231d847d45bc3facbcb496", - ); - - // Execute, and wait for execution - transaction = interaction.withNonce(2).buildTransaction(); - transaction.setSender(alice.address); - transaction.applySignature(await alice.signer.sign(transaction.serializeForSigning())); - provider.mockGetTransactionWithAnyHashAsNotarizedWithOneResult("@6f6b@2bs"); - let { bundle } = await controller.execute(interaction, transaction); - - assert.lengthOf(bundle.values, 1); - assert.deepEqual(bundle.firstValue!.valueOf(), new BigNumber(43)); - assert.isTrue(bundle.returnCode.equals(ReturnCode.Ok)); - }); - - it("should interact with 'counter'", async function () { - setupUnitTestWatcherTimeouts(); - - let abiRegistry = await loadAbiRegistry("src/testdata/counter.abi.json"); - let contract = new SmartContract({ address: dummyAddress, abi: abiRegistry }); - let controller = new ContractController(provider); - - let getInteraction = contract.methodsExplicit.get().check(); - let incrementInteraction = (contract.methods.increment()).withGasLimit(543210); - let decrementInteraction = (contract.methods.decrement()).withGasLimit(987654); - - // For "get()", return fake 7 - provider.mockQueryContractOnFunction( - "get", - new ContractQueryResponse({ returnData: [Buffer.from([7]).toString("base64")], returnCode: "ok" }), - ); - - // Query "get()" - let { firstValue: counterValue } = await controller.query(getInteraction); - - assert.deepEqual(counterValue!.valueOf(), new BigNumber(7)); - - let incrementTransaction = incrementInteraction - .withSender(alice.address) - .withNonce(14) - .withChainID("mock") - .buildTransaction(); - - incrementTransaction.applySignature(await alice.signer.sign(incrementTransaction.serializeForSigning())); - provider.mockGetTransactionWithAnyHashAsNotarizedWithOneResult("@6f6b@08"); - let { - bundle: { firstValue: valueAfterIncrement }, - } = await controller.execute(incrementInteraction, incrementTransaction); - assert.deepEqual(valueAfterIncrement!.valueOf(), new BigNumber(8)); - - // Decrement three times (simulate three parallel broadcasts). Wait for execution of the latter (third transaction). Return fake "5". - // Decrement #1 - let decrementTransaction = decrementInteraction - .withSender(alice.address) - .withNonce(15) - .withChainID("mock") - .buildTransaction(); - - decrementTransaction.applySignature(await alice.signer.sign(decrementTransaction.serializeForSigning())); - await provider.sendTransaction(decrementTransaction); - // Decrement #2 - decrementTransaction = decrementInteraction.withNonce(16).buildTransaction(); - decrementTransaction.applySignature(await alice.signer.sign(decrementTransaction.serializeForSigning())); - await provider.sendTransaction(decrementTransaction); - // Decrement #3 - - decrementTransaction = decrementInteraction.withNonce(17).buildTransaction(); - decrementTransaction.applySignature(await alice.signer.sign(decrementTransaction.serializeForSigning())); - provider.mockGetTransactionWithAnyHashAsNotarizedWithOneResult("@6f6b@05"); - let { - bundle: { firstValue: valueAfterDecrement }, - } = await controller.execute(decrementInteraction, decrementTransaction); - assert.deepEqual(valueAfterDecrement!.valueOf(), new BigNumber(5)); - }); - - it("should interact with 'lottery-esdt'", async function () { - setupUnitTestWatcherTimeouts(); - - let abiRegistry = await loadAbiRegistry("src/testdata/lottery-esdt.abi.json"); - let contract = new SmartContract({ address: dummyAddress, abi: abiRegistry }); - let controller = new ContractController(provider); - - let startInteraction = contract.methodsExplicit - .start([ - BytesValue.fromUTF8("lucky"), - new TokenIdentifierValue("lucky-token"), - new BigUIntValue(1), - OptionValue.newMissing(), - OptionValue.newMissing(), - OptionValue.newProvided(new U32Value(1)), - OptionValue.newMissing(), - OptionValue.newMissing(), - OptionalValue.newMissing(), - ]) - .withGasLimit(5000000) - .check(); - - let statusInteraction = contract.methods.status(["lucky"]).withGasLimit(5000000); - - let getLotteryInfoInteraction = contract.methods.getLotteryInfo(["lucky"]).withGasLimit(5000000); - - // start() - let startTransaction = startInteraction - .withSender(alice.address) - .withNonce(14) - .withChainID("mock") - .buildTransaction(); - - startTransaction.applySignature(await alice.signer.sign(startTransaction.serializeForSigning())); - provider.mockGetTransactionWithAnyHashAsNotarizedWithOneResult("@6f6b"); - let { - bundle: { returnCode: startReturnCode, values: startReturnValues }, - } = await controller.execute(startInteraction, startTransaction); - - assert.equal( - startTransaction.getData().toString(), - "start@6c75636b79@6c75636b792d746f6b656e@01@@@0100000001@@", - ); - assert.isTrue(startReturnCode.equals(ReturnCode.Ok)); - assert.lengthOf(startReturnValues, 0); - - // status() (this is a view function, but for the sake of the test, we'll execute it) - let statusTransaction = statusInteraction - .withSender(alice.address) - .withNonce(15) - .withChainID("mock") - .buildTransaction(); - - statusTransaction.applySignature(await alice.signer.sign(statusTransaction.serializeForSigning())); - provider.mockGetTransactionWithAnyHashAsNotarizedWithOneResult("@6f6b@01"); - let { - bundle: { returnCode: statusReturnCode, values: statusReturnValues, firstValue: statusFirstValue }, - } = await controller.execute(statusInteraction, statusTransaction); - - assert.equal(statusTransaction.getData().toString(), "status@6c75636b79"); - assert.isTrue(statusReturnCode.equals(ReturnCode.Ok)); - assert.lengthOf(statusReturnValues, 1); - 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 - .withSender(alice.address) - .withNonce(15) - .withChainID("mock") - .buildTransaction(); - - getLotteryInfoTransaction.applySignature( - await alice.signer.sign(getLotteryInfoTransaction.serializeForSigning()), - ); - provider.mockGetTransactionWithAnyHashAsNotarizedWithOneResult( - "@6f6b@0000000b6c75636b792d746f6b656e000000010100000000000000005fc2b9dbffffffff00000001640000000a140ec80fa7ee88000000", - ); - let { - bundle: { returnCode: infoReturnCode, values: infoReturnValues, firstValue: infoFirstValue }, - } = await controller.execute(getLotteryInfoInteraction, getLotteryInfoTransaction); - - assert.equal(getLotteryInfoTransaction.getData().toString(), "getLotteryInfo@6c75636b79"); - assert.isTrue(infoReturnCode.equals(ReturnCode.Ok)); - assert.lengthOf(infoReturnValues, 1); - - assert.deepEqual(infoFirstValue!.valueOf(), { - token_identifier: "lucky-token", - ticket_price: new BigNumber("1"), - tickets_left: new BigNumber(0), - deadline: new BigNumber("0x000000005fc2b9db", 16), - max_entries_per_user: new BigNumber(0xffffffff), - prize_distribution: Buffer.from([0x64]), - prize_pool: new BigNumber("94720000000000000000000"), - }); - }); -}); diff --git a/src/smartcontracts/interactionChecker.spec.ts b/src/smartcontracts/interactionChecker.spec.ts deleted file mode 100644 index 84da7dab0..000000000 --- a/src/smartcontracts/interactionChecker.spec.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { assert } from "chai"; -import { Address } from "../address"; -import * as errors from "../errors"; -import { loadAbiRegistry } from "../testutils"; -import { TokenTransfer } from "../tokens"; -import { Interaction } from "./interaction"; -import { InteractionChecker } from "./interactionChecker"; -import { SmartContract } from "./smartContract"; -import { - BigUIntType, - BigUIntValue, - OptionalType, - OptionalValue, - OptionValue, - TokenIdentifierValue, - U32Value, -} from "./typesystem"; -import { BytesValue } from "./typesystem/bytes"; - -describe("integration tests: test checker within interactor", function () { - let dummyAddress = new Address("erd1qqqqqqqqqqqqqpgqak8zt22wl2ph4tswtyc39namqx6ysa2sd8ss4xmlj3"); - let checker = new InteractionChecker(); - - it("should detect errors for 'ultimate answer'", async function () { - let abiRegistry = await loadAbiRegistry("src/testdata/answer.abi.json"); - let contract = new SmartContract({ address: dummyAddress, abi: abiRegistry }); - let endpoint = abiRegistry.getEndpoint("getUltimateAnswer"); - - // Send value to non-payable - assert.throw( - () => { - let interaction = (contract.methods.getUltimateAnswer()).withValue( - TokenTransfer.egldFromAmount(1), - ); - checker.checkInteraction(interaction, endpoint); - }, - errors.ErrContractInteraction, - "cannot send EGLD value to non-payable", - ); - - // Bad arguments - assert.throw( - () => { - contract.methods.getUltimateAnswer(["abba"]); - }, - Error, - "Wrong number of arguments for endpoint getUltimateAnswer: expected between 0 and 0 arguments, have 1", - ); - }); - - it("should detect errors for 'lottery'", async function () { - let abiRegistry = await loadAbiRegistry("src/testdata/lottery-esdt.abi.json"); - let contract = new SmartContract({ address: dummyAddress, abi: abiRegistry }); - let endpoint = abiRegistry.getEndpoint("start"); - - // Bad number of arguments - assert.throw( - () => { - contract.methods.start(["lucky", TokenTransfer.egldFromAmount(1)]); - }, - Error, - "Wrong number of arguments for endpoint start: expected between 8 and 9 arguments, have 2", - ); - - // Bad types (U64 instead of U32) - assert.throw( - () => { - let interaction = contract.methodsExplicit.start([ - BytesValue.fromUTF8("lucky"), - new TokenIdentifierValue("lucky-token"), - new BigUIntValue(1), - OptionValue.newMissing(), - OptionValue.newProvided(new U32Value(1)), - OptionValue.newProvided(new U32Value(1)), - OptionValue.newMissing(), - OptionValue.newMissing(), - new OptionalValue(new OptionalType(new BigUIntType())), - ]); - checker.checkInteraction(interaction, endpoint); - }, - errors.ErrContractInteraction, - "type mismatch at index 4, expected: Option, got: Option", - ); - }); -}); diff --git a/src/smartcontracts/interactionChecker.ts b/src/smartcontracts/interactionChecker.ts deleted file mode 100644 index 744525f56..000000000 --- a/src/smartcontracts/interactionChecker.ts +++ /dev/null @@ -1,59 +0,0 @@ -import * as errors from "../errors"; -import { EndpointDefinition } from "./typesystem"; -import { Interaction } from "./interaction"; -import BigNumber from "bignumber.js"; - -/** - * An interaction checker that aims to be as strict as possible. - * It is designed to catch programmer errors such as: - * - incorrect types of contract call arguments - * - errors related to calling "non-payable" functions with some value provided - * - gas estimation errors (not yet implemented) - */ -/** - * @deprecated The Interaction checker is deprecated due to lack of use. - */ -export class InteractionChecker { - checkInteraction(interaction: Interaction, definition: EndpointDefinition): void { - this.checkPayable(interaction, definition); - this.checkArguments(interaction, definition); - } - - private checkPayable(interaction: Interaction, definition: EndpointDefinition) { - let hasValue = !new BigNumber(interaction.getValue().toString()).isZero(); - let isPayableInEGLD = definition.modifiers.isPayableInEGLD(); - - if (hasValue && !isPayableInEGLD) { - throw new errors.ErrContractInteraction("cannot send EGLD value to non-payable"); - } - } - - private checkArguments(interaction: Interaction, definition: EndpointDefinition) { - let formalArguments = definition.input; - let actualArguments = interaction.getArguments(); - let numFormalArguments = formalArguments.length; - let numActualArguments = actualArguments.length; - - if (numFormalArguments != numActualArguments) { - throw new errors.ErrContractInteraction( - `bad arguments, expected: ${numFormalArguments}, got: ${numActualArguments}`, - ); - } - - // TODO: discuss again, possibly redesign the handling of covariance / contravariance. - - for (let i = 0; i < numFormalArguments; i++) { - let expectedType = formalArguments[i].type; - let argument = actualArguments[i]; - let actualType = argument.getType(); - // isAssignableFrom() is responsible to handle covariance and contravariance (depending on the class that overrides it). - let ok = expectedType.isAssignableFrom(actualType); - - if (!ok) { - throw new errors.ErrContractInteraction( - `type mismatch at index ${i}, expected: ${expectedType}, got: ${actualType}`, - ); - } - } - } -} diff --git a/src/smartcontracts/resultsParser.spec.ts b/src/smartcontracts/resultsParser.spec.ts deleted file mode 100644 index c21234d1c..000000000 --- a/src/smartcontracts/resultsParser.spec.ts +++ /dev/null @@ -1,379 +0,0 @@ -import BigNumber from "bignumber.js"; -import { assert } from "chai"; -import { Address } from "../address"; -import { IAddress } from "../interface"; -import { - ContractQueryResponse, - ContractResultItem, - ContractResults, - TransactionEventData, - TransactionEventOnNetwork, - TransactionEventTopic, - TransactionLogsOnNetwork, - TransactionOnNetwork, -} from "../networkProviders"; -import { loadAbiRegistry } from "../testutils"; -import { ArgSerializer } from "./argSerializer"; -import { ResultsParser } from "./resultsParser"; -import { ReturnCode } from "./returnCode"; -import { - AbiRegistry, - BigUIntType, - BigUIntValue, - EndpointDefinition, - EndpointModifiers, - EndpointParameterDefinition, - StringType, - StringValue, - TypedValue, - U32Type, - U32Value, - U64Type, - U64Value, - VariadicType, - VariadicValue, -} from "./typesystem"; -import { BytesType, BytesValue } from "./typesystem/bytes"; - -const KnownReturnCodes: string[] = [ - ReturnCode.None.valueOf(), - ReturnCode.Ok.valueOf(), - ReturnCode.FunctionNotFound.valueOf(), - ReturnCode.FunctionWrongSignature.valueOf(), - ReturnCode.ContractNotFound.valueOf(), - ReturnCode.UserError.valueOf(), - ReturnCode.OutOfGas.valueOf(), - ReturnCode.AccountCollision.valueOf(), - ReturnCode.OutOfFunds.valueOf(), - ReturnCode.CallStackOverFlow.valueOf(), - ReturnCode.ContractInvalid.valueOf(), - ReturnCode.ExecutionFailed.valueOf(), - // Returned by protocol, not by VM: - "insufficient funds", - "operation in account not permitted not the owner of the account", - "sending value to non payable contract", - "invalid receiver address", -]; - -describe("test smart contract results parser", () => { - let parser = new ResultsParser(); - - it("should create parser with custom dependencies (1)", async () => { - const customParser = new ResultsParser({ - argsSerializer: { - buffersToValues(_buffers, _parameters) { - return [new U64Value(42)]; - }, - stringToBuffers(_joinedString) { - return []; - }, - }, - }); - - const endpoint = new EndpointDefinition("", [], [], new EndpointModifiers("", [])); - const queryResponse = new ContractQueryResponse({}); - const bundle = customParser.parseQueryResponse(queryResponse, endpoint); - assert.deepEqual(bundle.firstValue, new U64Value(42)); - }); - - it("should create parser with custom dependencies (2)", async () => { - const customParser = new ResultsParser({ - argsSerializer: new ArgSerializer({ - codec: { - decodeTopLevel(_buffer, _type): TypedValue { - return new U64Value(42); - }, - encodeTopLevel(_typedValue): Buffer { - return Buffer.from([]); - }, - }, - }), - }); - - const outputParameters = [new EndpointParameterDefinition("", "", new U64Type())]; - const endpoint = new EndpointDefinition("", [], outputParameters, new EndpointModifiers("", [])); - const queryResponse = new ContractQueryResponse({ returnData: [""] }); - const bundle = customParser.parseQueryResponse(queryResponse, endpoint); - assert.deepEqual(bundle.firstValue, new U64Value(42)); - }); - - it("should parse query response", async () => { - let endpointModifiers = new EndpointModifiers("", []); - let outputParameters = [ - new EndpointParameterDefinition("a", "a", new BigUIntType()), - new EndpointParameterDefinition("b", "b", new BytesType()), - ]; - let endpoint = new EndpointDefinition("foo", [], outputParameters, endpointModifiers); - - let queryResponse = new ContractQueryResponse({ - returnData: [Buffer.from([42]).toString("base64"), Buffer.from("abba", "hex").toString("base64")], - returnCode: "ok", - returnMessage: "foobar", - }); - - let bundle = parser.parseQueryResponse(queryResponse, endpoint); - assert.deepEqual(bundle.returnCode, ReturnCode.Ok); - assert.equal(bundle.returnMessage, "foobar"); - assert.deepEqual(bundle.firstValue, new BigUIntValue(42)); - assert.deepEqual(bundle.secondValue, BytesValue.fromHex("abba")); - assert.lengthOf(bundle.values, 2); - }); - - it("should parse query response (variadic arguments)", async () => { - const endpointModifiers = new EndpointModifiers("", []); - const outputParameters = [new EndpointParameterDefinition("a", "a", new VariadicType(new U32Type(), false))]; - const endpoint = new EndpointDefinition("foo", [], outputParameters, endpointModifiers); - const queryResponse = new ContractQueryResponse({ - returnData: [Buffer.from([42]).toString("base64"), Buffer.from([43]).toString("base64")], - }); - - const bundle = parser.parseQueryResponse(queryResponse, endpoint); - assert.deepEqual(bundle.values[0], VariadicValue.fromItems(new U32Value(42), new U32Value(43))); - }); - - it("should parse query response (one counted-variadic arguments)", async () => { - const endpointModifiers = new EndpointModifiers("", []); - const outputParameters = [new EndpointParameterDefinition("a", "a", new VariadicType(new U32Type(), true))]; - const endpoint = new EndpointDefinition("foo", [], outputParameters, endpointModifiers); - const queryResponse = new ContractQueryResponse({ - returnData: [ - Buffer.from([2]).toString("base64"), - Buffer.from([42]).toString("base64"), - Buffer.from([43]).toString("base64"), - ], - }); - - const bundle = parser.parseQueryResponse(queryResponse, endpoint); - assert.deepEqual(bundle.values[0], VariadicValue.fromItemsCounted(new U32Value(42), new U32Value(43))); - }); - - it("should parse query response (multiple counted-variadic arguments)", async () => { - const endpointModifiers = new EndpointModifiers("", []); - const outputParameters = [ - new EndpointParameterDefinition("a", "a", new VariadicType(new U32Type(), true)), - new EndpointParameterDefinition("b", "b", new VariadicType(new StringType(), true)), - new EndpointParameterDefinition("c", "c", new BigUIntType()), - ]; - const endpoint = new EndpointDefinition("foo", [], outputParameters, endpointModifiers); - const queryResponse = new ContractQueryResponse({ - returnData: [ - Buffer.from([2]).toString("base64"), - Buffer.from([42]).toString("base64"), - Buffer.from([43]).toString("base64"), - Buffer.from([3]).toString("base64"), - Buffer.from("a").toString("base64"), - Buffer.from("b").toString("base64"), - Buffer.from("c").toString("base64"), - Buffer.from([42]).toString("base64"), - ], - }); - - const bundle = parser.parseQueryResponse(queryResponse, endpoint); - assert.deepEqual(bundle.values[0], VariadicValue.fromItemsCounted(new U32Value(42), new U32Value(43))); - assert.deepEqual( - bundle.values[1], - VariadicValue.fromItemsCounted(new StringValue("a"), new StringValue("b"), new StringValue("c")), - ); - assert.deepEqual(bundle.values[2], new BigUIntValue(42)); - }); - - it("should parse contract outcome", async () => { - let endpointModifiers = new EndpointModifiers("", []); - let outputParameters = [ - new EndpointParameterDefinition("a", "a", new BigUIntType()), - new EndpointParameterDefinition("b", "b", new BytesType()), - ]; - let endpoint = new EndpointDefinition("foo", [], outputParameters, endpointModifiers); - - let transactionOnNetwork = new TransactionOnNetwork({ - contractResults: new ContractResults([new ContractResultItem({ nonce: 7, data: "@6f6b@2a@abba" })]), - }); - - let bundle = parser.parseOutcome(transactionOnNetwork, endpoint); - assert.deepEqual(bundle.returnCode, ReturnCode.Ok); - assert.equal(bundle.returnMessage, "ok"); - assert.deepEqual(bundle.firstValue, new BigUIntValue(42)); - assert.deepEqual(bundle.secondValue, BytesValue.fromHex("abba")); - assert.lengthOf(bundle.values, 2); - }); - - it("should parse contract outcome, on easily found result with return data", async () => { - let transaction = new TransactionOnNetwork({ - contractResults: new ContractResults([ - new ContractResultItem({ - nonce: 42, - data: "@6f6b@03", - returnMessage: "foobar", - }), - ]), - }); - - let bundle = parser.parseUntypedOutcome(transaction); - assert.deepEqual(bundle.returnCode, ReturnCode.Ok); - assert.equal(bundle.returnMessage, "foobar"); - assert.deepEqual(bundle.values, [Buffer.from("03", "hex")]); - }); - - it("should parse contract outcome, on signal error", async () => { - let transaction = new TransactionOnNetwork({ - logs: new TransactionLogsOnNetwork({ - address: Address.empty(), - events: [ - new TransactionEventOnNetwork({ - identifier: "signalError", - topics: [new TransactionEventTopic(Buffer.from("something happened").toString("base64"))], - data: `@${Buffer.from("user error").toString("hex")}@07`, - }), - ], - }), - }); - - let bundle = parser.parseUntypedOutcome(transaction); - assert.deepEqual(bundle.returnCode, ReturnCode.UserError); - assert.equal(bundle.returnMessage, "something happened"); - assert.deepEqual(bundle.values, [Buffer.from("07", "hex")]); - }); - - it("should parse contract outcome, on too much gas warning", async () => { - let transaction = new TransactionOnNetwork({ - logs: new TransactionLogsOnNetwork({ - address: Address.empty(), - events: [ - new TransactionEventOnNetwork({ - identifier: "writeLog", - topics: [ - new TransactionEventTopic( - "QHRvbyBtdWNoIGdhcyBwcm92aWRlZCBmb3IgcHJvY2Vzc2luZzogZ2FzIHByb3ZpZGVkID0gNTk2Mzg0NTAwLCBnYXMgdXNlZCA9IDczMzAxMA==", - ), - ], - data: Buffer.from("QDZmNmI=", "base64").toString(), - }), - ], - }), - }); - - let bundle = parser.parseUntypedOutcome(transaction); - assert.deepEqual(bundle.returnCode, ReturnCode.Ok); - assert.equal(bundle.returnMessage, "ok"); - assert.deepEqual(bundle.values, []); - }); - - it("should parse contract event", async () => { - const abiRegistry = await loadAbiRegistry("src/testdata/esdt-safe.abi.json"); - const eventDefinition = abiRegistry.getEvent("deposit"); - - const event = new TransactionEventOnNetwork({ - topics: [ - new TransactionEventTopic("ZGVwb3NpdA=="), - new TransactionEventTopic("cmzC1LRt1r10pMhNAnFb+FyudjGMq4G8CefCYdQUmmc="), - new TransactionEventTopic("AAAADFdFR0xELTAxZTQ5ZAAAAAAAAAAAAAAAAWQ="), - ], - dataPayload: new TransactionEventData(Buffer.from("AAAAAAAAA9sAAAA=", "base64")), - }); - - const bundle = parser.parseEvent(event, eventDefinition); - - assert.equal( - (bundle.dest_address).bech32(), - "erd1wfkv9495dhtt6a9yepxsyu2mlpw2ua333j4cr0qfulpxr4q5nfnshgyqun", - ); - assert.equal(bundle.tokens[0].token_identifier, "WEGLD-01e49d"); - assert.deepEqual(bundle.tokens[0].token_nonce, new BigNumber(0)); - assert.deepEqual(bundle.tokens[0].amount, new BigNumber(100)); - assert.deepEqual(bundle.event_data.tx_nonce, new BigNumber(987)); - assert.isNull(bundle.event_data.opt_function); - assert.isNull(bundle.event_data.opt_arguments); - assert.isNull(bundle.event_data.opt_gas_limit); - }); - - it("should parse contract event (with multi-values)", async () => { - const abiRegistry = AbiRegistry.create({ - events: [ - { - identifier: "foobar", - inputs: [ - { - name: "a", - type: "multi", - indexed: true, - }, - { - name: "b", - type: "multi", - indexed: true, - }, - { - name: "c", - type: "u8", - indexed: false, - }, - ], - }, - ], - }); - - const eventDefinition = abiRegistry.getEvent("foobar"); - - const event = { - topics: [ - new TransactionEventTopic(Buffer.from("not used").toString("base64")), - new TransactionEventTopic(Buffer.from([42]).toString("base64")), - new TransactionEventTopic(Buffer.from("test").toString("base64")), - new TransactionEventTopic(Buffer.from([43]).toString("base64")), - new TransactionEventTopic(Buffer.from("test").toString("base64")), - new TransactionEventTopic(Buffer.from("test").toString("base64")), - new TransactionEventTopic(Buffer.from([44]).toString("base64")), - ], - dataPayload: new TransactionEventData(Buffer.from([42])), - }; - - const bundle = parser.parseEvent(event, eventDefinition); - assert.deepEqual(bundle.a, [new BigNumber(42), "test", new BigNumber(43), "test"]); - assert.deepEqual(bundle.b, ["test", new BigNumber(44)]); - assert.deepEqual(bundle.c, new BigNumber(42)); - }); - - it("should parse contract event (Sirius)", async () => { - const abiRegistry = AbiRegistry.create({ - events: [ - { - identifier: "foobar", - inputs: [ - { - name: "a", - type: "u8", - indexed: true, - }, - { - name: "b", - type: "u8", - indexed: false, - }, - { - name: "c", - type: "u8", - indexed: false, - }, - ], - }, - ], - }); - - const eventDefinition = abiRegistry.getEvent("foobar"); - - const event = { - topics: [ - new TransactionEventTopic(Buffer.from("not used").toString("base64")), - new TransactionEventTopic(Buffer.from([42]).toString("base64")), - ], - additionalData: [new TransactionEventData(Buffer.from([43])), new TransactionEventData(Buffer.from([44]))], - // Will be ignored. - dataPayload: new TransactionEventData(Buffer.from([43])), - }; - - const bundle = parser.parseEvent(event, eventDefinition); - assert.deepEqual(bundle.a, new BigNumber(42)); - assert.deepEqual(bundle.b, new BigNumber(43)); - assert.deepEqual(bundle.c, new BigNumber(44)); - }); -}); diff --git a/src/smartcontracts/resultsParser.ts b/src/smartcontracts/resultsParser.ts deleted file mode 100644 index 4b4d71860..000000000 --- a/src/smartcontracts/resultsParser.ts +++ /dev/null @@ -1,457 +0,0 @@ -import { - TransactionDecoder, - TransactionMetadata, -} from "@multiversx/sdk-transaction-decoder/lib/src/transaction.decoder"; -import { Address } from "../address"; -import { ErrCannotParseContractResults } from "../errors"; -import { IAddress } from "../interface"; -import { - IContractQueryResponse, - IContractResults, - ITransactionLogs, - ITransactionOnNetwork, -} from "../interfaceOfNetwork"; -import { Logger } from "../logger"; -import { ArgSerializer } from "./argSerializer"; -import { TypedOutcomeBundle, UntypedOutcomeBundle } from "./interface"; -import { ReturnCode } from "./returnCode"; -import { Type, TypedValue } from "./typesystem"; - -enum WellKnownEvents { - OnTransactionCompleted = "completedTxEvent", - OnSignalError = "signalError", - OnWriteLog = "writeLog", -} - -enum WellKnownTopics { - TooMuchGas = "@too much gas provided for processing", -} - -interface IResultsParserOptions { - argsSerializer: IArgsSerializer; -} - -interface IParameterDefinition { - type: Type; -} - -interface IEventInputDefinition { - name: string; - type: Type; - indexed: boolean; -} - -interface ITransactionEvent { - readonly topics: { valueOf(): Uint8Array }[]; - readonly dataPayload?: { valueOf(): Uint8Array }; - readonly additionalData?: { valueOf(): Uint8Array }[]; -} - -interface IArgsSerializer { - buffersToValues(buffers: Buffer[], parameters: IParameterDefinition[]): TypedValue[]; - stringToBuffers(joinedString: string): Buffer[]; -} - -// TODO: perhaps move default construction options to a factory (ResultsParserFactory), instead of referencing them in the constructor -// (postpone as much as possible, breaking change) -const defaultResultsParserOptions: IResultsParserOptions = { - argsSerializer: new ArgSerializer(), -}; - -/** - * Legacy component. - * For parsing contract query responses, use the "SmartContractQueriesController" instead. - * For parsing smart contract outcome (return data), use the "SmartContractTransactionsOutcomeParser" instead. - * For parding smart contract events, use the "TransactionEventsParser" instead. - * - * Parses contract query responses and smart contract results. - * The parsing involves some heuristics, in order to handle slight inconsistencies (e.g. some SCRs are present on API, but missing on Gateway). - */ -export class ResultsParser { - private readonly argsSerializer: IArgsSerializer; - - constructor(options?: IResultsParserOptions) { - options = { ...defaultResultsParserOptions, ...options }; - this.argsSerializer = options.argsSerializer; - } - - /** - * Legacy method, use "SmartContractQueriesController.parseQueryResponse()" instead. - */ - parseQueryResponse( - queryResponse: IContractQueryResponse, - endpoint: { output: IParameterDefinition[] }, - ): TypedOutcomeBundle { - let parts = queryResponse.getReturnDataParts(); - let values = this.argsSerializer.buffersToValues(parts, endpoint.output); - let returnCode = new ReturnCode(queryResponse.returnCode.toString()); - - return { - returnCode: returnCode, - returnMessage: queryResponse.returnMessage, - values: values, - firstValue: values[0], - secondValue: values[1], - thirdValue: values[2], - lastValue: values[values.length - 1], - }; - } - - /** - * Legacy method, use "SmartContractQueriesController.parseQueryResponse()" instead. - */ - parseUntypedQueryResponse(queryResponse: IContractQueryResponse): UntypedOutcomeBundle { - let returnCode = new ReturnCode(queryResponse.returnCode.toString()); - - return { - returnCode: returnCode, - returnMessage: queryResponse.returnMessage, - values: queryResponse.getReturnDataParts(), - }; - } - - /** - * Legacy method, use "SmartContractTransactionsOutcomeParser.parseExecute()" instead. - */ - parseOutcome(transaction: ITransactionOnNetwork, endpoint: { output: IParameterDefinition[] }): TypedOutcomeBundle { - const untypedBundle = this.parseUntypedOutcome(transaction); - const typedBundle = this.parseOutcomeFromUntypedBundle(untypedBundle, endpoint); - return typedBundle; - } - - /** - * @internal - * For internal use only. - */ - parseOutcomeFromUntypedBundle(bundle: UntypedOutcomeBundle, endpoint: { output: IParameterDefinition[] }) { - const values = this.argsSerializer.buffersToValues(bundle.values, endpoint.output); - - return { - returnCode: bundle.returnCode, - returnMessage: bundle.returnMessage, - values: values, - firstValue: values[0], - secondValue: values[1], - thirdValue: values[2], - lastValue: values[values.length - 1], - }; - } - - /** - * Legacy method, use "SmartContractTransactionsOutcomeParser.parseExecute()" instead. - */ - parseUntypedOutcome(transaction: ITransactionOnNetwork): UntypedOutcomeBundle { - let bundle: UntypedOutcomeBundle | null; - - let transactionMetadata = this.parseTransactionMetadata(transaction); - - bundle = this.createBundleOnSimpleMoveBalance(transaction); - if (bundle) { - Logger.trace("parseUntypedOutcome(): on simple move balance"); - return bundle; - } - - bundle = this.createBundleOnInvalidTransaction(transaction); - if (bundle) { - Logger.trace("parseUntypedOutcome(): on invalid transaction"); - return bundle; - } - - bundle = this.createBundleOnEasilyFoundResultWithReturnData(transaction.contractResults); - if (bundle) { - Logger.trace("parseUntypedOutcome(): on easily found result with return data"); - return bundle; - } - - bundle = this.createBundleOnSignalError(transaction.logs); - if (bundle) { - Logger.trace("parseUntypedOutcome(): on signal error"); - return bundle; - } - - bundle = this.createBundleOnTooMuchGasWarning(transaction.logs); - if (bundle) { - Logger.trace("parseUntypedOutcome(): on 'too much gas' warning"); - return bundle; - } - - bundle = this.createBundleOnWriteLogWhereFirstTopicEqualsAddress(transaction.logs, transaction.sender); - if (bundle) { - Logger.trace("parseUntypedOutcome(): on writelog with topics[0] == tx.sender"); - return bundle; - } - - bundle = this.createBundleWithCustomHeuristics(transaction, transactionMetadata); - if (bundle) { - Logger.trace("parseUntypedOutcome(): with custom heuristics"); - return bundle; - } - - bundle = this.createBundleWithFallbackHeuristics(transaction, transactionMetadata); - if (bundle) { - Logger.trace("parseUntypedOutcome(): with fallback heuristics"); - return bundle; - } - - throw new ErrCannotParseContractResults(`transaction ${transaction.hash.toString()}`); - } - - protected parseTransactionMetadata(transaction: ITransactionOnNetwork): TransactionMetadata { - return new TransactionDecoder().getTransactionMetadata({ - sender: transaction.sender.bech32(), - receiver: transaction.receiver.bech32(), - data: transaction.data.toString("base64"), - value: transaction.value.toString(), - }); - } - - protected createBundleOnSimpleMoveBalance(transaction: ITransactionOnNetwork): UntypedOutcomeBundle | null { - let noResults = transaction.contractResults.items.length == 0; - let noLogs = transaction.logs.events.length == 0; - - if (noResults && noLogs) { - return { - returnCode: ReturnCode.None, - returnMessage: ReturnCode.None.toString(), - values: [], - }; - } - - return null; - } - - protected createBundleOnInvalidTransaction(transaction: ITransactionOnNetwork): UntypedOutcomeBundle | null { - if (transaction.status.isInvalid()) { - if (transaction.receipt.data) { - return { - returnCode: ReturnCode.OutOfFunds, - returnMessage: transaction.receipt.data, - values: [], - }; - } - - // If there's no receipt message, let other heuristics to handle the outcome (most probably, a log with "signalError" is emitted). - } - - return null; - } - - protected createBundleOnEasilyFoundResultWithReturnData(results: IContractResults): UntypedOutcomeBundle | null { - let resultItemWithReturnData = results.items.find( - (item) => item.nonce.valueOf() != 0 && item.data.startsWith("@"), - ); - if (!resultItemWithReturnData) { - return null; - } - - let { returnCode, returnDataParts } = this.sliceDataFieldInParts(resultItemWithReturnData.data); - let returnMessage = resultItemWithReturnData.returnMessage || returnCode.toString(); - - return { - returnCode: returnCode, - returnMessage: returnMessage, - values: returnDataParts, - }; - } - - protected createBundleOnSignalError(logs: ITransactionLogs): UntypedOutcomeBundle | null { - let eventSignalError = logs.findSingleOrNoneEvent(WellKnownEvents.OnSignalError); - if (!eventSignalError) { - return null; - } - - let { returnCode, returnDataParts } = this.sliceDataFieldInParts(eventSignalError.data); - let lastTopic = eventSignalError.getLastTopic(); - let returnMessage = lastTopic?.toString() || returnCode.toString(); - - return { - returnCode: returnCode, - returnMessage: returnMessage, - values: returnDataParts, - }; - } - - protected createBundleOnTooMuchGasWarning(logs: ITransactionLogs): UntypedOutcomeBundle | null { - let eventTooMuchGas = logs.findSingleOrNoneEvent( - WellKnownEvents.OnWriteLog, - (event) => - event.findFirstOrNoneTopic((topic) => topic.toString().startsWith(WellKnownTopics.TooMuchGas)) != - undefined, - ); - - if (!eventTooMuchGas) { - return null; - } - - let { returnCode, returnDataParts } = this.sliceDataFieldInParts(eventTooMuchGas.data); - - return { - returnCode: returnCode, - returnMessage: returnCode.toString(), - values: returnDataParts, - }; - } - - protected createBundleOnWriteLogWhereFirstTopicEqualsAddress( - logs: ITransactionLogs, - address: IAddress, - ): UntypedOutcomeBundle | null { - let hexAddress = new Address(address.bech32()).hex(); - - let eventWriteLogWhereTopicIsSender = logs.findSingleOrNoneEvent( - WellKnownEvents.OnWriteLog, - (event) => event.findFirstOrNoneTopic((topic) => topic.hex() == hexAddress) != undefined, - ); - - if (!eventWriteLogWhereTopicIsSender) { - return null; - } - - let { returnCode, returnDataParts } = this.sliceDataFieldInParts(eventWriteLogWhereTopicIsSender.data); - let returnMessage = returnCode.toString(); - - return { - returnCode: returnCode, - returnMessage: returnMessage, - values: returnDataParts, - }; - } - - /** - * Override this method (in a subclass of {@link ResultsParser}) if the basic heuristics of the parser are not sufficient. - */ - protected createBundleWithCustomHeuristics( - _transaction: ITransactionOnNetwork, - _transactionMetadata: TransactionMetadata, - ): UntypedOutcomeBundle | null { - return null; - } - - protected createBundleWithFallbackHeuristics( - transaction: ITransactionOnNetwork, - transactionMetadata: TransactionMetadata, - ): UntypedOutcomeBundle | null { - let contractAddress = new Address(transactionMetadata.receiver); - - // Search the nested logs for matching events (writeLog): - for (const resultItem of transaction.contractResults.items) { - let writeLogWithReturnData = resultItem.logs.findSingleOrNoneEvent(WellKnownEvents.OnWriteLog, (event) => { - let addressIsSender = event.address.bech32() == transaction.sender.bech32(); - let firstTopicIsContract = event.topics[0]?.hex() == contractAddress.hex(); - return addressIsSender && firstTopicIsContract; - }); - - if (writeLogWithReturnData) { - let { returnCode, returnDataParts } = this.sliceDataFieldInParts(writeLogWithReturnData.data); - let returnMessage = returnCode.toString(); - - return { - returnCode: returnCode, - returnMessage: returnMessage, - values: returnDataParts, - }; - } - } - - // Additional fallback heuristics (alter search constraints): - for (const resultItem of transaction.contractResults.items) { - let writeLogWithReturnData = resultItem.logs.findSingleOrNoneEvent(WellKnownEvents.OnWriteLog, (event) => { - const addressIsContract = event.address.bech32() == contractAddress.toBech32(); - return addressIsContract; - }); - - if (writeLogWithReturnData) { - const { returnCode, returnDataParts } = this.sliceDataFieldInParts(writeLogWithReturnData.data); - const returnMessage = returnCode.toString(); - - return { - returnCode: returnCode, - returnMessage: returnMessage, - values: returnDataParts, - }; - } - } - - return null; - } - - protected sliceDataFieldInParts(data: string): { returnCode: ReturnCode; returnDataParts: Buffer[] } { - // By default, skip the first part, which is usually empty (e.g. "[empty]@6f6b") - let startingIndex = 1; - - // Before trying to parse the hex strings, cut the unwanted parts of the data field, in case of token transfers: - if (data.startsWith("ESDTTransfer")) { - // Skip "ESDTTransfer" (1), token identifier (2), amount (3) - startingIndex = 3; - } else { - // TODO: Upon gathering more transaction samples, fix for other kinds of transfers, as well (future PR, as needed). - } - - let parts = this.argsSerializer.stringToBuffers(data); - let returnCodePart = parts[startingIndex] || Buffer.from([]); - let returnDataParts = parts.slice(startingIndex + 1); - - if (returnCodePart.length == 0) { - throw new ErrCannotParseContractResults("no return code"); - } - - let returnCode = ReturnCode.fromBuffer(returnCodePart); - return { returnCode, returnDataParts }; - } - - /** - * Legacy method, use "TransactionEventsParser.parseEvent()" instead. - */ - parseEvent(transactionEvent: ITransactionEvent, eventDefinition: { inputs: IEventInputDefinition[] }): any { - // We skip the first topic, because, for log entries emitted by smart contracts, that's the same as the event identifier. See: - // https://github.com/multiversx/mx-chain-vm-go/blob/v1.5.27/vmhost/contexts/output.go#L283 - const topics = transactionEvent.topics.map((topic) => Buffer.from(topic.valueOf())).slice(1); - - // Before Sirius, there was no "additionalData" field on transaction logs. - // After Sirius, the "additionalData" field includes the "data" field, as well (as the first element): - // https://github.com/multiversx/mx-chain-go/blob/v1.6.18/process/transactionLog/process.go#L159 - // Right now, the logic below is duplicated (see "TransactionsConverter"). However, "ResultsParser" will be deprecated & removed at a later time. - const legacyData = transactionEvent.dataPayload?.valueOf() || Buffer.from([]); - const dataItems = transactionEvent.additionalData?.map((data) => Buffer.from(data.valueOf())) || []; - - if (dataItems.length === 0) { - if (legacyData.length) { - dataItems.push(Buffer.from(legacyData)); - } - } - - return this.doParseEvent({ topics, dataItems, eventDefinition }); - } - - /** - * @internal - * For internal use only. - * - * Once the legacy "ResultParser" is deprecated & removed, this logic will be absorbed into "TransactionEventsParser". - */ - doParseEvent(options: { - topics: Buffer[]; - dataItems: Buffer[]; - eventDefinition: { inputs: IEventInputDefinition[] }; - }): any { - const result: any = {}; - - // "Indexed" ABI "event.inputs" correspond to "event.topics[1:]": - const indexedInputs = options.eventDefinition.inputs.filter((input) => input.indexed); - const decodedTopics = this.argsSerializer.buffersToValues(options.topics, indexedInputs); - - for (let i = 0; i < indexedInputs.length; i++) { - result[indexedInputs[i].name] = decodedTopics[i].valueOf(); - } - - // "Non-indexed" ABI "event.inputs" correspond to "event.data": - const nonIndexedInputs = options.eventDefinition.inputs.filter((input) => !input.indexed); - const decodedDataParts = this.argsSerializer.buffersToValues(options.dataItems, nonIndexedInputs); - - for (let i = 0; i < nonIndexedInputs.length; i++) { - result[nonIndexedInputs[i].name] = decodedDataParts[i].valueOf(); - } - - return result; - } -} diff --git a/src/smartcontracts/smartContract.local.net.spec.ts b/src/smartcontracts/smartContract.local.net.spec.ts deleted file mode 100644 index 99219622b..000000000 --- a/src/smartcontracts/smartContract.local.net.spec.ts +++ /dev/null @@ -1,700 +0,0 @@ -import { assert } from "chai"; -import { promises } from "fs"; -import { QueryRunnerAdapter } from "../adapters/queryRunnerAdapter"; -import { Logger } from "../logger"; -import { SmartContractQueriesController } from "../smartContractQueriesController"; -import { prepareDeployment } from "../testutils"; -import { createLocalnetProvider } from "../testutils/networkProviders"; -import { loadTestWallets, TestWallet } from "../testutils/wallets"; -import { TransactionComputer } from "../transactionComputer"; -import { SmartContractTransactionsFactory } from "../transactionsFactories/smartContractTransactionsFactory"; -import { TransactionsFactoryConfig } from "../transactionsFactories/transactionsFactoryConfig"; -import { TransactionWatcher } from "../transactionWatcher"; -import { decodeUnsignedNumber } from "./codec"; -import { ContractFunction } from "./function"; -import { ResultsParser } from "./resultsParser"; -import { SmartContract } from "./smartContract"; -import { AddressValue, BigUIntValue, OptionalValue, OptionValue, TokenIdentifierValue, U32Value } from "./typesystem"; -import { BytesValue } from "./typesystem/bytes"; - -describe("test on local testnet", function () { - let alice: TestWallet, bob: TestWallet, carol: TestWallet; - let provider = createLocalnetProvider(); - let watcher: TransactionWatcher; - let resultsParser = new ResultsParser(); - - before(async function () { - ({ alice, bob, carol } = await loadTestWallets()); - - watcher = new TransactionWatcher({ - getTransaction: async (hash: string) => { - return await provider.getTransaction(hash, true); - }, - }); - }); - - it("counter: should deploy, then simulate transactions", async function () { - this.timeout(60000); - - TransactionWatcher.DefaultPollingInterval = 5000; - TransactionWatcher.DefaultTimeout = 50000; - - let network = await provider.getNetworkConfig(); - await alice.sync(provider); - - // Deploy - let contract = new SmartContract({}); - - let transactionDeploy = await prepareDeployment({ - contract: contract, - deployer: alice, - codePath: "src/testdata/counter.wasm", - gasLimit: 3000000, - initArguments: [], - chainID: network.ChainID, - }); - - // ++ - let transactionIncrement = contract.call({ - func: new ContractFunction("increment"), - gasLimit: 3000000, - chainID: network.ChainID, - caller: alice.address, - }); - transactionIncrement.setNonce(alice.account.nonce); - transactionIncrement.applySignature(await alice.signer.sign(transactionIncrement.serializeForSigning())); - - alice.account.incrementNonce(); - - // Now, let's build a few transactions, to be simulated - let simulateOne = contract.call({ - func: new ContractFunction("increment"), - gasLimit: 100000, - chainID: network.ChainID, - caller: alice.address, - }); - simulateOne.setSender(alice.address); - - let simulateTwo = contract.call({ - func: new ContractFunction("foobar"), - gasLimit: 500000, - chainID: network.ChainID, - caller: alice.address, - }); - simulateTwo.setSender(alice.address); - - simulateOne.setNonce(alice.account.nonce); - simulateTwo.setNonce(alice.account.nonce); - - simulateOne.applySignature(await alice.signer.sign(simulateOne.serializeForSigning())); - simulateTwo.applySignature(await alice.signer.sign(simulateTwo.serializeForSigning())); - - // Broadcast & execute - const txHashDeploy = await provider.sendTransaction(transactionDeploy); - const txHashIncrement = await provider.sendTransaction(transactionIncrement); - - await watcher.awaitCompleted(txHashDeploy); - let transactionOnNetwork = await provider.getTransaction(txHashDeploy); - let bundle = resultsParser.parseUntypedOutcome(transactionOnNetwork); - assert.isTrue(bundle.returnCode.isSuccess()); - - await watcher.awaitCompleted(txHashIncrement); - transactionOnNetwork = await provider.getTransaction(txHashIncrement); - bundle = resultsParser.parseUntypedOutcome(transactionOnNetwork); - assert.isTrue(bundle.returnCode.isSuccess()); - - // Simulate - Logger.trace(JSON.stringify(await provider.simulateTransaction(simulateOne), null, 4)); - Logger.trace(JSON.stringify(await provider.simulateTransaction(simulateTwo), null, 4)); - }); - - it("counter: should deploy, then simulate transactions using SmartContractTransactionsFactory", async function () { - this.timeout(60000); - - TransactionWatcher.DefaultPollingInterval = 5000; - TransactionWatcher.DefaultTimeout = 50000; - - let network = await provider.getNetworkConfig(); - await alice.sync(provider); - - const config = new TransactionsFactoryConfig({ chainID: network.ChainID }); - const factory = new SmartContractTransactionsFactory({ config: config }); - - const bytecode = await promises.readFile("src/testdata/counter.wasm"); - - const deployTransaction = factory.createTransactionForDeploy({ - sender: alice.address, - bytecode: bytecode, - gasLimit: 3000000n, - }); - deployTransaction.nonce = BigInt(alice.account.nonce.valueOf()); - - const transactionComputer = new TransactionComputer(); - deployTransaction.signature = await alice.signer.sign( - Buffer.from(transactionComputer.computeBytesForSigning(deployTransaction)), - ); - - const contractAddress = SmartContract.computeAddress(alice.address, alice.account.nonce); - alice.account.incrementNonce(); - - const smartContractCallTransaction = factory.createTransactionForExecute({ - sender: alice.address, - contract: contractAddress, - function: "increment", - gasLimit: 3000000n, - }); - smartContractCallTransaction.nonce = BigInt(alice.account.nonce.valueOf()); - smartContractCallTransaction.signature = await alice.signer.sign( - Buffer.from(transactionComputer.computeBytesForSigning(smartContractCallTransaction)), - ); - - alice.account.incrementNonce(); - - const simulateOne = factory.createTransactionForExecute({ - sender: alice.address, - function: "increment", - contract: contractAddress, - gasLimit: 100000n, - }); - - simulateOne.nonce = BigInt(alice.account.nonce.valueOf()); - simulateOne.signature = await alice.signer.sign( - Buffer.from(transactionComputer.computeBytesForSigning(simulateOne)), - ); - - alice.account.incrementNonce(); - - const simulateTwo = factory.createTransactionForExecute({ - sender: alice.address, - function: "foobar", - contract: contractAddress, - gasLimit: 500000n, - }); - - simulateTwo.nonce = BigInt(alice.account.nonce.valueOf()); - simulateTwo.signature = await alice.signer.sign( - Buffer.from(transactionComputer.computeBytesForSigning(simulateTwo)), - ); - - alice.account.incrementNonce(); - - // Broadcast & execute - const deployTxHash = await provider.sendTransaction(deployTransaction); - const callTxHash = await provider.sendTransaction(smartContractCallTransaction); - - await watcher.awaitCompleted(deployTxHash); - let transactionOnNetwork = await provider.getTransaction(deployTxHash); - let bundle = resultsParser.parseUntypedOutcome(transactionOnNetwork); - assert.isTrue(bundle.returnCode.isSuccess()); - - await watcher.awaitCompleted(callTxHash); - transactionOnNetwork = await provider.getTransaction(callTxHash); - bundle = resultsParser.parseUntypedOutcome(transactionOnNetwork); - assert.isTrue(bundle.returnCode.isSuccess()); - - // Simulate - Logger.trace(JSON.stringify(await provider.simulateTransaction(simulateOne), null, 4)); - Logger.trace(JSON.stringify(await provider.simulateTransaction(simulateTwo), null, 4)); - }); - - it("counter: should deploy, call and query contract", async function () { - this.timeout(80000); - - TransactionWatcher.DefaultPollingInterval = 5000; - TransactionWatcher.DefaultTimeout = 50000; - - let network = await provider.getNetworkConfig(); - await alice.sync(provider); - - // Deploy - let contract = new SmartContract({}); - - let transactionDeploy = await prepareDeployment({ - contract: contract, - deployer: alice, - codePath: "src/testdata/counter.wasm", - gasLimit: 3000000, - initArguments: [], - chainID: network.ChainID, - }); - - // ++ - let transactionIncrementFirst = contract.call({ - func: new ContractFunction("increment"), - gasLimit: 2000000, - chainID: network.ChainID, - caller: alice.address, - }); - transactionIncrementFirst.setNonce(alice.account.nonce); - transactionIncrementFirst.applySignature( - await alice.signer.sign(transactionIncrementFirst.serializeForSigning()), - ); - - alice.account.incrementNonce(); - - // ++ - let transactionIncrementSecond = contract.call({ - func: new ContractFunction("increment"), - gasLimit: 2000000, - chainID: network.ChainID, - caller: alice.address, - }); - transactionIncrementSecond.setNonce(alice.account.nonce); - transactionIncrementSecond.applySignature( - await alice.signer.sign(transactionIncrementSecond.serializeForSigning()), - ); - - alice.account.incrementNonce(); - - // Broadcast & execute - await provider.sendTransaction(transactionDeploy); - await provider.sendTransaction(transactionIncrementFirst); - await provider.sendTransaction(transactionIncrementSecond); - - await watcher.awaitCompleted(transactionDeploy.getHash().hex()); - await watcher.awaitCompleted(transactionIncrementFirst.getHash().hex()); - await watcher.awaitCompleted(transactionIncrementSecond.getHash().hex()); - - // Check counter - let query = contract.createQuery({ func: new ContractFunction("get") }); - let queryResponse = await provider.queryContract(query); - assert.lengthOf(queryResponse.getReturnDataParts(), 1); - assert.equal(3, decodeUnsignedNumber(queryResponse.getReturnDataParts()[0])); - }); - - it("counter: should deploy, call and query contract using SmartContractTransactionsFactory", async function () { - this.timeout(80000); - - TransactionWatcher.DefaultPollingInterval = 5000; - TransactionWatcher.DefaultTimeout = 50000; - - let network = await provider.getNetworkConfig(); - await alice.sync(provider); - - const config = new TransactionsFactoryConfig({ chainID: network.ChainID }); - const factory = new SmartContractTransactionsFactory({ config: config }); - - const bytecode = await promises.readFile("src/testdata/counter.wasm"); - - const deployTransaction = factory.createTransactionForDeploy({ - sender: alice.address, - bytecode: bytecode, - gasLimit: 3000000n, - }); - deployTransaction.nonce = BigInt(alice.account.nonce.valueOf()); - - const transactionComputer = new TransactionComputer(); - deployTransaction.signature = await alice.signer.sign( - Buffer.from(transactionComputer.computeBytesForSigning(deployTransaction)), - ); - - const contractAddress = SmartContract.computeAddress(alice.address, alice.account.nonce); - alice.account.incrementNonce(); - - const firstScCallTransaction = factory.createTransactionForExecute({ - sender: alice.address, - contract: contractAddress, - function: "increment", - gasLimit: 3000000n, - }); - firstScCallTransaction.nonce = BigInt(alice.account.nonce.valueOf()); - firstScCallTransaction.signature = await alice.signer.sign( - Buffer.from(transactionComputer.computeBytesForSigning(firstScCallTransaction)), - ); - - alice.account.incrementNonce(); - - const secondScCallTransaction = factory.createTransactionForExecute({ - sender: alice.address, - contract: contractAddress, - function: "increment", - gasLimit: 3000000n, - }); - secondScCallTransaction.nonce = BigInt(alice.account.nonce.valueOf()); - secondScCallTransaction.signature = await alice.signer.sign( - Buffer.from(transactionComputer.computeBytesForSigning(secondScCallTransaction)), - ); - - alice.account.incrementNonce(); - - // Broadcast & execute - const deployTxHash = await provider.sendTransaction(deployTransaction); - const firstScCallHash = await provider.sendTransaction(firstScCallTransaction); - const secondScCallHash = await provider.sendTransaction(secondScCallTransaction); - - await watcher.awaitCompleted(deployTxHash); - await watcher.awaitCompleted(firstScCallHash); - await watcher.awaitCompleted(secondScCallHash); - - // Check counter - const queryRunner = new QueryRunnerAdapter({ networkProvider: provider }); - const smartContractQueriesController = new SmartContractQueriesController({ queryRunner: queryRunner }); - - const query = smartContractQueriesController.createQuery({ - contract: contractAddress.bech32(), - function: "get", - arguments: [], - }); - - const queryResponse = await smartContractQueriesController.runQuery(query); - assert.lengthOf(queryResponse.returnDataParts, 1); - assert.equal(3, decodeUnsignedNumber(Buffer.from(queryResponse.returnDataParts[0]))); - }); - - it("erc20: should deploy, call and query contract", async function () { - this.timeout(60000); - - TransactionWatcher.DefaultPollingInterval = 5000; - TransactionWatcher.DefaultTimeout = 50000; - - let network = await provider.getNetworkConfig(); - await alice.sync(provider); - - // Deploy - let contract = new SmartContract({}); - - let transactionDeploy = await prepareDeployment({ - contract: contract, - deployer: alice, - codePath: "src/testdata/erc20.wasm", - gasLimit: 50000000, - initArguments: [new U32Value(10000)], - chainID: network.ChainID, - }); - - // Minting - let transactionMintBob = contract.call({ - func: new ContractFunction("transferToken"), - gasLimit: 9000000, - args: [new AddressValue(bob.address), new U32Value(1000)], - chainID: network.ChainID, - caller: alice.address, - }); - - let transactionMintCarol = contract.call({ - func: new ContractFunction("transferToken"), - gasLimit: 9000000, - args: [new AddressValue(carol.address), new U32Value(1500)], - chainID: network.ChainID, - caller: alice.address, - }); - - // Apply nonces and sign the remaining transactions - transactionMintBob.setNonce(alice.account.nonce); - alice.account.incrementNonce(); - transactionMintCarol.setNonce(alice.account.nonce); - alice.account.incrementNonce(); - - transactionMintBob.applySignature(await alice.signer.sign(transactionMintBob.serializeForSigning())); - transactionMintCarol.applySignature(await alice.signer.sign(transactionMintCarol.serializeForSigning())); - - // Broadcast & execute - await provider.sendTransaction(transactionDeploy); - await provider.sendTransaction(transactionMintBob); - await provider.sendTransaction(transactionMintCarol); - - await watcher.awaitCompleted(transactionDeploy.getHash().hex()); - await watcher.awaitCompleted(transactionMintBob.getHash().hex()); - await watcher.awaitCompleted(transactionMintCarol.getHash().hex()); - - // Query state, do some assertions - let query = contract.createQuery({ func: new ContractFunction("totalSupply") }); - let queryResponse = await provider.queryContract(query); - assert.lengthOf(queryResponse.getReturnDataParts(), 1); - assert.equal(10000, decodeUnsignedNumber(queryResponse.getReturnDataParts()[0])); - - query = contract.createQuery({ - func: new ContractFunction("balanceOf"), - args: [new AddressValue(alice.address)], - }); - queryResponse = await provider.queryContract(query); - - assert.equal(7500, decodeUnsignedNumber(queryResponse.getReturnDataParts()[0])); - - query = contract.createQuery({ - func: new ContractFunction("balanceOf"), - args: [new AddressValue(bob.address)], - }); - queryResponse = await provider.queryContract(query); - - assert.equal(1000, decodeUnsignedNumber(queryResponse.getReturnDataParts()[0])); - - query = contract.createQuery({ - func: new ContractFunction("balanceOf"), - args: [new AddressValue(carol.address)], - }); - queryResponse = await provider.queryContract(query); - - assert.equal(1500, decodeUnsignedNumber(queryResponse.getReturnDataParts()[0])); - }); - - it("erc20: should deploy, call and query contract using SmartContractTransactionsFactory", async function () { - this.timeout(60000); - - TransactionWatcher.DefaultPollingInterval = 5000; - TransactionWatcher.DefaultTimeout = 50000; - - let network = await provider.getNetworkConfig(); - await alice.sync(provider); - - const config = new TransactionsFactoryConfig({ chainID: network.ChainID }); - const factory = new SmartContractTransactionsFactory({ config: config }); - - const bytecode = await promises.readFile("src/testdata/erc20.wasm"); - - const deployTransaction = factory.createTransactionForDeploy({ - sender: alice.address, - bytecode: bytecode, - gasLimit: 50000000n, - arguments: [new U32Value(10000)], - }); - deployTransaction.nonce = BigInt(alice.account.nonce.valueOf()); - const transactionComputer = new TransactionComputer(); - deployTransaction.signature = await alice.signer.sign( - Buffer.from(transactionComputer.computeBytesForSigning(deployTransaction)), - ); - - const contractAddress = SmartContract.computeAddress(alice.address, alice.account.nonce); - alice.account.incrementNonce(); - - const transactionMintBob = factory.createTransactionForExecute({ - sender: alice.address, - contract: contractAddress, - function: "transferToken", - gasLimit: 9000000n, - arguments: [new AddressValue(bob.address), new U32Value(1000)], - }); - transactionMintBob.nonce = BigInt(alice.account.nonce.valueOf()); - transactionMintBob.signature = await alice.signer.sign( - Buffer.from(transactionComputer.computeBytesForSigning(transactionMintBob)), - ); - - alice.account.incrementNonce(); - - const transactionMintCarol = factory.createTransactionForExecute({ - sender: alice.address, - contract: contractAddress, - function: "transferToken", - gasLimit: 9000000n, - arguments: [new AddressValue(carol.address), new U32Value(1500)], - }); - transactionMintCarol.nonce = BigInt(alice.account.nonce.valueOf()); - transactionMintCarol.signature = await alice.signer.sign( - Buffer.from(transactionComputer.computeBytesForSigning(transactionMintCarol)), - ); - - alice.account.incrementNonce(); - - // Broadcast & execute - const deployTxHash = await provider.sendTransaction(deployTransaction); - const mintBobTxHash = await provider.sendTransaction(transactionMintBob); - const mintCarolTxHash = await provider.sendTransaction(transactionMintCarol); - - await watcher.awaitCompleted(deployTxHash); - await watcher.awaitCompleted(mintBobTxHash); - await watcher.awaitCompleted(mintCarolTxHash); - - // Query state, do some assertions - const queryRunner = new QueryRunnerAdapter({ networkProvider: provider }); - const smartContractQueriesController = new SmartContractQueriesController({ queryRunner: queryRunner }); - - let query = smartContractQueriesController.createQuery({ - contract: contractAddress.bech32(), - function: "totalSupply", - arguments: [], - }); - let queryResponse = await smartContractQueriesController.runQuery(query); - assert.lengthOf(queryResponse.returnDataParts, 1); - assert.equal(10000, decodeUnsignedNumber(Buffer.from(queryResponse.returnDataParts[0]))); - - query = smartContractQueriesController.createQuery({ - contract: contractAddress.bech32(), - function: "balanceOf", - arguments: [new AddressValue(alice.address)], - }); - queryResponse = await smartContractQueriesController.runQuery(query); - assert.equal(7500, decodeUnsignedNumber(Buffer.from(queryResponse.returnDataParts[0]))); - - query = smartContractQueriesController.createQuery({ - contract: contractAddress.bech32(), - function: "balanceOf", - arguments: [new AddressValue(bob.address)], - }); - queryResponse = await smartContractQueriesController.runQuery(query); - assert.equal(1000, decodeUnsignedNumber(Buffer.from(queryResponse.returnDataParts[0]))); - - query = smartContractQueriesController.createQuery({ - contract: contractAddress.bech32(), - function: "balanceOf", - arguments: [new AddressValue(carol.address)], - }); - queryResponse = await smartContractQueriesController.runQuery(query); - assert.equal(1500, decodeUnsignedNumber(Buffer.from(queryResponse.returnDataParts[0]))); - }); - - it("lottery: should deploy, call and query contract", async function () { - this.timeout(60000); - - TransactionWatcher.DefaultPollingInterval = 5000; - TransactionWatcher.DefaultTimeout = 50000; - - let network = await provider.getNetworkConfig(); - await alice.sync(provider); - - // Deploy - let contract = new SmartContract({}); - - let transactionDeploy = await prepareDeployment({ - contract: contract, - deployer: alice, - codePath: "src/testdata/lottery-esdt.wasm", - gasLimit: 50000000, - initArguments: [], - chainID: network.ChainID, - }); - - // Start - let transactionStart = contract.call({ - func: new ContractFunction("start"), - gasLimit: 10000000, - args: [ - BytesValue.fromUTF8("lucky"), - new TokenIdentifierValue("EGLD"), - new BigUIntValue(1), - OptionValue.newMissing(), - OptionValue.newMissing(), - OptionValue.newProvided(new U32Value(1)), - OptionValue.newMissing(), - OptionValue.newMissing(), - OptionalValue.newMissing(), - ], - chainID: network.ChainID, - caller: alice.address, - }); - // Apply nonces and sign the remaining transactions - transactionStart.setNonce(alice.account.nonce); - - transactionStart.applySignature(await alice.signer.sign(transactionStart.serializeForSigning())); - - // Broadcast & execute - await provider.sendTransaction(transactionDeploy); - await provider.sendTransaction(transactionStart); - - await watcher.awaitAllEvents(transactionDeploy.getHash().hex(), ["SCDeploy"]); - await watcher.awaitAnyEvent(transactionStart.getHash().hex(), ["completedTxEvent"]); - - // Let's check the SCRs - let transactionOnNetwork = await provider.getTransaction(transactionDeploy.getHash().hex()); - let bundle = resultsParser.parseUntypedOutcome(transactionOnNetwork); - assert.isTrue(bundle.returnCode.isSuccess()); - - transactionOnNetwork = await provider.getTransaction(transactionStart.getHash().hex()); - bundle = resultsParser.parseUntypedOutcome(transactionOnNetwork); - assert.isTrue(bundle.returnCode.isSuccess()); - - // Query state, do some assertions - let query = contract.createQuery({ - func: new ContractFunction("status"), - args: [BytesValue.fromUTF8("lucky")], - }); - let queryResponse = await provider.queryContract(query); - assert.equal(decodeUnsignedNumber(queryResponse.getReturnDataParts()[0]), 1); - - query = contract.createQuery({ - func: new ContractFunction("status"), - args: [BytesValue.fromUTF8("missingLottery")], - }); - queryResponse = await provider.queryContract(query); - assert.equal(decodeUnsignedNumber(queryResponse.getReturnDataParts()[0]), 0); - }); - - it("lottery: should deploy, call and query contract using SmartContractTransactionsFactory", async function () { - this.timeout(60000); - - TransactionWatcher.DefaultPollingInterval = 5000; - TransactionWatcher.DefaultTimeout = 50000; - - let network = await provider.getNetworkConfig(); - await alice.sync(provider); - - const config = new TransactionsFactoryConfig({ chainID: network.ChainID }); - const factory = new SmartContractTransactionsFactory({ config: config }); - - const bytecode = await promises.readFile("src/testdata/lottery-esdt.wasm"); - - const deployTransaction = factory.createTransactionForDeploy({ - sender: alice.address, - bytecode: bytecode, - gasLimit: 50000000n, - }); - deployTransaction.nonce = BigInt(alice.account.nonce.valueOf()); - - const transactionComputer = new TransactionComputer(); - deployTransaction.signature = await alice.signer.sign( - Buffer.from(transactionComputer.computeBytesForSigning(deployTransaction)), - ); - - const contractAddress = SmartContract.computeAddress(alice.address, alice.account.nonce); - alice.account.incrementNonce(); - - const startTransaction = factory.createTransactionForExecute({ - sender: alice.address, - contract: contractAddress, - function: "start", - gasLimit: 10000000n, - arguments: [ - BytesValue.fromUTF8("lucky"), - new TokenIdentifierValue("EGLD"), - new BigUIntValue(1), - OptionValue.newMissing(), - OptionValue.newMissing(), - OptionValue.newProvided(new U32Value(1)), - OptionValue.newMissing(), - OptionValue.newMissing(), - OptionalValue.newMissing(), - ], - }); - startTransaction.nonce = BigInt(alice.account.nonce.valueOf()); - startTransaction.signature = await alice.signer.sign( - Buffer.from(transactionComputer.computeBytesForSigning(startTransaction)), - ); - - alice.account.incrementNonce(); - - // Broadcast & execute - const deployTx = await provider.sendTransaction(deployTransaction); - const startTx = await provider.sendTransaction(startTransaction); - - await watcher.awaitAllEvents(deployTx, ["SCDeploy"]); - await watcher.awaitAnyEvent(startTx, ["completedTxEvent"]); - - // Let's check the SCRs - let transactionOnNetwork = await provider.getTransaction(deployTx); - let bundle = resultsParser.parseUntypedOutcome(transactionOnNetwork); - assert.isTrue(bundle.returnCode.isSuccess()); - - transactionOnNetwork = await provider.getTransaction(startTx); - bundle = resultsParser.parseUntypedOutcome(transactionOnNetwork); - assert.isTrue(bundle.returnCode.isSuccess()); - - // Query state, do some assertions - const queryRunner = new QueryRunnerAdapter({ networkProvider: provider }); - const smartContractQueriesController = new SmartContractQueriesController({ queryRunner: queryRunner }); - - let query = smartContractQueriesController.createQuery({ - contract: contractAddress.bech32(), - function: "status", - arguments: [BytesValue.fromUTF8("lucky")], - }); - let queryResponse = await smartContractQueriesController.runQuery(query); - assert.equal(decodeUnsignedNumber(Buffer.from(queryResponse.returnDataParts[0])), 1); - - query = smartContractQueriesController.createQuery({ - contract: contractAddress.bech32(), - function: "status", - arguments: [BytesValue.fromUTF8("missingLottery")], - }); - queryResponse = await smartContractQueriesController.runQuery(query); - assert.equal(decodeUnsignedNumber(Buffer.from(queryResponse.returnDataParts[0])), 0); - }); -}); diff --git a/src/smartcontracts/transactionPayloadBuilders.spec.ts b/src/smartcontracts/transactionPayloadBuilders.spec.ts deleted file mode 100644 index 884c48b06..000000000 --- a/src/smartcontracts/transactionPayloadBuilders.spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { assert } from "chai"; -import { Address } from "../address"; -import { ContractFunction } from "./function"; -import { Code } from "./code"; -import { CodeMetadata } from "./codeMetadata"; -import { AddressValue, U32Value } from "./typesystem"; -import { - ContractCallPayloadBuilder, - ContractDeployPayloadBuilder, - ContractUpgradePayloadBuilder, -} from "./transactionPayloadBuilders"; - -describe("test contract payload builders", () => { - it("should prepare deploy correctly", async () => { - let payload = new ContractDeployPayloadBuilder() - .setCode(Code.fromBuffer(Buffer.from([1, 2, 3, 4]))) - .setCodeMetadata(new CodeMetadata(true, false, true)) - .addInitArg(new U32Value(1024)) - .build(); - - assert.equal(payload.valueOf().toString(), "01020304@0500@0102@0400"); - }); - - it("should prepare upgrade correctly", async () => { - let payload = new ContractUpgradePayloadBuilder() - .setCode(Code.fromBuffer(Buffer.from([1, 2, 3, 4]))) - .setCodeMetadata(new CodeMetadata(true, false, true)) - .addInitArg(new U32Value(1024)) - .build(); - - assert.equal(payload.valueOf().toString(), "upgradeContract@01020304@0102@0400"); - }); - - it("should prepare call correctly", async () => { - let alice = new Address("erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz"); - let payload = new ContractCallPayloadBuilder() - .setFunction(new ContractFunction("transferToken")) - .addArg(new AddressValue(alice)) - .addArg(new U32Value(1024)) - .build(); - - assert.equal( - payload.valueOf().toString(), - "transferToken@fd691bb5e85d102687d81079dffce842d4dc328276d2d4c60d8fd1c3433c3293@0400", - ); - }); -}); diff --git a/src/smartcontracts/transactionPayloadBuilders.ts b/src/smartcontracts/transactionPayloadBuilders.ts deleted file mode 100644 index 215b9e729..000000000 --- a/src/smartcontracts/transactionPayloadBuilders.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { WasmVirtualMachine } from "../constants"; -import { TransactionPayload } from "../transactionPayload"; -import { guardValueIsSet } from "../utils"; -import { ArgSerializer } from "./argSerializer"; -import { ICode, ICodeMetadata, IContractFunction } from "./interface"; -import { TypedValue } from "./typesystem"; - -/** - * @deprecated Use {@link SmartContractTransactionsFactory} instead. - * - * A builder for {@link TransactionPayload} objects, to be used for Smart Contract deployment transactions. - */ -export class ContractDeployPayloadBuilder { - private code: ICode | null = null; - private codeMetadata: ICodeMetadata = ""; - private arguments: TypedValue[] = []; - - /** - * Sets the code of the Smart Contract. - */ - setCode(code: ICode): ContractDeployPayloadBuilder { - this.code = code; - return this; - } - - /** - * Sets the code metadata of the Smart Contract. - */ - setCodeMetadata(codeMetadata: ICodeMetadata): ContractDeployPayloadBuilder { - this.codeMetadata = codeMetadata; - return this; - } - - /** - * Adds constructor (`init`) arguments. - */ - addInitArg(arg: TypedValue): ContractDeployPayloadBuilder { - this.arguments.push(arg); - return this; - } - - /** - * Sets constructor (`init`) arguments. - */ - setInitArgs(args: TypedValue[]): ContractDeployPayloadBuilder { - this.arguments = args; - return this; - } - - /** - * Builds the {@link TransactionPayload}. - */ - build(): TransactionPayload { - guardValueIsSet("code", this.code); - - let code = this.code!.toString(); - let codeMetadata = this.codeMetadata.toString(); - let data = `${code}@${WasmVirtualMachine}@${codeMetadata}`; - data = appendArgumentsToString(data, this.arguments); - - return new TransactionPayload(data); - } -} - -/** - * @deprecated Use {@link SmartContractTransactionsFactory} instead. - * - * A builder for {@link TransactionPayload} objects, to be used for Smart Contract upgrade transactions. - */ -export class ContractUpgradePayloadBuilder { - private code: ICode | null = null; - private codeMetadata: ICodeMetadata = ""; - private arguments: TypedValue[] = []; - - /** - * Sets the code of the Smart Contract. - */ - setCode(code: ICode): ContractUpgradePayloadBuilder { - this.code = code; - return this; - } - - /** - * Sets the code metadata of the Smart Contract. - */ - setCodeMetadata(codeMetadata: ICodeMetadata): ContractUpgradePayloadBuilder { - this.codeMetadata = codeMetadata; - return this; - } - - /** - * Adds upgrade (`init`) arguments. - */ - addInitArg(arg: TypedValue): ContractUpgradePayloadBuilder { - this.arguments.push(arg); - return this; - } - - /** - * Sets upgrade (`init`) arguments. - */ - setInitArgs(args: TypedValue[]): ContractUpgradePayloadBuilder { - this.arguments = args; - return this; - } - - /** - * Builds the {@link TransactionPayload}. - */ - build(): TransactionPayload { - guardValueIsSet("code", this.code); - - let code = this.code!.toString(); - let codeMetadata = this.codeMetadata.toString(); - let data = `upgradeContract@${code}@${codeMetadata}`; - data = appendArgumentsToString(data, this.arguments); - - return new TransactionPayload(data); - } -} - -/** - * @deprecated Use {@link SmartContractTransactionsFactory} instead. - * - * A builder for {@link TransactionPayload} objects, to be used for Smart Contract execution transactions. - */ -export class ContractCallPayloadBuilder { - private contractFunction: IContractFunction | null = null; - private arguments: TypedValue[] = []; - - /** - * Sets the function to be called (executed). - */ - setFunction(contractFunction: IContractFunction): ContractCallPayloadBuilder { - this.contractFunction = contractFunction; - return this; - } - - /** - * Adds a function argument. - */ - addArg(arg: TypedValue): ContractCallPayloadBuilder { - this.arguments.push(arg); - return this; - } - - /** - * Sets the function arguments. - */ - setArgs(args: TypedValue[]): ContractCallPayloadBuilder { - this.arguments = args; - return this; - } - - /** - * Builds the {@link TransactionPayload}. - */ - build(): TransactionPayload { - guardValueIsSet("calledFunction", this.contractFunction); - - let data = this.contractFunction!.toString(); - data = appendArgumentsToString(data, this.arguments); - - return new TransactionPayload(data); - } -} - -function appendArgumentsToString(to: string, values: TypedValue[]) { - let { argumentsString, count } = new ArgSerializer().valuesToString(values); - if (count == 0) { - return to; - } - return `${to}@${argumentsString}`; -} diff --git a/src/testdata/testwallets/alice.json b/src/testdata/testwallets/alice.json index 9e83170cf..18c4ea1e9 100644 --- a/src/testdata/testwallets/alice.json +++ b/src/testdata/testwallets/alice.json @@ -1,5 +1,6 @@ { "version": 4, + "kind": "secretKey", "id": "0dc10c02-b59b-4bac-9710-6b2cfa4284ba", "address": "0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1", "bech32": "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", diff --git a/src/testdata/testwallets/bob.json b/src/testdata/testwallets/bob.json index 439b394a5..9efb41109 100644 --- a/src/testdata/testwallets/bob.json +++ b/src/testdata/testwallets/bob.json @@ -1,5 +1,6 @@ { "version": 4, + "kind": "secretKey", "id": "85fdc8a7-7119-479d-b7fb-ab4413ed038d", "address": "8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8", "bech32": "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx", diff --git a/src/testdata/testwallets/carol.json b/src/testdata/testwallets/carol.json index 3614a5ba2..1014a8230 100644 --- a/src/testdata/testwallets/carol.json +++ b/src/testdata/testwallets/carol.json @@ -1,5 +1,6 @@ { "version": 4, + "kind": "secretKey", "id": "65894f35-d142-41d2-9335-6ad02e0ed0be", "address": "b2a11555ce521e4944e09ab17549d85b487dcd26c84b5017a39e31a3670889ba", "bech32": "erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8", diff --git a/src/testdata/testwallets/dan.json b/src/testdata/testwallets/dan.json index 15eb9f793..9b6cf26b6 100644 --- a/src/testdata/testwallets/dan.json +++ b/src/testdata/testwallets/dan.json @@ -1,5 +1,6 @@ { "version": 4, + "kind": "secretKey", "id": "fc8b9b89-2227-41ec-afd1-5e6853feb7b2", "address": "b13a017423c366caff8cecfb77a12610a130f4888134122c7937feae0d6d7d17", "bech32": "erd1kyaqzaprcdnv4luvanah0gfxzzsnpaygsy6pytrexll2urtd05ts9vegu7", diff --git a/src/testdata/testwallets/eve.json b/src/testdata/testwallets/eve.json index b95f2582a..507778be8 100644 --- a/src/testdata/testwallets/eve.json +++ b/src/testdata/testwallets/eve.json @@ -1,5 +1,6 @@ { "version": 4, + "kind": "secretKey", "id": "e770c455-a23b-4dcd-a7a5-0e22375dc233", "address": "3af8d9c9423b2577c6252722c1d90212a4111f7203f9744f76fcfa1d0a310033", "bech32": "erd18tudnj2z8vjh0339yu3vrkgzz2jpz8mjq0uhgnmklnap6z33qqeszq2yn4", diff --git a/src/testdata/testwallets/frank.json b/src/testdata/testwallets/frank.json index afb4a0f6b..74f69eadd 100644 --- a/src/testdata/testwallets/frank.json +++ b/src/testdata/testwallets/frank.json @@ -1,5 +1,6 @@ { "version": 4, + "kind": "secretKey", "id": "df70f3ef-bb40-4afd-8751-77b26b29356d", "address": "b37f5d130beb8885b90ab574a8bfcdd894ca531a7d3d1f3431158d77d6185fbb", "bech32": "erd1kdl46yctawygtwg2k462307dmz2v55c605737dp3zkxh04sct7asqylhyv", diff --git a/src/testdata/testwallets/grace.json b/src/testdata/testwallets/grace.json index 81d640dcf..a438757b7 100644 --- a/src/testdata/testwallets/grace.json +++ b/src/testdata/testwallets/grace.json @@ -1,5 +1,6 @@ { "version": 4, + "kind": "secretKey", "id": "9aff338f-f504-403c-86cd-5e623bc81c42", "address": "1e8a8b6b49de5b7be10aaa158a5a6a4abb4b56cc08f524bb5e6cd5f211ad3e13", "bech32": "erd1r69gk66fmedhhcg24g2c5kn2f2a5k4kvpr6jfw67dn2lyydd8cfswy6ede", diff --git a/src/testdata/testwallets/heidi.json b/src/testdata/testwallets/heidi.json index 7608b981f..d9799fdd1 100644 --- a/src/testdata/testwallets/heidi.json +++ b/src/testdata/testwallets/heidi.json @@ -1,5 +1,6 @@ { "version": 4, + "kind": "secretKey", "id": "1b55836f-946f-4dc3-946d-3c27e5096873", "address": "6e224118d9068ae626878a1cfbebcb6a95a4715db86d1b51e06a04226cf30fd6", "bech32": "erd1dc3yzxxeq69wvf583gw0h67td226gu2ahpk3k50qdgzzym8npltq7ndgha", diff --git a/src/testdata/testwallets/ivan.json b/src/testdata/testwallets/ivan.json index ba0f7f34b..5d2b1eec4 100644 --- a/src/testdata/testwallets/ivan.json +++ b/src/testdata/testwallets/ivan.json @@ -1,5 +1,6 @@ { "version": 4, + "kind": "secretKey", "id": "0b80d732-d4e6-4145-b5bb-698bdd323b3c", "address": "899451b361a83e89d73b4096d3c90c209b27874c9c7cb01bb08b0bb4dc15693d", "bech32": "erd13x29rvmp4qlgn4emgztd8jgvyzdj0p6vn37tqxas3v9mfhq4dy7shalqrx", diff --git a/src/testdata/testwallets/judy.json b/src/testdata/testwallets/judy.json index 286c9ffd7..6d64175bb 100644 --- a/src/testdata/testwallets/judy.json +++ b/src/testdata/testwallets/judy.json @@ -1,5 +1,6 @@ { "version": 4, + "kind": "secretKey", "id": "a3108434-5d2c-4dca-9f94-29e822697e20", "address": "4a101a0f8f95f1218683900801cd971c6028b1597a771b2ed367d1ede09d9d2a", "bech32": "erd1fggp5ru0jhcjrp5rjqyqrnvhr3sz3v2e0fm3ktknvlg7mcyan54qzccnan", diff --git a/src/testdata/testwallets/mallory.json b/src/testdata/testwallets/mallory.json index 1416c2f14..bf9c1490e 100644 --- a/src/testdata/testwallets/mallory.json +++ b/src/testdata/testwallets/mallory.json @@ -1,5 +1,6 @@ { "version": 4, + "kind": "secretKey", "id": "6756aa76-5934-4dc6-90c7-c261db98fe44", "address": "1454931ffa758ab35654a5206b28e9dbf1fb8df8f9ced093bb2887eb39f7e7af", "bech32": "erd1z32fx8l6wk9tx4j555sxk28fm0clhr0cl88dpyam9zr7kw0hu7hsx2j524", diff --git a/src/testdata/testwallets/mike.json b/src/testdata/testwallets/mike.json index d46a9e7bf..6a1bbc3a3 100644 --- a/src/testdata/testwallets/mike.json +++ b/src/testdata/testwallets/mike.json @@ -1,5 +1,6 @@ { "version": 4, + "kind": "secretKey", "id": "3f6adbc3-1215-4c31-9a61-a049b430e6f7", "address": "e32afedc904fe1939746ad973beb383563cf63642ba669b3040f9b9428a5ed60", "bech32": "erd1uv40ahysflse896x4ktnh6ecx43u7cmy9wnxnvcyp7deg299a4sq6vaywa", diff --git a/src/testdata/testwallets/multipleUserKeys.pem b/src/testdata/testwallets/multipleUserKeys.pem new file mode 100644 index 000000000..c8e708eff --- /dev/null +++ b/src/testdata/testwallets/multipleUserKeys.pem @@ -0,0 +1,15 @@ +-----BEGIN PRIVATE KEY for erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th----- +NDEzZjQyNTc1ZjdmMjZmYWQzMzE3YTc3ODc3MTIxMmZkYjgwMjQ1ODUwOTgxZTQ4 +YjU4YTRmMjVlMzQ0ZThmOTAxMzk0NzJlZmY2ODg2NzcxYTk4MmYzMDgzZGE1ZDQy +MWYyNGMyOTE4MWU2Mzg4ODIyOGRjODFjYTYwZDY5ZTE= +-----END PRIVATE KEY for erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th----- +-----BEGIN PRIVATE KEY for erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx----- +YjhjYTZmODIwM2ZiNGI1NDVhOGU4M2M1Mzg0ZGEwMzNjNDE1ZGIxNTViNTNmYjVi +OGViYTdmZjVhMDM5ZDYzOTgwNDlkNjM5ZTVhNjk4MGQxY2QyMzkyYWJjY2U0MTAy +OWNkYTc0YTE1NjM1MjNhMjAyZjA5NjQxY2MyNjE4Zjg= +-----END PRIVATE KEY for erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx----- +-----BEGIN PRIVATE KEY for erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8----- +ZTI1M2E1NzFjYTE1M2RjMmFlZTg0NTgxOWY3NGJjYzk3NzNiMDU4NmVkZWFkMTVh +OTRjYjcyMzVhNTAyNzQzNmIyYTExNTU1Y2U1MjFlNDk0NGUwOWFiMTc1NDlkODVi +NDg3ZGNkMjZjODRiNTAxN2EzOWUzMWEzNjcwODg5YmE= +-----END PRIVATE KEY for erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8----- diff --git a/src/testdata/testwallets/multipleValidatorKeys.pem b/src/testdata/testwallets/multipleValidatorKeys.pem new file mode 100644 index 000000000..073fdbcc0 --- /dev/null +++ b/src/testdata/testwallets/multipleValidatorKeys.pem @@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY for f8910e47cf9464777c912e6390758bb39715fffcb861b184017920e4a807b42553f2f21e7f3914b81bcf58b66a72ab16d97013ae1cff807cefc977ef8cbf116258534b9e46d19528042d16ef8374404a89b184e0a4ee18c77c49e454d04eae8d----- +N2MxOWJmM2EwYzU3Y2RkMWZiMDhlNDYwN2NlYmFhMzY0N2Q2YjkyNjFiNDY5M2Y2 +MWU5NmU1NGIyMThkNDQyYQ== +-----END PRIVATE KEY for f8910e47cf9464777c912e6390758bb39715fffcb861b184017920e4a807b42553f2f21e7f3914b81bcf58b66a72ab16d97013ae1cff807cefc977ef8cbf116258534b9e46d19528042d16ef8374404a89b184e0a4ee18c77c49e454d04eae8d----- +-----BEGIN PRIVATE KEY for 1b4e60e6d100cdf234d3427494dac55fbac49856cadc86bcb13a01b9bb05a0d9143e86c186c948e7ae9e52427c9523102efe9019a2a9c06db02993f2e3e6756576ae5a3ec7c235d548bc79de1a6990e1120ae435cb48f7fc436c9f9098b92a0d----- +MzAzNGIxZDU4NjI4YTg0Mjk4NGRhMGM3MGRhMGI1YTI1MWViYjJhZWJmNTFhZmM1 +YjU4NmUyODM5YjVlNTI2Mw== +-----END PRIVATE KEY for 1b4e60e6d100cdf234d3427494dac55fbac49856cadc86bcb13a01b9bb05a0d9143e86c186c948e7ae9e52427c9523102efe9019a2a9c06db02993f2e3e6756576ae5a3ec7c235d548bc79de1a6990e1120ae435cb48f7fc436c9f9098b92a0d----- +-----BEGIN PRIVATE KEY for e5dc552b4b170cdec4405ff8f9af20313bf0e2756d06c35877b6fbcfa6b354a7b3e2d439ea87999befb09a8fa1b3f014e57ec747bf738c4199338fcd4a87b373dd62f5c8329f1f5f245956bbb06685596a2e83dc38befa63e4a2b5c4ce408506----- +ZGU3ZTFiMzg1ZWRiYjBlMWU4ZjlmYzI1ZDkxYmQ4ZWVkNzFhMWRhN2NhYWI3MzJl +NmI0N2E0ODA0MmQ4NTIzZA== +-----END PRIVATE KEY for e5dc552b4b170cdec4405ff8f9af20313bf0e2756d06c35877b6fbcfa6b354a7b3e2d439ea87999befb09a8fa1b3f014e57ec747bf738c4199338fcd4a87b373dd62f5c8329f1f5f245956bbb06685596a2e83dc38befa63e4a2b5c4ce408506----- +-----BEGIN PRIVATE KEY for 12773304cb718250edd89770cedcbf675ccdb7fe2b30bd3185ca65ffa0d516879768ed03f92e41a6e5bc5340b78a9d02655e3b727c79730ead791fb68eaa02b84e1be92a816a9604a1ab9a6d3874b638487e2145239438a4bafac3889348d405----- +OGViZWIwN2QyOTZhZDI1Mjk0MDBiNDA2ODdhNzQxYTEzNWY4MzU3Zjc5ZjM5ZmNi +Mjg5NGE2Zjk3MDNhNTgxNg== +-----END PRIVATE KEY for 12773304cb718250edd89770cedcbf675ccdb7fe2b30bd3185ca65ffa0d516879768ed03f92e41a6e5bc5340b78a9d02655e3b727c79730ead791fb68eaa02b84e1be92a816a9604a1ab9a6d3874b638487e2145239438a4bafac3889348d405----- diff --git a/src/testdata/testwallets/validatorKey00.pem b/src/testdata/testwallets/validatorKey00.pem new file mode 100644 index 000000000..50a57c40b --- /dev/null +++ b/src/testdata/testwallets/validatorKey00.pem @@ -0,0 +1,4 @@ +-----BEGIN PRIVATE KEY for e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208----- +N2NmZjk5YmQ2NzE1MDJkYjdkMTViYzhhYmMwYzlhODA0ZmI5MjU0MDZmYmRkNTBm +MWU0YzE3YTRjZDc3NDI0Nw== +-----END PRIVATE KEY for e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208----- diff --git a/src/testdata/testwallets/validatorKey00WithExtraLines.pem b/src/testdata/testwallets/validatorKey00WithExtraLines.pem new file mode 100644 index 000000000..138f171e2 --- /dev/null +++ b/src/testdata/testwallets/validatorKey00WithExtraLines.pem @@ -0,0 +1,9 @@ +-----BEGIN PRIVATE KEY for e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208----- +N2NmZjk5YmQ2NzE1MDJkYjdkMTViYzhhYmMwYzlhODA0ZmI5MjU0MDZmYmRkNTBm + +MWU0YzE3YTRjZDc3NDI0Nw== + +-----END PRIVATE KEY for e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208----- + + + diff --git a/src/testutils/contractController.ts b/src/testutils/contractController.ts deleted file mode 100644 index f72d30794..000000000 --- a/src/testutils/contractController.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { ITransactionOnNetwork } from "../interfaceOfNetwork"; -import { Logger } from "../logger"; -import { Interaction } from "../smartcontracts/interaction"; -import { TypedOutcomeBundle, UntypedOutcomeBundle } from "../smartcontracts/interface"; -import { ResultsParser } from "../smartcontracts/resultsParser"; -import { Transaction } from "../transaction"; -import { TransactionWatcher } from "../transactionWatcher"; -import { INetworkProvider } from "./networkProviders"; - -export class ContractController { - private readonly parser: ResultsParser; - private readonly provider: INetworkProvider; - private readonly transactionCompletionAwaiter: TransactionWatcher; - - constructor(provider: INetworkProvider) { - this.parser = new ResultsParser(); - this.provider = provider; - this.transactionCompletionAwaiter = new TransactionWatcher({ - getTransaction: async (hash: string) => { - return await provider.getTransaction(hash, true); - }, - }); - } - - async deploy( - transaction: Transaction, - ): Promise<{ transactionOnNetwork: ITransactionOnNetwork; bundle: UntypedOutcomeBundle }> { - const txHash = await this.provider.sendTransaction(transaction); - Logger.info(`ContractController.deploy [begin]: transaction = ${txHash}`); - - let transactionOnNetwork = await this.transactionCompletionAwaiter.awaitCompleted(txHash); - let bundle = this.parser.parseUntypedOutcome(transactionOnNetwork); - - Logger.info(`ContractController.deploy [end]: transaction = ${txHash}, return code = ${bundle.returnCode}`); - return { transactionOnNetwork, bundle }; - } - - async execute( - interaction: Interaction, - transaction: Transaction, - ): Promise<{ transactionOnNetwork: ITransactionOnNetwork; bundle: TypedOutcomeBundle }> { - const txHash = await this.provider.sendTransaction(transaction); - Logger.info( - `ContractController.execute [begin]: function = ${interaction.getFunction()}, transaction = ${txHash}`, - ); - - interaction.check(); - - let transactionOnNetwork = await this.transactionCompletionAwaiter.awaitCompleted(txHash); - let bundle = this.parser.parseOutcome(transactionOnNetwork, interaction.getEndpoint()); - - Logger.info( - `ContractController.execute [end]: function = ${interaction.getFunction()}, transaction = ${txHash}, return code = ${bundle.returnCode}`, - ); - return { transactionOnNetwork, bundle }; - } - - async query(interaction: Interaction): Promise { - Logger.debug(`ContractController.query [begin]: function = ${interaction.getFunction()}`); - - interaction.check(); - - let queryResponse = await this.provider.queryContract(interaction.buildQuery()); - let bundle = this.parser.parseQueryResponse(queryResponse, interaction.getEndpoint()); - - Logger.debug( - `ContractController.query [end]: function = ${interaction.getFunction()}, return code = ${bundle.returnCode}`, - ); - return bundle; - } -} diff --git a/src/testutils/dummyQuery.ts b/src/testutils/dummyQuery.ts deleted file mode 100644 index 076369df3..000000000 --- a/src/testutils/dummyQuery.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Address } from "../address"; -import { IAddress } from "../interface"; -import { IContractQuery } from "../networkProviders/interface"; - -export class MockQuery implements IContractQuery { - caller: IAddress = Address.empty(); - address: IAddress = Address.empty(); - func: string = ""; - args: string[] = []; - value: string = ""; - - constructor(init?: Partial) { - Object.assign(this, init); - } - - getEncodedArguments(): string[] { - return this.args; - } -} diff --git a/src/testutils/message.ts b/src/testutils/message.ts deleted file mode 100644 index c115d20dc..000000000 --- a/src/testutils/message.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * A dummy message used in tests. - */ -export class TestMessage { - foo: string = ""; - bar: string = ""; - - constructor(init?: Partial) { - Object.assign(this, init); - } - - serializeForSigning(): Buffer { - let plainObject = { - foo: this.foo, - bar: this.bar - }; - - let serialized = JSON.stringify(plainObject); - return Buffer.from(serialized); - } -} diff --git a/src/testutils/mockNetworkProvider.ts b/src/testutils/mockNetworkProvider.ts index 988d693f5..5760c970e 100644 --- a/src/testutils/mockNetworkProvider.ts +++ b/src/testutils/mockNetworkProvider.ts @@ -1,92 +1,192 @@ -import { ContractResultItem, ContractResults, TransactionOnNetwork, TransactionStatus } from "../networkProviders"; -import { Address } from "../address"; -import { AsyncTimer } from "../asyncTimer"; -import * as errors from "../errors"; -import { ErrMock } from "../errors"; -import { IAddress } from "../interface"; +import { Address } from "../core/address"; +import { AsyncTimer } from "../core/asyncTimer"; +import * as errors from "../core/errors"; +import { ErrMock } from "../core/errors"; +import { SmartContractQuery, SmartContractQueryResponse } from "../core/smartContractQuery"; +import { Token } from "../core/tokens"; +import { Transaction } from "../core/transaction"; +import { TransactionComputer } from "../core/transactionComputer"; +import { TransactionOnNetwork } from "../core/transactionOnNetwork"; +import { TransactionStatus } from "../core/transactionStatus"; import { - IAccountOnNetwork, - IContractQueryResponse, - INetworkConfig, - ITransactionOnNetwork, - ITransactionStatus, -} from "../interfaceOfNetwork"; -import { Query } from "../smartcontracts/query"; -import { Transaction, TransactionHash } from "../transaction"; + AccountOnNetwork, + AccountStorage, + AccountStorageEntry, + DefinitionOfFungibleTokenOnNetwork, + DefinitionOfTokenCollectionOnNetwork, + NetworkConfig, + NetworkStatus, +} from "../networkProviders"; +import { BlockOnNetwork } from "../networkProviders/blocks"; +import { INetworkProvider, IPagination } from "../networkProviders/interface"; +import { AwaitingOptions, TransactionCostResponse } from "../networkProviders/resources"; +import { TokenAmountOnNetwork } from "../networkProviders/tokens"; +import { SmartContractResult } from "../transactionsOutcomeParsers"; import { createAccountBalance } from "./utils"; -export class MockNetworkProvider { +export class MockNetworkProvider implements INetworkProvider { static AddressOfAlice = new Address("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); static AddressOfBob = new Address("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); static AddressOfCarol = new Address("erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8"); - private readonly transactions: Map; + private readonly transactions: Map; private nextTransactionTimelinePoints: any[] = []; - private readonly accounts: Map; + private readonly accounts: Map; private readonly queryContractResponders: QueryContractResponder[] = []; private readonly getTransactionResponders: GetTransactionResponder[] = []; constructor() { - this.transactions = new Map(); - this.accounts = new Map(); + this.transactions = new Map(); + this.accounts = new Map(); + + this.accounts.set( + MockNetworkProvider.AddressOfAlice.toBech32(), + new AccountOnNetwork({ + nonce: 0n, + balance: createAccountBalance(1000), + }), + ); + this.accounts.set( + MockNetworkProvider.AddressOfBob.toBech32(), + new AccountOnNetwork({ nonce: 5n, balance: createAccountBalance(500) }), + ); + this.accounts.set( + MockNetworkProvider.AddressOfCarol.toBech32(), + new AccountOnNetwork({ + nonce: 42n, + balance: createAccountBalance(300), + }), + ); + } + getBlock(): Promise { + throw new Error("Method not implemented."); + } + getLatestBlock(_shard: number): Promise { + throw new Error("Method not implemented."); + } + getAccountStorage(_address: Address): Promise { + throw new Error("Method not implemented."); + } + getAccountStorageEntry(_address: Address, _entryKey: string): Promise { + throw new Error("Method not implemented."); + } + awaitAccountOnCondition( + _address: Address, + _condition: (account: AccountOnNetwork) => boolean, + _options?: AwaitingOptions, + ): Promise { + throw new Error("Method not implemented."); + } + estimateTransactionCost(_tx: Transaction): Promise { + throw new Error("Method not implemented."); + } + awaitTransactionOnCondition( + _transactionHash: string, + _condition: (account: TransactionOnNetwork) => boolean, + _options?: AwaitingOptions, + ): Promise { + throw new Error("Method not implemented."); + } + awaitTransactionCompleted(_transactionHash: string, _options?: AwaitingOptions): Promise { + throw new Error("Method not implemented."); + } + getTokenOfAccount(_address: Address, _token: Token): Promise { + throw new Error("Method not implemented."); + } + getNetworkStatus(): Promise { + throw new Error("Method not implemented."); + } - this.accounts.set(MockNetworkProvider.AddressOfAlice.bech32(), { - nonce: 0, - balance: createAccountBalance(1000), - }); - this.accounts.set(MockNetworkProvider.AddressOfBob.bech32(), { nonce: 5, balance: createAccountBalance(500) }); - this.accounts.set(MockNetworkProvider.AddressOfCarol.bech32(), { - nonce: 42, - balance: createAccountBalance(300), - }); + getFungibleTokensOfAccount(_address: Address, _pagination?: IPagination): Promise { + throw new Error("Method not implemented."); + } + getNonFungibleTokensOfAccount(_address: Address, _pagination?: IPagination): Promise { + throw new Error("Method not implemented."); + } + sendTransactions(_txs: Transaction[]): Promise<[number, string[]]> { + throw new Error("Method not implemented."); + } + getDefinitionOfFungibleToken(_tokenIdentifier: string): Promise { + throw new Error("Method not implemented."); + } + getDefinitionOfTokenCollection(_collection: string): Promise { + throw new Error("Method not implemented."); + } + doGetGeneric(_resourceUrl: string): Promise { + throw new Error("Method not implemented."); + } + doPostGeneric(_resourceUrl: string, _payload: any): Promise { + throw new Error("Method not implemented."); } - mockUpdateAccount(address: Address, mutate: (item: IAccountOnNetwork) => void) { - let account = this.accounts.get(address.bech32()); + mockUpdateAccount(address: Address, mutate: (item: AccountOnNetwork) => void) { + let account = this.accounts.get(address.toBech32()); if (account) { mutate(account); } } - mockUpdateTransaction(hash: TransactionHash, mutate: (item: ITransactionOnNetwork) => void) { - let transaction = this.transactions.get(hash.toString()); + mockUpdateTransaction(hash: string, mutate: (item: TransactionOnNetwork) => void) { + let transaction = this.transactions.get(hash); if (transaction) { mutate(transaction); } } - mockPutTransaction(hash: TransactionHash, item: ITransactionOnNetwork) { - item.isCompleted = false; - this.transactions.set(hash.toString(), item); + mockPutTransaction(hash: string, item: TransactionOnNetwork) { + item.status = TransactionStatus.createUnknown(); + this.transactions.set(hash, item); } - mockQueryContractOnFunction(functionName: string, response: IContractQueryResponse) { - let predicate = (query: Query) => query.func.toString() == functionName; + mockQueryContractOnFunction(functionName: string, response: SmartContractQueryResponse) { + let predicate = (query: SmartContractQuery) => query.function.toString() == functionName; this.queryContractResponders.push(new QueryContractResponder(predicate, response)); } - mockGetTransactionWithAnyHashAsNotarizedWithOneResult(returnCodeAndData: string) { - let contractResult = new ContractResultItem({ nonce: 1, data: returnCodeAndData }); + mockGetTransactionWithAnyHashAsNotarizedWithOneResult(returnCodeAndData: string, functionName: string = "") { + let contractResult = new SmartContractResult({ data: Buffer.from(returnCodeAndData) }); let predicate = (_hash: string) => true; let response = new TransactionOnNetwork({ status: new TransactionStatus("executed"), - contractResults: new ContractResults([contractResult]), - isCompleted: true, + smartContractResults: [contractResult], + function: functionName, }); this.getTransactionResponders.unshift(new GetTransactionResponder(predicate, response)); } + mockAccountBalanceTimelineByAddress(address: Address, timelinePoints: Array): void { + const executeTimeline = async () => { + for (const point of timelinePoints) { + if (point instanceof MarkCompleted) { + // Mark account condition as reached + this.mockUpdateAccount(address, (account) => { + account.balance += createAccountBalance(7); + }); + } else if (point instanceof Wait) { + // Wait for the specified time + await this.sleep(point.milliseconds); + } + } + }; + + // Start the timeline execution in a separate async "thread" + executeTimeline().catch((err) => { + console.error("Error executing timeline:", err); + }); + } + async mockTransactionTimeline(transaction: Transaction, timelinePoints: any[]): Promise { - return this.mockTransactionTimelineByHash(transaction.getHash(), timelinePoints); + const computer = new TransactionComputer(); + return this.mockTransactionTimelineByHash(computer.computeTransactionHash(transaction), timelinePoints); } async mockNextTransactionTimeline(timelinePoints: any[]): Promise { this.nextTransactionTimelinePoints = timelinePoints; } - async mockTransactionTimelineByHash(hash: TransactionHash, timelinePoints: any[]): Promise { + async mockTransactionTimelineByHash(hash: string, timelinePoints: any[]): Promise { let timeline = new AsyncTimer(`mock timeline of ${hash}`); await timeline.start(0); @@ -98,7 +198,7 @@ export class MockNetworkProvider { }); } else if (point instanceof MarkCompleted) { this.mockUpdateTransaction(hash, (transaction) => { - transaction.isCompleted = true; + transaction.status = new TransactionStatus("success"); }); } else if (point instanceof Wait) { await timeline.start(point.milliseconds); @@ -106,8 +206,12 @@ export class MockNetworkProvider { } } - async getAccount(address: IAddress): Promise { - let account = this.accounts.get(address.bech32()); + private sleep(milliseconds: number): Promise { + return new Promise((resolve) => setTimeout(resolve, milliseconds)); + } + + async getAccount(address: Address): Promise { + let account = this.accounts.get(address.toBech32()); if (account) { return account; } @@ -116,25 +220,26 @@ export class MockNetworkProvider { } async sendTransaction(transaction: Transaction): Promise { + const computer = new TransactionComputer(); this.mockPutTransaction( - transaction.getHash(), + computer.computeTransactionHash(transaction), new TransactionOnNetwork({ - sender: transaction.getSender(), - receiver: transaction.getReceiver(), - data: transaction.getData().valueOf(), + sender: transaction.sender, + receiver: transaction.receiver, + data: Buffer.from(transaction.data), status: new TransactionStatus("pending"), }), ); this.mockTransactionTimeline(transaction, this.nextTransactionTimelinePoints); - return transaction.getHash().hex(); + return computer.computeTransactionHash(transaction); } async simulateTransaction(_transaction: Transaction): Promise { return {}; } - async getTransaction(txHash: string): Promise { + async getTransaction(txHash: string): Promise { // At first, try to use a mock responder for (const responder of this.getTransactionResponders) { if (responder.matches(txHash)) { @@ -151,16 +256,16 @@ export class MockNetworkProvider { throw new ErrMock("Transaction not found"); } - async getTransactionStatus(txHash: string): Promise { + async getTransactionStatus(txHash: string): Promise { let transaction = await this.getTransaction(txHash); return transaction.status; } - async getNetworkConfig(): Promise { + async getNetworkConfig(): Promise { throw new errors.ErrNotImplemented(); } - async queryContract(query: Query): Promise { + async queryContract(query: SmartContractQuery): Promise { for (const responder of this.queryContractResponders) { if (responder.matches(query)) { return responder.response; @@ -182,10 +287,10 @@ export class Wait { export class MarkCompleted {} class QueryContractResponder { - readonly matches: (query: Query) => boolean; - readonly response: IContractQueryResponse; + readonly matches: (query: SmartContractQuery) => boolean; + readonly response: SmartContractQueryResponse; - constructor(matches: (query: Query) => boolean, response: IContractQueryResponse) { + constructor(matches: (query: SmartContractQuery) => boolean, response: SmartContractQueryResponse) { this.matches = matches; this.response = response; } @@ -193,9 +298,9 @@ class QueryContractResponder { class GetTransactionResponder { readonly matches: (hash: string) => boolean; - readonly response: ITransactionOnNetwork; + readonly response: TransactionOnNetwork; - constructor(matches: (hash: string) => boolean, response: ITransactionOnNetwork) { + constructor(matches: (hash: string) => boolean, response: TransactionOnNetwork) { this.matches = matches; this.response = response; } diff --git a/src/testutils/networkProviders.ts b/src/testutils/networkProviders.ts index 67ed2a4d1..1892c1d6d 100644 --- a/src/testutils/networkProviders.ts +++ b/src/testutils/networkProviders.ts @@ -1,14 +1,5 @@ -import { IAddress } from "../interface"; -import { - IAccountOnNetwork, - IContractQueryResponse, - INetworkConfig, - ITransactionOnNetwork, - ITransactionStatus, -} from "../interfaceOfNetwork"; import { ApiNetworkProvider, ProxyNetworkProvider } from "../networkProviders"; -import { Query } from "../smartcontracts/query"; -import { Transaction } from "../transaction"; +import { INetworkProvider } from "../networkProviders/interface"; export function createLocalnetProvider(): INetworkProvider { return new ProxyNetworkProvider("http://localhost:7950", { timeout: 5000 }); @@ -34,13 +25,3 @@ export function createMainnetProvider(): INetworkProvider { clientName: "mx-sdk-js-core/tests", }); } - -export interface INetworkProvider { - getNetworkConfig(): Promise; - getAccount(address: IAddress): Promise; - getTransaction(txHash: string, withProcessStatus?: boolean): Promise; - getTransactionStatus(txHash: string): Promise; - sendTransaction(tx: Transaction): Promise; - simulateTransaction(tx: Transaction): Promise; - queryContract(query: Query): Promise; -} diff --git a/src/testutils/transaction.ts b/src/testutils/transaction.ts deleted file mode 100644 index 318b9a8b6..000000000 --- a/src/testutils/transaction.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * A dummy transaction used in tests. - */ -export class TestTransaction { - nonce: number = 0; - value: string = ""; - receiver: string = ""; - sender: string = ""; - guardian: string = ""; - gasPrice: number = 0; - gasLimit: number = 0; - data: string = ""; - chainID: string = ""; - version: number = 1; - options: number = 0; - - constructor(init?: Partial) { - Object.assign(this, init); - } - - serializeForSigning(): Buffer { - const dataEncoded = this.data ? Buffer.from(this.data).toString("base64") : undefined; - const guardian = this.guardian ? this.guardian : undefined; - const options = this.options ? this.options : undefined; - - const plainObject = { - nonce: this.nonce, - value: this.value, - receiver: this.receiver, - sender: this.sender, - guardian: guardian, - gasPrice: this.gasPrice, - gasLimit: this.gasLimit, - data: dataEncoded, - chainID: this.chainID, - options: options, - version: this.version - }; - - const serialized = JSON.stringify(plainObject); - return Buffer.from(serialized); - } -} diff --git a/src/testutils/utils.ts b/src/testutils/utils.ts index 349e82f3b..6c185c6bb 100644 --- a/src/testutils/utils.ts +++ b/src/testutils/utils.ts @@ -1,22 +1,18 @@ -import BigNumber from "bignumber.js"; import * as fs from "fs"; import { PathLike } from "fs"; -import { IChainID, IGasLimit } from "../interface"; -import { Code } from "../smartcontracts/code"; -import { SmartContract } from "../smartcontracts/smartContract"; -import { AbiRegistry, TypedValue } from "../smartcontracts/typesystem"; -import { Transaction } from "../transaction"; -import { TransactionWatcher } from "../transactionWatcher"; -import { getAxios } from "../utils"; -import { TestWallet } from "./wallets"; +import { resolve } from "path"; +import { Abi, Code, SmartContract, TypedValue } from "../abi"; +import { Account } from "../accounts"; +import { Transaction } from "../core/transaction"; +import { getAxios } from "../core/utils"; export async function prepareDeployment(obj: { - deployer: TestWallet; + deployer: Account; contract: SmartContract; codePath: string; initArguments: TypedValue[]; - gasLimit: IGasLimit; - chainID: IChainID; + gasLimit: bigint; + chainID: string; }): Promise { let contract = obj.contract; let deployer = obj.deployer; @@ -29,12 +25,12 @@ export async function prepareDeployment(obj: { deployer: deployer.address, }); - let nonce = deployer.account.getNonceThenIncrement(); + let nonce = deployer.getNonceThenIncrement(); let contractAddress = SmartContract.computeAddress(deployer.address, nonce); - transaction.setNonce(nonce); - transaction.setSender(deployer.address); + transaction.nonce = nonce; + transaction.sender = deployer.address; contract.setAddress(contractAddress); - transaction.applySignature(await deployer.signer.sign(transaction.serializeForSigning())); + transaction.signature = await deployer.signTransaction(transaction); return transaction; } @@ -59,17 +55,17 @@ export async function loadContractCode(path: PathLike): Promise { return Code.fromBuffer(buffer); } -export async function loadAbiRegistry(path: PathLike): Promise { +export async function loadAbiRegistry(path: PathLike): Promise { if (isOnBrowserTests()) { const axios = await getAxios(); let response: any = await axios.default.get(path.toString()); - return AbiRegistry.create(response.data); + return Abi.create(response.data); } // Load from files let jsonContent: string = await fs.promises.readFile(path, { encoding: "utf8" }); let json = JSON.parse(jsonContent); - return AbiRegistry.create(json); + return Abi.create(json); } export function isOnBrowserTests() { @@ -84,15 +80,27 @@ export function isOnBrowserTests() { return isOnTests; } -export function setupUnitTestWatcherTimeouts() { - TransactionWatcher.DefaultPollingInterval = 42; - TransactionWatcher.DefaultTimeout = 42 * 42; -} - -export function createAccountBalance(egld: number): BigNumber { - return new BigNumber(egld.toString() + "0".repeat(18)); +export function createAccountBalance(egld: number): bigint { + return BigInt(egld.toString() + "0".repeat(18)); } export function b64TopicsToBytes(topics: string[]): Uint8Array[] { return topics.map((topic) => Buffer.from(topic, "base64")); } + +export function b64ToHex(value: string): string { + return Buffer.from(value, "base64").toString("hex"); +} + +export function getTestWalletsPath(): string { + return resolve(__dirname, "..", "testdata", "testwallets"); +} + +export const stringifyBigIntJSON = (jsonString: any): any => { + const JSONBig = require("json-bigint")({ constructorAction: "ignore" }); + try { + return JSONBig.stringify(jsonString); + } catch (error) { + throw new Error(`Failed to parse JSON: ${error.message}`); + } +}; diff --git a/src/testutils/wallets.ts b/src/testutils/wallets.ts index e332cec0c..723e4ef5a 100644 --- a/src/testutils/wallets.ts +++ b/src/testutils/wallets.ts @@ -1,10 +1,9 @@ import * as fs from "fs"; import * as path from "path"; -import { Account } from "../account"; -import { Address } from "../address"; -import { IAddress } from "../interface"; -import { IAccountOnNetwork } from "../interfaceOfNetwork"; -import { getAxios } from "../utils"; +import { Account } from "../accounts"; +import { Address } from "../core/address"; +import { getAxios } from "../core/utils"; +import { AccountOnNetwork } from "../networkProviders"; import { UserSecretKey, UserSigner } from "./../wallet"; import { readTestFile } from "./files"; import { isOnBrowserTests } from "./utils"; @@ -12,7 +11,7 @@ import { isOnBrowserTests } from "./utils"; export const DummyMnemonicOf12Words = "matter trumpet twenty parade fame north lift sail valve salon foster cinnamon"; interface IAccountFetcher { - getAccount(address: IAddress): Promise; + getAccount(address: Address): Promise; } export async function loadAndSyncTestWallets(provider: IAccountFetcher): Promise> { @@ -104,7 +103,7 @@ export class TestWallet { this.signer = new UserSigner(UserSecretKey.fromString(secretKeyHex)); this.keyFileObject = keyFileObject; this.pemFileText = pemFileText; - this.account = new Account(this.address); + this.account = new Account(new UserSecretKey(this.secretKey)); } getAddress(): Address { @@ -113,7 +112,12 @@ export class TestWallet { async sync(provider: IAccountFetcher) { let accountOnNetwork = await provider.getAccount(this.address); - this.account.update(accountOnNetwork); + this.account.nonce = accountOnNetwork.nonce; return this; } + + async getBalance(provider: IAccountFetcher) { + let accountOnNetwork = await provider.getAccount(this.address); + return accountOnNetwork.balance; + } } diff --git a/src/tokenManagement/index.ts b/src/tokenManagement/index.ts new file mode 100644 index 000000000..ff3c75dee --- /dev/null +++ b/src/tokenManagement/index.ts @@ -0,0 +1,4 @@ +export * from "./resources"; +export * from "./tokenManagementController"; +export * from "./tokenManagementTransactionsFactory"; +export * from "./tokenManagementTransactionsOutcomeParser"; diff --git a/src/tokenManagement/resources.ts b/src/tokenManagement/resources.ts new file mode 100644 index 000000000..69b5d5c23 --- /dev/null +++ b/src/tokenManagement/resources.ts @@ -0,0 +1,171 @@ +import { TokenType } from "../core"; +import { Address } from "../core/address"; + +export type IssueFungibleInput = IssueInput & { initialSupply: bigint; numDecimals: bigint }; + +export type IssueSemiFungibleInput = IssueNonFungibleInput; + +export type IssueNonFungibleInput = IssueInput & { canTransferNFTCreateRole: boolean }; + +export type IssueInput = { + tokenName: string; + tokenTicker: string; + canFreeze: boolean; + canWipe: boolean; + canPause: boolean; + canChangeOwner: boolean; + canUpgrade: boolean; + canAddSpecialRoles: boolean; +}; + +export type FungibleSpecialRoleInput = { + user: Address; + tokenIdentifier: string; + addRoleLocalMint: boolean; + addRoleLocalBurn: boolean; + addRoleESDTTransferRole: boolean; +}; +export type SemiFungibleSpecialRoleInput = { + user: Address; + tokenIdentifier: string; + addRoleNFTCreate: boolean; + addRoleNFTBurn: boolean; + addRoleNFTAddQuantity: boolean; + addRoleESDTTransferRole: boolean; +}; + +export type SpecialRoleInput = { + user: Address; + tokenIdentifier: string; + addRoleNFTCreate: boolean; + addRoleNFTBurn: boolean; + addRoleNFTUpdateAttributes: boolean; + addRoleNFTAddURI: boolean; + addRoleESDTTransferRole: boolean; + addRoleESDTModifyCreator?: boolean; + addRoleNFTRecreate?: boolean; + addRoleESDTSetNewURI?: boolean; + addRoleESDTModifyRoyalties?: boolean; +}; + +export type UnsetFungibleSpecialRoleInput = { + user: Address; + tokenIdentifier: string; + removeRoleLocalMint: boolean; + removeRoleLocalBurn: boolean; + removeRoleESDTTransferRole: boolean; +}; +export type UnsetSemiFungibleSpecialRoleInput = { + user: Address; + tokenIdentifier: string; + removeRoleNFTBurn: boolean; + removeRoleNFTAddQuantity: boolean; + removeRoleESDTTransferRole: boolean; +}; + +export type UnsetSpecialRoleInput = { + user: Address; + tokenIdentifier: string; + removeRoleNFTBurn: boolean; + removeRoleNFTUpdateAttributes: boolean; + removeRoleNFTAddURI: boolean; + removeRoleESDTTransferRole: boolean; + removeRoleESDTModifyCreator?: boolean; + removeRoleNFTRecreate?: boolean; + removeRoleESDTSetNewURI?: boolean; + removeRoleESDTModifyRoyalties?: boolean; +}; + +export type MintInput = { + tokenIdentifier: string; + initialQuantity: bigint; + name: string; + royalties: number; + hash: string; + attributes: Uint8Array; + uris: string[]; +}; +export type ManagementInput = { user: Address; tokenIdentifier: string }; +export type PausingInput = { tokenIdentifier: string }; +export type LocalBurnInput = { tokenIdentifier: string; supplyToBurn: bigint }; +export type LocalMintInput = { tokenIdentifier: string; supplyToMint: bigint }; + +export type UpdateAttributesInput = UpdateInput & { attributes: Uint8Array }; + +export type UpdateQuantityInput = UpdateInput & { quantity: bigint }; + +export type UpdateInput = { tokenIdentifier: string; tokenNonce: bigint }; +export type BurnRoleGloballyInput = { tokenIdentifier: string }; +export type UpdateTokenIDInput = { tokenIdentifier: string }; +export type ChangeTokenToDynamicInput = { tokenIdentifier: string }; + +export type RegisterRolesInput = { + tokenName: string; + tokenTicker: string; + tokenType: TokenType; + numDecimals: bigint; +}; + +export type RegisterMetaESDTInput = { + tokenName: string; + tokenTicker: string; + numDecimals: bigint; + canFreeze: boolean; + canWipe: boolean; + canPause: boolean; + canTransferNFTCreateRole: boolean; + canChangeOwner: boolean; + canUpgrade: boolean; + canAddSpecialRoles: boolean; +}; + +export type ModifyRoyaltiesInput = BaseInput & { newRoyalties: bigint }; +export type ModifyCreatorInput = BaseInput; + +export type BaseInput = { tokenIdentifier: string; tokenNonce: bigint }; + +export type SetNewUriInput = BaseInput & { newUris: string[] }; + +export type ManageMetadataInput = { + tokenIdentifier: string; + tokenNonce: bigint; + newTokenName?: string; + newRoyalties?: bigint; + newHash?: string; + newAttributes?: Uint8Array; + newUris?: string[]; +}; + +export type RegisteringDynamicTokenInput = { tokenName: string; tokenTicker: string; tokenType: TokenType }; + +export type SpecialRoleOutput = { + userAddress: Address; + tokenIdentifier: string; + roles: string[]; +}; + +export type MintNftOutput = { + tokenIdentifier: string; + nonce: bigint; + initialQuantity: bigint; +}; + +export type EsdtOutput = { tokenIdentifier: string }; +export type ModifyRoyaltiesOutput = { tokenIdentifier: string; nonce: bigint; royalties: bigint }; +export type SetNewUrisOutput = { tokenIdentifier: string; nonce: bigint; uri: string }; +export type ModifyingCreatorOutput = { tokenIdentifier: string; nonce: bigint }; +export type UpdateAttibutesOutput = { tokenIdentifier: string; nonce: bigint; metadata: Uint8Array }; +export type ChangeToDynamicOutput = { + tokenIdentifier: string; + tokenName: string; + tickerName: string; + tokenType: string; +}; +export type UpdateTokenIDOutput = { caller: Address; tokenIdentifier: string; token: string }; +export type RegisterDynamicOutput = { + tokenIdentifier: string; + tokenName: string; + tokenTicker: string; + tokenType: string; + numOfDecimals: number; +}; diff --git a/src/tokenManagement/tokenManagementController.ts b/src/tokenManagement/tokenManagementController.ts new file mode 100644 index 000000000..33d8e3158 --- /dev/null +++ b/src/tokenManagement/tokenManagementController.ts @@ -0,0 +1,881 @@ +import { + Address, + BaseController, + BaseControllerInput, + IAccount, + Transaction, + TransactionOnNetwork, + TransactionsFactoryConfig, + TransactionWatcher, +} from "../core"; +import { INetworkProvider } from "../networkProviders/interface"; +import { TokenManagementTransactionsOutcomeParser } from "../transactionsOutcomeParsers"; +import * as resources from "./resources"; +import { TokenManagementTransactionsFactory } from "./tokenManagementTransactionsFactory"; + +export class TokenManagementController extends BaseController { + private factory: TokenManagementTransactionsFactory; + private transactionAwaiter: TransactionWatcher; + private parser: TokenManagementTransactionsOutcomeParser; + + constructor(options: { chainID: string; networkProvider: INetworkProvider }) { + super(); + this.factory = new TokenManagementTransactionsFactory({ + config: new TransactionsFactoryConfig({ chainID: options.chainID }), + }); + this.transactionAwaiter = new TransactionWatcher(options.networkProvider); + this.parser = new TokenManagementTransactionsOutcomeParser(); + } + + async createTransactionForIssuingFungible( + sender: IAccount, + nonce: bigint, + options: resources.IssueFungibleInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForIssuingFungible(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async awaitCompletedIssueFungible(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.parseIssueFungible(transaction); + } + + parseIssueFungible(transactionOnNetwork: TransactionOnNetwork): resources.EsdtOutput[] { + return this.parser.parseIssueFungible(transactionOnNetwork); + } + + async createTransactionForIssuingSemiFungible( + sender: IAccount, + nonce: bigint, + options: resources.IssueSemiFungibleInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForIssuingSemiFungible(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async awaitCompletedIssueSemiFungible(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.parseIssueSemiFungible(transaction); + } + + parseIssueSemiFungible(transactionOnNetwork: TransactionOnNetwork): resources.EsdtOutput[] { + return this.parser.parseIssueSemiFungible(transactionOnNetwork); + } + + async createTransactionForIssuingNonFungible( + sender: IAccount, + nonce: bigint, + options: resources.IssueNonFungibleInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForIssuingNonFungible(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async awaitCompletedIssueNonFungible(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.parseIssueNonFungible(transaction); + } + + parseIssueNonFungible(transactionOnNetwork: TransactionOnNetwork): resources.EsdtOutput[] { + return this.parser.parseIssueNonFungible(transactionOnNetwork); + } + + async createTransactionForRegisteringMetaEsdt( + sender: IAccount, + nonce: bigint, + options: resources.RegisterMetaESDTInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForRegisteringMetaESDT(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async awaitCompletedRegisterMetaEsdt(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.parseRegisterMetaEsdt(transaction); + } + + parseRegisterMetaEsdt(transactionOnNetwork: TransactionOnNetwork): resources.EsdtOutput[] { + return this.parser.parseRegisterMetaEsdt(transactionOnNetwork); + } + + async createTransactionForRegisteringAndSettingRoles( + sender: IAccount, + nonce: bigint, + options: resources.RegisterRolesInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForRegisteringAndSettingRoles(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async awaitCompletedRegisterAndSettingRoles(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.parseRegisterAndSetAllRoles(transaction); + } + + parseRegisterAndSetAllRoles(transactionOnNetwork: TransactionOnNetwork): resources.EsdtOutput[] { + return this.parser.parseRegisterMetaEsdt(transactionOnNetwork); + } + + async createTransactionForSetBurnRoleGlobally( + sender: IAccount, + nonce: bigint, + options: resources.BurnRoleGloballyInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForSettingBurnRoleGlobally(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async awaitCompletedSetBurnRoleGlobally(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.parseSetBurnRoleGlobally(transaction); + } + + parseSetBurnRoleGlobally(transactionOnNetwork: TransactionOnNetwork): void { + return this.parser.parseSetBurnRoleGlobally(transactionOnNetwork); + } + + async createTransactionForUnsettingBurnRoleGlobally( + sender: IAccount, + nonce: bigint, + options: resources.BurnRoleGloballyInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForUnsettingBurnRoleGlobally(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async awaitCompletedUnsetBurnRoleGlobally(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.parseUnsetBurnRoleGlobally(transaction); + } + + parseUnsetBurnRoleGlobally(transactionOnNetwork: TransactionOnNetwork): void { + return this.parser.parseUnsetBurnRoleGlobally(transactionOnNetwork); + } + + async createTransactionForSettingSpecialRoleOnFungibleToken( + sender: IAccount, + nonce: bigint, + options: resources.FungibleSpecialRoleInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForSettingSpecialRoleOnFungibleToken(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async awaitCompletedSetSpecialRoleOnFungibleToken(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.parseSetSpecialRoleOnFungible(transaction); + } + + parseSetSpecialRoleOnFungible(transactionOnNetwork: TransactionOnNetwork): resources.SpecialRoleOutput[] { + return this.parser.parseSetSpecialRole(transactionOnNetwork); + } + + async createTransactionForUnsettingSpecialRoleOnFungibleToken( + sender: IAccount, + nonce: bigint, + options: resources.UnsetFungibleSpecialRoleInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForUnsettingSpecialRoleOnFungibleToken( + sender.address, + options, + ); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async createTransactionForSettingSpecialRoleOnSemiFungibleToken( + sender: IAccount, + nonce: bigint, + options: resources.SemiFungibleSpecialRoleInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForSettingSpecialRoleOnSemiFungibleToken( + sender.address, + options, + ); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async awaitCompletedSetSpecialRoleOnSemiFungibleToken(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.parseSetSpecialRoleOnSemiFungibleToken(transaction); + } + + parseSetSpecialRoleOnSemiFungibleToken(transactionOnNetwork: TransactionOnNetwork): resources.SpecialRoleOutput[] { + return this.parser.parseSetSpecialRole(transactionOnNetwork); + } + + async createTransactionForUnsettingSpecialRoleOnSemiFungibleToken( + sender: IAccount, + nonce: bigint, + options: resources.UnsetSemiFungibleSpecialRoleInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForUnsettingSpecialRoleOnSemiFungibleToken( + sender.address, + options, + ); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async createTransactionForSettingSpecialRoleOnMetaESDT( + sender: IAccount, + nonce: bigint, + options: resources.SemiFungibleSpecialRoleInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForSettingSpecialRoleOnMetaESDT(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async awaitCompletedSetSpecialRoleOnMetaESDTToken(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.parseSetSpecialRoleOnSemiFungibleToken(transaction); + } + + parseSetSpecialRoleOnMetaESDTToken(transactionOnNetwork: TransactionOnNetwork): resources.SpecialRoleOutput[] { + return this.parser.parseSetSpecialRole(transactionOnNetwork); + } + + async createTransactionForUnsettingSpecialRoleOnMetaESDT( + sender: IAccount, + nonce: bigint, + options: resources.UnsetSemiFungibleSpecialRoleInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForUnsettingSpecialRoleOnMetaESDT(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async createTransactionForSettingSpecialRoleOnNonFungibleToken( + sender: IAccount, + nonce: bigint, + options: resources.SpecialRoleInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForSettingSpecialRoleOnNonFungibleToken( + sender.address, + options, + ); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async awaitCompletedSetSpecialRoleOnNonFungibleToken(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.parseSetSpecialRoleOnNonFungibleToken(transaction); + } + + parseSetSpecialRoleOnNonFungibleToken(transactionOnNetwork: TransactionOnNetwork): resources.SpecialRoleOutput[] { + return this.parser.parseSetSpecialRole(transactionOnNetwork); + } + + async createTransactionForUnsettingSpecialRoleOnNonFungibleToken( + sender: IAccount, + nonce: bigint, + options: resources.UnsetSpecialRoleInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForUnsettingSpecialRoleOnNonFungibleToken( + sender.address, + options, + ); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async createTransactionForCreatingNft( + sender: IAccount, + nonce: bigint, + options: resources.MintInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForCreatingNFT(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async awaitCompletedCreateNft(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.parseNftCreate(transaction); + } + + parseNftCreate(transactionOnNetwork: TransactionOnNetwork): resources.MintNftOutput[] { + return this.parser.parseNftCreate(transactionOnNetwork); + } + + async createTransactionForPausing( + sender: IAccount, + nonce: bigint, + options: resources.PausingInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForPausing(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async awaitCompletedPause(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.parsePause(transaction); + } + + parsePause(transactionOnNetwork: TransactionOnNetwork): resources.EsdtOutput[] { + return this.parser.parsePause(transactionOnNetwork); + } + + async createTransactionForUnpausing( + sender: IAccount, + nonce: bigint, + options: resources.PausingInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForUnpausing(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async awaitCompletedUnpause(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.parseUnpause(transaction); + } + + parseUnpause(transactionOnNetwork: TransactionOnNetwork): resources.EsdtOutput[] { + return this.parser.parseUnpause(transactionOnNetwork); + } + + async createTransactionForFreezing( + sender: IAccount, + nonce: bigint, + options: resources.ManagementInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForFreezing(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async awaitCompletedFreeze(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.parseFreeze(transaction); + } + + parseFreeze(transactionOnNetwork: TransactionOnNetwork): resources.EsdtOutput[] { + return this.parser.parseFreeze(transactionOnNetwork); + } + + async createTransactionForUnFreezing( + sender: IAccount, + nonce: bigint, + options: resources.ManagementInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForUnfreezing(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async awaitCompletedUnfreeze(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.parseUnfreeze(transaction); + } + + parseUnfreeze(transactionOnNetwork: TransactionOnNetwork): resources.EsdtOutput[] { + return this.parser.parseUnfreeze(transactionOnNetwork); + } + + async createTransactionForWiping( + sender: IAccount, + nonce: bigint, + options: resources.ManagementInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForWiping(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async awaitCompletedWipe(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.parser.parseWipe(transaction); + } + + parseWipe(transactionOnNetwork: TransactionOnNetwork): resources.EsdtOutput[] { + return this.parser.parseWipe(transactionOnNetwork); + } + + async createTransactionForLocaMinting( + sender: IAccount, + nonce: bigint, + options: resources.LocalMintInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForLocalMint(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async awaitCompletedLocalMint(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.parseLocalMint(transaction); + } + + parseLocalMint(transactionOnNetwork: TransactionOnNetwork): resources.EsdtOutput[] { + return this.parser.parseLocalMint(transactionOnNetwork); + } + + async createTransactionForLocalBurning( + sender: IAccount, + nonce: bigint, + options: resources.LocalBurnInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForLocalBurning(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async awaitCompleteLocalBurn(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.parseLocalBurn(transaction); + } + + parseLocalBurn(transactionOnNetwork: TransactionOnNetwork): resources.EsdtOutput[] { + return this.parser.parseLocalBurn(transactionOnNetwork); + } + + async createTransactionForUpdatingAttributes( + sender: IAccount, + nonce: bigint, + options: resources.UpdateAttributesInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForUpdatingAttributes(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async awaitCompletedUpdateAttributes(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.parseUpdateAttributes(transaction); + } + + parseUpdateAttributes(transactionOnNetwork: TransactionOnNetwork): resources.EsdtOutput[] { + return this.parser.parseUpdateAttributes(transactionOnNetwork); + } + + async createTransactionForAddingQuantity( + sender: IAccount, + nonce: bigint, + options: resources.UpdateQuantityInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForAddingQuantity(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async awaitCompletedAddQuantity(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.parseAddQuantity(transaction); + } + + parseAddQuantity(transactionOnNetwork: TransactionOnNetwork): resources.EsdtOutput[] { + return this.parser.parseAddQuantity(transactionOnNetwork); + } + + async createTransactionForBurningQuantity( + sender: IAccount, + nonce: bigint, + options: resources.UpdateQuantityInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForBurningQuantity(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async awaitCompletedBurnQuantity(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.parseBurnQuantity(transaction); + } + + parseBurnQuantity(transactionOnNetwork: TransactionOnNetwork): resources.EsdtOutput[] { + return this.parser.parseBurnQuantity(transactionOnNetwork); + } + + async createTransactionForModifyingRoyalties( + sender: IAccount, + nonce: bigint, + options: resources.ModifyRoyaltiesInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForModifyingRoyalties(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async awaitCompletedModifyRoyalties(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.parseModifyRoyalties(transaction); + } + + parseModifyRoyalties(transactionOnNetwork: TransactionOnNetwork): resources.EsdtOutput[] { + return this.parser.parseModifyRoyalties(transactionOnNetwork); + } + + async createTransactionForSettingNewUris( + sender: IAccount, + nonce: bigint, + options: resources.SetNewUriInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForSettingNewUris(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async awaitCompletedSetNewUris(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.parseSetNewUris(transaction); + } + + parseSetNewUris(transactionOnNetwork: TransactionOnNetwork): resources.EsdtOutput[] { + return this.parser.parseSetNewUris(transactionOnNetwork); + } + + async createTransactionForModifyingCreator( + sender: IAccount, + nonce: bigint, + options: resources.SetNewUriInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForModifyingCreator(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async awaitCompletedModifyCreator(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.parseModifyCreator(transaction); + } + + parseModifyCreator(transactionOnNetwork: TransactionOnNetwork): resources.ModifyingCreatorOutput[] { + return this.parser.parseModifyCreator(transactionOnNetwork); + } + + async createTransactionForUpdatingMetadata( + sender: IAccount, + nonce: bigint, + options: resources.SetNewUriInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForModifyingCreator(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async awaitCompletedUpdateMetadata(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.parseUpdateMetadata(transaction); + } + + parseUpdateMetadata(transactionOnNetwork: TransactionOnNetwork): resources.EsdtOutput[] { + return this.parser.parseUpdateMetadata(transactionOnNetwork); + } + + async createTransactionForMetadataRecreate( + sender: IAccount, + nonce: bigint, + options: resources.SetNewUriInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForMetadataRecreate(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async awaitCompletedMetadataRecreate(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.parseMetadataRecreate(transaction); + } + + parseMetadataRecreate(transactionOnNetwork: TransactionOnNetwork): resources.EsdtOutput[] { + return this.parser.parseMetadataRecreate(transactionOnNetwork); + } + + async createTransactionForChangingTokenToDynamic( + sender: IAccount, + nonce: bigint, + options: resources.SetNewUriInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForChangingTokenToDynamic(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async awaitCompletedChangeTokenToDynamic(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.parseChangeTokenToDynamic(transaction); + } + + parseChangeTokenToDynamic(transactionOnNetwork: TransactionOnNetwork): resources.ChangeToDynamicOutput[] { + return this.parser.parseChangeTokenToDynamic(transactionOnNetwork); + } + + async createTransactionForUpdatingTokenId( + sender: IAccount, + nonce: bigint, + options: resources.UpdateTokenIDInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForUpdatingTokenId(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async awaitCompletedUpdateTokenId(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return transaction; + } + + async createTransactionForRegisteringDynamicToken( + sender: IAccount, + nonce: bigint, + options: resources.RegisteringDynamicTokenInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForRegisteringDynamicToken(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async awaitCompletedRegisterDynamicToken(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.parseRegisterDynamicToken(transaction); + } + + parseRegisterDynamicToken(transactionOnNetwork: TransactionOnNetwork): resources.RegisterDynamicOutput[] { + return this.parser.parseRegisterDynamicToken(transactionOnNetwork); + } + + async createTransactionForRegisteringDynamicTokenAndSettingRoles( + sender: IAccount, + nonce: bigint, + options: resources.RegisteringDynamicTokenInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForRegisteringDynamicAndSettingRoles(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async awaitCompletedRegisterDynamicTokenAndSettingRoles(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.parseRegisterDynamicTokenAndSettingRoles(transaction); + } + + parseRegisterDynamicTokenAndSettingRoles( + transactionOnNetwork: TransactionOnNetwork, + ): resources.RegisterDynamicOutput[] { + return this.parser.parseRegisterDynamicTokenAndSettingRoles(transactionOnNetwork); + } +} diff --git a/src/transactionsFactories/tokenManagementTransactionIntentsFactory.spec.ts b/src/tokenManagement/tokenManagementTransactionFactory.spec.ts similarity index 62% rename from src/transactionsFactories/tokenManagementTransactionIntentsFactory.spec.ts rename to src/tokenManagement/tokenManagementTransactionFactory.spec.ts index f5c489903..7f13ebd87 100644 --- a/src/transactionsFactories/tokenManagementTransactionIntentsFactory.spec.ts +++ b/src/tokenManagement/tokenManagementTransactionFactory.spec.ts @@ -1,9 +1,8 @@ import { assert } from "chai"; -import { Address } from "../address"; -import { ESDT_CONTRACT_ADDRESS_HEX } from "../constants"; +import { Address, TransactionsFactoryConfig } from "../core"; +import { ESDT_CONTRACT_ADDRESS_HEX } from "../core/constants"; import { loadTestWallets, TestWallet } from "../testutils"; import { TokenManagementTransactionsFactory } from "./tokenManagementTransactionsFactory"; -import { TransactionsFactoryConfig } from "./transactionsFactoryConfig"; describe("test token management transactions factory", () => { let frank: TestWallet, grace: TestWallet; @@ -17,8 +16,7 @@ describe("test token management transactions factory", () => { }); it("should create 'Transaction' for registering and setting roles", () => { - const transaction = tokenManagementFactory.createTransactionForRegisteringAndSettingRoles({ - sender: frank.address, + const transaction = tokenManagementFactory.createTransactionForRegisteringAndSettingRoles(frank.address, { tokenName: "TEST", tokenTicker: "TEST", tokenType: "FNG", @@ -26,15 +24,17 @@ describe("test token management transactions factory", () => { }); assert.deepEqual(transaction.data, Buffer.from("registerAndSetAllRoles@54455354@54455354@464e47@02")); - assert.equal(transaction.sender, frank.address.toString()); - assert.equal(transaction.receiver, "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u"); + assert.deepEqual(transaction.sender, frank.address); + assert.deepEqual( + transaction.receiver, + Address.newFromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u"), + ); assert.deepEqual(transaction.value, config.issueCost); assert.deepEqual(transaction.gasLimit, 60125000n); }); it("should create 'Transaction' for issuing fungible token", () => { - const transaction = tokenManagementFactory.createTransactionForIssuingFungible({ - sender: frank.address, + const transaction = tokenManagementFactory.createTransactionForIssuingFungible(frank.address, { tokenName: "FRANK", tokenTicker: "FRANK", initialSupply: 100n, @@ -53,14 +53,13 @@ describe("test token management transactions factory", () => { "issue@4652414e4b@4652414e4b@64@@63616e467265657a65@74727565@63616e57697065@74727565@63616e5061757365@74727565@63616e4368616e67654f776e6572@74727565@63616e55706772616465@66616c7365@63616e4164645370656369616c526f6c6573@66616c7365", ), ); - assert.equal(transaction.sender, frank.address.toString()); - assert.equal(transaction.receiver, Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, config.addressHrp).toBech32()); + assert.deepEqual(transaction.sender, frank.address); + assert.deepEqual(transaction.receiver, Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, config.addressHrp)); assert.deepEqual(transaction.value, config.issueCost); }); it("should create 'Transaction' for issuing semi-fungible token", () => { - const transaction = tokenManagementFactory.createTransactionForIssuingSemiFungible({ - sender: frank.address, + const transaction = tokenManagementFactory.createTransactionForIssuingSemiFungible(frank.address, { tokenName: "FRANK", tokenTicker: "FRANK", canFreeze: true, @@ -78,14 +77,13 @@ describe("test token management transactions factory", () => { "issueSemiFungible@4652414e4b@4652414e4b@63616e467265657a65@74727565@63616e57697065@74727565@63616e5061757365@74727565@63616e5472616e736665724e4654437265617465526f6c65@74727565@63616e4368616e67654f776e6572@74727565@63616e55706772616465@66616c7365@63616e4164645370656369616c526f6c6573@66616c7365", ), ); - assert.equal(transaction.sender, frank.address.toString()); - assert.equal(transaction.receiver, Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, config.addressHrp).toBech32()); + assert.deepEqual(transaction.sender, frank.address); + assert.deepEqual(transaction.receiver, Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, config.addressHrp)); assert.deepEqual(transaction.value, config.issueCost); }); it("should create 'Transaction' for issuing non-fungible token", () => { - const transaction = tokenManagementFactory.createTransactionForIssuingNonFungible({ - sender: frank.address, + const transaction = tokenManagementFactory.createTransactionForIssuingNonFungible(frank.address, { tokenName: "FRANK", tokenTicker: "FRANK", canFreeze: true, @@ -103,14 +101,13 @@ describe("test token management transactions factory", () => { "issueNonFungible@4652414e4b@4652414e4b@63616e467265657a65@74727565@63616e57697065@74727565@63616e5061757365@74727565@63616e5472616e736665724e4654437265617465526f6c65@74727565@63616e4368616e67654f776e6572@74727565@63616e55706772616465@66616c7365@63616e4164645370656369616c526f6c6573@66616c7365", ), ); - assert.equal(transaction.sender, frank.address.toString()); - assert.equal(transaction.receiver, Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, config.addressHrp).toBech32()); + assert.deepEqual(transaction.sender, frank.address); + assert.deepEqual(transaction.receiver, Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, config.addressHrp)); assert.deepEqual(transaction.value, config.issueCost); }); it("should create 'Transaction' for registering metaEsdt", () => { - const transaction = tokenManagementFactory.createTransactionForRegisteringMetaESDT({ - sender: frank.address, + const transaction = tokenManagementFactory.createTransactionForRegisteringMetaESDT(frank.address, { tokenName: "FRANK", tokenTicker: "FRANK", numDecimals: 10n, @@ -129,20 +126,22 @@ describe("test token management transactions factory", () => { "registerMetaESDT@4652414e4b@4652414e4b@0a@63616e467265657a65@74727565@63616e57697065@74727565@63616e5061757365@74727565@63616e5472616e736665724e4654437265617465526f6c65@74727565@63616e4368616e67654f776e6572@74727565@63616e55706772616465@66616c7365@63616e4164645370656369616c526f6c6573@66616c7365", ), ); - assert.equal(transaction.sender, frank.address.toString()); - assert.equal(transaction.receiver, Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, config.addressHrp).toBech32()); + assert.deepEqual(transaction.sender, frank.address); + assert.deepEqual(transaction.receiver, Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, config.addressHrp)); assert.deepEqual(transaction.value, config.issueCost); }); it("should create 'Transaction' for setting special role on fungible token", () => { - const transaction = tokenManagementFactory.createTransactionForSettingSpecialRoleOnFungibleToken({ - sender: frank.address, - user: grace.address, - tokenIdentifier: "FRANK-11ce3e", - addRoleLocalMint: true, - addRoleLocalBurn: false, - addRoleESDTTransferRole: false, - }); + const transaction = tokenManagementFactory.createTransactionForSettingSpecialRoleOnFungibleToken( + frank.address, + { + user: grace.address, + tokenIdentifier: "FRANK-11ce3e", + addRoleLocalMint: true, + addRoleLocalBurn: false, + addRoleESDTTransferRole: false, + }, + ); assert.deepEqual( transaction.data, @@ -150,20 +149,45 @@ describe("test token management transactions factory", () => { "setSpecialRole@4652414e4b2d313163653365@1e8a8b6b49de5b7be10aaa158a5a6a4abb4b56cc08f524bb5e6cd5f211ad3e13@45534454526f6c654c6f63616c4d696e74", ), ); - assert.equal(transaction.sender, frank.address.toString()); - assert.equal(transaction.receiver, Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, config.addressHrp).toBech32()); + assert.deepEqual(transaction.sender, frank.address); + assert.deepEqual(transaction.receiver, Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, config.addressHrp)); + assert.equal(transaction.value, 0n); + }); + + it("should create 'Transaction' for unsetting special role on fungible token", () => { + const transaction = tokenManagementFactory.createTransactionForUnsettingSpecialRoleOnFungibleToken( + frank.address, + { + user: grace.address, + tokenIdentifier: "FRANK-11ce3e", + removeRoleLocalMint: true, + removeRoleLocalBurn: false, + removeRoleESDTTransferRole: false, + }, + ); + + assert.deepEqual( + transaction.data, + Buffer.from( + "unSetSpecialRole@4652414e4b2d313163653365@1e8a8b6b49de5b7be10aaa158a5a6a4abb4b56cc08f524bb5e6cd5f211ad3e13@45534454526f6c654c6f63616c4d696e74", + ), + ); + assert.deepEqual(transaction.sender, frank.address); + assert.deepEqual(transaction.receiver, Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, config.addressHrp)); assert.equal(transaction.value, 0n); }); it("should create 'Transaction' for setting all special roles on fungible token", () => { - const transaction = tokenManagementFactory.createTransactionForSettingSpecialRoleOnFungibleToken({ - sender: frank.address, - user: grace.address, - tokenIdentifier: "FRANK-11ce3e", - addRoleLocalMint: true, - addRoleLocalBurn: true, - addRoleESDTTransferRole: true, - }); + const transaction = tokenManagementFactory.createTransactionForSettingSpecialRoleOnFungibleToken( + frank.address, + { + user: grace.address, + tokenIdentifier: "FRANK-11ce3e", + addRoleLocalMint: true, + addRoleLocalBurn: true, + addRoleESDTTransferRole: true, + }, + ); assert.deepEqual( transaction.data, @@ -171,24 +195,26 @@ describe("test token management transactions factory", () => { "setSpecialRole@4652414e4b2d313163653365@1e8a8b6b49de5b7be10aaa158a5a6a4abb4b56cc08f524bb5e6cd5f211ad3e13@45534454526f6c654c6f63616c4d696e74@45534454526f6c654c6f63616c4275726e@455344545472616e73666572526f6c65", ), ); - assert.equal(transaction.sender, frank.address.toString()); - assert.equal(transaction.receiver, Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, config.addressHrp).toBech32()); + assert.deepEqual(transaction.sender, frank.address); + assert.deepEqual(transaction.receiver, Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, config.addressHrp)); assert.equal(transaction.value, 0n); }); it("should create 'Transaction' for setting special role on non-fungible token", () => { - const transaction = tokenManagementFactory.createTransactionForSettingSpecialRoleOnNonFungibleToken({ - sender: frank.address, - user: grace.address, - tokenIdentifier: "FRANK-11ce3e", - addRoleNFTCreate: true, - addRoleNFTBurn: false, - addRoleNFTUpdateAttributes: true, - addRoleNFTAddURI: true, - addRoleESDTTransferRole: false, - addRoleESDTModifyCreator: true, - addRoleNFTRecreate: true, - }); + const transaction = tokenManagementFactory.createTransactionForSettingSpecialRoleOnNonFungibleToken( + frank.address, + { + user: grace.address, + tokenIdentifier: "FRANK-11ce3e", + addRoleNFTCreate: true, + addRoleNFTBurn: false, + addRoleNFTUpdateAttributes: true, + addRoleNFTAddURI: true, + addRoleESDTTransferRole: false, + addRoleESDTModifyCreator: true, + addRoleNFTRecreate: true, + }, + ); assert.deepEqual( transaction.data, @@ -196,14 +222,39 @@ describe("test token management transactions factory", () => { "setSpecialRole@4652414e4b2d313163653365@1e8a8b6b49de5b7be10aaa158a5a6a4abb4b56cc08f524bb5e6cd5f211ad3e13@45534454526f6c654e4654437265617465@45534454526f6c654e465455706461746541747472696275746573@45534454526f6c654e4654416464555249@45534454526f6c654d6f6469667943726561746f72@45534454526f6c654e46545265637265617465", ), ); - assert.equal(transaction.sender, frank.address.toString()); - assert.equal(transaction.receiver, Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, config.addressHrp).toBech32()); + assert.deepEqual(transaction.sender, frank.address); + assert.deepEqual(transaction.receiver, Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, config.addressHrp)); + assert.equal(transaction.value, 0n); + }); + + it("should create 'Transaction' for unsetting special role on non-fungible token", () => { + const transaction = tokenManagementFactory.createTransactionForUnsettingSpecialRoleOnNonFungibleToken( + frank.address, + { + user: grace.address, + tokenIdentifier: "FRANK-11ce3e", + removeRoleNFTBurn: false, + removeRoleNFTUpdateAttributes: true, + removeRoleNFTAddURI: true, + removeRoleESDTTransferRole: false, + removeRoleESDTModifyCreator: true, + removeRoleNFTRecreate: true, + }, + ); + + assert.deepEqual( + transaction.data, + Buffer.from( + "unSetSpecialRole@4652414e4b2d313163653365@1e8a8b6b49de5b7be10aaa158a5a6a4abb4b56cc08f524bb5e6cd5f211ad3e13@45534454526f6c654e465455706461746541747472696275746573@45534454526f6c654e4654416464555249@45534454526f6c654d6f6469667943726561746f72@45534454526f6c654e46545265637265617465", + ), + ); + assert.deepEqual(transaction.sender, frank.address); + assert.deepEqual(transaction.receiver, Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, config.addressHrp)); assert.equal(transaction.value, 0n); }); it("should create 'Transaction' for creating nft", () => { - const transaction = tokenManagementFactory.createTransactionForCreatingNFT({ - sender: grace.address, + const transaction = tokenManagementFactory.createTransactionForCreatingNFT(grace.address, { tokenIdentifier: "FRANK-aa9e8d", initialQuantity: 1n, name: "test", @@ -217,29 +268,27 @@ describe("test token management transactions factory", () => { transaction.data, Buffer.from("ESDTNFTCreate@4652414e4b2d616139653864@01@74657374@03e8@61626261@74657374@61@62"), ); - assert.equal(transaction.sender, grace.address.toString()); - assert.equal(transaction.receiver, grace.address.toString()); + assert.deepEqual(transaction.sender, grace.address); + assert.deepEqual(transaction.receiver, grace.address); assert.equal(transaction.value, 0n); }); it("should create 'Transaction' for modifying royalties", () => { - const transaction = tokenManagementFactory.createTransactionForModifyingRoyalties({ - sender: grace.address, + const transaction = tokenManagementFactory.createTransactionForModifyingRoyalties(grace.address, { tokenIdentifier: "TEST-123456", tokenNonce: 1n, newRoyalties: 1234n, }); assert.deepEqual(transaction.data, Buffer.from("ESDTModifyRoyalties@544553542d313233343536@01@04d2")); - assert.equal(transaction.sender, grace.address.toString()); - assert.equal(transaction.receiver, grace.address.toString()); + assert.deepEqual(transaction.sender, grace.address); + assert.deepEqual(transaction.receiver, grace.address); assert.equal(transaction.value, 0n); assert.equal(transaction.gasLimit, 60125000n); }); it("should create 'Transaction' for setting new URIs", () => { - const transaction = tokenManagementFactory.createTransactionForSettingNewUris({ - sender: grace.address, + const transaction = tokenManagementFactory.createTransactionForSettingNewUris(grace.address, { tokenIdentifier: "TEST-123456", tokenNonce: 1n, newUris: ["firstURI", "secondURI"], @@ -249,29 +298,27 @@ describe("test token management transactions factory", () => { transaction.data, Buffer.from("ESDTSetNewURIs@544553542d313233343536@01@6669727374555249@7365636f6e64555249"), ); - assert.equal(transaction.sender, grace.address.toString()); - assert.equal(transaction.receiver, grace.address.toString()); + assert.deepEqual(transaction.sender, grace.address); + assert.deepEqual(transaction.receiver, grace.address); assert.equal(transaction.value, 0n); assert.equal(transaction.gasLimit, 60164000n); }); it("should create 'Transaction' for modifying creator", () => { - const transaction = tokenManagementFactory.createTransactionForModifyingCreator({ - sender: grace.address, + const transaction = tokenManagementFactory.createTransactionForModifyingCreator(grace.address, { tokenIdentifier: "TEST-123456", tokenNonce: 1n, }); assert.deepEqual(transaction.data, Buffer.from("ESDTModifyCreator@544553542d313233343536@01")); - assert.equal(transaction.sender, grace.address.toString()); - assert.equal(transaction.receiver, grace.address.toString()); + assert.deepEqual(transaction.sender, grace.address); + assert.deepEqual(transaction.receiver, grace.address); assert.equal(transaction.value, 0n); assert.equal(transaction.gasLimit, 60114500n); }); it("should create 'Transaction' for updating metadata", () => { - const transaction = tokenManagementFactory.createTransactionForUpdatingMetadata({ - sender: grace.address, + const transaction = tokenManagementFactory.createTransactionForUpdatingMetadata(grace.address, { tokenIdentifier: "TEST-123456", tokenNonce: 1n, newTokenName: "Test", @@ -287,15 +334,14 @@ describe("test token management transactions factory", () => { "ESDTMetaDataUpdate@544553542d313233343536@01@54657374@04d2@61626261@74657374@6669727374555249@7365636f6e64555249", ), ); - assert.equal(transaction.sender, grace.address.toString()); - assert.equal(transaction.receiver, grace.address.toString()); + assert.deepEqual(transaction.sender, grace.address); + assert.deepEqual(transaction.receiver, grace.address); assert.equal(transaction.value, 0n); assert.equal(transaction.gasLimit, 60218000n); }); it("should create 'Transaction' for recreating metadata", () => { - const transaction = tokenManagementFactory.createTransactionForMetadataRecreate({ - sender: grace.address, + const transaction = tokenManagementFactory.createTransactionForMetadataRecreate(grace.address, { tokenIdentifier: "TEST-123456", tokenNonce: 1n, newTokenName: "Test", @@ -311,67 +357,66 @@ describe("test token management transactions factory", () => { "ESDTMetaDataRecreate@544553542d313233343536@01@54657374@04d2@61626261@74657374@6669727374555249@7365636f6e64555249", ), ); - assert.equal(transaction.sender, grace.address.toString()); - assert.equal(transaction.receiver, grace.address.toString()); + assert.deepEqual(transaction.sender, grace.address); + assert.deepEqual(transaction.receiver, grace.address); assert.equal(transaction.value, 0n); assert.equal(transaction.gasLimit, 60221000n); }); it("should create 'Transaction' for changing to dynamic", () => { - const transaction = tokenManagementFactory.createTransactionForChangingTokenToDynamic({ - sender: grace.address, + const transaction = tokenManagementFactory.createTransactionForChangingTokenToDynamic(grace.address, { tokenIdentifier: "TEST-123456", }); assert.deepEqual(transaction.data, Buffer.from("changeToDynamic@544553542d313233343536")); - assert.equal(transaction.sender, grace.address.toString()); - assert.equal(transaction.receiver, Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, config.addressHrp).toBech32()); + assert.deepEqual(transaction.sender, grace.address); + assert.deepEqual(transaction.receiver, Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, config.addressHrp)); assert.equal(transaction.value, 0n); assert.equal(transaction.gasLimit, 60107000n); }); it("should create 'Transaction' for updating token id", () => { - const transaction = tokenManagementFactory.createTransactionForUpdatingTokenId({ - sender: grace.address, + const transaction = tokenManagementFactory.createTransactionForUpdatingTokenId(grace.address, { tokenIdentifier: "TEST-123456", }); assert.deepEqual(transaction.data, Buffer.from("updateTokenID@544553542d313233343536")); - assert.equal(transaction.sender, grace.address.toString()); - assert.equal(transaction.receiver, Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, config.addressHrp).toBech32()); + assert.deepEqual(transaction.sender, grace.address); + assert.deepEqual(transaction.receiver, Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, config.addressHrp)); assert.equal(transaction.value, 0n); assert.equal(transaction.gasLimit, 60104000n); }); it("should create 'Transaction' for registering dynamic", () => { - const transaction = tokenManagementFactory.createTransactionForRegisteringDynamicToken({ - sender: grace.address, + const transaction = tokenManagementFactory.createTransactionForRegisteringDynamicToken(grace.address, { tokenName: "Test", tokenTicker: "TEST-123456", tokenType: "FNG", }); assert.deepEqual(transaction.data, Buffer.from("registerDynamic@54657374@544553542d313233343536@464e47")); - assert.equal(transaction.sender, grace.address.toString()); - assert.equal(transaction.receiver, Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, config.addressHrp).toBech32()); + assert.deepEqual(transaction.sender, grace.address); + assert.deepEqual(transaction.receiver, Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, config.addressHrp)); assert.equal(transaction.value, 50000000000000000n); assert.equal(transaction.gasLimit, 60131000n); }); it("should create 'Transaction' for registering and setting all roles", () => { - const transaction = tokenManagementFactory.createTransactionForRegisteringDynamicAndSettingRoles({ - sender: grace.address, - tokenName: "Test", - tokenTicker: "TEST-123456", - tokenType: "FNG", - }); + const transaction = tokenManagementFactory.createTransactionForRegisteringDynamicAndSettingRoles( + grace.address, + { + tokenName: "Test", + tokenTicker: "TEST-123456", + tokenType: "FNG", + }, + ); assert.deepEqual( transaction.data, Buffer.from("registerAndSetAllRolesDynamic@54657374@544553542d313233343536@464e47"), ); - assert.equal(transaction.sender, grace.address.toString()); - assert.equal(transaction.receiver, Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, config.addressHrp).toBech32()); + assert.deepEqual(transaction.sender, grace.address); + assert.deepEqual(transaction.receiver, Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, config.addressHrp)); assert.equal(transaction.value, 50000000000000000n); assert.equal(transaction.gasLimit, 60152000n); }); diff --git a/src/transactionsFactories/tokenManagementTransactionsFactory.ts b/src/tokenManagement/tokenManagementTransactionsFactory.ts similarity index 71% rename from src/transactionsFactories/tokenManagementTransactionsFactory.ts rename to src/tokenManagement/tokenManagementTransactionsFactory.ts index b674313a5..90ad40a27 100644 --- a/src/transactionsFactories/tokenManagementTransactionsFactory.ts +++ b/src/tokenManagement/tokenManagementTransactionsFactory.ts @@ -1,11 +1,11 @@ -import { Address } from "../address"; -import { ESDT_CONTRACT_ADDRESS_HEX } from "../constants"; -import { ErrBadUsage } from "../errors"; -import { IAddress } from "../interface"; -import { Logger } from "../logger"; -import { AddressValue, ArgSerializer, BigUIntValue, BytesValue, StringValue } from "../smartcontracts"; -import { Transaction } from "../transaction"; -import { TransactionBuilder } from "./transactionBuilder"; +import { AddressValue, ArgSerializer, BigUIntValue, BytesValue, StringValue } from "../abi"; +import { Address } from "../core/address"; +import { ESDT_CONTRACT_ADDRESS_HEX } from "../core/constants"; +import { ErrBadUsage } from "../core/errors"; +import { Logger } from "../core/logger"; +import { Transaction } from "../core/transaction"; +import { TransactionBuilder } from "../core/transactionBuilder"; +import * as resources from "./resources"; interface IConfig { chainID: string; @@ -36,8 +36,6 @@ interface IConfig { issueCost: bigint; } -type TokenType = "NFT" | "SFT" | "META" | "FNG"; - /** * Use this class to create token management transactions like issuing ESDTs, creating NFTs, setting roles, etc. */ @@ -53,22 +51,10 @@ export class TokenManagementTransactionsFactory { this.argSerializer = new ArgSerializer(); this.trueAsString = "true"; this.falseAsString = "false"; - this.esdtContractAddress = Address.fromHex(ESDT_CONTRACT_ADDRESS_HEX, this.config.addressHrp); - } - - createTransactionForIssuingFungible(options: { - sender: IAddress; - tokenName: string; - tokenTicker: string; - initialSupply: bigint; - numDecimals: bigint; - canFreeze: boolean; - canWipe: boolean; - canPause: boolean; - canChangeOwner: boolean; - canUpgrade: boolean; - canAddSpecialRoles: boolean; - }): Transaction { + this.esdtContractAddress = Address.newFromHex(ESDT_CONTRACT_ADDRESS_HEX, this.config.addressHrp); + } + + createTransactionForIssuingFungible(sender: Address, options: resources.IssueFungibleInput): Transaction { this.notifyAboutUnsettingBurnRoleGlobally(); const args = [ @@ -94,7 +80,7 @@ export class TokenManagementTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, + sender: sender, receiver: this.esdtContractAddress, dataParts: dataParts, gasLimit: this.config.gasLimitIssue, @@ -103,18 +89,7 @@ export class TokenManagementTransactionsFactory { }).build(); } - createTransactionForIssuingSemiFungible(options: { - sender: IAddress; - tokenName: string; - tokenTicker: string; - canFreeze: boolean; - canWipe: boolean; - canPause: boolean; - canTransferNFTCreateRole: boolean; - canChangeOwner: boolean; - canUpgrade: boolean; - canAddSpecialRoles: boolean; - }): Transaction { + createTransactionForIssuingSemiFungible(sender: Address, options: resources.IssueSemiFungibleInput): Transaction { this.notifyAboutUnsettingBurnRoleGlobally(); const args = [ @@ -140,7 +115,7 @@ export class TokenManagementTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, + sender: sender, receiver: this.esdtContractAddress, dataParts: dataParts, gasLimit: this.config.gasLimitIssue, @@ -149,18 +124,7 @@ export class TokenManagementTransactionsFactory { }).build(); } - createTransactionForIssuingNonFungible(options: { - sender: IAddress; - tokenName: string; - tokenTicker: string; - canFreeze: boolean; - canWipe: boolean; - canPause: boolean; - canTransferNFTCreateRole: boolean; - canChangeOwner: boolean; - canUpgrade: boolean; - canAddSpecialRoles: boolean; - }): Transaction { + createTransactionForIssuingNonFungible(sender: Address, options: resources.IssueNonFungibleInput): Transaction { this.notifyAboutUnsettingBurnRoleGlobally(); const args = [ @@ -186,7 +150,7 @@ export class TokenManagementTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, + sender: sender, receiver: this.esdtContractAddress, dataParts: dataParts, gasLimit: this.config.gasLimitIssue, @@ -195,19 +159,7 @@ export class TokenManagementTransactionsFactory { }).build(); } - createTransactionForRegisteringMetaESDT(options: { - sender: IAddress; - tokenName: string; - tokenTicker: string; - numDecimals: bigint; - canFreeze: boolean; - canWipe: boolean; - canPause: boolean; - canTransferNFTCreateRole: boolean; - canChangeOwner: boolean; - canUpgrade: boolean; - canAddSpecialRoles: boolean; - }): Transaction { + createTransactionForRegisteringMetaESDT(sender: Address, options: resources.RegisterMetaESDTInput): Transaction { this.notifyAboutUnsettingBurnRoleGlobally(); const args = [ @@ -234,7 +186,7 @@ export class TokenManagementTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, + sender: sender, receiver: this.esdtContractAddress, dataParts: dataParts, gasLimit: this.config.gasLimitIssue, @@ -243,13 +195,10 @@ export class TokenManagementTransactionsFactory { }).build(); } - createTransactionForRegisteringAndSettingRoles(options: { - sender: IAddress; - tokenName: string; - tokenTicker: string; - tokenType: TokenType; - numDecimals: bigint; - }): Transaction { + createTransactionForRegisteringAndSettingRoles( + sender: Address, + options: resources.RegisterRolesInput, + ): Transaction { this.notifyAboutUnsettingBurnRoleGlobally(); const dataParts = [ @@ -264,7 +213,7 @@ export class TokenManagementTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, + sender: sender, receiver: this.esdtContractAddress, dataParts: dataParts, gasLimit: this.config.gasLimitIssue, @@ -273,7 +222,10 @@ export class TokenManagementTransactionsFactory { }).build(); } - createTransactionForSettingBurnRoleGlobally(options: { sender: IAddress; tokenIdentifier: string }): Transaction { + createTransactionForSettingBurnRoleGlobally( + sender: Address, + options: resources.BurnRoleGloballyInput, + ): Transaction { const dataParts = [ "setBurnRoleGlobally", ...this.argSerializer.valuesToStrings([new StringValue(options.tokenIdentifier)]), @@ -281,7 +233,7 @@ export class TokenManagementTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, + sender: sender, receiver: this.esdtContractAddress, dataParts: dataParts, gasLimit: this.config.gasLimitToggleBurnRoleGlobally, @@ -289,7 +241,10 @@ export class TokenManagementTransactionsFactory { }).build(); } - createTransactionForUnsettingBurnRoleGlobally(options: { sender: IAddress; tokenIdentifier: string }): Transaction { + createTransactionForUnsettingBurnRoleGlobally( + sender: Address, + options: resources.BurnRoleGloballyInput, + ): Transaction { const dataParts = [ "unsetBurnRoleGlobally", ...this.argSerializer.valuesToStrings([new StringValue(options.tokenIdentifier)]), @@ -297,7 +252,7 @@ export class TokenManagementTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, + sender: sender, receiver: this.esdtContractAddress, dataParts: dataParts, gasLimit: this.config.gasLimitToggleBurnRoleGlobally, @@ -305,14 +260,10 @@ export class TokenManagementTransactionsFactory { }).build(); } - createTransactionForSettingSpecialRoleOnFungibleToken(options: { - sender: IAddress; - user: IAddress; - tokenIdentifier: string; - addRoleLocalMint: boolean; - addRoleLocalBurn: boolean; - addRoleESDTTransferRole: boolean; - }): Transaction { + createTransactionForSettingSpecialRoleOnFungibleToken( + sender: Address, + options: resources.FungibleSpecialRoleInput, + ): Transaction { const args = [new StringValue(options.tokenIdentifier), new AddressValue(options.user)]; options.addRoleLocalMint ? args.push(new StringValue("ESDTRoleLocalMint")) : 0; @@ -323,7 +274,29 @@ export class TokenManagementTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, + sender: sender, + receiver: this.esdtContractAddress, + dataParts: dataParts, + gasLimit: this.config.gasLimitSetSpecialRole, + addDataMovementGas: true, + }).build(); + } + + createTransactionForUnsettingSpecialRoleOnFungibleToken( + sender: Address, + options: resources.UnsetFungibleSpecialRoleInput, + ): Transaction { + const args = [new StringValue(options.tokenIdentifier), new AddressValue(options.user)]; + + options.removeRoleLocalMint ? args.push(new StringValue("ESDTRoleLocalMint")) : 0; + options.removeRoleESDTTransferRole ? args.push(new StringValue("ESDTRoleLocalBurn")) : 0; + options.removeRoleESDTTransferRole ? args.push(new StringValue("ESDTTransferRole")) : 0; + + const dataParts = ["unSetSpecialRole", ...this.argSerializer.valuesToStrings(args)]; + + return new TransactionBuilder({ + config: this.config, + sender: sender, receiver: this.esdtContractAddress, dataParts: dataParts, gasLimit: this.config.gasLimitSetSpecialRole, @@ -331,29 +304,22 @@ export class TokenManagementTransactionsFactory { }).build(); } - createTransactionForSettingSpecialRoleOnSemiFungibleToken(options: { - sender: IAddress; - user: IAddress; - tokenIdentifier: string; - addRoleNFTCreate: boolean; - addRoleNFTBurn: boolean; - addRoleNFTAddQuantity: boolean; - addRoleESDTTransferRole: boolean; - addRoleESDTModifyCreator?: boolean; - }): Transaction { + createTransactionForSettingSpecialRoleOnSemiFungibleToken( + sender: Address, + options: resources.SemiFungibleSpecialRoleInput, + ): Transaction { const args = [new StringValue(options.tokenIdentifier), new AddressValue(options.user)]; options.addRoleNFTCreate ? args.push(new StringValue("ESDTRoleNFTCreate")) : 0; options.addRoleNFTBurn ? args.push(new StringValue("ESDTRoleNFTBurn")) : 0; options.addRoleNFTAddQuantity ? args.push(new StringValue("ESDTRoleNFTAddQuantity")) : 0; options.addRoleESDTTransferRole ? args.push(new StringValue("ESDTTransferRole")) : 0; - options.addRoleESDTModifyCreator ? args.push(new StringValue("ESDTRoleModifyCreator")) : 0; const dataParts = ["setSpecialRole", ...this.argSerializer.valuesToStrings(args)]; return new TransactionBuilder({ config: this.config, - sender: options.sender, + sender: sender, receiver: this.esdtContractAddress, dataParts: dataParts, gasLimit: this.config.gasLimitSetSpecialRole, @@ -361,32 +327,46 @@ export class TokenManagementTransactionsFactory { }).build(); } - createTransactionForSettingSpecialRoleOnMetaESDT(options: { - sender: IAddress; - user: IAddress; - tokenIdentifier: string; - addRoleNFTCreate: boolean; - addRoleNFTBurn: boolean; - addRoleNFTAddQuantity: boolean; - addRoleESDTTransferRole: boolean; - }): Transaction { - return this.createTransactionForSettingSpecialRoleOnSemiFungibleToken(options); - } - - createTransactionForSettingSpecialRoleOnNonFungibleToken(options: { - sender: IAddress; - user: IAddress; - tokenIdentifier: string; - addRoleNFTCreate: boolean; - addRoleNFTBurn: boolean; - addRoleNFTUpdateAttributes: boolean; - addRoleNFTAddURI: boolean; - addRoleESDTTransferRole: boolean; - addRoleESDTModifyCreator?: boolean; - addRoleNFTRecreate?: boolean; - addRoleESDTSetNewURI?: boolean; - addRoleESDTModifyRoyalties?: boolean; - }): Transaction { + createTransactionForUnsettingSpecialRoleOnSemiFungibleToken( + sender: Address, + options: resources.UnsetSemiFungibleSpecialRoleInput, + ): Transaction { + const args = [new StringValue(options.tokenIdentifier), new AddressValue(options.user)]; + + options.removeRoleNFTBurn ? args.push(new StringValue("ESDTRoleNFTBurn")) : 0; + options.removeRoleNFTAddQuantity ? args.push(new StringValue("ESDTRoleNFTAddQuantity")) : 0; + options.removeRoleESDTTransferRole ? args.push(new StringValue("ESDTTransferRole")) : 0; + + const dataParts = ["unSetSpecialRole", ...this.argSerializer.valuesToStrings(args)]; + + return new TransactionBuilder({ + config: this.config, + sender: sender, + receiver: this.esdtContractAddress, + dataParts: dataParts, + gasLimit: this.config.gasLimitSetSpecialRole, + addDataMovementGas: true, + }).build(); + } + + createTransactionForSettingSpecialRoleOnMetaESDT( + sender: Address, + options: resources.SemiFungibleSpecialRoleInput, + ): Transaction { + return this.createTransactionForSettingSpecialRoleOnSemiFungibleToken(sender, options); + } + + createTransactionForUnsettingSpecialRoleOnMetaESDT( + sender: Address, + options: resources.UnsetSemiFungibleSpecialRoleInput, + ): Transaction { + return this.createTransactionForUnsettingSpecialRoleOnSemiFungibleToken(sender, options); + } + + createTransactionForSettingSpecialRoleOnNonFungibleToken( + sender: Address, + options: resources.SpecialRoleInput, + ): Transaction { const args = [new StringValue(options.tokenIdentifier), new AddressValue(options.user)]; options.addRoleNFTCreate ? args.push(new StringValue("ESDTRoleNFTCreate")) : 0; @@ -403,7 +383,34 @@ export class TokenManagementTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, + sender: sender, + receiver: this.esdtContractAddress, + dataParts: dataParts, + gasLimit: this.config.gasLimitSetSpecialRole, + addDataMovementGas: true, + }).build(); + } + + createTransactionForUnsettingSpecialRoleOnNonFungibleToken( + sender: Address, + options: resources.UnsetSpecialRoleInput, + ): Transaction { + const args = [new StringValue(options.tokenIdentifier), new AddressValue(options.user)]; + + options.removeRoleNFTBurn ? args.push(new StringValue("ESDTRoleNFTBurn")) : 0; + options.removeRoleNFTUpdateAttributes ? args.push(new StringValue("ESDTRoleNFTUpdateAttributes")) : 0; + options.removeRoleNFTAddURI ? args.push(new StringValue("ESDTRoleNFTAddURI")) : 0; + options.removeRoleESDTTransferRole ? args.push(new StringValue("ESDTTransferRole")) : 0; + options.removeRoleESDTModifyCreator ? args.push(new StringValue("ESDTRoleModifyCreator")) : 0; + options.removeRoleNFTRecreate ? args.push(new StringValue("ESDTRoleNFTRecreate")) : 0; + options.removeRoleESDTSetNewURI ? args.push(new StringValue("ESDTRoleSetNewURI")) : 0; + options.removeRoleESDTModifyRoyalties ? args.push(new StringValue("ESDTRoleModifyRoyalties")) : 0; + + const dataParts = ["unSetSpecialRole", ...this.argSerializer.valuesToStrings(args)]; + + return new TransactionBuilder({ + config: this.config, + sender: sender, receiver: this.esdtContractAddress, dataParts: dataParts, gasLimit: this.config.gasLimitSetSpecialRole, @@ -411,21 +418,12 @@ export class TokenManagementTransactionsFactory { }).build(); } - createTransactionForCreatingNFT(options: { - sender: IAddress; - tokenIdentifier: string; - initialQuantity: bigint; - name: string; - royalties: number; - hash: string; - attributes: Uint8Array; - uris: string[]; - }): Transaction { + createTransactionForCreatingNFT(sender: Address, options: resources.MintInput): Transaction { const dataParts = [ "ESDTNFTCreate", ...this.argSerializer.valuesToStrings([ new StringValue(options.tokenIdentifier), - new BigUIntValue(options.initialQuantity), + new BigUIntValue(options.initialQuantity ?? 1n), new StringValue(options.name), new BigUIntValue(options.royalties), new StringValue(options.hash), @@ -440,28 +438,28 @@ export class TokenManagementTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, - receiver: options.sender, + sender: sender, + receiver: sender, dataParts: dataParts, gasLimit: this.config.gasLimitEsdtNftCreate + storageGasLimit, addDataMovementGas: true, }).build(); } - createTransactionForPausing(options: { sender: IAddress; tokenIdentifier: string }): Transaction { + createTransactionForPausing(sender: Address, options: resources.PausingInput): Transaction { const dataParts = ["pause", ...this.argSerializer.valuesToStrings([new StringValue(options.tokenIdentifier)])]; return new TransactionBuilder({ config: this.config, - sender: options.sender, - receiver: options.sender, + sender: sender, + receiver: sender, dataParts: dataParts, gasLimit: this.config.gasLimitPausing, addDataMovementGas: true, }).build(); } - createTransactionForUnpausing(options: { sender: IAddress; tokenIdentifier: string }): Transaction { + createTransactionForUnpausing(sender: Address, options: resources.PausingInput): Transaction { const dataParts = [ "unPause", ...this.argSerializer.valuesToStrings([new StringValue(options.tokenIdentifier)]), @@ -469,15 +467,15 @@ export class TokenManagementTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, - receiver: options.sender, + sender: sender, + receiver: sender, dataParts: dataParts, gasLimit: this.config.gasLimitPausing, addDataMovementGas: true, }).build(); } - createTransactionForFreezing(options: { sender: IAddress; user: IAddress; tokenIdentifier: string }): Transaction { + createTransactionForFreezing(sender: Address, options: resources.ManagementInput): Transaction { const dataParts = [ "freeze", ...this.argSerializer.valuesToStrings([ @@ -488,19 +486,15 @@ export class TokenManagementTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, - receiver: options.sender, + sender: sender, + receiver: sender, dataParts: dataParts, gasLimit: this.config.gasLimitFreezing, addDataMovementGas: true, }).build(); } - createTransactionForUnfreezing(options: { - sender: IAddress; - user: IAddress; - tokenIdentifier: string; - }): Transaction { + createTransactionForUnfreezing(sender: Address, options: resources.ManagementInput): Transaction { const dataParts = [ "UnFreeze", ...this.argSerializer.valuesToStrings([ @@ -511,15 +505,15 @@ export class TokenManagementTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, - receiver: options.sender, + sender: sender, + receiver: sender, dataParts: dataParts, gasLimit: this.config.gasLimitFreezing, addDataMovementGas: true, }).build(); } - createTransactionForWiping(options: { sender: IAddress; user: IAddress; tokenIdentifier: string }): Transaction { + createTransactionForWiping(sender: Address, options: resources.ManagementInput): Transaction { const dataParts = [ "wipe", ...this.argSerializer.valuesToStrings([ @@ -530,19 +524,15 @@ export class TokenManagementTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, - receiver: options.sender, + sender: sender, + receiver: sender, dataParts: dataParts, gasLimit: this.config.gasLimitWiping, addDataMovementGas: true, }).build(); } - createTransactionForLocalMint(options: { - sender: IAddress; - tokenIdentifier: string; - supplyToMint: bigint; - }): Transaction { + createTransactionForLocalMint(sender: Address, options: resources.LocalMintInput): Transaction { const dataParts = [ "ESDTLocalMint", ...this.argSerializer.valuesToStrings([ @@ -553,19 +543,15 @@ export class TokenManagementTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, - receiver: options.sender, + sender: sender, + receiver: sender, dataParts: dataParts, gasLimit: this.config.gasLimitEsdtLocalMint, addDataMovementGas: true, }).build(); } - createTransactionForLocalBurning(options: { - sender: IAddress; - tokenIdentifier: string; - supplyToBurn: bigint; - }): Transaction { + createTransactionForLocalBurning(sender: Address, options: resources.LocalBurnInput): Transaction { const dataParts = [ "ESDTLocalBurn", ...this.argSerializer.valuesToStrings([ @@ -576,20 +562,15 @@ export class TokenManagementTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, - receiver: options.sender, + sender: sender, + receiver: sender, dataParts: dataParts, gasLimit: this.config.gasLimitEsdtLocalBurn, addDataMovementGas: true, }).build(); } - createTransactionForUpdatingAttributes(options: { - sender: IAddress; - tokenIdentifier: string; - tokenNonce: bigint; - attributes: Uint8Array; - }): Transaction { + createTransactionForUpdatingAttributes(sender: Address, options: resources.UpdateAttributesInput): Transaction { const dataParts = [ "ESDTNFTUpdateAttributes", ...this.argSerializer.valuesToStrings([ @@ -601,70 +582,55 @@ export class TokenManagementTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, - receiver: options.sender, + sender: sender, + receiver: sender, dataParts: dataParts, gasLimit: this.config.gasLimitEsdtNftUpdateAttributes, addDataMovementGas: true, }).build(); } - createTransactionForAddingQuantity(options: { - sender: IAddress; - tokenIdentifier: string; - tokenNonce: bigint; - quantityToAdd: bigint; - }): Transaction { + createTransactionForAddingQuantity(sender: Address, options: resources.UpdateQuantityInput): Transaction { const dataParts = [ "ESDTNFTAddQuantity", ...this.argSerializer.valuesToStrings([ new StringValue(options.tokenIdentifier), new BigUIntValue(options.tokenNonce), - new BigUIntValue(options.quantityToAdd), + new BigUIntValue(options.quantity), ]), ]; return new TransactionBuilder({ config: this.config, - sender: options.sender, - receiver: options.sender, + sender: sender, + receiver: sender, dataParts: dataParts, gasLimit: this.config.gasLimitEsdtNftAddQuantity, addDataMovementGas: true, }).build(); } - createTransactionForBurningQuantity(options: { - sender: IAddress; - tokenIdentifier: string; - tokenNonce: bigint; - quantityToBurn: bigint; - }): Transaction { + createTransactionForBurningQuantity(sender: Address, options: resources.UpdateQuantityInput): Transaction { const dataParts = [ "ESDTNFTBurn", ...this.argSerializer.valuesToStrings([ new StringValue(options.tokenIdentifier), new BigUIntValue(options.tokenNonce), - new BigUIntValue(options.quantityToBurn), + new BigUIntValue(options.quantity), ]), ]; return new TransactionBuilder({ config: this.config, - sender: options.sender, - receiver: options.sender, + sender: sender, + receiver: sender, dataParts: dataParts, gasLimit: this.config.gasLimitEsdtNftBurn, addDataMovementGas: true, }).build(); } - createTransactionForModifyingRoyalties(options: { - sender: IAddress; - tokenIdentifier: string; - tokenNonce: bigint; - newRoyalties: bigint; - }): Transaction { + createTransactionForModifyingRoyalties(sender: Address, options: resources.ModifyRoyaltiesInput): Transaction { const dataParts = [ "ESDTModifyRoyalties", ...this.argSerializer.valuesToStrings([ @@ -676,20 +642,15 @@ export class TokenManagementTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, - receiver: options.sender, + sender: sender, + receiver: sender, dataParts: dataParts, gasLimit: this.config.gasLimitEsdtModifyRoyalties, addDataMovementGas: true, }).build(); } - createTransactionForSettingNewUris(options: { - sender: IAddress; - tokenIdentifier: string; - tokenNonce: bigint; - newUris: string[]; - }): Transaction { + createTransactionForSettingNewUris(sender: Address, options: resources.SetNewUriInput): Transaction { if (!options.newUris.length) { throw new ErrBadUsage("No URIs provided"); } @@ -705,19 +666,15 @@ export class TokenManagementTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, - receiver: options.sender, + sender: sender, + receiver: sender, dataParts: dataParts, gasLimit: this.config.gasLimitSetNewUris, addDataMovementGas: true, }).build(); } - createTransactionForModifyingCreator(options: { - sender: IAddress; - tokenIdentifier: string; - tokenNonce: bigint; - }): Transaction { + createTransactionForModifyingCreator(sender: Address, options: resources.ModifyCreatorInput): Transaction { const dataParts = [ "ESDTModifyCreator", ...this.argSerializer.valuesToStrings([ @@ -728,24 +685,15 @@ export class TokenManagementTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, - receiver: options.sender, + sender: sender, + receiver: sender, dataParts: dataParts, gasLimit: this.config.gasLimitEsdtModifyCreator, addDataMovementGas: true, }).build(); } - createTransactionForUpdatingMetadata(options: { - sender: IAddress; - tokenIdentifier: string; - tokenNonce: bigint; - newTokenName?: string; - newRoyalties?: bigint; - newHash?: string; - newAttributes?: Uint8Array; - newUris?: string[]; - }): Transaction { + createTransactionForUpdatingMetadata(sender: Address, options: resources.ManageMetadataInput): Transaction { const dataParts = [ "ESDTMetaDataUpdate", ...this.argSerializer.valuesToStrings([ @@ -761,24 +709,15 @@ export class TokenManagementTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, - receiver: options.sender, + sender: sender, + receiver: sender, dataParts: dataParts, gasLimit: this.config.gasLimitEsdtMetadataUpdate, addDataMovementGas: true, }).build(); } - createTransactionForMetadataRecreate(options: { - sender: IAddress; - tokenIdentifier: string; - tokenNonce: bigint; - newTokenName: string; - newRoyalties: bigint; - newHash: string; - newAttributes: Uint8Array; - newUris: string[]; - }): Transaction { + createTransactionForMetadataRecreate(sender: Address, options: resources.ManageMetadataInput): Transaction { const dataParts = [ "ESDTMetaDataRecreate", ...this.argSerializer.valuesToStrings([ @@ -794,15 +733,18 @@ export class TokenManagementTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, - receiver: options.sender, + sender: sender, + receiver: sender, dataParts: dataParts, gasLimit: this.config.gasLimitNftMetadataRecreate, addDataMovementGas: true, }).build(); } - createTransactionForChangingTokenToDynamic(options: { sender: IAddress; tokenIdentifier: string }): Transaction { + createTransactionForChangingTokenToDynamic( + sender: Address, + options: resources.ChangeTokenToDynamicInput, + ): Transaction { const dataParts = [ "changeToDynamic", ...this.argSerializer.valuesToStrings([new StringValue(options.tokenIdentifier)]), @@ -810,7 +752,7 @@ export class TokenManagementTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, + sender: sender, receiver: this.esdtContractAddress, dataParts: dataParts, gasLimit: this.config.gasLimitNftChangeToDynamic, @@ -818,7 +760,7 @@ export class TokenManagementTransactionsFactory { }).build(); } - createTransactionForUpdatingTokenId(options: { sender: IAddress; tokenIdentifier: string }): Transaction { + createTransactionForUpdatingTokenId(sender: Address, options: resources.UpdateTokenIDInput): Transaction { const dataParts = [ "updateTokenID", ...this.argSerializer.valuesToStrings([new StringValue(options.tokenIdentifier)]), @@ -826,7 +768,7 @@ export class TokenManagementTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, + sender: sender, receiver: this.esdtContractAddress, dataParts: dataParts, gasLimit: this.config.gasLimitUpdateTokenId, @@ -834,12 +776,10 @@ export class TokenManagementTransactionsFactory { }).build(); } - createTransactionForRegisteringDynamicToken(options: { - sender: IAddress; - tokenName: string; - tokenTicker: string; - tokenType: TokenType; - }): Transaction { + createTransactionForRegisteringDynamicToken( + sender: Address, + options: resources.RegisteringDynamicTokenInput, + ): Transaction { const dataParts = [ "registerDynamic", ...this.argSerializer.valuesToStrings([ @@ -851,7 +791,7 @@ export class TokenManagementTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, + sender: sender, receiver: this.esdtContractAddress, dataParts: dataParts, gasLimit: this.config.gasLimitRegisterDynamic, @@ -860,12 +800,10 @@ export class TokenManagementTransactionsFactory { }).build(); } - createTransactionForRegisteringDynamicAndSettingRoles(options: { - sender: IAddress; - tokenName: string; - tokenTicker: string; - tokenType: TokenType; - }): Transaction { + createTransactionForRegisteringDynamicAndSettingRoles( + sender: Address, + options: resources.RegisteringDynamicTokenInput, + ): Transaction { const dataParts = [ "registerAndSetAllRolesDynamic", ...this.argSerializer.valuesToStrings([ @@ -877,7 +815,7 @@ export class TokenManagementTransactionsFactory { return new TransactionBuilder({ config: this.config, - sender: options.sender, + sender: sender, receiver: this.esdtContractAddress, dataParts: dataParts, gasLimit: this.config.gasLimitRegisterDynamic, diff --git a/src/tokenManagement/tokenManagementTransactionsOutcomeParser.spec.ts b/src/tokenManagement/tokenManagementTransactionsOutcomeParser.spec.ts new file mode 100644 index 000000000..1c68feccd --- /dev/null +++ b/src/tokenManagement/tokenManagementTransactionsOutcomeParser.spec.ts @@ -0,0 +1,859 @@ +import { assert } from "chai"; +import { Address, ErrParseTransactionOutcome, TransactionEvent, TransactionLogs, TransactionOnNetwork } from "../core"; +import { b64TopicsToBytes } from "../testutils"; +import { SmartContractResult } from "../transactionsOutcomeParsers"; +import { TokenManagementTransactionsOutcomeParser } from "./tokenManagementTransactionsOutcomeParser"; + +describe("test token management transactions outcome parser", () => { + const parser = new TokenManagementTransactionsOutcomeParser(); + + it("should test ensure error", () => { + const encodedTopics = ["Avk0jZ1kR+l9c76wQQoYcu4hvXPz+jxxTdqQeaCrbX8=", "dGlja2VyIG5hbWUgaXMgbm90IHZhbGlk"]; + const event = new TransactionEvent({ + address: new Address("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u"), + identifier: "signalError", + topics: b64TopicsToBytes(encodedTopics), + additionalData: [Buffer.from("QDc1NzM2NTcyMjA2NTcyNzI2Zjcy", "base64")], + }); + + const logs = new TransactionLogs({ events: [event] }); + const transaction = new TransactionOnNetwork({ logs: logs }); + + assert.throws( + () => { + parser.parseIssueFungible(transaction); + }, + ErrParseTransactionOutcome, + "encountered signalError: ticker name is not valid (user error)", + ); + }); + + it("should test parse issue fungible", () => { + const identifier = "ZZZ-9ee87d"; + const base64Identifier = Buffer.from(identifier).toString("base64"); + + const encodedTopics = [base64Identifier, "U0VDT05E", "Wlpa", "RnVuZ2libGVFU0RU", "Ag=="]; + const event = new TransactionEvent({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + identifier: "issue", + topics: b64TopicsToBytes(encodedTopics), + }); + + const logs = new TransactionLogs({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + events: [event], + }); + + const transaction = new TransactionOnNetwork({ logs: logs }); + + const outcome = parser.parseIssueFungible(transaction); + assert.lengthOf(outcome, 1); + assert.equal(outcome[0].tokenIdentifier, identifier); + }); + + it("should test parse issue non fungible", () => { + const identifier = "NFT-f01d1e"; + const base64Identifier = Buffer.from(identifier).toString("base64"); + + let encodedTopics = [ + "TkZULWYwMWQxZQ==", + "", + "Y2FuVXBncmFkZQ==", + "dHJ1ZQ==", + "Y2FuQWRkU3BlY2lhbFJvbGVz", + "dHJ1ZQ==", + ]; + const firstEvent = new TransactionEvent({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + identifier: "upgradeProperties", + topics: b64TopicsToBytes(encodedTopics), + }); + + encodedTopics = ["TkZULWYwMWQxZQ==", "", "", "RVNEVFJvbGVCdXJuRm9yQWxs"]; + const secondEvent = new TransactionEvent({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + identifier: "ESDTSetBurnRoleForAll", + topics: b64TopicsToBytes(encodedTopics), + }); + + encodedTopics = [base64Identifier, "TkZURVNU", "TkZU", "Tm9uRnVuZ2libGVFU0RU"]; + const thirdEvent = new TransactionEvent({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + identifier: "issueNonFungible", + topics: b64TopicsToBytes(encodedTopics), + }); + + const logs = new TransactionLogs({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + events: [firstEvent, secondEvent, thirdEvent], + }); + + const transaction = new TransactionOnNetwork({ logs: logs }); + + const outcome = parser.parseIssueNonFungible(transaction); + assert.lengthOf(outcome, 1); + assert.equal(outcome[0].tokenIdentifier, identifier); + }); + + it("should test parse issue semi fungible", () => { + const identifier = "SEMIFNG-2c6d9f"; + const base64Identifier = Buffer.from(identifier).toString("base64"); + + const encodedTopics = [base64Identifier, "U0VNSQ==", "U0VNSUZORw==", "U2VtaUZ1bmdpYmxlRVNEVA=="]; + const event = new TransactionEvent({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + identifier: "issueSemiFungible", + topics: b64TopicsToBytes(encodedTopics), + }); + + const logs = new TransactionLogs({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + events: [event], + }); + + const transaction = new TransactionOnNetwork({ logs: logs }); + + const outcome = parser.parseIssueSemiFungible(transaction); + assert.lengthOf(outcome, 1); + assert.equal(outcome[0].tokenIdentifier, identifier); + }); + + it("should test parse register meta esdt", () => { + const identifier = "METATEST-e05d11"; + const base64Identifier = Buffer.from(identifier).toString("base64"); + + const encodedTopics = [base64Identifier, "TUVURVNU", "TUVUQVRFU1Q=", "TWV0YUVTRFQ="]; + const event = new TransactionEvent({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + identifier: "registerMetaESDT", + topics: b64TopicsToBytes(encodedTopics), + }); + + const logs = new TransactionLogs({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + events: [event], + }); + + const transaction = new TransactionOnNetwork({ logs: logs }); + + const outcome = parser.parseRegisterMetaEsdt(transaction); + assert.lengthOf(outcome, 1); + assert.equal(outcome[0].tokenIdentifier, identifier); + }); + + it("should test parse register and set all roles", () => { + const firstIdentifier = "LMAO-d9f892"; + const firstBase64Identifier = Buffer.from(firstIdentifier).toString("base64"); + + const secondIdentifier = "TST-123456"; + const secondBase64Identifier = Buffer.from(secondIdentifier).toString("base64"); + + const roles = ["ESDTRoleLocalMint", "ESDTRoleLocalBurn"]; + + let encodedTopics = [firstBase64Identifier, "TE1BTw==", "TE1BTw==", "RnVuZ2libGVFU0RU", "Ag=="]; + const firstEvent = new TransactionEvent({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + identifier: "registerAndSetAllRoles", + topics: b64TopicsToBytes(encodedTopics), + }); + + encodedTopics = [secondBase64Identifier, "TE1BTw==", "TE1BTw==", "RnVuZ2libGVFU0RU", "Ag=="]; + const secondEvent = new TransactionEvent({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + identifier: "registerAndSetAllRoles", + topics: b64TopicsToBytes(encodedTopics), + }); + + const transactionLogs = new TransactionLogs({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + events: [firstEvent, secondEvent], + }); + + encodedTopics = ["TE1BTy1kOWY4OTI=", "", "", "RVNEVFJvbGVMb2NhbE1pbnQ=", "RVNEVFJvbGVMb2NhbEJ1cm4="]; + const firstResultEvent = new TransactionEvent({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + identifier: "ESDTSetRole", + topics: b64TopicsToBytes(encodedTopics), + }); + + encodedTopics = ["VFNULTEyMzQ1Ng==", "", "", "RVNEVFJvbGVMb2NhbE1pbnQ=", "RVNEVFJvbGVMb2NhbEJ1cm4="]; + const secondResultEvent = new TransactionEvent({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + identifier: "ESDTSetRole", + topics: b64TopicsToBytes(encodedTopics), + }); + + const resultLogs = new TransactionLogs({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + events: [firstResultEvent, secondResultEvent], + }); + + const scResult = new SmartContractResult({ + sender: new Address("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u"), + receiver: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + data: Buffer.from( + "RVNEVFNldFJvbGVANGM0ZDQxNGYyZDY0Mzk2NjM4MzkzMkA0NTUzNDQ1NDUyNmY2YzY1NGM2ZjYzNjE2YzRkNjk2ZTc0QDQ1NTM0NDU0NTI2ZjZjNjU0YzZmNjM2MTZjNDI3NTcyNmU=", + "base64", + ), + logs: resultLogs, + }); + + const transaction = new TransactionOnNetwork({ + smartContractResults: [scResult], + logs: transactionLogs, + }); + + const outcome = parser.parseRegisterAndSetAllRoles(transaction); + assert.lengthOf(outcome, 2); + + assert.equal(outcome[0].tokenIdentifier, firstIdentifier); + assert.deepEqual(outcome[0].roles, roles); + + assert.equal(outcome[1].tokenIdentifier, secondIdentifier); + assert.deepEqual(outcome[1].roles, roles); + }); + + it("should test parse register set special role", () => { + const identifier = "METATEST-e05d11"; + const base64Identifier = Buffer.from(identifier).toString("base64"); + const roles = ["ESDTRoleNFTCreate", "ESDTRoleNFTAddQuantity", "ESDTRoleNFTBurn"]; + + const encodedTopics = [ + base64Identifier, + "", + "", + "RVNEVFJvbGVORlRDcmVhdGU=", + "RVNEVFJvbGVORlRBZGRRdWFudGl0eQ==", + "RVNEVFJvbGVORlRCdXJu", + ]; + const event = new TransactionEvent({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + identifier: "ESDTSetRole", + topics: b64TopicsToBytes(encodedTopics), + }); + + const transactionLogs = new TransactionLogs({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + events: [event], + }); + + const transaction = new TransactionOnNetwork({ + logs: transactionLogs, + }); + + const outcome = parser.parseSetSpecialRole(transaction); + assert.lengthOf(outcome, 1); + assert.deepEqual( + outcome[0].userAddress, + new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + ); + assert.equal(outcome[0].tokenIdentifier, identifier); + assert.deepEqual(outcome[0].roles, roles); + }); + + it("should test parse nft create", () => { + const identifier = "NFT-f01d1e"; + const base64Identifier = Buffer.from(identifier).toString("base64"); + const nonce = BigInt(1); + const initialQuantity = BigInt(1); + + const encodedTopics = [ + base64Identifier, + "AQ==", + "AQ==", + "CAESAgABIuUBCAESCE5GVEZJUlNUGiA8NdfqyxqZpKDMqlN+8MwK4Qn0H2wrQCID5jO/uwcfXCDEEyouUW1ZM3ZKQ3NVcWpNM3hxeGR3VWczemJoVFNMUWZoN0szbW5aWXhyaGNRRFl4RzJDaHR0cHM6Ly9pcGZzLmlvL2lwZnMvUW1ZM3ZKQ3NVcWpNM3hxeGR3VWczemJoVFNMUWZoN0szbW5aWXhyaGNRRFl4Rzo9dGFnczo7bWV0YWRhdGE6UW1SY1A5NGtYcjV6WmpSR3ZpN21KNnVuN0xweFVoWVZSNFI0UnBpY3h6Z1lrdA==", + ]; + const event = new TransactionEvent({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + identifier: "ESDTNFTCreate", + topics: b64TopicsToBytes(encodedTopics), + }); + + const transactionLogs = new TransactionLogs({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + events: [event], + }); + + const transaction = new TransactionOnNetwork({ + logs: transactionLogs, + }); + + const outcome = parser.parseNftCreate(transaction); + assert.lengthOf(outcome, 1); + assert.equal(outcome[0].tokenIdentifier, identifier); + assert.equal(outcome[0].nonce, nonce); + assert.equal(outcome[0].initialQuantity, initialQuantity); + }); + + it("should test parse local mint", () => { + const identifier = "AAA-29c4c9"; + const base64Identifier = Buffer.from(identifier).toString("base64"); + const nonce = BigInt(0); + const mintedSupply = BigInt(100000); + + const encodedTopics = [base64Identifier, "", "AYag"]; + const event = new TransactionEvent({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + identifier: "ESDTLocalMint", + topics: b64TopicsToBytes(encodedTopics), + }); + + const transactionLogs = new TransactionLogs({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + events: [event], + }); + + const transaction = new TransactionOnNetwork({ + logs: transactionLogs, + }); + + const outcome = parser.parseLocalMint(transaction); + assert.lengthOf(outcome, 1); + assert.equal(outcome[0].userAddress, event.address); + assert.equal(outcome[0].tokenIdentifier, identifier); + assert.equal(outcome[0].nonce, nonce); + assert.equal(outcome[0].mintedSupply, mintedSupply); + }); + + it("should test parse local burn", () => { + const identifier = "AAA-29c4c9"; + const base64Identifier = Buffer.from(identifier).toString("base64"); + const nonce = BigInt(0); + const burntSupply = BigInt(100000); + + const encodedTopics = [base64Identifier, "", "AYag"]; + const event = new TransactionEvent({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + identifier: "ESDTLocalBurn", + topics: b64TopicsToBytes(encodedTopics), + }); + + const transactionLogs = new TransactionLogs({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + events: [event], + }); + + const transaction = new TransactionOnNetwork({ + logs: transactionLogs, + }); + + const outcome = parser.parseLocalBurn(transaction); + assert.lengthOf(outcome, 1); + assert.equal(outcome[0].userAddress, event.address); + assert.equal(outcome[0].tokenIdentifier, identifier); + assert.equal(outcome[0].nonce, nonce); + assert.equal(outcome[0].burntSupply, burntSupply); + }); + + it("should test parse pause", () => { + const identifier = "AAA-29c4c9"; + const base64Identifier = Buffer.from(identifier).toString("base64"); + + const encodedTopics = [base64Identifier]; + const event = new TransactionEvent({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + identifier: "ESDTPause", + topics: b64TopicsToBytes(encodedTopics), + }); + + const transactionLogs = new TransactionLogs({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + events: [event], + }); + + const transaction = new TransactionOnNetwork({ + logs: transactionLogs, + }); + + const outcome = parser.parsePause(transaction); + assert.lengthOf(outcome, 1); + assert.equal(outcome[0].tokenIdentifier, identifier); + }); + + it("should test parse unpause", () => { + const identifier = "AAA-29c4c9"; + const base64Identifier = Buffer.from(identifier).toString("base64"); + + const encodedTopics = [base64Identifier]; + const event = new TransactionEvent({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + identifier: "ESDTUnPause", + topics: b64TopicsToBytes(encodedTopics), + }); + + const transactionLogs = new TransactionLogs({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + events: [event], + }); + + const transaction = new TransactionOnNetwork({ + logs: transactionLogs, + }); + + const outcome = parser.parseUnpause(transaction); + assert.lengthOf(outcome, 1); + assert.equal(outcome[0].tokenIdentifier, identifier); + }); + + it("should test parse freeze", () => { + const identifier = "AAA-29c4c9"; + const base64Identifier = Buffer.from(identifier).toString("base64"); + const nonce = BigInt(0); + const balance = BigInt(10000000); + const address = "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"; + + const encodedTopics = [base64Identifier, "", "mJaA", "ATlHLv9ohncamC8wg9pdQh8kwpGB5jiIIo3IHKYNaeE="]; + const event = new TransactionEvent({ + address: new Address("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u"), + identifier: "ESDTFreeze", + topics: b64TopicsToBytes(encodedTopics), + }); + + const transactionLogs = new TransactionLogs({ + address: new Address("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + events: [event], + }); + + const scResult = [ + new SmartContractResult({ + sender: new Address("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u"), + receiver: new Address("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + data: Buffer.from("RVNEVEZyZWV6ZUA0MTQxNDEyZDMyMzk2MzM0NjMzOQ=="), + logs: transactionLogs, + }), + ]; + + const transaction = new TransactionOnNetwork({ + smartContractResults: scResult, + }); + + const outcome = parser.parseFreeze(transaction); + assert.lengthOf(outcome, 1); + assert.equal(outcome[0].userAddress, address); + assert.equal(outcome[0].tokenIdentifier, identifier); + assert.equal(outcome[0].nonce, nonce); + assert.equal(outcome[0].balance, balance); + }); + + it("should test parse unfreeze", () => { + const identifier = "AAA-29c4c9"; + const base64Identifier = Buffer.from(identifier).toString("base64"); + const nonce = BigInt(0); + const balance = BigInt(10000000); + const address = "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"; + + const encodedTopics = [base64Identifier, "", "mJaA", "ATlHLv9ohncamC8wg9pdQh8kwpGB5jiIIo3IHKYNaeE="]; + const event = new TransactionEvent({ + address: new Address("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u"), + identifier: "ESDTUnFreeze", + topics: b64TopicsToBytes(encodedTopics), + }); + + const transactionLogs = new TransactionLogs({ + address: new Address("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + events: [event], + }); + + const scResult = [ + new SmartContractResult({ + sender: new Address("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u"), + receiver: new Address("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + data: Buffer.from("RVNEVEZyZWV6ZUA0MTQxNDEyZDMyMzk2MzM0NjMzOQ=="), + logs: transactionLogs, + }), + ]; + + const transaction = new TransactionOnNetwork({ + smartContractResults: scResult, + }); + + const outcome = parser.parseUnfreeze(transaction); + assert.lengthOf(outcome, 1); + assert.equal(outcome[0].userAddress, address); + assert.equal(outcome[0].tokenIdentifier, identifier); + assert.equal(outcome[0].nonce, nonce); + assert.equal(outcome[0].balance, balance); + }); + + it("should test parse wipe", () => { + const identifier = "AAA-29c4c9"; + const base64Identifier = Buffer.from(identifier).toString("base64"); + const nonce = BigInt(0); + const balance = BigInt(10000000); + const address = "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"; + + const encodedTopics = [base64Identifier, "", "mJaA", "ATlHLv9ohncamC8wg9pdQh8kwpGB5jiIIo3IHKYNaeE="]; + const event = new TransactionEvent({ + address: new Address("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u"), + identifier: "ESDTWipe", + topics: b64TopicsToBytes(encodedTopics), + }); + + const transactionLogs = new TransactionLogs({ + address: new Address("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + events: [event], + }); + + const scResult = [ + new SmartContractResult({ + sender: new Address("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u"), + receiver: new Address("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + data: Buffer.from("RVNEVEZyZWV6ZUA0MTQxNDEyZDMyMzk2MzM0NjMzOQ=="), + logs: transactionLogs, + }), + ]; + + const transaction = new TransactionOnNetwork({ smartContractResults: scResult }); + + const outcome = parser.parseWipe(transaction); + assert.lengthOf(outcome, 1); + assert.equal(outcome[0].userAddress, address); + assert.equal(outcome[0].tokenIdentifier, identifier); + assert.equal(outcome[0].nonce, nonce); + assert.equal(outcome[0].balance, balance); + }); + + it("should test parse update attributes", () => { + const identifier = "NFT-f01d1e"; + const base64Identifier = Buffer.from(identifier).toString("base64"); + const nonce = BigInt(1); + const attributes = "metadata:ipfsCID/test.json;tags:tag1,tag2"; + const base64Attributes = Buffer.from(attributes).toString("base64"); + + const encodedTopics = [base64Identifier, "AQ==", "", base64Attributes]; + const event = new TransactionEvent({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + identifier: "ESDTNFTUpdateAttributes", + topics: b64TopicsToBytes(encodedTopics), + }); + + const transactionLogs = new TransactionLogs({ + address: new Address("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + events: [event], + }); + + const transaction = new TransactionOnNetwork({ + logs: transactionLogs, + }); + + const outcome = parser.parseUpdateAttributes(transaction); + assert.lengthOf(outcome, 1); + assert.equal(outcome[0].tokenIdentifier, identifier); + assert.equal(outcome[0].nonce, nonce); + assert.equal(Buffer.from(outcome[0].attributes).toString(), attributes); + }); + + it("should test parse add quantity", () => { + const identifier = "NFT-f01d1e"; + const base64Identifier = Buffer.from(identifier).toString("base64"); + const nonce = BigInt(1); + const addedQuantity = BigInt(10); + + const encodedTopics = [base64Identifier, "AQ==", "Cg=="]; + const event = new TransactionEvent({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + identifier: "ESDTNFTAddQuantity", + topics: b64TopicsToBytes(encodedTopics), + }); + + const transactionLogs = new TransactionLogs({ + address: new Address("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + events: [event], + }); + + const transaction = new TransactionOnNetwork({ + logs: transactionLogs, + }); + + const outcome = parser.parseAddQuantity(transaction); + assert.lengthOf(outcome, 1); + assert.equal(outcome[0].tokenIdentifier, identifier); + assert.equal(outcome[0].nonce, nonce); + assert.equal(outcome[0].addedQuantity, addedQuantity); + }); + + it("should test parse burn quantity", () => { + const identifier = "NFT-f01d1e"; + const base64Identifier = Buffer.from(identifier).toString("base64"); + const nonce = BigInt(1); + const burntQuantity = BigInt(16); + + const encodedTopics = [base64Identifier, "AQ==", "EA=="]; + const event = new TransactionEvent({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + identifier: "ESDTNFTBurn", + topics: b64TopicsToBytes(encodedTopics), + }); + + const transactionLogs = new TransactionLogs({ + address: new Address("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + events: [event], + }); + + const transaction = new TransactionOnNetwork({ + logs: transactionLogs, + }); + + const outcome = parser.parseBurnQuantity(transaction); + assert.lengthOf(outcome, 1); + assert.equal(outcome[0].tokenIdentifier, identifier); + assert.equal(outcome[0].nonce, nonce); + assert.equal(outcome[0].burntQuantity, burntQuantity); + }); + + it("should test parse modify royalties", () => { + const identifier = "TEST-e2b0f9"; + const base64Identifier = Buffer.from(identifier).toString("base64"); + const nonce = BigInt(1); + const royalties = 20n; + + const encodedTopics = [base64Identifier, "AQ==", "", "FA=="]; + const event = new TransactionEvent({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + identifier: "ESDTModifyRoyalties", + topics: b64TopicsToBytes(encodedTopics), + }); + + const transactionLogs = new TransactionLogs({ + address: new Address("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + events: [event], + }); + + const transaction = new TransactionOnNetwork({ + logs: transactionLogs, + }); + + const outcome = parser.parseModifyRoyalties(transaction); + assert.lengthOf(outcome, 1); + assert.equal(outcome[0].tokenIdentifier, identifier); + assert.equal(outcome[0].nonce, nonce); + assert.equal(outcome[0].royalties, royalties); + }); + + it("should test parse set new uris", () => { + const identifier = "TEST-e2b0f9"; + const base64Identifier = Buffer.from(identifier).toString("base64"); + const nonce = BigInt(1); + const uri = "thisianuri.com"; + + const encodedTopics = [base64Identifier, "AQ==", "", "dGhpc2lhbnVyaS5jb20="]; + const event = new TransactionEvent({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + identifier: "ESDTSetNewURIs", + topics: b64TopicsToBytes(encodedTopics), + }); + + const transactionLogs = new TransactionLogs({ + address: new Address("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + events: [event], + }); + + const transaction = new TransactionOnNetwork({ + logs: transactionLogs, + }); + + const outcome = parser.parseSetNewUris(transaction); + assert.lengthOf(outcome, 1); + assert.equal(outcome[0].tokenIdentifier, identifier); + assert.equal(outcome[0].nonce, nonce); + assert.equal(outcome[0].uri, uri); + }); + + it("should test parse modify creator", () => { + const identifier = "TEST-e2b0f9"; + const base64Identifier = Buffer.from(identifier).toString("base64"); + const nonce = BigInt(1); + + const encodedTopics = [base64Identifier, "AQ=="]; + const event = new TransactionEvent({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + identifier: "ESDTModifyCreator", + topics: b64TopicsToBytes(encodedTopics), + }); + + const transactionLogs = new TransactionLogs({ + address: new Address("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + events: [event], + }); + + const transaction = new TransactionOnNetwork({ + logs: transactionLogs, + }); + + const outcome = parser.parseModifyCreator(transaction); + assert.lengthOf(outcome, 1); + assert.equal(outcome[0].tokenIdentifier, identifier); + assert.equal(outcome[0].nonce, nonce); + }); + + it("should test parse update metadata", () => { + const identifier = "TEST-e2b0f9"; + const base64Identifier = Buffer.from(identifier).toString("base64"); + const nonce = BigInt(1); + const metadata = new Uint8Array( + Buffer.from( + "CAUSAgABIlQIARIHVEVTVE5GVBogATlHLv9ohncamC8wg9pdQh8kwpGB5jiIIo3IHKYNaeEgHioIbmV3X2hhc2gyDnRoaXNpYW51cmkuY29tOgkAAAAAAAAAAwUqHgjH9a4DEMf1rgMYx/WuAyDH9a4DKMb1rgMwx/WuAw==", + "base64", + ), + ); + + const encodedTopics = [ + base64Identifier, + "AQ==", + "", + "CAUSAgABIlQIARIHVEVTVE5GVBogATlHLv9ohncamC8wg9pdQh8kwpGB5jiIIo3IHKYNaeEgHioIbmV3X2hhc2gyDnRoaXNpYW51cmkuY29tOgkAAAAAAAAAAwUqHgjH9a4DEMf1rgMYx/WuAyDH9a4DKMb1rgMwx/WuAw==", + ]; + const event = new TransactionEvent({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + identifier: "ESDTMetaDataUpdate", + topics: b64TopicsToBytes(encodedTopics), + }); + + const transactionLogs = new TransactionLogs({ + address: new Address("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + events: [event], + }); + + const transaction = new TransactionOnNetwork({ + logs: transactionLogs, + }); + + const outcome = parser.parseUpdateMetadata(transaction); + assert.lengthOf(outcome, 1); + assert.equal(outcome[0].tokenIdentifier, identifier); + assert.equal(outcome[0].nonce, nonce); + assert.deepEqual(outcome[0].metadata, metadata); + }); + + it("should test parse recreate metadata", () => { + const identifier = "TEST-e2b0f9"; + const base64Identifier = Buffer.from(identifier).toString("base64"); + const nonce = BigInt(1); + const metadata = new Uint8Array( + Buffer.from( + "CAUSAgABIlAIARIHVEVTVE5GVBogATlHLv9ohncamC8wg9pdQh8kwpGB5jiIIo3IHKYNaeEgHioSbmV3X2hhc2hfcmVjcmVhdGVkMgA6CQAAAAAAAABkASoeCMj1rgMQyPWuAxjI9a4DIMj1rgMoyPWuAzDI9a4D", + "base64", + ), + ); + + const encodedTopics = [ + base64Identifier, + "AQ==", + "", + "CAUSAgABIlAIARIHVEVTVE5GVBogATlHLv9ohncamC8wg9pdQh8kwpGB5jiIIo3IHKYNaeEgHioSbmV3X2hhc2hfcmVjcmVhdGVkMgA6CQAAAAAAAABkASoeCMj1rgMQyPWuAxjI9a4DIMj1rgMoyPWuAzDI9a4D", + ]; + const event = new TransactionEvent({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + identifier: "ESDTMetaDataRecreate", + topics: b64TopicsToBytes(encodedTopics), + }); + + const transactionLogs = new TransactionLogs({ + address: new Address("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + events: [event], + }); + + const transaction = new TransactionOnNetwork({ + logs: transactionLogs, + }); + + const outcome = parser.parseMetadataRecreate(transaction); + assert.lengthOf(outcome, 1); + assert.equal(outcome[0].tokenIdentifier, identifier); + assert.equal(outcome[0].nonce, nonce); + assert.deepEqual(outcome[0].metadata, metadata); + }); + + it("should test parse change to dynamic", () => { + const identifier = "LKXOXNO-503365"; + const base64Identifier = Buffer.from(identifier).toString("base64"); + const tokenName = "LKXOXNO"; + const tokenTicker = "LKXOXNO"; + const tokenType = "DynamicMetaESDT"; + + const encodedTopics = [base64Identifier, "TEtYT1hOTw==", "TEtYT1hOTw==", "RHluYW1pY01ldGFFU0RU"]; + const event = new TransactionEvent({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + identifier: "changeToDynamic", + topics: b64TopicsToBytes(encodedTopics), + }); + + const transactionLogs = new TransactionLogs({ + address: new Address("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + events: [event], + }); + + const transaction = new TransactionOnNetwork({ + logs: transactionLogs, + }); + + const outcome = parser.parseChangeTokenToDynamic(transaction); + assert.lengthOf(outcome, 1); + assert.equal(outcome[0].tokenIdentifier, identifier); + assert.equal(outcome[0].tokenName, tokenName); + assert.equal(outcome[0].tickerName, tokenTicker); + assert.deepEqual(outcome[0].tokenType, tokenType); + }); + + it("should test parse register dynamic", () => { + const identifier = "TEST-9bbb21"; + const base64Identifier = Buffer.from(identifier).toString("base64"); + const tokenName = "TESTNFT"; + const tokenTicker = "TEST"; + const tokenType = "DynamicNonFungibleESDT"; + + const encodedTopics = [base64Identifier, "VEVTVE5GVA==", "VEVTVA==", "RHluYW1pY05vbkZ1bmdpYmxlRVNEVA=="]; + const event = new TransactionEvent({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + identifier: "registerDynamic", + topics: b64TopicsToBytes(encodedTopics), + }); + + const transactionLogs = new TransactionLogs({ + address: new Address("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + events: [event], + }); + + const transaction = new TransactionOnNetwork({ + logs: transactionLogs, + }); + + const outcome = parser.parseRegisterDynamicToken(transaction); + assert.lengthOf(outcome, 1); + assert.equal(outcome[0].tokenIdentifier, identifier); + assert.equal(outcome[0].tokenName, tokenName); + assert.equal(outcome[0].tokenTicker, tokenTicker); + assert.deepEqual(outcome[0].tokenType, tokenType); + }); + + it("should test parse register dynamic", () => { + const identifier = "TEST-9bbb21"; + const base64Identifier = Buffer.from(identifier).toString("base64"); + const tokenName = "TESTNFT"; + const tokenTicker = "TEST"; + const tokenType = "DynamicNonFungibleESDT"; + + const encodedTopics = [base64Identifier, "VEVTVE5GVA==", "VEVTVA==", "RHluYW1pY05vbkZ1bmdpYmxlRVNEVA=="]; + const event = new TransactionEvent({ + address: new Address("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"), + identifier: "registerAndSetAllRolesDynamic", + topics: b64TopicsToBytes(encodedTopics), + }); + + const transactionLogs = new TransactionLogs({ + address: new Address("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + events: [event], + }); + + const transaction = new TransactionOnNetwork({ + logs: transactionLogs, + }); + + const outcome = parser.parseRegisterDynamicTokenAndSettingRoles(transaction); + assert.lengthOf(outcome, 1); + assert.equal(outcome[0].tokenIdentifier, identifier); + assert.equal(outcome[0].tokenName, tokenName); + assert.equal(outcome[0].tokenTicker, tokenTicker); + assert.deepEqual(outcome[0].tokenType, tokenType); + }); +}); diff --git a/src/transactionsOutcomeParsers/tokenManagementTransactionsOutcomeParser.ts b/src/tokenManagement/tokenManagementTransactionsOutcomeParser.ts similarity index 57% rename from src/transactionsOutcomeParsers/tokenManagementTransactionsOutcomeParser.ts rename to src/tokenManagement/tokenManagementTransactionsOutcomeParser.ts index fcf78d585..6bb5be95d 100644 --- a/src/transactionsOutcomeParsers/tokenManagementTransactionsOutcomeParser.ts +++ b/src/tokenManagement/tokenManagementTransactionsOutcomeParser.ts @@ -1,54 +1,53 @@ -import { Address } from "../address"; -import { TransactionsConverter } from "../converters/transactionsConverter"; -import { ErrParseTransactionOutcome } from "../errors"; -import { ITransactionOnNetwork } from "../interfaceOfNetwork"; -import { bufferToBigInt } from "../smartcontracts/codec/utils"; -import { TransactionEvent, TransactionOutcome, findEventsByIdentifier } from "./resources"; +import BigNumber from "bignumber.js"; +import { bufferToBigInt } from "../abi/codec/utils"; +import { Address } from "../core/address"; +import { ErrParseTransactionOutcome } from "../core/errors"; +import { TransactionEvent } from "../core/transactionEvents"; +import { TransactionOnNetwork } from "../core/transactionOnNetwork"; +import { findEventsByIdentifier } from "../transactionsOutcomeParsers/resources"; +import { + ChangeToDynamicOutput, + MintNftOutput, + ModifyingCreatorOutput, + ModifyRoyaltiesOutput, + RegisterDynamicOutput, + SetNewUrisOutput, + SpecialRoleOutput, + UpdateAttibutesOutput, +} from "./resources"; export class TokenManagementTransactionsOutcomeParser { constructor() {} - parseIssueFungible(transaction: TransactionOutcome | ITransactionOnNetwork): { tokenIdentifier: string }[] { - transaction = this.ensureTransactionOutcome(transaction); - + parseIssueFungible(transaction: TransactionOnNetwork): { tokenIdentifier: string }[] { this.ensureNoError(transaction.logs.events); const events = findEventsByIdentifier(transaction, "issue"); return events.map((event) => ({ tokenIdentifier: this.extractTokenIdentifier(event) })); } - parseIssueNonFungible(transaction: TransactionOutcome | ITransactionOnNetwork): { tokenIdentifier: string }[] { - transaction = this.ensureTransactionOutcome(transaction); - + parseIssueNonFungible(transaction: TransactionOnNetwork): { tokenIdentifier: string }[] { this.ensureNoError(transaction.logs.events); const events = findEventsByIdentifier(transaction, "issueNonFungible"); return events.map((event) => ({ tokenIdentifier: this.extractTokenIdentifier(event) })); } - parseIssueSemiFungible(transaction: TransactionOutcome | ITransactionOnNetwork): { tokenIdentifier: string }[] { - transaction = this.ensureTransactionOutcome(transaction); - + parseIssueSemiFungible(transaction: TransactionOnNetwork): { tokenIdentifier: string }[] { this.ensureNoError(transaction.logs.events); const events = findEventsByIdentifier(transaction, "issueSemiFungible"); return events.map((event) => ({ tokenIdentifier: this.extractTokenIdentifier(event) })); } - parseRegisterMetaEsdt(transaction: TransactionOutcome | ITransactionOnNetwork): { tokenIdentifier: string }[] { - transaction = this.ensureTransactionOutcome(transaction); - + parseRegisterMetaEsdt(transaction: TransactionOnNetwork): { tokenIdentifier: string }[] { this.ensureNoError(transaction.logs.events); const events = findEventsByIdentifier(transaction, "registerMetaESDT"); return events.map((event) => ({ tokenIdentifier: this.extractTokenIdentifier(event) })); } - parseRegisterAndSetAllRoles( - transaction: TransactionOutcome | ITransactionOnNetwork, - ): { tokenIdentifier: string; roles: string[] }[] { - transaction = this.ensureTransactionOutcome(transaction); - + parseRegisterAndSetAllRoles(transaction: TransactionOnNetwork): { tokenIdentifier: string; roles: string[] }[] { this.ensureNoError(transaction.logs.events); const registerEvents = findEventsByIdentifier(transaction, "registerAndSetAllRoles"); const setRoleEvents = findEventsByIdentifier(transaction, "ESDTSetRole"); @@ -67,36 +66,22 @@ export class TokenManagementTransactionsOutcomeParser { }); } - parseSetBurnRoleGlobally(transaction: TransactionOutcome | ITransactionOnNetwork) { - transaction = this.ensureTransactionOutcome(transaction); - + parseSetBurnRoleGlobally(transaction: TransactionOnNetwork) { this.ensureNoError(transaction.logs.events); } - parseUnsetBurnRoleGlobally(transaction: TransactionOutcome | ITransactionOnNetwork) { - transaction = this.ensureTransactionOutcome(transaction); - + parseUnsetBurnRoleGlobally(transaction: TransactionOnNetwork) { this.ensureNoError(transaction.logs.events); } - parseSetSpecialRole(transaction: TransactionOutcome | ITransactionOnNetwork): { - userAddress: string; - tokenIdentifier: string; - roles: string[]; - }[] { - transaction = this.ensureTransactionOutcome(transaction); - + parseSetSpecialRole(transaction: TransactionOnNetwork): SpecialRoleOutput[] { this.ensureNoError(transaction.logs.events); const events = findEventsByIdentifier(transaction, "ESDTSetRole"); return events.map((event) => this.getOutputForSetSpecialRoleEvent(event)); } - private getOutputForSetSpecialRoleEvent(event: TransactionEvent): { - userAddress: string; - tokenIdentifier: string; - roles: string[]; - } { + private getOutputForSetSpecialRoleEvent(event: TransactionEvent): SpecialRoleOutput { const userAddress = event.address; const tokenIdentifier = this.extractTokenIdentifier(event); const encodedRoles = event.topics.slice(3); @@ -105,13 +90,7 @@ export class TokenManagementTransactionsOutcomeParser { return { userAddress: userAddress, tokenIdentifier: tokenIdentifier, roles: roles }; } - parseNftCreate(transaction: TransactionOutcome | ITransactionOnNetwork): { - tokenIdentifier: string; - nonce: bigint; - initialQuantity: bigint; - }[] { - transaction = this.ensureTransactionOutcome(transaction); - + parseNftCreate(transaction: TransactionOnNetwork): MintNftOutput[] { this.ensureNoError(transaction.logs.events); const events = findEventsByIdentifier(transaction, "ESDTNFTCreate"); @@ -130,14 +109,12 @@ export class TokenManagementTransactionsOutcomeParser { return { tokenIdentifier: tokenIdentifier, nonce: nonce, initialQuantity: amount }; } - parseLocalMint(transaction: TransactionOutcome | ITransactionOnNetwork): { - userAddress: string; + parseLocalMint(transaction: TransactionOnNetwork): { + userAddress: Address; tokenIdentifier: string; nonce: bigint; mintedSupply: bigint; }[] { - transaction = this.ensureTransactionOutcome(transaction); - this.ensureNoError(transaction.logs.events); const events = findEventsByIdentifier(transaction, "ESDTLocalMint"); @@ -145,7 +122,7 @@ export class TokenManagementTransactionsOutcomeParser { } private getOutputForLocalMintEvent(event: TransactionEvent): { - userAddress: string; + userAddress: Address; tokenIdentifier: string; nonce: bigint; mintedSupply: bigint; @@ -163,14 +140,12 @@ export class TokenManagementTransactionsOutcomeParser { }; } - parseLocalBurn(transaction: TransactionOutcome | ITransactionOnNetwork): { - userAddress: string; + parseLocalBurn(transaction: TransactionOnNetwork): { + userAddress: Address; tokenIdentifier: string; nonce: bigint; burntSupply: bigint; }[] { - transaction = this.ensureTransactionOutcome(transaction); - this.ensureNoError(transaction.logs.events); const events = findEventsByIdentifier(transaction, "ESDTLocalBurn"); @@ -178,7 +153,7 @@ export class TokenManagementTransactionsOutcomeParser { } private getOutputForLocalBurnEvent(event: TransactionEvent): { - userAddress: string; + userAddress: Address; tokenIdentifier: string; nonce: bigint; burntSupply: bigint; @@ -196,32 +171,26 @@ export class TokenManagementTransactionsOutcomeParser { }; } - parsePause(transaction: TransactionOutcome | ITransactionOnNetwork): { tokenIdentifier: string }[] { - transaction = this.ensureTransactionOutcome(transaction); - + parsePause(transaction: TransactionOnNetwork): { tokenIdentifier: string }[] { this.ensureNoError(transaction.logs.events); const events = findEventsByIdentifier(transaction, "ESDTPause"); return events.map((event) => ({ tokenIdentifier: this.extractTokenIdentifier(event) })); } - parseUnpause(transaction: TransactionOutcome | ITransactionOnNetwork): { tokenIdentifier: string }[] { - transaction = this.ensureTransactionOutcome(transaction); - + parseUnpause(transaction: TransactionOnNetwork): { tokenIdentifier: string }[] { this.ensureNoError(transaction.logs.events); const events = findEventsByIdentifier(transaction, "ESDTUnPause"); return events.map((event) => ({ tokenIdentifier: this.extractTokenIdentifier(event) })); } - parseFreeze(transaction: TransactionOutcome | ITransactionOnNetwork): { + parseFreeze(transaction: TransactionOnNetwork): { userAddress: string; tokenIdentifier: string; nonce: bigint; balance: bigint; }[] { - transaction = this.ensureTransactionOutcome(transaction); - this.ensureNoError(transaction.logs.events); const events = findEventsByIdentifier(transaction, "ESDTFreeze"); @@ -247,14 +216,12 @@ export class TokenManagementTransactionsOutcomeParser { }; } - parseUnfreeze(transaction: TransactionOutcome | ITransactionOnNetwork): { + parseUnfreeze(transaction: TransactionOnNetwork): { userAddress: string; tokenIdentifier: string; nonce: bigint; balance: bigint; }[] { - transaction = this.ensureTransactionOutcome(transaction); - this.ensureNoError(transaction.logs.events); const events = findEventsByIdentifier(transaction, "ESDTUnFreeze"); @@ -280,14 +247,12 @@ export class TokenManagementTransactionsOutcomeParser { }; } - parseWipe(transaction: TransactionOutcome | ITransactionOnNetwork): { + parseWipe(transaction: TransactionOnNetwork): { userAddress: string; tokenIdentifier: string; nonce: bigint; balance: bigint; }[] { - transaction = this.ensureTransactionOutcome(transaction); - this.ensureNoError(transaction.logs.events); const events = findEventsByIdentifier(transaction, "ESDTWipe"); @@ -313,13 +278,11 @@ export class TokenManagementTransactionsOutcomeParser { }; } - parseUpdateAttributes(transaction: TransactionOutcome | ITransactionOnNetwork): { + parseUpdateAttributes(transaction: TransactionOnNetwork): { tokenIdentifier: string; nonce: bigint; attributes: Uint8Array; }[] { - transaction = this.ensureTransactionOutcome(transaction); - this.ensureNoError(transaction.logs.events); const events = findEventsByIdentifier(transaction, "ESDTNFTUpdateAttributes"); @@ -333,7 +296,7 @@ export class TokenManagementTransactionsOutcomeParser { } { const tokenIdentifier = this.extractTokenIdentifier(event); const nonce = this.extractNonce(event); - const attributes = event.topics[3] ? event.topics[3] : new Uint8Array(); + const attributes = event.topics[3] ? event.topics[3] : Buffer.from(""); return { tokenIdentifier: tokenIdentifier, @@ -342,13 +305,11 @@ export class TokenManagementTransactionsOutcomeParser { }; } - parseAddQuantity(transaction: TransactionOutcome | ITransactionOnNetwork): { + parseAddQuantity(transaction: TransactionOnNetwork): { tokenIdentifier: string; nonce: bigint; addedQuantity: bigint; }[] { - transaction = this.ensureTransactionOutcome(transaction); - this.ensureNoError(transaction.logs.events); const events = findEventsByIdentifier(transaction, "ESDTNFTAddQuantity"); @@ -371,13 +332,11 @@ export class TokenManagementTransactionsOutcomeParser { }; } - parseBurnQuantity(transaction: TransactionOutcome | ITransactionOnNetwork): { + parseBurnQuantity(transaction: TransactionOnNetwork): { tokenIdentifier: string; nonce: bigint; burntQuantity: bigint; }[] { - transaction = this.ensureTransactionOutcome(transaction); - this.ensureNoError(transaction.logs.events); const events = findEventsByIdentifier(transaction, "ESDTNFTBurn"); @@ -400,21 +359,139 @@ export class TokenManagementTransactionsOutcomeParser { }; } - /** - * Temporary workaround, until "TransactionOnNetwork" completely replaces "TransactionOutcome". - */ - private ensureTransactionOutcome(transaction: TransactionOutcome | ITransactionOnNetwork): TransactionOutcome { - if ("hash" in transaction) { - return new TransactionsConverter().transactionOnNetworkToOutcome(transaction); - } + parseModifyRoyalties(transaction: TransactionOnNetwork): ModifyRoyaltiesOutput[] { + this.ensureNoError(transaction.logs.events); + + const events = findEventsByIdentifier(transaction, "ESDTModifyRoyalties"); + return events.map((event) => this.getOutputForESDTModifyRoyaltiesEvent(event)); + } + + private getOutputForESDTModifyRoyaltiesEvent(event: TransactionEvent): ModifyRoyaltiesOutput { + const tokenIdentifier = this.extractTokenIdentifier(event); + const nonce = this.extractNonce(event); + const royalties = !event.topics[3]?.length + ? BigInt(0) + : BigInt(new BigNumber(Buffer.from(event.topics[3]).toString("hex"), 16).toFixed()); + + return { + tokenIdentifier: tokenIdentifier, + nonce: nonce, + royalties, + }; + } + + parseSetNewUris(transaction: TransactionOnNetwork): SetNewUrisOutput[] { + this.ensureNoError(transaction.logs.events); + + const events = findEventsByIdentifier(transaction, "ESDTSetNewURIs"); + return events.map((event) => this.getOutputForESDTSetNewURIsEvent(event)); + } + + private getOutputForESDTSetNewURIsEvent(event: TransactionEvent): SetNewUrisOutput { + const tokenIdentifier = this.extractTokenIdentifier(event); + const nonce = this.extractNonce(event); + const uri = event.topics[3]?.length ? event.topics[3].toString() : ""; + return { tokenIdentifier, nonce, uri }; + } + + parseModifyCreator(transaction: TransactionOnNetwork): ModifyingCreatorOutput[] { + this.ensureNoError(transaction.logs.events); + + const events = findEventsByIdentifier(transaction, "ESDTModifyCreator"); + return events.map((event) => this.getOutputForESDTModifyCreatorEvent(event)); + } + + private getOutputForESDTModifyCreatorEvent(event: TransactionEvent): ModifyingCreatorOutput { + const tokenIdentifier = this.extractTokenIdentifier(event); + const nonce = this.extractNonce(event); + + return { tokenIdentifier, nonce }; + } + + parseUpdateMetadata(transaction: TransactionOnNetwork): UpdateAttibutesOutput[] { + this.ensureNoError(transaction.logs.events); + + const events = findEventsByIdentifier(transaction, "ESDTMetaDataUpdate"); + return events.map((event) => this.getOutputForESDTUpdateMetadataEvent(event)); + } + + private getOutputForESDTUpdateMetadataEvent(event: TransactionEvent): UpdateAttibutesOutput { + const tokenIdentifier = this.extractTokenIdentifier(event); + const nonce = this.extractNonce(event); + const metadata = event.topics[3]?.length ? new Uint8Array(Buffer.from(event.topics[3])) : new Uint8Array(); + + return { tokenIdentifier, nonce, metadata }; + } + + parseMetadataRecreate(transaction: TransactionOnNetwork): UpdateAttibutesOutput[] { + this.ensureNoError(transaction.logs.events); + + const events = findEventsByIdentifier(transaction, "ESDTMetaDataRecreate"); + return events.map((event) => this.getOutputForESDTMetadataRecreateEvent(event)); + } + + private getOutputForESDTMetadataRecreateEvent(event: TransactionEvent): UpdateAttibutesOutput { + const tokenIdentifier = this.extractTokenIdentifier(event); + const nonce = this.extractNonce(event); + const metadata = event.topics[3]?.length ? new Uint8Array(Buffer.from(event.topics[3])) : new Uint8Array(); + + return { tokenIdentifier, nonce, metadata }; + } + + parseChangeTokenToDynamic(transaction: TransactionOnNetwork): ChangeToDynamicOutput[] { + this.ensureNoError(transaction.logs.events); + + const events = findEventsByIdentifier(transaction, "changeToDynamic"); + return events.map((event) => this.getOutputForChangeToDynamicEvent(event)); + } + + private getOutputForChangeToDynamicEvent(event: TransactionEvent): ChangeToDynamicOutput { + const tokenIdentifier = this.extractTokenIdentifier(event); + const tokenName = event.topics[1]?.length ? event.topics[1].toString() : ""; + const tickerName = event.topics[2]?.length ? event.topics[2].toString() : ""; + const tokenType = event.topics[3]?.length ? event.topics[3].toString() : ""; + + return { tokenIdentifier, tokenName, tickerName, tokenType }; + } + + parseRegisterDynamicToken(transaction: TransactionOnNetwork): RegisterDynamicOutput[] { + this.ensureNoError(transaction.logs.events); + + const events = findEventsByIdentifier(transaction, "registerDynamic"); + return events.map((event) => this.getOutputForRegisterDynamicToken(event)); + } + + private getOutputForRegisterDynamicToken(event: TransactionEvent): RegisterDynamicOutput { + const tokenIdentifier = event.topics[0]?.length ? event.topics[0].toString() : ""; + const tokenName = event.topics[1]?.length ? event.topics[1].toString() : ""; + const tokenTicker = event.topics[2]?.length ? event.topics[2].toString() : ""; + const tokenType = event.topics[3]?.length ? event.topics[3].toString() : ""; + const numOfDecimals = event.topics[4]?.length ? Number(Buffer.from(event.topics[4]).toString()) : 0; + + return { tokenIdentifier, tokenName, tokenTicker, tokenType, numOfDecimals }; + } + + parseRegisterDynamicTokenAndSettingRoles(transaction: TransactionOnNetwork): RegisterDynamicOutput[] { + this.ensureNoError(transaction.logs.events); + + const events = findEventsByIdentifier(transaction, "registerAndSetAllRolesDynamic"); + return events.map((event) => this.getOutputForRegisterDynamicTokenAndSettingRoles(event)); + } + + private getOutputForRegisterDynamicTokenAndSettingRoles(event: TransactionEvent): RegisterDynamicOutput { + const tokenIdentifier = event.topics[0]?.length ? event.topics[0].toString() : ""; + const tokenName = event.topics[1]?.length ? event.topics[1].toString() : ""; + const tokenTicker = event.topics[2]?.length ? event.topics[2].toString() : ""; + const tokenType = event.topics[3]?.length ? event.topics[3].toString() : ""; + const numOfDecimals = event.topics[4]?.length ? Number(Buffer.from(event.topics[4]).toString()) : 0; - return transaction; + return { tokenIdentifier, tokenName, tokenTicker, tokenType, numOfDecimals }; } private ensureNoError(transactionEvents: TransactionEvent[]) { for (const event of transactionEvents) { if (event.identifier == "signalError") { - const data = Buffer.from(event.dataItems[0]?.toString().slice(1)).toString() || ""; + const data = Buffer.from(event.additionalData[0]?.toString().slice(1)).toString() || ""; const message = this.decodeTopicAsString(event.topics[1]); throw new ErrParseTransactionOutcome( @@ -428,7 +505,7 @@ export class TokenManagementTransactionsOutcomeParser { if (!event.topics[0]?.length) { return ""; } - return this.decodeTopicAsString(event.topics[0]); + return event.topics[0].toString(); } private extractNonce(event: TransactionEvent): bigint { @@ -452,7 +529,7 @@ export class TokenManagementTransactionsOutcomeParser { return ""; } const address = Buffer.from(event.topics[3]); - return Address.fromBuffer(address).bech32(); + return new Address(address).toBech32(); } private decodeTopicAsString(topic: Uint8Array): string { diff --git a/src/tokenOperations/codec.ts b/src/tokenOperations/codec.ts index 64b4c4844..be43feff8 100644 --- a/src/tokenOperations/codec.ts +++ b/src/tokenOperations/codec.ts @@ -1,6 +1,6 @@ import BigNumber from "bignumber.js"; -import * as contractsCodecUtils from "../smartcontracts/codec/utils"; -import * as codecUtils from "../utils.codec"; +import * as contractsCodecUtils from "../abi/codec/utils"; +import * as codecUtils from "../core/utils.codec"; export function stringToBuffer(value: string): Buffer { return Buffer.from(value); @@ -22,7 +22,7 @@ export function bigIntToBuffer(value: BigNumber.Value): Buffer { return contractsCodecUtils.bigIntToBuffer(value); } -export { utf8ToHex, bigIntToHex, addressToHex } from "../utils.codec"; +export { bigIntToHex, utf8ToHex } from "../core/utils.codec"; export function bufferToHex(value: Buffer) { const hex = value.toString("hex"); diff --git a/src/tokenOperations/index.ts b/src/tokenOperations/index.ts deleted file mode 100644 index 561db6c7b..000000000 --- a/src/tokenOperations/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./tokenOperationsFactory"; -export * from "./tokenOperationsFactoryConfig"; -export * from "./tokenOperationsOutcomeParser"; diff --git a/src/tokenOperations/tokenOperationsFactory.spec.ts b/src/tokenOperations/tokenOperationsFactory.spec.ts deleted file mode 100644 index f4e97ac6c..000000000 --- a/src/tokenOperations/tokenOperationsFactory.spec.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { assert } from "chai"; -import { loadTestWallets, TestWallet } from "../testutils"; -import { TokenOperationsFactory } from "./tokenOperationsFactory"; -import { TokenOperationsFactoryConfig } from "./tokenOperationsFactoryConfig"; - -describe("test factory", () => { - let frank: TestWallet, grace: TestWallet; - let factory: TokenOperationsFactory; - - before(async function () { - ({ frank, grace } = await loadTestWallets()); - factory = new TokenOperationsFactory(new TokenOperationsFactoryConfig("T")); - }); - - it("should create ", () => { - const transaction = factory.registerAndSetAllRoles({ - issuer: frank.address, - tokenName: "TEST", - tokenTicker: "TEST", - tokenType: "FNG", - numDecimals: 2, - transactionNonce: 42, - }); - - assert.equal(transaction.getData().toString(), "registerAndSetAllRoles@54455354@54455354@464e47@02"); - assert.equal(transaction.getNonce(), 42); - assert.equal(transaction.getSender().toString(), frank.address.toString()); - assert.equal( - transaction.getReceiver().toString(), - "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u", - ); - }); - - it("should create ", () => { - const transaction = factory.issueFungible({ - issuer: frank.address, - tokenName: "FRANK", - tokenTicker: "FRANK", - initialSupply: 100, - numDecimals: 0, - canFreeze: true, - canWipe: true, - canPause: true, - canChangeOwner: true, - canUpgrade: false, - canAddSpecialRoles: false, - transactionNonce: 42, - }); - - assert.equal( - transaction.getData().toString(), - "issue@4652414e4b@4652414e4b@64@@63616e467265657a65@74727565@63616e57697065@74727565@63616e5061757365@74727565@63616e4368616e67654f776e6572@74727565@63616e55706772616465@66616c7365@63616e4164645370656369616c526f6c6573@66616c7365", - ); - assert.equal(transaction.getNonce(), 42); - assert.equal(transaction.getSender().toString(), frank.address.toString()); - assert.equal( - transaction.getReceiver().toString(), - "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u", - ); - }); - - it("should create ", () => { - const transaction = factory.issueSemiFungible({ - issuer: frank.address, - tokenName: "FRANK", - tokenTicker: "FRANK", - canFreeze: true, - canWipe: true, - canPause: true, - canTransferNFTCreateRole: true, - canChangeOwner: true, - canUpgrade: false, - canAddSpecialRoles: false, - transactionNonce: 42, - }); - - assert.equal( - transaction.getData().toString(), - "issueSemiFungible@4652414e4b@4652414e4b@63616e467265657a65@74727565@63616e57697065@74727565@63616e5061757365@74727565@63616e5472616e736665724e4654437265617465526f6c65@74727565@63616e4368616e67654f776e6572@74727565@63616e55706772616465@66616c7365@63616e4164645370656369616c526f6c6573@66616c7365", - ); - assert.equal(transaction.getNonce(), 42); - assert.equal(transaction.getSender().toString(), frank.address.toString()); - assert.equal( - transaction.getReceiver().toString(), - "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u", - ); - }); - - it("should create ", () => { - const transaction = factory.issueNonFungible({ - issuer: frank.address, - tokenName: "FRANK", - tokenTicker: "FRANK", - canFreeze: true, - canWipe: true, - canPause: true, - canTransferNFTCreateRole: true, - canChangeOwner: true, - canUpgrade: false, - canAddSpecialRoles: false, - transactionNonce: 42, - }); - - assert.equal( - transaction.getData().toString(), - "issueNonFungible@4652414e4b@4652414e4b@63616e467265657a65@74727565@63616e57697065@74727565@63616e5061757365@74727565@63616e5472616e736665724e4654437265617465526f6c65@74727565@63616e4368616e67654f776e6572@74727565@63616e55706772616465@66616c7365@63616e4164645370656369616c526f6c6573@66616c7365", - ); - assert.equal(transaction.getNonce(), 42); - assert.equal(transaction.getSender().toString(), frank.address.toString()); - assert.equal( - transaction.getReceiver().toString(), - "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u", - ); - }); - - it("should create ", () => { - const transaction = factory.registerMetaESDT({ - issuer: frank.address, - tokenName: "FRANK", - tokenTicker: "FRANK", - numDecimals: 10, - canFreeze: true, - canWipe: true, - canPause: true, - canTransferNFTCreateRole: true, - canChangeOwner: true, - canUpgrade: false, - canAddSpecialRoles: false, - transactionNonce: 42, - }); - - assert.equal( - transaction.getData().toString(), - "registerMetaESDT@4652414e4b@4652414e4b@0a@63616e467265657a65@74727565@63616e57697065@74727565@63616e5061757365@74727565@63616e5472616e736665724e4654437265617465526f6c65@74727565@63616e4368616e67654f776e6572@74727565@63616e55706772616465@66616c7365@63616e4164645370656369616c526f6c6573@66616c7365", - ); - assert.equal(transaction.getNonce(), 42); - assert.equal(transaction.getSender().toString(), frank.address.toString()); - assert.equal( - transaction.getReceiver().toString(), - "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u", - ); - }); - - it("should create ", () => { - const transaction = factory.setSpecialRoleOnNonFungible({ - manager: frank.address, - user: grace.address, - tokenIdentifier: "FRANK-11ce3e", - addRoleNFTCreate: true, - addRoleNFTBurn: false, - addRoleNFTUpdateAttributes: true, - addRoleNFTAddURI: true, - addRoleESDTTransferRole: false, - transactionNonce: 42, - }); - - assert.equal( - transaction.getData().toString(), - "setSpecialRole@4652414e4b2d313163653365@1e8a8b6b49de5b7be10aaa158a5a6a4abb4b56cc08f524bb5e6cd5f211ad3e13@45534454526f6c654e4654437265617465@45534454526f6c654e465455706461746541747472696275746573@45534454526f6c654e4654416464555249", - ); - assert.equal(transaction.getNonce(), 42); - assert.equal(transaction.getSender().toString(), frank.address.toString()); - assert.equal( - transaction.getReceiver().toString(), - "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u", - ); - }); - - it("should create ", () => { - const transaction = factory.nftCreate({ - creator: grace.address, - tokenIdentifier: "FRANK-aa9e8d", - initialQuantity: 1, - name: `test`, - royalties: 1000, - hash: "abba", - attributes: Buffer.from("test"), - uris: ["a", "b"], - transactionNonce: 42, - }); - - assert.equal( - transaction.getData().toString(), - "ESDTNFTCreate@4652414e4b2d616139653864@01@74657374@03e8@61626261@74657374@61@62", - ); - assert.equal(transaction.getNonce(), 42); - assert.equal(transaction.getSender().toString(), grace.address.toString()); - assert.equal(transaction.getReceiver().toString(), grace.address.toString()); - }); -}); diff --git a/src/tokenOperations/tokenOperationsFactory.test.net.spec.ts b/src/tokenOperations/tokenOperationsFactory.test.net.spec.ts deleted file mode 100644 index 2ae574fa8..000000000 --- a/src/tokenOperations/tokenOperationsFactory.test.net.spec.ts +++ /dev/null @@ -1,688 +0,0 @@ -import { assert } from "chai"; -import { GasEstimator } from "../gasEstimator"; -import { INetworkConfig, ITransactionOnNetwork } from "../interfaceOfNetwork"; -import { TestWallet, loadTestWallets } from "../testutils"; -import { INetworkProvider, createTestnetProvider } from "../testutils/networkProviders"; -import { TokenTransfer } from "../tokens"; -import { Transaction } from "../transaction"; -import { TransactionWatcher } from "../transactionWatcher"; -import { TransferTransactionsFactory } from "../transactionsFactories/transferTransactionsFactory"; -import { TokenOperationsFactory } from "./tokenOperationsFactory"; -import { TokenOperationsFactoryConfig } from "./tokenOperationsFactoryConfig"; -import { TokenOperationsOutcomeParser } from "./tokenOperationsOutcomeParser"; - -describe("test factory on testnet", function () { - let frank: TestWallet, grace: TestWallet; - let provider: INetworkProvider; - let watcher: TransactionWatcher; - let network: INetworkConfig; - let factory: TokenOperationsFactory; - let parser: TokenOperationsOutcomeParser; - let transferTransactionsFactory: TransferTransactionsFactory; - - before(async function () { - console.log(`> ${this.currentTest?.title} ...`); - - ({ frank, grace } = await loadTestWallets()); - - provider = createTestnetProvider(); - watcher = new TransactionWatcher(provider, { patienceMilliseconds: 8000 }); - network = await provider.getNetworkConfig(); - factory = new TokenOperationsFactory(new TokenOperationsFactoryConfig(network.ChainID)); - parser = new TokenOperationsOutcomeParser(); - transferTransactionsFactory = new TransferTransactionsFactory(new GasEstimator()); - }); - - it("should register and set all roles (FNG)", async function () { - this.timeout(120000); - await frank.sync(provider); - - const tx1 = factory.registerAndSetAllRoles({ - issuer: frank.address, - tokenName: "TEST", - tokenTicker: "TEST", - tokenType: "FNG", - numDecimals: 2, - transactionNonce: frank.account.nonce, - }); - - const tx1OnNetwork = await processTransaction(frank, tx1, "tx1"); - const tx1Outcome = parser.parseRegisterAndSetAllRoles(tx1OnNetwork); - - assert.isTrue(tx1Outcome.tokenIdentifier.includes("TEST")); - assert.isTrue(tx1Outcome.roles.includes("ESDTRoleLocalMint")); - assert.isTrue(tx1Outcome.roles.includes("ESDTRoleLocalBurn")); - }); - - it("should register and set all roles (NFT)", async function () { - this.timeout(120000); - await frank.sync(provider); - - const tx1 = factory.registerAndSetAllRoles({ - issuer: frank.address, - tokenName: "TEST", - tokenTicker: "TEST", - tokenType: "NFT", - numDecimals: 0, - transactionNonce: frank.account.nonce, - }); - - const tx1OnNetwork = await processTransaction(frank, tx1, "tx1"); - const tx1Outcome = parser.parseRegisterAndSetAllRoles(tx1OnNetwork); - - assert.isTrue(tx1Outcome.tokenIdentifier.includes("TEST")); - assert.isTrue(tx1Outcome.roles.includes("ESDTRoleNFTCreate")); - assert.isTrue(tx1Outcome.roles.includes("ESDTRoleNFTBurn")); - assert.isTrue(tx1Outcome.roles.includes("ESDTRoleNFTUpdateAttributes")); - assert.isTrue(tx1Outcome.roles.includes("ESDTRoleNFTAddURI")); - }); - - it("should register and set all roles (SFT)", async function () { - this.timeout(120000); - await frank.sync(provider); - - const tx1 = factory.registerAndSetAllRoles({ - issuer: frank.address, - tokenName: "TEST", - tokenTicker: "TEST", - tokenType: "SFT", - numDecimals: 0, - transactionNonce: frank.account.nonce, - }); - - const tx1OnNetwork = await processTransaction(frank, tx1, "tx1"); - const tx1Outcome = parser.parseRegisterAndSetAllRoles(tx1OnNetwork); - - assert.isTrue(tx1Outcome.tokenIdentifier.includes("TEST")); - assert.isTrue(tx1Outcome.roles.includes("ESDTRoleNFTCreate")); - assert.isTrue(tx1Outcome.roles.includes("ESDTRoleNFTBurn")); - assert.isTrue(tx1Outcome.roles.includes("ESDTRoleNFTAddQuantity")); - }); - - it("should register and set all roles (META)", async function () { - this.timeout(120000); - await frank.sync(provider); - - const tx1 = factory.registerAndSetAllRoles({ - issuer: frank.address, - tokenName: "TEST", - tokenTicker: "TEST", - tokenType: "META", - numDecimals: 2, - transactionNonce: frank.account.nonce, - }); - - const tx1OnNetwork = await processTransaction(frank, tx1, "tx1"); - const tx1Outcome = parser.parseRegisterAndSetAllRoles(tx1OnNetwork); - - assert.isTrue(tx1Outcome.tokenIdentifier.includes("TEST")); - assert.isTrue(tx1Outcome.roles.includes("ESDTRoleNFTCreate")); - assert.isTrue(tx1Outcome.roles.includes("ESDTRoleNFTBurn")); - assert.isTrue(tx1Outcome.roles.includes("ESDTRoleNFTAddQuantity")); - }); - - it("should issue fungible, then toggle global burn", async function () { - this.timeout(180000); - await frank.sync(provider); - - // Issue - const tx1 = factory.issueFungible({ - issuer: frank.address, - tokenName: "FRANK", - tokenTicker: "FRANK", - initialSupply: 100, - numDecimals: 0, - canFreeze: true, - canWipe: true, - canPause: true, - canChangeOwner: true, - canUpgrade: true, - canAddSpecialRoles: true, - transactionNonce: frank.account.nonce, - }); - - const tx1OnNetwork = await processTransaction(frank, tx1, "tx1"); - const tx1Outcome = parser.parseIssueFungible(tx1OnNetwork); - const tokenIdentifier = tx1Outcome.tokenIdentifier; - assert.isTrue(tokenIdentifier.includes("FRANK")); - - // Unset global burn - const tx2 = factory.unsetBurnRoleGlobally({ - manager: frank.address, - tokenIdentifier: tx1Outcome.tokenIdentifier, - transactionNonce: frank.account.nonce, - }); - - const tx2OnNetwork = await processTransaction(frank, tx2, "tx2"); - const _tx2Outcome = parser.parseUnsetBurnRoleGlobally(tx2OnNetwork); - - // Set global burn - const tx3 = factory.setBurnRoleGlobally({ - manager: frank.address, - tokenIdentifier: tx1Outcome.tokenIdentifier, - transactionNonce: frank.account.nonce, - }); - - const tx3OnNetwork = await processTransaction(frank, tx3, "tx3"); - const _tx3Outcome = parser.parseSetBurnRoleGlobally(tx3OnNetwork); - }); - - it("should issue fungible, mint, burn", async function () { - this.timeout(120000); - await frank.sync(provider); - await grace.sync(provider); - - // Issue - const tx1 = factory.issueFungible({ - issuer: frank.address, - tokenName: "FRANK", - tokenTicker: "FRANK", - initialSupply: 100, - numDecimals: 0, - canFreeze: true, - canWipe: true, - canPause: true, - canChangeOwner: true, - canUpgrade: true, - canAddSpecialRoles: true, - transactionNonce: frank.account.nonce, - }); - - const tx1OnNetwork = await processTransaction(frank, tx1, "tx1"); - const tx1Outcome = parser.parseIssueFungible(tx1OnNetwork); - const tokenIdentifier = tx1Outcome.tokenIdentifier; - assert.isTrue(tokenIdentifier.includes("FRANK")); - - // Set roles (give Grace the ability to mint and burn) - const tx2 = factory.setSpecialRoleOnFungible({ - manager: frank.address, - user: grace.address, - tokenIdentifier: tx1Outcome.tokenIdentifier, - addRoleLocalMint: true, - addRoleLocalBurn: true, - transactionNonce: frank.account.nonce, - }); - - const tx2OnNetwork = await processTransaction(frank, tx2, "tx2"); - const tx2Outcome = parser.parseSetSpecialRole(tx2OnNetwork); - assert.include(tx2Outcome.roles, "ESDTRoleLocalMint"); - assert.include(tx2Outcome.roles, "ESDTRoleLocalBurn"); - - // Mint (Grace mints for herself) - const tx3 = factory.localMint({ - manager: grace.address, - user: grace.address, - tokenIdentifier: tokenIdentifier, - supplyToMint: 200, - transactionNonce: grace.account.nonce, - }); - - const tx3OnNetwork = await processTransaction(grace, tx3, "tx3"); - const tx3Outcome = parser.parseLocalMint(tx3OnNetwork); - assert.equal(tx3Outcome.mintedSupply, "200"); - - // Burn (Grace burns 50 of her tokens) - const tx4 = factory.localBurn({ - manager: grace.address, - user: grace.address, - tokenIdentifier: tokenIdentifier, - supplyToBurn: 50, - transactionNonce: grace.account.nonce, - }); - - const tx4OnNetwork = await processTransaction(grace, tx4, "tx4"); - const tx4Outcome = parser.parseLocalBurn(tx4OnNetwork); - assert.equal(tx4Outcome.burntSupply, "50"); - }); - - it("should issue fungible, pause, unpause", async function () { - this.timeout(240000); - await frank.sync(provider); - - // Issue - const tx1 = factory.issueFungible({ - issuer: frank.address, - tokenName: "FRANK", - tokenTicker: "FRANK", - initialSupply: 100, - numDecimals: 0, - canFreeze: true, - canWipe: true, - canPause: true, - canChangeOwner: true, - canUpgrade: true, - canAddSpecialRoles: true, - transactionNonce: frank.account.nonce, - }); - - const tx1OnNetwork = await processTransaction(frank, tx1, "tx1"); - const tx1Outcome = parser.parseIssueFungible(tx1OnNetwork); - const tokenIdentifier = tx1Outcome.tokenIdentifier; - assert.isTrue(tokenIdentifier.includes("FRANK")); - - // Pause - const tx2 = factory.pause({ - manager: frank.address, - tokenIdentifier: tokenIdentifier, - transactionNonce: frank.account.nonce, - }); - - const tx2OnNetwork = await processTransaction(frank, tx2, "tx2"); - const _tx2Outcome = parser.parsePause(tx2OnNetwork); - - // Unpause - const tx3 = factory.unpause({ - manager: frank.address, - tokenIdentifier: tokenIdentifier, - transactionNonce: frank.account.nonce, - }); - - const tx3OnNetwork = await processTransaction(frank, tx3, "tx3"); - const _tx3Outcome = parser.parseUnpause(tx3OnNetwork); - - // Send some tokens to Grace - const tx4 = transferTransactionsFactory.createESDTTransfer({ - tokenTransfer: TokenTransfer.fungibleFromBigInteger(tokenIdentifier, 10), - sender: frank.account.address, - receiver: grace.account.address, - chainID: network.ChainID, - nonce: frank.account.nonce, - }); - - const _tx4OnNetwork = await processTransaction(frank, tx4, "tx4"); - }); - - it("should issue fungible, freeze, unfreeze", async function () { - this.timeout(240000); - await frank.sync(provider); - - // Issue - const tx1 = factory.issueFungible({ - issuer: frank.address, - tokenName: "FRANK", - tokenTicker: "FRANK", - initialSupply: 100, - numDecimals: 0, - canFreeze: true, - canWipe: true, - canPause: true, - canChangeOwner: true, - canUpgrade: true, - canAddSpecialRoles: true, - transactionNonce: frank.account.nonce, - }); - - const tx1OnNetwork = await processTransaction(frank, tx1, "tx1"); - const tx1Outcome = parser.parseIssueFungible(tx1OnNetwork); - const tokenIdentifier = tx1Outcome.tokenIdentifier; - assert.isTrue(tokenIdentifier.includes("FRANK")); - - // Send some tokens to Grace - const tx2 = transferTransactionsFactory.createESDTTransfer({ - tokenTransfer: TokenTransfer.fungibleFromBigInteger(tokenIdentifier, 10), - sender: frank.account.address, - receiver: grace.account.address, - chainID: network.ChainID, - nonce: frank.account.nonce, - }); - - const _tx2OnNetwork = await processTransaction(frank, tx2, "tx2"); - - // Freeze - const tx3 = factory.freeze({ - manager: frank.address, - user: grace.address, - tokenIdentifier: tokenIdentifier, - transactionNonce: frank.account.nonce, - }); - - const tx3OnNetwork = await processTransaction(frank, tx3, "tx3"); - const tx3Outcome = parser.parseFreeze(tx3OnNetwork); - assert.equal(tx3Outcome.userAddress, grace.address.bech32()); - assert.equal(tx3Outcome.tokenIdentifier, tokenIdentifier); - assert.equal(tx3Outcome.nonce, "0"); - assert.equal(tx3Outcome.balance, "10"); - - // Unfreeze - const tx4 = factory.unfreeze({ - manager: frank.address, - user: grace.address, - tokenIdentifier: tokenIdentifier, - transactionNonce: frank.account.nonce, - }); - - const tx4OnNetwork = await processTransaction(frank, tx4, "tx4"); - const tx4Outcome = parser.parseUnfreeze(tx4OnNetwork); - assert.equal(tx4Outcome.userAddress, grace.address.bech32()); - assert.equal(tx4Outcome.tokenIdentifier, tokenIdentifier); - assert.equal(tx4Outcome.nonce, "0"); - assert.equal(tx4Outcome.balance, "10"); - }); - - it("should issue fungible, freeze, wipe", async function () { - this.timeout(240000); - await frank.sync(provider); - - // Issue - const tx1 = factory.issueFungible({ - issuer: frank.address, - tokenName: "FRANK", - tokenTicker: "FRANK", - initialSupply: "100", - numDecimals: 0, - canFreeze: true, - canWipe: true, - canPause: true, - canChangeOwner: true, - canUpgrade: true, - canAddSpecialRoles: true, - transactionNonce: frank.account.nonce, - }); - - const tx1OnNetwork = await processTransaction(frank, tx1, "tx1"); - const tx1Outcome = parser.parseIssueFungible(tx1OnNetwork); - const tokenIdentifier = tx1Outcome.tokenIdentifier; - assert.isTrue(tokenIdentifier.includes("FRANK")); - - // Send some tokens to Grace - const tx2 = transferTransactionsFactory.createESDTTransfer({ - tokenTransfer: TokenTransfer.fungibleFromBigInteger(tokenIdentifier, 10), - sender: frank.account.address, - receiver: grace.account.address, - chainID: network.ChainID, - nonce: frank.account.nonce, - }); - - const _tx2OnNetwork = await processTransaction(frank, tx2, "tx2"); - - // Freeze - const tx3 = factory.freeze({ - manager: frank.address, - user: grace.address, - tokenIdentifier: tokenIdentifier, - transactionNonce: frank.account.nonce, - }); - - const tx3OnNetwork = await processTransaction(frank, tx3, "tx3"); - const tx3Outcome = parser.parseFreeze(tx3OnNetwork); - assert.equal(tx3Outcome.userAddress, grace.address.bech32()); - assert.equal(tx3Outcome.tokenIdentifier, tokenIdentifier); - assert.equal(tx3Outcome.nonce, "0"); - assert.equal(tx3Outcome.balance, "10"); - - // Wipe - const tx4 = factory.wipe({ - manager: frank.address, - user: grace.address, - tokenIdentifier: tokenIdentifier, - transactionNonce: frank.account.nonce, - }); - - const tx4OnNetwork = await processTransaction(frank, tx4, "tx4"); - const tx4Outcome = parser.parseWipe(tx4OnNetwork); - assert.equal(tx4Outcome.userAddress, grace.address.bech32()); - assert.equal(tx4Outcome.tokenIdentifier, tokenIdentifier); - assert.equal(tx4Outcome.nonce, "0"); - assert.equal(tx4Outcome.balance, "10"); - }); - - it("should issue and create NFT, then update attributes", async function () { - this.timeout(180000); - await frank.sync(provider); - await grace.sync(provider); - - // Issue NFT - const tx1 = factory.issueNonFungible({ - issuer: frank.address, - tokenName: "FRANK", - tokenTicker: "FRANK", - canFreeze: true, - canWipe: true, - canPause: true, - canTransferNFTCreateRole: true, - canChangeOwner: true, - canUpgrade: true, - canAddSpecialRoles: true, - transactionNonce: frank.account.nonce, - }); - - const tx1OnNetwork = await processTransaction(frank, tx1, "tx1"); - const tx1Outcome = parser.parseIssueNonFungible(tx1OnNetwork); - const tokenIdentifier = tx1Outcome.tokenIdentifier; - assert.isTrue(tokenIdentifier.includes("FRANK")); - - // Set roles (give Grace the ability to create NFTs) - const tx2 = factory.setSpecialRoleOnNonFungible({ - manager: frank.address, - user: grace.address, - tokenIdentifier: tokenIdentifier, - addRoleNFTCreate: true, - addRoleNFTBurn: false, - addRoleNFTUpdateAttributes: true, - addRoleNFTAddURI: true, - addRoleESDTTransferRole: false, - transactionNonce: frank.account.nonce, - }); - - const tx2OnNetwork = await processTransaction(frank, tx2, "tx2"); - const tx2Outcome = parser.parseSetSpecialRole(tx2OnNetwork); - assert.include(tx2Outcome.roles, "ESDTRoleNFTCreate"); - assert.include(tx2Outcome.roles, "ESDTRoleNFTUpdateAttributes"); - - // Create NFTs, then update their attributes - for (let i = 1; i <= 2; i++) { - // Create - const txCreate = factory.nftCreate({ - creator: grace.address, - tokenIdentifier: tokenIdentifier, - initialQuantity: "1", - name: `test-${i}`, - royalties: 1000, - hash: "abba", - attributes: Buffer.from("test"), - uris: ["a", "b"], - transactionNonce: grace.account.nonce, - }); - - const txCreateOnNetwork = await processTransaction(grace, txCreate, "txCreate"); - const txCreateOutcome = parser.parseNFTCreate(txCreateOnNetwork); - - assert.equal(txCreateOutcome.tokenIdentifier, tokenIdentifier); - assert.equal(txCreateOutcome.nonce, i.toString()); - assert.equal(txCreateOutcome.initialQuantity, "1"); - - // Update attributes - const txUpdate = factory.updateAttributes({ - manager: grace.address, - tokenIdentifier: txCreateOutcome.tokenIdentifier, - tokenNonce: txCreateOutcome.nonce, - attributes: Buffer.from("updated"), - transactionNonce: grace.account.nonce, - }); - - const txUpdateOnNetwork = await processTransaction(grace, txUpdate, "txUpdate"); - const txUpdateOutcome = parser.parseUpdateAttributes(txUpdateOnNetwork); - - assert.equal(txUpdateOutcome.tokenIdentifier, tokenIdentifier); - assert.equal(txUpdateOutcome.nonce, i.toString()); - assert.deepEqual(txUpdateOutcome.attributes, Buffer.from("updated")); - } - }); - - it("should issue and create SFT, add quantity, burn quantity", async function () { - this.timeout(200000); - await frank.sync(provider); - await grace.sync(provider); - - // Issue SFT - const tx1 = factory.issueSemiFungible({ - issuer: frank.address, - tokenName: "FRANK", - tokenTicker: "FRANK", - canFreeze: true, - canWipe: true, - canPause: true, - canTransferNFTCreateRole: true, - canChangeOwner: true, - canUpgrade: true, - canAddSpecialRoles: true, - transactionNonce: frank.account.nonce, - }); - - const tx1OnNetwork = await processTransaction(frank, tx1, "tx1"); - const tx1Outcome = parser.parseIssueSemiFungible(tx1OnNetwork); - const tokenIdentifier = tx1Outcome.tokenIdentifier; - assert.isTrue(tokenIdentifier.includes("FRANK")); - - // Set roles (give Grace the ability to create SFTs) - const tx2 = factory.setSpecialRoleOnSemiFungible({ - manager: frank.address, - user: grace.address, - tokenIdentifier: tokenIdentifier, - addRoleNFTCreate: true, - addRoleNFTBurn: false, - addRoleNFTAddQuantity: true, - addRoleESDTTransferRole: false, - transactionNonce: frank.account.nonce, - }); - - const tx2OnNetwork = await processTransaction(frank, tx2, "tx2"); - const tx2Outcome = parser.parseSetSpecialRole(tx2OnNetwork); - assert.include(tx2Outcome.roles, "ESDTRoleNFTCreate"); - assert.include(tx2Outcome.roles, "ESDTRoleNFTAddQuantity"); - - for (let i = 1; i <= 2; i++) { - // Create SFT - const txCreate = factory.nftCreate({ - creator: grace.address, - tokenIdentifier: tokenIdentifier, - initialQuantity: i * 10, - name: `test-${i}`, - royalties: 1000, - hash: "abba", - attributes: Buffer.from("test"), - uris: ["a", "b"], - transactionNonce: grace.account.nonce, - }); - - const txCreateOnNetwork = await processTransaction(grace, txCreate, "txCreate"); - const txCreateOutcome = parser.parseNFTCreate(txCreateOnNetwork); - - assert.equal(txCreateOutcome.tokenIdentifier, tokenIdentifier); - assert.equal(txCreateOutcome.nonce, i.toString()); - assert.equal(txCreateOutcome.initialQuantity, (i * 10).toString()); - - // Add quantity - const txAddQuantity = factory.addQuantity({ - manager: grace.address, - tokenIdentifier: txCreateOutcome.tokenIdentifier, - tokenNonce: txCreateOutcome.nonce, - quantityToAdd: "3", - transactionNonce: grace.account.nonce, - }); - - const txAddQuantityOnNetwork = await processTransaction(grace, txAddQuantity, "txAddQuantity"); - const txAddQuantityOutcome = parser.parseAddQuantity(txAddQuantityOnNetwork); - - assert.equal(txAddQuantityOutcome.tokenIdentifier, tokenIdentifier); - assert.equal(txAddQuantityOutcome.nonce, i.toString()); - assert.equal(txAddQuantityOutcome.addedQuantity, "3"); - - // Burn quantity - const txBurnQuantity = factory.burnQuantity({ - manager: grace.address, - tokenIdentifier: txCreateOutcome.tokenIdentifier, - tokenNonce: txCreateOutcome.nonce, - quantityToBurn: "2", - transactionNonce: grace.account.nonce, - }); - - const txBurnQuantityOnNetwork = await processTransaction(grace, txBurnQuantity, "txBurnQuantity"); - const txBurnQuantityOutcome = parser.parseBurnQuantity(txBurnQuantityOnNetwork); - - assert.equal(txBurnQuantityOutcome.tokenIdentifier, tokenIdentifier); - assert.equal(txBurnQuantityOutcome.nonce, i.toString()); - assert.equal(txBurnQuantityOutcome.burntQuantity, "2"); - } - }); - - it("should register and create Meta ESDT", async function () { - this.timeout(180000); - await frank.sync(provider); - await grace.sync(provider); - - // Register Meta ESDT - const tx1 = factory.registerMetaESDT({ - issuer: frank.address, - tokenName: "FRANK", - tokenTicker: "FRANK", - numDecimals: 10, - canFreeze: true, - canWipe: true, - canPause: true, - canTransferNFTCreateRole: true, - canChangeOwner: true, - canUpgrade: true, - canAddSpecialRoles: true, - transactionNonce: frank.account.nonce, - }); - - const tx1OnNetwork = await processTransaction(frank, tx1, "tx1"); - const tx1Outcome = parser.parseRegisterMetaESDT(tx1OnNetwork); - const tokenIdentifier = tx1Outcome.tokenIdentifier; - assert.isTrue(tokenIdentifier.includes("FRANK")); - - // Set roles (give Grace the ability to create Meta ESDTs) - const tx2 = factory.setSpecialRoleOnMetaESDT({ - manager: frank.address, - user: grace.address, - tokenIdentifier: tokenIdentifier, - addRoleNFTCreate: true, - addRoleNFTBurn: false, - addRoleNFTAddQuantity: true, - addRoleESDTTransferRole: false, - transactionNonce: frank.account.nonce, - }); - - const tx2OnNetwork = await processTransaction(frank, tx2, "tx2"); - const tx2Outcome = parser.parseSetSpecialRole(tx2OnNetwork); - assert.include(tx2Outcome.roles, "ESDTRoleNFTCreate"); - assert.include(tx2Outcome.roles, "ESDTRoleNFTAddQuantity"); - - // Create tokens - for (let i = 1; i <= 3; i++) { - const tx = factory.nftCreate({ - creator: grace.address, - tokenIdentifier: tokenIdentifier, - initialQuantity: i * 10, - name: `test-${i}`, - royalties: 1000, - hash: "abba", - attributes: Buffer.from("test"), - uris: ["a", "b"], - transactionNonce: grace.account.nonce, - }); - - const txOnNetwork = await processTransaction(grace, tx, "tx"); - const txOutcome = parser.parseNFTCreate(txOnNetwork); - - assert.equal(txOutcome.tokenIdentifier, tokenIdentifier); - assert.equal(txOutcome.nonce, i.toString()); - assert.equal(txOutcome.initialQuantity, (i * 10).toString()); - } - }); - - async function processTransaction( - wallet: TestWallet, - transaction: Transaction, - tag: string, - ): Promise { - wallet.account.incrementNonce(); - transaction.applySignature(await wallet.signer.sign(transaction.serializeForSigning())); - await provider.sendTransaction(transaction); - console.log(`Sent transaction [${tag}]: ${transaction.getHash().hex()}`); - - const transactionOnNetwork = await watcher.awaitCompleted(transaction.getHash().hex()); - return transactionOnNetwork; - } -}); diff --git a/src/tokenOperations/tokenOperationsFactory.ts b/src/tokenOperations/tokenOperationsFactory.ts deleted file mode 100644 index a4a4ffc52..000000000 --- a/src/tokenOperations/tokenOperationsFactory.ts +++ /dev/null @@ -1,693 +0,0 @@ -import BigNumber from "bignumber.js"; -import { ARGUMENTS_SEPARATOR, TRANSACTION_OPTIONS_DEFAULT, TRANSACTION_VERSION_DEFAULT } from "../constants"; -import { IAddress, IChainID, IGasLimit, IGasPrice, INonce, ITransactionValue } from "../interface"; -import { Logger } from "../logger"; -import { TransactionOptions, TransactionVersion } from "../networkParams"; -import { Transaction } from "../transaction"; -import { TransactionPayload } from "../transactionPayload"; -import { addressToHex, bigIntToHex, bufferToHex, utf8ToHex } from "./codec"; - -interface IConfig { - chainID: IChainID; - minGasPrice: IGasPrice; - minGasLimit: IGasLimit; - gasLimitPerByte: IGasLimit; - gasLimitIssue: IGasLimit; - gasLimitToggleBurnRoleGlobally: IGasLimit; - gasLimitESDTLocalMint: IGasLimit; - gasLimitESDTLocalBurn: IGasLimit; - gasLimitSetSpecialRole: IGasLimit; - gasLimitPausing: IGasLimit; - gasLimitFreezing: IGasLimit; - gasLimitWiping: IGasLimit; - gasLimitESDTNFTCreate: IGasLimit; - gasLimitESDTNFTUpdateAttributes: IGasLimit; - gasLimitESDTNFTAddQuantity: IGasLimit; - gasLimitESDTNFTBurn: IGasLimit; - gasLimitStorePerByte: IGasLimit; - issueCost: BigNumber.Value; - esdtContractAddress: IAddress; -} - -interface IBaseArgs { - transactionNonce?: INonce; - value?: ITransactionValue; - gasPrice?: IGasPrice; - gasLimit?: IGasLimit; -} - -interface IIssueFungibleArgs extends IBaseArgs { - issuer: IAddress; - tokenName: string; - tokenTicker: string; - initialSupply: BigNumber.Value; - numDecimals: number; - canFreeze: boolean; - canWipe: boolean; - canPause: boolean; - canChangeOwner: boolean; - canUpgrade: boolean; - canAddSpecialRoles: boolean; -} - -interface IIssueSemiFungibleArgs extends IBaseArgs { - issuer: IAddress; - tokenName: string; - tokenTicker: string; - canFreeze: boolean; - canWipe: boolean; - canPause: boolean; - canTransferNFTCreateRole: boolean; - canChangeOwner: boolean; - canUpgrade: boolean; - canAddSpecialRoles: boolean; -} - -interface IIssueNonFungibleArgs extends IIssueSemiFungibleArgs {} - -interface IRegisterMetaESDT extends IIssueSemiFungibleArgs { - numDecimals: number; -} - -interface IRegisterAndSetAllRoles extends IBaseArgs { - issuer: IAddress; - tokenName: string; - tokenTicker: string; - tokenType: RegisterAndSetAllRolesTokenType; - numDecimals: number; -} - -type RegisterAndSetAllRolesTokenType = "NFT" | "SFT" | "META" | "FNG"; - -interface IToggleBurnRoleGloballyArgs extends IBaseArgs { - manager: IAddress; - tokenIdentifier: string; -} - -interface IFungibleSetSpecialRoleArgs extends IBaseArgs { - manager: IAddress; - user: IAddress; - tokenIdentifier: string; - addRoleLocalMint: boolean; - addRoleLocalBurn: boolean; -} - -interface ISemiFungibleSetSpecialRoleArgs extends IBaseArgs { - manager: IAddress; - user: IAddress; - tokenIdentifier: string; - addRoleNFTCreate: boolean; - addRoleNFTBurn: boolean; - addRoleNFTAddQuantity: boolean; - addRoleESDTTransferRole: boolean; -} - -interface INonFungibleSetSpecialRoleArgs extends IBaseArgs { - manager: IAddress; - user: IAddress; - tokenIdentifier: string; - addRoleNFTCreate: boolean; - addRoleNFTBurn: boolean; - addRoleNFTUpdateAttributes: boolean; - addRoleNFTAddURI: boolean; - addRoleESDTTransferRole: boolean; -} - -interface INFTCreateArgs extends IBaseArgs { - creator: IAddress; - tokenIdentifier: string; - initialQuantity: BigNumber.Value; - name: string; - royalties: number; - hash: string; - attributes: Buffer; - uris: string[]; -} - -interface IPausingArgs extends IBaseArgs { - manager: IAddress; - tokenIdentifier: string; -} - -interface IFreezingArgs extends IBaseArgs { - manager: IAddress; - user: IAddress; - tokenIdentifier: string; -} - -interface IWipingArgs extends IBaseArgs { - manager: IAddress; - user: IAddress; - tokenIdentifier: string; -} - -interface ILocalMintArgs extends IBaseArgs { - manager: IAddress; - user: IAddress; - tokenIdentifier: string; - supplyToMint: BigNumber.Value; -} - -interface ILocalBurnArgs extends IBaseArgs { - manager: IAddress; - user: IAddress; - tokenIdentifier: string; - supplyToBurn: BigNumber.Value; -} - -interface IUpdateAttributesArgs extends IBaseArgs { - manager: IAddress; - tokenIdentifier: string; - tokenNonce: BigNumber.Value; - attributes: Buffer; -} - -interface IAddQuantityArgs extends IBaseArgs { - manager: IAddress; - tokenIdentifier: string; - tokenNonce: BigNumber.Value; - quantityToAdd: BigNumber.Value; -} - -interface IBurnQuantityArgs extends IBaseArgs { - manager: IAddress; - tokenIdentifier: string; - tokenNonce: BigNumber.Value; - quantityToBurn: BigNumber.Value; -} - -/** - * @deprecated Use {@link TokenManagementTransactionsFactory} instead. - */ -export class TokenOperationsFactory { - private readonly config: IConfig; - private readonly trueAsHex; - private readonly falseAsHex; - - constructor(config: IConfig) { - this.config = config; - this.trueAsHex = utf8ToHex("true"); - this.falseAsHex = utf8ToHex("false"); - } - - issueFungible(args: IIssueFungibleArgs): Transaction { - this.notifyAboutUnsettingBurnRoleGlobally(); - - const parts = [ - "issue", - utf8ToHex(args.tokenName), - utf8ToHex(args.tokenTicker), - bigIntToHex(args.initialSupply), - bigIntToHex(args.numDecimals), - utf8ToHex("canFreeze"), - args.canFreeze ? this.trueAsHex : this.falseAsHex, - utf8ToHex("canWipe"), - args.canWipe ? this.trueAsHex : this.falseAsHex, - utf8ToHex("canPause"), - args.canPause ? this.trueAsHex : this.falseAsHex, - utf8ToHex("canChangeOwner"), - args.canChangeOwner ? this.trueAsHex : this.falseAsHex, - utf8ToHex("canUpgrade"), - args.canUpgrade ? this.trueAsHex : this.falseAsHex, - utf8ToHex("canAddSpecialRoles"), - args.canAddSpecialRoles ? this.trueAsHex : this.falseAsHex, - ]; - - return this.createTransaction({ - sender: args.issuer, - receiver: this.config.esdtContractAddress, - nonce: args.transactionNonce, - value: this.config.issueCost, - gasPrice: args.gasPrice, - gasLimitHint: args.gasLimit, - executionGasLimit: this.config.gasLimitIssue, - dataParts: parts, - }); - } - - 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).`); - } - - issueSemiFungible(args: IIssueSemiFungibleArgs): Transaction { - this.notifyAboutUnsettingBurnRoleGlobally(); - - const parts = [ - "issueSemiFungible", - utf8ToHex(args.tokenName), - utf8ToHex(args.tokenTicker), - utf8ToHex("canFreeze"), - args.canFreeze ? this.trueAsHex : this.falseAsHex, - utf8ToHex("canWipe"), - args.canWipe ? this.trueAsHex : this.falseAsHex, - utf8ToHex("canPause"), - args.canPause ? this.trueAsHex : this.falseAsHex, - utf8ToHex("canTransferNFTCreateRole"), - args.canTransferNFTCreateRole ? this.trueAsHex : this.falseAsHex, - utf8ToHex("canChangeOwner"), - args.canChangeOwner ? this.trueAsHex : this.falseAsHex, - utf8ToHex("canUpgrade"), - args.canUpgrade ? this.trueAsHex : this.falseAsHex, - utf8ToHex("canAddSpecialRoles"), - args.canAddSpecialRoles ? this.trueAsHex : this.falseAsHex, - ]; - - return this.createTransaction({ - sender: args.issuer, - receiver: this.config.esdtContractAddress, - nonce: args.transactionNonce, - value: this.config.issueCost, - gasPrice: args.gasPrice, - gasLimitHint: args.gasLimit, - executionGasLimit: this.config.gasLimitIssue, - dataParts: parts, - }); - } - - issueNonFungible(args: IIssueNonFungibleArgs): Transaction { - this.notifyAboutUnsettingBurnRoleGlobally(); - - const parts = [ - "issueNonFungible", - utf8ToHex(args.tokenName), - utf8ToHex(args.tokenTicker), - utf8ToHex("canFreeze"), - args.canFreeze ? this.trueAsHex : this.falseAsHex, - utf8ToHex("canWipe"), - args.canWipe ? this.trueAsHex : this.falseAsHex, - utf8ToHex("canPause"), - args.canPause ? this.trueAsHex : this.falseAsHex, - utf8ToHex("canTransferNFTCreateRole"), - args.canTransferNFTCreateRole ? this.trueAsHex : this.falseAsHex, - utf8ToHex("canChangeOwner"), - args.canChangeOwner ? this.trueAsHex : this.falseAsHex, - utf8ToHex("canUpgrade"), - args.canUpgrade ? this.trueAsHex : this.falseAsHex, - utf8ToHex("canAddSpecialRoles"), - args.canAddSpecialRoles ? this.trueAsHex : this.falseAsHex, - ]; - - return this.createTransaction({ - sender: args.issuer, - receiver: this.config.esdtContractAddress, - nonce: args.transactionNonce, - value: this.config.issueCost, - gasPrice: args.gasPrice, - gasLimitHint: args.gasLimit, - executionGasLimit: this.config.gasLimitIssue, - dataParts: parts, - }); - } - - registerMetaESDT(args: IRegisterMetaESDT): Transaction { - this.notifyAboutUnsettingBurnRoleGlobally(); - - const parts = [ - "registerMetaESDT", - utf8ToHex(args.tokenName), - utf8ToHex(args.tokenTicker), - bigIntToHex(args.numDecimals), - utf8ToHex("canFreeze"), - args.canFreeze ? this.trueAsHex : this.falseAsHex, - utf8ToHex("canWipe"), - args.canWipe ? this.trueAsHex : this.falseAsHex, - utf8ToHex("canPause"), - args.canPause ? this.trueAsHex : this.falseAsHex, - utf8ToHex("canTransferNFTCreateRole"), - args.canTransferNFTCreateRole ? this.trueAsHex : this.falseAsHex, - utf8ToHex("canChangeOwner"), - args.canChangeOwner ? this.trueAsHex : this.falseAsHex, - utf8ToHex("canUpgrade"), - args.canUpgrade ? this.trueAsHex : this.falseAsHex, - utf8ToHex("canAddSpecialRoles"), - args.canAddSpecialRoles ? this.trueAsHex : this.falseAsHex, - ]; - - return this.createTransaction({ - sender: args.issuer, - receiver: this.config.esdtContractAddress, - nonce: args.transactionNonce, - value: this.config.issueCost, - gasPrice: args.gasPrice, - gasLimitHint: args.gasLimit, - executionGasLimit: this.config.gasLimitIssue, - dataParts: parts, - }); - } - - registerAndSetAllRoles(args: IRegisterAndSetAllRoles): Transaction { - this.notifyAboutUnsettingBurnRoleGlobally(); - - const parts = [ - "registerAndSetAllRoles", - utf8ToHex(args.tokenName), - utf8ToHex(args.tokenTicker), - utf8ToHex(args.tokenType), - bigIntToHex(args.numDecimals), - ]; - - return this.createTransaction({ - sender: args.issuer, - receiver: this.config.esdtContractAddress, - nonce: args.transactionNonce, - value: this.config.issueCost, - gasPrice: args.gasPrice, - gasLimitHint: args.gasLimit, - executionGasLimit: this.config.gasLimitIssue, - dataParts: parts, - }); - } - - setBurnRoleGlobally(args: IToggleBurnRoleGloballyArgs): Transaction { - const parts = ["setBurnRoleGlobally", utf8ToHex(args.tokenIdentifier)]; - - return this.createTransaction({ - sender: args.manager, - receiver: this.config.esdtContractAddress, - nonce: args.transactionNonce, - gasPrice: args.gasPrice, - gasLimitHint: args.gasLimit, - executionGasLimit: this.config.gasLimitToggleBurnRoleGlobally, - dataParts: parts, - }); - } - - unsetBurnRoleGlobally(args: IToggleBurnRoleGloballyArgs): Transaction { - const parts = ["unsetBurnRoleGlobally", utf8ToHex(args.tokenIdentifier)]; - - return this.createTransaction({ - sender: args.manager, - receiver: this.config.esdtContractAddress, - nonce: args.transactionNonce, - gasPrice: args.gasPrice, - gasLimitHint: args.gasLimit, - executionGasLimit: this.config.gasLimitToggleBurnRoleGlobally, - dataParts: parts, - }); - } - - setSpecialRoleOnFungible(args: IFungibleSetSpecialRoleArgs): Transaction { - const parts = [ - "setSpecialRole", - utf8ToHex(args.tokenIdentifier), - addressToHex(args.user), - ...(args.addRoleLocalMint ? [utf8ToHex("ESDTRoleLocalMint")] : []), - ...(args.addRoleLocalBurn ? [utf8ToHex("ESDTRoleLocalBurn")] : []), - ]; - - return this.createTransaction({ - sender: args.manager, - receiver: this.config.esdtContractAddress, - nonce: args.transactionNonce, - gasPrice: args.gasPrice, - gasLimitHint: args.gasLimit, - executionGasLimit: this.config.gasLimitSetSpecialRole, - dataParts: parts, - }); - } - - setSpecialRoleOnSemiFungible(args: ISemiFungibleSetSpecialRoleArgs): Transaction { - const parts = [ - "setSpecialRole", - utf8ToHex(args.tokenIdentifier), - addressToHex(args.user), - ...(args.addRoleNFTCreate ? [utf8ToHex("ESDTRoleNFTCreate")] : []), - ...(args.addRoleNFTBurn ? [utf8ToHex("ESDTRoleNFTBurn")] : []), - ...(args.addRoleNFTAddQuantity ? [utf8ToHex("ESDTRoleNFTAddQuantity")] : []), - ...(args.addRoleESDTTransferRole ? [utf8ToHex("ESDTTransferRole")] : []), - ]; - - return this.createTransaction({ - sender: args.manager, - receiver: this.config.esdtContractAddress, - nonce: args.transactionNonce, - gasPrice: args.gasPrice, - gasLimitHint: args.gasLimit, - executionGasLimit: this.config.gasLimitSetSpecialRole, - dataParts: parts, - }); - } - - setSpecialRoleOnMetaESDT(args: ISemiFungibleSetSpecialRoleArgs): Transaction { - return this.setSpecialRoleOnSemiFungible(args); - } - - setSpecialRoleOnNonFungible(args: INonFungibleSetSpecialRoleArgs): Transaction { - const parts = [ - "setSpecialRole", - utf8ToHex(args.tokenIdentifier), - addressToHex(args.user), - ...(args.addRoleNFTCreate ? [utf8ToHex("ESDTRoleNFTCreate")] : []), - ...(args.addRoleNFTBurn ? [utf8ToHex("ESDTRoleNFTBurn")] : []), - ...(args.addRoleNFTUpdateAttributes ? [utf8ToHex("ESDTRoleNFTUpdateAttributes")] : []), - ...(args.addRoleNFTAddURI ? [utf8ToHex("ESDTRoleNFTAddURI")] : []), - ...(args.addRoleESDTTransferRole ? [utf8ToHex("ESDTTransferRole")] : []), - ]; - - return this.createTransaction({ - sender: args.manager, - receiver: this.config.esdtContractAddress, - nonce: args.transactionNonce, - gasPrice: args.gasPrice, - gasLimitHint: args.gasLimit, - executionGasLimit: this.config.gasLimitSetSpecialRole, - dataParts: parts, - }); - } - - nftCreate(args: INFTCreateArgs): Transaction { - const parts = [ - "ESDTNFTCreate", - utf8ToHex(args.tokenIdentifier), - bigIntToHex(args.initialQuantity), - utf8ToHex(args.name), - bigIntToHex(args.royalties), - utf8ToHex(args.hash), - bufferToHex(args.attributes), - ...args.uris.map(utf8ToHex), - ]; - - // Note that the following is an approximation (a reasonable one): - const nftData = args.name + args.hash + args.attributes + args.uris.join(""); - const storageGasLimit = nftData.length * this.config.gasLimitStorePerByte.valueOf(); - - return this.createTransaction({ - sender: args.creator, - receiver: args.creator, - nonce: args.transactionNonce, - gasPrice: args.gasPrice, - gasLimitHint: args.gasLimit, - executionGasLimit: this.config.gasLimitESDTNFTCreate.valueOf() + storageGasLimit.valueOf(), - dataParts: parts, - }); - } - - pause(args: IPausingArgs): Transaction { - const parts = ["pause", utf8ToHex(args.tokenIdentifier)]; - - return this.createTransaction({ - sender: args.manager, - receiver: this.config.esdtContractAddress, - nonce: args.transactionNonce, - gasPrice: args.gasPrice, - gasLimitHint: args.gasLimit, - executionGasLimit: this.config.gasLimitPausing, - dataParts: parts, - }); - } - - unpause(args: IPausingArgs): Transaction { - const parts = ["unPause", utf8ToHex(args.tokenIdentifier)]; - - return this.createTransaction({ - sender: args.manager, - receiver: this.config.esdtContractAddress, - nonce: args.transactionNonce, - gasPrice: args.gasPrice, - gasLimitHint: args.gasLimit, - executionGasLimit: this.config.gasLimitPausing, - dataParts: parts, - }); - } - - freeze(args: IFreezingArgs): Transaction { - const parts = ["freeze", utf8ToHex(args.tokenIdentifier), addressToHex(args.user)]; - - return this.createTransaction({ - sender: args.manager, - receiver: this.config.esdtContractAddress, - nonce: args.transactionNonce, - gasPrice: args.gasPrice, - gasLimitHint: args.gasLimit, - executionGasLimit: this.config.gasLimitFreezing, - dataParts: parts, - }); - } - - unfreeze(args: IFreezingArgs): Transaction { - const parts = ["unFreeze", utf8ToHex(args.tokenIdentifier), addressToHex(args.user)]; - - return this.createTransaction({ - sender: args.manager, - receiver: this.config.esdtContractAddress, - nonce: args.transactionNonce, - gasPrice: args.gasPrice, - gasLimitHint: args.gasLimit, - executionGasLimit: this.config.gasLimitFreezing, - dataParts: parts, - }); - } - - wipe(args: IWipingArgs): Transaction { - const parts = ["wipe", utf8ToHex(args.tokenIdentifier), addressToHex(args.user)]; - - return this.createTransaction({ - sender: args.manager, - receiver: this.config.esdtContractAddress, - nonce: args.transactionNonce, - gasPrice: args.gasPrice, - gasLimitHint: args.gasLimit, - executionGasLimit: this.config.gasLimitWiping, - dataParts: parts, - }); - } - - localMint(args: ILocalMintArgs): Transaction { - const parts = ["ESDTLocalMint", utf8ToHex(args.tokenIdentifier), bigIntToHex(args.supplyToMint)]; - - return this.createTransaction({ - sender: args.manager, - receiver: args.manager, - nonce: args.transactionNonce, - gasPrice: args.gasPrice, - gasLimitHint: args.gasLimit, - executionGasLimit: this.config.gasLimitESDTLocalMint, - dataParts: parts, - }); - } - - localBurn(args: ILocalBurnArgs): Transaction { - const parts = ["ESDTLocalBurn", utf8ToHex(args.tokenIdentifier), bigIntToHex(args.supplyToBurn)]; - - return this.createTransaction({ - sender: args.manager, - receiver: args.manager, - nonce: args.transactionNonce, - gasPrice: args.gasPrice, - gasLimitHint: args.gasLimit, - executionGasLimit: this.config.gasLimitESDTLocalBurn, - dataParts: parts, - }); - } - - updateAttributes(args: IUpdateAttributesArgs): Transaction { - const parts = [ - "ESDTNFTUpdateAttributes", - utf8ToHex(args.tokenIdentifier), - bigIntToHex(args.tokenNonce), - bufferToHex(args.attributes), - ]; - - return this.createTransaction({ - sender: args.manager, - receiver: args.manager, - nonce: args.transactionNonce, - gasPrice: args.gasPrice, - gasLimitHint: args.gasLimit, - executionGasLimit: this.config.gasLimitESDTNFTUpdateAttributes, - dataParts: parts, - }); - } - - addQuantity(args: IAddQuantityArgs): Transaction { - const parts = [ - "ESDTNFTAddQuantity", - utf8ToHex(args.tokenIdentifier), - bigIntToHex(args.tokenNonce), - bigIntToHex(args.quantityToAdd), - ]; - - return this.createTransaction({ - sender: args.manager, - receiver: args.manager, - nonce: args.transactionNonce, - gasPrice: args.gasPrice, - gasLimitHint: args.gasLimit, - executionGasLimit: this.config.gasLimitESDTNFTAddQuantity, - dataParts: parts, - }); - } - - burnQuantity(args: IBurnQuantityArgs): Transaction { - const parts = [ - "ESDTNFTBurn", - utf8ToHex(args.tokenIdentifier), - bigIntToHex(args.tokenNonce), - bigIntToHex(args.quantityToBurn), - ]; - - return this.createTransaction({ - sender: args.manager, - receiver: args.manager, - nonce: args.transactionNonce, - gasPrice: args.gasPrice, - gasLimitHint: args.gasLimit, - executionGasLimit: this.config.gasLimitESDTNFTBurn, - dataParts: parts, - }); - } - - private createTransaction({ - sender, - receiver, - nonce, - value, - gasPrice, - gasLimitHint, - executionGasLimit, - dataParts, - }: { - sender: IAddress; - receiver: IAddress; - nonce?: INonce; - value?: ITransactionValue; - gasPrice?: IGasPrice; - gasLimitHint?: IGasLimit; - executionGasLimit: IGasLimit; - dataParts: string[]; - }): Transaction { - const payload = this.buildTransactionPayload(dataParts); - const gasLimit = gasLimitHint || this.computeGasLimit(payload, executionGasLimit); - const version = new TransactionVersion(TRANSACTION_VERSION_DEFAULT); - const options = new TransactionOptions(TRANSACTION_OPTIONS_DEFAULT); - - return new Transaction({ - chainID: this.config.chainID, - sender: sender, - receiver: receiver, - gasLimit: gasLimit, - gasPrice: gasPrice, - nonce: nonce || 0, - value: value || 0, - data: payload, - version: version, - options: options, - }); - } - - private buildTransactionPayload(parts: string[]): TransactionPayload { - const data = parts.join(ARGUMENTS_SEPARATOR); - return new TransactionPayload(data); - } - - private computeGasLimit(payload: TransactionPayload, executionGas: IGasLimit): IGasLimit { - const dataMovementGas = - this.config.minGasLimit.valueOf() + this.config.gasLimitPerByte.valueOf() * payload.length(); - return dataMovementGas + executionGas.valueOf(); - } -} diff --git a/src/tokenOperations/tokenOperationsFactoryConfig.ts b/src/tokenOperations/tokenOperationsFactoryConfig.ts deleted file mode 100644 index 5a3466859..000000000 --- a/src/tokenOperations/tokenOperationsFactoryConfig.ts +++ /dev/null @@ -1,34 +0,0 @@ -import BigNumber from "bignumber.js"; -import { Address } from "../address"; -import { IAddress, IChainID, IGasLimit, IGasPrice } from "../interface"; - -/** - * @deprecated Use {@link TransactionsFactoryConfig} instead. - */ -export class TokenOperationsFactoryConfig { - chainID: IChainID; - minGasPrice: IGasPrice = 1000000000; - minGasLimit = 50000; - gasLimitPerByte = 1500; - gasLimitIssue: IGasLimit = 60000000; - gasLimitToggleBurnRoleGlobally: IGasLimit = 60000000; - gasLimitESDTLocalMint: IGasLimit = 300000; - gasLimitESDTLocalBurn: IGasLimit = 300000; - gasLimitSetSpecialRole: IGasLimit = 60000000; - gasLimitPausing: IGasLimit = 60000000; - gasLimitFreezing: IGasLimit = 60000000; - gasLimitWiping: IGasLimit = 60000000; - gasLimitESDTNFTCreate: IGasLimit = 3000000; - gasLimitESDTNFTUpdateAttributes: IGasLimit = 1000000; - gasLimitESDTNFTAddQuantity: IGasLimit = 1000000; - gasLimitESDTNFTBurn: IGasLimit = 1000000; - gasLimitStorePerByte: IGasLimit = 50000; - issueCost: BigNumber.Value = "50000000000000000"; - esdtContractAddress: IAddress = Address.fromBech32( - "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u", - ); - - constructor(chainID: IChainID) { - this.chainID = chainID; - } -} diff --git a/src/tokenOperations/tokenOperationsOutcomeParser.spec.ts b/src/tokenOperations/tokenOperationsOutcomeParser.spec.ts deleted file mode 100644 index ec3c5077f..000000000 --- a/src/tokenOperations/tokenOperationsOutcomeParser.spec.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { assert } from "chai"; -import { loadTestWallets, TestWallet } from "../testutils"; -import { bigIntToBuffer } from "./codec"; -import { TokenOperationsOutcomeParser } from "./tokenOperationsOutcomeParser"; - -describe("test parsers", () => { - const parser = new TokenOperationsOutcomeParser(); - let frank: TestWallet, grace: TestWallet; - - before(async function () { - ({ frank, grace } = await loadTestWallets()); - }); - - it("should parse outcome of issueFungible", () => { - const outcome = parser.parseIssueFungible({ - hash: "hash", - contractResults: { items: [] }, - logs: { - events: [ - { - address: frank.address, - identifier: "issue", - topics: [createTopic(Buffer.from("FOOBAR"))], - data: "", - }, - ], - }, - }); - - assert.equal(outcome.tokenIdentifier, "FOOBAR"); - }); - - it("should parse outcome of setSpecialRole", () => { - const outcome = parser.parseSetSpecialRole({ - hash: "hash", - contractResults: { items: [] }, - logs: { - events: [ - { - address: grace.address, - identifier: "ESDTSetRole", - topics: [ - createTopic(Buffer.from("FOOBAR")), - createTopic(Buffer.from("")), - createTopic(Buffer.from("")), - createTopic(Buffer.from("ESDTRoleLocalMint")), - createTopic(Buffer.from("ESDTRoleLocalBurn")), - ], - data: "", - }, - ], - }, - }); - - assert.equal(outcome.tokenIdentifier, "FOOBAR"); - assert.deepEqual(outcome.roles, ["ESDTRoleLocalMint", "ESDTRoleLocalBurn"]); - assert.equal(outcome.userAddress, grace.address.toString()); - }); - - it("should parse outcome of localMint", () => { - const outcome = parser.parseLocalMint({ - hash: "hash", - contractResults: { items: [] }, - logs: { - events: [ - { - address: grace.address, - identifier: "ESDTLocalMint", - topics: [ - createTopic(Buffer.from("FOOBAR")), - createTopic(Buffer.from("")), - createTopic(bigIntToBuffer("200")), - ], - data: "", - }, - ], - }, - }); - - assert.equal(outcome.tokenIdentifier, "FOOBAR"); - assert.equal(outcome.nonce, "0"); - assert.equal(outcome.mintedSupply, "200"); - assert.equal(outcome.userAddress, grace.address.toString()); - }); - - it("should parse outcome of nftCreate", () => { - const outcome = parser.parseNFTCreate({ - hash: "hash", - contractResults: { items: [] }, - logs: { - events: [ - { - address: grace.address, - identifier: "ESDTNFTCreate", - topics: [ - createTopic(Buffer.from("FOOBAR")), - createTopic(bigIntToBuffer("42")), - createTopic(bigIntToBuffer("1")), - ], - data: "", - }, - ], - }, - }); - - assert.equal(outcome.tokenIdentifier, "FOOBAR"); - assert.equal(outcome.nonce, "42"); - assert.equal(outcome.initialQuantity, "1"); - }); - - function createTopic(value: Buffer): any { - return { - valueOf: () => value, - }; - } -}); diff --git a/src/tokenOperations/tokenOperationsOutcomeParser.ts b/src/tokenOperations/tokenOperationsOutcomeParser.ts deleted file mode 100644 index 8fc489f3c..000000000 --- a/src/tokenOperations/tokenOperationsOutcomeParser.ts +++ /dev/null @@ -1,335 +0,0 @@ -import { Address } from "../address"; -import { ErrCannotParseTransactionOutcome } from "../errors"; -import { IAddress } from "../interface"; -import { bufferToBigInt } from "./codec"; - -interface ITransactionOnNetwork { - hash: string; - contractResults: IContractResults; - logs: ITransactionLogs; -} - -interface IContractResults { - items: IContractResultItem[]; -} - -interface IContractResultItem { - logs: ITransactionLogs; -} - -interface ITransactionLogs { - events: ITransactionEvent[]; -} - -interface ITransactionEvent { - readonly address: IAddress; - readonly identifier: string; - readonly topics: ITransactionEventTopic[]; - readonly data: string; -} - -interface ITransactionEventTopic { - valueOf(): any; -} - -export interface IESDTIssueOutcome { - tokenIdentifier: string; -} - -export interface IRegisterAndSetAllRolesOutcome { - tokenIdentifier: string; - roles: string[]; -} - -export interface IToggleBurnRoleGloballyOutcome {} - -export interface ISetSpecialRoleOutcome { - userAddress: string; - tokenIdentifier: string; - roles: string[]; -} - -export interface INFTCreateOutcome { - tokenIdentifier: string; - nonce: string; - initialQuantity: string; -} - -export interface IMintOutcome { - userAddress: string; - tokenIdentifier: string; - nonce: string; - mintedSupply: string; -} - -export interface IBurnOutcome { - userAddress: string; - tokenIdentifier: string; - nonce: string; - burntSupply: string; -} - -export interface IPausingOutcome {} - -export interface IFreezingOutcome { - userAddress: string; - tokenIdentifier: string; - nonce: string; - balance: string; -} - -export interface IWipingOutcome { - userAddress: string; - tokenIdentifier: string; - nonce: string; - balance: string; -} - -export interface IUpdateAttributesOutcome { - tokenIdentifier: string; - nonce: string; - attributes: Buffer; -} - -export interface IAddQuantityOutcome { - tokenIdentifier: string; - nonce: string; - addedQuantity: string; -} - -export interface IBurnQuantityOutcome { - tokenIdentifier: string; - nonce: string; - burntQuantity: string; -} - -/** - * @deprecated Use {@link TokenManagementTransactionsOutcomeParser} - */ -export class TokenOperationsOutcomeParser { - parseIssueFungible(transaction: ITransactionOnNetwork): IESDTIssueOutcome { - this.ensureNoError(transaction); - - const event = this.findSingleEventByIdentifier(transaction, "issue"); - const tokenIdentifier = this.extractTokenIdentifier(event); - return { tokenIdentifier: tokenIdentifier }; - } - - parseIssueNonFungible(transaction: ITransactionOnNetwork): IESDTIssueOutcome { - this.ensureNoError(transaction); - - const event = this.findSingleEventByIdentifier(transaction, "issueNonFungible"); - const tokenIdentifier = this.extractTokenIdentifier(event); - return { tokenIdentifier: tokenIdentifier }; - } - - parseIssueSemiFungible(transaction: ITransactionOnNetwork): IESDTIssueOutcome { - this.ensureNoError(transaction); - - const event = this.findSingleEventByIdentifier(transaction, "issueSemiFungible"); - const tokenIdentifier = this.extractTokenIdentifier(event); - return { tokenIdentifier: tokenIdentifier }; - } - - parseRegisterMetaESDT(transaction: ITransactionOnNetwork): IESDTIssueOutcome { - this.ensureNoError(transaction); - - const event = this.findSingleEventByIdentifier(transaction, "registerMetaESDT"); - const tokenIdentifier = this.extractTokenIdentifier(event); - return { tokenIdentifier: tokenIdentifier }; - } - - parseRegisterAndSetAllRoles(transaction: ITransactionOnNetwork): IRegisterAndSetAllRolesOutcome { - this.ensureNoError(transaction); - - const eventRegister = this.findSingleEventByIdentifier(transaction, "registerAndSetAllRoles"); - const tokenIdentifier = this.extractTokenIdentifier(eventRegister); - - const eventSetRole = this.findSingleEventByIdentifier(transaction, "ESDTSetRole"); - const roles = eventSetRole.topics.slice(3).map((topic) => topic.valueOf().toString()); - - return { tokenIdentifier, roles }; - } - - parseSetBurnRoleGlobally(transaction: ITransactionOnNetwork): IToggleBurnRoleGloballyOutcome { - this.ensureNoError(transaction); - return {}; - } - - parseUnsetBurnRoleGlobally(transaction: ITransactionOnNetwork): IToggleBurnRoleGloballyOutcome { - this.ensureNoError(transaction); - return {}; - } - - parseSetSpecialRole(transaction: ITransactionOnNetwork): ISetSpecialRoleOutcome { - this.ensureNoError(transaction); - - const event = this.findSingleEventByIdentifier(transaction, "ESDTSetRole"); - const userAddress = event.address.toString(); - const tokenIdentifier = this.extractTokenIdentifier(event); - const roles = event.topics.slice(3).map((topic) => topic.valueOf().toString()); - return { userAddress, tokenIdentifier, roles }; - } - - parseNFTCreate(transaction: ITransactionOnNetwork): INFTCreateOutcome { - this.ensureNoError(transaction); - - const event = this.findSingleEventByIdentifier(transaction, "ESDTNFTCreate"); - const tokenIdentifier = this.extractTokenIdentifier(event); - const nonce = this.extractNonce(event); - const initialQuantity = this.extractAmount(event); - return { tokenIdentifier, nonce, initialQuantity }; - } - - parseLocalMint(transaction: ITransactionOnNetwork): IMintOutcome { - this.ensureNoError(transaction); - - const event = this.findSingleEventByIdentifier(transaction, "ESDTLocalMint"); - const userAddress = event.address.toString(); - const tokenIdentifier = this.extractTokenIdentifier(event); - const nonce = this.extractNonce(event); - const mintedSupply = this.extractAmount(event); - return { userAddress, tokenIdentifier, nonce, mintedSupply }; - } - - parseLocalBurn(transaction: ITransactionOnNetwork): IBurnOutcome { - this.ensureNoError(transaction); - - const event = this.findSingleEventByIdentifier(transaction, "ESDTLocalBurn"); - const userAddress = event.address.toString(); - const tokenIdentifier = this.extractTokenIdentifier(event); - const nonce = this.extractNonce(event); - const burntSupply = this.extractAmount(event); - return { userAddress, tokenIdentifier, nonce, burntSupply }; - } - - parsePause(transaction: ITransactionOnNetwork): IPausingOutcome { - this.ensureNoError(transaction); - const _ = this.findSingleEventByIdentifier(transaction, "ESDTPause"); - return {}; - } - - parseUnpause(transaction: ITransactionOnNetwork): IPausingOutcome { - this.ensureNoError(transaction); - const _ = this.findSingleEventByIdentifier(transaction, "ESDTUnPause"); - return {}; - } - - parseFreeze(transaction: ITransactionOnNetwork): IFreezingOutcome { - this.ensureNoError(transaction); - - const event = this.findSingleEventByIdentifier(transaction, "ESDTFreeze"); - const tokenIdentifier = this.extractTokenIdentifier(event); - const nonce = this.extractNonce(event); - const balance = this.extractAmount(event); - const userAddress = this.extractAddress(event); - return { userAddress, tokenIdentifier, nonce, balance }; - } - - parseUnfreeze(transaction: ITransactionOnNetwork): IFreezingOutcome { - this.ensureNoError(transaction); - - const event = this.findSingleEventByIdentifier(transaction, "ESDTUnFreeze"); - const tokenIdentifier = this.extractTokenIdentifier(event); - const nonce = this.extractNonce(event); - const balance = this.extractAmount(event); - const userAddress = this.extractAddress(event); - return { userAddress, tokenIdentifier, nonce, balance }; - } - - parseWipe(transaction: ITransactionOnNetwork): IWipingOutcome { - this.ensureNoError(transaction); - - const event = this.findSingleEventByIdentifier(transaction, "ESDTWipe"); - const tokenIdentifier = this.extractTokenIdentifier(event); - const nonce = this.extractNonce(event); - const balance = this.extractAmount(event); - const userAddress = this.extractAddress(event); - return { userAddress, tokenIdentifier, nonce, balance }; - } - - parseUpdateAttributes(transaction: ITransactionOnNetwork): IUpdateAttributesOutcome { - this.ensureNoError(transaction); - - const event = this.findSingleEventByIdentifier(transaction, "ESDTNFTUpdateAttributes"); - const tokenIdentifier = this.extractTokenIdentifier(event); - const nonce = this.extractNonce(event); - const attributes = event.topics[3]?.valueOf(); - return { tokenIdentifier, nonce, attributes }; - } - - parseAddQuantity(transaction: ITransactionOnNetwork): IAddQuantityOutcome { - this.ensureNoError(transaction); - - const event = this.findSingleEventByIdentifier(transaction, "ESDTNFTAddQuantity"); - const tokenIdentifier = this.extractTokenIdentifier(event); - const nonce = this.extractNonce(event); - const addedQuantity = this.extractAmount(event); - return { tokenIdentifier, nonce, addedQuantity }; - } - - parseBurnQuantity(transaction: ITransactionOnNetwork): IBurnQuantityOutcome { - this.ensureNoError(transaction); - - const event = this.findSingleEventByIdentifier(transaction, "ESDTNFTBurn"); - const tokenIdentifier = this.extractTokenIdentifier(event); - const nonce = this.extractNonce(event); - const burntQuantity = this.extractAmount(event); - return { tokenIdentifier, nonce, burntQuantity }; - } - - private ensureNoError(transaction: ITransactionOnNetwork) { - for (const event of transaction.logs.events) { - if (event.identifier == "signalError") { - const data = Buffer.from(event.data.substring(1), "hex").toString(); - const message = event.topics[1]?.valueOf().toString(); - - throw new ErrCannotParseTransactionOutcome( - transaction.hash, - `encountered signalError: ${message} (${data})`, - ); - } - } - } - - private findSingleEventByIdentifier(transaction: ITransactionOnNetwork, identifier: string): ITransactionEvent { - const events = this.gatherAllEvents(transaction).filter((event) => event.identifier == identifier); - - if (events.length == 0) { - throw new ErrCannotParseTransactionOutcome(transaction.hash, `cannot find event of type ${identifier}`); - } - if (events.length > 1) { - throw new ErrCannotParseTransactionOutcome(transaction.hash, `more than one event of type ${identifier}`); - } - - return events[0]; - } - - private gatherAllEvents(transaction: ITransactionOnNetwork): ITransactionEvent[] { - const allEvents = []; - - allEvents.push(...transaction.logs.events); - - for (const item of transaction.contractResults.items) { - allEvents.push(...item.logs.events); - } - - return allEvents; - } - - private extractTokenIdentifier(event: ITransactionEvent): string { - return event.topics[0]?.valueOf().toString(); - } - - private extractNonce(event: ITransactionEvent): string { - return bufferToBigInt(event.topics[1]?.valueOf()).toFixed(0); - } - - private extractAmount(event: ITransactionEvent): string { - return bufferToBigInt(event.topics[2]?.valueOf()).toFixed(0); - } - - private extractAddress(event: ITransactionEvent): string { - return Address.fromBuffer(event.topics[3]?.valueOf()).toString(); - } -} diff --git a/src/tokenTransferBuilders.spec.ts b/src/tokenTransferBuilders.spec.ts deleted file mode 100644 index e3fd02add..000000000 --- a/src/tokenTransferBuilders.spec.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { assert } from "chai"; -import { Address } from "./address"; -import { TokenTransfer } from "./tokens"; -import { - ESDTNFTTransferPayloadBuilder, - ESDTTransferPayloadBuilder, - MultiESDTNFTTransferPayloadBuilder, -} from "./tokenTransferBuilders"; - -describe("test token transfer builders", () => { - it("should work with ESDT transfers", () => { - const transfer = TokenTransfer.fungibleFromAmount("COUNTER-8b028f", "100.00", 0); - const payload = new ESDTTransferPayloadBuilder().setPayment(transfer).build(); - assert.equal(payload.toString(), "ESDTTransfer@434f554e5445522d386230323866@64"); - }); - - it("should work with ESDTNFT transfers (NFT)", () => { - const transfer = TokenTransfer.nonFungible("ERDJS-38f249", 1); - const payload = new ESDTNFTTransferPayloadBuilder() - .setPayment(transfer) - .setDestination(new Address("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx")) - .build(); - - assert.equal( - payload.toString(), - "ESDTNFTTransfer@4552444a532d333866323439@01@01@8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8", - ); - }); - - it("should work with ESDTNFT transfers (SFT)", () => { - const transfer = TokenTransfer.semiFungible("SEMI-9efd0f", 1, 5); - - const payload = new ESDTNFTTransferPayloadBuilder() - .setPayment(transfer) - .setDestination(new Address("erd1testnlersh4z0wsv8kjx39me4rmnvjkwu8dsaea7ukdvvc9z396qykv7z7")) - .build(); - - assert.equal( - payload.toString(), - "ESDTNFTTransfer@53454d492d396566643066@01@05@5e60b9ff2385ea27ba0c3da4689779a8f7364acee1db0ee7bee59ac660a28974", - ); - }); - - it("should work with Multi ESDTNFT transfers", () => { - const transferOne = TokenTransfer.nonFungible("ERDJS-38f249", 1); - const transferTwo = TokenTransfer.fungibleFromAmount("BAR-c80d29", "10.00", 18); - const payload = new MultiESDTNFTTransferPayloadBuilder() - .setPayments([transferOne, transferTwo]) - .setDestination(new Address("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th")) - .build(); - - assert.equal( - payload.toString(), - "MultiESDTNFTTransfer@0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1@02@4552444a532d333866323439@01@01@4241522d633830643239@@8ac7230489e80000", - ); - }); -}); diff --git a/src/tokenTransferBuilders.ts b/src/tokenTransferBuilders.ts deleted file mode 100644 index a557ca363..000000000 --- a/src/tokenTransferBuilders.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { Address } from "./address"; -import { IAddress, ITokenTransfer } from "./interface"; -import { ArgSerializer } from "./smartcontracts/argSerializer"; -import { AddressValue, BigUIntValue, BytesValue, TypedValue, U16Value, U64Value } from "./smartcontracts/typesystem"; -import { TokenTransfer } from "./tokens"; -import { TransactionPayload } from "./transactionPayload"; - -/** - * @deprecated Use {@link TransferTransactionsFactory} instead. - */ -export class ESDTTransferPayloadBuilder { - payment: ITokenTransfer = TokenTransfer.fungibleFromAmount("", "0", 0); - - setPayment(payment: ITokenTransfer): ESDTTransferPayloadBuilder { - this.payment = payment; - return this; - } - - build(): TransactionPayload { - let args: TypedValue[] = [ - // The token identifier - BytesValue.fromUTF8(this.payment.tokenIdentifier), - // The transfered amount - new BigUIntValue(this.payment.valueOf()), - ]; - - let { argumentsString } = new ArgSerializer().valuesToString(args); - let data = `ESDTTransfer@${argumentsString}`; - return new TransactionPayload(data); - } -} - -/** - * @deprecated Use {@link TransferTransactionsFactory} instead. - */ -export class ESDTNFTTransferPayloadBuilder { - payment: ITokenTransfer = TokenTransfer.nonFungible("", 0); - destination: IAddress = Address.empty(); - - setPayment(payment: ITokenTransfer): ESDTNFTTransferPayloadBuilder { - this.payment = payment; - return this; - } - - setDestination(destination: IAddress): ESDTNFTTransferPayloadBuilder { - this.destination = destination; - return this; - } - - build(): TransactionPayload { - let args: TypedValue[] = [ - // The token identifier - BytesValue.fromUTF8(this.payment.tokenIdentifier), - // The nonce of the token - new U64Value(this.payment.nonce), - // The transferred quantity - new BigUIntValue(this.payment.valueOf()), - // The destination address - new AddressValue(this.destination), - ]; - - let { argumentsString } = new ArgSerializer().valuesToString(args); - let data = `ESDTNFTTransfer@${argumentsString}`; - return new TransactionPayload(data); - } -} - -/** - * @deprecated Use {@link TransferTransactionsFactory} instead. - */ -export class MultiESDTNFTTransferPayloadBuilder { - payments: ITokenTransfer[] = []; - destination: IAddress = Address.empty(); - - setPayments(payments: ITokenTransfer[]): MultiESDTNFTTransferPayloadBuilder { - this.payments = payments; - return this; - } - - setDestination(destination: IAddress): MultiESDTNFTTransferPayloadBuilder { - this.destination = destination; - return this; - } - - build(): TransactionPayload { - let args: TypedValue[] = [ - // The destination address - new AddressValue(this.destination), - // Number of tokens - new U16Value(this.payments.length), - ]; - - for (const payment of this.payments) { - args.push( - ...[ - // The token identifier - BytesValue.fromUTF8(payment.tokenIdentifier), - // The nonce of the token - new U64Value(payment.nonce), - // The transfered quantity - new BigUIntValue(payment.valueOf()), - ], - ); - } - - let { argumentsString } = new ArgSerializer().valuesToString(args); - let data = `MultiESDTNFTTransfer@${argumentsString}`; - return new TransactionPayload(data); - } -} diff --git a/src/transaction.ts b/src/transaction.ts deleted file mode 100644 index 04aaa92f4..000000000 --- a/src/transaction.ts +++ /dev/null @@ -1,458 +0,0 @@ -import { BigNumber } from "bignumber.js"; -import { Address } from "./address"; -import { TRANSACTION_MIN_GAS_PRICE, TRANSACTION_OPTIONS_DEFAULT, TRANSACTION_VERSION_DEFAULT } from "./constants"; -import { TransactionsConverter } from "./converters/transactionsConverter"; -import { Hash } from "./hash"; -import { - IAddress, - IChainID, - IGasLimit, - IGasPrice, - INonce, - IPlainTransactionObject, - ISignature, - ITransactionOptions, - ITransactionPayload, - ITransactionValue, - ITransactionVersion, -} from "./interface"; -import { INetworkConfig } from "./interfaceOfNetwork"; -import { TransactionOptions, TransactionVersion } from "./networkParams"; -import { interpretSignatureAsBuffer } from "./signature"; -import { TransactionComputer } from "./transactionComputer"; -import { TransactionPayload } from "./transactionPayload"; - -/** - * An abstraction for creating and signing transactions. - */ -export class Transaction { - /** - * The nonce of the transaction (the account sequence number of the sender). - */ - public nonce: bigint; - - /** - * The value to transfer. - */ - public value: bigint; - - /** - * The address of the sender, in bech32 format. - */ - public sender: string; - - /** - * The address of the receiver, in bech32 format. - */ - public receiver: string; - - /** - * The username of the sender. - */ - public senderUsername: string; - - /** - * The username of the receiver. - */ - public receiverUsername: string; - - /** - * The gas price to be used. - */ - public gasPrice: bigint; - - /** - * The maximum amount of gas to be consumed when processing the transaction. - */ - public gasLimit: bigint; - - /** - * The payload of the transaction. - */ - public data: Uint8Array; - - /** - * The chain ID of the Network (e.g. "1" for Mainnet). - */ - public chainID: string; - - /** - * The version, required by the Network in order to correctly interpret the contents of the transaction. - */ - public version: number; - - /** - * The options field, useful for describing different settings available for transactions. - */ - public options: number; - - /** - * The address of the guardian, in bech32 format. - */ - public guardian: string; - - /** - * The relayer address. - * Note: in the next major version, `sender`, `receiver` and `guardian` will also have the type `Address`, instead of `string`. - */ - public relayer: Address; - - /** - * The signature. - */ - public signature: Uint8Array; - - /** - * The signature of the guardian. - */ - public guardianSignature: Uint8Array; - - /** - * The signature of the relayer. - */ - public relayerSignature: Uint8Array; - - /** - * Creates a new Transaction object. - */ - public constructor(options: { - nonce?: INonce | bigint; - value?: ITransactionValue | bigint; - sender: IAddress | string; - receiver: IAddress | string; - relayer?: Address; - senderUsername?: string; - receiverUsername?: string; - gasPrice?: IGasPrice | bigint; - gasLimit: IGasLimit | bigint; - data?: ITransactionPayload | Uint8Array; - chainID: IChainID | string; - version?: ITransactionVersion | number; - options?: ITransactionOptions | number; - guardian?: IAddress | string; - signature?: Uint8Array; - guardianSignature?: Uint8Array; - relayerSignature?: Uint8Array; - }) { - this.nonce = BigInt(options.nonce?.valueOf() || 0n); - // We still rely on "bigNumber" for value, because client code might be passing a BigNumber object as a legacy "ITransactionValue", - // and we want to keep compatibility. - this.value = options.value ? BigInt(new BigNumber(options.value.toString()).toFixed(0)) : 0n; - this.sender = this.addressAsBech32(options.sender); - this.receiver = this.addressAsBech32(options.receiver); - this.senderUsername = options.senderUsername || ""; - this.receiverUsername = options.receiverUsername || ""; - this.gasPrice = BigInt(options.gasPrice?.valueOf() || TRANSACTION_MIN_GAS_PRICE); - this.gasLimit = BigInt(options.gasLimit.valueOf()); - this.data = options.data?.valueOf() || new Uint8Array(); - this.chainID = options.chainID.valueOf(); - this.version = Number(options.version?.valueOf() || TRANSACTION_VERSION_DEFAULT); - this.options = Number(options.options?.valueOf() || TRANSACTION_OPTIONS_DEFAULT); - this.guardian = options.guardian ? this.addressAsBech32(options.guardian) : ""; - this.relayer = options.relayer ? options.relayer : Address.empty(); - - this.signature = options.signature || Buffer.from([]); - this.guardianSignature = options.guardianSignature || Buffer.from([]); - this.relayerSignature = options.relayerSignature || Buffer.from([]); - } - - private addressAsBech32(address: IAddress | string): string { - return typeof address === "string" ? address : address.bech32(); - } - - /** - * Legacy method, use the "nonce" property instead. - */ - getNonce(): INonce { - return Number(this.nonce); - } - - /** - * Legacy method, use the "nonce" property instead. - * Sets the account sequence number of the sender. Must be done prior signing. - */ - setNonce(nonce: INonce | bigint) { - this.nonce = BigInt(nonce.valueOf()); - } - - /** - * Legacy method, use the "value" property instead. - */ - getValue(): ITransactionValue { - return this.value; - } - - /** - * Legacy method, use the "value" property instead. - */ - setValue(value: ITransactionValue | bigint) { - this.value = BigInt(value.toString()); - } - - /** - * Legacy method, use the "sender" property instead. - */ - getSender(): IAddress { - return Address.fromBech32(this.sender); - } - - /** - * Legacy method, use the "sender" property instead. - */ - setSender(sender: IAddress | string) { - this.sender = typeof sender === "string" ? sender : sender.bech32(); - } - - /** - * Legacy method, use the "receiver" property instead. - */ - getReceiver(): IAddress { - return Address.fromBech32(this.receiver); - } - - /** - * Legacy method, use the "senderUsername" property instead. - */ - getSenderUsername(): string { - return this.senderUsername; - } - - /** - * Legacy method, use the "senderUsername" property instead. - */ - setSenderUsername(senderUsername: string) { - this.senderUsername = senderUsername; - } - - /** - * Legacy method, use the "receiverUsername" property instead. - */ - getReceiverUsername(): string { - return this.receiverUsername; - } - - /** - * Legacy method, use the "receiverUsername" property instead. - */ - setReceiverUsername(receiverUsername: string) { - this.receiverUsername = receiverUsername; - } - - /** - * Legacy method, use the "guardian" property instead. - */ - getGuardian(): IAddress { - return new Address(this.guardian); - } - - /** - * Legacy method, use the "gasPrice" property instead. - */ - getGasPrice(): IGasPrice { - return Number(this.gasPrice); - } - - /** - * Legacy method, use the "gasPrice" property instead. - */ - setGasPrice(gasPrice: IGasPrice | bigint) { - this.gasPrice = BigInt(gasPrice.valueOf()); - } - - /** - * Legacy method, use the "gasLimit" property instead. - */ - getGasLimit(): IGasLimit { - return Number(this.gasLimit); - } - - /** - * Legacy method, use the "gasLimit" property instead. - */ - setGasLimit(gasLimit: IGasLimit | bigint) { - this.gasLimit = BigInt(gasLimit.valueOf()); - } - - /** - * Legacy method, use the "data" property instead. - */ - getData(): ITransactionPayload { - return new TransactionPayload(Buffer.from(this.data)); - } - - /** - * Legacy method, use the "chainID" property instead. - */ - getChainID(): IChainID { - return this.chainID; - } - - /** - * Legacy method, use the "chainID" property instead. - */ - setChainID(chainID: IChainID | string) { - this.chainID = chainID.valueOf(); - } - - /** - * Legacy method, use the "version" property instead. - */ - getVersion(): TransactionVersion { - return new TransactionVersion(this.version); - } - - /** - * Legacy method, use the "version" property instead. - */ - setVersion(version: ITransactionVersion | number) { - this.version = version.valueOf(); - } - - /** - * Legacy method, use the "options" property instead. - */ - getOptions(): TransactionOptions { - return new TransactionOptions(this.options.valueOf()); - } - - /** - * Legacy method, use the "options" property instead. - * - * Question for review: check how the options are set by sdk-dapp, wallet, ledger, extension. - */ - setOptions(options: ITransactionOptions | number) { - this.options = options.valueOf(); - } - - /** - * Legacy method, use the "signature" property instead. - */ - getSignature(): Buffer { - return Buffer.from(this.signature); - } - - /** - * Legacy method, use the "guardianSignature" property instead. - */ - getGuardianSignature(): Buffer { - return Buffer.from(this.guardianSignature); - } - - /** - * Legacy method, use the "guardian" property instead. - */ - setGuardian(guardian: IAddress | string) { - this.guardian = typeof guardian === "string" ? guardian : guardian.bech32(); - } - - /** - * Legacy method, use "TransactionComputer.computeTransactionHash()" instead. - */ - getHash(): TransactionHash { - return TransactionHash.compute(this); - } - - /** - * Legacy method, use "TransactionComputer.computeBytesForSigning()" instead. - * Serializes a transaction to a sequence of bytes, ready to be signed. - * This function is called internally by signers. - */ - serializeForSigning(): Buffer { - const computer = new TransactionComputer(); - const bytes = computer.computeBytesForSigning(this); - return Buffer.from(bytes); - } - - /** - * Checks the integrity of the guarded transaction - */ - isGuardedTransaction(): boolean { - const hasGuardian = this.guardian.length > 0; - const hasGuardianSignature = this.guardianSignature.length > 0; - return this.getOptions().isWithGuardian() && hasGuardian && hasGuardianSignature; - } - - /** - * Legacy method, use "TransactionsConverter.transactionToPlainObject()" instead. - * - * Converts the transaction object into a ready-to-serialize, plain JavaScript object. - * This function is called internally within the signing procedure. - */ - toPlainObject(): IPlainTransactionObject { - // Ideally, "converters" package should be outside of "core", and not referenced here. - const converter = new TransactionsConverter(); - return converter.transactionToPlainObject(this); - } - - /** - * Legacy method, use "TransactionsConverter.plainObjectToTransaction()" instead. - * Converts a plain object transaction into a Transaction Object. - * - * @param plainObjectTransaction Raw data of a transaction, usually obtained by calling toPlainObject() - */ - static fromPlainObject(plainObjectTransaction: IPlainTransactionObject): Transaction { - // Ideally, "converters" package should be outside of "core", and not referenced here. - const converter = new TransactionsConverter(); - return converter.plainObjectToTransaction(plainObjectTransaction); - } - - /** - * Legacy method, use the "signature" property instead. - * Applies the signature on the transaction. - * - * @param signature The signature, as computed by a signer. - */ - applySignature(signature: ISignature | Uint8Array) { - this.signature = interpretSignatureAsBuffer(signature); - } - - /** - * Legacy method, use the "guardianSignature" property instead. - * Applies the guardian signature on the transaction. - * - * @param guardianSignature The signature, as computed by a signer. - */ - applyGuardianSignature(guardianSignature: ISignature | Uint8Array) { - this.guardianSignature = interpretSignatureAsBuffer(guardianSignature); - } - - /** - * Converts a transaction to a ready-to-broadcast object. - * Called internally by the network provider. - */ - toSendable(): any { - return this.toPlainObject(); - } - - /** - * Legacy method, use "TransactionComputer.computeTransactionFee()" instead. - * - * Computes the current transaction fee based on the {@link NetworkConfig} and transaction properties - * @param networkConfig {@link NetworkConfig} - */ - computeFee(networkConfig: INetworkConfig): BigNumber { - const computer = new TransactionComputer(); - const fee = computer.computeTransactionFee(this, networkConfig); - return new BigNumber(fee.toString()); - } -} - -/** - * Legacy class, use "TransactionComputer.computeTransactionHash()" instead. - * An abstraction for handling and computing transaction hashes. - */ -export class TransactionHash extends Hash { - constructor(hash: string) { - super(hash); - } - - /** - * Legacy method, use "TransactionComputer.computeTransactionHash()" instead. - * Computes the hash of a transaction. - */ - static compute(transaction: Transaction): TransactionHash { - const computer = new TransactionComputer(); - const hash = computer.computeTransactionHash(transaction); - return new TransactionHash(Buffer.from(hash).toString("hex")); - } -} diff --git a/src/transactionsFactories/delegationTransactionsFactory.spec.ts b/src/transactionsFactories/delegationTransactionsFactory.spec.ts deleted file mode 100644 index c7d72902f..000000000 --- a/src/transactionsFactories/delegationTransactionsFactory.spec.ts +++ /dev/null @@ -1,307 +0,0 @@ -import { ValidatorPublicKey } from "./../wallet"; -import { assert } from "chai"; -import { Address } from "../address"; -import { DELEGATION_MANAGER_SC_ADDRESS } from "../constants"; -import { DelegationTransactionsFactory } from "./delegationTransactionsFactory"; -import { TransactionsFactoryConfig } from "./transactionsFactoryConfig"; - -describe("test delegation transactions factory", function () { - const config = new TransactionsFactoryConfig({ chainID: "D" }); - const delegationFactory = new DelegationTransactionsFactory({ config: config }); - - it("should create 'Transaction' for new delegation contract", async function () { - const sender = Address.fromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); - const delagationCap = 5000000000000000000000n; - const serviceFee = 10n; - const value = 1250000000000000000000n; - - const transaction = delegationFactory.createTransactionForNewDelegationContract({ - sender: sender, - totalDelegationCap: delagationCap, - serviceFee: serviceFee, - amount: value, - }); - - assert.equal(transaction.sender, "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); - assert.equal(transaction.receiver, DELEGATION_MANAGER_SC_ADDRESS); - assert.isDefined(transaction.data); - assert.deepEqual(transaction.data, Buffer.from("createNewDelegationContract@010f0cf064dd59200000@0a")); - assert.equal(transaction.gasLimit, 60126500n); - assert.equal(transaction.value, value); - assert.equal(transaction.chainID, config.chainID); - }); - - it("should create '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 transaction = delegationFactory.createTransactionForAddingNodes({ - sender: sender, - delegationContract: delegationContract, - publicKeys: [publicKey], - signedMessages: [mockMessage.getSignature()], - }); - - assert.equal(transaction.sender, "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); - assert.equal(transaction.receiver, "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); - assert.isDefined(transaction.data); - assert.deepEqual( - transaction.data, - Buffer.from( - "addNodes@e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208@81109fa1c8d3dc7b6c2d6e65206cc0bc1a83c9b2d1eb91a601d66ad32def430827d5eb52917bd2b0d04ce195738db216", - ), - ); - assert.equal(transaction.value, 0n); - }); - - it("should create '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 transaction = delegationFactory.createTransactionForRemovingNodes({ - sender: sender, - delegationContract: delegationContract, - publicKeys: [publicKey], - }); - - assert.equal(transaction.sender, "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); - assert.equal(transaction.receiver, "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); - assert.isDefined(transaction.data); - assert.deepEqual(transaction.data, Buffer.from("removeNodes@61626261")); - assert.equal(transaction.value, 0n); - }); - - it("should create '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 transaction = delegationFactory.createTransactionForStakingNodes({ - sender: sender, - delegationContract: delegationContract, - publicKeys: [publicKey], - }); - - assert.equal(transaction.sender, "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); - assert.equal(transaction.receiver, "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); - assert.isDefined(transaction.data); - assert.deepEqual(transaction.data, Buffer.from("stakeNodes@61626261")); - assert.equal(transaction.value, 0n); - }); - - it("should create '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 transaction = delegationFactory.createTransactionForUnbondingNodes({ - sender: sender, - delegationContract: delegationContract, - publicKeys: [publicKey], - }); - - assert.equal(transaction.sender, "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); - assert.equal(transaction.receiver, "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); - assert.isDefined(transaction.data); - assert.deepEqual(transaction.data, Buffer.from("unBondNodes@61626261")); - assert.equal(transaction.value, 0n); - assert.equal(transaction.gasLimit, 12080000n); - }); - - it("should create '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 transaction = delegationFactory.createTransactionForUnstakingNodes({ - sender: sender, - delegationContract: delegationContract, - publicKeys: [publicKey], - }); - - assert.equal(transaction.sender, "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); - assert.equal(transaction.receiver, "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); - assert.isDefined(transaction.data); - assert.deepEqual(transaction.data, Buffer.from("unStakeNodes@61626261")); - assert.equal(transaction.value, 0n); - assert.equal(transaction.gasLimit, 12081500n); - }); - - it("should create '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 transaction = delegationFactory.createTransactionForUnjailingNodes({ - sender: sender, - delegationContract: delegationContract, - publicKeys: [publicKey], - amount: 25000000000000000000n, - }); - - assert.equal(transaction.sender, "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); - assert.equal(transaction.receiver, "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); - assert.isDefined(transaction.data); - assert.deepEqual(transaction.data, Buffer.from("unJailNodes@61626261")); - assert.equal(transaction.value, 25000000000000000000n); - }); - - it("should create 'Transaction' for changing service fee", async function () { - const sender = Address.fromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); - const delegationContract = Address.fromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); - const serviceFee = 10n; - - const transaction = delegationFactory.createTransactionForChangingServiceFee({ - sender: sender, - delegationContract: delegationContract, - serviceFee: serviceFee, - }); - - assert.equal(transaction.sender, "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); - assert.equal(transaction.receiver, "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); - assert.isDefined(transaction.data); - assert.deepEqual(transaction.data, Buffer.from("changeServiceFee@0a")); - assert.equal(transaction.value, 0n); - }); - - it("should create 'Transaction' for changing delegation cap", async function () { - const sender = Address.fromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); - const delegationContract = Address.fromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); - const delegationCap = 5000000000000000000000n; - - const transaction = delegationFactory.createTransactionForModifyingDelegationCap({ - sender: sender, - delegationContract: delegationContract, - delegationCap: delegationCap, - }); - - assert.equal(transaction.sender, "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); - assert.equal(transaction.receiver, "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); - assert.isDefined(transaction.data); - assert.deepEqual(transaction.data, Buffer.from("modifyTotalDelegationCap@010f0cf064dd59200000")); - assert.equal(transaction.value, 0n); - }); - - it("should create 'Transaction' for setting automatic activation", async function () { - const sender = Address.fromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); - const delegationContract = Address.fromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); - - const transaction = delegationFactory.createTransactionForSettingAutomaticActivation({ - sender: sender, - delegationContract: delegationContract, - }); - - assert.equal(transaction.sender, "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); - assert.equal(transaction.receiver, "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); - assert.isDefined(transaction.data); - assert.deepEqual(transaction.data, Buffer.from("setAutomaticActivation@74727565")); - assert.equal(transaction.value, 0n); - }); - - it("should create 'Transaction' for unsetting automatic activation", async function () { - const sender = Address.fromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); - const delegationContract = Address.fromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); - - const transaction = delegationFactory.createTransactionForUnsettingAutomaticActivation({ - sender: sender, - delegationContract: delegationContract, - }); - - assert.equal(transaction.sender, "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); - assert.equal(transaction.receiver, "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); - assert.isDefined(transaction.data); - assert.deepEqual(transaction.data, Buffer.from("setAutomaticActivation@66616c7365")); - assert.equal(transaction.value, 0n); - }); - - it("should create 'Transaction' for setting cap check on redelegate rewards", async function () { - const sender = Address.fromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); - const delegationContract = Address.fromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); - - const transaction = delegationFactory.createTransactionForSettingCapCheckOnRedelegateRewards({ - sender: sender, - delegationContract: delegationContract, - }); - - assert.equal(transaction.sender, "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); - assert.equal(transaction.receiver, "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); - assert.isDefined(transaction.data); - assert.deepEqual(transaction.data, Buffer.from("setCheckCapOnReDelegateRewards@74727565")); - assert.equal(transaction.value, 0n); - }); - - it("should create 'Transaction' for unsetting cap check on redelegate rewards", async function () { - const sender = Address.fromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); - const delegationContract = Address.fromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); - - const transaction = delegationFactory.createTransactionForUnsettingCapCheckOnRedelegateRewards({ - sender: sender, - delegationContract: delegationContract, - }); - - assert.equal(transaction.sender, "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); - assert.equal(transaction.receiver, "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); - assert.isDefined(transaction.data); - assert.deepEqual(transaction.data, Buffer.from("setCheckCapOnReDelegateRewards@66616c7365")); - assert.equal(transaction.value, 0n); - }); - - it("should create 'Transaction' for setting metadata", async function () { - const sender = Address.fromBech32("erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); - const delegationContract = Address.fromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); - - const transaction = delegationFactory.createTransactionForSettingMetadata({ - sender: sender, - delegationContract: delegationContract, - name: "name", - website: "website", - identifier: "identifier", - }); - - assert.equal(transaction.sender, "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); - assert.equal(transaction.receiver, "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc"); - assert.isDefined(transaction.data); - assert.deepEqual(transaction.data, Buffer.from("setMetaData@6e616d65@77656273697465@6964656e746966696572")); - assert.equal(transaction.value, 0n); - }); -}); diff --git a/src/transactionsFactories/index.ts b/src/transactionsFactories/index.ts deleted file mode 100644 index fd9df3521..000000000 --- a/src/transactionsFactories/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from "./delegationTransactionsFactory"; -export * from "./relayedTransactionsFactory"; -export * from "./smartContractTransactionsFactory"; -export * from "./tokenManagementTransactionsFactory"; -export * from "./transactionsFactoryConfig"; -export * from "./transferTransactionsFactory"; -export * from "./accountTransactionsFactory"; diff --git a/src/transactionsFactories/relayedTransactionsFactory.spec.ts b/src/transactionsFactories/relayedTransactionsFactory.spec.ts deleted file mode 100644 index 6c61d331a..000000000 --- a/src/transactionsFactories/relayedTransactionsFactory.spec.ts +++ /dev/null @@ -1,278 +0,0 @@ -import { assert } from "chai"; -import { TestWallet, loadTestWallets } from "../testutils"; -import { Transaction } from "../transaction"; -import { TransactionComputer } from "../transactionComputer"; -import { RelayedTransactionsFactory } from "./relayedTransactionsFactory"; -import { TransactionsFactoryConfig } from "./transactionsFactoryConfig"; - -describe("test relayed transactions factory", function () { - const config = new TransactionsFactoryConfig({ chainID: "T" }); - const factory = new RelayedTransactionsFactory({ config: config }); - const transactionComputer = new TransactionComputer(); - let alice: TestWallet, bob: TestWallet, carol: TestWallet, grace: TestWallet, frank: TestWallet; - - before(async function () { - ({ alice, bob, carol, grace, frank } = await loadTestWallets()); - }); - - it("should throw exception when creating relayed v1 transaction with invalid inner transaction", async function () { - let innerTransaction = new Transaction({ - sender: alice.address.bech32(), - receiver: "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u", - gasLimit: 10000000n, - data: Buffer.from("getContractConfig"), - chainID: config.chainID, - }); - - assert.throws(() => { - factory.createRelayedV1Transaction({ innerTransaction: innerTransaction, relayerAddress: bob.address }), - "The inner transaction is not signed"; - }); - - innerTransaction.gasLimit = 0n; - innerTransaction.signature = Buffer.from("invalidsignature"); - - assert.throws(() => { - factory.createRelayedV1Transaction({ innerTransaction: innerTransaction, relayerAddress: bob.address }), - "The gas limit is not set for the inner transaction"; - }); - }); - - it("should create relayed v1 transaction", async function () { - let innerTransaction = new Transaction({ - sender: bob.address.bech32(), - receiver: "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u", - gasLimit: 60000000n, - data: Buffer.from("getContractConfig"), - chainID: config.chainID, - nonce: 198n, - }); - - const serializedInnerTransaction = transactionComputer.computeBytesForSigning(innerTransaction); - innerTransaction.signature = await bob.signer.sign(serializedInnerTransaction); - - const relayedTransaction = factory.createRelayedV1Transaction({ - innerTransaction: innerTransaction, - relayerAddress: alice.address, - }); - relayedTransaction.nonce = 2627n; - - const serializedRelayedTransaction = transactionComputer.computeBytesForSigning(relayedTransaction); - relayedTransaction.signature = await alice.signer.sign(serializedRelayedTransaction); - - assert.equal( - Buffer.from(relayedTransaction.data).toString(), - "relayedTx@7b226e6f6e6365223a3139382c2273656e646572223a2267456e574f65576d6d413063306a6b71764d354241707a61644b46574e534f69417643575163776d4750673d222c227265636569766572223a22414141414141414141414141415141414141414141414141414141414141414141414141414141432f2f383d222c2276616c7565223a302c226761735072696365223a313030303030303030302c226761734c696d6974223a36303030303030302c2264617461223a225a3256305132397564484a68593352446232356d6157633d222c227369676e6174757265223a2272525455544858677a4273496e4f6e454b6b7869642b354e66524d486e33534948314673746f577352434c434b3258514c41614f4e704449346531476173624c5150616130566f364144516d4f2b52446b6f364a43413d3d222c22636861696e4944223a2256413d3d222c2276657273696f6e223a327d", - ); - assert.equal( - Buffer.from(relayedTransaction.signature).toString("hex"), - "128e7cdc14c2b9beee2f3ff7a7fa5d1f5ef31a654a0c92e223c90ab28265fa277d306f23a06536248cf9573e828017004fb639617fade4d68a37524aafca710d", - ); - }); - - it("should create relayed v1 transaction with usernames", async function () { - let innerTransaction = new Transaction({ - sender: carol.address.bech32(), - receiver: alice.address.bech32(), - gasLimit: 50000n, - chainID: config.chainID, - nonce: 208n, - senderUsername: "carol", - receiverUsername: "alice", - value: 1000000000000000000n, - }); - - const serializedInnerTransaction = transactionComputer.computeBytesForSigning(innerTransaction); - innerTransaction.signature = await carol.signer.sign(serializedInnerTransaction); - - const relayedTransaction = factory.createRelayedV1Transaction({ - innerTransaction: innerTransaction, - relayerAddress: frank.address, - }); - relayedTransaction.nonce = 715n; - - const serializedRelayedTransaction = transactionComputer.computeBytesForSigning(relayedTransaction); - relayedTransaction.signature = await frank.signer.sign(serializedRelayedTransaction); - - assert.equal( - Buffer.from(relayedTransaction.data).toString(), - "relayedTx@7b226e6f6e6365223a3230382c2273656e646572223a227371455656633553486b6c45344a717864556e59573068397a536249533141586f3534786f32634969626f3d222c227265636569766572223a2241546c484c76396f686e63616d433877673970645168386b77704742356a6949496f3349484b594e6165453d222c2276616c7565223a313030303030303030303030303030303030302c226761735072696365223a313030303030303030302c226761734c696d6974223a35303030302c2264617461223a22222c227369676e6174757265223a226a33427a6469554144325963517473576c65707663664a6f75657a48573063316b735a424a4d6339573167435450512b6870636759457858326f6f367a4b5654347464314b4b6f79783841526a346e336474576c44413d3d222c22636861696e4944223a2256413d3d222c2276657273696f6e223a322c22736e64557365724e616d65223a22593246796232773d222c22726376557365724e616d65223a22595778705932553d227d", - ); - assert.equal( - Buffer.from(relayedTransaction.signature).toString("hex"), - "3787d640e5a579e7977a4a1bcdd435ad11855632fa4a414a06fbf8355692d1a58d76ef0adbdd6ccd6bd3c329f36bd53c180d4873ec1a6c558e659aeb9ab92d00", - ); - }); - - it("should create relayed v1 transaction with big value", async function () { - let innerTransaction = new Transaction({ - sender: carol.address.bech32(), - receiver: alice.address.bech32(), - gasLimit: 50000n, - chainID: config.chainID, - nonce: 208n, - senderUsername: "carol", - receiverUsername: "alice", - value: 1999999000000000000000000n, - }); - - const serializedInnerTransaction = transactionComputer.computeBytesForSigning(innerTransaction); - innerTransaction.signature = await carol.signer.sign(serializedInnerTransaction); - - const relayedTransaction = factory.createRelayedV1Transaction({ - innerTransaction: innerTransaction, - relayerAddress: frank.address, - }); - relayedTransaction.nonce = 715n; - - const serializedRelayedTransaction = transactionComputer.computeBytesForSigning(relayedTransaction); - relayedTransaction.signature = await frank.signer.sign(serializedRelayedTransaction); - - assert.equal( - Buffer.from(relayedTransaction.data).toString(), - "relayedTx@7b226e6f6e6365223a3230382c2273656e646572223a227371455656633553486b6c45344a717864556e59573068397a536249533141586f3534786f32634969626f3d222c227265636569766572223a2241546c484c76396f686e63616d433877673970645168386b77704742356a6949496f3349484b594e6165453d222c2276616c7565223a313939393939393030303030303030303030303030303030302c226761735072696365223a313030303030303030302c226761734c696d6974223a35303030302c2264617461223a22222c227369676e6174757265223a22594661677972512f726d614c7333766e7159307657553858415a7939354b4e31725738347a4f764b62376c7a3773576e2f566a546d68704378774d682b7261314e444832574d6f3965507648304f79427453776a44773d3d222c22636861696e4944223a2256413d3d222c2276657273696f6e223a322c22736e64557365724e616d65223a22593246796232773d222c22726376557365724e616d65223a22595778705932553d227d", - ); - assert.equal( - Buffer.from(relayedTransaction.signature).toString("hex"), - "c0fb5cf8c0a413d6988ba35dc279c63f8849572c5f23b1cab36dcc50952dc3ed9da01068d6ac0cbde7e14167bfc2eca5164d5c2154c89eb313c9c596e3f8b801", - ); - }); - - it("should create relayed v1 transaction with guarded inner transaction", async function () { - let innerTransaction = new Transaction({ - sender: bob.address.bech32(), - receiver: "erd1qqqqqqqqqqqqqpgq54tsxmej537z9leghvp69hfu4f8gg5eu396q83gnnz", - gasLimit: 60000000n, - chainID: config.chainID, - data: Buffer.from("getContractConfig"), - nonce: 198n, - version: 2, - options: 2, - guardian: grace.address.bech32(), - }); - - const serializedInnerTransaction = transactionComputer.computeBytesForSigning(innerTransaction); - innerTransaction.signature = await bob.signer.sign(serializedInnerTransaction); - innerTransaction.guardianSignature = await grace.signer.sign(serializedInnerTransaction); - - const relayedTransaction = factory.createRelayedV1Transaction({ - innerTransaction: innerTransaction, - relayerAddress: alice.address, - }); - relayedTransaction.nonce = 2627n; - - const serializedRelayedTransaction = transactionComputer.computeBytesForSigning(relayedTransaction); - relayedTransaction.signature = await alice.signer.sign(serializedRelayedTransaction); - - assert.equal( - Buffer.from(relayedTransaction.data).toString(), - "relayedTx@7b226e6f6e6365223a3139382c2273656e646572223a2267456e574f65576d6d413063306a6b71764d354241707a61644b46574e534f69417643575163776d4750673d222c227265636569766572223a22414141414141414141414146414b565841323879704877692f79693741364c64504b704f68464d386958513d222c2276616c7565223a302c226761735072696365223a313030303030303030302c226761734c696d6974223a36303030303030302c2264617461223a225a3256305132397564484a68593352446232356d6157633d222c227369676e6174757265223a224b4b78324f33383655725135416b4f465258307578327933446a384853334b373038487174344668377161557669424550716c45614e746e6158706a6f2f333651476d4a456934784435457a6c6f4f677a634d4442773d3d222c22636861696e4944223a2256413d3d222c2276657273696f6e223a322c226f7074696f6e73223a322c22677561726469616e223a22486f714c61306e655733766843716f56696c70715372744c5673774939535337586d7a563868477450684d3d222c22677561726469616e5369676e6174757265223a222b5431526f4833625a792f54423177342b6a365155477258645637457577553073753948646551626453515269463953757a686d634b705463526d58595252366c534c6652394931624d7134674730436538363741513d3d227d", - ); - assert.equal( - Buffer.from(relayedTransaction.signature).toString("hex"), - "39cff9d5100e290fbc7361cb6e2402261caf864257b4116f150e0c61e7869155dff8361fa5449431eb7a8ed847c01ba9b3b5ebafe5fac1a3d40c64829d827e00", - ); - }); - - it("should create guarded relayed v1 transaction with guarded inner transaction", async function () { - let innerTransaction = new Transaction({ - sender: bob.address.bech32(), - receiver: "erd1qqqqqqqqqqqqqpgq54tsxmej537z9leghvp69hfu4f8gg5eu396q83gnnz", - gasLimit: 60000000n, - chainID: config.chainID, - data: Buffer.from("addNumber"), - nonce: 198n, - version: 2, - options: 2, - guardian: grace.address.bech32(), - }); - - const serializedInnerTransaction = transactionComputer.computeBytesForSigning(innerTransaction); - innerTransaction.signature = await bob.signer.sign(serializedInnerTransaction); - innerTransaction.guardianSignature = await grace.signer.sign(serializedInnerTransaction); - - const relayedTransaction = factory.createRelayedV1Transaction({ - innerTransaction: innerTransaction, - relayerAddress: alice.address, - }); - relayedTransaction.nonce = 2627n; - relayedTransaction.options = 2; - relayedTransaction.guardian = frank.address.bech32(); - - const serializedRelayedTransaction = transactionComputer.computeBytesForSigning(relayedTransaction); - relayedTransaction.signature = await alice.signer.sign(serializedRelayedTransaction); - relayedTransaction.guardianSignature = await frank.signer.sign(serializedRelayedTransaction); - - assert.equal( - Buffer.from(relayedTransaction.data).toString(), - "relayedTx@7b226e6f6e6365223a3139382c2273656e646572223a2267456e574f65576d6d413063306a6b71764d354241707a61644b46574e534f69417643575163776d4750673d222c227265636569766572223a22414141414141414141414146414b565841323879704877692f79693741364c64504b704f68464d386958513d222c2276616c7565223a302c226761735072696365223a313030303030303030302c226761734c696d6974223a36303030303030302c2264617461223a225957526b546e5674596d5679222c227369676e6174757265223a223469724d4b4a656d724d375174344e7635487633544c44683775654779487045564c4371674a3677652f7a662b746a4933354975573452633458543451533433475333356158386c6a533834324a38426854645043673d3d222c22636861696e4944223a2256413d3d222c2276657273696f6e223a322c226f7074696f6e73223a322c22677561726469616e223a22486f714c61306e655733766843716f56696c70715372744c5673774939535337586d7a563868477450684d3d222c22677561726469616e5369676e6174757265223a2270424754394e674a78307539624c56796b654d78786a454865374269696c37764932324a46676f32787a6e2f496e3032463769546563356b44395045324f747065386c475335412b532f4a36417762576834446744673d3d227d", - ); - assert.equal( - Buffer.from(relayedTransaction.signature).toString("hex"), - "8ede1bbeed96b102344dffeac12c2592c62b7313cdeb132e8c8bf11d2b1d3bb8189d257a6dbcc99e222393d9b9ec77656c349dae97a32e68bdebd636066bf706", - ); - }); - - it("should throw exception when creating relayed v2 transaction with invalid inner transaction", async function () { - let innerTransaction = new Transaction({ - sender: bob.address.bech32(), - receiver: bob.address.bech32(), - gasLimit: 50000n, - chainID: config.chainID, - }); - - assert.throws(() => { - factory.createRelayedV2Transaction({ - innerTransaction: innerTransaction, - innerTransactionGasLimit: 50000n, - relayerAddress: carol.address, - }), - "The gas limit should not be set for the inner transaction"; - }); - - innerTransaction.gasLimit = 0n; - - assert.throws(() => { - factory.createRelayedV2Transaction({ - innerTransaction: innerTransaction, - innerTransactionGasLimit: 50000n, - relayerAddress: carol.address, - }), - "The inner transaction is not signed"; - }); - }); - - it("should create relayed v2 transaction", async function () { - let innerTransaction = new Transaction({ - sender: bob.address.bech32(), - receiver: "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u", - gasLimit: 0n, - chainID: config.chainID, - data: Buffer.from("getContractConfig"), - nonce: 15n, - version: 2, - options: 0, - }); - - const serializedInnerTransaction = transactionComputer.computeBytesForSigning(innerTransaction); - innerTransaction.signature = await bob.signer.sign(serializedInnerTransaction); - - const relayedTransaction = factory.createRelayedV2Transaction({ - innerTransaction: innerTransaction, - innerTransactionGasLimit: 60000000n, - relayerAddress: alice.address, - }); - relayedTransaction.nonce = 37n; - - const serializedRelayedTransaction = transactionComputer.computeBytesForSigning(relayedTransaction); - relayedTransaction.signature = await alice.signer.sign(serializedRelayedTransaction); - - assert.equal(relayedTransaction.version, 2); - assert.equal(relayedTransaction.options, 0); - assert.equal(relayedTransaction.gasLimit.toString(), "60414500"); - assert.equal( - Buffer.from(relayedTransaction.data).toString(), - "relayedTxV2@000000000000000000010000000000000000000000000000000000000002ffff@0f@676574436f6e7472616374436f6e666967@fc3ed87a51ee659f937c1a1ed11c1ae677e99629fae9cc289461f033e6514d1a8cfad1144ae9c1b70f28554d196bd6ba1604240c1c1dc19c959e96c1c3b62d0c", - ); - }); -}); diff --git a/src/transactionsFactories/relayedTransactionsFactory.ts b/src/transactionsFactories/relayedTransactionsFactory.ts deleted file mode 100644 index b69741025..000000000 --- a/src/transactionsFactories/relayedTransactionsFactory.ts +++ /dev/null @@ -1,116 +0,0 @@ -import BigNumber from "bignumber.js"; -import { Address } from "../address"; -import { ErrInvalidInnerTransaction } from "../errors"; -import { IAddress, ITransaction } from "../interface"; -import { AddressValue, ArgSerializer, BytesValue, U64Value } from "../smartcontracts"; -import { Transaction } from "../transaction"; - -const JSONbig = require("json-bigint"); - -interface IConfig { - chainID: string; - minGasLimit: bigint; - gasLimitPerByte: bigint; -} - -/** - * Use this class to create both RelayedV1 and RelayedV2 transactions. - */ -export class RelayedTransactionsFactory { - private readonly config: IConfig; - - constructor(options: { config: IConfig }) { - this.config = options.config; - } - - createRelayedV1Transaction(options: { innerTransaction: ITransaction; relayerAddress: IAddress }): Transaction { - if (!options.innerTransaction.gasLimit) { - throw new ErrInvalidInnerTransaction("The gas limit is not set for the inner transaction"); - } - - if (!options.innerTransaction.signature.length) { - throw new ErrInvalidInnerTransaction("The inner transaction is not signed"); - } - - const serializedTransaction = this.prepareInnerTransactionForRelayedV1(options.innerTransaction); - const data = `relayedTx@${Buffer.from(serializedTransaction).toString("hex")}`; - - const additionalGasForDataLength = this.config.gasLimitPerByte * BigInt(data.length); - const gasLimit = this.config.minGasLimit + additionalGasForDataLength + options.innerTransaction.gasLimit; - - return new Transaction({ - chainID: this.config.chainID, - sender: options.relayerAddress.bech32(), - receiver: options.innerTransaction.sender, - gasLimit: gasLimit, - data: Buffer.from(data), - }); - } - - createRelayedV2Transaction(options: { - innerTransaction: ITransaction; - innerTransactionGasLimit: bigint; - relayerAddress: IAddress; - }): Transaction { - if (options.innerTransaction.gasLimit) { - throw new ErrInvalidInnerTransaction("The gas limit should not be set for the inner transaction"); - } - - if (!options.innerTransaction.signature.length) { - throw new ErrInvalidInnerTransaction("The inner transaction is not signed"); - } - - const { argumentsString } = new ArgSerializer().valuesToString([ - new AddressValue(Address.fromBech32(options.innerTransaction.receiver)), - new U64Value(new BigNumber(options.innerTransaction.nonce.toString())), - new BytesValue(Buffer.from(options.innerTransaction.data)), - new BytesValue(Buffer.from(options.innerTransaction.signature)), - ]); - - const data = `relayedTxV2@${argumentsString}`; - - const additionalGasForDataLength = this.config.gasLimitPerByte * BigInt(data.length); - const gasLimit = options.innerTransactionGasLimit + this.config.minGasLimit + additionalGasForDataLength; - - return new Transaction({ - sender: options.relayerAddress.bech32(), - receiver: options.innerTransaction.sender, - value: 0n, - gasLimit: gasLimit, - chainID: this.config.chainID, - data: Buffer.from(data), - version: options.innerTransaction.version, - options: options.innerTransaction.options, - }); - } - - private prepareInnerTransactionForRelayedV1(innerTransaction: ITransaction): string { - const txObject = { - nonce: innerTransaction.nonce, - sender: Address.newFromBech32(innerTransaction.sender).getPublicKey().toString("base64"), - receiver: Address.newFromBech32(innerTransaction.receiver).getPublicKey().toString("base64"), - value: innerTransaction.value, - gasPrice: innerTransaction.gasPrice, - gasLimit: innerTransaction.gasLimit, - data: Buffer.from(innerTransaction.data).toString("base64"), - signature: Buffer.from(innerTransaction.signature).toString("base64"), - chainID: Buffer.from(innerTransaction.chainID).toString("base64"), - version: innerTransaction.version, - options: innerTransaction.options.valueOf() == 0 ? undefined : innerTransaction.options, - guardian: innerTransaction.guardian - ? Address.newFromBech32(innerTransaction.guardian).getPublicKey().toString("base64") - : undefined, - guardianSignature: innerTransaction.guardianSignature.length - ? Buffer.from(innerTransaction.guardianSignature).toString("base64") - : undefined, - sndUserName: innerTransaction.senderUsername - ? Buffer.from(innerTransaction.senderUsername).toString("base64") - : undefined, - rcvUserName: innerTransaction.receiverUsername - ? Buffer.from(innerTransaction.receiverUsername).toString("base64") - : undefined, - }; - - return JSONbig.stringify(txObject); - } -} diff --git a/src/transactionsFactories/transferTransactionsFactory.ts b/src/transactionsFactories/transferTransactionsFactory.ts deleted file mode 100644 index 84dcf2a63..000000000 --- a/src/transactionsFactories/transferTransactionsFactory.ts +++ /dev/null @@ -1,425 +0,0 @@ -import { EGLD_IDENTIFIER_FOR_MULTI_ESDTNFT_TRANSFER } from "../constants"; -import { Err, ErrBadUsage } from "../errors"; -import { - IAddress, - IChainID, - IGasLimit, - IGasPrice, - INonce, - ITokenTransfer, - ITransactionPayload, - ITransactionValue, -} from "../interface"; -import { - AddressValue, - ArgSerializer, - BigUIntValue, - BytesValue, - TypedValue, - U16Value, - U64Value, -} from "../smartcontracts"; -import { TokenComputer, TokenTransfer } from "../tokens"; -import { Transaction } from "../transaction"; -import { TransactionPayload } from "../transactionPayload"; -import { TokenTransfersDataBuilder } from "./tokenTransfersDataBuilder"; -import { TransactionBuilder } from "./transactionBuilder"; - -const ADDITIONAL_GAS_FOR_ESDT_TRANSFER = 100000; -const ADDITIONAL_GAS_FOR_ESDT_NFT_TRANSFER = 800000; - -interface IConfig { - chainID: string; - minGasLimit: bigint; - gasLimitPerByte: bigint; - gasLimitESDTTransfer: bigint; - gasLimitESDTNFTTransfer: bigint; - gasLimitMultiESDTNFTTransfer: bigint; -} - -interface IGasEstimator { - forEGLDTransfer(dataLength: number): number; - forESDTTransfer(dataLength: number): number; - forESDTNFTTransfer(dataLength: number): number; - forMultiESDTNFTTransfer(dataLength: number, numTransfers: number): number; -} - -/** - * Use this class to create transactions for native token transfers (EGLD) or custom tokens transfers (ESDT/NTF/MetaESDT). - */ -export class TransferTransactionsFactory { - private readonly config?: IConfig; - private readonly tokenTransfersDataBuilder?: TokenTransfersDataBuilder; - private readonly tokenComputer?: TokenComputer; - private readonly gasEstimator?: IGasEstimator; - - /** - * Should be instantiated using `Config`. - * Instantiating this class using GasEstimator represents the legacy version of this class. - * The legacy version contains methods like `createEGLDTransfer`, `createESDTTransfer`, `createESDTNFTTransfer` and `createMultiESDTNFTTransfer`. - * This was done in order to minimize breaking changes in client code. - */ - constructor(options: IGasEstimator | { config: IConfig }) { - if (this.isGasEstimator(options)) { - this.gasEstimator = options; - } else { - this.config = options.config; - this.tokenComputer = new TokenComputer(); - this.tokenTransfersDataBuilder = new TokenTransfersDataBuilder(); - } - } - - private isGasEstimator(options: any): options is IGasEstimator { - return ( - typeof options === "object" && - typeof options.forEGLDTransfer === "function" && - typeof options.forESDTTransfer === "function" && - typeof options.forESDTNFTTransfer === "function" && - typeof options.forMultiESDTNFTTransfer === "function" - ); - } - - private isGasEstimatorDefined(): boolean { - return this.gasEstimator !== undefined; - } - - private ensureConfigIsDefined() { - if (this.config === undefined) { - throw new Err("'config' is not defined"); - } - } - - createTransactionForNativeTokenTransfer(options: { - sender: IAddress; - receiver: IAddress; - nativeAmount: bigint; - data?: Uint8Array; - }): Transaction { - this.ensureConfigIsDefined(); - - const data = options.data || new Uint8Array(); - - return new Transaction({ - sender: options.sender.bech32(), - receiver: options.receiver.bech32(), - chainID: this.config!.chainID, - gasLimit: this.computeGasForMoveBalance(this.config!, data), - data: data, - value: options.nativeAmount, - }); - } - - createTransactionForESDTTokenTransfer(options: { - sender: IAddress; - receiver: IAddress; - tokenTransfers: TokenTransfer[]; - }): Transaction { - this.ensureConfigIsDefined(); - - const numberOfTransfers = options.tokenTransfers.length; - - if (numberOfTransfers === 0) { - throw new ErrBadUsage("No token transfer has been provided"); - } - - if (numberOfTransfers === 1) { - return this.createSingleESDTTransferTransaction(options); - } - - const { dataParts, extraGasForTransfer } = this.buildMultiESDTNFTTransferData( - options.tokenTransfers, - options.receiver, - ); - - return new TransactionBuilder({ - config: this.config!, - sender: options.sender, - receiver: options.sender, - dataParts: dataParts, - gasLimit: extraGasForTransfer, - addDataMovementGas: true, - }).build(); - } - - createTransactionForTransfer(options: { - sender: IAddress; - receiver: IAddress; - nativeAmount?: bigint; - tokenTransfers?: TokenTransfer[]; - data?: Uint8Array; - }): Transaction { - const nativeAmount = options.nativeAmount ?? 0n; - let tokenTransfers = options.tokenTransfers ? [...options.tokenTransfers] : []; - const numberOfTokens = tokenTransfers.length; - - if (numberOfTokens && options.data?.length) { - throw new ErrBadUsage("Can't set data field when sending esdt tokens"); - } - - if ((nativeAmount && numberOfTokens === 0) || options.data) { - return this.createTransactionForNativeTokenTransfer({ - sender: options.sender, - receiver: options.receiver, - nativeAmount: nativeAmount, - data: options.data, - }); - } - - const nativeTransfer = nativeAmount ? TokenTransfer.newFromEgldAmount(nativeAmount) : undefined; - if (nativeTransfer) { - tokenTransfers.push(nativeTransfer); - } - - return this.createTransactionForESDTTokenTransfer({ - sender: options.sender, - receiver: options.receiver, - tokenTransfers: tokenTransfers, - }); - } - - /** - * This is a legacy method. Can only be used if the class was instantiated using `GasEstimator`. - * Use {@link createTransactionForNativeTokenTransfer} instead. - */ - createEGLDTransfer(args: { - nonce?: INonce; - value: ITransactionValue; - receiver: IAddress; - sender: IAddress; - gasPrice?: IGasPrice; - gasLimit?: IGasLimit; - data?: ITransactionPayload; - chainID: IChainID; - }) { - if (!this.isGasEstimatorDefined()) { - throw new Err( - "You are calling a legacy function to create an EGLD transfer transaction. If this is your intent, then instantiate the class using a `GasEstimator`. Or, instead, use the new, recommended `createTransactionForNativeTokenTransfer` method.", - ); - } - - const dataLength = args.data?.length() || 0; - const estimatedGasLimit = this.gasEstimator!.forEGLDTransfer(dataLength); - - return new Transaction({ - nonce: args.nonce, - value: args.value, - receiver: args.receiver, - sender: args.sender, - gasPrice: args.gasPrice, - gasLimit: args.gasLimit || estimatedGasLimit, - data: args.data, - chainID: args.chainID, - }); - } - - /** - * This is a legacy method. Can only be used if the class was instantiated using `GasEstimator`. - * Use {@link createTransactionForESDTTokenTransfer} instead. - */ - createESDTTransfer(args: { - tokenTransfer: ITokenTransfer; - nonce?: INonce; - receiver: IAddress; - sender: IAddress; - gasPrice?: IGasPrice; - gasLimit?: IGasLimit; - chainID: IChainID; - }) { - if (!this.isGasEstimatorDefined()) { - throw new Err( - "You are calling a legacy function to create an ESDT transfer transaction. If this is your intent, then instantiate the class using a `GasEstimator`. Or, instead, use the new, recommended `createTransactionForESDTTokenTransfer` method.", - ); - } - - const { argumentsString } = new ArgSerializer().valuesToString([ - // The token identifier - BytesValue.fromUTF8(args.tokenTransfer.tokenIdentifier), - // The transfered amount - new BigUIntValue(args.tokenTransfer.valueOf()), - ]); - - const data = `ESDTTransfer@${argumentsString}`; - const transactionPayload = new TransactionPayload(data); - const dataLength = transactionPayload.length() || 0; - const estimatedGasLimit = this.gasEstimator!.forESDTTransfer(dataLength); - - return new Transaction({ - nonce: args.nonce, - receiver: args.receiver, - sender: args.sender, - gasPrice: args.gasPrice, - gasLimit: args.gasLimit || estimatedGasLimit, - data: transactionPayload, - chainID: args.chainID, - }); - } - - /** - * This is a legacy method. Can only be used if the class was instantiated using `GasEstimator`. - * Use {@link createTransactionForESDTTokenTransfer} instead. - */ - createESDTNFTTransfer(args: { - tokenTransfer: ITokenTransfer; - nonce?: INonce; - destination: IAddress; - sender: IAddress; - gasPrice?: IGasPrice; - gasLimit?: IGasLimit; - chainID: IChainID; - }) { - if (!this.isGasEstimatorDefined()) { - throw new Err( - "You are calling a legacy function to create an ESDTNFT transfer transaction. If this is your intent, then instantiate the class using a `GasEstimator`. Or, instead, use the new, recommended `createTransactionForESDTTokenTransfer` method.", - ); - } - - const { argumentsString } = new ArgSerializer().valuesToString([ - // The token identifier - BytesValue.fromUTF8(args.tokenTransfer.tokenIdentifier), - // The nonce of the token - new U64Value(args.tokenTransfer.nonce), - // The transferred quantity - new BigUIntValue(args.tokenTransfer.valueOf()), - // The destination address - new AddressValue(args.destination), - ]); - - const data = `ESDTNFTTransfer@${argumentsString}`; - const transactionPayload = new TransactionPayload(data); - const dataLength = transactionPayload.length() || 0; - const estimatedGasLimit = this.gasEstimator!.forESDTNFTTransfer(dataLength); - - return new Transaction({ - nonce: args.nonce, - receiver: args.sender, - sender: args.sender, - gasPrice: args.gasPrice, - gasLimit: args.gasLimit || estimatedGasLimit, - data: transactionPayload, - chainID: args.chainID, - }); - } - - /** - * This is a legacy method. Can only be used if the class was instantiated using `GasEstimator`. - * Use {@link createTransactionForESDTTokenTransfer} instead. - */ - createMultiESDTNFTTransfer(args: { - tokenTransfers: ITokenTransfer[]; - nonce?: INonce; - destination: IAddress; - sender: IAddress; - gasPrice?: IGasPrice; - gasLimit?: IGasLimit; - chainID: IChainID; - }) { - if (!this.isGasEstimatorDefined()) { - throw new Err( - "You are calling a legacy function to create a MultiESDTNFT transfer transaction. If this is your intent, then instantiate the class using a `GasEstimator`. Or, instead, use the new, recommended `createTransactionForESDTTokenTransfer` method.", - ); - } - - const parts: TypedValue[] = [ - // The destination address - new AddressValue(args.destination), - // Number of tokens - new U16Value(args.tokenTransfers.length), - ]; - - for (const payment of args.tokenTransfers) { - parts.push( - ...[ - // The token identifier - BytesValue.fromUTF8(payment.tokenIdentifier), - // The nonce of the token - new U64Value(payment.nonce), - // The transfered quantity - new BigUIntValue(payment.valueOf()), - ], - ); - } - - const { argumentsString } = new ArgSerializer().valuesToString(parts); - const data = `MultiESDTNFTTransfer@${argumentsString}`; - const transactionPayload = new TransactionPayload(data); - const dataLength = transactionPayload.length() || 0; - const estimatedGasLimit = this.gasEstimator!.forMultiESDTNFTTransfer(dataLength, args.tokenTransfers.length); - - return new Transaction({ - nonce: args.nonce, - receiver: args.sender, - sender: args.sender, - gasPrice: args.gasPrice, - gasLimit: args.gasLimit || estimatedGasLimit, - data: transactionPayload, - chainID: args.chainID, - }); - } - - private createSingleESDTTransferTransaction(options: { - sender: IAddress; - receiver: IAddress; - tokenTransfers: TokenTransfer[]; - }): Transaction { - this.ensureConfigIsDefined(); - - const transfer = options.tokenTransfers[0]; - - const { dataParts, extraGasForTransfer, receiver } = this.buildTransferData(transfer, options); - - return new TransactionBuilder({ - config: this.config!, - sender: options.sender, - receiver: receiver, - dataParts: dataParts, - gasLimit: extraGasForTransfer, - addDataMovementGas: true, - }).build(); - } - - private buildTransferData(transfer: TokenTransfer, options: { sender: IAddress; receiver: IAddress }) { - let dataParts: string[] = []; - let extraGasForTransfer: bigint; - let receiver = options.receiver; - - if (this.tokenComputer!.isFungible(transfer.token)) { - if (transfer.token.identifier === EGLD_IDENTIFIER_FOR_MULTI_ESDTNFT_TRANSFER) { - ({ dataParts, extraGasForTransfer } = this.buildMultiESDTNFTTransferData([transfer], receiver)); - receiver = options.sender; - } else { - ({ dataParts, extraGasForTransfer } = this.buildESDTTransferData(transfer)); - } - } else { - ({ dataParts, extraGasForTransfer } = this.buildSingleESDTNFTTransferData(transfer, receiver)); - receiver = options.sender; // Override receiver for non-fungible tokens - } - return { dataParts, extraGasForTransfer, receiver }; - } - - private buildMultiESDTNFTTransferData(transfer: TokenTransfer[], receiver: IAddress) { - return { - dataParts: this.tokenTransfersDataBuilder!.buildDataPartsForMultiESDTNFTTransfer(receiver, transfer), - extraGasForTransfer: - this.config!.gasLimitMultiESDTNFTTransfer * BigInt(transfer.length) + - BigInt(ADDITIONAL_GAS_FOR_ESDT_NFT_TRANSFER), - }; - } - - private buildESDTTransferData(transfer: TokenTransfer) { - return { - dataParts: this.tokenTransfersDataBuilder!.buildDataPartsForESDTTransfer(transfer), - extraGasForTransfer: this.config!.gasLimitESDTTransfer + BigInt(ADDITIONAL_GAS_FOR_ESDT_TRANSFER), - }; - } - - private buildSingleESDTNFTTransferData(transfer: TokenTransfer, receiver: IAddress) { - return { - dataParts: this.tokenTransfersDataBuilder!.buildDataPartsForSingleESDTNFTTransfer(transfer, receiver), - extraGasForTransfer: this.config!.gasLimitESDTNFTTransfer + BigInt(ADDITIONAL_GAS_FOR_ESDT_NFT_TRANSFER), - }; - } - - private computeGasForMoveBalance(config: IConfig, data: Uint8Array): bigint { - return config.minGasLimit + config.gasLimitPerByte * BigInt(data.length); - } -} diff --git a/src/transactionsOutcomeParsers/delegationTransactionsOutcomeParser.ts b/src/transactionsOutcomeParsers/delegationTransactionsOutcomeParser.ts deleted file mode 100644 index b7e9a0a03..000000000 --- a/src/transactionsOutcomeParsers/delegationTransactionsOutcomeParser.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Address } from "../address"; -import { TransactionsConverter } from "../converters/transactionsConverter"; -import { ErrParseTransactionOutcome } from "../errors"; -import { ITransactionOnNetwork } from "../interfaceOfNetwork"; -import { TransactionEvent, TransactionOutcome, findEventsByIdentifier } from "./resources"; - -export class DelegationTransactionsOutcomeParser { - constructor() {} - - parseCreateNewDelegationContract( - transaction: TransactionOutcome | ITransactionOnNetwork, - ): { contractAddress: string }[] { - transaction = this.ensureTransactionOutcome(transaction); - - this.ensureNoError(transaction.logs.events); - - const events = findEventsByIdentifier(transaction, "SCDeploy"); - - return events.map((event) => ({ contractAddress: this.extractContractAddress(event) })); - } - - /** - * Temporary workaround, until "TransactionOnNetwork" completely replaces "TransactionOutcome". - */ - private ensureTransactionOutcome(transaction: TransactionOutcome | ITransactionOnNetwork): TransactionOutcome { - if ("hash" in transaction) { - return new TransactionsConverter().transactionOnNetworkToOutcome(transaction); - } - - return transaction; - } - - private ensureNoError(transactionEvents: TransactionEvent[]) { - for (const event of transactionEvents) { - if (event.identifier == "signalError") { - const data = Buffer.from(event.dataItems[0]?.toString().slice(1)).toString() || ""; - const message = this.decodeTopicAsString(event.topics[1]); - - throw new ErrParseTransactionOutcome( - `encountered signalError: ${message} (${Buffer.from(data, "hex").toString()})`, - ); - } - } - } - - private extractContractAddress(event: TransactionEvent): string { - if (!event.topics[0]?.length) { - return ""; - } - const address = Buffer.from(event.topics[0]); - return Address.fromBuffer(address).bech32(); - } - - private decodeTopicAsString(topic: Uint8Array): string { - return Buffer.from(topic).toString(); - } -} diff --git a/src/transactionsOutcomeParsers/index.ts b/src/transactionsOutcomeParsers/index.ts index b50f8ba9c..e3f3bafff 100644 --- a/src/transactionsOutcomeParsers/index.ts +++ b/src/transactionsOutcomeParsers/index.ts @@ -1,5 +1,4 @@ -export * from "./delegationTransactionsOutcomeParser"; +export * from "../smartContracts/smartContractTransactionsOutcomeParser"; +export * from "../tokenManagement/tokenManagementTransactionsOutcomeParser"; export * from "./resources"; -export * from "./smartContractTransactionsOutcomeParser"; -export * from "./tokenManagementTransactionsOutcomeParser"; export * from "./transactionEventsParser"; diff --git a/src/transactionsOutcomeParsers/resources.spec.ts b/src/transactionsOutcomeParsers/resources.spec.ts index 1d8fbd8ea..c02d567cd 100644 --- a/src/transactionsOutcomeParsers/resources.spec.ts +++ b/src/transactionsOutcomeParsers/resources.spec.ts @@ -1,16 +1,10 @@ import { assert } from "chai"; -import { - SmartContractResult, - TransactionEvent, - TransactionLogs, - TransactionOutcome, - findEventsByFirstTopic, - findEventsByIdentifier, -} from "./resources"; +import { TransactionEvent, TransactionLogs, TransactionOnNetwork } from "../core"; +import { findEventsByFirstTopic, findEventsByIdentifier, SmartContractResult } from "./resources"; describe("test resources", () => { it("finds events by identifier, by first topic", async function () { - const outcome = new TransactionOutcome({ + const outcome = new TransactionOnNetwork({ logs: new TransactionLogs({ events: [ new TransactionEvent({ diff --git a/src/transactionsOutcomeParsers/resources.ts b/src/transactionsOutcomeParsers/resources.ts index b30bd2f1c..d47382038 100644 --- a/src/transactionsOutcomeParsers/resources.ts +++ b/src/transactionsOutcomeParsers/resources.ts @@ -1,56 +1,21 @@ -export class TransactionEvent { - address: string; - identifier: string; - topics: Uint8Array[]; - dataItems: Uint8Array[]; - - constructor(init: Partial) { - this.address = ""; - this.identifier = ""; - this.topics = []; - this.dataItems = []; - - Object.assign(this, init); - } -} - -export class TransactionLogs { - address: string; - events: TransactionEvent[]; - - constructor(init: Partial) { - this.address = ""; - this.events = []; - - Object.assign(this, init); - } -} +import { Address } from "../core/address"; +import { TransactionEvent } from "../core/transactionEvents"; +import { TransactionLogs } from "../core/transactionLogs"; +import { TransactionOnNetwork } from "../core/transactionOnNetwork"; export class SmartContractResult { - sender: string; - receiver: string; + raw: Record; + sender: Address; + receiver: Address; data: Uint8Array; logs: TransactionLogs; constructor(init: Partial) { - this.sender = ""; - this.receiver = ""; + this.sender = Address.empty(); + this.receiver = Address.empty(); this.data = new Uint8Array(); this.logs = new TransactionLogs({}); - - Object.assign(this, init); - } -} - -export class TransactionOutcome { - directSmartContractCallOutcome: SmartContractCallOutcome; - smartContractResults: SmartContractResult[]; - logs: TransactionLogs; - - constructor(init: Partial) { - this.directSmartContractCallOutcome = new SmartContractCallOutcome({}); - this.smartContractResults = []; - this.logs = new TransactionLogs({}); + this.raw = {}; Object.assign(this, init); } @@ -73,21 +38,24 @@ export class SmartContractCallOutcome { } export function findEventsByPredicate( - transactionOutcome: TransactionOutcome, + transactionOutcome: TransactionOnNetwork, predicate: (event: TransactionEvent) => boolean, ): TransactionEvent[] { return gatherAllEvents(transactionOutcome).filter(predicate); } -export function findEventsByIdentifier(transactionOutcome: TransactionOutcome, identifier: string): TransactionEvent[] { +export function findEventsByIdentifier( + transactionOutcome: TransactionOnNetwork, + identifier: string, +): TransactionEvent[] { return findEventsByPredicate(transactionOutcome, (event) => event.identifier == identifier); } -export function findEventsByFirstTopic(transactionOutcome: TransactionOutcome, topic: string): TransactionEvent[] { +export function findEventsByFirstTopic(transactionOutcome: TransactionOnNetwork, topic: string): TransactionEvent[] { return findEventsByPredicate(transactionOutcome, (event) => event.topics[0]?.toString() == topic); } -export function gatherAllEvents(transactionOutcome: TransactionOutcome): TransactionEvent[] { +export function gatherAllEvents(transactionOutcome: TransactionOnNetwork): TransactionEvent[] { const allEvents = []; allEvents.push(...transactionOutcome.logs.events); diff --git a/src/transactionsOutcomeParsers/smartContractTransactionsOutcomeParser.spec.ts b/src/transactionsOutcomeParsers/smartContractTransactionsOutcomeParser.spec.ts deleted file mode 100644 index e005f1b39..000000000 --- a/src/transactionsOutcomeParsers/smartContractTransactionsOutcomeParser.spec.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { - ContractResultItem, - ContractResults, - TransactionEventTopic, - TransactionOnNetwork, - TransactionEventOnNetwork, - TransactionLogsOnNetwork, -} from "../networkProviders"; -import BigNumber from "bignumber.js"; -import { assert } from "chai"; -import { Address } from "../address"; -import { TransactionsConverter } from "../converters/transactionsConverter"; -import { loadAbiRegistry } from "../testutils"; -import { SmartContractCallOutcome, TransactionEvent, TransactionLogs, TransactionOutcome } from "./resources"; -import { SmartContractTransactionsOutcomeParser } from "./smartContractTransactionsOutcomeParser"; - -describe("test smart contract transactions outcome parser", () => { - it("parses deploy outcome (minimalistic)", async function () { - const contract = Address.fromBech32("erd1qqqqqqqqqqqqqpgqqacl85rd0gl2q8wggl8pwcyzcr4fflc5d8ssve45cj"); - const deployer = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - const codeHash = Buffer.from("abba", "hex"); - - const parser = new SmartContractTransactionsOutcomeParser(); - - const parsed = parser.parseDeploy({ - transactionOutcome: new TransactionOutcome({ - directSmartContractCallOutcome: new SmartContractCallOutcome({ - returnCode: "ok", - returnMessage: "ok", - }), - logs: new TransactionLogs({ - events: [ - new TransactionEvent({ - identifier: "SCDeploy", - topics: [contract.getPublicKey(), deployer.getPublicKey(), codeHash], - }), - ], - }), - }), - }); - - assert.equal(parsed.returnCode, "ok"); - assert.equal(parsed.returnMessage, "ok"); - assert.deepEqual(parsed.contracts, [ - { - address: contract.toBech32(), - ownerAddress: deployer.toBech32(), - codeHash: codeHash, - }, - ]); - }); - - it("parses deploy outcome", async function () { - const contract = Address.fromBech32("erd1qqqqqqqqqqqqqpgqqacl85rd0gl2q8wggl8pwcyzcr4fflc5d8ssve45cj"); - const deployer = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - const codeHash = Buffer.from("abba", "hex"); - - const parser = new SmartContractTransactionsOutcomeParser(); - const transactionsConverter = new TransactionsConverter(); - - const transactionOnNetwork = new TransactionOnNetwork({ - nonce: 7, - logs: new TransactionLogsOnNetwork({ - events: [ - new TransactionEventOnNetwork({ - identifier: "SCDeploy", - topics: [ - new TransactionEventTopic(contract.getPublicKey().toString("base64")), - new TransactionEventTopic(deployer.getPublicKey().toString("base64")), - new TransactionEventTopic(codeHash.toString("base64")), - ], - }), - ], - }), - contractResults: new ContractResults([ - new ContractResultItem({ - nonce: 8, - data: "@6f6b", - }), - ]), - }); - - const transactionOutcome = transactionsConverter.transactionOnNetworkToOutcome(transactionOnNetwork); - const parsed = parser.parseDeploy({ transactionOutcome }); - - assert.equal(parsed.returnCode, "ok"); - assert.equal(parsed.returnMessage, "ok"); - assert.deepEqual(parsed.contracts, [ - { - address: contract.toBech32(), - ownerAddress: deployer.toBech32(), - codeHash: codeHash, - }, - ]); - }); - - it("parses deploy outcome (with error)", async function () { - const deployer = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - - const parser = new SmartContractTransactionsOutcomeParser(); - const transactionsConverter = new TransactionsConverter(); - - const transactionOnNetwork = new TransactionOnNetwork({ - nonce: 7, - logs: new TransactionLogsOnNetwork({ - events: [ - new TransactionEventOnNetwork({ - identifier: "signalError", - topics: [ - new TransactionEventTopic(deployer.getPublicKey().toString("base64")), - new TransactionEventTopic(Buffer.from("wrong number of arguments").toString("base64")), - ], - data: "@75736572206572726f72", - }), - ], - }), - }); - - const transactionOutcome = transactionsConverter.transactionOnNetworkToOutcome(transactionOnNetwork); - const parsed = parser.parseDeploy({ transactionOutcome }); - - assert.equal(parsed.returnCode, "user error"); - assert.equal(parsed.returnMessage, "wrong number of arguments"); - assert.deepEqual(parsed.contracts, []); - }); - - it("parses execute outcome, without ABI (minimalistic)", function () { - const parser = new SmartContractTransactionsOutcomeParser(); - - const parsed = parser.parseExecute({ - transactionOutcome: new TransactionOutcome({ - directSmartContractCallOutcome: new SmartContractCallOutcome({ - function: "hello", - returnCode: "ok", - returnMessage: "ok", - returnDataParts: [Buffer.from([42])], - }), - }), - }); - - assert.deepEqual(parsed.values, [Buffer.from([42])]); - assert.equal(parsed.returnCode, "ok"); - assert.equal(parsed.returnMessage, "ok"); - }); - - it("parses execute outcome, without ABI", function () { - const parser = new SmartContractTransactionsOutcomeParser(); - const transactionsConverter = new TransactionsConverter(); - const transactionOnNetwork = new TransactionOnNetwork({ - nonce: 7, - contractResults: new ContractResults([ - new ContractResultItem({ - nonce: 8, - data: "@6f6b@2a", - }), - ]), - }); - - const transactionOutcome = transactionsConverter.transactionOnNetworkToOutcome(transactionOnNetwork); - - const parsed = parser.parseExecute({ transactionOutcome }); - - assert.deepEqual(parsed.values, [Buffer.from([42])]); - assert.equal(parsed.returnCode, "ok"); - assert.equal(parsed.returnMessage, "ok"); - }); - - it("parses execute outcome, with ABI (minimalistic)", async function () { - const parser = new SmartContractTransactionsOutcomeParser({ - abi: await loadAbiRegistry("src/testdata/answer.abi.json"), - }); - - const parsed = parser.parseExecute({ - transactionOutcome: new TransactionOutcome({ - directSmartContractCallOutcome: new SmartContractCallOutcome({ - // For the sake of the test, let's say that we've called this function as a transaction, not as a query. - function: "getUltimateAnswer", - returnCode: "ok", - returnMessage: "ok", - returnDataParts: [Buffer.from([42])], - }), - }), - }); - - // At this moment, U64Value.valueOf() returns a BigNumber. This might change in the future. - assert.deepEqual(parsed.values, [new BigNumber("42")]); - assert.equal(parsed.returnCode, "ok"); - assert.equal(parsed.returnMessage, "ok"); - }); - - it("parses execute outcome, with ABI", async function () { - const parser = new SmartContractTransactionsOutcomeParser({ - abi: await loadAbiRegistry("src/testdata/answer.abi.json"), - }); - - const transactionsConverter = new TransactionsConverter(); - const transactionOnNetwork = new TransactionOnNetwork({ - nonce: 7, - function: "getUltimateAnswer", - contractResults: new ContractResults([ - new ContractResultItem({ - nonce: 8, - data: "@6f6b@2a", - }), - ]), - }); - - const transactionOutcome = transactionsConverter.transactionOnNetworkToOutcome(transactionOnNetwork); - const parsed = parser.parseExecute({ transactionOutcome }); - - // At this moment, U64Value.valueOf() returns a BigNumber. This might change in the future. - assert.deepEqual(parsed.values, [new BigNumber("42")]); - assert.equal(parsed.returnCode, "ok"); - assert.equal(parsed.returnMessage, "ok"); - }); - - it("cannot parse execute outcome, with ABI, when function name is missing", async function () { - const parser = new SmartContractTransactionsOutcomeParser({ - abi: await loadAbiRegistry("src/testdata/answer.abi.json"), - }); - - const transactionsConverter = new TransactionsConverter(); - const transactionOnNetwork = new TransactionOnNetwork({ - nonce: 7, - contractResults: new ContractResults([ - new ContractResultItem({ - nonce: 8, - data: "@6f6b@2a", - }), - ]), - }); - - const transactionOutcome = transactionsConverter.transactionOnNetworkToOutcome(transactionOnNetwork); - - assert.throws(() => { - parser.parseExecute({ transactionOutcome }); - }, 'Function name is not available in the transaction outcome, thus endpoint definition (ABI) cannot be picked (for parsing). Maybe provide the "function" parameter explicitly?'); - }); -}); diff --git a/src/transactionsOutcomeParsers/tokenManagementTransactionsOutcomeParser.spec.ts b/src/transactionsOutcomeParsers/tokenManagementTransactionsOutcomeParser.spec.ts deleted file mode 100644 index 136fe6d5b..000000000 --- a/src/transactionsOutcomeParsers/tokenManagementTransactionsOutcomeParser.spec.ts +++ /dev/null @@ -1,596 +0,0 @@ -import { assert } from "chai"; -import { ErrParseTransactionOutcome } from "../errors"; -import { b64TopicsToBytes } from "../testutils"; -import { SmartContractResult, TransactionEvent, TransactionLogs, TransactionOutcome } from "./resources"; -import { TokenManagementTransactionsOutcomeParser } from "./tokenManagementTransactionsOutcomeParser"; - -describe("test token management transactions outcome parser", () => { - const parser = new TokenManagementTransactionsOutcomeParser(); - - it("should test ensure error", () => { - const encodedTopics = ["Avk0jZ1kR+l9c76wQQoYcu4hvXPz+jxxTdqQeaCrbX8=", "dGlja2VyIG5hbWUgaXMgbm90IHZhbGlk"]; - const event = new TransactionEvent({ - address: "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u", - identifier: "signalError", - topics: b64TopicsToBytes(encodedTopics), - dataItems: [Buffer.from("QDc1NzM2NTcyMjA2NTcyNzI2Zjcy", "base64")], - }); - - const logs = new TransactionLogs({ events: [event] }); - const txOutcome = new TransactionOutcome({ logs: logs }); - - assert.throws( - () => { - parser.parseIssueFungible(txOutcome); - }, - ErrParseTransactionOutcome, - "encountered signalError: ticker name is not valid (user error)", - ); - }); - - it("should test parse issue fungible", () => { - const identifier = "ZZZ-9ee87d"; - const base64Identifier = Buffer.from(identifier).toString("base64"); - - const encodedTopics = [base64Identifier, "U0VDT05E", "Wlpa", "RnVuZ2libGVFU0RU", "Ag=="]; - const event = new TransactionEvent({ - address: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", - identifier: "issue", - topics: b64TopicsToBytes(encodedTopics), - }); - - const logs = new TransactionLogs({ - address: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", - events: [event], - }); - - const txOutcome = new TransactionOutcome({ logs: logs }); - - const outcome = parser.parseIssueFungible(txOutcome); - assert.lengthOf(outcome, 1); - assert.equal(outcome[0].tokenIdentifier, identifier); - }); - - it("should test parse issue non fungible", () => { - const identifier = "NFT-f01d1e"; - const base64Identifier = Buffer.from(identifier).toString("base64"); - - let encodedTopics = [ - "TkZULWYwMWQxZQ==", - "", - "Y2FuVXBncmFkZQ==", - "dHJ1ZQ==", - "Y2FuQWRkU3BlY2lhbFJvbGVz", - "dHJ1ZQ==", - ]; - const firstEvent = new TransactionEvent({ - address: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", - identifier: "upgradeProperties", - topics: b64TopicsToBytes(encodedTopics), - }); - - encodedTopics = ["TkZULWYwMWQxZQ==", "", "", "RVNEVFJvbGVCdXJuRm9yQWxs"]; - const secondEvent = new TransactionEvent({ - address: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", - identifier: "ESDTSetBurnRoleForAll", - topics: b64TopicsToBytes(encodedTopics), - }); - - encodedTopics = [base64Identifier, "TkZURVNU", "TkZU", "Tm9uRnVuZ2libGVFU0RU"]; - const thirdEvent = new TransactionEvent({ - address: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", - identifier: "issueNonFungible", - topics: b64TopicsToBytes(encodedTopics), - }); - - const logs = new TransactionLogs({ - address: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", - events: [firstEvent, secondEvent, thirdEvent], - }); - - const txOutcome = new TransactionOutcome({ logs: logs }); - - const outcome = parser.parseIssueNonFungible(txOutcome); - assert.lengthOf(outcome, 1); - assert.equal(outcome[0].tokenIdentifier, identifier); - }); - - it("should test parse issue semi fungible", () => { - const identifier = "SEMIFNG-2c6d9f"; - const base64Identifier = Buffer.from(identifier).toString("base64"); - - const encodedTopics = [base64Identifier, "U0VNSQ==", "U0VNSUZORw==", "U2VtaUZ1bmdpYmxlRVNEVA=="]; - const event = new TransactionEvent({ - address: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", - identifier: "issueSemiFungible", - topics: b64TopicsToBytes(encodedTopics), - }); - - const logs = new TransactionLogs({ - address: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", - events: [event], - }); - - const txOutcome = new TransactionOutcome({ logs: logs }); - - const outcome = parser.parseIssueSemiFungible(txOutcome); - assert.lengthOf(outcome, 1); - assert.equal(outcome[0].tokenIdentifier, identifier); - }); - - it("should test parse register meta esdt", () => { - const identifier = "METATEST-e05d11"; - const base64Identifier = Buffer.from(identifier).toString("base64"); - - const encodedTopics = [base64Identifier, "TUVURVNU", "TUVUQVRFU1Q=", "TWV0YUVTRFQ="]; - const event = new TransactionEvent({ - address: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", - identifier: "registerMetaESDT", - topics: b64TopicsToBytes(encodedTopics), - }); - - const logs = new TransactionLogs({ - address: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", - events: [event], - }); - - const txOutcome = new TransactionOutcome({ logs: logs }); - - const outcome = parser.parseRegisterMetaEsdt(txOutcome); - assert.lengthOf(outcome, 1); - assert.equal(outcome[0].tokenIdentifier, identifier); - }); - - it("should test parse register and set all roles", () => { - const firstIdentifier = "LMAO-d9f892"; - const firstBase64Identifier = Buffer.from(firstIdentifier).toString("base64"); - - const secondIdentifier = "TST-123456"; - const secondBase64Identifier = Buffer.from(secondIdentifier).toString("base64"); - - const roles = ["ESDTRoleLocalMint", "ESDTRoleLocalBurn"]; - - let encodedTopics = [firstBase64Identifier, "TE1BTw==", "TE1BTw==", "RnVuZ2libGVFU0RU", "Ag=="]; - const firstEvent = new TransactionEvent({ - address: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", - identifier: "registerAndSetAllRoles", - topics: b64TopicsToBytes(encodedTopics), - }); - - encodedTopics = [secondBase64Identifier, "TE1BTw==", "TE1BTw==", "RnVuZ2libGVFU0RU", "Ag=="]; - const secondEvent = new TransactionEvent({ - address: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", - identifier: "registerAndSetAllRoles", - topics: b64TopicsToBytes(encodedTopics), - }); - - const transactionLogs = new TransactionLogs({ - address: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", - events: [firstEvent, secondEvent], - }); - - encodedTopics = ["TE1BTy1kOWY4OTI=", "", "", "RVNEVFJvbGVMb2NhbE1pbnQ=", "RVNEVFJvbGVMb2NhbEJ1cm4="]; - const firstResultEvent = new TransactionEvent({ - address: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", - identifier: "ESDTSetRole", - topics: b64TopicsToBytes(encodedTopics), - }); - - encodedTopics = ["VFNULTEyMzQ1Ng==", "", "", "RVNEVFJvbGVMb2NhbE1pbnQ=", "RVNEVFJvbGVMb2NhbEJ1cm4="]; - const secondResultEvent = new TransactionEvent({ - address: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", - identifier: "ESDTSetRole", - topics: b64TopicsToBytes(encodedTopics), - }); - - const resultLogs = new TransactionLogs({ - address: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", - events: [firstResultEvent, secondResultEvent], - }); - - const scResult = new SmartContractResult({ - sender: "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u", - receiver: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", - data: Buffer.from( - "RVNEVFNldFJvbGVANGM0ZDQxNGYyZDY0Mzk2NjM4MzkzMkA0NTUzNDQ1NDUyNmY2YzY1NGM2ZjYzNjE2YzRkNjk2ZTc0QDQ1NTM0NDU0NTI2ZjZjNjU0YzZmNjM2MTZjNDI3NTcyNmU=", - "base64", - ), - logs: resultLogs, - }); - - const txOutcome = new TransactionOutcome({ - smartContractResults: [scResult], - logs: transactionLogs, - }); - - const outcome = parser.parseRegisterAndSetAllRoles(txOutcome); - assert.lengthOf(outcome, 2); - - assert.equal(outcome[0].tokenIdentifier, firstIdentifier); - assert.deepEqual(outcome[0].roles, roles); - - assert.equal(outcome[1].tokenIdentifier, secondIdentifier); - assert.deepEqual(outcome[1].roles, roles); - }); - - it("should test parse register set special role", () => { - const identifier = "METATEST-e05d11"; - const base64Identifier = Buffer.from(identifier).toString("base64"); - const roles = ["ESDTRoleNFTCreate", "ESDTRoleNFTAddQuantity", "ESDTRoleNFTBurn"]; - - const encodedTopics = [ - base64Identifier, - "", - "", - "RVNEVFJvbGVORlRDcmVhdGU=", - "RVNEVFJvbGVORlRBZGRRdWFudGl0eQ==", - "RVNEVFJvbGVORlRCdXJu", - ]; - const event = new TransactionEvent({ - address: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", - identifier: "ESDTSetRole", - topics: b64TopicsToBytes(encodedTopics), - }); - - const transactionLogs = new TransactionLogs({ - address: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", - events: [event], - }); - - const txOutcome = new TransactionOutcome({ - logs: transactionLogs, - }); - - const outcome = parser.parseSetSpecialRole(txOutcome); - assert.lengthOf(outcome, 1); - assert.equal(outcome[0].userAddress, "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2"); - assert.equal(outcome[0].tokenIdentifier, identifier); - assert.deepEqual(outcome[0].roles, roles); - }); - - it("should test parse nft create", () => { - const identifier = "NFT-f01d1e"; - const base64Identifier = Buffer.from(identifier).toString("base64"); - const nonce = BigInt(1); - const initialQuantity = BigInt(1); - - const encodedTopics = [ - base64Identifier, - "AQ==", - "AQ==", - "CAESAgABIuUBCAESCE5GVEZJUlNUGiA8NdfqyxqZpKDMqlN+8MwK4Qn0H2wrQCID5jO/uwcfXCDEEyouUW1ZM3ZKQ3NVcWpNM3hxeGR3VWczemJoVFNMUWZoN0szbW5aWXhyaGNRRFl4RzJDaHR0cHM6Ly9pcGZzLmlvL2lwZnMvUW1ZM3ZKQ3NVcWpNM3hxeGR3VWczemJoVFNMUWZoN0szbW5aWXhyaGNRRFl4Rzo9dGFnczo7bWV0YWRhdGE6UW1SY1A5NGtYcjV6WmpSR3ZpN21KNnVuN0xweFVoWVZSNFI0UnBpY3h6Z1lrdA==", - ]; - const event = new TransactionEvent({ - address: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", - identifier: "ESDTNFTCreate", - topics: b64TopicsToBytes(encodedTopics), - }); - - const transactionLogs = new TransactionLogs({ - address: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", - events: [event], - }); - - const txOutcome = new TransactionOutcome({ - logs: transactionLogs, - }); - - const outcome = parser.parseNftCreate(txOutcome); - assert.lengthOf(outcome, 1); - assert.equal(outcome[0].tokenIdentifier, identifier); - assert.equal(outcome[0].nonce, nonce); - assert.equal(outcome[0].initialQuantity, initialQuantity); - }); - - it("should test parse local mint", () => { - const identifier = "AAA-29c4c9"; - const base64Identifier = Buffer.from(identifier).toString("base64"); - const nonce = BigInt(0); - const mintedSupply = BigInt(100000); - - const encodedTopics = [base64Identifier, "", "AYag"]; - const event = new TransactionEvent({ - address: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", - identifier: "ESDTLocalMint", - topics: b64TopicsToBytes(encodedTopics), - }); - - const transactionLogs = new TransactionLogs({ - address: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", - events: [event], - }); - - const txOutcome = new TransactionOutcome({ - logs: transactionLogs, - }); - - const outcome = parser.parseLocalMint(txOutcome); - assert.lengthOf(outcome, 1); - assert.equal(outcome[0].userAddress, event.address); - assert.equal(outcome[0].tokenIdentifier, identifier); - assert.equal(outcome[0].nonce, nonce); - assert.equal(outcome[0].mintedSupply, mintedSupply); - }); - - it("should test parse local burn", () => { - const identifier = "AAA-29c4c9"; - const base64Identifier = Buffer.from(identifier).toString("base64"); - const nonce = BigInt(0); - const burntSupply = BigInt(100000); - - const encodedTopics = [base64Identifier, "", "AYag"]; - const event = new TransactionEvent({ - address: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", - identifier: "ESDTLocalBurn", - topics: b64TopicsToBytes(encodedTopics), - }); - - const transactionLogs = new TransactionLogs({ - address: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", - events: [event], - }); - - const txOutcome = new TransactionOutcome({ - logs: transactionLogs, - }); - - const outcome = parser.parseLocalBurn(txOutcome); - assert.lengthOf(outcome, 1); - assert.equal(outcome[0].userAddress, event.address); - assert.equal(outcome[0].tokenIdentifier, identifier); - assert.equal(outcome[0].nonce, nonce); - assert.equal(outcome[0].burntSupply, burntSupply); - }); - - it("should test parse pause", () => { - const identifier = "AAA-29c4c9"; - const base64Identifier = Buffer.from(identifier).toString("base64"); - - const encodedTopics = [base64Identifier]; - const event = new TransactionEvent({ - address: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", - identifier: "ESDTPause", - topics: b64TopicsToBytes(encodedTopics), - }); - - const transactionLogs = new TransactionLogs({ - address: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", - events: [event], - }); - - const txOutcome = new TransactionOutcome({ - logs: transactionLogs, - }); - - const outcome = parser.parsePause(txOutcome); - assert.lengthOf(outcome, 1); - assert.equal(outcome[0].tokenIdentifier, identifier); - }); - - it("should test parse unpause", () => { - const identifier = "AAA-29c4c9"; - const base64Identifier = Buffer.from(identifier).toString("base64"); - - const encodedTopics = [base64Identifier]; - const event = new TransactionEvent({ - address: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", - identifier: "ESDTUnPause", - topics: b64TopicsToBytes(encodedTopics), - }); - - const transactionLogs = new TransactionLogs({ - address: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", - events: [event], - }); - - const txOutcome = new TransactionOutcome({ - logs: transactionLogs, - }); - - const outcome = parser.parseUnpause(txOutcome); - assert.lengthOf(outcome, 1); - assert.equal(outcome[0].tokenIdentifier, identifier); - }); - - it("should test parse freeze", () => { - const identifier = "AAA-29c4c9"; - const base64Identifier = Buffer.from(identifier).toString("base64"); - const nonce = BigInt(0); - const balance = BigInt(10000000); - const address = "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"; - - const encodedTopics = [base64Identifier, "", "mJaA", "ATlHLv9ohncamC8wg9pdQh8kwpGB5jiIIo3IHKYNaeE="]; - const event = new TransactionEvent({ - address: "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u", - identifier: "ESDTFreeze", - topics: b64TopicsToBytes(encodedTopics), - }); - - const transactionLogs = new TransactionLogs({ - address: "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", - events: [event], - }); - - const scResult = new SmartContractResult({ - sender: "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u", - receiver: "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", - data: Buffer.from("RVNEVEZyZWV6ZUA0MTQxNDEyZDMyMzk2MzM0NjMzOQ==", "base64"), - logs: transactionLogs, - }); - - const txOutcome = new TransactionOutcome({ - smartContractResults: [scResult], - }); - - const outcome = parser.parseFreeze(txOutcome); - assert.lengthOf(outcome, 1); - assert.equal(outcome[0].userAddress, address); - assert.equal(outcome[0].tokenIdentifier, identifier); - assert.equal(outcome[0].nonce, nonce); - assert.equal(outcome[0].balance, balance); - }); - - it("should test parse unfreeze", () => { - const identifier = "AAA-29c4c9"; - const base64Identifier = Buffer.from(identifier).toString("base64"); - const nonce = BigInt(0); - const balance = BigInt(10000000); - const address = "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"; - - const encodedTopics = [base64Identifier, "", "mJaA", "ATlHLv9ohncamC8wg9pdQh8kwpGB5jiIIo3IHKYNaeE="]; - const event = new TransactionEvent({ - address: "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u", - identifier: "ESDTUnFreeze", - topics: b64TopicsToBytes(encodedTopics), - }); - - const transactionLogs = new TransactionLogs({ - address: "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", - events: [event], - }); - - const scResult = new SmartContractResult({ - sender: "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u", - receiver: "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", - data: Buffer.from("RVNEVEZyZWV6ZUA0MTQxNDEyZDMyMzk2MzM0NjMzOQ==", "base64"), - logs: transactionLogs, - }); - - const txOutcome = new TransactionOutcome({ - smartContractResults: [scResult], - }); - - const outcome = parser.parseUnfreeze(txOutcome); - assert.lengthOf(outcome, 1); - assert.equal(outcome[0].userAddress, address); - assert.equal(outcome[0].tokenIdentifier, identifier); - assert.equal(outcome[0].nonce, nonce); - assert.equal(outcome[0].balance, balance); - }); - - it("should test parse wipe", () => { - const identifier = "AAA-29c4c9"; - const base64Identifier = Buffer.from(identifier).toString("base64"); - const nonce = BigInt(0); - const balance = BigInt(10000000); - const address = "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"; - - const encodedTopics = [base64Identifier, "", "mJaA", "ATlHLv9ohncamC8wg9pdQh8kwpGB5jiIIo3IHKYNaeE="]; - const event = new TransactionEvent({ - address: "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u", - identifier: "ESDTWipe", - topics: b64TopicsToBytes(encodedTopics), - }); - - const transactionLogs = new TransactionLogs({ - address: "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", - events: [event], - }); - - const scResult = new SmartContractResult({ - sender: "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u", - receiver: "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", - data: Buffer.from("RVNEVEZyZWV6ZUA0MTQxNDEyZDMyMzk2MzM0NjMzOQ==", "base64"), - logs: transactionLogs, - }); - - const txOutcome = new TransactionOutcome({ - smartContractResults: [scResult], - }); - - const outcome = parser.parseWipe(txOutcome); - assert.lengthOf(outcome, 1); - assert.equal(outcome[0].userAddress, address); - assert.equal(outcome[0].tokenIdentifier, identifier); - assert.equal(outcome[0].nonce, nonce); - assert.equal(outcome[0].balance, balance); - }); - - it("should test parse update attributes", () => { - const identifier = "NFT-f01d1e"; - const base64Identifier = Buffer.from(identifier).toString("base64"); - const nonce = BigInt(1); - const attributes = "metadata:ipfsCID/test.json;tags:tag1,tag2"; - const base64Attributes = Buffer.from(attributes).toString("base64"); - - const encodedTopics = [base64Identifier, "AQ==", "", base64Attributes]; - const event = new TransactionEvent({ - address: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", - identifier: "ESDTNFTUpdateAttributes", - topics: b64TopicsToBytes(encodedTopics), - }); - - const transactionLogs = new TransactionLogs({ - address: "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", - events: [event], - }); - - const txOutcome = new TransactionOutcome({ - logs: transactionLogs, - }); - - const outcome = parser.parseUpdateAttributes(txOutcome); - assert.lengthOf(outcome, 1); - assert.equal(outcome[0].tokenIdentifier, identifier); - assert.equal(outcome[0].nonce, nonce); - assert.equal(Buffer.from(outcome[0].attributes).toString(), attributes); - }); - - it("should test parse add quantity", () => { - const identifier = "NFT-f01d1e"; - const base64Identifier = Buffer.from(identifier).toString("base64"); - const nonce = BigInt(1); - const addedQuantity = BigInt(10); - - const encodedTopics = [base64Identifier, "AQ==", "Cg=="]; - const event = new TransactionEvent({ - address: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", - identifier: "ESDTNFTAddQuantity", - topics: b64TopicsToBytes(encodedTopics), - }); - - const transactionLogs = new TransactionLogs({ - address: "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", - events: [event], - }); - - const txOutcome = new TransactionOutcome({ - logs: transactionLogs, - }); - - const outcome = parser.parseAddQuantity(txOutcome); - assert.lengthOf(outcome, 1); - assert.equal(outcome[0].tokenIdentifier, identifier); - assert.equal(outcome[0].nonce, nonce); - assert.equal(outcome[0].addedQuantity, addedQuantity); - }); - - it("should test parse burn quantity", () => { - const identifier = "NFT-f01d1e"; - const base64Identifier = Buffer.from(identifier).toString("base64"); - const nonce = BigInt(1); - const burntQuantity = BigInt(16); - - const encodedTopics = [base64Identifier, "AQ==", "EA=="]; - const event = new TransactionEvent({ - address: "erd18s6a06ktr2v6fgxv4ffhauxvptssnaqlds45qgsrucemlwc8rawq553rt2", - identifier: "ESDTNFTBurn", - topics: b64TopicsToBytes(encodedTopics), - }); - - const transactionLogs = new TransactionLogs({ - address: "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", - events: [event], - }); - - const txOutcome = new TransactionOutcome({ - logs: transactionLogs, - }); - - const outcome = parser.parseBurnQuantity(txOutcome); - assert.lengthOf(outcome, 1); - assert.equal(outcome[0].tokenIdentifier, identifier); - assert.equal(outcome[0].nonce, nonce); - assert.equal(outcome[0].burntQuantity, burntQuantity); - }); -}); diff --git a/src/transactionsOutcomeParsers/transactionEventsParser.spec.ts b/src/transactionsOutcomeParsers/transactionEventsParser.spec.ts index cddc8ea3d..ed3676274 100644 --- a/src/transactionsOutcomeParsers/transactionEventsParser.spec.ts +++ b/src/transactionsOutcomeParsers/transactionEventsParser.spec.ts @@ -1,19 +1,9 @@ -import { - ContractResultItem, - ContractResults, - TransactionEventData, - TransactionEventOnNetwork, - TransactionEventTopic, - TransactionLogsOnNetwork, - TransactionOnNetwork, -} from "../networkProviders"; import BigNumber from "bignumber.js"; import { assert } from "chai"; -import { Address } from "../address"; -import { TransactionsConverter } from "../converters/transactionsConverter"; -import { AbiRegistry } from "../smartcontracts"; -import { loadAbiRegistry } from "../testutils"; -import { TransactionEvent, findEventsByFirstTopic } from "./resources"; +import { Abi } from "../abi"; +import { Address, TransactionEvent, TransactionLogs, TransactionOnNetwork } from "../core"; +import { b64TopicsToBytes, loadAbiRegistry } from "../testutils"; +import { findEventsByFirstTopic, SmartContractResult } from "./resources"; import { TransactionEventsParser } from "./transactionEventsParser"; describe("test transaction events parser", () => { @@ -26,7 +16,11 @@ describe("test transaction events parser", () => { events: [ new TransactionEvent({ identifier: "transferOverMaxAmount", - topics: [Buffer.from("transferOverMaxAmount"), Buffer.from([0x2a]), Buffer.from([0x2b])], + topics: b64TopicsToBytes([ + Buffer.from("transferOverMaxAmount").toString("base64"), + Buffer.from([42]).toString("base64"), + Buffer.from([43]).toString("base64"), + ]), }), ], }); @@ -44,37 +38,34 @@ describe("test transaction events parser", () => { abi: await loadAbiRegistry("src/testdata/esdt-safe.abi.json"), }); - const transactionsConverter = new TransactionsConverter(); const transactionOnNetwork = new TransactionOnNetwork({ - nonce: 7, - contractResults: new ContractResults([ - new ContractResultItem({ - nonce: 8, - data: "@6f6b", - logs: new TransactionLogsOnNetwork({ + nonce: 7n, + smartContractResults: [ + new SmartContractResult({ + data: Buffer.from("@6f6b"), + logs: new TransactionLogs({ events: [ - new TransactionEventOnNetwork({ + new TransactionEvent({ identifier: "deposit", - topics: [ - new TransactionEventTopic("ZGVwb3NpdA=="), - new TransactionEventTopic("cmzC1LRt1r10pMhNAnFb+FyudjGMq4G8CefCYdQUmmc="), - new TransactionEventTopic("AAAADFdFR0xELTAxZTQ5ZAAAAAAAAAAAAAAAAWQ="), - ], - dataPayload: new TransactionEventData(Buffer.from("AAAAAAAAA9sAAAA=", "base64")), + topics: b64TopicsToBytes([ + "ZGVwb3NpdA==", + "cmzC1LRt1r10pMhNAnFb+FyudjGMq4G8CefCYdQUmmc=", + "AAAADFdFR0xELTAxZTQ5ZAAAAAAAAAAAAAAAAWQ=", + ]), + additionalData: [Buffer.from("AAAAAAAAA9sAAAA=", "base64")], }), ], }), }), - ]), + ], }); - const transactionOutcome = transactionsConverter.transactionOnNetworkToOutcome(transactionOnNetwork); - const events = findEventsByFirstTopic(transactionOutcome, "deposit"); + const events = findEventsByFirstTopic(transactionOnNetwork, "deposit"); const parsed = parser.parseEvents({ events }); assert.deepEqual(parsed, [ { - dest_address: Address.fromBech32("erd1wfkv9495dhtt6a9yepxsyu2mlpw2ua333j4cr0qfulpxr4q5nfnshgyqun"), + dest_address: Address.newFromBech32("erd1wfkv9495dhtt6a9yepxsyu2mlpw2ua333j4cr0qfulpxr4q5nfnshgyqun"), tokens: [ { token_identifier: "WEGLD-01e49d", @@ -97,33 +88,26 @@ describe("test transaction events parser", () => { abi: await loadAbiRegistry("src/testdata/multisig-full.abi.json"), }); - const transactionsConverter = new TransactionsConverter(); const transactionOnNetwork = new TransactionOnNetwork({ - nonce: 7, - contractResults: new ContractResults([ - new ContractResultItem({ - nonce: 8, - data: "@6f6b", - }), - ]), - logs: new TransactionLogsOnNetwork({ + nonce: 7n, + smartContractResults: [new SmartContractResult({ data: Buffer.from("@6f6b") })], + logs: new TransactionLogs({ events: [ - new TransactionEventOnNetwork({ + new TransactionEvent({ identifier: "performAction", - topics: [new TransactionEventTopic("c3RhcnRQZXJmb3JtQWN0aW9u")], - dataPayload: new TransactionEventData( + topics: b64TopicsToBytes(["c3RhcnRQZXJmb3JtQWN0aW9u"]), + additionalData: [ Buffer.from( "00000001000000000500000000000000000500d006f73c4221216fa679bc559005584c4f1160e569e1000000000000000003616464000000010000000107000000010139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1", "hex", ), - ), + ], }), ], }), }); - const transactionOutcome = transactionsConverter.transactionOnNetworkToOutcome(transactionOnNetwork); - const events = findEventsByFirstTopic(transactionOutcome, "startPerformAction"); + const events = findEventsByFirstTopic(transactionOnNetwork, "startPerformAction"); const parsed = parser.parseEvents({ events }); assert.deepEqual(parsed, [ @@ -135,7 +119,7 @@ describe("test transaction events parser", () => { name: "SendTransferExecuteEgld", fields: [ { - to: Address.fromBech32( + to: Address.newFromBech32( "erd1qqqqqqqqqqqqqpgq6qr0w0zzyysklfneh32eqp2cf383zc89d8sstnkl60", ), egld_amount: new BigNumber("0"), @@ -145,7 +129,7 @@ describe("test transaction events parser", () => { }, ], }, - signers: [Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th")], + signers: [Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th")], }, }, ]); @@ -161,7 +145,7 @@ describe("test transaction events parser", () => { events: [ new TransactionEvent({ identifier: "foobar", - topics: [Buffer.from("doFoobar")], + topics: b64TopicsToBytes([Buffer.from("doFoobar").toString("base64")]), }), ], }); @@ -169,7 +153,7 @@ describe("test transaction events parser", () => { }); it("parses event (with multi-values)", async function () { - const abi = AbiRegistry.create({ + const abi = Abi.create({ events: [ { identifier: "doFoobar", @@ -198,16 +182,16 @@ describe("test transaction events parser", () => { const parsed = parser.parseEvent({ event: new TransactionEvent({ identifier: "foobar", - topics: [ - Buffer.from("doFoobar"), - Buffer.from([42]), - Buffer.from("test"), - Buffer.from([43]), - Buffer.from("test"), - Buffer.from("test"), - Buffer.from([44]), - ], - dataItems: [Buffer.from([42])], + topics: b64TopicsToBytes([ + Buffer.from("doFoobar").toString("base64"), + Buffer.from([42]).toString("base64"), + Buffer.from("test").toString("base64"), + Buffer.from([43]).toString("base64"), + Buffer.from("test").toString("base64"), + Buffer.from("test").toString("base64"), + Buffer.from([44]).toString("base64"), + ]), + additionalData: [Buffer.from([42])], }), }); diff --git a/src/transactionsOutcomeParsers/transactionEventsParser.ts b/src/transactionsOutcomeParsers/transactionEventsParser.ts index 4d568e077..4cd5ed945 100644 --- a/src/transactionsOutcomeParsers/transactionEventsParser.ts +++ b/src/transactionsOutcomeParsers/transactionEventsParser.ts @@ -1,18 +1,11 @@ -import { ResultsParser } from "../smartcontracts"; -import { EventDefinition } from "../smartcontracts/typesystem/event"; -import { TransactionEvent } from "./resources"; - -interface IAbi { - getEvent(name: string): EventDefinition; -} +import { Abi, ArgSerializer } from "../abi"; +import { TransactionEvent } from "../core/transactionEvents"; export class TransactionEventsParser { - private readonly legacyResultsParser: ResultsParser; - private readonly abi: IAbi; + private readonly abi: Abi; private readonly firstTopicIsIdentifier: boolean; - constructor(options: { abi: IAbi; firstTopicIsIdentifier?: boolean }) { - this.legacyResultsParser = new ResultsParser(); + constructor(options: { abi: Abi; firstTopicIsIdentifier?: boolean }) { this.abi = options.abi; // By default, we consider that the first topic is the event identifier. @@ -41,15 +34,26 @@ export class TransactionEventsParser { topics.shift(); } - const dataItems = options.event.dataItems.map((dataItem) => Buffer.from(dataItem)); + const dataItems = options.event.additionalData.map((dataItem) => Buffer.from(dataItem)); const eventDefinition = this.abi.getEvent(abiIdentifier); - const parsedEvent = this.legacyResultsParser.doParseEvent({ - topics: topics, - dataItems: dataItems, - eventDefinition: eventDefinition, - }); + const result: any = {}; + const argsSerializer = new ArgSerializer(); + + // "Indexed" ABI "event.inputs" correspond to "event.topics[1:]": + const indexedInputs = eventDefinition.inputs.filter((input: { indexed: any }) => input.indexed); + const decodedTopics = argsSerializer.buffersToValues(topics, indexedInputs); + for (let i = 0; i < indexedInputs.length; i++) { + result[indexedInputs[i].name] = decodedTopics[i].valueOf(); + } + + // "Non-indexed" ABI "event.inputs" correspond to "event.data": + const nonIndexedInputs = eventDefinition.inputs.filter((input: { indexed: any }) => !input.indexed); + const decodedDataParts = argsSerializer.buffersToValues(dataItems, nonIndexedInputs); + for (let i = 0; i < nonIndexedInputs.length; i++) { + result[nonIndexedInputs[i].name] = decodedDataParts[i]?.valueOf(); + } - return parsedEvent; + return result; } } diff --git a/src/transferTransactionsFactory.spec.ts b/src/transferTransactionsFactory.spec.ts deleted file mode 100644 index 39e6f96d1..000000000 --- a/src/transferTransactionsFactory.spec.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { assert } from "chai"; -import { Address } from "./address"; -import { GasEstimator } from "./gasEstimator"; -import { TokenTransfer } from "./tokens"; -import { TransactionPayload } from "./transactionPayload"; -import { TransferTransactionsFactory } from "./transactionsFactories/transferTransactionsFactory"; - -describe("test transaction factory", () => { - const factory = new TransferTransactionsFactory(new GasEstimator()); - - it("should create EGLD transfers", () => { - const transactionWithData = factory.createEGLDTransfer({ - value: TokenTransfer.egldFromAmount(10.5), - sender: Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), - receiver: new Address("erd1dc3yzxxeq69wvf583gw0h67td226gu2ahpk3k50qdgzzym8npltq7ndgha"), - data: new TransactionPayload("hello"), - chainID: "D", - }); - - assert.equal( - transactionWithData.getSender().bech32(), - "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", - ); - assert.equal( - transactionWithData.getReceiver().bech32(), - "erd1dc3yzxxeq69wvf583gw0h67td226gu2ahpk3k50qdgzzym8npltq7ndgha", - ); - assert.equal(transactionWithData.getValue(), "10500000000000000000"); - assert.equal(transactionWithData.getGasLimit(), 50000 + 5 * 1500); - assert.equal(transactionWithData.getData().toString(), "hello"); - assert.equal(transactionWithData.getChainID(), "D"); - - const transactionWithoutData = factory.createEGLDTransfer({ - value: TokenTransfer.egldFromAmount(10.5), - sender: Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), - receiver: new Address("erd1dc3yzxxeq69wvf583gw0h67td226gu2ahpk3k50qdgzzym8npltq7ndgha"), - chainID: "D", - }); - - assert.equal( - transactionWithoutData.getSender().bech32(), - "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", - ); - assert.equal( - transactionWithoutData.getReceiver().bech32(), - "erd1dc3yzxxeq69wvf583gw0h67td226gu2ahpk3k50qdgzzym8npltq7ndgha", - ); - assert.equal(transactionWithoutData.getValue(), "10500000000000000000"); - assert.equal(transactionWithoutData.getGasLimit(), 50000); - assert.equal(transactionWithoutData.getData().toString(), ""); - assert.equal(transactionWithoutData.getChainID(), "D"); - }); - - it("should create ESDT transfers", () => { - const transaction = factory.createESDTTransfer({ - tokenTransfer: TokenTransfer.fungibleFromAmount("TEST-8b028f", "100.00", 2), - sender: Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), - receiver: Address.fromBech32("erd1dc3yzxxeq69wvf583gw0h67td226gu2ahpk3k50qdgzzym8npltq7ndgha"), - chainID: "D", - }); - - assert.equal( - transaction.getSender().bech32(), - "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", - ); - assert.equal( - transaction.getReceiver().bech32(), - "erd1dc3yzxxeq69wvf583gw0h67td226gu2ahpk3k50qdgzzym8npltq7ndgha", - ); - assert.equal(transaction.getValue(), ""); - assert.equal(transaction.getGasLimit(), 50000 + 40 * 1500 + 200000 + 100000); - assert.equal(transaction.getData().toString(), "ESDTTransfer@544553542d386230323866@2710"); - assert.equal(transaction.getChainID(), "D"); - }); - - it("should create ESDTNFT transfers", () => { - const transaction = factory.createESDTNFTTransfer({ - tokenTransfer: TokenTransfer.nonFungible("TEST-38f249", 1), - destination: new Address("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"), - sender: new Address("erd1dc3yzxxeq69wvf583gw0h67td226gu2ahpk3k50qdgzzym8npltq7ndgha"), - chainID: "D", - }); - - assert.equal( - transaction.getSender().bech32(), - "erd1dc3yzxxeq69wvf583gw0h67td226gu2ahpk3k50qdgzzym8npltq7ndgha", - ); - assert.equal( - transaction.getReceiver().bech32(), - "erd1dc3yzxxeq69wvf583gw0h67td226gu2ahpk3k50qdgzzym8npltq7ndgha", - ); - assert.equal(transaction.getValue(), ""); - assert.equal(transaction.getGasLimit(), 50000 + 109 * 1500 + 200000 + 800000); - assert.equal( - transaction.getData().toString(), - "ESDTNFTTransfer@544553542d333866323439@01@01@8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8", - ); - assert.equal(transaction.getChainID(), "D"); - }); - - it("should create Multi ESDTNFT transfers", () => { - const transaction = factory.createMultiESDTNFTTransfer({ - tokenTransfers: [ - TokenTransfer.nonFungible("FOO-38f249", 1), - TokenTransfer.fungibleFromAmount("BAR-c80d29", "10.00", 18), - ], - destination: new Address("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), - sender: new Address("erd1dc3yzxxeq69wvf583gw0h67td226gu2ahpk3k50qdgzzym8npltq7ndgha"), - chainID: "D", - }); - - assert.equal( - transaction.getSender().bech32(), - "erd1dc3yzxxeq69wvf583gw0h67td226gu2ahpk3k50qdgzzym8npltq7ndgha", - ); - assert.equal( - transaction.getReceiver().bech32(), - "erd1dc3yzxxeq69wvf583gw0h67td226gu2ahpk3k50qdgzzym8npltq7ndgha", - ); - assert.equal(transaction.getValue(), ""); - assert.equal(transaction.getGasLimit(), 50000 + 154 * 1500 + (200000 + 800000) * 2); - assert.equal( - transaction.getData().toString(), - "MultiESDTNFTTransfer@0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1@02@464f4f2d333866323439@01@01@4241522d633830643239@@8ac7230489e80000", - ); - assert.equal(transaction.getChainID(), "D"); - }); -}); diff --git a/src/transfers/index.ts b/src/transfers/index.ts new file mode 100644 index 000000000..279375198 --- /dev/null +++ b/src/transfers/index.ts @@ -0,0 +1,3 @@ +export * from "./resources"; +export * from "./transfersControllers"; +export * from "./transferTransactionsFactory"; diff --git a/src/transfers/resources.ts b/src/transfers/resources.ts new file mode 100644 index 000000000..779a858fa --- /dev/null +++ b/src/transfers/resources.ts @@ -0,0 +1,20 @@ +import { Address } from "../core/address"; +import { TokenTransfer } from "../core/tokens"; + +export type NativeTokenTransferInput = { + receiver: Address; + nativeAmount?: bigint; + data?: Uint8Array; +}; + +export type CustomTokenTransferInput = { + receiver: Address; + tokenTransfers: TokenTransfer[]; +}; + +export type CreateTransferTransactionInput = { + receiver: Address; + nativeAmount?: bigint; + tokenTransfers?: TokenTransfer[]; + data?: Uint8Array; +}; diff --git a/src/transactionsFactories/transferTransactionsFactory.spec.ts b/src/transfers/transferTransactionsFactory.spec.ts similarity index 79% rename from src/transactionsFactories/transferTransactionsFactory.spec.ts rename to src/transfers/transferTransactionsFactory.spec.ts index 6a7581c3e..348d61602 100644 --- a/src/transactionsFactories/transferTransactionsFactory.spec.ts +++ b/src/transfers/transferTransactionsFactory.spec.ts @@ -1,9 +1,7 @@ import { assert } from "chai"; -import { Address } from "../address"; -import { EGLD_IDENTIFIER_FOR_MULTI_ESDTNFT_TRANSFER } from "../constants"; -import { ErrBadUsage } from "../errors"; -import { Token, TokenTransfer } from "../tokens"; -import { TransactionsFactoryConfig } from "./transactionsFactoryConfig"; +import { Address, ErrBadUsage, Token, TokenTransfer } from "../core"; +import { EGLD_IDENTIFIER_FOR_MULTI_ESDTNFT_TRANSFER } from "../core/constants"; +import { TransactionsFactoryConfig } from "../core/transactionsFactoryConfig"; import { TransferTransactionsFactory } from "./transferTransactionsFactory"; describe("test transfer transactions factory", function () { @@ -12,16 +10,15 @@ describe("test transfer transactions factory", function () { config: config, }); - const alice = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - const bob = Address.fromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + const alice = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + const bob = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); it("should throw error, no token transfer provided", async () => { let transfers: any = []; assert.throw( () => { - transferFactory.createTransactionForESDTTokenTransfer({ - sender: alice, + transferFactory.createTransactionForESDTTokenTransfer(alice, { receiver: bob, tokenTransfers: transfers, }); @@ -32,29 +29,27 @@ describe("test transfer transactions factory", function () { }); it("should create 'Transaction' for native token transfer without data", async () => { - const transaction = transferFactory.createTransactionForNativeTokenTransfer({ - sender: alice, + const transaction = transferFactory.createTransactionForNativeTokenTransfer(alice, { receiver: bob, nativeAmount: 1000000000000000000n, }); - assert.equal(transaction.sender, alice.toBech32()); - assert.equal(transaction.receiver, bob.toBech32()); + assert.equal(transaction.sender, alice); + assert.equal(transaction.receiver, bob); assert.equal(transaction.value.valueOf(), 1000000000000000000n); assert.equal(transaction.gasLimit.valueOf(), 50000n); assert.deepEqual(transaction.data, new Uint8Array()); }); it("should create 'Transaction' for native token transfer with data", async () => { - const transaction = transferFactory.createTransactionForNativeTokenTransfer({ - sender: alice, + const transaction = transferFactory.createTransactionForNativeTokenTransfer(alice, { receiver: bob, nativeAmount: 1000000000000000000n, data: Buffer.from("test data"), }); - assert.equal(transaction.sender, alice.toBech32()); - assert.equal(transaction.receiver, bob.toBech32()); + assert.equal(transaction.sender, alice); + assert.equal(transaction.receiver, bob); assert.equal(transaction.value.valueOf(), 1000000000000000000n); assert.equal(transaction.gasLimit.valueOf(), 63500n); assert.deepEqual(transaction.data, Buffer.from("test data")); @@ -64,14 +59,13 @@ describe("test transfer transactions factory", function () { const fooToken = new Token({ identifier: "FOO-123456", nonce: 0n }); const transfer = new TokenTransfer({ token: fooToken, amount: 1000000n }); - const transaction = transferFactory.createTransactionForESDTTokenTransfer({ - sender: alice, + const transaction = transferFactory.createTransactionForESDTTokenTransfer(alice, { receiver: bob, tokenTransfers: [transfer], }); - assert.equal(transaction.sender, alice.toBech32()); - assert.equal(transaction.receiver, bob.toBech32()); + assert.equal(transaction.sender, alice); + assert.equal(transaction.receiver, bob); assert.equal(transaction.value.valueOf(), 0n); assert.equal(transaction.gasLimit.valueOf(), 410000n); assert.deepEqual(transaction.data.toString(), "ESDTTransfer@464f4f2d313233343536@0f4240"); @@ -81,14 +75,13 @@ describe("test transfer transactions factory", function () { const nft = new Token({ identifier: "NFT-123456", nonce: 10n }); const transfer = new TokenTransfer({ token: nft, amount: 1n }); - const transaction = transferFactory.createTransactionForESDTTokenTransfer({ - sender: alice, + const transaction = transferFactory.createTransactionForESDTTokenTransfer(alice, { receiver: bob, tokenTransfers: [transfer], }); - assert.equal(transaction.sender, alice.toBech32()); - assert.equal(transaction.receiver, alice.toBech32()); + assert.equal(transaction.sender, alice); + assert.equal(transaction.receiver, alice); assert.equal(transaction.value.valueOf(), 0n); assert.equal(transaction.gasLimit.valueOf(), 1210500n); assert.deepEqual( @@ -101,14 +94,13 @@ describe("test transfer transactions factory", function () { const nft = new Token({ identifier: "t0-NFT-123456", nonce: 10n }); const transfer = new TokenTransfer({ token: nft, amount: 1n }); - const transaction = transferFactory.createTransactionForESDTTokenTransfer({ - sender: alice, + const transaction = transferFactory.createTransactionForESDTTokenTransfer(alice, { receiver: bob, tokenTransfers: [transfer], }); - assert.equal(transaction.sender, alice.toBech32()); - assert.equal(transaction.receiver, alice.toBech32()); + assert.equal(transaction.sender, alice); + assert.equal(transaction.receiver, alice); assert.equal(transaction.value.valueOf(), 0n); assert.equal(transaction.gasLimit.valueOf(), 1219500n); assert.deepEqual( @@ -124,14 +116,13 @@ describe("test transfer transactions factory", function () { const secondNft = new Token({ identifier: "TEST-987654", nonce: 1n }); const secondTransfer = new TokenTransfer({ token: secondNft, amount: 1n }); - const transaction = transferFactory.createTransactionForESDTTokenTransfer({ - sender: alice, + const transaction = transferFactory.createTransactionForESDTTokenTransfer(alice, { receiver: bob, tokenTransfers: [firstTransfer, secondTransfer], }); - assert.equal(transaction.sender, alice.toBech32()); - assert.equal(transaction.receiver, alice.toBech32()); + assert.equal(transaction.sender, alice); + assert.equal(transaction.receiver, alice); assert.equal(transaction.value.valueOf(), 0n); assert.equal(transaction.gasLimit.valueOf(), 1466000n); assert.deepEqual( @@ -139,8 +130,7 @@ describe("test transfer transactions factory", function () { "MultiESDTNFTTransfer@8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8@02@4e46542d313233343536@0a@01@544553542d393837363534@01@01", ); - const secondTransaction = transferFactory.createTransactionForTransfer({ - sender: alice, + const secondTransaction = transferFactory.createTransactionForTransfer(alice, { receiver: bob, tokenTransfers: [firstTransfer, secondTransfer], }); @@ -155,14 +145,13 @@ describe("test transfer transactions factory", function () { const secondNft = new Token({ identifier: "t0-TEST-987654", nonce: 1n }); const secondTransfer = new TokenTransfer({ token: secondNft, amount: 1n }); - const transaction = transferFactory.createTransactionForESDTTokenTransfer({ - sender: alice, + const transaction = transferFactory.createTransactionForESDTTokenTransfer(alice, { receiver: bob, tokenTransfers: [firstTransfer, secondTransfer], }); - assert.equal(transaction.sender, alice.toBech32()); - assert.equal(transaction.receiver, alice.toBech32()); + assert.equal(transaction.sender, alice); + assert.equal(transaction.receiver, alice); assert.equal(transaction.value.valueOf(), 0n); assert.equal(transaction.gasLimit.valueOf(), 1484000n); assert.deepEqual( @@ -170,8 +159,7 @@ describe("test transfer transactions factory", function () { "MultiESDTNFTTransfer@8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8@02@74302d4e46542d313233343536@0a@01@74302d544553542d393837363534@01@01", ); - const secondTransaction = transferFactory.createTransactionForTransfer({ - sender: alice, + const secondTransaction = transferFactory.createTransactionForTransfer(alice, { receiver: bob, tokenTransfers: [firstTransfer, secondTransfer], }); @@ -184,8 +172,7 @@ describe("test transfer transactions factory", function () { const nft = new Token({ identifier: "NFT-123456", nonce: 10n }); const transfer = new TokenTransfer({ token: nft, amount: 1n }); - transferFactory.createTransactionForTransfer({ - sender: alice, + transferFactory.createTransactionForTransfer(alice, { receiver: bob, tokenTransfers: [transfer], data: Buffer.from("test"), @@ -194,42 +181,39 @@ describe("test transfer transactions factory", function () { }); it("should create transaction for native transfers", async () => { - const transaction = transferFactory.createTransactionForTransfer({ - sender: alice, + const transaction = transferFactory.createTransactionForTransfer(alice, { receiver: bob, nativeAmount: 1000000000000000000n, }); - assert.equal(transaction.sender, alice.toBech32()); - assert.equal(transaction.receiver, bob.toBech32()); + assert.equal(transaction.sender, alice); + assert.equal(transaction.receiver, bob); assert.equal(transaction.value.valueOf(), 1000000000000000000n); assert.equal(transaction.gasLimit.valueOf(), 50000n); }); it("should create transaction for native transfers and set data field", async () => { - const transaction = transferFactory.createTransactionForTransfer({ - sender: alice, + const transaction = transferFactory.createTransactionForTransfer(alice, { receiver: bob, nativeAmount: 1000000000000000000n, data: Buffer.from("hello"), }); - assert.equal(transaction.sender, alice.toBech32()); - assert.equal(transaction.receiver, bob.toBech32()); + assert.equal(transaction.sender, alice); + assert.equal(transaction.receiver, bob); assert.equal(transaction.value.valueOf(), 1000000000000000000n); assert.equal(transaction.gasLimit.valueOf(), 57500n); assert.deepEqual(transaction.data, Buffer.from("hello")); }); it("should create transaction for notarizing", async () => { - const transaction = transferFactory.createTransactionForTransfer({ - sender: alice, + const transaction = transferFactory.createTransactionForTransfer(alice, { receiver: bob, data: Buffer.from("hello"), }); - assert.equal(transaction.sender, alice.toBech32()); - assert.equal(transaction.receiver, bob.toBech32()); + assert.equal(transaction.sender, alice); + assert.equal(transaction.receiver, bob); assert.equal(transaction.gasLimit.valueOf(), 57500n); assert.deepEqual(transaction.data, Buffer.from("hello")); }); @@ -241,15 +225,14 @@ describe("test transfer transactions factory", function () { const secondNft = new Token({ identifier: "TEST-987654", nonce: 1n }); const secondTransfer = new TokenTransfer({ token: secondNft, amount: 1n }); - const transaction = transferFactory.createTransactionForTransfer({ - sender: alice, + const transaction = transferFactory.createTransactionForTransfer(alice, { receiver: bob, nativeAmount: 1000000000000000000n, tokenTransfers: [firstTransfer, secondTransfer], }); - assert.equal(transaction.sender, alice.toBech32()); - assert.equal(transaction.receiver, alice.toBech32()); + assert.equal(transaction.sender, alice); + assert.equal(transaction.receiver, alice); assert.equal(transaction.value.valueOf(), 0n); assert.equal(transaction.gasLimit.valueOf(), 1727500n); assert.deepEqual( @@ -265,15 +248,14 @@ describe("test transfer transactions factory", function () { const secondNft = new Token({ identifier: "t0-TEST-987654", nonce: 1n }); const secondTransfer = new TokenTransfer({ token: secondNft, amount: 1n }); - const transaction = transferFactory.createTransactionForTransfer({ - sender: alice, + const transaction = transferFactory.createTransactionForTransfer(alice, { receiver: bob, nativeAmount: 1000000000000000000n, tokenTransfers: [firstTransfer, secondTransfer], }); - assert.equal(transaction.sender, alice.toBech32()); - assert.equal(transaction.receiver, alice.toBech32()); + assert.equal(transaction.sender, alice); + assert.equal(transaction.receiver, alice); assert.equal(transaction.value.valueOf(), 0n); assert.equal(transaction.gasLimit.valueOf(), 1745500n); assert.deepEqual( @@ -286,14 +268,13 @@ describe("test transfer transactions factory", function () { const firstNft = new Token({ identifier: EGLD_IDENTIFIER_FOR_MULTI_ESDTNFT_TRANSFER }); const firstTransfer = new TokenTransfer({ token: firstNft, amount: 1000000000000000000n }); - const transaction = transferFactory.createTransactionForESDTTokenTransfer({ - sender: alice, + const transaction = transferFactory.createTransactionForESDTTokenTransfer(alice, { receiver: bob, tokenTransfers: [firstTransfer], }); - assert.equal(transaction.sender, alice.toBech32()); - assert.equal(transaction.receiver, alice.toBech32()); + assert.equal(transaction.sender, alice); + assert.equal(transaction.receiver, alice); assert.equal(transaction.value.valueOf(), 0n); assert.equal(transaction.gasLimit.valueOf(), 1_243_500n); assert.deepEqual( diff --git a/src/transfers/transferTransactionsFactory.ts b/src/transfers/transferTransactionsFactory.ts new file mode 100644 index 000000000..733a95107 --- /dev/null +++ b/src/transfers/transferTransactionsFactory.ts @@ -0,0 +1,171 @@ +import { Address } from "../core/address"; +import { EGLD_IDENTIFIER_FOR_MULTI_ESDTNFT_TRANSFER } from "../core/constants"; +import { ErrBadUsage } from "../core/errors"; +import { TokenComputer, TokenTransfer } from "../core/tokens"; +import { TokenTransfersDataBuilder } from "../core/tokenTransfersDataBuilder"; +import { Transaction } from "../core/transaction"; +import { TransactionBuilder } from "../core/transactionBuilder"; +import * as resources from "./resources"; + +const ADDITIONAL_GAS_FOR_ESDT_TRANSFER = 100000; +const ADDITIONAL_GAS_FOR_ESDT_NFT_TRANSFER = 800000; + +interface IConfig { + chainID: string; + minGasLimit: bigint; + gasLimitPerByte: bigint; + gasLimitESDTTransfer: bigint; + gasLimitESDTNFTTransfer: bigint; + gasLimitMultiESDTNFTTransfer: bigint; +} + +/** + * Use this class to create transactions for native token transfers (EGLD) or custom tokens transfers (ESDT/NTF/MetaESDT). + */ +export class TransferTransactionsFactory { + private readonly config?: IConfig; + private readonly tokenTransfersDataBuilder?: TokenTransfersDataBuilder; + private readonly tokenComputer?: TokenComputer; + + constructor(options: { config: IConfig }) { + this.config = options.config; + this.tokenComputer = new TokenComputer(); + this.tokenTransfersDataBuilder = new TokenTransfersDataBuilder(); + } + + createTransactionForNativeTokenTransfer(sender: Address, options: resources.NativeTokenTransferInput): Transaction { + const data = options.data || new Uint8Array(); + + return new Transaction({ + sender: sender, + receiver: options.receiver, + chainID: this.config!.chainID, + gasLimit: this.computeGasForMoveBalance(this.config!, data), + data: data, + value: options.nativeAmount ?? BigInt(0), + }); + } + + createTransactionForESDTTokenTransfer(sender: Address, options: resources.CustomTokenTransferInput): Transaction { + const numberOfTransfers = options.tokenTransfers.length; + + if (numberOfTransfers === 0) { + throw new ErrBadUsage("No token transfer has been provided"); + } + + if (numberOfTransfers === 1) { + return this.createSingleESDTTransferTransaction(sender, options); + } + + const { dataParts, extraGasForTransfer } = this.buildMultiESDTNFTTransferData( + options.tokenTransfers, + options.receiver, + ); + + return new TransactionBuilder({ + config: this.config!, + sender: sender, + receiver: sender, + dataParts: dataParts, + gasLimit: extraGasForTransfer, + addDataMovementGas: true, + }).build(); + } + + createTransactionForTransfer(sender: Address, options: resources.CreateTransferTransactionInput): Transaction { + const nativeAmount = options.nativeAmount ?? 0n; + let tokenTransfers = options.tokenTransfers ? [...options.tokenTransfers] : []; + const numberOfTokens = tokenTransfers.length; + + if (numberOfTokens && options.data?.length) { + throw new ErrBadUsage("Can't set data field when sending esdt tokens"); + } + + if ((nativeAmount && numberOfTokens === 0) || options.data) { + return this.createTransactionForNativeTokenTransfer(sender, { + receiver: options.receiver, + nativeAmount: nativeAmount, + data: options.data, + }); + } + + const nativeTransfer = nativeAmount ? TokenTransfer.newFromNativeAmount(nativeAmount) : undefined; + if (nativeTransfer) { + tokenTransfers.push(nativeTransfer); + } + + return this.createTransactionForESDTTokenTransfer(sender, { + receiver: options.receiver, + tokenTransfers: tokenTransfers, + }); + } + + private createSingleESDTTransferTransaction( + sender: Address, + options: { + receiver: Address; + tokenTransfers: TokenTransfer[]; + }, + ): Transaction { + const transfer = options.tokenTransfers[0]; + const { dataParts, extraGasForTransfer, receiver } = this.buildTransferData(transfer, { + sender, + receiver: options.receiver, + }); + + return new TransactionBuilder({ + config: this.config!, + sender: sender, + receiver: receiver, + dataParts: dataParts, + gasLimit: extraGasForTransfer, + addDataMovementGas: true, + }).build(); + } + + private buildTransferData(transfer: TokenTransfer, options: { sender: Address; receiver: Address }) { + let dataParts: string[] = []; + let extraGasForTransfer: bigint; + let receiver = options.receiver; + + if (this.tokenComputer!.isFungible(transfer.token)) { + if (transfer.token.identifier === EGLD_IDENTIFIER_FOR_MULTI_ESDTNFT_TRANSFER) { + ({ dataParts, extraGasForTransfer } = this.buildMultiESDTNFTTransferData([transfer], receiver)); + receiver = options.sender; + } else { + ({ dataParts, extraGasForTransfer } = this.buildESDTTransferData(transfer)); + } + } else { + ({ dataParts, extraGasForTransfer } = this.buildSingleESDTNFTTransferData(transfer, receiver)); + receiver = options.sender; // Override receiver for non-fungible tokens + } + return { dataParts, extraGasForTransfer, receiver }; + } + + private buildMultiESDTNFTTransferData(transfer: TokenTransfer[], receiver: Address) { + return { + dataParts: this.tokenTransfersDataBuilder!.buildDataPartsForMultiESDTNFTTransfer(receiver, transfer), + extraGasForTransfer: + this.config!.gasLimitMultiESDTNFTTransfer * BigInt(transfer.length) + + BigInt(ADDITIONAL_GAS_FOR_ESDT_NFT_TRANSFER), + }; + } + + private buildESDTTransferData(transfer: TokenTransfer) { + return { + dataParts: this.tokenTransfersDataBuilder!.buildDataPartsForESDTTransfer(transfer), + extraGasForTransfer: this.config!.gasLimitESDTTransfer + BigInt(ADDITIONAL_GAS_FOR_ESDT_TRANSFER), + }; + } + + private buildSingleESDTNFTTransferData(transfer: TokenTransfer, receiver: Address) { + return { + dataParts: this.tokenTransfersDataBuilder!.buildDataPartsForSingleESDTNFTTransfer(transfer, receiver), + extraGasForTransfer: this.config!.gasLimitESDTNFTTransfer + BigInt(ADDITIONAL_GAS_FOR_ESDT_NFT_TRANSFER), + }; + } + + private computeGasForMoveBalance(config: IConfig, data: Uint8Array): bigint { + return config.minGasLimit + config.gasLimitPerByte * BigInt(data.length); + } +} diff --git a/src/transfers/transfersControllers.ts b/src/transfers/transfersControllers.ts new file mode 100644 index 000000000..50c797e29 --- /dev/null +++ b/src/transfers/transfersControllers.ts @@ -0,0 +1,67 @@ +import { + Address, + BaseController, + BaseControllerInput, + IAccount, + Transaction, + TransactionsFactoryConfig, +} from "../core"; +import * as resources from "./resources"; +import { TransferTransactionsFactory } from "./transferTransactionsFactory"; + +export class TransfersController extends BaseController { + private factory: TransferTransactionsFactory; + + constructor(options: { chainID: string }) { + super(); + this.factory = new TransferTransactionsFactory({ config: new TransactionsFactoryConfig(options) }); + } + + async createTransactionForNativeTokenTransfer( + sender: IAccount, + nonce: bigint, + options: resources.NativeTokenTransferInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForNativeTokenTransfer(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async createTransactionForEsdtTokenTransfer( + sender: IAccount, + nonce: bigint, + options: resources.CustomTokenTransferInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForESDTTokenTransfer(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async createTransactionForTransfer( + sender: IAccount, + nonce: bigint, + options: resources.CreateTransferTransactionInput & BaseControllerInput, + ): Promise { + const transaction = this.factory.createTransactionForTransfer(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } +} diff --git a/src/wallet/assertions.ts b/src/wallet/assertions.ts index 09ac481d2..07e240e36 100644 --- a/src/wallet/assertions.ts +++ b/src/wallet/assertions.ts @@ -1,4 +1,4 @@ -import { ErrInvariantFailed } from "../errors"; +import { ErrInvariantFailed } from "../core/errors"; export function guardLength(withLength: { length?: number }, expectedLength: number) { let actualLength = withLength.length || 0; diff --git a/src/wallet/crypto/decryptor.ts b/src/wallet/crypto/decryptor.ts index 388b9fd63..296a6f259 100644 --- a/src/wallet/crypto/decryptor.ts +++ b/src/wallet/crypto/decryptor.ts @@ -1,5 +1,5 @@ import crypto from "crypto"; -import { Err } from "../../errors"; +import { Err } from "../../core/errors"; import { DigestAlgorithm } from "./constants"; import { EncryptedData } from "./encryptedData"; diff --git a/src/wallet/crypto/pubkeyDecryptor.ts b/src/wallet/crypto/pubkeyDecryptor.ts index e9b6d7a00..b70a58915 100644 --- a/src/wallet/crypto/pubkeyDecryptor.ts +++ b/src/wallet/crypto/pubkeyDecryptor.ts @@ -1,25 +1,26 @@ import crypto from "crypto"; -import nacl from "tweetnacl"; import ed2curve from "ed2curve"; -import { X25519EncryptedData } from "./x25519EncryptedData"; +import nacl from "tweetnacl"; import { UserPublicKey, UserSecretKey } from "../userKeys"; +import { X25519EncryptedData } from "./x25519EncryptedData"; export class PubkeyDecryptor { - static decrypt(data: X25519EncryptedData, decryptorSecretKey: UserSecretKey): Buffer { - const ciphertext = Buffer.from(data.ciphertext, 'hex'); - const edhPubKey = Buffer.from(data.identities.ephemeralPubKey, 'hex'); - const originatorPubKeyBuffer = Buffer.from(data.identities.originatorPubKey, 'hex'); + static async decrypt(data: X25519EncryptedData, decryptorSecretKey: UserSecretKey): Promise { + const ciphertext = Buffer.from(data.ciphertext, "hex"); + const edhPubKey = Buffer.from(data.identities.ephemeralPubKey, "hex"); + const originatorPubKeyBuffer = Buffer.from(data.identities.originatorPubKey, "hex"); const originatorPubKey = new UserPublicKey(originatorPubKeyBuffer); - const authMessage = crypto.createHash('sha256').update( - Buffer.concat([ciphertext, edhPubKey]) - ).digest(); + const authMessage = crypto + .createHash("sha256") + .update(Buffer.concat([ciphertext, edhPubKey])) + .digest(); - if (!originatorPubKey.verify(authMessage, Buffer.from(data.mac, 'hex'))) { + if (!(await originatorPubKey.verify(authMessage, Buffer.from(data.mac, "hex")))) { throw new Error("Invalid authentication for encrypted message originator"); } - const nonce = Buffer.from(data.nonce, 'hex'); + const nonce = Buffer.from(data.nonce, "hex"); const x25519Secret = ed2curve.convertSecretKey(decryptorSecretKey.valueOf()); const x25519EdhPubKey = ed2curve.convertPublicKey(edhPubKey); if (x25519EdhPubKey === null) { diff --git a/src/wallet/crypto/pubkeyEncrypt.spec.ts b/src/wallet/crypto/pubkeyEncrypt.spec.ts index 804a35f5b..f550cd2f2 100644 --- a/src/wallet/crypto/pubkeyEncrypt.spec.ts +++ b/src/wallet/crypto/pubkeyEncrypt.spec.ts @@ -22,24 +22,39 @@ describe("test address", () => { ); }); - it("encrypts/decrypts", () => { - const decryptedData = PubkeyDecryptor.decrypt(encryptedDataOfAliceForBob, new UserSecretKey(bob.secretKey)); + it("encrypts/decrypts", async function () { + const decryptedData = await PubkeyDecryptor.decrypt( + encryptedDataOfAliceForBob, + new UserSecretKey(bob.secretKey), + ); assert.equal(sensitiveData.toString("hex"), decryptedData.toString("hex")); }); - it("fails for different originator", () => { - encryptedDataOfAliceForBob.identities.originatorPubKey = carol.address.hex(); - assert.throws( - () => PubkeyDecryptor.decrypt(encryptedDataOfAliceForBob, new UserSecretKey(bob.secretKey)), - "Invalid authentication for encrypted message originator", - ); + it("fails for different originator", async function () { + encryptedDataOfAliceForBob.identities.originatorPubKey = carol.address.toHex(); + + try { + await PubkeyDecryptor.decrypt(encryptedDataOfAliceForBob, new UserSecretKey(bob.secretKey)); + assert.fail("Invalid authentication for encrypted message originator"); + } catch (error) { + assert( + error.message.includes("Invalid authentication for encrypted message originator"), + `Unexpected error message: ${error.message}`, + ); + } }); - it("fails for different DH public key", () => { - encryptedDataOfAliceForBob.identities.ephemeralPubKey = carol.address.hex(); - assert.throws( - () => PubkeyDecryptor.decrypt(encryptedDataOfAliceForBob, new UserSecretKey(bob.secretKey)), - "Invalid authentication for encrypted message originator", - ); + it("fails for different DH public key", async function () { + encryptedDataOfAliceForBob.identities.ephemeralPubKey = carol.address.toHex(); + + try { + await PubkeyDecryptor.decrypt(encryptedDataOfAliceForBob, new UserSecretKey(bob.secretKey)); + assert.fail("Expected an error but none was thrown"); + } catch (error) { + assert( + error.message.includes("Invalid authentication for encrypted message originator"), + `Unexpected error message: ${error.message}`, + ); + } }); }); diff --git a/src/wallet/crypto/pubkeyEncryptor.ts b/src/wallet/crypto/pubkeyEncryptor.ts index 0c89bf16c..3210c8dc8 100644 --- a/src/wallet/crypto/pubkeyEncryptor.ts +++ b/src/wallet/crypto/pubkeyEncryptor.ts @@ -17,7 +17,11 @@ export class PubkeyEncryptor { // For the nonce we use a random component and a deterministic one based on the message // - this is so we won't completely rely on the random number generator - const nonceDeterministic = crypto.createHash('sha256').update(data).digest().slice(0, PubKeyEncNonceLength / 2); + const nonceDeterministic = crypto + .createHash("sha256") + .update(data) + .digest() + .slice(0, PubKeyEncNonceLength / 2); const nonceRandom = nacl.randomBytes(PubKeyEncNonceLength / 2); const nonce = Buffer.concat([nonceDeterministic, nonceRandom]); const encryptedMessage = nacl.box(data, nonce, recipientDHPubKey, edhConvertedSecretKey); @@ -25,23 +29,24 @@ export class PubkeyEncryptor { // Note that the ciphertext is already authenticated for the ephemeral key - but we want it authenticated by // the ed25519 key which the user interacts with. A signature over H(ciphertext | edhPubKey) // would be enough - const authMessage = crypto.createHash('sha256').update( - Buffer.concat([encryptedMessage, edhPair.publicKey]) - ).digest(); + const authMessage = crypto + .createHash("sha256") + .update(Buffer.concat([encryptedMessage, edhPair.publicKey])) + .digest(); const signature = authSecretKey.sign(authMessage); return new X25519EncryptedData({ version: PubKeyEncVersion, - nonce: Buffer.from(nonce).toString('hex'), + nonce: Buffer.from(nonce).toString("hex"), cipher: PubKeyEncCipher, - ciphertext: Buffer.from(encryptedMessage).toString('hex'), - mac: signature.toString('hex'), + ciphertext: Buffer.from(encryptedMessage).toString("hex"), + mac: Buffer.from(signature).toString("hex"), identities: { recipient: recipientPubKey.hex(), - ephemeralPubKey: Buffer.from(edhPair.publicKey).toString('hex'), + ephemeralPubKey: Buffer.from(edhPair.publicKey).toString("hex"), originatorPubKey: authSecretKey.generatePublicKey().hex(), - } + }, }); } } diff --git a/src/wallet/index.ts b/src/wallet/index.ts index 7207e0c14..a0181b16d 100644 --- a/src/wallet/index.ts +++ b/src/wallet/index.ts @@ -1,7 +1,9 @@ export * from "./crypto"; +export * from "./keypair"; export * from "./mnemonic"; export * from "./pem"; export * from "./userKeys"; +export * from "./userPem"; export * from "./userSigner"; export * from "./userVerifier"; export * from "./userWallet"; diff --git a/src/wallet/keypair.spec.ts b/src/wallet/keypair.spec.ts new file mode 100644 index 000000000..3549c466e --- /dev/null +++ b/src/wallet/keypair.spec.ts @@ -0,0 +1,53 @@ +import { assert } from "chai"; +import { Address, Transaction, TransactionComputer } from "../core"; +import { KeyPair } from "./keypair"; +import { UserSecretKey } from "./userKeys"; + +describe("test keypair", () => { + it("should create keypair", () => { + const buffer_hex = "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9"; + const buffer = Uint8Array.from(Buffer.from(buffer_hex, "hex")); + const userSecretKey = UserSecretKey.fromString(buffer_hex); + let keypair = KeyPair.newFromBytes(buffer); + let secretKey = keypair.getSecretKey(); + assert.equal(secretKey.hex(), buffer_hex); + assert.equal(keypair.secretKey.hex(), buffer_hex); + assert.deepEqual(secretKey, userSecretKey); + + keypair = new KeyPair(secretKey); + assert.deepEqual(keypair.getSecretKey(), userSecretKey); + assert.deepEqual(keypair.getPublicKey(), userSecretKey.generatePublicKey()); + + keypair = KeyPair.generate(); + const pubkey = keypair.getPublicKey(); + secretKey = keypair.getSecretKey(); + assert.lengthOf(pubkey.valueOf(), 32); + assert.lengthOf(secretKey.valueOf(), 32); + }); + + it("should sign and verify transaction", async () => { + const transaction = new Transaction({ + nonce: 89n, + value: 0n, + receiver: Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"), + sender: Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + gasPrice: 1000000000n, + gasLimit: 50000n, + chainID: "local-testnet", + version: 1, + options: 0, + }); + const bufferHex = "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9"; + const buffer = Uint8Array.from(Buffer.from(bufferHex, "hex")); + const keypair = KeyPair.newFromBytes(buffer); + + const transactionComputer = new TransactionComputer(); + const serializedTx = transactionComputer.computeBytesForSigning(transaction); + transaction.signature = await keypair.sign(serializedTx); + assert.equal( + Buffer.from(transaction.signature).toString("hex"), + "b56769014f2bdc5cf9fc4a05356807d71fcf8775c819b0f1b0964625b679c918ffa64862313bfef86f99b38cb84fcdb16fa33ad6eb565276616723405cd8f109", + ); + assert.isTrue(await keypair.verify(serializedTx, transaction.signature)); + }); +}); diff --git a/src/wallet/keypair.ts b/src/wallet/keypair.ts new file mode 100644 index 000000000..3d1d545f5 --- /dev/null +++ b/src/wallet/keypair.ts @@ -0,0 +1,37 @@ +import { UserPublicKey, UserSecretKey } from "./userKeys"; + +export class KeyPair { + readonly secretKey: UserSecretKey; + readonly publicKey: UserPublicKey; + + constructor(secretKey: UserSecretKey) { + this.secretKey = secretKey; + this.publicKey = this.secretKey.generatePublicKey(); + } + + static generate(): KeyPair { + const secretKey = UserSecretKey.generate(); + return new KeyPair(secretKey); + } + + static newFromBytes(data: Uint8Array): KeyPair { + const secretKey = new UserSecretKey(data); + return new KeyPair(secretKey); + } + + async sign(data: Uint8Array): Promise { + return this.secretKey.sign(data); + } + + async verify(data: Uint8Array, signature: Uint8Array): Promise { + return this.publicKey.verify(data, signature); + } + + getSecretKey(): UserSecretKey { + return this.secretKey; + } + + getPublicKey(): UserPublicKey { + return this.publicKey; + } +} diff --git a/src/wallet/mnemonic.ts b/src/wallet/mnemonic.ts index 77c397b4c..7b69eeef8 100644 --- a/src/wallet/mnemonic.ts +++ b/src/wallet/mnemonic.ts @@ -1,5 +1,5 @@ import { derivePath } from "ed25519-hd-key"; -import { ErrBadMnemonicEntropy, ErrWrongMnemonic } from "../errors"; +import { ErrBadMnemonicEntropy, ErrWrongMnemonic } from "../core/errors"; import { UserSecretKey } from "./userKeys"; const MNEMONIC_STRENGTH = 256; diff --git a/src/wallet/pem.spec.ts b/src/wallet/pem.spec.ts index 73eda6b96..34a3a9ef4 100644 --- a/src/wallet/pem.spec.ts +++ b/src/wallet/pem.spec.ts @@ -1,6 +1,6 @@ import { Buffer } from "buffer"; import { assert } from "chai"; -import { ErrBadPEM } from "../errors"; +import { ErrBadPEM } from "../core/errors"; import { loadTestWallet, TestWallet } from "./../testutils/wallets"; import { parse, parseUserKey, parseValidatorKey } from "./pem"; import { BLS } from "./validatorKeys"; @@ -18,7 +18,7 @@ describe("test PEMs", () => { let aliceKey = parseUserKey(alice.pemFileText); assert.equal(aliceKey.hex(), alice.secretKeyHex); - assert.equal(aliceKey.generatePublicKey().toAddress().bech32(), alice.address.bech32()); + assert.equal(aliceKey.generatePublicKey().toAddress().toBech32(), alice.address.toBech32()); }); it("should parseValidatorKey", async () => { diff --git a/src/wallet/pem.ts b/src/wallet/pem.ts index b60119699..f95a5b8fe 100644 --- a/src/wallet/pem.ts +++ b/src/wallet/pem.ts @@ -1,4 +1,4 @@ -import { ErrBadPEM } from "../errors"; +import { ErrBadPEM } from "../core/errors"; import { USER_PUBKEY_LENGTH, USER_SEED_LENGTH, UserSecretKey } from "./userKeys"; import { VALIDATOR_SECRETKEY_LENGTH, ValidatorSecretKey } from "./validatorKeys"; diff --git a/src/wallet/pemEntry.spec.ts b/src/wallet/pemEntry.spec.ts new file mode 100644 index 000000000..3ab50d5b1 --- /dev/null +++ b/src/wallet/pemEntry.spec.ts @@ -0,0 +1,184 @@ +import { assert } from "chai"; +import { readFileSync } from "fs"; +import path from "path"; +import { PemEntry } from "./pemEntry"; +import { USER_SEED_LENGTH } from "./userKeys"; + +describe("test pem entry", () => { + const walletsPath = path.join("src", "testdata", "testwallets"); + + it("should create from text all", () => { + let text = readFileSync(path.join(walletsPath, "alice.pem"), "utf-8"); + let entries = PemEntry.fromTextAll(text); + let entry = entries[0]; + + assert.lengthOf(entries, 1); + assert.equal(entry.label, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + assert.equal( + Buffer.from(entry.message.slice(0, USER_SEED_LENGTH)).toString("hex"), + "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9", + ); + + text = readFileSync(path.join(walletsPath, "multipleUserKeys.pem"), "utf-8"); + entries = PemEntry.fromTextAll(text); + entry = entries[0]; + + assert.lengthOf(entries, 3); + assert.equal(entry.label, "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + assert.equal( + Buffer.from(entry.message.slice(0, USER_SEED_LENGTH)).toString("hex"), + "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9", + ); + + entry = entries[1]; + assert.equal(entry.label, "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + assert.equal( + Buffer.from(entry.message.slice(0, USER_SEED_LENGTH)).toString("hex"), + "b8ca6f8203fb4b545a8e83c5384da033c415db155b53fb5b8eba7ff5a039d639", + ); + + entry = entries[2]; + assert.equal(entry.label, "erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8"); + assert.equal( + Buffer.from(entry.message.slice(0, USER_SEED_LENGTH)).toString("hex"), + "e253a571ca153dc2aee845819f74bcc9773b0586edead15a94cb7235a5027436", + ); + }); + + it("should create from text all for validators", () => { + let text = readFileSync(path.join(walletsPath, "validatorKey00.pem"), "utf-8"); + let entries = PemEntry.fromTextAll(text); + let entry = entries[0]; + + assert.lengthOf(entries, 1); + assert.equal( + entry.label, + "e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208", + ); + assert.equal( + Buffer.from(entry.message.slice(0, USER_SEED_LENGTH)).toString("hex"), + "7cff99bd671502db7d15bc8abc0c9a804fb925406fbdd50f1e4c17a4cd774247", + ); + + text = readFileSync(path.join(walletsPath, "multipleValidatorKeys.pem"), "utf-8"); + entries = PemEntry.fromTextAll(text); + entry = entries[0]; + + assert.lengthOf(entries, 4); + assert.equal( + entry.label, + "f8910e47cf9464777c912e6390758bb39715fffcb861b184017920e4a807b42553f2f21e7f3914b81bcf58b66a72ab16d97013ae1cff807cefc977ef8cbf116258534b9e46d19528042d16ef8374404a89b184e0a4ee18c77c49e454d04eae8d", + ); + assert.equal( + Buffer.from(entry.message).toString("hex"), + "7c19bf3a0c57cdd1fb08e4607cebaa3647d6b9261b4693f61e96e54b218d442a", + ); + + entry = entries[1]; + assert.equal( + entry.label, + "1b4e60e6d100cdf234d3427494dac55fbac49856cadc86bcb13a01b9bb05a0d9143e86c186c948e7ae9e52427c9523102efe9019a2a9c06db02993f2e3e6756576ae5a3ec7c235d548bc79de1a6990e1120ae435cb48f7fc436c9f9098b92a0d", + ); + assert.equal( + Buffer.from(entry.message).toString("hex"), + "3034b1d58628a842984da0c70da0b5a251ebb2aebf51afc5b586e2839b5e5263", + ); + + entry = entries[2]; + assert.equal( + entry.label, + "e5dc552b4b170cdec4405ff8f9af20313bf0e2756d06c35877b6fbcfa6b354a7b3e2d439ea87999befb09a8fa1b3f014e57ec747bf738c4199338fcd4a87b373dd62f5c8329f1f5f245956bbb06685596a2e83dc38befa63e4a2b5c4ce408506", + ); + assert.equal( + Buffer.from(entry.message).toString("hex"), + "de7e1b385edbb0e1e8f9fc25d91bd8eed71a1da7caab732e6b47a48042d8523d", + ); + + entry = entries[3]; + assert.equal( + entry.label, + "12773304cb718250edd89770cedcbf675ccdb7fe2b30bd3185ca65ffa0d516879768ed03f92e41a6e5bc5340b78a9d02655e3b727c79730ead791fb68eaa02b84e1be92a816a9604a1ab9a6d3874b638487e2145239438a4bafac3889348d405", + ); + assert.equal( + Buffer.from(entry.message).toString("hex"), + "8ebeb07d296ad2529400b40687a741a135f8357f79f39fcb2894a6f9703a5816", + ); + }); + + it("should create from text all for validators with extra lines in pem file", () => { + let text = readFileSync(path.join(walletsPath, "validatorKey00WithExtraLines.pem"), "utf-8"); + let entries = PemEntry.fromTextAll(text); + let entry = entries[0]; + + assert.lengthOf(entries, 1); + assert.equal( + entry.label, + "e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208", + ); + assert.equal( + Buffer.from(entry.message.slice(0, USER_SEED_LENGTH)).toString("hex"), + "7cff99bd671502db7d15bc8abc0c9a804fb925406fbdd50f1e4c17a4cd774247", + ); + + text = readFileSync(path.join(walletsPath, "multipleValidatorKeys.pem"), "utf-8"); + entries = PemEntry.fromTextAll(text); + entry = entries[0]; + + assert.lengthOf(entries, 4); + assert.equal( + entry.label, + "f8910e47cf9464777c912e6390758bb39715fffcb861b184017920e4a807b42553f2f21e7f3914b81bcf58b66a72ab16d97013ae1cff807cefc977ef8cbf116258534b9e46d19528042d16ef8374404a89b184e0a4ee18c77c49e454d04eae8d", + ); + assert.equal( + Buffer.from(entry.message).toString("hex"), + "7c19bf3a0c57cdd1fb08e4607cebaa3647d6b9261b4693f61e96e54b218d442a", + ); + + entry = entries[1]; + assert.equal( + entry.label, + "1b4e60e6d100cdf234d3427494dac55fbac49856cadc86bcb13a01b9bb05a0d9143e86c186c948e7ae9e52427c9523102efe9019a2a9c06db02993f2e3e6756576ae5a3ec7c235d548bc79de1a6990e1120ae435cb48f7fc436c9f9098b92a0d", + ); + assert.equal( + Buffer.from(entry.message).toString("hex"), + "3034b1d58628a842984da0c70da0b5a251ebb2aebf51afc5b586e2839b5e5263", + ); + + entry = entries[2]; + assert.equal( + entry.label, + "e5dc552b4b170cdec4405ff8f9af20313bf0e2756d06c35877b6fbcfa6b354a7b3e2d439ea87999befb09a8fa1b3f014e57ec747bf738c4199338fcd4a87b373dd62f5c8329f1f5f245956bbb06685596a2e83dc38befa63e4a2b5c4ce408506", + ); + assert.equal( + Buffer.from(entry.message).toString("hex"), + "de7e1b385edbb0e1e8f9fc25d91bd8eed71a1da7caab732e6b47a48042d8523d", + ); + + entry = entries[3]; + assert.equal( + entry.label, + "12773304cb718250edd89770cedcbf675ccdb7fe2b30bd3185ca65ffa0d516879768ed03f92e41a6e5bc5340b78a9d02655e3b727c79730ead791fb68eaa02b84e1be92a816a9604a1ab9a6d3874b638487e2145239438a4bafac3889348d405", + ); + assert.equal( + Buffer.from(entry.message).toString("hex"), + "8ebeb07d296ad2529400b40687a741a135f8357f79f39fcb2894a6f9703a5816", + ); + }); + it("should convert to text", () => { + let text = readFileSync(path.join(walletsPath, "alice.pem"), "utf-8").trim(); + assert.deepEqual(PemEntry.fromTextAll(text)[0].toText(), text); + + text = readFileSync(path.join(walletsPath, "validatorKey00.pem"), "utf-8").trim(); + assert.deepEqual(PemEntry.fromTextAll(text)[0].toText(), text); + + text = readFileSync(path.join(walletsPath, "multipleUserKeys.pem"), "utf-8").trim(); + let entries = PemEntry.fromTextAll(text); + let actualText = entries.map((entry) => entry.toText()).join("\n"); + assert.deepEqual(actualText, text); + + text = readFileSync(path.join(walletsPath, "multipleValidatorKeys.pem"), "utf-8").trim(); + entries = PemEntry.fromTextAll(text); + actualText = entries.map((entry) => entry.toText()).join("\n"); + assert.deepEqual(actualText, text); + }); +}); diff --git a/src/wallet/pemEntry.ts b/src/wallet/pemEntry.ts new file mode 100644 index 000000000..ef2a5c9e2 --- /dev/null +++ b/src/wallet/pemEntry.ts @@ -0,0 +1,85 @@ +export class PemEntry { + label: string; + message: Uint8Array; + + constructor(label: string, message: Uint8Array) { + this.label = label; + this.message = message; + } + + static fromTextAll(pemText: string): PemEntry[] { + const lines = PemEntry.cleanLines(pemText.split("\n")); + + // Group PEM entries into blocks of header, content, and footer + const blocks: string[][] = PemEntry.groupBlocks(lines); + + return blocks.map((block) => { + // Extract label from the header line + const header = block[0]; + const footer = block[block.length - 1]; + if (!header.startsWith("-----BEGIN PRIVATE KEY for") || !footer.startsWith("-----END PRIVATE KEY for")) { + throw new Error("Invalid PEM format"); + } + + const label = header.replace("-----BEGIN PRIVATE KEY for", "").replace("-----", "").trim(); + + // Join all content lines between header and footer + const base64Message = block.slice(1, block.length - 1).join(""); + + // Decode Base64 to Uint8Array + const messageBytes = Buffer.from(Buffer.from(base64Message, "base64").toString(), "hex"); + + return new PemEntry(label, messageBytes); + }); + } + + private static groupBlocks(lines: string[]): string[][] { + const blocks: string[][] = []; + let currentBlock: string[] = []; + + for (const line of lines) { + if (line.startsWith("-----BEGIN PRIVATE KEY for")) { + if (currentBlock.length > 0) { + blocks.push(currentBlock); + } + currentBlock = [line]; // Start a new block + } else if (line.startsWith("-----END PRIVATE KEY for")) { + currentBlock.push(line); + blocks.push(currentBlock); // Finalize the current block + currentBlock = []; + } else { + currentBlock.push(line); // Add content to the current block + } + } + + if (currentBlock.length > 0) { + throw new Error("Invalid PEM format: Missing END line for a block"); + } + + return blocks; + } + + toText(): string { + const header = `-----BEGIN PRIVATE KEY for ${this.label}-----`; + const footer = `-----END PRIVATE KEY for ${this.label}-----`; + + const messageHex = Buffer.from(this.message).toString("hex"); + const messageBase64 = Buffer.from(messageHex, "utf-8").toString("base64"); + const payloadLines = PemEntry.wrapText(messageBase64, 64); + const payload = payloadLines.join("\n"); + + return [header, payload, footer].join("\n"); + } + + private static cleanLines(lines: string[]): string[] { + return lines.map((line) => line.trim()).filter((line) => line.length > 0); + } + + private static wrapText(text: string, width: number): string[] { + const lines: string[] = []; + for (let i = 0; i < text.length; i += width) { + lines.push(text.slice(i, i + width)); + } + return lines; + } +} diff --git a/src/wallet/userKeys.ts b/src/wallet/userKeys.ts index eaee0e10a..0a0259cec 100644 --- a/src/wallet/userKeys.ts +++ b/src/wallet/userKeys.ts @@ -1,6 +1,7 @@ import * as ed from "@noble/ed25519"; import { sha512 } from "@noble/hashes/sha512"; -import { Address } from "../address"; +import nacl from "tweetnacl"; +import { Address } from "../core/address"; import { guardLength } from "./assertions"; import { parseUserKey } from "./pem"; @@ -36,9 +37,17 @@ export class UserSecretKey { return new UserPublicKey(buffer); } - sign(message: Buffer | Uint8Array): Buffer { + static generate(): UserSecretKey { + // Generates a new signing keypair + const keyPair = nacl.sign.keyPair(); + // Extract only the private key part + const secretKey = keyPair.secretKey.subarray(0, USER_SEED_LENGTH); + return new UserSecretKey(secretKey); + } + + sign(message: Uint8Array): Uint8Array { const signature = ed.sync.sign(new Uint8Array(message), new Uint8Array(this.buffer)); - return Buffer.from(signature); + return signature; } hex(): string { @@ -59,7 +68,7 @@ export class UserPublicKey { this.buffer = Buffer.from(buffer); } - verify(data: Buffer | Uint8Array, signature: Buffer | Uint8Array): boolean { + async verify(data: Buffer | Uint8Array, signature: Buffer | Uint8Array): Promise { try { const ok = ed.sync.verify(new Uint8Array(signature), new Uint8Array(data), new Uint8Array(this.buffer)); return ok; diff --git a/src/wallet/userPem.ts b/src/wallet/userPem.ts new file mode 100644 index 000000000..5aafddade --- /dev/null +++ b/src/wallet/userPem.ts @@ -0,0 +1,59 @@ +import { PathLike, readFileSync, writeFileSync } from "fs"; +import { isAbsolute, join, resolve } from "path"; +import { PemEntry } from "./pemEntry"; +import { USER_SEED_LENGTH, UserPublicKey, UserSecretKey } from "./userKeys"; + +export class UserPem { + label: string; + secretKey: UserSecretKey; + publicKey: UserPublicKey; + + constructor(label: string, secretKey: UserSecretKey) { + this.label = label; + this.secretKey = secretKey; + this.publicKey = secretKey.generatePublicKey(); + } + + static fromFile(path: PathLike, index: number = 0): UserPem { + return this.fromFileAll(path)[index]; + } + + static fromFileAll(path: PathLike): UserPem[] { + const resolvedPath = isAbsolute(path.toString()) + ? resolve(path.toString()) + : resolve(join(process.cwd(), path.toString())); + const text = readFileSync(resolvedPath, "utf-8"); + return this.fromTextAll(text); + } + + static fromText(text: string, index: number = 0): UserPem { + const items = this.fromTextAll(text); + return items[index]; + } + + static fromTextAll(text: string): UserPem[] { + const entries = PemEntry.fromTextAll(text); + const resultItems: UserPem[] = []; + + for (const entry of entries) { + const secretKey = new UserSecretKey(entry.message.slice(0, USER_SEED_LENGTH)); + const item = new UserPem(entry.label, secretKey); + resultItems.push(item); + } + + return resultItems; + } + + save(path: PathLike): void { + const resolvedPath = isAbsolute(path.toString()) + ? resolve(path.toString()) + : resolve(join(process.cwd(), path.toString())); + writeFileSync(resolvedPath, this.toText(), { encoding: "utf-8" }); + } + + toText(): string { + const message = new Uint8Array([...this.secretKey.valueOf(), ...this.publicKey.valueOf()]); + const pemEntry = new PemEntry(this.label, message); + return pemEntry.toText(); + } +} diff --git a/src/wallet/userSigner.ts b/src/wallet/userSigner.ts index acc8f6f09..b8313b150 100644 --- a/src/wallet/userSigner.ts +++ b/src/wallet/userSigner.ts @@ -1,24 +1,15 @@ -import { Address } from "../address"; -import { ErrSignerCannotSign } from "../errors"; +import { Address } from "../core/address"; +import { ErrSignerCannotSign } from "../core/errors"; import { UserSecretKey } from "./userKeys"; import { UserWallet } from "./userWallet"; -interface IUserSecretKey { - sign(message: Buffer | Uint8Array): Buffer; - generatePublicKey(): IUserPublicKey; -} - -interface IUserPublicKey { - toAddress(hrp?: string): { bech32(): string }; -} - /** * ed25519 signer */ export class UserSigner { - protected readonly secretKey: IUserSecretKey; + readonly secretKey: UserSecretKey; - constructor(secretKey: IUserSecretKey) { + constructor(secretKey: UserSecretKey) { this.secretKey = secretKey; } @@ -32,7 +23,7 @@ export class UserSigner { return new UserSigner(secretKey); } - async sign(data: Buffer | Uint8Array): Promise { + async sign(data: Uint8Array): Promise { try { const signature = this.secretKey.sign(data); return signature; @@ -45,7 +36,7 @@ export class UserSigner { * Gets the address of the signer. */ getAddress(hrp?: string): Address { - const bech32 = this.secretKey.generatePublicKey().toAddress(hrp).bech32(); + const bech32 = this.secretKey.generatePublicKey().toAddress(hrp).toBech32(); return Address.newFromBech32(bech32); } } diff --git a/src/wallet/userVerifier.ts b/src/wallet/userVerifier.ts index ca56585a0..00426ca47 100644 --- a/src/wallet/userVerifier.ts +++ b/src/wallet/userVerifier.ts @@ -1,31 +1,28 @@ +import { Address } from "../core/address"; import { UserPublicKey } from "./userKeys"; -interface IAddress { - pubkey(): Buffer; -} - /** * ed25519 signature verification */ export class UserVerifier { - publicKey: UserPublicKey; + publicKey: UserPublicKey; - constructor(publicKey: UserPublicKey) { - this.publicKey = publicKey; - } + constructor(publicKey: UserPublicKey) { + this.publicKey = publicKey; + } - static fromAddress(address: IAddress): UserVerifier { - let publicKey = new UserPublicKey(address.pubkey()); - return new UserVerifier(publicKey); - } + static fromAddress(address: Address): UserVerifier { + let publicKey = new UserPublicKey(address.getPublicKey()); + return new UserVerifier(publicKey); + } - /** - * - * @param data the raw data to be verified (e.g. an already-serialized enveloped message) - * @param signature the signature to be verified - * @returns true if the signature is valid, false otherwise - */ - verify(data: Buffer | Uint8Array, signature: Buffer | Uint8Array): boolean { - return this.publicKey.verify(data, signature); - } + /** + * + * @param data the raw data to be verified (e.g. an already-serialized enveloped message) + * @param signature the signature to be verified + * @returns true if the signature is valid, false otherwise + */ + async verify(data: Buffer | Uint8Array, signature: Buffer | Uint8Array): Promise { + return this.publicKey.verify(data, signature); + } } diff --git a/src/wallet/userWallet.ts b/src/wallet/userWallet.ts index 743f4ac69..70712d339 100644 --- a/src/wallet/userWallet.ts +++ b/src/wallet/userWallet.ts @@ -1,4 +1,8 @@ -import { Err } from "../errors"; +import { PathLike, readFileSync, writeFileSync } from "fs"; +import path, { isAbsolute, join, resolve } from "path"; +import { LibraryConfig } from "../core/config"; +import { Err } from "../core/errors"; +import { Logger } from "../core/logger"; import { CipherAlgorithm, Decryptor, EncryptedData, Encryptor, KeyDerivationFunction, Randomness } from "./crypto"; import { ScryptKeyDerivationParams } from "./crypto/derivationParams"; import { Mnemonic } from "./mnemonic"; @@ -77,6 +81,31 @@ export class UserWallet { }); } + static loadSecretKey(filePath: string, password: string, addressIndex?: number): UserSecretKey { + // Load and parse the keystore file + const keyFileJson = readFileSync(path.resolve(filePath), "utf8"); + const keyFileObject = JSON.parse(keyFileJson); + const kind = keyFileObject.kind || UserWalletKind.SecretKey.valueOf(); + + Logger.debug(`UserWallet.loadSecretKey(), kind = ${kind}`); + + let secretKey: UserSecretKey; + + if (kind === UserWalletKind.SecretKey.valueOf()) { + if (addressIndex !== undefined) { + throw new Error("address_index must not be provided when kind == 'secretKey'"); + } + secretKey = UserWallet.decryptSecretKey(keyFileObject, password); + } else if (kind === UserWalletKind.Mnemonic.valueOf()) { + const mnemonic = UserWallet.decryptMnemonic(keyFileObject, password); + secretKey = mnemonic.deriveKey(addressIndex || 0); + } else { + throw new Error(`Unknown kind: ${kind}`); + } + + return secretKey; + } + static decrypt(keyFileObject: any, password: string, addressIndex?: number): UserSecretKey { const kind = keyFileObject.kind || UserWalletKind.SecretKey; @@ -214,4 +243,13 @@ export class UserWallet { crypto: cryptoSection, }; } + + save(path: PathLike, addressHrp: string = LibraryConfig.DefaultAddressHrp): void { + const resolvedPath = isAbsolute(path.toString()) + ? resolve(path.toString()) + : resolve(join(process.cwd(), path.toString())); + + const jsonContent = this.toJSON(addressHrp); + writeFileSync(resolvedPath, jsonContent, { encoding: "utf-8" }); + } } diff --git a/src/wallet/users.spec.ts b/src/wallet/users.spec.ts index 976f4ef85..b24138326 100644 --- a/src/wallet/users.spec.ts +++ b/src/wallet/users.spec.ts @@ -1,7 +1,7 @@ import { assert } from "chai"; -import { ErrBadMnemonicEntropy, ErrInvariantFailed } from "../errors"; -import { TestMessage } from "./../testutils/message"; -import { TestTransaction } from "./../testutils/transaction"; +import path from "path"; +import { Account } from "../accounts"; +import { Address, ErrBadMnemonicEntropy, ErrInvariantFailed, Message, Transaction } from "../core"; import { DummyMnemonicOf12Words, loadMnemonic, @@ -17,15 +17,17 @@ import { UserSigner } from "./userSigner"; import { UserVerifier } from "./userVerifier"; import { UserWallet } from "./userWallet"; -describe("test user wallets", async () => { +describe("test user wallets", () => { let alice: TestWallet, bob: TestWallet, carol: TestWallet; - let password: string = await loadPassword(); - const dummyMnemonic = await loadMnemonic(); + let password: string; + let dummyMnemonic: string; before(async function () { alice = await loadTestWallet("alice"); bob = await loadTestWallet("bob"); carol = await loadTestWallet("carol"); + password = await loadPassword(); + dummyMnemonic = await loadMnemonic(); }); it("should generate mnemonic", () => { @@ -74,28 +76,28 @@ describe("test user wallets", async () => { const mnemonic = Mnemonic.fromString(DummyMnemonicOf12Words); assert.equal( - mnemonic.deriveKey(0).generatePublicKey().toAddress().bech32(), + mnemonic.deriveKey(0).generatePublicKey().toAddress().toBech32(), "erd1l8g9dk3gz035gkjhwegsjkqzdu3augrwhcfxrnucnyyrpc2220pqg4g7na", ); assert.equal( - mnemonic.deriveKey(1).generatePublicKey().toAddress().bech32(), + mnemonic.deriveKey(1).generatePublicKey().toAddress().toBech32(), "erd1fmhwg84rldg0xzngf53m0y607wvefvamh07n2mkypedx27lcqnts4zs09p", ); assert.equal( - mnemonic.deriveKey(2).generatePublicKey().toAddress().bech32(), + mnemonic.deriveKey(2).generatePublicKey().toAddress().toBech32(), "erd1tyuyemt4xz2yjvc7rxxp8kyfmk2n3h8gv3aavzd9ru4v2vhrkcksptewtj", ); assert.equal( - mnemonic.deriveKey(0).generatePublicKey().toAddress("test").bech32(), + mnemonic.deriveKey(0).generatePublicKey().toAddress("test").toBech32(), "test1l8g9dk3gz035gkjhwegsjkqzdu3augrwhcfxrnucnyyrpc2220pqc6tnnf", ); assert.equal( - mnemonic.deriveKey(1).generatePublicKey().toAddress("xerd").bech32(), + mnemonic.deriveKey(1).generatePublicKey().toAddress("xerd").toBech32(), "xerd1fmhwg84rldg0xzngf53m0y607wvefvamh07n2mkypedx27lcqntsj4adj4", ); assert.equal( - mnemonic.deriveKey(2).generatePublicKey().toAddress("yerd").bech32(), + mnemonic.deriveKey(2).generatePublicKey().toAddress("yerd").toBech32(), "yerd1tyuyemt4xz2yjvc7rxxp8kyfmk2n3h8gv3aavzd9ru4v2vhrkcksn8p0n5", ); }); @@ -151,9 +153,9 @@ describe("test user wallets", async () => { let carolKeyFile = UserWallet.fromSecretKey({ secretKey: carolSecretKey, password: password }); console.timeEnd("encrypt"); - assert.equal(aliceKeyFile.toJSON().bech32, alice.address.bech32()); - assert.equal(bobKeyFile.toJSON().bech32, bob.address.bech32()); - assert.equal(carolKeyFile.toJSON().bech32, carol.address.bech32()); + assert.equal(aliceKeyFile.toJSON().bech32, alice.address.toBech32()); + assert.equal(bobKeyFile.toJSON().bech32, bob.address.toBech32()); + assert.equal(carolKeyFile.toJSON().bech32, carol.address.toBech32()); console.time("decrypt"); assert.deepEqual(UserWallet.decryptSecretKey(aliceKeyFile.toJSON(), password), aliceSecretKey); @@ -203,7 +205,7 @@ describe("test user wallets", async () => { const secretKey = UserWallet.decryptSecretKey(keyFileObject, password); assert.equal( - secretKey.generatePublicKey().toAddress().bech32(), + secretKey.generatePublicKey().toAddress().toBech32(), "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", ); }); @@ -216,15 +218,15 @@ describe("test user wallets", async () => { assert.equal(json.version, 4); assert.equal(json.kind, "mnemonic"); - assert.isUndefined(json.bech32); + assert.isUndefined(json.toBech32); const mnemonic = UserWallet.decryptMnemonic(json, password); const mnemonicText = mnemonic.toString(); assert.equal(mnemonicText, dummyMnemonic); - assert.equal(mnemonic.deriveKey(0).generatePublicKey().toAddress().bech32(), alice.address.bech32()); - assert.equal(mnemonic.deriveKey(1).generatePublicKey().toAddress().bech32(), bob.address.bech32()); - assert.equal(mnemonic.deriveKey(2).generatePublicKey().toAddress().bech32(), carol.address.bech32()); + assert.equal(mnemonic.deriveKey(0).generatePublicKey().toAddress().toBech32(), alice.address.toBech32()); + assert.equal(mnemonic.deriveKey(1).generatePublicKey().toAddress().toBech32(), bob.address.toBech32()); + assert.equal(mnemonic.deriveKey(2).generatePublicKey().toAddress().toBech32(), carol.address.toBech32()); // With provided randomness, in order to reproduce our test wallets const expectedDummyWallet = await loadTestKeystore("withDummyMnemonic.json"); @@ -241,12 +243,23 @@ describe("test user wallets", async () => { assert.deepEqual(dummyWallet.toJSON(), expectedDummyWallet); }); - it("should loadSecretKey, but without 'kind' field", async function () { + it("should create user wallet from secret key, but without 'kind' field", async function () { const keyFileObject = await loadTestKeystore("withoutKind.json"); const secretKey = UserWallet.decrypt(keyFileObject, password); assert.equal( - secretKey.generatePublicKey().toAddress().bech32(), + secretKey.generatePublicKey().toAddress().toBech32(), + "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + ); + }); + + it("should loadSecretKey, but without 'kind' field", async function () { + const testdataPath = path.resolve(__dirname, "..", "testdata/testwallets"); + const keystorePath = path.resolve(testdataPath, "withoutKind.json"); + const secretKey = UserWallet.loadSecretKey(keystorePath, password); + + assert.equal( + secretKey.generatePublicKey().toAddress().toBech32(), "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", ); }); @@ -264,21 +277,21 @@ describe("test user wallets", async () => { const keyFileObject = await loadTestKeystore("withDummyMnemonic.json"); assert.equal( - UserWallet.decrypt(keyFileObject, password, 0).generatePublicKey().toAddress().bech32(), + UserWallet.decrypt(keyFileObject, password, 0).generatePublicKey().toAddress().toBech32(), "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", ); assert.equal( - UserWallet.decrypt(keyFileObject, password, 1).generatePublicKey().toAddress().bech32(), + UserWallet.decrypt(keyFileObject, password, 1).generatePublicKey().toAddress().toBech32(), "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx", ); assert.equal( - UserWallet.decrypt(keyFileObject, password, 2).generatePublicKey().toAddress().bech32(), + UserWallet.decrypt(keyFileObject, password, 2).generatePublicKey().toAddress().toBech32(), "erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8", ); }); it("should sign transactions", async () => { - let signer = new UserSigner( + let signer = new Account( UserSecretKey.fromString("1a927e2af5306a9bb2ea777f73e06ecc0ac9aaa72fb4ea3fecf659451394cccf"), ); let verifier = new UserVerifier( @@ -288,13 +301,14 @@ describe("test user wallets", async () => { ); // With data field - let transaction = new TestTransaction({ - nonce: 0, - value: "0", - receiver: "erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r", - gasPrice: 1000000000, - gasLimit: 50000, - data: "foo", + let transaction = new Transaction({ + nonce: 0n, + value: 0n, + sender: Address.newFromBech32("erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz"), + receiver: Address.newFromBech32("erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r"), + gasPrice: 1000000000n, + gasLimit: 50000n, + data: new TextEncoder().encode("foo"), chainID: "1", }); @@ -302,23 +316,24 @@ describe("test user wallets", async () => { let signature = await signer.sign(serialized); assert.deepEqual(await signer.sign(serialized), await signer.sign(Uint8Array.from(serialized))); - assert.equal( + assert.deepEqual( serialized.toString(), - `{"nonce":0,"value":"0","receiver":"erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r","sender":"","gasPrice":1000000000,"gasLimit":50000,"data":"Zm9v","chainID":"1","version":1}`, + `{"nonce":0,"value":"0","receiver":"erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r","sender":"erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz","gasPrice":1000000000,"gasLimit":50000,"data":"Zm9v","chainID":"1","version":2}`, ); assert.equal( - signature.toString("hex"), - "a3b61a2fe461f3393c42e6cb0477a6b52ffd92168f10c111f6aa8d0a310ee0c314fae0670f8313f1ad992933ac637c61a8ff20cc20b6a8b2260a4af1a120a70d", + Buffer.from(signature).toString("hex"), + "a5db62c6186612d44094f83576aa6a664299315fb6e42d0c17a40e9cd33efa9a9df8b76943aeac7dceaff3d78a16a7414c914f03f7a88e786c2cf939eb111c06", ); - assert.isTrue(verifier.verify(serialized, signature)); + assert.isTrue(await verifier.verify(serialized, signature)); // Without data field - transaction = new TestTransaction({ - nonce: 8, - value: "10000000000000000000", - receiver: "erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r", - gasPrice: 1000000000, - gasLimit: 50000, + transaction = new Transaction({ + nonce: 8n, + value: 10000000000000000000n, + sender: Address.newFromBech32("erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz"), + receiver: Address.newFromBech32("erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r"), + gasPrice: 1000000000n, + gasLimit: 50000n, chainID: "1", }); @@ -328,11 +343,11 @@ describe("test user wallets", async () => { assert.deepEqual(await signer.sign(serialized), await signer.sign(Uint8Array.from(serialized))); assert.equal( serialized.toString(), - `{"nonce":8,"value":"10000000000000000000","receiver":"erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r","sender":"","gasPrice":1000000000,"gasLimit":50000,"chainID":"1","version":1}`, + `{"nonce":8,"value":"10000000000000000000","receiver":"erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r","sender":"erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz","gasPrice":1000000000,"gasLimit":50000,"chainID":"1","version":2}`, ); assert.equal( - signature.toString("hex"), - "f136c901d37349a7da8cfe3ab5ec8ef333b0bc351517c0e9bef9eb9704aed3077bf222769cade5ff29dffe5f42e4f0c5e0b068bdba90cd2cb41da51fd45d5a03", + Buffer.from(signature).toString("hex"), + "024f007f7eae87141b34708e33afd66c85a49ea8c8422e55292832ee870f879cdc033d2511c174d0f2ed62799b9f597c4a8399309578a258f558131d74374f0d", ); }); @@ -349,16 +364,16 @@ describe("test user wallets", async () => { let guardianSigner = new UserSigner(UserSecretKey.fromPem(bob.pemFileText)); // With data field - let transaction = new TestTransaction({ - nonce: 0, - value: "0", - receiver: "erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r", - sender: "erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz", - gasPrice: 1000000000, - gasLimit: 50000, - data: "foo", + let transaction = new Transaction({ + nonce: 0n, + value: 0n, + receiver: Address.newFromBech32("erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r"), + sender: Address.newFromBech32("erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz"), + gasPrice: 1000000000n, + gasLimit: 50000n, + data: new TextEncoder().encode("foo"), chainID: "1", - guardian: "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx", + guardian: Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"), options: 2, version: 2, }); @@ -367,30 +382,30 @@ describe("test user wallets", async () => { let signature = await signer.sign(serialized); let guardianSignature = await guardianSigner.sign(serialized); - assert.equal( + assert.deepEqual( serialized.toString(), - `{"nonce":0,"value":"0","receiver":"erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r","sender":"erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz","guardian":"erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx","gasPrice":1000000000,"gasLimit":50000,"data":"Zm9v","chainID":"1","options":2,"version":2}`, + `{"nonce":0,"value":"0","receiver":"erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r","sender":"erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz","gasPrice":1000000000,"gasLimit":50000,"data":"Zm9v","chainID":"1","version":2,"options":2,"guardian":"erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"}`, ); assert.equal( - signature.toString("hex"), - "00b867ae749616954711ef227c0a3f5c6556246f26dbde12ad929a099094065341a0fae7c5ced98e6bdd100ce922c975667444ea859dce9597b46e63cade2a03", + Buffer.from(signature).toString("hex"), + "fa067dc9508ec9df04896665fc9c9e3e7e9cbdc6577c10d56128e3c891ea502572be637bd7cdfb466779cee3e208a2be1f32b0267af1710a6532848e5e5e6f0d", ); assert.equal( - guardianSignature.toString("hex"), - "1326e44941ef7bfbad3edf346e72abe23704ee32b4b6a6a6a9b793bd7c62b6d4a69d3c6ea2dddf7eabc8df8fe291cd24822409ab9194b6a0f3bbbf1c59b0a10f", + Buffer.from(guardianSignature).toString("hex"), + "5695fde5d9c77a94bb320438fbebe3bbd60b7cc4d633fb38e42bb65f83d253cbb82cc5ae40d701a7f0b839a5231320ca356018ced949885baae473e469ec770e", ); - assert.isTrue(verifier.verify(serialized, signature)); + assert.isTrue(await verifier.verify(serialized, signature)); // Without data field - transaction = new TestTransaction({ - nonce: 8, - value: "10000000000000000000", - receiver: "erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r", - sender: "erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz", - gasPrice: 1000000000, - gasLimit: 50000, + transaction = new Transaction({ + nonce: 8n, + value: 10000000000000000000n, + receiver: Address.newFromBech32("erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r"), + sender: Address.newFromBech32("erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz"), + gasPrice: 1000000000n, + gasLimit: 50000n, chainID: "1", - guardian: "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx", + guardian: Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"), options: 2, version: 2, }); @@ -401,29 +416,30 @@ describe("test user wallets", async () => { assert.equal( serialized.toString(), - `{"nonce":8,"value":"10000000000000000000","receiver":"erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r","sender":"erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz","guardian":"erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx","gasPrice":1000000000,"gasLimit":50000,"chainID":"1","options":2,"version":2}`, + `{"nonce":8,"value":"10000000000000000000","receiver":"erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r","sender":"erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz","gasPrice":1000000000,"gasLimit":50000,"chainID":"1","version":2,"options":2,"guardian":"erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"}`, ); assert.equal( - signature.toString("hex"), - "49a63fa0e3cfb81a2b6d926c741328fb270ea4f58fa32585fe8aa3cde191245e5a13c5c059d5576f4c05fc24d2534a2124ff79c98d067ce8412c806779066b03", + Buffer.from(signature).toString("hex"), + "50d61a408cf032b3e70b15ecc313dbea43e35a1b33ea89aadb42b25a672d3427147bcda0d911be539629fcd3183c22b30f8ac30023abb230b13abf2cd1befd04", ); assert.equal( - guardianSignature.toString("hex"), - "4c25a54381bf66576d05f32659d30672b5b0bfbfb6b6aee52290d28cfbc87860637f095f83663a1893d12d0d5a27b2ab3325829ff1f1215b81a7ced8ee5d7203", + Buffer.from(guardianSignature).toString("hex"), + "ea3b83adcc468b0c7d3613fca5f429a9764d5710137c34c27e15d06e625326724ccfa758968507acadb14345d19389ba6004a4f0a6c527799c01713e10cf650b", ); - assert.isTrue(verifier.verify(serialized, signature)); + assert.isTrue(await verifier.verify(serialized, signature)); }); it("should sign transactions using PEM files", async () => { const signer = UserSigner.fromPem(alice.pemFileText); - const transaction = new TestTransaction({ - nonce: 0, - value: "0", - receiver: "erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r", - gasPrice: 1000000000, - gasLimit: 50000, - data: "foo", + const transaction = new Transaction({ + nonce: 0n, + value: 0n, + sender: signer.getAddress(), + receiver: Address.newFromBech32("erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r"), + gasPrice: 1000000000n, + gasLimit: 50000n, + data: new TextEncoder().encode("foo"), chainID: "1", }); @@ -432,8 +448,8 @@ describe("test user wallets", async () => { assert.deepEqual(await signer.sign(serialized), await signer.sign(Uint8Array.from(serialized))); assert.equal( - signature.toString("hex"), - "ba4fa95fea1402e4876abf1d5a510615aab374ee48bb76f5230798a7d3f2fcae6ba91ba56c6d62e6e7003ce531ff02f219cb7218dd00dd2ca650ba747f19640a", + Buffer.from(signature).toString("hex"), + "b6feb8b50711cc8436040de561355e94585b2cf9e33e9e887125ad9c6877829dbc75afaf878c690e249455b738e89f63067930bc8c46fcf0779ac0bd3590a206", ); }); @@ -447,19 +463,17 @@ describe("test user wallets", async () => { ).generatePublicKey(), ); - const message = new TestMessage({ - foo: "hello", - bar: "world", + const message = new Message({ + data: new TextEncoder().encode(JSON.stringify({ foo: "hello", bar: "world" })), }); - const data = message.serializeForSigning(); - const signature = await signer.sign(data); + const signature = await signer.sign(message.data); - assert.deepEqual(await signer.sign(data), await signer.sign(Uint8Array.from(data))); - assert.isTrue(verifier.verify(data, signature)); - assert.isTrue(verifier.verify(Uint8Array.from(data), Uint8Array.from(signature))); - assert.isFalse(verifier.verify(Buffer.from("hello"), signature)); - assert.isFalse(verifier.verify(new TextEncoder().encode("hello"), signature)); + assert.deepEqual(await signer.sign(message.data), await signer.sign(Uint8Array.from(message.data))); + assert.isTrue(await verifier.verify(message.data, signature)); + assert.isTrue(await verifier.verify(Uint8Array.from(message.data), Uint8Array.from(signature))); + assert.isFalse(await verifier.verify(Buffer.from("hello"), signature)); + assert.isFalse(await verifier.verify(new TextEncoder().encode("hello"), signature)); }); it("should create UserSigner from wallet", async function () { @@ -468,40 +482,40 @@ describe("test user wallets", async () => { const keyFileObjectWithSecretKey = await loadTestKeystore("withDummySecretKey.json"); assert.equal( - UserSigner.fromWallet(keyFileObjectWithoutKind, password).getAddress().bech32(), + UserSigner.fromWallet(keyFileObjectWithoutKind, password).getAddress().toBech32(), "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", ); assert.equal( - UserSigner.fromWallet(keyFileObjectWithMnemonic, password).getAddress().bech32(), + UserSigner.fromWallet(keyFileObjectWithMnemonic, password).getAddress().toBech32(), "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", ); assert.equal( - UserSigner.fromWallet(keyFileObjectWithSecretKey, password).getAddress().bech32(), + UserSigner.fromWallet(keyFileObjectWithSecretKey, password).getAddress().toBech32(), "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", ); assert.equal( - UserSigner.fromWallet(keyFileObjectWithMnemonic, password, 0).getAddress().bech32(), + UserSigner.fromWallet(keyFileObjectWithMnemonic, password, 0).getAddress().toBech32(), "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", ); assert.equal( - UserSigner.fromWallet(keyFileObjectWithMnemonic, password, 1).getAddress().bech32(), + UserSigner.fromWallet(keyFileObjectWithMnemonic, password, 1).getAddress().toBech32(), "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx", ); assert.equal( - UserSigner.fromWallet(keyFileObjectWithMnemonic, password, 2).getAddress().bech32(), + UserSigner.fromWallet(keyFileObjectWithMnemonic, password, 2).getAddress().toBech32(), "erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8", ); assert.equal( - UserSigner.fromWallet(keyFileObjectWithMnemonic, password, 0).getAddress("test").bech32(), + UserSigner.fromWallet(keyFileObjectWithMnemonic, password, 0).getAddress("test").toBech32(), "test1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ss5hqhtr", ); assert.equal( - UserSigner.fromWallet(keyFileObjectWithMnemonic, password, 1).getAddress("xerd").bech32(), + UserSigner.fromWallet(keyFileObjectWithMnemonic, password, 1).getAddress("xerd").toBech32(), "xerd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruq9thc9j", ); assert.equal( - UserSigner.fromWallet(keyFileObjectWithMnemonic, password, 2).getAddress("yerd").bech32(), + UserSigner.fromWallet(keyFileObjectWithMnemonic, password, 2).getAddress("yerd").toBech32(), "yerd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaqgh23pp", ); }); diff --git a/src/wallet/usersBenchmark.spec.ts b/src/wallet/usersBenchmark.spec.ts index eeb318e0a..5d053ef90 100644 --- a/src/wallet/usersBenchmark.spec.ts +++ b/src/wallet/usersBenchmark.spec.ts @@ -10,7 +10,7 @@ describe("behchmark sign and verify", () => { const secretKeys: UserSecretKey[] = []; const publicKeys: UserPublicKey[] = []; const messages: Buffer[] = []; - const goodSignatures: Buffer[] = []; + const goodSignatures: Uint8Array[] = []; for (let i = 0; i < n; i++) { const secretKey = new UserSecretKey(Buffer.from(utils.randomBytes(32))); @@ -36,7 +36,7 @@ describe("behchmark sign and verify", () => { console.time("verify (good)"); for (let i = 0; i < n; i++) { - const ok = publicKeys[i].verify(messages[i], goodSignatures[i]); + const ok = await publicKeys[i].verify(messages[i], goodSignatures[i]); assert.isTrue(ok); } @@ -45,7 +45,7 @@ describe("behchmark sign and verify", () => { console.time("verify (bad)"); for (let i = 0; i < n; i++) { - const ok = publicKeys[i].verify(messages[messages.length - i - 1], goodSignatures[i]); + const ok = await publicKeys[i].verify(messages[messages.length - i - 1], goodSignatures[i]); assert.isFalse(ok); } diff --git a/src/wallet/validatorKeys.ts b/src/wallet/validatorKeys.ts index 4b2b856f7..888f80c1b 100644 --- a/src/wallet/validatorKeys.ts +++ b/src/wallet/validatorKeys.ts @@ -1,4 +1,4 @@ -import { ErrInvariantFailed } from "../errors"; +import { ErrInvariantFailed } from "../core/errors"; import { guardLength } from "./assertions"; import { parseValidatorKey } from "./pem"; diff --git a/src/wallet/validatorSigner.ts b/src/wallet/validatorSigner.ts index 161c2dad1..7842c53a6 100644 --- a/src/wallet/validatorSigner.ts +++ b/src/wallet/validatorSigner.ts @@ -1,4 +1,4 @@ -import { ErrSignerCannotSign } from "../errors"; +import { ErrSignerCannotSign } from "../core/errors"; import { BLS, ValidatorSecretKey } from "./validatorKeys"; /** diff --git a/src/wallet/validators.spec.ts b/src/wallet/validators.spec.ts index 5a01b7a45..c3a93bae5 100644 --- a/src/wallet/validators.spec.ts +++ b/src/wallet/validators.spec.ts @@ -2,31 +2,54 @@ import { assert } from "chai"; import { BLS, ValidatorSecretKey } from "./validatorKeys"; describe("test validator keys", () => { - it("should create secret key and sign a message", async () => { await BLS.initIfNecessary(); - let secretKey = Buffer.from(Buffer.from("N2NmZjk5YmQ2NzE1MDJkYjdkMTViYzhhYmMwYzlhODA0ZmI5MjU0MDZmYmRkNTBmMWU0YzE3YTRjZDc3NDI0Nw==", "base64").toString(), "hex"); + let secretKey = Buffer.from( + Buffer.from( + "N2NmZjk5YmQ2NzE1MDJkYjdkMTViYzhhYmMwYzlhODA0ZmI5MjU0MDZmYmRkNTBmMWU0YzE3YTRjZDc3NDI0Nw==", + "base64", + ).toString(), + "hex", + ); let key = new ValidatorSecretKey(secretKey); - + assert.deepEqual(new ValidatorSecretKey(secretKey), new ValidatorSecretKey(Uint8Array.from(secretKey))); - assert.equal(key.generatePublicKey().hex(), "e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208"); + assert.equal( + key.generatePublicKey().hex(), + "e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208", + ); const data = Buffer.from("hello"); let signature = key.sign(data); assert.deepEqual(key.sign(data), key.sign(Uint8Array.from(data))); - assert.equal(signature.toString("hex"), "84fd0a3a9d4f1ea2d4b40c6da67f9b786284a1c3895b7253fec7311597cda3f757862bb0690a92a13ce612c33889fd86"); + assert.equal( + Buffer.from(signature).toString("hex"), + "84fd0a3a9d4f1ea2d4b40c6da67f9b786284a1c3895b7253fec7311597cda3f757862bb0690a92a13ce612c33889fd86", + ); - secretKey = Buffer.from(Buffer.from("ODA4NWJhMWQ3ZjdjM2RiOTM4YWQ3MDU5NWEyYmRhYjA5NjQ0ZjFlYzM4MDNiZTE3MWMzM2YxNGJjODBkNGUzYg==", "base64").toString(), "hex"); + secretKey = Buffer.from( + Buffer.from( + "ODA4NWJhMWQ3ZjdjM2RiOTM4YWQ3MDU5NWEyYmRhYjA5NjQ0ZjFlYzM4MDNiZTE3MWMzM2YxNGJjODBkNGUzYg==", + "base64", + ).toString(), + "hex", + ); key = new ValidatorSecretKey(secretKey); - assert.equal(key.generatePublicKey().hex(), "78689fd4b1e2e434d567fe01e61598a42717d83124308266bd09ccc15d2339dd318c019914b86ac29adbae5dd8a02d0307425e9bd85a296e94943708c72f8c670f0b7c50a890a5719088dbd9f1d062cad9acffa06df834106eebe1a4257ef00d"); + assert.equal( + key.generatePublicKey().hex(), + "78689fd4b1e2e434d567fe01e61598a42717d83124308266bd09ccc15d2339dd318c019914b86ac29adbae5dd8a02d0307425e9bd85a296e94943708c72f8c670f0b7c50a890a5719088dbd9f1d062cad9acffa06df834106eebe1a4257ef00d", + ); signature = key.sign(data); assert.deepEqual(key.sign(data), key.sign(Uint8Array.from(data))); - assert.equal(signature.toString("hex"), "be2e593ff10899a2ee8e1d5c8094e36c9f48e04b87e129991ff09475808743e07bb41bf6e7bc1463fa554c4b46594b98"); + assert.equal( + Buffer.from(signature).toString("hex"), + "be2e593ff10899a2ee8e1d5c8094e36c9f48e04b87e129991ff09475808743e07bb41bf6e7bc1463fa554c4b46594b98", + ); }); it("should handle PEM files", async () => { @@ -35,7 +58,13 @@ describe("test validator keys", () => { let text = `-----BEGIN foobar N2NmZjk5YmQ2NzE1MDJkYjdkMTViYzhhYmMwYzlhODA0ZmI5MjU0MDZmYmRkNTBmMWU0YzE3YTRjZDc3NDI0Nw== -----END foobar`; - assert.equal(ValidatorSecretKey.fromPem(text).hex(), "7cff99bd671502db7d15bc8abc0c9a804fb925406fbdd50f1e4c17a4cd774247"); - assert.equal(ValidatorSecretKey.fromPem(text).generatePublicKey().hex(), "e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208"); + assert.equal( + ValidatorSecretKey.fromPem(text).hex(), + "7cff99bd671502db7d15bc8abc0c9a804fb925406fbdd50f1e4c17a4cd774247", + ); + assert.equal( + ValidatorSecretKey.fromPem(text).generatePublicKey().hex(), + "e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208", + ); }); });