diff --git a/src/abi/index.ts b/src/abi/index.ts index 58f299694..58a6a1f79 100644 --- a/src/abi/index.ts +++ b/src/abi/index.ts @@ -8,7 +8,6 @@ export * from "./interactionChecker"; export * from "./interface"; export * from "./nativeSerializer"; export * from "./query"; -export * from "./resultsParser"; export * from "./returnCode"; export * from "./smartContract"; export * from "./transactionPayloadBuilders"; diff --git a/src/abi/interaction.local.net.spec.ts b/src/abi/interaction.local.net.spec.ts index 2af44644e..fd92cc089 100644 --- a/src/abi/interaction.local.net.spec.ts +++ b/src/abi/interaction.local.net.spec.ts @@ -1,30 +1,28 @@ import BigNumber from "bignumber.js"; import { assert } from "chai"; import { promises } from "fs"; -import { QueryRunnerAdapter } from "../adapters/queryRunnerAdapter"; -import { SmartContractQueriesController } from "../smartContractQueriesController"; -import { SmartContractTransactionsFactory } from "../smartContracts"; +import { SmartContractQueryInput } from "../smartContractQuery"; +import { + SmartContractController, + SmartContractTransactionsFactory, + SmartContractTransactionsOutcomeParser, +} from "../smartContracts"; 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 { TransactionsFactoryConfig } from "../transactionsFactoryConfig"; import { TransactionWatcher } from "../transactionWatcher"; import { Interaction } from "./interaction"; -import { ResultsParser } from "./resultsParser"; -import { ReturnCode } from "./returnCode"; import { SmartContract } from "./smartContract"; import { ManagedDecimalSignedValue, ManagedDecimalValue } from "./typesystem"; -describe("test smart contract interactor", function () { +describe.only("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 () { @@ -32,7 +30,11 @@ describe("test smart contract interactor", function () { let abiRegistry = await loadAbiRegistry("src/testdata/answer.abi.json"); let contract = new SmartContract({ abi: abiRegistry }); - let controller = new ContractController(provider); + let controller = new SmartContractController({ + chainID: "localnet", + networkProvider: provider, + abi: abiRegistry, + }); let network = await provider.getNetworkConfig(); await alice.sync(provider); @@ -47,10 +49,9 @@ describe("test smart contract interactor", function () { chainID: network.ChainID, }); - let { - bundle: { returnCode }, - } = await controller.deploy(deployTransaction); - assert.isTrue(returnCode.isSuccess()); + let deployTxHash = await provider.sendTransaction(deployTransaction); + let deployResponse = await controller.awaitCompletedDeploy(deployTxHash); + assert.isTrue(deployResponse.returnCode == "ok"); const interaction = ( contract.methods @@ -59,12 +60,20 @@ describe("test smart contract interactor", function () { .withChainID(network.ChainID) .withSender(alice.address) ); + const interactionQuery = interaction.buildQuery(); // 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)); + const queryResponse = await controller.query( + new SmartContractQueryInput({ + contract: interactionQuery.address, + arguments: interactionQuery.getEncodedArguments(), + function: interactionQuery.func.toString(), + caller: interactionQuery.caller, + value: BigInt(interactionQuery.value.toString()), + }), + ); + assert.lengthOf(queryResponse, 1); + assert.deepEqual(queryResponse[0]!.valueOf(), new BigNumber(42)); // Execute, do not wait for execution let transaction = interaction @@ -79,11 +88,11 @@ describe("test smart contract interactor", function () { 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)); + let txHash = await provider.sendTransaction(transaction); + let response = await controller.awaitCompletedExecute(txHash); + assert.isTrue(response.returnCode == "ok"); + assert.lengthOf(response.values, 1); + assert.deepEqual(response.values[0], new BigNumber(42)); }); it("should interact with 'answer' (local testnet) using the SmartContractTransactionsFactory", async function () { @@ -123,16 +132,20 @@ describe("test smart contract interactor", function () { }); 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 queryController = new SmartContractController({ + chainID: "localnet", + networkProvider: provider, + abi: abiRegistry, + }); + + let transactionOnNetwork = await transactionCompletionAwaiter.awaitCompleted(deployTxHash); + let response = queryController.parseExecute(transactionOnNetwork); + assert.isTrue(response.returnCode == "ok"); const query = queryController.createQuery({ - contract: contractAddress.bech32(), - caller: alice.address.bech32(), + contract: contractAddress, + caller: alice.address, function: "getUltimateAnswer", arguments: [], }); @@ -172,14 +185,11 @@ describe("test smart contract interactor", function () { const executeTxHash = await provider.sendTransaction(transaction); transactionOnNetwork = await transactionCompletionAwaiter.awaitCompleted(executeTxHash); - const typedBundle = resultsParser.parseOutcome( - transactionOnNetwork, - abiRegistry.getEndpoint("getUltimateAnswer"), - ); + response = queryController.parseExecute(transactionOnNetwork); - assert.lengthOf(typedBundle.values, 1); - assert.deepEqual(typedBundle.firstValue!.valueOf(), new BigNumber(42)); - assert.isTrue(typedBundle.returnCode.equals(ReturnCode.Ok)); + assert.isTrue(response.values.length == 1); + assert.deepEqual(response.values[0], new BigNumber(42)); + assert.isTrue(response.returnCode == "ok"); }); it("should interact with 'basic-features' (local testnet)", async function () { @@ -187,7 +197,11 @@ describe("test smart contract interactor", function () { let abiRegistry = await loadAbiRegistry("src/testdata/basic-features.abi.json"); let contract = new SmartContract({ abi: abiRegistry }); - let controller = new ContractController(provider); + let controller = new SmartContractController({ + chainID: "localnet", + networkProvider: provider, + abi: abiRegistry, + }); let network = await provider.getNetworkConfig(); await alice.sync(provider); @@ -201,11 +215,9 @@ describe("test smart contract interactor", function () { initArguments: [], chainID: network.ChainID, }); - - let { - bundle: { returnCode }, - } = await controller.deploy(deployTransaction); - assert.isTrue(returnCode.isSuccess()); + let deployTxHash = await provider.sendTransaction(deployTransaction); + let deployResponse = await controller.awaitCompletedDeploy(deployTxHash); + assert.isTrue(deployResponse.returnCode == "ok"); let returnEgldInteraction = ( contract.methods @@ -280,38 +292,44 @@ describe("test smart contract interactor", function () { // 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)); + 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 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)); + 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 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)); + 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 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)); + 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 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)); + 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 ManagedDecimalSignedValue("3.135553845", 9)); }); it("should interact with 'counter' (local testnet)", async function () { @@ -319,7 +337,11 @@ describe("test smart contract interactor", function () { let abiRegistry = await loadAbiRegistry("src/testdata/counter.abi.json"); let contract = new SmartContract({ abi: abiRegistry }); - let controller = new ContractController(provider); + let controller = new SmartContractController({ + chainID: "localnet", + networkProvider: provider, + abi: abiRegistry, + }); let network = await provider.getNetworkConfig(); await alice.sync(provider); @@ -334,12 +356,11 @@ describe("test smart contract interactor", function () { chainID: network.ChainID, }); - let { - bundle: { returnCode }, - } = await controller.deploy(deployTransaction); - assert.isTrue(returnCode.isSuccess()); + await provider.sendTransaction(deployTransaction); + let hash = await provider.sendTransaction(deployTransaction); + let responseExecute = await controller.awaitCompletedExecute(hash); + assert.isTrue(responseExecute.returnCode == "ok"); - let getInteraction = contract.methods.get(); let incrementInteraction = (contract.methods.increment()) .withGasLimit(3000000) .withChainID(network.ChainID) @@ -350,16 +371,27 @@ describe("test smart contract interactor", function () { .withSender(alice.address); // Query "get()" - let { firstValue: counterValue } = await controller.query(getInteraction); - assert.deepEqual(counterValue!.valueOf(), new BigNumber(1)); + + let interactionQuery = incrementInteraction.buildQuery(); + let response = await controller.query( + new SmartContractQueryInput({ + contract: interactionQuery.address, + arguments: interactionQuery.getEncodedArguments(), + function: interactionQuery.func.toString(), + caller: interactionQuery.caller, + value: BigInt(interactionQuery.value.toString()), + }), + ); + assert.deepEqual(response[0]!.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)); + + hash = await provider.sendTransaction(incrementTransaction); + responseExecute = await controller.awaitCompletedExecute(hash); + assert.isTrue(responseExecute.returnCode == "ok"); + assert.deepEqual(responseExecute.values[0], new BigNumber(2)); // Decrement twice. Wait for execution of the second transaction. let decrementTransaction = decrementInteraction.useThenIncrementNonceOf(alice.account).buildTransaction(); @@ -367,11 +399,12 @@ describe("test smart contract interactor", function () { 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)); + hash = await provider.sendTransaction(decrementTransaction); + responseExecute = await controller.awaitCompletedExecute(hash); + assert.isTrue(responseExecute.returnCode == "ok"); + assert.deepEqual(responseExecute.values[0], new BigNumber(0)); }); it("should interact with 'counter' (local testnet) using the SmartContractTransactionsFactory", async function () { @@ -387,6 +420,7 @@ describe("test smart contract interactor", function () { config: config, abi: abiRegistry, }); + const parser = new SmartContractTransactionsOutcomeParser(); const bytecode = await promises.readFile("src/testdata/counter.wasm"); @@ -412,11 +446,14 @@ describe("test smart contract interactor", function () { const deployTxHash = await provider.sendTransaction(deployTransaction); let transactionOnNetwork = await transactionCompletionAwaiter.awaitCompleted(deployTxHash); - const untypedBundle = resultsParser.parseUntypedOutcome(transactionOnNetwork); - assert.isTrue(untypedBundle.returnCode.isSuccess()); + let response = parser.parseExecute({ transactionOnNetwork }); + assert.isTrue(response.returnCode == "ok"); - const queryRunner = new QueryRunnerAdapter({ networkProvider: provider }); - const queryController = new SmartContractQueriesController({ abi: abiRegistry, queryRunner: queryRunner }); + const queryController = new SmartContractController({ + chainID: "localnet", + networkProvider: provider, + abi: abiRegistry, + }); let incrementTransaction = factory.createTransactionForExecute(alice.address, { contract: contractAddress, @@ -433,7 +470,7 @@ describe("test smart contract interactor", function () { // Query "get()" const query = queryController.createQuery({ - contract: contractAddress.bech32(), + contract: contractAddress, function: "get", arguments: [], }); @@ -443,8 +480,9 @@ describe("test smart contract interactor", function () { 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)); + + response = parser.parseExecute({ transactionOnNetwork }); + assert.deepEqual(response.values[0], new BigNumber(2)); let decrementTransaction = factory.createTransactionForExecute(alice.address, { contract: contractAddress, @@ -467,7 +505,7 @@ describe("test smart contract interactor", function () { const decrementTxHash = await provider.sendTransaction(decrementTransaction); transactionOnNetwork = await transactionCompletionAwaiter.awaitCompleted(decrementTxHash); - typedBundle = resultsParser.parseOutcome(transactionOnNetwork, abiRegistry.getEndpoint("decrement")); + response = parser.parseExecute({ transactionOnNetwork }); }); it("should interact with 'lottery-esdt' (local testnet)", async function () { @@ -475,7 +513,11 @@ describe("test smart contract interactor", function () { let abiRegistry = await loadAbiRegistry("src/testdata/lottery-esdt.abi.json"); let contract = new SmartContract({ abi: abiRegistry }); - let controller = new ContractController(provider); + let controller = new SmartContractController({ + chainID: "localnet", + networkProvider: provider, + abi: abiRegistry, + }); let network = await provider.getNetworkConfig(); await alice.sync(provider); @@ -490,10 +532,10 @@ describe("test smart contract interactor", function () { chainID: network.ChainID, }); - let { - bundle: { returnCode }, - } = await controller.deploy(deployTransaction); - assert.isTrue(returnCode.isSuccess()); + const deployTxHash = await provider.sendTransaction(deployTransaction); + let parsedResponse = await controller.awaitCompletedDeploy(deployTxHash); + + assert.isTrue(parsedResponse.returnCode == "ok"); let startInteraction = ( contract.methods @@ -526,9 +568,10 @@ describe("test smart contract interactor", function () { .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); + const startTxHash = await provider.sendTransaction(startTransaction); + let response = await controller.awaitCompletedExecute(startTxHash); + assert.isTrue(response.returnCode == "ok"); + assert.lengthOf(response.values, 0); // status() let lotteryStatusTransaction = lotteryStatusInteraction @@ -537,10 +580,11 @@ describe("test smart contract interactor", function () { .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"); + const lotteryStatusTxHash = await provider.sendTransaction(lotteryStatusTransaction); + response = await controller.awaitCompletedExecute(lotteryStatusTxHash); + assert.isTrue(response.returnCode == "ok"); + assert.lengthOf(response.values, 1); + assert.equal(response.values[0].name, "Running"); // lotteryInfo() (this is a view function, but for the sake of the test, we'll execute it) let lotteryInfoTransaction = getLotteryInfoInteraction @@ -549,12 +593,13 @@ describe("test smart contract interactor", function () { .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); + const lotteryInfoTxHash = await provider.sendTransaction(lotteryInfoTransaction); + response = await controller.awaitCompletedExecute(lotteryInfoTxHash); + assert.isTrue(response.returnCode == "ok"); + assert.lengthOf(response.values, 1); // Ignore "deadline" field in our test - let info = bundleLotteryInfo.firstValue!.valueOf(); + let info = response.values[0].valueOf(); delete info.deadline; assert.deepEqual(info, { @@ -571,6 +616,7 @@ describe("test smart contract interactor", function () { this.timeout(140000); let abiRegistry = await loadAbiRegistry("src/testdata/lottery-esdt.abi.json"); + let parser = new SmartContractTransactionsOutcomeParser(); let network = await provider.getNetworkConfig(); await alice.sync(provider); @@ -606,8 +652,8 @@ describe("test smart contract interactor", function () { const deployTxHash = await provider.sendTransaction(deployTransaction); let transactionOnNetwork = await transactionCompletionAwaiter.awaitCompleted(deployTxHash); - const untypedBundle = resultsParser.parseUntypedOutcome(transactionOnNetwork); - assert.isTrue(untypedBundle.returnCode.isSuccess()); + const deployResponse = parser.parseDeploy({ transactionOnNetwork }); + assert.isTrue(deployResponse.returnCode == "ok"); // start() let startTransaction = factory.createTransactionForExecute(alice.address, { @@ -625,9 +671,9 @@ describe("test smart contract interactor", function () { 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); + let response = parser.parseExecute({ transactionOnNetwork }); + assert.isTrue(response.returnCode == "ok"); + assert.lengthOf(response.values, 0); // status() let lotteryStatusTransaction = factory.createTransactionForExecute(alice.address, { @@ -645,10 +691,10 @@ describe("test smart contract interactor", function () { 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"); + response = parser.parseExecute({ transactionOnNetwork }); + assert.isTrue(response.returnCode == "ok"); + assert.lengthOf(response.values, 0); + 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, { @@ -666,12 +712,12 @@ describe("test smart contract interactor", function () { 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); + response = parser.parseExecute({ transactionOnNetwork }); + assert.isTrue(response.returnCode == "ok"); + assert.lengthOf(response.values, 0); // Ignore "deadline" field in our test - let info = typedBundle.firstValue!.valueOf(); + let info = response.values[0]!.valueOf(); delete info.deadline; assert.deepEqual(info, { diff --git a/src/abi/interaction.spec.ts b/src/abi/interaction.spec.ts index 3e1f5b808..468d078a9 100644 --- a/src/abi/interaction.spec.ts +++ b/src/abi/interaction.spec.ts @@ -1,7 +1,8 @@ import BigNumber from "bignumber.js"; import { assert } from "chai"; import { Address } from "../address"; -import { ContractQueryResponse } from "../networkProviders"; +import { SmartContractQueryInput, SmartContractQueryResponse } from "../smartContractQuery"; +import { SmartContractController } from "../smartContracts"; import { loadAbiRegistry, loadTestWallets, @@ -9,12 +10,10 @@ import { 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, BytesValue, OptionalValue, OptionValue, TokenIdentifierValue, U32Value } from "./typesystem"; @@ -173,8 +172,8 @@ describe("test smart contract interactor", function () { }); 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 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 }); @@ -207,7 +206,7 @@ describe("test smart contract interactor", function () { let abiRegistry = await loadAbiRegistry("src/testdata/answer.abi.json"); let contract = new SmartContract({ address: dummyAddress, abi: abiRegistry }); - let controller = new ContractController(provider); + let controller = new SmartContractController({ chainID: "D", networkProvider: provider, abi: abiRegistry }); let interaction = contract.methods.getUltimateAnswer().withGasLimit(543210).withChainID("T"); @@ -218,18 +217,28 @@ describe("test smart contract interactor", function () { provider.mockQueryContractOnFunction( "getUltimateAnswer", - new ContractQueryResponse({ returnData: [Buffer.from([42]).toString("base64")], returnCode: "ok" }), + new SmartContractQueryResponse({ + returnDataParts: [Buffer.from([42])], + returnCode: "ok", + returnMessage: "msg", + function: "getUltimateAnswer", + }), ); - // 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)); + // Query; + + const interactionQuery = interaction.buildQuery(); + let response = await controller.query( + new SmartContractQueryInput({ + 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(0).buildTransaction(); @@ -257,20 +266,21 @@ describe("test smart contract interactor", function () { 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); + provider.mockGetTransactionWithAnyHashAsNotarizedWithOneResult("@6f6b@2bs", "getUltimateAnswer"); + let hash = await provider.sendTransaction(transaction); + let responseExecute = await controller.awaitCompletedExecute(hash); - assert.lengthOf(bundle.values, 1); - assert.deepEqual(bundle.firstValue!.valueOf(), new BigNumber(43)); - assert.isTrue(bundle.returnCode.equals(ReturnCode.Ok)); + 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 () { setupUnitTestWatcherTimeouts(); - let abiRegistry = await loadAbiRegistry("src/testdata/counter.abi.json"); - let contract = new SmartContract({ address: dummyAddress, abi: abiRegistry }); - let controller = new ContractController(provider); + 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().check(); let incrementInteraction = (contract.methods.increment()).withGasLimit(543210); @@ -279,13 +289,26 @@ describe("test smart contract interactor", function () { // For "get()", return fake 7 provider.mockQueryContractOnFunction( "get", - new ContractQueryResponse({ returnData: [Buffer.from([7]).toString("base64")], returnCode: "ok" }), + new SmartContractQueryResponse({ + returnDataParts: [Buffer.from([7])], + returnCode: "ok", + function: "get", + returnMessage: "", + }), ); // Query "get()" - let { firstValue: counterValue } = await controller.query(getInteraction); - - assert.deepEqual(counterValue!.valueOf(), new BigNumber(7)); + const interactionQuery = getInteraction.buildQuery(); + let response = await controller.query( + new SmartContractQueryInput({ + 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) @@ -294,11 +317,10 @@ describe("test smart contract interactor", function () { .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)); + 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 @@ -318,11 +340,10 @@ describe("test smart contract interactor", function () { 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)); + 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 () { @@ -330,7 +351,7 @@ describe("test smart contract interactor", function () { let abiRegistry = await loadAbiRegistry("src/testdata/lottery-esdt.abi.json"); let contract = new SmartContract({ address: dummyAddress, abi: abiRegistry }); - let controller = new ContractController(provider); + let controller = new SmartContractController({ chainID: "D", networkProvider: provider, abi: abiRegistry }); let startInteraction = contract.methodsExplicit .start([ @@ -359,17 +380,17 @@ describe("test smart contract interactor", function () { .buildTransaction(); startTransaction.applySignature(await alice.signer.sign(startTransaction.serializeForSigning())); - provider.mockGetTransactionWithAnyHashAsNotarizedWithOneResult("@6f6b"); - let { - bundle: { returnCode: startReturnCode, values: startReturnValues }, - } = await controller.execute(startInteraction, startTransaction); + + provider.mockGetTransactionWithAnyHashAsNotarizedWithOneResult("@6f6b", "start"); + let hash = await provider.sendTransaction(startTransaction); + let response = await controller.awaitCompletedExecute(hash); assert.equal( startTransaction.getData().toString(), "start@6c75636b79@6c75636b792d746f6b656e@01@@@0100000001@@", ); - assert.isTrue(startReturnCode.equals(ReturnCode.Ok)); - assert.lengthOf(startReturnValues, 0); + 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 @@ -379,15 +400,15 @@ describe("test smart contract interactor", function () { .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); + provider.mockGetTransactionWithAnyHashAsNotarizedWithOneResult("@6f6b@01", "status"); + + hash = await provider.sendTransaction(startTransaction); + response = await controller.awaitCompletedExecute(hash); assert.equal(statusTransaction.getData().toString(), "status@6c75636b79"); - assert.isTrue(statusReturnCode.equals(ReturnCode.Ok)); - assert.lengthOf(statusReturnValues, 1); - assert.deepEqual(statusFirstValue!.valueOf(), { name: "Running", fields: [] }); + 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 @@ -401,16 +422,15 @@ describe("test smart contract interactor", function () { ); provider.mockGetTransactionWithAnyHashAsNotarizedWithOneResult( "@6f6b@0000000b6c75636b792d746f6b656e000000010100000000000000005fc2b9dbffffffff00000001640000000a140ec80fa7ee88000000", + "getLotteryInfo", ); - let { - bundle: { returnCode: infoReturnCode, values: infoReturnValues, firstValue: infoFirstValue }, - } = await controller.execute(getLotteryInfoInteraction, getLotteryInfoTransaction); - + hash = await provider.sendTransaction(startTransaction); + response = await controller.awaitCompletedExecute(hash); assert.equal(getLotteryInfoTransaction.getData().toString(), "getLotteryInfo@6c75636b79"); - assert.isTrue(infoReturnCode.equals(ReturnCode.Ok)); - assert.lengthOf(infoReturnValues, 1); + assert.isTrue(response.returnCode == "ok"); + assert.isTrue(response.values.length == 1); - assert.deepEqual(infoFirstValue!.valueOf(), { + assert.deepEqual(response.values[0]!.valueOf(), { token_identifier: "lucky-token", ticket_price: new BigNumber("1"), tickets_left: new BigNumber(0), diff --git a/src/abi/interaction.ts b/src/abi/interaction.ts index 0cca6ae47..d11edf864 100644 --- a/src/abi/interaction.ts +++ b/src/abi/interaction.ts @@ -40,7 +40,7 @@ export class Interaction { private gasLimit: IGasLimit = 0; private gasPrice: IGasPrice | undefined = undefined; private chainID: IChainID = ""; - private querent: IAddress = Address.empty(); + private querent: Address = Address.empty(); private explicitReceiver?: IAddress; private sender: Address = Address.empty(); private version: number = TRANSACTION_VERSION_DEFAULT; @@ -186,7 +186,7 @@ export class Interaction { /** * Sets the "caller" field on contract queries. */ - withQuerent(querent: IAddress): Interaction { + withQuerent(querent: Address): Interaction { this.querent = querent; return this; } diff --git a/src/abi/query.ts b/src/abi/query.ts index 7249ebd6a..f773f9f21 100644 --- a/src/abi/query.ts +++ b/src/abi/query.ts @@ -1,19 +1,19 @@ import { Address } from "../address"; -import { TypedValue } from "./typesystem"; +import { ITransactionValue } from "../interface"; 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; constructor(obj: { - caller?: IAddress; - address: IAddress; + caller?: Address; + address: Address; func: IContractFunction; args?: TypedValue[]; value?: ITransactionValue; diff --git a/src/abi/resultsParser.spec.ts b/src/abi/resultsParser.spec.ts deleted file mode 100644 index 7ce441301..000000000 --- a/src/abi/resultsParser.spec.ts +++ /dev/null @@ -1,371 +0,0 @@ -import BigNumber from "bignumber.js"; -import { assert } from "chai"; -import { Address } from "../address"; -import { ContractQueryResponse } from "../networkProviders"; -import { b64TopicsToBytes, loadAbiRegistry } from "../testutils"; -import { TransactionEvent } from "../transactionEvents"; -import { TransactionLogs } from "../transactionLogs"; -import { TransactionOnNetwork } from "../transactionOnNetwork"; -import { SmartContractResult } from "../transactionsOutcomeParsers"; -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({ - smartContractResults: [new SmartContractResult({ data: Buffer.from("@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({ - smartContractResults: [ - new SmartContractResult({ - data: Buffer.from("@6f6b@03"), - }), - ], - }); - - let bundle = parser.parseUntypedOutcome(transaction); - assert.deepEqual(bundle.returnCode, ReturnCode.Ok); - assert.deepEqual(bundle.values, [Buffer.from("03", "hex")]); - }); - - it("should parse contract outcome, on signal error", async () => { - let transaction = new TransactionOnNetwork({ - logs: new TransactionLogs({ - address: Address.empty(), - events: [ - new TransactionEvent({ - identifier: "signalError", - topics: [Buffer.from("something happened")], - data: Buffer.from("QDc1NzM2NTcyMjA2NTcyNzI2ZjcyQDA3", "base64"), - }), - ], - }), - }); - - 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 TransactionLogs({ - address: Address.empty(), - events: [ - new TransactionEvent({ - identifier: "writeLog", - topics: [ - Buffer.from( - "QHRvbyBtdWNoIGdhcyBwcm92aWRlZCBmb3IgcHJvY2Vzc2luZzogZ2FzIHByb3ZpZGVkID0gNTk2Mzg0NTAwLCBnYXMgdXNlZCA9IDczMzAxMA==", - "base64", - ), - ], - data: Buffer.from("QDZmNmI=", "base64"), - }), - ], - }), - }); - - 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 TransactionEvent({ - topics: [ - Buffer.from("ZGVwb3NpdA==", "base64"), - Buffer.from("cmzC1LRt1r10pMhNAnFb+FyudjGMq4G8CefCYdQUmmc=", "base64"), - Buffer.from("AAAADFdFR0xELTAxZTQ5ZAAAAAAAAAAAAAAAAWQ=", "base64"), - ], - data: 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 = new TransactionEvent({ - topics: b64TopicsToBytes([ - Buffer.from("not used").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"), - ]), - data: 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 = new TransactionEvent({ - topics: b64TopicsToBytes([ - Buffer.from("not used").toString("base64"), - Buffer.from([42]).toString("base64"), - ]), - additionalData: [Buffer.from([43]), Buffer.from([44])], - // Will be ignored. - data: new Uint8Array(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/abi/resultsParser.ts b/src/abi/resultsParser.ts deleted file mode 100644 index fda7147ac..000000000 --- a/src/abi/resultsParser.ts +++ /dev/null @@ -1,435 +0,0 @@ -import { - TransactionDecoder, - TransactionMetadata, -} from "@multiversx/sdk-transaction-decoder/lib/src/transaction.decoder"; -import { Address } from "../address"; -import { ErrCannotParseContractResults } from "../errors"; -import { IContractQueryResponse } from "../interfaceOfNetwork"; -import { Logger } from "../logger"; -import { TransactionEvent } from "../transactionEvents"; -import { TransactionLogs } from "../transactionLogs"; -import { TransactionOnNetwork } from "../transactionOnNetwork"; -import { SmartContractResult } from "../transactionsOutcomeParsers"; -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 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: TransactionOnNetwork, 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: TransactionOnNetwork): 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; - } - - if (bundle) { - Logger.trace("parseUntypedOutcome(): on invalid transaction"); - return bundle; - } - - bundle = this.createBundleOnEasilyFoundResultWithReturnData(transaction.smartContractResults); - 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: TransactionOnNetwork): 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: TransactionOnNetwork): UntypedOutcomeBundle | null { - let noResults = transaction.smartContractResults.length == 0; - let noLogs = transaction.logs.events.length == 0; - - if (noResults && noLogs) { - return { - returnCode: ReturnCode.None, - returnMessage: ReturnCode.None.toString(), - values: [], - }; - } - - return null; - } - - protected createBundleOnEasilyFoundResultWithReturnData( - results: SmartContractResult[], - ): UntypedOutcomeBundle | null { - let resultItemWithReturnData = results.find((item) => item.data.toString().startsWith("@")); - if (!resultItemWithReturnData) { - return null; - } - - let { returnCode, returnDataParts } = this.sliceDataFieldInParts(resultItemWithReturnData.data.toString()); - let returnMessage = resultItemWithReturnData.raw["prevTxHash"] || returnCode.toString(); - - return { - returnCode: returnCode, - returnMessage: returnMessage, - values: returnDataParts, - }; - } - - protected createBundleOnSignalError(logs: TransactionLogs): UntypedOutcomeBundle | null { - let eventSignalError = logs.findSingleOrNoneEvent(WellKnownEvents.OnSignalError); - if (!eventSignalError) { - return null; - } - - let { returnCode, returnDataParts } = this.sliceDataFieldInParts(Buffer.from(eventSignalError.data).toString()); - let lastTopic = eventSignalError.getLastTopic(); - let returnMessage = lastTopic?.toString() || returnCode.toString(); - - return { - returnCode: returnCode, - returnMessage: returnMessage, - values: returnDataParts, - }; - } - - protected createBundleOnTooMuchGasWarning(logs: TransactionLogs): 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.toString()); - - return { - returnCode: returnCode, - returnMessage: returnCode.toString(), - values: returnDataParts, - }; - } - - protected createBundleOnWriteLogWhereFirstTopicEqualsAddress( - logs: TransactionLogs, - address: Address, - ): UntypedOutcomeBundle | null { - let hexAddress = new Address(address.bech32()).hex(); - - let eventWriteLogWhereTopicIsSender = logs.findSingleOrNoneEvent( - WellKnownEvents.OnWriteLog, - (event) => event.findFirstOrNoneTopic((topic) => topic.toString() == hexAddress) != undefined, - ); - - if (!eventWriteLogWhereTopicIsSender) { - return null; - } - - let { returnCode, returnDataParts } = this.sliceDataFieldInParts( - eventWriteLogWhereTopicIsSender.data.toString(), - ); - 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: TransactionOnNetwork, - _transactionMetadata: TransactionMetadata, - ): UntypedOutcomeBundle | null { - return null; - } - - protected createBundleWithFallbackHeuristics( - transaction: TransactionOnNetwork, - transactionMetadata: TransactionMetadata, - ): UntypedOutcomeBundle | null { - let contractAddress = new Address(transactionMetadata.receiver); - - // Search the nested logs for matching events (writeLog): - for (const resultItem of transaction.smartContractResults) { - let writeLogWithReturnData = resultItem.logs.findSingleOrNoneEvent(WellKnownEvents.OnWriteLog, (event) => { - let addressIsSender = event.address.bech32() == transaction.sender.bech32(); - let firstTopicIsContract = event.topics[0].toString() == contractAddress.hex(); - return addressIsSender && firstTopicIsContract; - }); - - if (writeLogWithReturnData) { - let { returnCode, returnDataParts } = this.sliceDataFieldInParts( - writeLogWithReturnData.data.toString(), - ); - let returnMessage = returnCode.toString(); - - return { - returnCode: returnCode, - returnMessage: returnMessage, - values: returnDataParts, - }; - } - } - - // Additional fallback heuristics (alter search constraints): - for (const resultItem of transaction.smartContractResults) { - 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.toString(), - ); - 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: TransactionEvent, 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.data?.valueOf() || Buffer.from([]); - const dataItems = transactionEvent.additionalData?.map((data) => Buffer.from(data)) || []; - 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/abi/smartContract.local.net.spec.ts b/src/abi/smartContract.local.net.spec.ts index 9266c0eae..c06c15c9d 100644 --- a/src/abi/smartContract.local.net.spec.ts +++ b/src/abi/smartContract.local.net.spec.ts @@ -1,9 +1,12 @@ import { assert } from "chai"; import { promises } from "fs"; -import { QueryRunnerAdapter } from "../adapters/queryRunnerAdapter"; import { Logger } from "../logger"; -import { SmartContractQueriesController } from "../smartContractQueriesController"; -import { SmartContractTransactionsFactory } from "../smartContracts"; +import { SmartContractQueryInput } from "../smartContractQuery"; +import { + SmartContractController, + SmartContractTransactionsFactory, + SmartContractTransactionsOutcomeParser, +} from "../smartContracts"; import { prepareDeployment } from "../testutils"; import { createLocalnetProvider } from "../testutils/networkProviders"; import { loadTestWallets, TestWallet } from "../testutils/wallets"; @@ -12,7 +15,6 @@ import { TransactionsFactoryConfig } from "../transactionsFactoryConfig"; import { TransactionWatcher } from "../transactionWatcher"; import { decodeUnsignedNumber } from "./codec"; import { ContractFunction } from "./function"; -import { ResultsParser } from "./resultsParser"; import { SmartContract } from "./smartContract"; import { AddressValue, @@ -28,7 +30,7 @@ describe("test on local testnet", function () { let alice: TestWallet, bob: TestWallet, carol: TestWallet; let provider = createLocalnetProvider(); let watcher: TransactionWatcher; - let resultsParser = new ResultsParser(); + let parser: SmartContractTransactionsOutcomeParser; before(async function () { ({ alice, bob, carol } = await loadTestWallets()); @@ -38,6 +40,7 @@ describe("test on local testnet", function () { return await provider.getTransaction(hash); }, }); + parser = new SmartContractTransactionsOutcomeParser(); }); it("counter: should deploy, then simulate transactions", async function () { @@ -102,13 +105,14 @@ describe("test on local testnet", function () { await watcher.awaitCompleted(txHashDeploy); let transactionOnNetwork = await provider.getTransaction(txHashDeploy); - let bundle = resultsParser.parseUntypedOutcome(transactionOnNetwork); - assert.isTrue(bundle.returnCode.isSuccess()); + let response = parser.parseExecute({ transactionOnNetwork }); + + assert.isTrue(response.returnCode == "ok"); await watcher.awaitCompleted(txHashIncrement); transactionOnNetwork = await provider.getTransaction(txHashIncrement); - bundle = resultsParser.parseUntypedOutcome(transactionOnNetwork); - assert.isTrue(bundle.returnCode.isSuccess()); + response = parser.parseExecute({ transactionOnNetwork }); + assert.isTrue(response.returnCode == "ok"); // Simulate Logger.trace(JSON.stringify(await provider.simulateTransaction(simulateOne), null, 4)); @@ -187,13 +191,14 @@ describe("test on local testnet", function () { await watcher.awaitCompleted(deployTxHash); let transactionOnNetwork = await provider.getTransaction(deployTxHash); - let bundle = resultsParser.parseUntypedOutcome(transactionOnNetwork); - assert.isTrue(bundle.returnCode.isSuccess()); + let response = parser.parseExecute({ transactionOnNetwork }); + + assert.isTrue(response.returnCode == "ok"); await watcher.awaitCompleted(callTxHash); transactionOnNetwork = await provider.getTransaction(callTxHash); - bundle = resultsParser.parseUntypedOutcome(transactionOnNetwork); - assert.isTrue(bundle.returnCode.isSuccess()); + response = parser.parseExecute({ transactionOnNetwork }); + assert.isTrue(response.returnCode == "ok"); // Simulate Logger.trace(JSON.stringify(await provider.simulateTransaction(simulateOne), null, 4)); @@ -260,9 +265,15 @@ describe("test on local testnet", function () { // 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])); + let queryResponse = await provider.queryContract( + new SmartContractQueryInput({ + contract: query.address, + arguments: query.args, + function: query.func.toString(), + }), + ); + assert.isTrue(queryResponse.returnDataParts.length == 1); + assert.equal(3, decodeUnsignedNumber(Buffer.from(queryResponse.returnDataParts[0]))); }); it("counter: should deploy, call and query contract using SmartContractTransactionsFactory", async function () { @@ -327,11 +338,13 @@ describe("test on local testnet", function () { await watcher.awaitCompleted(secondScCallHash); // Check counter - const queryRunner = new QueryRunnerAdapter({ networkProvider: provider }); - const smartContractQueriesController = new SmartContractQueriesController({ queryRunner: queryRunner }); + const smartContractQueriesController = new SmartContractController({ + chainID: "localnet", + networkProvider: provider, + }); const query = smartContractQueriesController.createQuery({ - contract: contractAddress.bech32(), + contract: contractAddress, function: "get", arguments: [], }); @@ -399,33 +412,56 @@ describe("test on local testnet", function () { // 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])); + let queryResponse = await provider.queryContract( + new SmartContractQueryInput({ + contract: query.address, + arguments: query.args, + function: query.func.toString(), + }), + ); + assert.isTrue(queryResponse.returnDataParts.length == 1); + assert.equal(10000, decodeUnsignedNumber(Buffer.from(queryResponse.returnDataParts[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])); + queryResponse = await provider.queryContract( + new SmartContractQueryInput({ + contract: query.address, + arguments: query.args, + function: query.func.toString(), + }), + ); + assert.equal(7500, decodeUnsignedNumber(Buffer.from(queryResponse.returnDataParts[0]))); query = contract.createQuery({ func: new ContractFunction("balanceOf"), args: [new AddressValue(bob.address)], }); - queryResponse = await provider.queryContract(query); + queryResponse = await provider.queryContract( + new SmartContractQueryInput({ + contract: query.address, + arguments: query.args, + function: query.func.toString(), + }), + ); - assert.equal(1000, decodeUnsignedNumber(queryResponse.getReturnDataParts()[0])); + assert.equal(1000, decodeUnsignedNumber(Buffer.from(queryResponse.returnDataParts[0]))); query = contract.createQuery({ func: new ContractFunction("balanceOf"), args: [new AddressValue(carol.address)], }); - queryResponse = await provider.queryContract(query); + queryResponse = await provider.queryContract( + new SmartContractQueryInput({ + contract: query.address, + arguments: query.args, + function: query.func.toString(), + }), + ); - assert.equal(1500, decodeUnsignedNumber(queryResponse.getReturnDataParts()[0])); + assert.equal(1500, decodeUnsignedNumber(Buffer.from(queryResponse.returnDataParts[0]))); }); it("erc20: should deploy, call and query contract using SmartContractTransactionsFactory", async function () { @@ -492,11 +528,13 @@ describe("test on local testnet", function () { await watcher.awaitCompleted(mintCarolTxHash); // Query state, do some assertions - const queryRunner = new QueryRunnerAdapter({ networkProvider: provider }); - const smartContractQueriesController = new SmartContractQueriesController({ queryRunner: queryRunner }); + const smartContractQueriesController = new SmartContractController({ + chainID: "localnet", + networkProvider: provider, + }); let query = smartContractQueriesController.createQuery({ - contract: contractAddress.bech32(), + contract: contractAddress, function: "totalSupply", arguments: [], }); @@ -505,7 +543,7 @@ describe("test on local testnet", function () { assert.equal(10000, decodeUnsignedNumber(Buffer.from(queryResponse.returnDataParts[0]))); query = smartContractQueriesController.createQuery({ - contract: contractAddress.bech32(), + contract: contractAddress, function: "balanceOf", arguments: [new AddressValue(alice.address)], }); @@ -513,7 +551,7 @@ describe("test on local testnet", function () { assert.equal(7500, decodeUnsignedNumber(Buffer.from(queryResponse.returnDataParts[0]))); query = smartContractQueriesController.createQuery({ - contract: contractAddress.bech32(), + contract: contractAddress, function: "balanceOf", arguments: [new AddressValue(bob.address)], }); @@ -521,7 +559,7 @@ describe("test on local testnet", function () { assert.equal(1000, decodeUnsignedNumber(Buffer.from(queryResponse.returnDataParts[0]))); query = smartContractQueriesController.createQuery({ - contract: contractAddress.bech32(), + contract: contractAddress, function: "balanceOf", arguments: [new AddressValue(carol.address)], }); @@ -582,27 +620,39 @@ describe("test on local testnet", function () { // Let's check the SCRs let transactionOnNetwork = await provider.getTransaction(transactionDeploy.getHash().hex()); - let bundle = resultsParser.parseUntypedOutcome(transactionOnNetwork); - assert.isTrue(bundle.returnCode.isSuccess()); + let response = parser.parseExecute({ transactionOnNetwork }); + assert.isTrue(response.returnCode == "ok"); transactionOnNetwork = await provider.getTransaction(transactionStart.getHash().hex()); - bundle = resultsParser.parseUntypedOutcome(transactionOnNetwork); - assert.isTrue(bundle.returnCode.isSuccess()); + response = parser.parseExecute({ transactionOnNetwork }); + assert.isTrue(response.returnCode == "ok"); // 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); + let queryResponse = await provider.queryContract( + new SmartContractQueryInput({ + contract: query.address, + arguments: query.args, + function: query.func.toString(), + }), + ); + assert.equal(decodeUnsignedNumber(Buffer.from(queryResponse.returnDataParts[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); + queryResponse = await provider.queryContract( + new SmartContractQueryInput({ + contract: query.address, + arguments: query.args, + function: query.func.toString(), + }), + ); + assert.equal(decodeUnsignedNumber(Buffer.from(queryResponse.returnDataParts[0])), 0); }); it("lottery: should deploy, call and query contract using SmartContractTransactionsFactory", async function () { @@ -665,19 +715,21 @@ describe("test on local testnet", function () { // Let's check the SCRs let transactionOnNetwork = await provider.getTransaction(deployTx); - let bundle = resultsParser.parseUntypedOutcome(transactionOnNetwork); - assert.isTrue(bundle.returnCode.isSuccess()); + let response = parser.parseExecute({ transactionOnNetwork }); + assert.isTrue(response.returnCode == "ok"); transactionOnNetwork = await provider.getTransaction(startTx); - bundle = resultsParser.parseUntypedOutcome(transactionOnNetwork); - assert.isTrue(bundle.returnCode.isSuccess()); + response = parser.parseExecute({ transactionOnNetwork }); + assert.isTrue(response.returnCode == "ok"); // Query state, do some assertions - const queryRunner = new QueryRunnerAdapter({ networkProvider: provider }); - const smartContractQueriesController = new SmartContractQueriesController({ queryRunner: queryRunner }); + const smartContractQueriesController = new SmartContractController({ + chainID: "localnet", + networkProvider: provider, + }); let query = smartContractQueriesController.createQuery({ - contract: contractAddress.bech32(), + contract: contractAddress, function: "status", arguments: [BytesValue.fromUTF8("lucky")], }); @@ -685,7 +737,7 @@ describe("test on local testnet", function () { assert.equal(decodeUnsignedNumber(Buffer.from(queryResponse.returnDataParts[0])), 1); query = smartContractQueriesController.createQuery({ - contract: contractAddress.bech32(), + contract: contractAddress, function: "status", arguments: [BytesValue.fromUTF8("missingLottery")], }); diff --git a/src/abi/smartContractResults.local.net.spec.ts b/src/abi/smartContractResults.local.net.spec.ts index f9e5c1005..87201fa5e 100644 --- a/src/abi/smartContractResults.local.net.spec.ts +++ b/src/abi/smartContractResults.local.net.spec.ts @@ -1,21 +1,19 @@ import { assert } from "chai"; import { promises } from "fs"; -import { SmartContractTransactionsFactory } from "../smartContracts"; +import { SmartContractTransactionsFactory, SmartContractTransactionsOutcomeParser } from "../smartContracts"; import { loadTestWallets, prepareDeployment, TestWallet } from "../testutils"; import { createLocalnetProvider } from "../testutils/networkProviders"; import { TransactionComputer } from "../transactionComputer"; import { TransactionsFactoryConfig } from "../transactionsFactoryConfig"; import { TransactionWatcher } from "../transactionWatcher"; import { ContractFunction } from "./function"; -import { ResultsParser } from "./resultsParser"; import { SmartContract } from "./smartContract"; describe("fetch transactions from local testnet", function () { let alice: TestWallet; let provider = createLocalnetProvider(); let watcher: TransactionWatcher; - - let resultsParser = new ResultsParser(); + let parser: SmartContractTransactionsOutcomeParser; before(async function () { ({ alice } = await loadTestWallets()); @@ -24,6 +22,8 @@ describe("fetch transactions from local testnet", function () { return await provider.getTransaction(hash); }, }); + + parser = new SmartContractTransactionsOutcomeParser(); }); it("counter smart contract", async function () { @@ -70,11 +70,11 @@ 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 () { @@ -127,10 +127,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/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/entrypoints/entrypoints.spec.ts b/src/entrypoints/entrypoints.spec.ts index 42b2241e6..f0a850398 100644 --- a/src/entrypoints/entrypoints.spec.ts +++ b/src/entrypoints/entrypoints.spec.ts @@ -79,7 +79,7 @@ describe("TestEntrypoint", () => { const txHashExecute = await entrypoint.sendTransaction(executeTransaction); await entrypoint.awaitCompletedTransaction(txHashExecute); - const queryResult = await controller.queryContract(contractAddress, "getSum", []); + const queryResult = await controller.query({ contract: contractAddress, function: "getSum", arguments: [] }); assert.equal(queryResult.length, 1); assert.equal(queryResult[0], 7); }); diff --git a/src/index.ts b/src/index.ts index 88e182683..1816c81e1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,7 +9,6 @@ require("./globals"); export * from "./abi"; export * from "./accountManagement"; export * from "./accounts"; -export * from "./adapters"; export * from "./address"; export * from "./asyncTimer"; export * from "./config"; @@ -28,7 +27,6 @@ export * from "./relayed"; export * from "./relayedTransactionV1Builder"; export * from "./relayedTransactionV2Builder"; export * from "./signableMessage"; -export * from "./smartContractQueriesController"; export * from "./tokenManagement"; export * from "./tokens"; export * from "./transaction"; diff --git a/src/networkProviders/apiNetworkProvider.ts b/src/networkProviders/apiNetworkProvider.ts index 807f1640e..3891ca47a 100644 --- a/src/networkProviders/apiNetworkProvider.ts +++ b/src/networkProviders/apiNetworkProvider.ts @@ -1,4 +1,5 @@ import { ErrContractQuery, ErrNetworkProvider } from "../errors"; +import { SmartContractQuery, SmartContractQueryResponse } from "../smartContractQuery"; import { TransactionOnNetwork, prepareTransactionForBroadcasting } from "../transactionOnNetwork"; import { TransactionStatus } from "../transactionStatus"; import { getAxios } from "../utils"; @@ -7,8 +8,7 @@ import { AccountOnNetwork, GuardianData } from "./accounts"; 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 { IAddress, INetworkProvider, IPagination, ITransaction, ITransactionNext } from "./interface"; import { NetworkConfig } from "./networkConfig"; import { NetworkGeneralStatistics } from "./networkGeneralStatistics"; import { NetworkProviderConfig } from "./networkProviderConfig"; @@ -160,11 +160,11 @@ export class ApiNetworkProvider implements INetworkProvider { return await this.backingProxyNetworkProvider.simulateTransaction(tx); } - async queryContract(query: IContractQuery): Promise { + async queryContract(query: SmartContractQuery): Promise { try { const request = new ContractQueryRequest(query).toHttpRequest(); const response = await this.doPostGeneric("query", request); - return ContractQueryResponse.fromHttpResponse(response); + return SmartContractQueryResponse.fromHttpResponse(response, query.function); } catch (error: any) { throw new ErrContractQuery(error); } diff --git a/src/networkProviders/contractQueryRequest.ts b/src/networkProviders/contractQueryRequest.ts index d64183c9a..2b1f8b0fc 100644 --- a/src/networkProviders/contractQueryRequest.ts +++ b/src/networkProviders/contractQueryRequest.ts @@ -1,21 +1,20 @@ -import { IContractQuery } from "./interface"; +import { SmartContractQuery } from "../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/interface.ts b/src/networkProviders/interface.ts index b2045ecbc..f1f3a1cf4 100644 --- a/src/networkProviders/interface.ts +++ b/src/networkProviders/interface.ts @@ -1,8 +1,8 @@ import { ITransaction as ITransactionAsInSpecs } from "../interface"; +import { SmartContractQuery, SmartContractQueryResponse } from "../smartContractQuery"; import { TransactionOnNetwork } from "../transactionOnNetwork"; import { TransactionStatus } from "../transactionStatus"; import { AccountOnNetwork } from "./accounts"; -import { ContractQueryResponse } from "./contractQueryResponse"; import { NetworkConfig } from "./networkConfig"; import { NetworkGeneralStatistics } from "./networkGeneralStatistics"; import { NetworkStake } from "./networkStake"; @@ -95,7 +95,7 @@ export interface INetworkProvider { /** * Queries a Smart Contract - runs a pure function defined by the contract and returns its results. */ - queryContract(query: IContractQuery): Promise; + queryContract(query: SmartContractQuery): Promise; /** * Fetches the definition of a fungible token. diff --git a/src/networkProviders/providers.dev.net.spec.ts b/src/networkProviders/providers.dev.net.spec.ts index 0e24a89bf..d1c121e2a 100644 --- a/src/networkProviders/providers.dev.net.spec.ts +++ b/src/networkProviders/providers.dev.net.spec.ts @@ -1,7 +1,7 @@ import { AxiosHeaders } from "axios"; import { assert } from "chai"; import { Address } from "../address"; -import { MockQuery } from "../testutils/dummyQuery"; +import { SmartContractQuery } from "../smartContractQuery"; import { TransactionOnNetwork } from "../transactionOnNetwork"; import { ApiNetworkProvider } from "./apiNetworkProvider"; import { INetworkProvider, ITransactionNext } from "./interface"; @@ -379,20 +379,16 @@ describe("test network providers on devnet: Proxy and API", 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 () { diff --git a/src/networkProviders/proxyNetworkProvider.ts b/src/networkProviders/proxyNetworkProvider.ts index a9989b62b..1c2cbedef 100644 --- a/src/networkProviders/proxyNetworkProvider.ts +++ b/src/networkProviders/proxyNetworkProvider.ts @@ -1,4 +1,5 @@ import { ErrContractQuery, ErrNetworkProvider } from "../errors"; +import { SmartContractQuery, SmartContractQueryResponse } from "../smartContractQuery"; import { TransactionOnNetwork, prepareTransactionForBroadcasting } from "../transactionOnNetwork"; import { TransactionStatus } from "../transactionStatus"; import { getAxios } from "../utils"; @@ -6,8 +7,7 @@ import { AccountOnNetwork, GuardianData } from "./accounts"; import { defaultAxiosConfig } from "./config"; import { BaseUserAgent, EsdtContractAddress } from "./constants"; import { ContractQueryRequest } from "./contractQueryRequest"; -import { ContractQueryResponse } from "./contractQueryResponse"; -import { IAddress, IContractQuery, INetworkProvider, IPagination, ITransaction, ITransactionNext } from "./interface"; +import { IAddress, INetworkProvider, IPagination, ITransaction, ITransactionNext } from "./interface"; import { NetworkConfig } from "./networkConfig"; import { NetworkGeneralStatistics } from "./networkGeneralStatistics"; import { NetworkProviderConfig } from "./networkProviderConfig"; @@ -159,11 +159,11 @@ export class ProxyNetworkProvider implements INetworkProvider { return response; } - async queryContract(query: IContractQuery): Promise { + async queryContract(query: SmartContractQuery): Promise { try { const request = new ContractQueryRequest(query).toHttpRequest(); const response = await this.doPostGeneric("vm-values/query", request); - return ContractQueryResponse.fromHttpResponse(response.data); + return SmartContractQueryResponse.fromHttpResponse(response.data, query.function); } catch (error: any) { throw new ErrContractQuery(error); } @@ -179,16 +179,16 @@ export class ProxyNetworkProvider implements INetworkProvider { } 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: EsdtContractAddress, + 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 { diff --git a/src/smartContractQueriesController.ts b/src/smartContractQueriesController.ts deleted file mode 100644 index aee47a980..000000000 --- a/src/smartContractQueriesController.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { ArgSerializer, ContractFunction, EndpointDefinition, isTyped, NativeSerializer, ResultsParser } from "./abi"; -import { Err, ErrSmartContractQuery } from "./errors"; -import { IContractQueryResponse } from "./interfaceOfNetwork"; -import { SmartContractQuery, SmartContractQueryResponse } from "./smartContractQuery"; - -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 index 275d16bc8..e2eea4836 100644 --- a/src/smartContractQuery.ts +++ b/src/smartContractQuery.ts @@ -1,16 +1,18 @@ +import { Address } from "./address"; + export class SmartContractQuery { - contract: string; - caller?: string; + contract: Address; + caller?: Address; value?: bigint; function: string; - arguments: Uint8Array[]; + arguments?: Uint8Array[]; constructor(options: { - contract: string; - caller?: string; + contract: Address; + caller?: Address; value?: bigint; function: string; - arguments: Uint8Array[]; + arguments?: Uint8Array[]; }) { this.contract = options.contract; this.caller = options.caller; @@ -20,6 +22,22 @@ export class SmartContractQuery { } } +export class SmartContractQueryInput { + contract: Address; + caller?: Address; + value?: bigint; + function: string; + arguments: any[]; + + constructor(options: { contract: Address; caller?: Address; value?: bigint; function: string; arguments: any[] }) { + 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; @@ -32,4 +50,17 @@ export class SmartContractQueryResponse { 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/smartContracts/resources.ts b/src/smartContracts/resources.ts index 78285aad8..71b1f5c60 100644 --- a/src/smartContracts/resources.ts +++ b/src/smartContracts/resources.ts @@ -28,6 +28,11 @@ export interface SmartContractDeployOutcome { returnMessage: string; contracts: DeployedSmartContract[]; } +export type ParsedSmartContractCallOutcome = { + values: any[]; + returnCode: string; + returnMessage: string; +}; export class DeployedSmartContract { address: string; diff --git a/src/smartContractQueriesController.spec.ts b/src/smartContracts/smartContractController.spec.ts similarity index 67% rename from src/smartContractQueriesController.spec.ts rename to src/smartContracts/smartContractController.spec.ts index 63c7887a1..679e124ee 100644 --- a/src/smartContractQueriesController.spec.ts +++ b/src/smartContracts/smartContractController.spec.ts @@ -1,58 +1,57 @@ import BigNumber from "bignumber.js"; import { assert } from "chai"; -import { AbiRegistry, BigUIntValue, BooleanValue, BytesValue, Tuple, U16Value, U64Value } from "./abi"; -import { QueryRunnerAdapter } from "./adapters/queryRunnerAdapter"; -import { ContractQueryResponse } from "./networkProviders"; -import { SmartContractQueriesController } from "./smartContractQueriesController"; -import { SmartContractQueryResponse } from "./smartContractQuery"; -import { MockNetworkProvider, loadAbiRegistry } from "./testutils"; -import { bigIntToBuffer } from "./tokenOperations/codec"; +import { AbiRegistry, BigUIntValue, BooleanValue, BytesValue, Tuple, U16Value, U64Value } from "../abi"; +import { Address } from "../address"; +import { SmartContractQueryResponse } from "../smartContractQuery"; +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,41 +59,37 @@ 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")]); }); @@ -123,16 +118,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 +141,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 +155,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.fromBech32("erd1qqqqqqqqqqqqqpgqvc7gdl0p4s97guh498wgz75k8sav6sjfjlwqh679jy"), function: "bar", arguments: [], }; @@ -192,11 +186,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 +204,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 index e62955fb6..8b6744e74 100644 --- a/src/smartContracts/smartContractController.ts +++ b/src/smartContracts/smartContractController.ts @@ -1,9 +1,9 @@ -import { AbiRegistry } from "../abi"; +import { AbiRegistry, ArgSerializer, isTyped, NativeSerializer } from "../abi"; import { IAccount } from "../accounts/interfaces"; -import { QueryRunnerAdapter } from "../adapters"; -import { IAddress } from "../interface"; +import { Err, ErrSmartContractQuery } from "../errors"; +import { ApiNetworkProvider, ProxyNetworkProvider } from "../networkProviders"; import { INetworkProvider } from "../networkProviders/interface"; -import { SmartContractQueriesController } from "../smartContractQueriesController"; +import { SmartContractQuery, SmartContractQueryInput, SmartContractQueryResponse } from "../smartContractQuery"; import { Transaction } from "../transaction"; import { TransactionComputer } from "../transactionComputer"; import { TransactionOnNetwork } from "../transactionOnNetwork"; @@ -16,23 +16,25 @@ import { SmartContractTransactionsFactory } from "./smartContractTransactionsFac export class SmartContractController { private factory: SmartContractTransactionsFactory; private parser: SmartContractTransactionsOutcomeParser; - private queryController: SmartContractQueriesController; private transactionWatcher: TransactionWatcher; private txComputer: TransactionComputer; + private networkProvider: ApiNetworkProvider | ProxyNetworkProvider | INetworkProvider; + private abi?: AbiRegistry; - constructor(options: { chainID: string; networkProvider: INetworkProvider; abi?: AbiRegistry }) { + constructor(options: { + chainID: string; + networkProvider: ApiNetworkProvider | ProxyNetworkProvider | INetworkProvider; + abi?: AbiRegistry; + }) { this.factory = new SmartContractTransactionsFactory({ config: new TransactionsFactoryConfig({ chainID: options.chainID }), abi: options.abi, }); this.parser = new SmartContractTransactionsOutcomeParser(options); - - this.queryController = new SmartContractQueriesController({ - queryRunner: new QueryRunnerAdapter(options), - abi: options.abi, - }); this.transactionWatcher = new TransactionWatcher(options.networkProvider); this.txComputer = new TransactionComputer(); + this.networkProvider = options.networkProvider; + this.abi = options.abi; } async createTransactionForDeploy( @@ -48,6 +50,10 @@ export class SmartContractController { 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); @@ -79,17 +85,93 @@ export class SmartContractController { return transaction; } - queryContract(contract: IAddress, func: string, args: any[], caller?: IAddress, value?: bigint): Promise { - return this.queryController.query({ - contract: contract.bech32(), - function: func, - arguments: args, - caller: caller ? caller.bech32() : undefined, - value: BigInt(value ?? 0), + 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, }); } - parseDeploy(transactionOnNetwork: TransactionOnNetwork): resources.SmartContractDeployOutcome { - return this.parser.parseDeploy({ transactionOnNetwork }); + 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/smartContracts/smartContractTransactionsOutcomeParser.ts b/src/smartContracts/smartContractTransactionsOutcomeParser.ts index 72923d428..12cdab62d 100644 --- a/src/smartContracts/smartContractTransactionsOutcomeParser.ts +++ b/src/smartContracts/smartContractTransactionsOutcomeParser.ts @@ -5,6 +5,7 @@ import { Err } from "../errors"; import { TransactionEvent } from "../transactionEvents"; import { TransactionOnNetwork } from "../transactionOnNetwork"; import { SmartContractCallOutcome, SmartContractResult } from "../transactionsOutcomeParsers/resources"; +import * as resources from "./resources"; enum Events { SCDeploy = "SCDeploy", @@ -78,24 +79,18 @@ export class SmartContractTransactionsOutcomeParser { }; } - parseExecute(options: { transactionOnNetwork: TransactionOnNetwork; function?: string }): { - values: any[]; - returnCode: string; - returnMessage: string; - } { + parseExecute(options: { + transactionOnNetwork: TransactionOnNetwork; + function?: string; + }): resources.ParsedSmartContractCallOutcome { return this.parseExecuteGivenTransactionOnNetwork(options.transactionOnNetwork, options.function); } protected parseExecuteGivenTransactionOnNetwork( transactionOnNetwork: TransactionOnNetwork, functionName?: string, - ): { - values: any[]; - returnCode: string; - returnMessage: string; - } { + ): resources.ParsedSmartContractCallOutcome { const directCallOutcome = this.findDirectSmartContractCallOutcome(transactionOnNetwork); - if (!this.abi) { return { values: directCallOutcome.returnDataParts, @@ -158,7 +153,7 @@ export class SmartContractTransactionsOutcomeParser { for (const result of transactionOnNetwork.smartContractResults) { const matchesCriteriaOnData = result.data.toString().startsWith(ARGUMENTS_SEPARATOR); - const matchesCriteriaOnReceiver = result.receiver.bech32() === transactionOnNetwork.sender.bech32(); + const matchesCriteriaOnReceiver = result.receiver.toBech32() === transactionOnNetwork.sender.toBech32(); const matchesCriteriaOnPreviousHash = result; const matchesCriteria = matchesCriteriaOnData && matchesCriteriaOnReceiver && matchesCriteriaOnPreviousHash; @@ -220,7 +215,7 @@ export class SmartContractTransactionsOutcomeParser { const [event] = eligibleEvents; const data = Buffer.from(event.data).toString(); - const lastTopic = event.getLastTopic()?.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]; diff --git a/src/testutils/contractController.ts b/src/testutils/contractController.ts deleted file mode 100644 index 18f3f9e46..000000000 --- a/src/testutils/contractController.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Interaction, ResultsParser, TypedOutcomeBundle, UntypedOutcomeBundle } from "../abi"; -import { Logger } from "../logger"; -import { Transaction } from "../transaction"; -import { TransactionOnNetwork } from "../transactionOnNetwork"; -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); - }, - }); - } - - async deploy( - transaction: Transaction, - ): Promise<{ transactionOnNetwork: TransactionOnNetwork; 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: TransactionOnNetwork; 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/mockNetworkProvider.ts b/src/testutils/mockNetworkProvider.ts index 1e6efc012..1f4dc2281 100644 --- a/src/testutils/mockNetworkProvider.ts +++ b/src/testutils/mockNetworkProvider.ts @@ -1,40 +1,109 @@ -import { Query } from "../abi/query"; import { Address } from "../address"; import { AsyncTimer } from "../asyncTimer"; import * as errors from "../errors"; import { ErrMock } from "../errors"; -import { IAddress } from "../interface"; -import { IAccountOnNetwork, IContractQueryResponse, INetworkConfig, ITransactionStatus } from "../interfaceOfNetwork"; +import { IAccountOnNetwork } from "../interfaceOfNetwork"; +import { + AccountOnNetwork, + DefinitionOfFungibleTokenOnNetwork, + DefinitionOfTokenCollectionOnNetwork, + FungibleTokenOfAccountOnNetwork, + NetworkConfig, + NetworkGeneralStatistics, + NetworkStake, + NetworkStatus, + NonFungibleTokenOfAccountOnNetwork, +} from "../networkProviders"; +import { IAddress, INetworkProvider, IPagination, ITransaction, ITransactionNext } from "../networkProviders/interface"; +import { SmartContractQuery, SmartContractQueryResponse } from "../smartContractQuery"; import { Transaction, TransactionHash } from "../transaction"; import { TransactionOnNetwork } from "../transactionOnNetwork"; import { SmartContractResult } from "../transactionsOutcomeParsers"; import { TransactionStatus } from "../transactionStatus"; 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 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.accounts = new Map(); - 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), - }); + this.accounts.set( + MockNetworkProvider.AddressOfAlice.bech32(), + new AccountOnNetwork({ + nonce: 0, + balance: createAccountBalance(1000), + }), + ); + this.accounts.set( + MockNetworkProvider.AddressOfBob.bech32(), + new AccountOnNetwork({ nonce: 5, balance: createAccountBalance(500) }), + ); + this.accounts.set( + MockNetworkProvider.AddressOfCarol.bech32(), + new AccountOnNetwork({ + nonce: 42, + balance: createAccountBalance(300), + }), + ); + } + getNetworkStatus(): Promise { + throw new Error("Method not implemented."); + } + getNetworkStakeStatistics(): Promise { + throw new Error("Method not implemented."); + } + getNetworkGeneralStatistics(): Promise { + throw new Error("Method not implemented."); + } + getFungibleTokensOfAccount( + _address: IAddress, + _pagination?: IPagination, + ): Promise { + throw new Error("Method not implemented."); + } + getNonFungibleTokensOfAccount( + _address: IAddress, + _pagination?: IPagination, + ): Promise { + throw new Error("Method not implemented."); + } + getFungibleTokenOfAccount(_address: IAddress, _tokenIdentifier: string): Promise { + throw new Error("Method not implemented."); + } + getNonFungibleTokenOfAccount( + _address: IAddress, + _collection: string, + _nonce: number, + ): Promise { + throw new Error("Method not implemented."); + } + sendTransactions(_txs: (ITransaction | ITransactionNext)[]): Promise { + 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."); + } + getNonFungibleToken(_collection: string, _nonce: number): 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) { @@ -56,12 +125,12 @@ export class MockNetworkProvider { this.transactions.set(hash.toString(), 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) { + mockGetTransactionWithAnyHashAsNotarizedWithOneResult(returnCodeAndData: string, functionName: string = "") { let contractResult = new SmartContractResult({ data: Buffer.from(returnCodeAndData) }); let predicate = (_hash: string) => true; @@ -69,6 +138,7 @@ export class MockNetworkProvider { status: new TransactionStatus("executed"), smartContractResults: [contractResult], isCompleted: true, + function: functionName, }); this.getTransactionResponders.unshift(new GetTransactionResponder(predicate, response)); @@ -102,7 +172,7 @@ export class MockNetworkProvider { } } - async getAccount(address: IAddress): Promise { + async getAccount(address: IAddress): Promise { let account = this.accounts.get(address.bech32()); if (account) { return account; @@ -147,16 +217,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; @@ -178,10 +248,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; } diff --git a/src/testutils/networkProviders.ts b/src/testutils/networkProviders.ts index e75ffa19b..1892c1d6d 100644 --- a/src/testutils/networkProviders.ts +++ b/src/testutils/networkProviders.ts @@ -1,10 +1,5 @@ -import { Query } from "../abi"; -import { IAddress } from "../interface"; -import { IAccountOnNetwork, IContractQueryResponse, INetworkConfig, ITransactionStatus } from "../interfaceOfNetwork"; import { ApiNetworkProvider, ProxyNetworkProvider } from "../networkProviders"; - -import { Transaction } from "../transaction"; -import { TransactionOnNetwork } from "../transactionOnNetwork"; +import { INetworkProvider } from "../networkProviders/interface"; export function createLocalnetProvider(): INetworkProvider { return new ProxyNetworkProvider("http://localhost:7950", { timeout: 5000 }); @@ -30,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): Promise; - getTransactionStatus(txHash: string): Promise; - sendTransaction(tx: Transaction): Promise; - simulateTransaction(tx: Transaction): Promise; - queryContract(query: Query): Promise; -} diff --git a/src/transaction.local.net.spec.ts b/src/transaction.local.net.spec.ts index 1ac7e632e..8d8201dd9 100644 --- a/src/transaction.local.net.spec.ts +++ b/src/transaction.local.net.spec.ts @@ -1,8 +1,9 @@ import BigNumber from "bignumber.js"; import { assert } from "chai"; import { Logger } from "./logger"; +import { INetworkProvider } from "./networkProviders/interface"; import { loadTestWallets, TestWallet } from "./testutils"; -import { createLocalnetProvider, INetworkProvider } from "./testutils/networkProviders"; +import { createLocalnetProvider } from "./testutils/networkProviders"; import { TokenTransfer } from "./tokens"; import { Transaction } from "./transaction"; import { TransactionComputer } from "./transactionComputer"; diff --git a/src/transactionEvents.ts b/src/transactionEvents.ts index 725f92009..5aadce52c 100644 --- a/src/transactionEvents.ts +++ b/src/transactionEvents.ts @@ -30,12 +30,4 @@ export class TransactionEvent { return result; } - - findFirstOrNoneTopic(predicate: (topic: Uint8Array) => boolean): Uint8Array | undefined { - return this.topics.filter((topic) => predicate(topic))[0]; - } - - getLastTopic(): Uint8Array { - return this.topics[this.topics.length - 1]; - } } diff --git a/src/transactionOnNetwork.ts b/src/transactionOnNetwork.ts index 5b33fa992..10adef8c0 100644 --- a/src/transactionOnNetwork.ts +++ b/src/transactionOnNetwork.ts @@ -72,7 +72,13 @@ export class TransactionOnNetwork { let result = TransactionOnNetwork.fromHttpResponse(txHash, response); result.smartContractResults = response.smartContractResults?.map( - (result: Partial) => new SmartContractResult({ ...result, raw: result }), + (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) { @@ -87,7 +93,13 @@ export class TransactionOnNetwork { let result = TransactionOnNetwork.fromHttpResponse(txHash, response); result.smartContractResults = response.results?.map( - (result: Partial) => new SmartContractResult({ ...result, raw: result }), + (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; @@ -95,7 +107,6 @@ export class TransactionOnNetwork { private static fromHttpResponse(txHash: string, response: any): TransactionOnNetwork { let result = new TransactionOnNetwork(); - result.hash = txHash; result.type = response.type || ""; result.nonce = response.nonce || 0; diff --git a/src/transactionsOutcomeParsers/transactionEventsParser.ts b/src/transactionsOutcomeParsers/transactionEventsParser.ts index 7eb0363fc..93b53df94 100644 --- a/src/transactionsOutcomeParsers/transactionEventsParser.ts +++ b/src/transactionsOutcomeParsers/transactionEventsParser.ts @@ -1,13 +1,11 @@ -import { AbiRegistry, ResultsParser } from "../abi"; +import { AbiRegistry, ArgSerializer } from "../abi"; import { TransactionEvent } from "../transactionEvents"; export class TransactionEventsParser { - private readonly legacyResultsParser: ResultsParser; private readonly abi: AbiRegistry; private readonly firstTopicIsIdentifier: boolean; constructor(options: { abi: AbiRegistry; firstTopicIsIdentifier?: boolean }) { - this.legacyResultsParser = new ResultsParser(); this.abi = options.abi; // By default, we consider that the first topic is the event identifier. @@ -39,12 +37,23 @@ export class TransactionEventsParser { 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(); - return parsedEvent; + // "Indexed" ABI "event.inputs" correspond to "event.topics[1:]": + const indexedInputs = eventDefinition.inputs.filter((input) => 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) => !input.indexed); + const decodedDataParts = argsSerializer.buffersToValues(dataItems, nonIndexedInputs); + for (let i = 0; i < nonIndexedInputs.length; i++) { + result[nonIndexedInputs[i].name] = decodedDataParts[i]?.valueOf(); + } + + return result; } }