diff --git a/.github/workflows/erdjs-publish-alpha-beta.yml b/.github/workflows/erdjs-publish-alpha-beta.yml new file mode 100644 index 00000000..49e22765 --- /dev/null +++ b/.github/workflows/erdjs-publish-alpha-beta.yml @@ -0,0 +1,29 @@ +name: Publish erdjs (alpha / beta) + +on: + workflow_dispatch: + inputs: + channel: + type: choice + description: NPM channel + options: + - alpha + - beta + +jobs: + publish-npm: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: 12 + registry-url: https://registry.npmjs.org/ + + - run: npm ci + - run: npm test + + - name: Publish to npmjs + env: + NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} + run: npm publish --access=public --tag=${{ github.event.inputs.channel }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 83b622f9..f8a4d7d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,36 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how ## Unreleased - TBD +## [10.0.0] + - [Breaking changes: improve contract interactions and interpretation of contract results](https://github.com/ElrondNetwork/elrond-sdk-erdjs/pull/159) + +**Breaking changes** + + - `ExecutionResultsBundle` and `QueryResponseBundle` have been removed, and replaced by `TypedOutcomeBundle` (and its untyped counterpart, `UntypedOutcomeBundle`). + - `SmartContractResults` has been changed to not use the concepts `immediate result` and `resulting calls` anymore. Instead, interpreting `SmartContractResults.items` is now the responsibility of the `ResultsParser` (on which the contract controllers depend). + - Redesigned `QueryResponse`, changed most of its public interface. Results interpretation is now the responsibility of the results parser, called by the smart contract controllers. + - `interpretQueryResponse()` and `interpretExecutionResults()` do not exist on the `Interaction` object anymore. Now, querying / executing an interaction against the controller will return the interpreted results. + - `TokenIdentifierValue` is constructed using a `string`, not a `buffer`. Its `valueOf()` is now a string, as well. + - The `Interaction` constructor does not receive the `interpretingFunction` parameter anymore. + - `Interaction.getInterpretingFunction()` and `Interaction.getExecutingFunction()` have been removed, replaced by `Interaction.getFunction()`. + - `DefaultInteractionRunner` has been removed, and replaced by **smart contract controllers**. + - `StrictChecker` has been renamed to `InteractionChecker`. It's public interface - the function `checkInteraction()` - has changed as well (it also requires the endpoint definition now, as a second parameter). + - The functions `getReceipt()`, `getSmartContractResults()` and `getLogs()` of `TransactionOnNetwork` have been removed. The underlying properties are now public. + - Renamed `OptionValue.newMissingType()` to `OptionValue.newMissingTyped()` + - Queries with a return code different than `Ok` do not automatically throw an exception anymore (`assertSuccess()` has to be called explicitly in order to throw). + +**Other changes** + + - `SmartContract`, in addition to `methods`, now also has a `methodAuto` object that allows one to create interactions without explicitly specifying the types of the arguments. Automatic type inference (within erdjs' typesystem) is leveraged. The type inference system was implemented in the past, in the `nativeSerializer` component - PR https://github.com/ElrondNetwork/elrond-sdk-erdjs/pull/9 by @claudiu725. + - Added utility function `getFieldValue()` on `Struct` and `EnumValue`. + - Refactoring in the `networkProvider` package (under development, in order to merge the provider interfaces under a single one) + - Added utility function `Interaction.useThenIncrementNonceOf()` + - Fixed `nativeSerializer` to not depend on `SmartContract`, `ContractWrapper` and `TestWallet` anymore (gathered under an interface). + - Replaced the old `lottery-egld` with the new `lottery-esdt` in integration tests. + - Added missing tests for some components: `nativeSerializer`, `struct`, `enum`. + - Added utility function `OptionalValue.newMissing()`. Added "fake" covariance wrt. "null type parameter" (when value is missing) on `OptionalType`. + - Added utility functions (typed value factories): `createListOfAddresses`, `createListOfTokenIdentifiers`. + ## [9.2.3] - [Fix log level in transaction watcher.](https://github.com/ElrondNetwork/elrond-sdk-erdjs/pull/160) diff --git a/README.md b/README.md index 49e6b23b..8d508ed5 100644 --- a/README.md +++ b/README.md @@ -18,17 +18,17 @@ Elrond SDK for JavaScript and TypeScript (written in TypeScript). The most comprehensive usage examples are captured within the unit and the integration tests. Specifically, in the `*.spec.ts` files of the source code. For example: - - [transaction.dev.net.spec.ts](https://github.com/ElrondNetwork/elrond-sdk-erdjs/tree/main/src/transaction.dev.net.spec.ts) + - [transaction.local.net.spec.ts](https://github.com/ElrondNetwork/elrond-sdk-erdjs/tree/main/src/transaction.local.net.spec.ts) - [address.spec.ts](https://github.com/ElrondNetwork/elrond-sdk-erdjs/tree/main/src/address.spec.ts) - [transactionPayloadBuilders.spec.ts](https://github.com/ElrondNetwork/elrond-sdk-erdjs/tree/main/src/smartcontracts/transactionPayloadBuilders.spec.ts) - [smartContract.spec.ts](https://github.com/ElrondNetwork/elrond-sdk-erdjs/tree/main/src/smartcontracts/smartContract.spec.ts) - - [smartContract.dev.net.spec.ts](https://github.com/ElrondNetwork/elrond-sdk-erdjs/tree/main/src/smartcontracts/smartContract.dev.net.spec.ts) + - [smartContract.local.net.spec.ts](https://github.com/ElrondNetwork/elrond-sdk-erdjs/tree/main/src/smartcontracts/smartContract.local.net.spec.ts) - [query.spec.ts](https://github.com/ElrondNetwork/elrond-sdk-erdjs/tree/main/src/smartcontracts/query.spec.ts) - [query.main.net.spec.ts](https://github.com/ElrondNetwork/elrond-sdk-erdjs/tree/main/src/smartcontracts/query.main.net.spec.ts) For advanced smart contract interaction, using ABIs, please see the following test files: - - [interaction.dev.net.spec.ts](https://github.com/ElrondNetwork/elrond-sdk-erdjs/tree/main/src/smartcontracts/interaction.dev.net.spec.ts) + - [interaction.local.net.spec.ts](https://github.com/ElrondNetwork/elrond-sdk-erdjs/tree/main/src/smartcontracts/interaction.local.net.spec.ts) - [abiRegistry.spec.ts](https://github.com/ElrondNetwork/elrond-sdk-erdjs/tree/main/src/smartcontracts/typesystem/abiRegistry.spec.ts) - [argSerializer.spec.ts](https://github.com/ElrondNetwork/elrond-sdk-erdjs/tree/main/src/smartcontracts/argSerializer.spec.ts) diff --git a/package-lock.json b/package-lock.json index 56a8e577..0f4bb372 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@elrondnetwork/erdjs", - "version": "9.2.3", + "version": "10.0.0-beta.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index ca1a90b6..db03282d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@elrondnetwork/erdjs", - "version": "9.2.3", + "version": "10.0.0-beta.1", "description": "Smart Contracts interaction framework", "main": "out/index.js", "types": "out/index.d.js", diff --git a/src/errors.ts b/src/errors.ts index 5aa28431..73d4563a 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -409,6 +409,33 @@ export class ErrTypingSystem extends Err { } } +/** + * Signals a missing field on a struct. + */ + export class ErrMissingFieldOnStruct extends Err { + public constructor(fieldName: string, structName: string) { + super(`field ${fieldName} does not exist on struct ${structName}`); + } +} + +/** + * Signals a missing field on an enum. + */ + export class ErrMissingFieldOnEnum extends Err { + public constructor(fieldName: string, enumName: string) { + super(`field ${fieldName} does not exist on enum ${enumName}`); + } +} + +/** + * Signals an error when parsing the contract results. + */ + export class ErrCannotParseContractResults extends Err { + public constructor(details: string) { + super(`cannot parse contract results: ${details}`); + } +} + /** * Signals a generic codec (encode / decode) error. */ diff --git a/src/networkProvider/contractResults.ts b/src/networkProvider/contractResults.ts index c4b0126a..8cebbb3e 100644 --- a/src/networkProvider/contractResults.ts +++ b/src/networkProvider/contractResults.ts @@ -5,7 +5,7 @@ import { Hash } from "../hash"; import { IContractQueryResponse, IContractResultItem, IContractResults } from "./interface"; import { GasLimit, GasPrice } from "../networkParams"; import { Nonce } from "../nonce"; -import { ArgSerializer, EndpointDefinition, MaxUint64, ReturnCode, TypedValue } from "../smartcontracts"; +import { MaxUint64, ReturnCode } from "../smartcontracts"; import { TransactionHash } from "../transaction"; export class ContractResults implements IContractResults { @@ -80,16 +80,6 @@ export class ContractResultItem implements IContractResultItem { return item; } - - getOutputUntyped(): Buffer[] { - // TODO: Decide how to parse "data" (immediate results vs. other results). - throw new Error("Method not implemented."); - } - - getOutputTyped(_endpointDefinition: EndpointDefinition): TypedValue[] { - // TODO: Decide how to parse "data" (immediate results vs. other results). - throw new Error("Method not implemented."); - } } export class ContractQueryResponse implements IContractQueryResponse { @@ -110,14 +100,7 @@ export class ContractQueryResponse implements IContractQueryResponse { return response; } - getOutputUntyped(): Buffer[] { - let buffers = this.returnData.map((item) => Buffer.from(item || "", "base64")); - return buffers; - } - - getOutputTyped(endpointDefinition: EndpointDefinition): TypedValue[] { - let buffers = this.getOutputUntyped(); - let values = new ArgSerializer().buffersToValues(buffers, endpointDefinition!.output); - return values; + getReturnDataParts(): Buffer[] { + return this.returnData.map((item) => Buffer.from(item || "")); } } diff --git a/src/networkProvider/interface.ts b/src/networkProvider/interface.ts index 287fd791..4f21dbe4 100644 --- a/src/networkProvider/interface.ts +++ b/src/networkProvider/interface.ts @@ -9,7 +9,7 @@ import { NetworkStake } from "../networkStake"; import { NetworkStatus } from "../networkStatus"; import { Nonce } from "../nonce"; import { Signature } from "../signature"; -import { EndpointDefinition, Query, ReturnCode, TypedValue } from "../smartcontracts"; +import { Query, ReturnCode } from "../smartcontracts"; import { Stats } from "../stats"; import { Transaction, TransactionHash, TransactionStatus } from "../transaction"; import { TransactionLogs } from "../transactionLogs"; @@ -204,9 +204,6 @@ export interface IContractResultItem { gasPrice: GasPrice; callType: number; returnMessage: string; - - getOutputUntyped(): Buffer[]; - getOutputTyped(endpointDefinition: EndpointDefinition): TypedValue[]; } export interface IContractQueryResponse { @@ -214,9 +211,8 @@ export interface IContractQueryResponse { returnCode: ReturnCode; returnMessage: string; gasUsed: GasLimit; - - getOutputUntyped(): Buffer[]; - getOutputTyped(endpointDefinition: EndpointDefinition): TypedValue[]; + + getReturnDataParts(): Buffer[]; } export interface IContractSimulation { diff --git a/src/networkProvider/providers.dev.net.spec.ts b/src/networkProvider/providers.dev.net.spec.ts index 8ee0031b..affbd8d0 100644 --- a/src/networkProvider/providers.dev.net.spec.ts +++ b/src/networkProvider/providers.dev.net.spec.ts @@ -227,7 +227,7 @@ describe("test network providers on devnet: Proxy and API", function () { let proxyResponse = await proxyProvider.queryContract(query); assert.deepEqual(apiResponse, proxyResponse); - assert.deepEqual(apiResponse.getOutputUntyped(), proxyResponse.getOutputUntyped()); + assert.deepEqual(apiResponse.getReturnDataParts(), proxyResponse.getReturnDataParts()); // Query: increment counter query = new Query({ @@ -240,7 +240,7 @@ describe("test network providers on devnet: Proxy and API", function () { proxyResponse = await proxyProvider.queryContract(query); assert.deepEqual(apiResponse, proxyResponse); - assert.deepEqual(apiResponse.getOutputUntyped(), proxyResponse.getOutputUntyped()); + assert.deepEqual(apiResponse.getReturnDataParts(), proxyResponse.getReturnDataParts()); // Query: issue ESDT query = new Query({ @@ -260,6 +260,6 @@ describe("test network providers on devnet: Proxy and API", function () { proxyResponse = await proxyProvider.queryContract(query); assert.deepEqual(apiResponse, proxyResponse); - assert.deepEqual(apiResponse.getOutputUntyped(), proxyResponse.getOutputUntyped()); + assert.deepEqual(apiResponse.getReturnDataParts(), proxyResponse.getReturnDataParts()); }); }); diff --git a/src/proxyProvider.ts b/src/proxyProvider.ts index 65f9b7b9..2650e25e 100644 --- a/src/proxyProvider.ts +++ b/src/proxyProvider.ts @@ -70,7 +70,7 @@ export class ProxyProvider implements IProvider { return this.doPostGeneric("vm-values/query", data, (response) => QueryResponse.fromHttpResponse(response.data || response.vmOutput) ); - } catch (err) { + } catch (err: any) { throw errors.ErrContractQuery.increaseSpecificity(err); } } diff --git a/src/smartcontracts/codec/binary.spec.ts b/src/smartcontracts/codec/binary.spec.ts index 278c681d..7d64f6b3 100644 --- a/src/smartcontracts/codec/binary.spec.ts +++ b/src/smartcontracts/codec/binary.spec.ts @@ -3,7 +3,6 @@ import { BinaryCodec, BinaryCodecConstraints } from "./binary"; import { AddressType, AddressValue, BigIntType, BigUIntType, BigUIntValue, BooleanType, BooleanValue, I16Type, I32Type, I64Type, I8Type, NumericalType, NumericalValue, Struct, Field, StructType, TypedValue, U16Type, U32Type, U32Value, U64Type, U64Value, U8Type, U8Value, List, ListType, EnumType, EnumVariantDefinition, EnumValue, ArrayVec, ArrayVecType, U16Value, TokenIdentifierType, TokenIdentifierValue, StringValue, StringType } from "../typesystem"; import { isMsbOne } from "./utils"; import { Address } from "../../address"; -import { Balance } from "../../balance"; import { BytesType, BytesValue } from "../typesystem/bytes"; import BigNumber from "bignumber.js"; import { FieldDefinition } from "../typesystem/fields"; @@ -189,29 +188,27 @@ describe("test binary codec (advanced)", () => { let fooType = new StructType( "Foo", [ + new FieldDefinition("token_identifier", "", new TokenIdentifierType()), new FieldDefinition("ticket_price", "", new BigUIntType()), new FieldDefinition("tickets_left", "", new U32Type()), new FieldDefinition("deadline", "", new U64Type()), new FieldDefinition("max_entries_per_user", "", new U32Type()), new FieldDefinition("prize_distribution", "", new BytesType()), - new FieldDefinition("whitelist", "", new ListType(new AddressType())), - new FieldDefinition("current_ticket_number", "", new U32Type()), new FieldDefinition("prize_pool", "", new BigUIntType()) ] ); let fooStruct = new Struct(fooType, [ - new Field(new BigUIntValue(Balance.egld(10).valueOf()), "ticket_price"), + new Field(new TokenIdentifierValue("lucky-token"), "token_identifier"), + new Field(new BigUIntValue(1), "ticket_price"), new Field(new U32Value(0), "tickets_left"), new Field(new U64Value(new BigNumber("0x000000005fc2b9db")), "deadline"), new Field(new U32Value(0xffffffff), "max_entries_per_user"), new Field(new BytesValue(Buffer.from([0x64])), "prize_distribution"), - new Field(new List(new ListType(new AddressType()), []), "whitelist"), - new Field(new U32Value(9472), "current_ticket_number"), new Field(new BigUIntValue(new BigNumber("94720000000000000000000")), "prize_pool") ]); - let encodedExpected = serialized("[00000008|8ac7230489e80000] [00000000] [000000005fc2b9db] [ffffffff] [00000001|64] [00000000] [00002500] [0000000a|140ec80fa7ee88000000]"); + let encodedExpected = serialized("[0000000b|6c75636b792d746f6b656e] [00000001|01] [00000000] [000000005fc2b9db] [ffffffff] [00000001|64] [0000000a|140ec80fa7ee88000000]"); let encoded = codec.encodeNested(fooStruct); assert.deepEqual(encoded, encodedExpected); @@ -221,13 +218,12 @@ describe("test binary codec (advanced)", () => { let plainFoo = decoded.valueOf(); assert.deepEqual(plainFoo, { - ticket_price: new BigNumber("10000000000000000000"), + token_identifier: "lucky-token", + ticket_price: new BigNumber("1"), tickets_left: new BigNumber(0), deadline: new BigNumber("0x000000005fc2b9db", 16), max_entries_per_user: new BigNumber(0xffffffff), prize_distribution: Buffer.from([0x64]), - whitelist: [], - current_ticket_number: new BigNumber(9472), prize_pool: new BigNumber("94720000000000000000000") }); }); @@ -244,7 +240,7 @@ describe("test binary codec (advanced)", () => { ); let paymentStruct = new Struct(paymentType, [ - new Field(new TokenIdentifierValue(Buffer.from("TEST-1234")), "token_identifier"), + new Field(new TokenIdentifierValue("TEST-1234"), "token_identifier"), new Field(new U64Value(new BigNumber(42)), "nonce"), new Field(new BigUIntValue(new BigNumber("123450000000000000000")), "amount") ]); @@ -259,7 +255,7 @@ describe("test binary codec (advanced)", () => { let decodedPayment = decoded.valueOf(); assert.deepEqual(decodedPayment, { - token_identifier: Buffer.from("TEST-1234"), + token_identifier: "TEST-1234", nonce: new BigNumber(42), amount: new BigNumber("123450000000000000000"), }); diff --git a/src/smartcontracts/codec/option.ts b/src/smartcontracts/codec/option.ts index 8bbe758e..1b556d05 100644 --- a/src/smartcontracts/codec/option.ts +++ b/src/smartcontracts/codec/option.ts @@ -15,7 +15,7 @@ export class OptionValueBinaryCodec { decodeNested(buffer: Buffer, type: Type): [OptionValue, number] { if (buffer[0] == 0x00) { - return [OptionValue.newMissingType(type), 1]; + return [OptionValue.newMissingTyped(type), 1]; } if (buffer[0] != 0x01) { diff --git a/src/smartcontracts/codec/tokenIdentifier.ts b/src/smartcontracts/codec/tokenIdentifier.ts index da005943..ea28d321 100644 --- a/src/smartcontracts/codec/tokenIdentifier.ts +++ b/src/smartcontracts/codec/tokenIdentifier.ts @@ -7,20 +7,20 @@ export class TokenIdentifierCodec { decodeNested(buffer: Buffer): [TokenIdentifierValue, number] { let [bytesValue, length] = this.bytesCodec.decodeNested(buffer); - return [new TokenIdentifierValue(bytesValue.valueOf()), length]; + return [new TokenIdentifierValue(bytesValue.toString()), length]; } decodeTopLevel(buffer: Buffer): TokenIdentifierValue { let bytesValue = this.bytesCodec.decodeTopLevel(buffer); - return new TokenIdentifierValue(bytesValue.valueOf()); + return new TokenIdentifierValue(bytesValue.toString()); } encodeNested(tokenIdentifier: TokenIdentifierValue): Buffer { - let bytesValue = new BytesValue(tokenIdentifier.valueOf()); + let bytesValue = BytesValue.fromUTF8(tokenIdentifier.valueOf()); return this.bytesCodec.encodeNested(bytesValue); } encodeTopLevel(tokenIdentifier: TokenIdentifierValue): Buffer { - return tokenIdentifier.valueOf(); + return Buffer.from(tokenIdentifier.valueOf()); } } diff --git a/src/smartcontracts/defaultRunner.ts b/src/smartcontracts/defaultRunner.ts deleted file mode 100644 index 4979638e..00000000 --- a/src/smartcontracts/defaultRunner.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { IProvider, ISigner } from "../interface"; -import { ExecutionResultsBundle, IInteractionChecker, IInteractionRunner, QueryResponseBundle } from "./interface"; -import { Interaction } from "./interaction"; -import { Transaction } from "../transaction"; -import { Address } from "../address"; - -/** - * An interaction runner suitable for backends or wallets. - * Not suitable for dapps, which depend on external signers (wallets, ledger etc.). - */ -export class DefaultInteractionRunner implements IInteractionRunner { - private readonly checker: IInteractionChecker; - private readonly signer: ISigner; - private readonly provider: IProvider; - - constructor(checker: IInteractionChecker, signer: ISigner, provider: IProvider) { - this.checker = checker; - this.signer = signer; - this.provider = provider; - } - - /** - * Given an interaction, broadcasts its compiled transaction. - */ - async run(interaction: Interaction): Promise { - this.checkInteraction(interaction); - - let transaction = interaction.buildTransaction(); - await this.signer.sign(transaction); - await transaction.send(this.provider); - return transaction; - } - - /** - * Given an interaction, broadcasts its compiled transaction (and also waits for its execution on the Network). - */ - async runAwaitExecution(interaction: Interaction): Promise { - this.checkInteraction(interaction); - - let transaction = await this.run(interaction); - await transaction.awaitExecuted(this.provider); - // This will wait until the transaction is notarized, as well (so that SCRs are returned by the API). - let transactionOnNetwork = await transaction.getAsOnNetwork(this.provider); - let bundle = interaction.interpretExecutionResults(transactionOnNetwork); - return bundle; - } - - async runQuery(interaction: Interaction, caller?: Address): Promise { - this.checkInteraction(interaction); - - let query = interaction.buildQuery(); - query.caller = caller || this.signer.getAddress(); - let response = await this.provider.queryContract(query); - let bundle = interaction.interpretQueryResponse(response); - return bundle; - } - - async runSimulation(interaction: Interaction): Promise { - this.checkInteraction(interaction); - - let transaction = interaction.buildTransaction(); - await this.signer.sign(transaction); - return await transaction.simulate(this.provider); - } - - private checkInteraction(interaction: Interaction) { - this.checker.checkInteraction(interaction); - } -} diff --git a/src/smartcontracts/index.ts b/src/smartcontracts/index.ts index 50bcd518..3e08174a 100644 --- a/src/smartcontracts/index.ts +++ b/src/smartcontracts/index.ts @@ -8,17 +8,18 @@ export * from "./argSerializer"; export * from "./code"; export * from "./codec"; export * from "./codeMetadata"; -export * from "./defaultRunner"; +export * from "./smartContractController"; export * from "./function"; export * from "./interaction"; +export * from "./interactionChecker"; export * from "./interface"; export * from "./nativeSerializer"; export * from "./query"; export * from "./queryResponse"; +export * from "./resultsParser"; export * from "./returnCode"; export * from "./smartContract"; export * from "./smartContractResults"; -export * from "./strictChecker"; export * from "./transactionPayloadBuilders"; export * from "./typesystem"; export * from "./wrapper"; diff --git a/src/smartcontracts/interaction.local.net.spec.ts b/src/smartcontracts/interaction.local.net.spec.ts index 1b3ed40d..8dec908b 100644 --- a/src/smartcontracts/interaction.local.net.spec.ts +++ b/src/smartcontracts/interaction.local.net.spec.ts @@ -1,14 +1,12 @@ -import { StrictChecker } from "./strictChecker"; -import { DefaultInteractionRunner } from "./defaultRunner"; +import { DefaultSmartContractController } from "./smartContractController"; import { SmartContract } from "./smartContract"; -import { BigUIntValue, OptionValue, TypedValue, U32Value } from "./typesystem"; +import { BigUIntValue, OptionalValue, OptionValue, TokenIdentifierValue, U32Value } from "./typesystem"; import { loadAbiRegistry, loadContractCode, loadTestWallets, TestWallet } from "../testutils"; import { SmartContractAbi } from "./abi"; import { assert } from "chai"; import { Interaction } from "./interaction"; import { GasLimit } from "../networkParams"; import { ReturnCode } from "./returnCode"; -import { Balance } from "../balance"; import BigNumber from "bignumber.js"; import { NetworkConfig } from "../networkConfig"; import { BytesValue } from "./typesystem/bytes"; @@ -16,43 +14,56 @@ import { chooseProxyProvider } from "../interactive"; describe("test smart contract interactor", function () { - let checker = new StrictChecker(); let provider = chooseProxyProvider("local-testnet"); let alice: TestWallet; - let runner: DefaultInteractionRunner; before(async function () { ({ alice } = await loadTestWallets()); - runner = new DefaultInteractionRunner(checker, alice.signer, provider); }); it("should interact with 'answer' (local testnet)", async function () { - this.timeout(60000); + this.timeout(80000); let abiRegistry = await loadAbiRegistry(["src/testdata/answer.abi.json"]); let abi = new SmartContractAbi(abiRegistry, ["answer"]); let contract = new SmartContract({ abi: abi }); + let controller = new DefaultSmartContractController(abi, provider); // Currently, this has to be called before creating any Interaction objects, // because the Transaction objects created under the hood point to the "default" NetworkConfig. await NetworkConfig.getDefault().sync(provider); await alice.sync(provider); - await deploy(contract, "src/testdata/answer.wasm", new GasLimit(3000000), []); + + // Deploy the contract + let deployTransaction = contract.deploy({ + code: await loadContractCode("src/testdata/answer.wasm"), + gasLimit: new GasLimit(3000000), + initArguments: [] + }); + + deployTransaction.setNonce(alice.account.getNonceThenIncrement()); + await alice.signer.sign(deployTransaction); + let { bundle: { returnCode } } = await controller.deploy(deployTransaction); + assert.isTrue(returnCode.isSuccess()); let interaction = contract.methods.getUltimateAnswer().withGasLimit(new GasLimit(3000000)); // Query - let queryResponseBundle = await runner.runQuery(interaction); + let queryResponseBundle = await controller.query(interaction); assert.lengthOf(queryResponseBundle.values, 1); - assert.deepEqual(queryResponseBundle.firstValue.valueOf(), new BigNumber(42)); + assert.deepEqual(queryResponseBundle.firstValue!.valueOf(), new BigNumber(42)); assert.isTrue(queryResponseBundle.returnCode.equals(ReturnCode.Ok)); // Execute, do not wait for execution - await runner.run(interaction.withNonce(alice.account.getNonceThenIncrement())); + let transaction = interaction.useThenIncrementNonceOf(alice.account).buildTransaction(); + await alice.signer.sign(transaction); + await transaction.send(provider); // Execute, and wait for execution - let executionResultsBundle = await runner.runAwaitExecution(interaction.withNonce(alice.account.getNonceThenIncrement())); + transaction = interaction.useThenIncrementNonceOf(alice.account).buildTransaction(); + await alice.signer.sign(transaction); + let { bundle: executionResultsBundle } = await controller.execute(interaction, transaction); assert.lengthOf(executionResultsBundle.values, 1); - assert.deepEqual(executionResultsBundle.firstValue.valueOf(), new BigNumber(42)); + assert.deepEqual(executionResultsBundle.firstValue!.valueOf(), new BigNumber(42)); assert.isTrue(executionResultsBundle.returnCode.equals(ReturnCode.Ok)); }); @@ -62,108 +73,128 @@ describe("test smart contract interactor", function () { let abiRegistry = await loadAbiRegistry(["src/testdata/counter.abi.json"]); let abi = new SmartContractAbi(abiRegistry, ["counter"]); let contract = new SmartContract({ abi: abi }); + let controller = new DefaultSmartContractController(abi, provider); // Currently, this has to be called before creating any Interaction objects, // because the Transaction objects created under the hood point to the "default" NetworkConfig. await NetworkConfig.getDefault().sync(provider); await alice.sync(provider); - await deploy(contract, "src/testdata/counter.wasm", new GasLimit(3000000), []); + + // Deploy the contract + let deployTransaction = contract.deploy({ + code: await loadContractCode("src/testdata/counter.wasm"), + gasLimit: new GasLimit(3000000), + initArguments: [] + }); + + deployTransaction.setNonce(alice.account.getNonceThenIncrement()); + await alice.signer.sign(deployTransaction); + let { bundle: { returnCode } } = await controller.deploy(deployTransaction); + assert.isTrue(returnCode.isSuccess()); let getInteraction = contract.methods.get(); let incrementInteraction = (contract.methods.increment()).withGasLimit(new GasLimit(3000000)); let decrementInteraction = (contract.methods.decrement()).withGasLimit(new GasLimit(3000000)); // Query "get()" - let { firstValue: counterValue } = await runner.runQuery(getInteraction); - assert.deepEqual(counterValue.valueOf(), new BigNumber(1)); + let { firstValue: counterValue } = await controller.query(getInteraction); + assert.deepEqual(counterValue!.valueOf(), new BigNumber(1)); // Increment, wait for execution. - let { firstValue: valueAfterIncrement } = await runner.runAwaitExecution(incrementInteraction.withNonce(alice.account.getNonceThenIncrement())); - assert.deepEqual(valueAfterIncrement.valueOf(), new BigNumber(2)); - - // Decrement. Wait for execution of the second transaction. - await runner.run(decrementInteraction.withNonce(alice.account.getNonceThenIncrement())); - let { firstValue: valueAfterDecrement } = await runner.runAwaitExecution(decrementInteraction.withNonce(alice.account.getNonceThenIncrement())) - assert.deepEqual(valueAfterDecrement.valueOf(), new BigNumber(0)); + let incrementTransaction = incrementInteraction.useThenIncrementNonceOf(alice.account).buildTransaction(); + await alice.signer.sign(incrementTransaction); + let { bundle: { firstValue: valueAfterIncrement } } = await controller.execute(incrementInteraction, incrementTransaction); + assert.deepEqual(valueAfterIncrement!.valueOf(), new BigNumber(2)); + + // Decrement twice. Wait for execution of the second transaction. + let decrementTransaction = decrementInteraction.useThenIncrementNonceOf(alice.account).buildTransaction(); + await alice.signer.sign(decrementTransaction); + await decrementTransaction.send(provider); + + decrementTransaction = decrementInteraction.useThenIncrementNonceOf(alice.account).buildTransaction(); + await alice.signer.sign(decrementTransaction); + let { bundle: { firstValue: valueAfterDecrement } } = await controller.execute(decrementInteraction, decrementTransaction); + assert.deepEqual(valueAfterDecrement!.valueOf(), new BigNumber(0)); }); - it("should interact with 'lottery_egld' (local testnet)", async function () { - this.timeout(120000); + it("should interact with 'lottery-esdt' (local testnet)", async function () { + this.timeout(140000); - let abiRegistry = await loadAbiRegistry(["src/testdata/lottery_egld.abi.json"]); + let abiRegistry = await loadAbiRegistry(["src/testdata/lottery-esdt.abi.json"]); let abi = new SmartContractAbi(abiRegistry, ["Lottery"]); let contract = new SmartContract({ abi: abi }); + let controller = new DefaultSmartContractController(abi, provider); // Currently, this has to be called before creating any Interaction objects, // because the Transaction objects created under the hood point to the "default" NetworkConfig. await NetworkConfig.getDefault().sync(provider); await alice.sync(provider); - await deploy(contract, "src/testdata/lottery_egld.wasm", new GasLimit(100000000), []); + + // Deploy the contract + let deployTransaction = contract.deploy({ + code: await loadContractCode("src/testdata/lottery-esdt.wasm"), + gasLimit: new GasLimit(100000000), + initArguments: [] + }); + + deployTransaction.setNonce(alice.account.getNonceThenIncrement()); + await alice.signer.sign(deployTransaction); + let { bundle: { returnCode } } = await controller.deploy(deployTransaction); + assert.isTrue(returnCode.isSuccess()); let startInteraction = contract.methods.start([ BytesValue.fromUTF8("lucky"), - new BigUIntValue(Balance.egld(1).valueOf()), + new TokenIdentifierValue("EGLD"), + new BigUIntValue(1), OptionValue.newMissing(), OptionValue.newMissing(), OptionValue.newProvided(new U32Value(1)), OptionValue.newMissing(), OptionValue.newMissing(), - ]).withGasLimit(new GasLimit(15000000)); + OptionalValue.newMissing() + ]).withGasLimit(new GasLimit(30000000)); let lotteryStatusInteraction = contract.methods.status([ BytesValue.fromUTF8("lucky") - ]).withGasLimit(new GasLimit(15000000)); + ]).withGasLimit(new GasLimit(5000000)); - let getLotteryInfoInteraction = contract.methods.lotteryInfo([ + let getLotteryInfoInteraction = contract.methods.getLotteryInfo([ BytesValue.fromUTF8("lucky") - ]).withGasLimit(new GasLimit(15000000)); + ]).withGasLimit(new GasLimit(5000000)); // start() - let { returnCode: startReturnCode, values: startReturnvalues } = await runner.runAwaitExecution(startInteraction.withNonce(alice.account.getNonceThenIncrement())) - assert.isTrue(startReturnCode.equals(ReturnCode.Ok)); - assert.lengthOf(startReturnvalues, 0); + let startTransaction = startInteraction.useThenIncrementNonceOf(alice.account).buildTransaction(); + await alice.signer.sign(startTransaction); + let { bundle: bundleStart } = await controller.execute(startInteraction, startTransaction); + assert.isTrue(bundleStart.returnCode.equals(ReturnCode.Ok)); + assert.lengthOf(bundleStart.values, 0); // status() - let { returnCode: statusReturnCode, values: statusReturnValues, firstValue: statusFirstValue } = await runner.runAwaitExecution(lotteryStatusInteraction.withNonce(alice.account.getNonceThenIncrement())) - assert.isTrue(statusReturnCode.equals(ReturnCode.Ok)); - assert.lengthOf(statusReturnValues, 1); - assert.equal(statusFirstValue.valueOf().name, "Running"); + let lotteryStatusTransaction = lotteryStatusInteraction.useThenIncrementNonceOf(alice.account).buildTransaction(); + await alice.signer.sign(lotteryStatusTransaction); + let { bundle: bundleStatus } = await controller.execute(lotteryStatusInteraction, lotteryStatusTransaction); + assert.isTrue(bundleStatus.returnCode.equals(ReturnCode.Ok)); + assert.lengthOf(bundleStatus.values, 1); + assert.equal(bundleStatus.firstValue!.valueOf().name, "Running"); // lotteryInfo() (this is a view function, but for the sake of the test, we'll execute it) - let { returnCode: infoReturnCode, values: infoReturnValues, firstValue: infoFirstValue } = await runner.runAwaitExecution(getLotteryInfoInteraction.withNonce(alice.account.getNonceThenIncrement())) - assert.isTrue(infoReturnCode.equals(ReturnCode.Ok)); - assert.lengthOf(infoReturnValues, 1); + let lotteryInfoTransaction = getLotteryInfoInteraction.useThenIncrementNonceOf(alice.account).buildTransaction(); + await alice.signer.sign(lotteryInfoTransaction); + let { bundle: bundleLotteryInfo } = await controller.execute(getLotteryInfoInteraction, lotteryInfoTransaction); + assert.isTrue(bundleLotteryInfo.returnCode.equals(ReturnCode.Ok)); + assert.lengthOf(bundleLotteryInfo.values, 1); // Ignore "deadline" field in our test - let info = infoFirstValue.valueOf(); + let info = bundleLotteryInfo.firstValue!.valueOf(); delete info.deadline; assert.deepEqual(info, { - ticket_price: new BigNumber("1000000000000000000"), + token_identifier: "EGLD", + ticket_price: new BigNumber("1"), tickets_left: new BigNumber(800), max_entries_per_user: new BigNumber(1), prize_distribution: Buffer.from([0x64]), - whitelist: [], - current_ticket_number: new BigNumber(0), prize_pool: new BigNumber("0") }); }); - - /** - * Deploy is not currently supported by interactors yet. - * We will deploy the contracts using the existing approach. - */ - async function deploy(contract: SmartContract, path: string, gasLimit: GasLimit, initArguments: TypedValue[]): Promise { - let transactionDeploy = contract.deploy({ - code: await loadContractCode(path), - gasLimit: gasLimit, - initArguments: initArguments - }); - - // In these tests, all contracts are deployed by Alice. - transactionDeploy.setNonce(alice.account.getNonceThenIncrement()); - await alice.signer.sign(transactionDeploy); - await transactionDeploy.send(provider); - await transactionDeploy.awaitExecuted(provider); - } }); diff --git a/src/smartcontracts/interaction.spec.ts b/src/smartcontracts/interaction.spec.ts index dfb2620a..75e03be8 100644 --- a/src/smartcontracts/interaction.spec.ts +++ b/src/smartcontracts/interaction.spec.ts @@ -1,12 +1,9 @@ -import { StrictChecker } from "./strictChecker"; -import { DefaultInteractionRunner } from "./defaultRunner"; +import { DefaultSmartContractController } from "./smartContractController"; import { SmartContract } from "./smartContract"; -import { BigUIntValue, OptionValue, U32Value } from "./typesystem"; +import { BigUIntValue, OptionalValue, OptionValue, TokenIdentifierValue, U32Value } from "./typesystem"; import { - AddImmediateResult, loadAbiRegistry, loadTestWallets, - MarkNotarized, MockProvider, setupUnitTestWatcherTimeouts, TestWallet, @@ -19,7 +16,6 @@ import { GasLimit } from "../networkParams"; import { ContractFunction } from "./function"; import { QueryResponse } from "./queryResponse"; import { Nonce } from "../nonce"; -import { TransactionStatus } from "../transaction"; import { ReturnCode } from "./returnCode"; import { Balance } from "../balance"; import BigNumber from "bignumber.js"; @@ -29,19 +25,17 @@ import { createBalanceBuilder } from "../balanceBuilder"; describe("test smart contract interactor", function() { let dummyAddress = new Address("erd1qqqqqqqqqqqqqpgqak8zt22wl2ph4tswtyc39namqx6ysa2sd8ss4xmlj3"); - let checker = new StrictChecker(); let provider = new MockProvider(); let alice: TestWallet; - let runner: DefaultInteractionRunner; + before(async function() { ({ alice } = await loadTestWallets()); - runner = new DefaultInteractionRunner(checker, alice.signer, provider); }); it("should set transaction fields", async function () { let contract = new SmartContract({ address: dummyAddress }); let dummyFunction = new ContractFunction("dummy"); - let interaction = new Interaction(contract, dummyFunction, dummyFunction, []); + let interaction = new Interaction(contract, dummyFunction, []); let transaction = interaction .withNonce(new Nonce(7)) @@ -77,35 +71,35 @@ describe("test smart contract interactor", function() { const hexDummyFunction = "64756d6d79"; // ESDT, single - let transaction = new Interaction(contract, dummyFunction, dummyFunction, []) + let transaction = new Interaction(contract, dummyFunction, []) .withSingleESDTTransfer(TokenFoo("10")) .buildTransaction(); assert.equal(transaction.getData().toString(), `ESDTTransfer@${hexFoo}@0a@${hexDummyFunction}`); // Meta ESDT (special SFT), single - transaction = new Interaction(contract, dummyFunction, dummyFunction, []) + transaction = new Interaction(contract, dummyFunction, []) .withSingleESDTNFTTransfer(LKMEX.nonce(123456).value(123.456), alice) .buildTransaction(); assert.equal(transaction.getData().toString(), `ESDTNFTTransfer@${hexLKMEX}@01e240@06b14bd1e6eea00000@${hexContractAddress}@${hexDummyFunction}`); // NFT, single - transaction = new Interaction(contract, dummyFunction, dummyFunction, []) + transaction = new Interaction(contract, dummyFunction, []) .withSingleESDTNFTTransfer(Strămoși.nonce(1).one(), alice) .buildTransaction(); assert.equal(transaction.getData().toString(), `ESDTNFTTransfer@${hexStrămoși}@01@01@${hexContractAddress}@${hexDummyFunction}`); // ESDT, multiple - transaction = new Interaction(contract, dummyFunction, dummyFunction, []) + transaction = new Interaction(contract, dummyFunction, []) .withMultiESDTNFTTransfer([TokenFoo(3), TokenBar(3.14)], alice) .buildTransaction(); assert.equal(transaction.getData().toString(), `MultiESDTNFTTransfer@${hexContractAddress}@02@${hexFoo}@@03@${hexBar}@@0c44@${hexDummyFunction}`); // NFT, multiple - transaction = new Interaction(contract, dummyFunction, dummyFunction, []) + transaction = new Interaction(contract, dummyFunction, []) .withMultiESDTNFTTransfer([Strămoși.nonce(1).one(), Strămoși.nonce(42).one()], alice) .buildTransaction(); @@ -118,29 +112,34 @@ describe("test smart contract interactor", function() { let abiRegistry = await loadAbiRegistry(["src/testdata/answer.abi.json"]); let abi = new SmartContractAbi(abiRegistry, ["answer"]); let contract = new SmartContract({ address: dummyAddress, abi: abi }); + let controller = new DefaultSmartContractController(abi, provider); + + let interaction = contract.methods + .getUltimateAnswer() + .withGasLimit(new GasLimit(543210)); - let interaction = contract.methods.getUltimateAnswer().withGasLimit(new GasLimit(543210)); - assert.equal(interaction.getContract().getAddress(), dummyAddress); - assert.deepEqual(interaction.getInterpretingFunction(), new ContractFunction("getUltimateAnswer")); - assert.deepEqual(interaction.getExecutingFunction(), new ContractFunction("getUltimateAnswer")); + assert.equal(contract.getAddress(), dummyAddress); + assert.deepEqual(interaction.getFunction(), new ContractFunction("getUltimateAnswer")); assert.lengthOf(interaction.getArguments(), 0); assert.deepEqual(interaction.getGasLimit(), new GasLimit(543210)); - provider.mockQueryResponseOnFunction( + provider.mockQueryContractOnFunction( "getUltimateAnswer", new QueryResponse({ returnData: ["Kg=="], returnCode: ReturnCode.Ok }) ); // Query - let { values: queryValues, firstValue: queryAnwser, returnCode: queryCode } = await runner.runQuery( + let { values: queryValues, firstValue: queryAnwser, returnCode: queryCode } = await controller.query( interaction ); assert.lengthOf(queryValues, 1); - assert.deepEqual(queryAnwser.valueOf(), new BigNumber(42)); + assert.deepEqual(queryAnwser!.valueOf(), new BigNumber(42)); assert.isTrue(queryCode.equals(ReturnCode.Ok)); // Execute, do not wait for execution - let transaction = await runner.run(interaction.withNonce(new Nonce(0))); + let transaction = interaction.withNonce(new Nonce(0)).buildTransaction(); + await alice.signer.sign(transaction); + transaction.send(provider); assert.equal(transaction.getNonce().valueOf(), 0); assert.equal(transaction.getData().toString(), "getUltimateAnswer"); assert.equal( @@ -148,7 +147,9 @@ describe("test smart contract interactor", function() { "60d0956a8902c1179dce92d91bd9670e31b9a9cd07c1d620edb7754a315b4818" ); - transaction = await runner.run(interaction.withNonce(new Nonce(1))); + transaction = interaction.withNonce(new Nonce(1)).buildTransaction(); + await alice.signer.sign(transaction); + await transaction.send(provider); assert.equal(transaction.getNonce().valueOf(), 1); assert.equal( transaction.getHash().toString(), @@ -156,21 +157,14 @@ describe("test smart contract interactor", function() { ); // Execute, and wait for execution - let [ - , - { values: executionValues, firstValue: executionAnswer, returnCode: executionCode }, - ] = await Promise.all([ - provider.mockNextTransactionTimeline([ - new TransactionStatus("executed"), - new AddImmediateResult("@6f6b@2b"), - new MarkNotarized(), - ]), - runner.runAwaitExecution(interaction.withNonce(new Nonce(2))), - ]); - - assert.lengthOf(executionValues, 1); - assert.deepEqual(executionAnswer.valueOf(), new BigNumber(43)); - assert.isTrue(executionCode.equals(ReturnCode.Ok)); + transaction = interaction.withNonce(new Nonce(2)).buildTransaction(); + await alice.signer.sign(transaction); + provider.mockGetTransactionWithAnyHashAsNotarizedWithOneResult("@6f6b@2bs"); + let { bundle } = await controller.execute(interaction, transaction); + + assert.lengthOf(bundle.values, 1); + assert.deepEqual(bundle.firstValue!.valueOf(), new BigNumber(43)); + assert.isTrue(bundle.returnCode.equals(ReturnCode.Ok)); }); it("should interact with 'counter'", async function() { @@ -179,157 +173,118 @@ describe("test smart contract interactor", function() { let abiRegistry = await loadAbiRegistry(["src/testdata/counter.abi.json"]); let abi = new SmartContractAbi(abiRegistry, ["counter"]); let contract = new SmartContract({ address: dummyAddress, abi: abi }); + let controller = new DefaultSmartContractController(abi, provider); let getInteraction = contract.methods.get(); let incrementInteraction = (contract.methods.increment()).withGasLimit(new GasLimit(543210)); let decrementInteraction = (contract.methods.decrement()).withGasLimit(new GasLimit(987654)); // For "get()", return fake 7 - provider.mockQueryResponseOnFunction( + provider.mockQueryContractOnFunction( "get", new QueryResponse({ returnData: ["Bw=="], returnCode: ReturnCode.Ok }) ); // Query "get()" - let { firstValue: counterValue } = await runner.runQuery(getInteraction); - - assert.deepEqual(counterValue.valueOf(), new BigNumber(7)); + let { firstValue: counterValue } = await controller.query(getInteraction); - // Increment, wait for execution. Return fake 8 - let [, { firstValue: valueAfterIncrement }] = await Promise.all([ - provider.mockNextTransactionTimeline([ - new TransactionStatus("executed"), - new AddImmediateResult("@6f6b@08"), - new MarkNotarized(), - ]), - runner.runAwaitExecution(incrementInteraction.withNonce(new Nonce(14))), - ]); + assert.deepEqual(counterValue!.valueOf(), new BigNumber(7)); - assert.deepEqual(valueAfterIncrement.valueOf(), new BigNumber(8)); + let incrementTransaction = incrementInteraction.withNonce(new Nonce(14)).buildTransaction(); + await alice.signer.sign(incrementTransaction); + provider.mockGetTransactionWithAnyHashAsNotarizedWithOneResult("@6f6b@08"); + let { bundle: { firstValue: valueAfterIncrement } } = await controller.execute(incrementInteraction, incrementTransaction); + assert.deepEqual(valueAfterIncrement!.valueOf(), new BigNumber(8)); // Decrement three times (simulate three parallel broadcasts). Wait for execution of the latter (third transaction). Return fake "5". - await runner.run(decrementInteraction.withNonce(new Nonce(15))); - await runner.run(decrementInteraction.withNonce(new Nonce(16))); - - let [, { firstValue: valueAfterDecrement }] = await Promise.all([ - provider.mockNextTransactionTimeline([ - new TransactionStatus("executed"), - new AddImmediateResult("@6f6b@05"), - new MarkNotarized(), - ]), - runner.runAwaitExecution(decrementInteraction.withNonce(new Nonce(17))), - ]); - - assert.deepEqual(valueAfterDecrement.valueOf(), new BigNumber(5)); + // Decrement #1 + let decrementTransaction = decrementInteraction.withNonce(new Nonce(15)).buildTransaction(); + await alice.signer.sign(decrementTransaction); + decrementTransaction.send(provider); + // Decrement #2 + decrementTransaction = decrementInteraction.withNonce(new Nonce(16)).buildTransaction(); + await alice.signer.sign(decrementTransaction); + decrementTransaction.send(provider); + // Decrement #3 + + decrementTransaction = decrementInteraction.withNonce(new Nonce(17)).buildTransaction(); + await alice.signer.sign(decrementTransaction); + provider.mockGetTransactionWithAnyHashAsNotarizedWithOneResult("@6f6b@05"); + let { bundle: { firstValue: valueAfterDecrement } } = await controller.execute(decrementInteraction, decrementTransaction); + assert.deepEqual(valueAfterDecrement!.valueOf(), new BigNumber(5)); }); - it("should interact with 'lottery_egld'", async function() { + it("should interact with 'lottery-esdt'", async function() { setupUnitTestWatcherTimeouts(); - let abiRegistry = await loadAbiRegistry(["src/testdata/lottery_egld.abi.json"]); + let abiRegistry = await loadAbiRegistry(["src/testdata/lottery-esdt.abi.json"]); let abi = new SmartContractAbi(abiRegistry, ["Lottery"]); let contract = new SmartContract({ address: dummyAddress, abi: abi }); + let controller = new DefaultSmartContractController(abi, provider); let startInteraction = ( contract.methods .start([ BytesValue.fromUTF8("lucky"), - new BigUIntValue(Balance.egld(1).valueOf()), + new TokenIdentifierValue("lucky-token"), + new BigUIntValue(1), OptionValue.newMissing(), OptionValue.newMissing(), OptionValue.newProvided(new U32Value(1)), OptionValue.newMissing(), OptionValue.newMissing(), + OptionalValue.newMissing() ]) .withGasLimit(new GasLimit(5000000)) ); - let lotteryStatusInteraction = ( + let statusInteraction = ( contract.methods.status([BytesValue.fromUTF8("lucky")]).withGasLimit(new GasLimit(5000000)) ); let getLotteryInfoInteraction = ( - contract.methods.lotteryInfo([BytesValue.fromUTF8("lucky")]).withGasLimit(new GasLimit(5000000)) + contract.methods.getLotteryInfo([BytesValue.fromUTF8("lucky")]).withGasLimit(new GasLimit(5000000)) ); // start() - let [, { returnCode: startReturnCode, values: startReturnvalues }] = await Promise.all([ - provider.mockNextTransactionTimeline([ - new TransactionStatus("executed"), - new AddImmediateResult("@6f6b"), - new MarkNotarized(), - ]), - runner.runAwaitExecution(startInteraction.withNonce(new Nonce(14))), - ]); + let startTransaction = startInteraction.withNonce(new Nonce(14)).buildTransaction(); + await alice.signer.sign(startTransaction); + provider.mockGetTransactionWithAnyHashAsNotarizedWithOneResult("@6f6b"); + let { bundle: { returnCode: startReturnCode, values: startReturnValues } } = await controller.execute(startInteraction, startTransaction); - assert.equal( - startInteraction - .buildTransaction() - .getData() - .toString(), - "start@6c75636b79@0de0b6b3a7640000@@@0100000001@@" - ); + assert.equal(startTransaction.getData().toString(), "start@6c75636b79@6c75636b792d746f6b656e@01@@@0100000001@@"); assert.isTrue(startReturnCode.equals(ReturnCode.Ok)); - assert.lengthOf(startReturnvalues, 0); - - // lotteryExists() (this is a view function, but for the sake of the test, we'll execute it) - let [ - , - { returnCode: statusReturnCode, values: statusReturnvalues, firstValue: statusFirstValue }, - ] = await Promise.all([ - provider.mockNextTransactionTimeline([ - new TransactionStatus("executed"), - new AddImmediateResult("@6f6b@01"), - new MarkNotarized(), - ]), - runner.runAwaitExecution(lotteryStatusInteraction.withNonce(new Nonce(15))), - ]); + assert.lengthOf(startReturnValues, 0); - assert.equal( - lotteryStatusInteraction - .buildTransaction() - .getData() - .toString(), - "status@6c75636b79" - ); + // status() (this is a view function, but for the sake of the test, we'll execute it) + let statusTransaction = statusInteraction.withNonce(new Nonce(15)).buildTransaction(); + await alice.signer.sign(statusTransaction); + provider.mockGetTransactionWithAnyHashAsNotarizedWithOneResult("@6f6b@01"); + let { bundle: { returnCode: statusReturnCode, values: statusReturnValues, firstValue: statusFirstValue } } = await controller.execute(statusInteraction, statusTransaction); + + assert.equal(statusTransaction.getData().toString(),"status@6c75636b79"); assert.isTrue(statusReturnCode.equals(ReturnCode.Ok)); - assert.lengthOf(statusReturnvalues, 1); - assert.deepEqual(statusFirstValue.valueOf(), { name: "Running", fields: [] }); + assert.lengthOf(statusReturnValues, 1); + assert.deepEqual(statusFirstValue!.valueOf(), { name: "Running", fields: [] }); // lotteryInfo() (this is a view function, but for the sake of the test, we'll execute it) - let [ - , - { returnCode: infoReturnCode, values: infoReturnvalues, firstValue: infoFirstValue }, - ] = await Promise.all([ - provider.mockNextTransactionTimeline([ - new TransactionStatus("executed"), - new AddImmediateResult( - "@6f6b@000000080de0b6b3a764000000000320000000006012a806000000010000000164000000000000000000000000" - ), - new MarkNotarized(), - ]), - runner.runAwaitExecution(getLotteryInfoInteraction.withNonce(new Nonce(16))), - ]); + let getLotteryInfoTransaction = getLotteryInfoInteraction.withNonce(new Nonce(15)).buildTransaction(); + await alice.signer.sign(getLotteryInfoTransaction); + provider.mockGetTransactionWithAnyHashAsNotarizedWithOneResult("@6f6b@0000000b6c75636b792d746f6b656e000000010100000000000000005fc2b9dbffffffff00000001640000000a140ec80fa7ee88000000"); + let { bundle: { returnCode: infoReturnCode, values: infoReturnValues, firstValue: infoFirstValue} } = await controller.execute(getLotteryInfoInteraction, getLotteryInfoTransaction); - assert.equal( - getLotteryInfoInteraction - .buildTransaction() - .getData() - .toString(), - "lotteryInfo@6c75636b79" - ); + assert.equal(getLotteryInfoTransaction.getData().toString(), "getLotteryInfo@6c75636b79"); assert.isTrue(infoReturnCode.equals(ReturnCode.Ok)); - assert.lengthOf(infoReturnvalues, 1); - - assert.deepEqual(infoFirstValue.valueOf(), { - ticket_price: new BigNumber("1000000000000000000"), - tickets_left: new BigNumber(800), - deadline: new BigNumber("1611835398"), - max_entries_per_user: new BigNumber(1), + assert.lengthOf(infoReturnValues, 1); + + assert.deepEqual(infoFirstValue!.valueOf(), { + token_identifier: "lucky-token", + ticket_price: new BigNumber("1"), + tickets_left: new BigNumber(0), + deadline: new BigNumber("0x000000005fc2b9db", 16), + max_entries_per_user: new BigNumber(0xffffffff), prize_distribution: Buffer.from([0x64]), - whitelist: [], - current_ticket_number: new BigNumber(0), - prize_pool: new BigNumber("0"), + prize_pool: new BigNumber("94720000000000000000000") }); }); }); diff --git a/src/smartcontracts/interaction.ts b/src/smartcontracts/interaction.ts index be9d99fd..2102afe1 100644 --- a/src/smartcontracts/interaction.ts +++ b/src/smartcontracts/interaction.ts @@ -1,17 +1,23 @@ import { Balance } from "../balance"; import { GasLimit } from "../networkParams"; import { Transaction } from "../transaction"; -import { TransactionOnNetwork } from "../transactionOnNetwork"; import { Query } from "./query"; -import { QueryResponse } from "./queryResponse"; import { ContractFunction } from "./function"; import { Address } from "../address"; -import { SmartContract } from "./smartContract"; -import { AddressValue, BigUIntValue, BytesValue, EndpointDefinition, TypedValue, U64Value, U8Value } from "./typesystem"; +import { AddressValue, BigUIntValue, BytesValue, TypedValue, U64Value, U8Value } from "./typesystem"; import { Nonce } from "../nonce"; -import { ExecutionResultsBundle, QueryResponseBundle } from "./interface"; import { NetworkConfig } from "../networkConfig"; import { ESDTNFT_TRANSFER_FUNCTION_NAME, ESDT_TRANSFER_FUNCTION_NAME, MULTI_ESDTNFT_TRANSFER_FUNCTION_NAME } from "../constants"; +import { Account } from "../account"; +import { CallArguments } from "./interface"; + +/** + * Internal interface: the smart contract, as seen from the perspective of an {@link Interaction}. + */ +interface ISmartContractWithinInteraction { + call({ func, args, value, gasLimit, receiver }: CallArguments): Transaction; + getAddress(): Address; +} /** * Interactions can be seen as mutable transaction & query builders. @@ -20,15 +26,15 @@ import { ESDTNFT_TRANSFER_FUNCTION_NAME, ESDT_TRANSFER_FUNCTION_NAME, MULTI_ESDT * the execution outcome for the objects they've built. */ export class Interaction { - private readonly contract: SmartContract; - private readonly executingFunction: ContractFunction; - private readonly interpretingFunction: ContractFunction; + private readonly contract: ISmartContractWithinInteraction; + private readonly function: ContractFunction; private readonly args: TypedValue[]; private readonly receiver?: Address; private nonce: Nonce = new Nonce(0); private value: Balance = Balance.Zero(); private gasLimit: GasLimit = GasLimit.min(); + private querent: Address = new Address(); private isWithSingleESDTTransfer: boolean = false; private isWithSingleESDTNFTTransfer: boolean = false; @@ -37,30 +43,24 @@ export class Interaction { private tokenTransfersSender: Address = new Address(); constructor( - contract: SmartContract, - executingFunction: ContractFunction, - interpretingFunction: ContractFunction, + contract: ISmartContractWithinInteraction, + func: ContractFunction, args: TypedValue[], receiver?: Address, ) { this.contract = contract; - this.executingFunction = executingFunction; - this.interpretingFunction = interpretingFunction; + this.function = func; this.args = args; this.receiver = receiver; this.tokenTransfers = new TokenTransfersWithinInteraction([], this); } - - getContract(): SmartContract { + + getContract(): ISmartContractWithinInteraction { return this.contract; } - getInterpretingFunction(): ContractFunction { - return this.interpretingFunction; - } - - getExecutingFunction(): ContractFunction { - return this.executingFunction; + getFunction(): ContractFunction { + return this.function; } getArguments(): TypedValue[] { @@ -81,7 +81,7 @@ export class Interaction { buildTransaction(): Transaction { let receiver = this.receiver; - let func: ContractFunction = this.executingFunction; + let func: ContractFunction = this.function; let args = this.args; if (this.isWithSingleESDTTransfer) { @@ -117,42 +117,14 @@ export class Interaction { buildQuery(): Query { return new Query({ address: this.contract.getAddress(), - func: this.executingFunction, + func: this.function, args: this.args, // Value will be set using "withValue()". value: this.value, - // Caller will be set by the InteractionRunner. - caller: new Address() + caller: this.querent }); } - /** - * Interprets the results of a previously broadcasted (and fully executed) smart contract transaction. - * The outcome is structured such that it allows quick access to each level of detail. - */ - interpretExecutionResults(transactionOnNetwork: TransactionOnNetwork): ExecutionResultsBundle { - return interpretExecutionResults(this.getEndpoint(), transactionOnNetwork); - } - - /** - * Interprets the raw outcome of a Smart Contract query. - * The outcome is structured such that it allows quick access to each level of detail. - */ - interpretQueryResponse(queryResponse: QueryResponse): QueryResponseBundle { - let endpoint = this.getEndpoint(); - queryResponse.setEndpointDefinition(endpoint); - - let values = queryResponse.outputTyped(); - let returnCode = queryResponse.returnCode; - - return { - queryResponse: queryResponse, - values: values, - firstValue: values[0], - returnCode: returnCode - }; - } - withValue(value: Balance): Interaction { this.value = value; return this; @@ -201,30 +173,17 @@ export class Interaction { return this; } - getEndpoint(): EndpointDefinition { - return this.getContract().getAbi().getEndpoint(this.getInterpretingFunction()); + useThenIncrementNonceOf(account: Account) : Interaction { + return this.withNonce(account.getNonceThenIncrement()); } -} -function interpretExecutionResults(endpoint: EndpointDefinition, transactionOnNetwork: TransactionOnNetwork): ExecutionResultsBundle { - let smartContractResults = transactionOnNetwork.getSmartContractResults(); - let immediateResult = smartContractResults.getImmediate(); - let resultingCalls = smartContractResults.getResultingCalls(); - - immediateResult.setEndpointDefinition(endpoint); - - let values = immediateResult.outputTyped(); - let returnCode = immediateResult.getReturnCode(); - - return { - transactionOnNetwork: transactionOnNetwork, - smartContractResults: smartContractResults, - immediateResult, - resultingCalls, - values, - firstValue: values[0], - returnCode: returnCode - }; + /** + * Sets the "caller" field on contract queries. + */ + withQuerent(querent: Address): Interaction { + this.querent = querent; + return this; + } } class TokenTransfersWithinInteraction { @@ -308,7 +267,7 @@ class TokenTransfersWithinInteraction { } private getTypedInteractionFunction(): TypedValue { - return BytesValue.fromUTF8(this.interaction.getExecutingFunction().valueOf()) + return BytesValue.fromUTF8(this.interaction.getFunction().valueOf()) } private getInteractionArguments(): TypedValue[] { diff --git a/src/smartcontracts/strictChecker.spec.ts b/src/smartcontracts/interactionChecker.spec.ts similarity index 71% rename from src/smartcontracts/strictChecker.spec.ts rename to src/smartcontracts/interactionChecker.spec.ts index 2395cdf2..46c44826 100644 --- a/src/smartcontracts/strictChecker.spec.ts +++ b/src/smartcontracts/interactionChecker.spec.ts @@ -1,7 +1,7 @@ import * as errors from "../errors"; -import { StrictChecker as StrictInteractionChecker } from "./strictChecker"; +import { InteractionChecker } from "./interactionChecker"; import { SmartContract } from "./smartContract"; -import { BigUIntValue, OptionValue, U64Value } from "./typesystem"; +import { BigUIntType, BigUIntValue, OptionalType, OptionalValue, OptionValue, TokenIdentifierValue, U32Value, U64Value } from "./typesystem"; import { loadAbiRegistry } from "../testutils"; import { SmartContractAbi } from "./abi"; import { Address } from "../address"; @@ -13,30 +13,32 @@ import { BytesValue } from "./typesystem/bytes"; describe("integration tests: test checker within interactor", function () { let dummyAddress = new Address("erd1qqqqqqqqqqqqqpgqak8zt22wl2ph4tswtyc39namqx6ysa2sd8ss4xmlj3"); - let checker = new StrictInteractionChecker(); + let checker = new InteractionChecker(); it("should detect errors for 'ultimate answer'", async function () { let abiRegistry = await loadAbiRegistry(["src/testdata/answer.abi.json"]); let abi = new SmartContractAbi(abiRegistry, ["answer"]); let contract = new SmartContract({ address: dummyAddress, abi: abi }); + let endpoint = abi.getEndpoint("getUltimateAnswer"); // Send value to non-payable assert.throw(() => { let interaction = (contract.methods.getUltimateAnswer()).withValue(Balance.egld(1)); - checker.checkInteraction(interaction); + checker.checkInteraction(interaction, endpoint); }, errors.ErrContractInteraction, "cannot send EGLD value to non-payable"); // Bad arguments assert.throw(() => { let interaction = (contract.methods.getUltimateAnswer([BytesValue.fromHex("abba")])); - checker.checkInteraction(interaction); + checker.checkInteraction(interaction, endpoint); }, errors.ErrContractInteraction, "bad arguments, expected: 0, got: 1"); }); it("should detect errors for 'lottery'", async function () { - let abiRegistry = await loadAbiRegistry(["src/testdata/lottery_egld.abi.json"]); + let abiRegistry = await loadAbiRegistry(["src/testdata/lottery-esdt.abi.json"]); let abi = new SmartContractAbi(abiRegistry, ["Lottery"]); let contract = new SmartContract({ address: dummyAddress, abi: abi }); + let endpoint = abi.getEndpoint("start"); // Bad number of arguments assert.throw(() => { @@ -45,21 +47,23 @@ describe("integration tests: test checker within interactor", function () { new BigUIntValue(Balance.egld(1).valueOf()), OptionValue.newMissing() ]); - checker.checkInteraction(interaction); - }, errors.ErrContractInteraction, "bad arguments, expected: 7, got: 3"); + checker.checkInteraction(interaction, endpoint); + }, errors.ErrContractInteraction, "bad arguments, expected: 9, got: 3"); // Bad types (U64 instead of U32) assert.throw(() => { let interaction = contract.methods.start([ BytesValue.fromUTF8("lucky"), - new BigUIntValue(Balance.egld(1).valueOf()), - OptionValue.newMissing(), + new TokenIdentifierValue("lucky-token"), + new BigUIntValue(1), OptionValue.newMissing(), - OptionValue.newProvided(new U64Value(new BigNumber(1))), + OptionValue.newProvided(new U32Value(1)), + OptionValue.newProvided(new U32Value(1)), OptionValue.newMissing(), OptionValue.newMissing(), + new OptionalValue(new OptionalType(new BigUIntType())) ]); - checker.checkInteraction(interaction); - }, errors.ErrContractInteraction, "type mismatch at index 4, expected: Option, got: Option"); + checker.checkInteraction(interaction, endpoint); + }, errors.ErrContractInteraction, "type mismatch at index 4, expected: Option, got: Option"); }); }); diff --git a/src/smartcontracts/strictChecker.ts b/src/smartcontracts/interactionChecker.ts similarity index 87% rename from src/smartcontracts/strictChecker.ts rename to src/smartcontracts/interactionChecker.ts index 928bd8db..7ca78b91 100644 --- a/src/smartcontracts/strictChecker.ts +++ b/src/smartcontracts/interactionChecker.ts @@ -10,10 +10,8 @@ import { IInteractionChecker } from "./interface"; * - errors related to calling "non-payable" functions with some value provided * - gas estimation errors (not yet implemented) */ -export class StrictChecker implements IInteractionChecker { - checkInteraction(interaction: Interaction): void { - let definition = interaction.getEndpoint(); - +export class InteractionChecker implements IInteractionChecker { + checkInteraction(interaction: Interaction, definition: EndpointDefinition): void { this.checkPayable(interaction, definition); this.checkArguments(interaction, definition); } @@ -52,3 +50,7 @@ export class StrictChecker implements IInteractionChecker { } } } + +export class NullInteractionChecker implements IInteractionChecker { + checkInteraction(_interaction: Interaction, _definition: EndpointDefinition): void { } +} diff --git a/src/smartcontracts/interface.ts b/src/smartcontracts/interface.ts index a0c9ec5c..7df4026d 100644 --- a/src/smartcontracts/interface.ts +++ b/src/smartcontracts/interface.ts @@ -9,8 +9,7 @@ import { ContractFunction } from "./function"; import { Interaction } from "./interaction"; import { QueryResponse } from "./queryResponse"; import { ReturnCode } from "./returnCode"; -import { SmartContractResults, TypedResult } from "./smartContractResults"; -import { TypedValue } from "./typesystem"; +import { EndpointDefinition, TypedValue } from "./typesystem"; /** * ISmartContract defines a general interface for operating with {@link SmartContract} objects. @@ -37,18 +36,6 @@ export interface ISmartContract { call({ func, args, value, gasLimit }: CallArguments): Transaction; } -export interface IInteractionRunner { - run(interaction: Interaction): Promise; - runAwaitExecution(interaction: Interaction): Promise; - runQuery(interaction: Interaction, caller?: Address): Promise; - // TODO: Fix method signature (underspecified at this moment). - runSimulation(interaction: Interaction): Promise; -} - -export interface IInteractionChecker { - checkInteraction(interaction: Interaction): void; -} - export interface DeployArguments { code: Code; codeMetadata?: CodeMetadata; @@ -80,19 +67,35 @@ export interface QueryArguments { caller?: Address } -export interface ExecutionResultsBundle { - transactionOnNetwork: TransactionOnNetwork; - smartContractResults: SmartContractResults; - immediateResult: TypedResult; - resultingCalls: TypedResult[]; - values: TypedValue[]; - firstValue: TypedValue; +export interface TypedOutcomeBundle { returnCode: ReturnCode; + returnMessage: string; + values: TypedValue[]; + firstValue?: TypedValue; + secondValue?: TypedValue; + thirdValue?: TypedValue; } -export interface QueryResponseBundle { - queryResponse: QueryResponse; - firstValue: TypedValue; - values: TypedValue[]; +export interface UntypedOutcomeBundle { returnCode: ReturnCode; + returnMessage: string; + values: Buffer[]; +} + +export interface ISmartContractController { + deploy(transaction: Transaction): Promise<{ transactionOnNetwork: TransactionOnNetwork, bundle: UntypedOutcomeBundle }>; + execute(interaction: Interaction, transaction: Transaction): Promise<{ transactionOnNetwork: TransactionOnNetwork, bundle: TypedOutcomeBundle }>; + query(interaction: Interaction): Promise; +} + +export interface IInteractionChecker { + checkInteraction(interaction: Interaction, definition: EndpointDefinition): void; +} + +export interface IResultsParser { + parseQueryResponse(queryResponse: QueryResponse, endpoint: EndpointDefinition): TypedOutcomeBundle; + parseUntypedQueryResponse(queryResponse: QueryResponse): UntypedOutcomeBundle; + + parseOutcome(transaction: TransactionOnNetwork, endpoint: EndpointDefinition): TypedOutcomeBundle; + parseUntypedOutcome(transaction: TransactionOnNetwork): UntypedOutcomeBundle; } diff --git a/src/smartcontracts/nativeSerializer.spec.ts b/src/smartcontracts/nativeSerializer.spec.ts new file mode 100644 index 00000000..ee26f422 --- /dev/null +++ b/src/smartcontracts/nativeSerializer.spec.ts @@ -0,0 +1,41 @@ +import { assert } from "chai"; +import { Address } from "../address"; +import { AddressType, BigUIntType, EndpointDefinition, EndpointModifiers, EndpointParameterDefinition, ListType, NullType, OptionalType, OptionType, U32Type } from "./typesystem"; +import { BytesType } from "./typesystem/bytes"; +import { NativeSerializer } from "./nativeSerializer"; + +describe("test native serializer", () => { + it("should perform type inference", async () => { + let endpointModifiers = new EndpointModifiers("", []); + let inputParameters = [ + new EndpointParameterDefinition("a", "a", new BigUIntType()), + new EndpointParameterDefinition("b", "b", new ListType(new AddressType())), + new EndpointParameterDefinition("c", "c", new BytesType()), + new EndpointParameterDefinition("d", "d", new OptionType(new U32Type())), + new EndpointParameterDefinition("e", "e", new OptionType(new U32Type())), + new EndpointParameterDefinition("f", "f", new OptionalType(new BytesType())) + ]; + let endpoint = new EndpointDefinition("foo", inputParameters, [], endpointModifiers); + + let a = 42; + let b = [new Address("erd1dc3yzxxeq69wvf583gw0h67td226gu2ahpk3k50qdgzzym8npltq7ndgha"), new Address("erd1r69gk66fmedhhcg24g2c5kn2f2a5k4kvpr6jfw67dn2lyydd8cfswy6ede")]; + let c = Buffer.from("abba", "hex"); + let d = null; + let e = 7; + // Let's not provide "f" + let typedValues = NativeSerializer.nativeToTypedValues([a, b, c, d, e], endpoint); + + assert.deepEqual(typedValues[0].getType(), new BigUIntType()); + assert.deepEqual(typedValues[0].valueOf().toNumber(), a); + assert.deepEqual(typedValues[1].getType(), new ListType(new AddressType())); + assert.deepEqual(typedValues[1].valueOf(), b); + assert.deepEqual(typedValues[2].getType(), new BytesType()); + assert.deepEqual(typedValues[2].valueOf(), c); + assert.deepEqual(typedValues[3].getType(), new OptionType(new NullType())); + assert.deepEqual(typedValues[3].valueOf(), null); + assert.deepEqual(typedValues[4].getType(), new OptionType(new U32Type())); + assert.deepEqual(typedValues[4].valueOf().toNumber(), e); + assert.deepEqual(typedValues[5].getType(), new OptionalType(new BytesType())); + assert.deepEqual(typedValues[5].valueOf(), null); + }); +}); diff --git a/src/smartcontracts/nativeSerializer.ts b/src/smartcontracts/nativeSerializer.ts index 75db8c20..bc131614 100644 --- a/src/smartcontracts/nativeSerializer.ts +++ b/src/smartcontracts/nativeSerializer.ts @@ -1,10 +1,7 @@ import BigNumber from "bignumber.js"; import { AddressType, AddressValue, BigIntType, BigIntValue, BigUIntType, BigUIntValue, BooleanType, BooleanValue, BytesType, BytesValue, CompositeType, CompositeValue, EndpointDefinition, EndpointParameterDefinition, I16Type, I16Value, I32Type, I32Value, I64Type, I64Value, I8Type, I8Value, List, ListType, NumericalType, OptionalType, OptionalValue, OptionType, OptionValue, PrimitiveType, TokenIdentifierType, TokenIdentifierValue, TupleType, Type, TypedValue, U16Type, U16Value, U32Type, U32Value, U64Type, U64Value, U8Type, U8Value, VariadicType, VariadicValue } from "./typesystem"; -import { TestWallet } from "../testutils"; import { ArgumentErrorContext } from "./argumentErrorContext"; -import { SmartContract } from "./smartContract"; import { Struct, Field, StructType, Tuple } from "./typesystem"; -import { ContractWrapper } from "./wrapper/contractWrapper"; import { BalanceBuilder } from "../balanceBuilder"; import { Address } from "../address"; import { Code } from "./code"; @@ -13,7 +10,7 @@ import { ErrInvalidArgument } from "../errors"; export namespace NativeTypes { export type NativeBuffer = Buffer | string | BalanceBuilder; export type NativeBytes = Code | Buffer | string | BalanceBuilder; - export type NativeAddress = Address | string | Buffer | ContractWrapper | SmartContract | TestWallet; + export type NativeAddress = Address | string | Buffer | { getAddress(): Address }; } export namespace NativeSerializer { @@ -193,7 +190,7 @@ export namespace NativeSerializer { return new BooleanValue(native); } if (type instanceof TokenIdentifierType) { - return new TokenIdentifierValue(convertNativeToBuffer(native, errorContext)); + return new TokenIdentifierValue(convertNativeToString(native, errorContext)); } errorContext.throwError(`(function: toPrimitive) unsupported type ${type}`); } @@ -217,34 +214,32 @@ export namespace NativeSerializer { errorContext.convertError(native, "BytesValue"); } - function convertNativeToBuffer(native: NativeTypes.NativeBuffer, errorContext: ArgumentErrorContext): Buffer { + function convertNativeToString(native: NativeTypes.NativeBuffer, errorContext: ArgumentErrorContext): string { if (native === undefined) { errorContext.convertError(native, "Buffer"); } if (native instanceof Buffer) { - return native; + return native.toString(); } if (typeof native === "string") { - return Buffer.from(native); + return native; } if (((native).getTokenIdentifier)) { - return Buffer.from(native.getTokenIdentifier()); + return native.getTokenIdentifier(); } errorContext.convertError(native, "Buffer"); } export function convertNativeToAddress(native: NativeTypes.NativeAddress, errorContext: ArgumentErrorContext): Address { + if ((native).getAddress) { + return (native).getAddress(); + } + switch (native.constructor) { case Address: case Buffer: case String: return new Address(
native); - case ContractWrapper: - return (native).getAddress(); - case SmartContract: - return (native).getAddress(); - case TestWallet: - return (native).address; default: errorContext.convertError(native, "Address"); } diff --git a/src/smartcontracts/query.main.net.spec.ts b/src/smartcontracts/query.main.net.spec.ts index b5d92e55..0398df51 100644 --- a/src/smartcontracts/query.main.net.spec.ts +++ b/src/smartcontracts/query.main.net.spec.ts @@ -26,7 +26,7 @@ describe("test queries on mainnet", function () { assert.isTrue(response.isSuccess()); assert.lengthOf(response.returnData, 1); - assert.isAtLeast(response.gasUsed.valueOf(), 20000000); + assert.isAtLeast(response.gasUsed.valueOf(), 1000000); assert.isAtMost(response.gasUsed.valueOf(), 50000000); }); @@ -38,26 +38,22 @@ describe("test queries on mainnet", function () { }); assert.isTrue(response.isSuccess()); - assert.isAtLeast(response.returnData.length, 20000); + assert.isAtLeast(response.returnData.length, 42); }); it("delegation: should getClaimableRewards", async function () { this.timeout(5000); // First, expect an error (bad arguments): - try { - await delegationContract.runQuery(provider, { - func: new ContractFunction("getClaimableRewards") - }); + let response = await delegationContract.runQuery(provider, { + func: new ContractFunction("getClaimableRewards") + }); - throw new errors.ErrTest("unexpected"); - } catch (err) { - assert.instanceOf(err, errors.ErrContractQuery); - assert.include(err.toString(), "wrong number of arguments"); - } + assert.include(response.returnCode.toString(), "user error"); + assert.include(response.returnMessage, "wrong number of arguments"); // Then do a successful query: - let response = await delegationContract.runQuery(provider, { + response = await delegationContract.runQuery(provider, { func: new ContractFunction("getClaimableRewards"), args: [new AddressValue(new Address("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"))] }); diff --git a/src/smartcontracts/queryResponse.ts b/src/smartcontracts/queryResponse.ts index ad9f0565..847113a8 100644 --- a/src/smartcontracts/queryResponse.ts +++ b/src/smartcontracts/queryResponse.ts @@ -1,16 +1,11 @@ import { GasLimit } from "../networkParams"; -import { EndpointDefinition, TypedValue } from "./typesystem"; import { MaxUint64 } from "./query"; import { ReturnCode } from "./returnCode"; import BigNumber from "bignumber.js"; -import { Result } from "./result"; - -export class QueryResponse implements Result.IResult { - /** - * If available, will provide typed output arguments (with typed values). - */ - private endpointDefinition?: EndpointDefinition; +import { ErrContract } from "../errors"; +import { ArgSerializer } from "./argSerializer"; +export class QueryResponse { returnData: string[]; returnCode: ReturnCode; returnMessage: string; @@ -41,42 +36,30 @@ export class QueryResponse implements Result.IResult { }); } - getEndpointDefinition(): EndpointDefinition | undefined { - return this.endpointDefinition; - } getReturnCode(): ReturnCode { return this.returnCode; } + getReturnMessage(): string { return this.returnMessage; } - unpackOutput(): any { - return Result.unpackOutput(this); + + getReturnDataParts(): Buffer[] { + return this.returnData.map((item) => Buffer.from(item || "", "base64")); } assertSuccess() { - Result.assertSuccess(this); + if (this.isSuccess()) { + return; + } + + throw new ErrContract(`${this.getReturnCode()}: ${this.getReturnMessage()}`); } isSuccess(): boolean { return this.returnCode.isSuccess(); } - setEndpointDefinition(endpointDefinition: EndpointDefinition): void { - this.endpointDefinition = endpointDefinition; - } - - outputUntyped(): Buffer[] { - this.assertSuccess(); - - let buffers = this.returnData.map((item) => Buffer.from(item || "", "base64")); - return buffers; - } - - outputTyped(): TypedValue[] { - return Result.outputTyped(this); - } - /** * Converts the object to a pretty, plain JavaScript object. */ diff --git a/src/smartcontracts/result.ts b/src/smartcontracts/result.ts deleted file mode 100644 index 613c4e0f..00000000 --- a/src/smartcontracts/result.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { ErrContract } from "../errors"; -import { guardValueIsSet } from "../utils"; -import { ArgSerializer } from "./argSerializer"; -import { ReturnCode } from "./returnCode"; -import { EndpointDefinition, TypedValue } from "./typesystem"; - -export namespace Result { - - export interface IResult { - setEndpointDefinition(endpointDefinition: EndpointDefinition): void; - getEndpointDefinition(): EndpointDefinition | undefined; - getReturnCode(): ReturnCode; - getReturnMessage(): string; - isSuccess(): boolean; - assertSuccess(): void; - outputUntyped(): Buffer[]; - outputTyped(): TypedValue[]; - unpackOutput(): any; - } - - export function isSuccess(result: IResult): boolean { - return result.getReturnCode().isSuccess(); - } - - export function assertSuccess(result: IResult): void { - if (result.isSuccess()) { - return; - } - - throw new ErrContract(`${result.getReturnCode()}: ${result.getReturnMessage()}`); - } - - export function outputTyped(result: IResult) { - result.assertSuccess(); - - let endpointDefinition = result.getEndpointDefinition(); - guardValueIsSet("endpointDefinition", endpointDefinition); - - let buffers = result.outputUntyped(); - let values = new ArgSerializer().buffersToValues(buffers, endpointDefinition!.output); - return values; - } - - - export function unpackOutput(result: IResult) { - let values = result.outputTyped().map((value) => value?.valueOf()); - if (values.length <= 1) { - return values[0]; - } - return values; - } -} diff --git a/src/smartcontracts/resultsParser.spec.ts b/src/smartcontracts/resultsParser.spec.ts new file mode 100644 index 00000000..1332f8ce --- /dev/null +++ b/src/smartcontracts/resultsParser.spec.ts @@ -0,0 +1,60 @@ +import { assert } from "chai"; +import { BigUIntType, BigUIntValue, EndpointDefinition, EndpointModifiers, EndpointParameterDefinition } from "./typesystem"; +import { BytesType, BytesValue } from "./typesystem/bytes"; +import { QueryResponse } from "./queryResponse"; +import { ReturnCode } from "./returnCode"; +import { ResultsParser } from "./resultsParser"; +import { TransactionOnNetwork } from "../transactionOnNetwork"; +import { SmartContractResultItem, SmartContractResults } from "./smartContractResults"; +import { Nonce } from "../nonce"; + +describe("test smart contract results parser", () => { + let parser = new ResultsParser(); + + 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 QueryResponse({ + returnData: [ + Buffer.from([42]).toString("base64"), + Buffer.from("abba", "hex").toString("base64") + ], + returnCode: 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 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({ + results: new SmartContractResults([ + new SmartContractResultItem({ nonce: new Nonce(7), data: "@6f6b@2a@abba" }) + ]) + }); + + let bundle = parser.parseOutcome(transactionOnNetwork, endpoint); + assert.deepEqual(bundle.returnCode, ReturnCode.Ok); + assert.equal(bundle.returnMessage, "ok"); + assert.deepEqual(bundle.firstValue, new BigUIntValue(42)); + assert.deepEqual(bundle.secondValue, BytesValue.fromHex("abba")); + assert.lengthOf(bundle.values, 2); + }); +}); diff --git a/src/smartcontracts/resultsParser.ts b/src/smartcontracts/resultsParser.ts new file mode 100644 index 00000000..b579933e --- /dev/null +++ b/src/smartcontracts/resultsParser.ts @@ -0,0 +1,112 @@ +import { ErrCannotParseContractResults, ErrInvariantFailed } from "../errors"; +import { TransactionOnNetwork } from "../transactionOnNetwork"; +import { ArgSerializer } from "./argSerializer"; +import { TypedOutcomeBundle, IResultsParser, UntypedOutcomeBundle } from "./interface"; +import { QueryResponse } from "./queryResponse"; +import { ReturnCode } from "./returnCode"; +import { EndpointDefinition } from "./typesystem"; + +enum WellKnownEvents { + OnTransactionCompleted = "completedTxEvent", + OnContractDeployment = "SCDeploy", + OnUserError = "signalError" +} + +export class ResultsParser implements IResultsParser { + parseQueryResponse(queryResponse: QueryResponse, endpoint: EndpointDefinition): TypedOutcomeBundle { + let parts = queryResponse.getReturnDataParts(); + let values = new ArgSerializer().buffersToValues(parts, endpoint.output); + + return { + returnCode: queryResponse.returnCode, + returnMessage: queryResponse.returnMessage, + values: values, + firstValue: values[0], + secondValue: values[1], + thirdValue: values[2] + }; + } + + parseUntypedQueryResponse(queryResponse: QueryResponse): UntypedOutcomeBundle { + return { + returnCode: queryResponse.returnCode, + returnMessage: queryResponse.returnMessage, + values: queryResponse.getReturnDataParts() + }; + } + + parseOutcome(transaction: TransactionOnNetwork, endpoint: EndpointDefinition): TypedOutcomeBundle { + let untypedBundle = this.parseUntypedOutcome(transaction); + let values = new ArgSerializer().buffersToValues(untypedBundle.values, endpoint.output); + + return { + returnCode: untypedBundle.returnCode, + returnMessage: untypedBundle.returnMessage, + values: values, + firstValue: values[0], + secondValue: values[1], + thirdValue: values[2] + }; + } + + /** + * TODO: Upon further analysis, improve this function. Currently, the implementation makes some (possibly inaccurate) assumptions on the SCR & logs emission logic. + */ + parseUntypedOutcome(transaction: TransactionOnNetwork): UntypedOutcomeBundle { + let resultItems = transaction.results.getAll(); + // Let's search the result holding the returnData + // (possibly inaccurate logic at this moment) + let resultItemWithReturnData = resultItems.find(item => item.nonce.valueOf() != 0); + + // If we didn't find it, then fallback to events & logs: + // (possibly inaccurate logic at this moment) + if (!resultItemWithReturnData) { + let returnCode = ReturnCode.Unknown; + + if (transaction.logs.findEventByIdentifier(WellKnownEvents.OnTransactionCompleted)) { + // We do not extract any return data. + returnCode = ReturnCode.Ok; + } else if (transaction.logs.findEventByIdentifier(WellKnownEvents.OnContractDeployment)) { + // When encountering this event, we assume a successful deployment. + // We do not extract any return data. + // (possibly inaccurate logic at this moment, especially in case of deployments from other contracts) + returnCode = ReturnCode.Ok; + } else if (transaction.logs.findEventByIdentifier(WellKnownEvents.OnUserError)) { + returnCode = ReturnCode.UserError; + } + + // TODO: Also handle "too much gas provided" (writeLog event) - in this case, the returnData is held in the event.data field. + + return { + returnCode: returnCode, + returnMessage: returnCode.toString(), + values: [] + }; + } + + let parts = resultItemWithReturnData.getDataParts(); + let { returnCode, returnDataParts } = this.sliceDataParts(parts); + + return { + returnCode: returnCode, + returnMessage: returnCode.toString(), + values: returnDataParts + }; + } + + private sliceDataParts(parts: Buffer[]): { returnCode: ReturnCode, returnDataParts: Buffer[] } { + let emptyReturnPart = parts[0] || Buffer.from([]); + let returnCodePart = parts[1] || Buffer.from([]); + let returnDataParts = parts.slice(2); + + if (emptyReturnPart.length != 0) { + throw new ErrCannotParseContractResults("no leading empty part"); + } + if (returnCodePart.length == 0) { + throw new ErrCannotParseContractResults("no return code"); + } + + let returnCode = ReturnCode.fromBuffer(returnCodePart); + return { returnCode, returnDataParts }; + } +} diff --git a/src/smartcontracts/returnCode.ts b/src/smartcontracts/returnCode.ts index b537062e..9d41015b 100644 --- a/src/smartcontracts/returnCode.ts +++ b/src/smartcontracts/returnCode.ts @@ -1,3 +1,6 @@ +/** + * Also see: https://github.com/ElrondNetwork/elrond-vm-common/blob/master/returnCodes.go + */ export class ReturnCode { static None = new ReturnCode(""); static Ok = new ReturnCode("ok"); diff --git a/src/smartcontracts/smartContract.local.net.spec.ts b/src/smartcontracts/smartContract.local.net.spec.ts index 750ac5b9..959767cd 100644 --- a/src/smartcontracts/smartContract.local.net.spec.ts +++ b/src/smartcontracts/smartContract.local.net.spec.ts @@ -7,15 +7,17 @@ import { loadTestWallets, TestWallet } from "../testutils/wallets"; import { loadContractCode } from "../testutils"; import { Logger } from "../logger"; import { assert } from "chai"; -import { Balance } from "../balance"; -import { AddressValue, BigUIntValue, OptionValue, U32Value } from "./typesystem"; +import { AddressValue, BigUIntType, BigUIntValue, OptionalType, OptionalValue, OptionValue, TokenIdentifierValue, U32Value } from "./typesystem"; import { decodeUnsignedNumber } from "./codec"; import { BytesValue } from "./typesystem/bytes"; import { chooseProxyProvider } from "../interactive"; +import { ResultsParser } from "./resultsParser"; describe("test on local testnet", function () { let provider = chooseProxyProvider("local-testnet"); let alice: TestWallet, bob: TestWallet, carol: TestWallet; + let resultsParser = new ResultsParser(); + before(async function () { ({ alice, bob, carol } = await loadTestWallets()); }); @@ -74,7 +76,14 @@ describe("test on local testnet", function () { await transactionIncrement.send(provider); await transactionDeploy.awaitExecuted(provider); + let transactionOnNetwork = await transactionDeploy.getAsOnNetwork(provider); + let bundle = resultsParser.parseUntypedOutcome(transactionOnNetwork); + assert.isTrue(bundle.returnCode.isSuccess()); + await transactionIncrement.awaitExecuted(provider); + transactionOnNetwork = await transactionDeploy.getAsOnNetwork(provider); + bundle = resultsParser.parseUntypedOutcome(transactionOnNetwork); + assert.isTrue(bundle.returnCode.isSuccess()); // Simulate Logger.trace(JSON.stringify(await simulateOne.simulate(provider), null, 4)); @@ -135,7 +144,7 @@ describe("test on local testnet", function () { // Check counter let queryResponse = await contract.runQuery(provider, { func: new ContractFunction("get") }); - assert.equal(3, decodeUnsignedNumber(queryResponse.outputUntyped()[0])); + assert.equal(3, decodeUnsignedNumber(queryResponse.getReturnDataParts()[0])); }); it("erc20: should deploy, call and query contract", async function () { @@ -196,25 +205,25 @@ describe("test on local testnet", function () { let queryResponse = await contract.runQuery(provider, { func: new ContractFunction("totalSupply") }); - assert.equal(10000, decodeUnsignedNumber(queryResponse.outputUntyped()[0])); + assert.equal(10000, decodeUnsignedNumber(queryResponse.getReturnDataParts()[0])); queryResponse = await contract.runQuery(provider, { func: new ContractFunction("balanceOf"), args: [new AddressValue(alice.address)] }); - assert.equal(7500, decodeUnsignedNumber(queryResponse.outputUntyped()[0])); + assert.equal(7500, decodeUnsignedNumber(queryResponse.getReturnDataParts()[0])); queryResponse = await contract.runQuery(provider, { func: new ContractFunction("balanceOf"), args: [new AddressValue(bob.address)] }); - assert.equal(1000, decodeUnsignedNumber(queryResponse.outputUntyped()[0])); + assert.equal(1000, decodeUnsignedNumber(queryResponse.getReturnDataParts()[0])); queryResponse = await contract.runQuery(provider, { func: new ContractFunction("balanceOf"), args: [new AddressValue(carol.address)] }); - assert.equal(1500, decodeUnsignedNumber(queryResponse.outputUntyped()[0])); + assert.equal(1500, decodeUnsignedNumber(queryResponse.getReturnDataParts()[0])); }); it("lottery: should deploy, call and query contract", async function () { @@ -229,8 +238,8 @@ describe("test on local testnet", function () { // Deploy let contract = new SmartContract({}); let transactionDeploy = contract.deploy({ - code: await loadContractCode("src/testdata/lottery_egld.wasm"), - gasLimit: new GasLimit(100000000), + code: await loadContractCode("src/testdata/lottery-esdt.wasm"), + gasLimit: new GasLimit(50000000), initArguments: [] }); @@ -243,15 +252,17 @@ describe("test on local testnet", function () { // Start let transactionStart = contract.call({ func: new ContractFunction("start"), - gasLimit: new GasLimit(15000000), + gasLimit: new GasLimit(10000000), args: [ - BytesValue.fromUTF8("foobar"), - new BigUIntValue(Balance.egld(1).valueOf()), + BytesValue.fromUTF8("lucky"), + new TokenIdentifierValue("EGLD"), + new BigUIntValue(1), OptionValue.newMissing(), OptionValue.newMissing(), OptionValue.newProvided(new U32Value(1)), OptionValue.newMissing(), OptionValue.newMissing(), + OptionalValue.newMissing() ] }); @@ -268,20 +279,22 @@ describe("test on local testnet", function () { await transactionStart.awaitNotarized(provider); // Let's check the SCRs - let deployResults = (await transactionDeploy.getAsOnNetwork(provider)).getSmartContractResults(); - deployResults.getImmediate().assertSuccess(); + let transactionOnNetwork = await transactionDeploy.getAsOnNetwork(provider); + let bundle = resultsParser.parseUntypedOutcome(transactionOnNetwork); + assert.isTrue(bundle.returnCode.isSuccess()); - let startResults = (await transactionStart.getAsOnNetwork(provider)).getSmartContractResults(); - startResults.getImmediate().assertSuccess(); + transactionOnNetwork = await transactionStart.getAsOnNetwork(provider); + bundle = resultsParser.parseUntypedOutcome(transactionOnNetwork); + assert.isTrue(bundle.returnCode.isSuccess()); // Query state, do some assertions let queryResponse = await contract.runQuery(provider, { func: new ContractFunction("status"), args: [ - BytesValue.fromUTF8("foobar") + BytesValue.fromUTF8("lucky") ] }); - assert.equal(decodeUnsignedNumber(queryResponse.outputUntyped()[0]), 1); + assert.equal(decodeUnsignedNumber(queryResponse.getReturnDataParts()[0]), 1); queryResponse = await contract.runQuery(provider, { func: new ContractFunction("status"), @@ -289,6 +302,6 @@ describe("test on local testnet", function () { BytesValue.fromUTF8("missingLottery") ] }); - assert.equal(decodeUnsignedNumber(queryResponse.outputUntyped()[0]), 0); + assert.equal(decodeUnsignedNumber(queryResponse.getReturnDataParts()[0]), 0); }); }); diff --git a/src/smartcontracts/smartContract.ts b/src/smartcontracts/smartContract.ts index b7d46065..78238517 100644 --- a/src/smartcontracts/smartContract.ts +++ b/src/smartcontracts/smartContract.ts @@ -17,6 +17,7 @@ import { TypedValue } from "./typesystem"; import { bigIntToBuffer } from "./codec/utils"; import BigNumber from "bignumber.js"; import { Interaction } from "./interaction"; +import { NativeSerializer } from "./nativeSerializer"; const createKeccakHash = require("keccak"); /** @@ -28,6 +29,7 @@ export class SmartContract implements ISmartContract { private code: Code = Code.nothing(); private codeMetadata: CodeMetadata = new CodeMetadata(); private abi?: SmartContractAbi; + // TODO: Perhaps remove this? private readonly trackOfTransactions: Transaction[] = []; /** @@ -36,6 +38,15 @@ export class SmartContract implements ISmartContract { */ public readonly methods: { [key: string]: (args?: TypedValue[]) => Interaction } = {}; + /** + * This object contains a function for each endpoint defined by the contract. + * (a bit similar to web3js's "contract.methods"). + * + * This is an alternative to {@link methods}. + * Unlike {@link methods}, automatic type inference (wrt. ABI) is applied when using {@link methodsAuto}. + */ + public readonly methodsAuto: { [key: string]: (args?: any[]) => Interaction } = {}; + /** * Create a SmartContract object by providing its address on the Network. */ @@ -56,12 +67,20 @@ export class SmartContract implements ISmartContract { for (const definition of abi.getAllEndpoints()) { let functionName = definition.name; - // For each endpoint defined by the ABI, we attach a function to the "methods" object, + // For each endpoint defined by the ABI, we attach a function to the "methods" and "methodsAuto" objects, // a function that receives typed values as arguments // and returns a prepared contract interaction. this.methods[functionName] = function (args?: TypedValue[]) { let func = new ContractFunction(functionName); - let interaction = new Interaction(contract, func, func, args || []); + let interaction = new Interaction(contract, func, args || []); + return interaction; + }; + + this.methodsAuto[functionName] = function (args?: any[]) { + let func = new ContractFunction(functionName); + // Perform automatic type inference, wrt. the endpoint definition: + let typedArgs = NativeSerializer.nativeToTypedValues(args || [], definition); + let interaction = new Interaction(contract, func, typedArgs || []); return interaction; }; } diff --git a/src/smartcontracts/smartContractController.ts b/src/smartcontracts/smartContractController.ts new file mode 100644 index 00000000..a47e10c2 --- /dev/null +++ b/src/smartcontracts/smartContractController.ts @@ -0,0 +1,106 @@ +import { IProvider } from "../interface"; +import { Interaction } from "./interaction"; +import { Transaction } from "../transaction"; +import { TransactionOnNetwork } from "../transactionOnNetwork"; +import { TypedOutcomeBundle, IInteractionChecker, IResultsParser, ISmartContractController, UntypedOutcomeBundle } from "./interface"; +import { ContractFunction } from "./function"; +import { ResultsParser } from "./resultsParser"; +import { InteractionChecker, NullInteractionChecker } from "./interactionChecker"; +import { EndpointDefinition } from "./typesystem"; +import { Logger } from "../logger"; + +/** + * Internal interface: the smart contract ABI, as seen from the perspective of an {@link SmartContractController}. + */ +interface ISmartContractAbi { + getEndpoint(func: ContractFunction): EndpointDefinition; +} + +/** + * A (frontend) controller, suitable for frontends and dApp, + * where signing is performed by means of an external wallet provider. + */ +export class SmartContractController implements ISmartContractController { + private readonly abi: ISmartContractAbi; + private readonly checker: IInteractionChecker; + private readonly parser: IResultsParser; + private readonly provider: IProvider; + + constructor( + abi: ISmartContractAbi, + checker: IInteractionChecker, + parser: IResultsParser, + provider: IProvider, + ) { + this.abi = abi; + this.checker = checker; + this.parser = parser; + this.provider = provider; + } + + async deploy(transaction: Transaction): Promise<{ transactionOnNetwork: TransactionOnNetwork, bundle: UntypedOutcomeBundle }> { + Logger.info(`SmartContractController.deploy [begin]: transaction = ${transaction.getHash()}`); + + await transaction.send(this.provider); + await transaction.awaitExecuted(this.provider); + let transactionOnNetwork = await transaction.getAsOnNetwork(this.provider); + let bundle = this.parser.parseUntypedOutcome(transactionOnNetwork); + + Logger.info(`SmartContractController.deploy [end]: transaction = ${transaction.getHash()}, return code = ${bundle.returnCode}`); + return { transactionOnNetwork, bundle }; + } + + /** + * Broadcasts an alredy-signed interaction transaction, and also waits for its execution on the Network. + * + * @param interaction The interaction used to build the {@link transaction} + * @param transaction The interaction transaction, which must be signed beforehand + */ + async execute(interaction: Interaction, transaction: Transaction): Promise<{ transactionOnNetwork: TransactionOnNetwork, bundle: TypedOutcomeBundle }> { + Logger.info(`SmartContractController.execute [begin]: function = ${interaction.getFunction()}, transaction = ${transaction.getHash()}`); + + let endpoint = this.getEndpoint(interaction); + + this.checker.checkInteraction(interaction, endpoint); + + await transaction.send(this.provider); + await transaction.awaitExecuted(this.provider); + let transactionOnNetwork = await transaction.getAsOnNetwork(this.provider); + let bundle = this.parser.parseOutcome(transactionOnNetwork, endpoint); + + Logger.info(`SmartContractController.execute [end]: function = ${interaction.getFunction()}, transaction = ${transaction.getHash()}, return code = ${bundle.returnCode}`); + return { transactionOnNetwork, bundle }; + } + + async query(interaction: Interaction): Promise { + Logger.debug(`SmartContractController.query [begin]: function = ${interaction.getFunction()}`); + + let endpoint = this.getEndpoint(interaction); + + this.checker.checkInteraction(interaction, endpoint); + + let query = interaction.buildQuery(); + let queryResponse = await this.provider.queryContract(query); + let bundle = this.parser.parseQueryResponse(queryResponse, endpoint); + + Logger.debug(`SmartContractController.query [end]: function = ${interaction.getFunction()}, return code = ${bundle.returnCode}`); + return bundle; + } + + private getEndpoint(interaction: Interaction) { + let func = interaction.getFunction(); + return this.abi.getEndpoint(func); + } +} + +export class DefaultSmartContractController extends SmartContractController { + constructor(abi: ISmartContractAbi, provider: IProvider) { + super(abi, new InteractionChecker(), new ResultsParser(), provider); + } +} + +export class NoCheckSmartContractController extends SmartContractController { + constructor(abi: ISmartContractAbi, provider: IProvider) { + super(abi, new NullInteractionChecker(), new ResultsParser(), provider); + } +} diff --git a/src/smartcontracts/smartContractResults.local.net.spec.ts b/src/smartcontracts/smartContractResults.local.net.spec.ts index cd4dba23..57505655 100644 --- a/src/smartcontracts/smartContractResults.local.net.spec.ts +++ b/src/smartcontracts/smartContractResults.local.net.spec.ts @@ -6,10 +6,13 @@ import { assert } from "chai"; import { chooseProxyProvider } from "../interactive"; import { SmartContract } from "./smartContract"; import { ContractFunction } from "./function"; +import { ResultsParser } from "./resultsParser"; describe("fetch transactions from local testnet", function () { let provider = chooseProxyProvider("local-testnet");; let alice: TestWallet; + let resultsParser = new ResultsParser(); + before(async function () { ({ alice } = await loadTestWallets()); }); @@ -56,23 +59,13 @@ describe("fetch transactions from local testnet", function () { await transactionDeploy.getAsOnNetwork(provider); await transactionIncrement.getAsOnNetwork(provider); - let deployImmediateResult = transactionDeploy.getAsOnNetworkCached().getSmartContractResults().getImmediate(); - let deployResultingCalls = transactionDeploy.getAsOnNetworkCached().getSmartContractResults().getResultingCalls(); - let incrementImmediateResult = transactionIncrement.getAsOnNetworkCached().getSmartContractResults().getImmediate(); - let incrementResultingCalls = transactionIncrement.getAsOnNetworkCached().getSmartContractResults().getResultingCalls(); - - deployImmediateResult.assertSuccess(); - incrementImmediateResult.assertSuccess(); - - assert.lengthOf(deployImmediateResult.outputUntyped(), 0); - // There is some refund - assert.isTrue(deployImmediateResult.value.valueOf().gt(0)); - assert.lengthOf(deployResultingCalls, 0); + let transactionOnNetwork = transactionDeploy.getAsOnNetworkCached(); + let bundle = resultsParser.parseUntypedOutcome(transactionOnNetwork); + assert.isTrue(bundle.returnCode.isSuccess()); - assert.lengthOf(incrementImmediateResult.outputUntyped(), 1); - // There is some refund - assert.isTrue(incrementImmediateResult.value.valueOf().gt(0)); - assert.lengthOf(incrementResultingCalls, 0); + transactionOnNetwork = transactionIncrement.getAsOnNetworkCached(); + bundle = resultsParser.parseUntypedOutcome(transactionOnNetwork); + assert.isTrue(bundle.returnCode.isSuccess()); }); it("ESDT", async function () { diff --git a/src/smartcontracts/smartContractResults.ts b/src/smartcontracts/smartContractResults.ts index a91152b9..4e5afc18 100644 --- a/src/smartcontracts/smartContractResults.ts +++ b/src/smartcontracts/smartContractResults.ts @@ -5,25 +5,12 @@ import { GasLimit, GasPrice } from "../networkParams"; import { Nonce } from "../nonce"; import { TransactionHash } from "../transaction"; import { ArgSerializer } from "./argSerializer"; -import { EndpointDefinition, TypedValue } from "./typesystem"; -import { ReturnCode } from "./returnCode"; -import { Result } from "./result"; export class SmartContractResults { private readonly items: SmartContractResultItem[] = []; - private readonly immediate: TypedResult = new TypedResult(); - private readonly resultingCalls: TypedResult[] = []; constructor(items: SmartContractResultItem[]) { this.items = items; - - if (this.items.length > 0) { - let immediateResult = this.findImmediateResult(); - if (immediateResult) { - this.immediate = immediateResult; - } - this.resultingCalls = this.findResultingCalls(); - } } static empty(): SmartContractResults { @@ -35,38 +22,16 @@ export class SmartContractResults { return new SmartContractResults(items); } - private findImmediateResult(): TypedResult | undefined { - let immediateItem = this.items.filter(item => isImmediateResult(item))[0]; - if (immediateItem) { - return new TypedResult(immediateItem); - } - return undefined; - } - - private findResultingCalls(): TypedResult[] { - let otherItems = this.items.filter(item => !isImmediateResult(item)); - let resultingCalls = otherItems.map(item => new TypedResult(item)); - return resultingCalls; - } - - getImmediate(): TypedResult { - return this.immediate; + getAll(): SmartContractResultItem[] { + return this.items; } - - getResultingCalls(): TypedResult[] { - return this.resultingCalls; - } - - getAllResults(): TypedResult[] { - return this.items.map(item => new TypedResult(item)); - } -} - -function isImmediateResult(item: SmartContractResultItem): boolean { - return item.nonce.valueOf() != 0; } export class SmartContractResultItem { + constructor(init?: Partial) { + Object.assign(this, init); + } + hash: Hash = Hash.empty(); nonce: Nonce = new Nonce(0); value: Balance = Balance.Zero(); @@ -78,7 +43,6 @@ export class SmartContractResultItem { gasLimit: GasLimit = new GasLimit(0); gasPrice: GasPrice = new GasPrice(0); callType: number = 0; - returnMessage: string = ""; static fromHttpResponse(response: { hash: string, @@ -107,68 +71,11 @@ export class SmartContractResultItem { item.gasLimit = new GasLimit(response.gasLimit); item.gasPrice = new GasPrice(response.gasPrice); item.callType = response.callType; - item.returnMessage = response.returnMessage; return item; } - getDataTokens(): Buffer[] { + getDataParts(): Buffer[] { return new ArgSerializer().stringToBuffers(this.data); } } - -export class TypedResult extends SmartContractResultItem implements Result.IResult { - /** - * If available, will provide typed output arguments (with typed values). - */ - endpointDefinition?: EndpointDefinition; - - constructor(init?: Partial) { - super(); - Object.assign(this, init); - } - - assertSuccess() { - Result.assertSuccess(this); - } - - isSuccess(): boolean { - return this.getReturnCode().isSuccess(); - } - - getReturnCode(): ReturnCode { - let tokens = this.getDataTokens(); - if (tokens.length < 2) { - return ReturnCode.None; - } - let returnCodeToken = tokens[1]; - return ReturnCode.fromBuffer(returnCodeToken); - } - - outputUntyped(): Buffer[] { - this.assertSuccess(); - - // Skip the first 2 SCRs (eg. the @6f6b from @6f6b@2b). - return this.getDataTokens().slice(2); - } - - setEndpointDefinition(endpointDefinition: EndpointDefinition) { - this.endpointDefinition = endpointDefinition; - } - - getEndpointDefinition(): EndpointDefinition | undefined { - return this.endpointDefinition; - } - - getReturnMessage(): string { - return this.returnMessage; - } - - outputTyped(): TypedValue[] { - return Result.outputTyped(this); - } - - unpackOutput(): any { - return Result.unpackOutput(this); - } -} diff --git a/src/smartcontracts/typesystem/abiRegistry.spec.ts b/src/smartcontracts/typesystem/abiRegistry.spec.ts index e798814d..a45a5f31 100644 --- a/src/smartcontracts/typesystem/abiRegistry.spec.ts +++ b/src/smartcontracts/typesystem/abiRegistry.spec.ts @@ -3,11 +3,13 @@ import { extendAbiRegistry, loadAbiRegistry } from "../../testutils"; import { BinaryCodec } from "../codec"; import { AbiRegistry } from "./abiRegistry"; import { AddressType } from "./address"; +import { OptionalType } from "./algebraic"; import { BytesType } from "./bytes"; import { EnumType } from "./enum"; import { ListType, OptionType } from "./generic"; -import { BigUIntType, I64Type, U32Type, U8Type } from "./numerical"; +import { BigUIntType, I64Type, U32Type, U64Type, U8Type } from "./numerical"; import { StructType } from "./struct"; +import { TokenIdentifierType } from "./tokenIdentifier"; describe("test abi registry", () => { it("should extend", async () => { @@ -23,12 +25,12 @@ describe("test abi registry", () => { assert.lengthOf(registry.customTypes, 0); assert.lengthOf(registry.getInterface("counter").endpoints, 3); - await extendAbiRegistry(registry, "src/testdata/lottery_egld.abi.json"); + await extendAbiRegistry(registry, "src/testdata/lottery-esdt.abi.json"); assert.lengthOf(registry.interfaces, 3); assert.lengthOf(registry.customTypes, 2); - assert.lengthOf(registry.getInterface("Lottery").endpoints, 6); - assert.lengthOf(registry.getStruct("LotteryInfo").getFieldsDefinitions(), 8); + assert.lengthOf(registry.getInterface("Lottery").endpoints, 7); + assert.lengthOf(registry.getStruct("LotteryInfo").getFieldsDefinitions(), 7); assert.lengthOf(registry.getEnum("Status").variants, 3); }); @@ -36,7 +38,7 @@ describe("test abi registry", () => { let registry = await loadAbiRegistry([ "src/testdata/answer.abi.json", "src/testdata/counter.abi.json", - "src/testdata/lottery_egld.abi.json", + "src/testdata/lottery-esdt.abi.json", ]); // Ultimate answer @@ -53,13 +55,22 @@ describe("test abi registry", () => { let lottery = registry.getInterface("Lottery"); let start = lottery.getEndpoint("start"); let getStatus = lottery.getEndpoint("status"); - let getLotteryInfo = lottery.getEndpoint("lotteryInfo"); + let getLotteryInfo = lottery.getEndpoint("getLotteryInfo"); assert.instanceOf(start.input[0].type, BytesType); - assert.instanceOf(start.input[1].type, BigUIntType); - assert.instanceOf(start.input[2].type, OptionType); - assert.instanceOf(start.input[2].type.getFirstTypeParameter(), U32Type); - assert.instanceOf(start.input[6].type.getFirstTypeParameter(), ListType); - assert.instanceOf(start.input[6].type.getFirstTypeParameter().getFirstTypeParameter(), AddressType); + assert.instanceOf(start.input[1].type, TokenIdentifierType); + assert.instanceOf(start.input[2].type, BigUIntType); + assert.instanceOf(start.input[3].type, OptionType); + assert.instanceOf(start.input[3].type.getFirstTypeParameter(), U32Type); + assert.instanceOf(start.input[4].type, OptionType); + assert.instanceOf(start.input[4].type.getFirstTypeParameter(), U64Type); + assert.instanceOf(start.input[5].type, OptionType); + assert.instanceOf(start.input[5].type.getFirstTypeParameter(), U32Type); + assert.instanceOf(start.input[6].type, OptionType); + assert.instanceOf(start.input[6].type.getFirstTypeParameter(), BytesType); + assert.instanceOf(start.input[7].type.getFirstTypeParameter(), ListType); + assert.instanceOf(start.input[7].type.getFirstTypeParameter().getFirstTypeParameter(), AddressType); + assert.instanceOf(start.input[8].type, OptionalType); + assert.instanceOf(start.input[8].type.getFirstTypeParameter(), BigUIntType); assert.instanceOf(getStatus.input[0].type, BytesType); assert.instanceOf(getStatus.output[0].type, EnumType); assert.equal(getStatus.output[0].type.getName(), "Status"); @@ -68,9 +79,8 @@ describe("test abi registry", () => { assert.equal(getLotteryInfo.output[0].type.getName(), "LotteryInfo"); let fieldDefinitions = (getLotteryInfo.output[0].type).getFieldsDefinitions(); - assert.instanceOf(fieldDefinitions[0].type, BigUIntType); - assert.instanceOf(fieldDefinitions[5].type, ListType); - assert.instanceOf(fieldDefinitions[5].type.getFirstTypeParameter(), AddressType); + assert.instanceOf(fieldDefinitions[0].type, TokenIdentifierType); + assert.instanceOf(fieldDefinitions[5].type, BytesType); }); it("binary codec correctly decodes perform action result", async () => { diff --git a/src/smartcontracts/typesystem/algebraic.ts b/src/smartcontracts/typesystem/algebraic.ts index 14bc3d03..7a7781fb 100644 --- a/src/smartcontracts/typesystem/algebraic.ts +++ b/src/smartcontracts/typesystem/algebraic.ts @@ -1,5 +1,5 @@ import { guardValueIsSet } from "../../utils"; -import { Type, TypeCardinality, TypedValue } from "./types"; +import { NullType, Type, TypeCardinality, TypedValue } from "./types"; /** * An optional is an algebraic type. It holds zero or one values. @@ -8,6 +8,16 @@ export class OptionalType extends Type { constructor(typeParameter: Type) { super("Optional", [typeParameter], TypeCardinality.variable(1)); } + + isAssignableFrom(type: Type): boolean { + if (!(type.hasJavascriptConstructor(OptionalType.name))) { + return false; + } + + let invariantTypeParameters = this.getFirstTypeParameter().equals(type.getFirstTypeParameter()); + let fakeCovarianceToNull = type.getFirstTypeParameter().hasJavascriptConstructor(NullType.name); + return invariantTypeParameters || fakeCovarianceToNull; + } } export class OptionalValue extends TypedValue { @@ -21,6 +31,14 @@ export class OptionalValue extends TypedValue { this.value = value; } + /** + * Creates an OptionalValue, as not provided (missing). + */ + static newMissing(): OptionalValue { + let type = new OptionalType(new NullType()); + return new OptionalValue(type); + } + isSet(): boolean { return this.value ? true : false; } diff --git a/src/smartcontracts/typesystem/bytes.ts b/src/smartcontracts/typesystem/bytes.ts index 35b36a60..4907eaad 100644 --- a/src/smartcontracts/typesystem/bytes.ts +++ b/src/smartcontracts/typesystem/bytes.ts @@ -49,4 +49,8 @@ export class BytesValue extends PrimitiveValue { valueOf(): Buffer { return this.value; } + + toString() { + return this.value.toString(); + } } diff --git a/src/smartcontracts/typesystem/enum.spec.ts b/src/smartcontracts/typesystem/enum.spec.ts new file mode 100644 index 00000000..23876e5c --- /dev/null +++ b/src/smartcontracts/typesystem/enum.spec.ts @@ -0,0 +1,45 @@ +import * as errors from "../../errors"; +import { assert } from "chai"; +import { U8Type, U8Value } from "./numerical"; +import { Field, FieldDefinition } from "./fields"; +import { EnumType, EnumValue, EnumVariantDefinition } from "./enum"; +import { StringType, StringValue } from "./string"; + +describe("test enums", () => { + it("should get fields", () => { + let greenVariant = new EnumVariantDefinition("Green", 0, [ + new FieldDefinition("0", "red component", new U8Type()), + new FieldDefinition("1", "green component", new U8Type()), + new FieldDefinition("2", "blue component", new U8Type()), + ]); + + let orangeVariant = new EnumVariantDefinition("Orange", 1, [ + new FieldDefinition("0", "hex code", new StringType()) + ]); + + let enumType = new EnumType("Colour", [ + greenVariant, + orangeVariant + ]); + + let green = new EnumValue(enumType, greenVariant, [ + new Field(new U8Value(0), "0"), + new Field(new U8Value(255), "1"), + new Field(new U8Value(0), "2") + ]); + + let orange = new EnumValue(enumType, orangeVariant, [ + new Field(new StringValue("#FFA500"), "0") + ]); + + assert.lengthOf(green.getFields(), 3); + assert.lengthOf(orange.getFields(), 1); + assert.deepEqual(green.getFieldValue("0").toNumber(), 0); + assert.deepEqual(green.getFieldValue("1").toNumber(), 255); + assert.deepEqual(green.getFieldValue("2").toNumber(), 0); + assert.deepEqual(orange.getFieldValue("0"), "#FFA500"); + assert.throw(() => green.getFieldValue("3"), errors.ErrMissingFieldOnEnum); + assert.throw(() => orange.getFieldValue("1"), errors.ErrMissingFieldOnEnum); + }); +}); + diff --git a/src/smartcontracts/typesystem/enum.ts b/src/smartcontracts/typesystem/enum.ts index 1d1e20b1..c924d604 100644 --- a/src/smartcontracts/typesystem/enum.ts +++ b/src/smartcontracts/typesystem/enum.ts @@ -1,3 +1,4 @@ +import { ErrMissingFieldOnEnum } from "../../errors"; import { guardTrue, guardValueIsSet } from "../../utils"; import { Field, FieldDefinition, Fields } from "./fields"; import { CustomType, TypedValue } from "./types"; @@ -60,12 +61,14 @@ export class EnumValue extends TypedValue { readonly name: string; readonly discriminant: number; private readonly fields: Field[] = []; + private readonly fieldsByName: Map; constructor(type: EnumType, variant: EnumVariantDefinition, fields: Field[]) { super(type); this.name = variant.name; this.discriminant = variant.discriminant; this.fields = fields; + this.fieldsByName = new Map(fields.map(field => [field.name, field])); let definitions = variant.getFieldsDefinitions(); Fields.checkTyping(this.fields, definitions); @@ -106,6 +109,15 @@ export class EnumValue extends TypedValue { return this.fields; } + getFieldValue(name: string): any { + let field = this.fieldsByName.get(name); + if (field) { + return field.value.valueOf(); + } + + throw new ErrMissingFieldOnEnum(name, this.getType().getName()); + } + valueOf() { let result: any = { name: this.name, fields: [] }; diff --git a/src/smartcontracts/typesystem/factory.spec.ts b/src/smartcontracts/typesystem/factory.spec.ts new file mode 100644 index 00000000..afe82de1 --- /dev/null +++ b/src/smartcontracts/typesystem/factory.spec.ts @@ -0,0 +1,27 @@ +import { assert } from "chai"; +import { TokenIdentifierType } from "./tokenIdentifier"; +import { Address } from "../../address"; +import { createListOfAddresses, createListOfTokenIdentifiers } from "./factory"; +import { ListType } from "./generic"; +import { AddressType } from "./address"; + +describe("test factory", () => { + it("should create lists of addresses", () => { + let addresses = [ + new Address("erd1dc3yzxxeq69wvf583gw0h67td226gu2ahpk3k50qdgzzym8npltq7ndgha"), + new Address("erd1r69gk66fmedhhcg24g2c5kn2f2a5k4kvpr6jfw67dn2lyydd8cfswy6ede"), + new Address("erd1fggp5ru0jhcjrp5rjqyqrnvhr3sz3v2e0fm3ktknvlg7mcyan54qzccnan") + ]; + + let list = createListOfAddresses(addresses); + assert.deepEqual(list.getType(), new ListType(new AddressType())); + assert.deepEqual(list.valueOf(), addresses); + }); + + it("should create lists of token identifiers", () => { + let identifiers = ["RIDE-7d18e9", "MEX-455c57"]; + let list = createListOfTokenIdentifiers(identifiers); + assert.deepEqual(list.getType(), new ListType(new TokenIdentifierType())); + assert.deepEqual(list.valueOf(), identifiers); + }); +}); diff --git a/src/smartcontracts/typesystem/factory.ts b/src/smartcontracts/typesystem/factory.ts new file mode 100644 index 00000000..b4870829 --- /dev/null +++ b/src/smartcontracts/typesystem/factory.ts @@ -0,0 +1,16 @@ +import { Address } from "../../address"; +import { AddressValue } from "./address"; +import { List } from "./generic"; +import { TokenIdentifierValue } from "./tokenIdentifier"; + +export function createListOfAddresses(addresses: Address[]): List { + let addressesTyped = addresses.map(address => new AddressValue(address)); + let list = List.fromItems(addressesTyped); + return list; +} + +export function createListOfTokenIdentifiers(identifiers: string[]): List { + let identifiersTyped = identifiers.map(identifier => new TokenIdentifierValue(identifier)); + let list = List.fromItems(identifiersTyped); + return list; +} diff --git a/src/smartcontracts/typesystem/generic.ts b/src/smartcontracts/typesystem/generic.ts index 26457156..fa974fff 100644 --- a/src/smartcontracts/typesystem/generic.ts +++ b/src/smartcontracts/typesystem/generic.ts @@ -46,7 +46,7 @@ export class OptionValue extends TypedValue { return new OptionValue(type); } - static newMissingType(type: Type): OptionValue { + static newMissingTyped(type: Type): OptionValue { return new OptionValue(new OptionType(type)); } diff --git a/src/smartcontracts/typesystem/index.ts b/src/smartcontracts/typesystem/index.ts index 201a037d..c4644499 100644 --- a/src/smartcontracts/typesystem/index.ts +++ b/src/smartcontracts/typesystem/index.ts @@ -12,6 +12,7 @@ export * from "./composite"; export * from "./contractInterface"; export * from "./endpoint"; export * from "./enum"; +export * from "./factory"; export * from "./fields"; export * from "./generic"; export * from "./genericArray"; diff --git a/src/smartcontracts/typesystem/struct.spec.ts b/src/smartcontracts/typesystem/struct.spec.ts new file mode 100644 index 00000000..ae763996 --- /dev/null +++ b/src/smartcontracts/typesystem/struct.spec.ts @@ -0,0 +1,35 @@ +import * as errors from "../../errors"; +import { assert } from "chai"; +import { BigUIntType, BigUIntValue, U32Type, U32Value } from "./numerical"; +import { BytesType, BytesValue } from "./bytes"; +import { Struct, StructType } from "./struct"; +import { Field, FieldDefinition } from "./fields"; +import { TokenIdentifierType, TokenIdentifierValue } from "./tokenIdentifier"; + +describe("test structs", () => { + it("should get fields", () => { + let fooType = new StructType( + "Foo", + [ + new FieldDefinition("a", "", new TokenIdentifierType()), + new FieldDefinition("b", "", new BigUIntType()), + new FieldDefinition("c", "", new U32Type()), + new FieldDefinition("d", "", new BytesType()), + ] + ); + + let fooStruct = new Struct(fooType, [ + new Field(new TokenIdentifierValue("lucky-token"), "a"), + new Field(new BigUIntValue(1), "b"), + new Field(new U32Value(42), "c"), + new Field(new BytesValue(Buffer.from([0x64])), "d"), + ]); + + assert.lengthOf(fooStruct.getFields(), 4); + assert.deepEqual(fooStruct.getFieldValue("a"), "lucky-token"); + assert.deepEqual(fooStruct.getFieldValue("b").toNumber(), 1); + assert.deepEqual(fooStruct.getFieldValue("c").toNumber(), 42); + assert.deepEqual(fooStruct.getFieldValue("d"), Buffer.from([0x64])); + assert.throw(() => fooStruct.getFieldValue("e"), errors.ErrMissingFieldOnStruct); + }); +}); diff --git a/src/smartcontracts/typesystem/struct.ts b/src/smartcontracts/typesystem/struct.ts index b1eec34b..feab5e79 100644 --- a/src/smartcontracts/typesystem/struct.ts +++ b/src/smartcontracts/typesystem/struct.ts @@ -1,3 +1,4 @@ +import { ErrMissingFieldOnStruct, ErrTypingSystem } from "../../errors"; import { FieldDefinition, Field, Fields } from "./fields"; import { CustomType, TypedValue } from "./types"; @@ -19,17 +20,17 @@ export class StructType extends CustomType { } } -// TODO: implement setField(), convenience method. -// TODO: Hold fields in a map (by name), and use the order within "field definitions" to perform codec operations. export class Struct extends TypedValue { - private readonly fields: Field[] = []; + private readonly fields: Field[]; + private readonly fieldsByName: Map; /** - * Currently, one can only set fields at initialization time. Construction will be improved at a later time. + * One can only set fields at initialization time. */ constructor(type: StructType, fields: Field[]) { super(type); this.fields = fields; + this.fieldsByName = new Map(fields.map(field => [field.name, field])); this.checkTyping(); } @@ -44,6 +45,15 @@ export class Struct extends TypedValue { return this.fields; } + getFieldValue(name: string): any { + let field = this.fieldsByName.get(name); + if (field) { + return field.value.valueOf(); + } + + throw new ErrMissingFieldOnStruct(name, this.getType().getName()); + } + valueOf(): any { let result: any = {}; diff --git a/src/smartcontracts/typesystem/tokenIdentifier.ts b/src/smartcontracts/typesystem/tokenIdentifier.ts index 757056f9..ab7ac63a 100644 --- a/src/smartcontracts/typesystem/tokenIdentifier.ts +++ b/src/smartcontracts/typesystem/tokenIdentifier.ts @@ -8,9 +8,9 @@ export class TokenIdentifierType extends PrimitiveType { } export class TokenIdentifierValue extends PrimitiveValue { - private readonly value: Buffer; + private readonly value: string; - constructor(value: Buffer) { + constructor(value: string) { super(new TokenIdentifierType()); this.value = value; } @@ -27,10 +27,10 @@ export class TokenIdentifierValue extends PrimitiveValue { return false; } - return this.value.equals(other.value); + return this.value == other.value; } - valueOf(): Buffer { + valueOf(): string { return this.value; } diff --git a/src/smartcontracts/typesystem/types.ts b/src/smartcontracts/typesystem/types.ts index b8ae5672..b686fa42 100644 --- a/src/smartcontracts/typesystem/types.ts +++ b/src/smartcontracts/typesystem/types.ts @@ -93,7 +93,7 @@ export class Type { * * One exception though: for {@link OptionType}, we simulate covariance for missing (not provided) values. * For example, Option is assignable from Option. - * For more details, see the implementation of {@link OptionType}. + * For more details, see the implementation of {@link OptionType} and @{@link OptionalType}. * * Also see: * - https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science) diff --git a/src/smartcontracts/wrapper/contractLogger.ts b/src/smartcontracts/wrapper/contractLogger.ts index a9a13e60..ea7a6302 100644 --- a/src/smartcontracts/wrapper/contractLogger.ts +++ b/src/smartcontracts/wrapper/contractLogger.ts @@ -3,7 +3,8 @@ import { NetworkConfig } from "../../networkConfig"; import { Transaction } from "../../transaction"; import { Query } from "../query"; import { QueryResponse } from "../queryResponse"; -import { SmartContractResults, TypedResult } from "../smartContractResults"; +import { SmartContractResults } from "../smartContractResults"; +import { findImmediateResult, findResultingCalls, TypedResult } from "./deprecatedContractResults"; /** * Provides a simple interface in order to easily call or query the smart contract's methods. @@ -42,17 +43,17 @@ export class ContractLogger { } function logReturnMessages(transaction: Transaction, smartContractResults: SmartContractResults) { - let immediate = smartContractResults.getImmediate(); + let immediate = findImmediateResult(smartContractResults)!; logSmartContractResultIfMessage("(immediate)", transaction, immediate); - let resultingCalls = smartContractResults.getResultingCalls(); + let resultingCalls = findResultingCalls(smartContractResults); for (let i in resultingCalls) { logSmartContractResultIfMessage("(resulting call)", transaction, resultingCalls[i]); } } function logSmartContractResultIfMessage(info: string, _transaction: Transaction, smartContractResult: TypedResult) { - if (smartContractResult.returnMessage) { - console.log(`Return message ${info} message: ${smartContractResult.returnMessage}`); + if (smartContractResult.getReturnMessage()) { + console.log(`Return message ${info} message: ${smartContractResult.getReturnMessage()}`); } } diff --git a/src/smartcontracts/wrapper/contractWrapper.local.net.spec.ts b/src/smartcontracts/wrapper/contractWrapper.local.net.spec.ts index d9b8aae7..7a7fc064 100644 --- a/src/smartcontracts/wrapper/contractWrapper.local.net.spec.ts +++ b/src/smartcontracts/wrapper/contractWrapper.local.net.spec.ts @@ -3,10 +3,9 @@ import { BigNumber } from "bignumber.js"; import { TestWallet } from "../../testutils"; import { SystemWrapper } from "./systemWrapper"; import { setupInteractive } from "../../interactive"; -import { Balance } from "../../balance"; -describe("test smart contract interactor", function () { +describe("test smart contract wrapper", function () { let erdSys: SystemWrapper; let alice: TestWallet; @@ -44,30 +43,29 @@ describe("test smart contract interactor", function () { assert.deepEqual(await counter.call.decrement(), new BigNumber(0)); }); - it("should interact with 'lottery_egld' (local testnet)", async function () { + it("should interact with 'lottery-esdt' (local testnet)", async function () { this.timeout(120000); - let lottery = await erdSys.loadWrapper("src/testdata", "lottery_egld"); + let lottery = await erdSys.loadWrapper("src/testdata", "lottery-esdt"); - await lottery.sender(alice).gas(100_000_000).call.deploy(); + await lottery.sender(alice).gas(50_000_000).call.deploy(); - lottery.gas(15_000_000); - await lottery.call.start("lucky", Balance.egld(1), null, null, 1, null, null); + lottery.gas(5_000_000); + await lottery.call.start("lucky", "EGLD", 1, null, null, 1, null, null); let status = await lottery.query.status("lucky"); assert.equal(status.valueOf().name, "Running"); - let info = await lottery.query.lotteryInfo("lucky"); + let info = await lottery.query.getLotteryInfo("lucky"); // Ignore "deadline" field in our test delete info.deadline; assert.deepEqual(info, { - ticket_price: new BigNumber("1000000000000000000"), + token_identifier: "EGLD", + ticket_price: new BigNumber("1"), tickets_left: new BigNumber(800), max_entries_per_user: new BigNumber(1), prize_distribution: Buffer.from([0x64]), - whitelist: [], - current_ticket_number: new BigNumber(0), prize_pool: new BigNumber("0") }); }); diff --git a/src/smartcontracts/wrapper/contractWrapper.spec.ts b/src/smartcontracts/wrapper/contractWrapper.spec.ts index 4684e991..a0f3c815 100644 --- a/src/smartcontracts/wrapper/contractWrapper.spec.ts +++ b/src/smartcontracts/wrapper/contractWrapper.spec.ts @@ -1,6 +1,4 @@ import { - AddImmediateResult, - MarkNotarized, MockProvider, setupUnitTestWatcherTimeouts, TestWallet, @@ -8,7 +6,6 @@ import { import { Address } from "../../address"; import { assert } from "chai"; import { QueryResponse } from "../queryResponse"; -import { TransactionStatus } from "../../transaction"; import { ReturnCode } from "../returnCode"; import BigNumber from "bignumber.js"; import { SystemWrapper } from "./systemWrapper"; @@ -69,54 +66,46 @@ describe("test smart contract wrapper", async function() { assert.deepEqual(decrementResult, new BigNumber(7)); }); - it("should interact with 'lottery_egld'", async function() { + it("should interact with 'lottery-esdt'", async function() { setupUnitTestWatcherTimeouts(); - let lottery = await erdSys.loadWrapper("src/testdata", "lottery_egld"); + let lottery = await erdSys.loadWrapper("src/testdata", "lottery-esdt"); lottery .address(dummyAddress) .sender(alice) .gas(5_000_000); - await mockCall(provider, "@6f6b", lottery.call.start("lucky", Egld(1), null, null, 1, null, null)); + await mockCall(provider, "@6f6b", lottery.call.start("lucky", "lucky-token", 1, null, null, 1, null, null)); let status = await mockCall(provider, "@6f6b@01", lottery.call.status("lucky")); assert.deepEqual(status, { name: "Running", fields: [] }); let info = await mockCall( provider, - "@6f6b@000000080de0b6b3a764000000000320000000006012a806000000010000000164000000000000000000000000", - lottery.call.lotteryInfo("lucky") + "@6f6b@0000000b6c75636b792d746f6b656e000000010100000000000000005fc2b9dbffffffff00000001640000000a140ec80fa7ee88000000", + lottery.call.getLotteryInfo("lucky") ); assert.deepEqual(info, { - ticket_price: new BigNumber("1000000000000000000"), - tickets_left: new BigNumber(800), - deadline: new BigNumber("1611835398"), - max_entries_per_user: new BigNumber(1), + token_identifier: "lucky-token", + ticket_price: new BigNumber("1"), + tickets_left: new BigNumber(0), + deadline: new BigNumber("0x000000005fc2b9db", 16), + max_entries_per_user: new BigNumber(0xffffffff), prize_distribution: Buffer.from([0x64]), - whitelist: [], - current_ticket_number: new BigNumber(0), - prize_pool: new BigNumber("0"), + prize_pool: new BigNumber("94720000000000000000000") }); }); }); function mockQuery(provider: MockProvider, functionName: string, mockedResult: string) { - provider.mockQueryResponseOnFunction( + provider.mockQueryContractOnFunction( functionName, new QueryResponse({ returnData: [mockedResult], returnCode: ReturnCode.Ok }) ); } async function mockCall(provider: MockProvider, mockedResult: string, promise: Promise) { - let [, value] = await Promise.all([ - provider.mockNextTransactionTimeline([ - new TransactionStatus("executed"), - new AddImmediateResult(mockedResult), - new MarkNotarized(), - ]), - promise, - ]); - return value; + provider.mockGetTransactionWithAnyHashAsNotarizedWithOneResult(mockedResult); + return await promise; } diff --git a/src/smartcontracts/wrapper/contractWrapper.ts b/src/smartcontracts/wrapper/contractWrapper.ts index a1ebd7b3..797068aa 100644 --- a/src/smartcontracts/wrapper/contractWrapper.ts +++ b/src/smartcontracts/wrapper/contractWrapper.ts @@ -17,11 +17,12 @@ import { SmartContract } from "../smartContract"; import { Code } from "../code"; import { Transaction } from "../../transaction"; import { IProvider } from "../../interface"; -import { ExecutionResultsBundle } from "../interface"; import { Interaction } from "../interaction"; import { Err, ErrContract, ErrInvalidArgument } from "../../errors"; import { Egld } from "../../balanceBuilder"; import { Balance } from "../../balance"; +import { ExecutionResultsBundle, findImmediateResult, interpretExecutionResults } from "./deprecatedContractResults"; +import { Result } from "./result"; /** * Provides a simple interface in order to easily call or query the smart contract's methods. @@ -109,8 +110,8 @@ export class ContractWrapper extends ChainSendContext { let transactionOnNetwork = await this.processTransaction(transaction); - let smartContractResults = transactionOnNetwork.getSmartContractResults(); - let immediateResult = smartContractResults.getImmediate(); + let smartContractResults = transactionOnNetwork.results; + let immediateResult = findImmediateResult(smartContractResults)!; immediateResult.assertSuccess(); let logger = this.context.getLogger(); logger?.deployComplete(transaction, smartContractResults, this.smartContract.getAddress()); @@ -138,8 +139,7 @@ export class ContractWrapper extends ChainSendContext { query.caller = optionalSender.address; } let response = await provider.queryContract(query); - let queryResponseBundle = interaction.interpretQueryResponse(response); - let result = queryResponseBundle.queryResponse.unpackOutput(); + let result = Result.unpackQueryOutput(endpoint, response); logger?.queryComplete(result, response); return result; @@ -147,24 +147,24 @@ export class ContractWrapper extends ChainSendContext { async handleCall(endpoint: EndpointDefinition, ...args: any[]): Promise { let { transaction, interaction } = this.buildTransactionAndInteraction(endpoint, args); - let { result } = await this.processTransactionAndInterpretResults({ transaction, interaction }); + let { result } = await this.processTransactionAndInterpretResults({ endpoint, transaction }); return result; } async handleResults(endpoint: EndpointDefinition, ...args: any[]): Promise { let { transaction, interaction } = this.buildTransactionAndInteraction(endpoint, args); - let { executionResultsBundle } = await this.processTransactionAndInterpretResults({ transaction, interaction }); + let { executionResultsBundle } = await this.processTransactionAndInterpretResults({ endpoint, transaction }); return executionResultsBundle; } - async processTransactionAndInterpretResults({ transaction, interaction }: { - transaction: Transaction, - interaction: Interaction + async processTransactionAndInterpretResults({ endpoint, transaction }: { + endpoint: EndpointDefinition, + transaction: Transaction }): Promise<{ executionResultsBundle: ExecutionResultsBundle, result: any }> { let transactionOnNetwork = await this.processTransaction(transaction); - let executionResultsBundle = interaction.interpretExecutionResults(transactionOnNetwork); + let executionResultsBundle = interpretExecutionResults(endpoint, transactionOnNetwork); let { smartContractResults, immediateResult } = executionResultsBundle; - let result = immediateResult?.unpackOutput(); + let result = Result.unpackExecutionOutput(endpoint, immediateResult); let logger = this.context.getLogger(); logger?.transactionComplete(result, immediateResult?.data, transaction, smartContractResults); return { executionResultsBundle, result }; @@ -190,7 +190,7 @@ export class ContractWrapper extends ChainSendContext { let transactionOnNetwork = await transaction.getAsOnNetwork(provider, true, false, true); if (transaction.getStatus().isFailed()) { // TODO: extract the error messages - //let results = transactionOnNetwork.getSmartContractResults().getAllResults(); + //let results = transactionOnNetwork.results.getAllResults(); //let messages = results.map((result) => console.log(result)); throw new ErrContract(`Transaction status failed: [${transaction.getStatus().toString()}].`);// Return messages:\n${messages}`); } @@ -231,7 +231,8 @@ export class ContractWrapper extends ChainSendContext { let executingFunction = preparedCall.formattedCall.getExecutingFunction(); let interpretingFunction = preparedCall.formattedCall.getInterpretingFunction(); let typedValueArgs = preparedCall.formattedCall.toTypedValues(); - let interaction = new Interaction(this.smartContract, executingFunction, interpretingFunction, typedValueArgs, preparedCall.receiver); + // TODO: Most probably, this need to be fixed. Perhaps two interactions have to be instantiated? + let interaction = new Interaction(this.smartContract, executingFunction, typedValueArgs, preparedCall.receiver); interaction.withValue(preparedCall.egldValue); return interaction; } diff --git a/src/smartcontracts/wrapper/deprecatedContractResults.ts b/src/smartcontracts/wrapper/deprecatedContractResults.ts new file mode 100644 index 00000000..8b652b7b --- /dev/null +++ b/src/smartcontracts/wrapper/deprecatedContractResults.ts @@ -0,0 +1,139 @@ +// deprecatedContractResults.ts +/** + * This file contains the old (before erdjs 10) logic dealing with SCRs. + * The components / functions were moved here in order to change erdjs-repl / contract wrapper components as little as possible. + * When splitting the erdjs repository into multiple ones (for wallet providers, walletcore etc.), we should consider extracting erdjs-repl / contract wrappers + * to a separate repository, as well. Though excellent for CLI, these components are not suited for minimal web dApps - different use cases. + * @module + */ + +import { TransactionOnNetwork } from "../../transactionOnNetwork"; +import { ArgSerializer } from "../argSerializer"; +import { QueryResponse } from "../queryResponse"; +import { ReturnCode } from "../returnCode"; +import { SmartContractResultItem, SmartContractResults } from "../smartContractResults"; +import { EndpointDefinition, TypedValue } from "../typesystem"; +import { Result } from "./result"; + +/** + * @deprecated The concept of immediate results / resulting calls does not exist in the Protocol / in the API. + * The SCRs are more alike a graph. + */ +export function interpretExecutionResults(endpoint: EndpointDefinition, transactionOnNetwork: TransactionOnNetwork): ExecutionResultsBundle { + let smartContractResults = transactionOnNetwork.results; + let immediateResult = findImmediateResult(smartContractResults)!; + let resultingCalls = findResultingCalls(smartContractResults); + + let buffers = immediateResult.outputUntyped(); + let values = new ArgSerializer().buffersToValues(buffers, endpoint.output); + let returnCode = immediateResult.getReturnCode(); + + return { + smartContractResults: smartContractResults, + immediateResult, + resultingCalls, + values, + firstValue: values[0], + returnCode: returnCode + }; +} + +/** + * @deprecated The concept of immediate results / resulting calls does not exist in the Protocol / in the API. + * The SCRs are more alike a graph. + */ +export interface ExecutionResultsBundle { + smartContractResults: SmartContractResults; + immediateResult: TypedResult; + /** + * @deprecated Most probably, we should use logs & events instead + */ + resultingCalls: TypedResult[]; + values: TypedValue[]; + firstValue: TypedValue; + returnCode: ReturnCode; +} + +export interface QueryResponseBundle { + queryResponse: QueryResponse; + firstValue: TypedValue; + values: TypedValue[]; + returnCode: ReturnCode; +} + +/** + * @deprecated The concept of immediate results / resulting calls does not exist in the Protocol / in the API. + * The SCRs are more like a graph. + */ +export function findImmediateResult(results: SmartContractResults): TypedResult | undefined { + let immediateItem = results.getAll().filter(item => isImmediateResult(item))[0]; + if (immediateItem) { + return new TypedResult(immediateItem); + } + return undefined; +} + +/** + * @deprecated The concept of immediate results / resulting calls does not exist in the Protocol / in the API. + * The SCRs are more like a graph. + */ +export function findResultingCalls(results: SmartContractResults): TypedResult[] { + let otherItems = results.getAll().filter(item => !isImmediateResult(item)); + let resultingCalls = otherItems.map(item => new TypedResult(item)); + return resultingCalls; +} + +/** + * @deprecated The concept of immediate results / resulting calls does not exist in the Protocol / in the API. + * The SCRs are more like a graph. + */ +function isImmediateResult(item: SmartContractResultItem): boolean { + return item.nonce.valueOf() != 0; +} + +/** + * @deprecated getReturnCode(), outputUntyped are a bit fragile. + * They are not necessarily applicable to SCRs, in general (only in particular). + */ +export class TypedResult extends SmartContractResultItem implements Result.IResult { + /** + * If available, will provide typed output arguments (with typed values). + */ + endpointDefinition?: EndpointDefinition; + + constructor(init?: Partial) { + super(); + Object.assign(this, init); + } + + assertSuccess() { + Result.assertSuccess(this); + } + + isSuccess(): boolean { + return this.getReturnCode().isSuccess(); + } + + getReturnCode(): ReturnCode { + let tokens = this.getDataParts(); + if (tokens.length < 2) { + return ReturnCode.None; + } + let returnCodeToken = tokens[1]; + return ReturnCode.fromBuffer(returnCodeToken); + } + + outputUntyped(): Buffer[] { + this.assertSuccess(); + + // Skip the first 2 SCRs (eg. the @6f6b from @6f6b@2b). + return this.getDataParts().slice(2); + } + + /** + * @deprecated The return message isn't available on SmartContractResultItem (not provided by the API). + */ + getReturnMessage(): string { + return this.getReturnCode().toString(); + } +} diff --git a/src/smartcontracts/wrapper/esdt.spec.ts b/src/smartcontracts/wrapper/esdt.spec.ts index 53568ab2..f3cd5a5e 100644 --- a/src/smartcontracts/wrapper/esdt.spec.ts +++ b/src/smartcontracts/wrapper/esdt.spec.ts @@ -73,7 +73,7 @@ describe("test ESDT transfers via the smart contract wrapper", async function () }, auctioned_token: { nonce: new BigNumber(1), - token_type: Buffer.from("TEST-feed60"), + token_type: "TEST-feed60", }, creator_royalties_percentage: new BigNumber(2500), current_bid: new BigNumber(0), @@ -86,7 +86,7 @@ describe("test ESDT transfers via the smart contract wrapper", async function () original_owner: new Address("erd1wuq9x0w2yl7lc96653hw6pltc2ay0f098mxhztn64vh6322ccmussa83g9"), payment_token: { nonce: new BigNumber(0), - token_type: Buffer.from("EGLD") + token_type: "EGLD" }, start_time: new BigNumber(1643744844) } diff --git a/src/smartcontracts/wrapper/result.ts b/src/smartcontracts/wrapper/result.ts new file mode 100644 index 00000000..391cc66a --- /dev/null +++ b/src/smartcontracts/wrapper/result.ts @@ -0,0 +1,49 @@ +import { ErrContract } from "../../errors"; +import { ArgSerializer } from "../argSerializer"; +import { QueryResponse } from "../queryResponse"; +import { ReturnCode } from "../returnCode"; +import { EndpointDefinition } from "../typesystem"; +import { TypedResult } from "./deprecatedContractResults"; + +export namespace Result { + + export interface IResult { + getReturnCode(): ReturnCode; + getReturnMessage(): string; + isSuccess(): boolean; + assertSuccess(): void; + } + + export function isSuccess(result: IResult): boolean { + return result.getReturnCode().isSuccess(); + } + + export function assertSuccess(result: IResult): void { + if (result.isSuccess()) { + return; + } + + throw new ErrContract(`${result.getReturnCode()}: ${result.getReturnMessage()}`); + } + + export function unpackQueryOutput(endpoint: EndpointDefinition, queryResponse: QueryResponse) { + queryResponse.assertSuccess(); + let buffers = queryResponse.getReturnDataParts(); + let typedValues = new ArgSerializer().buffersToValues(buffers, endpoint.output); + let values = typedValues.map((value) => value?.valueOf()); + if (values.length <= 1) { + return values[0]; + } + return values; + } + + export function unpackExecutionOutput(endpoint: EndpointDefinition, result: TypedResult) { + let buffers = result.outputUntyped(); + let typedValues = new ArgSerializer().buffersToValues(buffers, endpoint.output); + let values = typedValues.map((value) => value?.valueOf()); + if (values.length <= 1) { + return values[0]; + } + return values; + } +} diff --git a/src/smartcontracts/wrapper/sendContext.ts b/src/smartcontracts/wrapper/sendContext.ts index 99390b9f..9eb53913 100644 --- a/src/smartcontracts/wrapper/sendContext.ts +++ b/src/smartcontracts/wrapper/sendContext.ts @@ -1,7 +1,5 @@ import { GasLimit } from "../../networkParams"; -import { IInteractionChecker } from "../interface"; import { IProvider } from "../../interface"; -import { StrictChecker } from "../strictChecker"; import { ContractLogger } from "./contractLogger"; import { TestWallet } from "../../testutils"; import { Balance } from "../../balance"; @@ -17,7 +15,6 @@ export class SendContext { private gas_: GasLimit | null; private logger_: ContractLogger | null; private value_: Balance | null; - readonly checker: IInteractionChecker; constructor(provider: IProvider) { this.sender_ = null; @@ -25,7 +22,6 @@ export class SendContext { this.gas_ = null; this.logger_ = null; this.value_ = null; - this.checker = new StrictChecker(); } provider(provider: IProvider): this { diff --git a/src/testdata/lottery_egld.abi.json b/src/testdata/lottery-esdt.abi.json similarity index 70% rename from src/testdata/lottery_egld.abi.json rename to src/testdata/lottery-esdt.abi.json index e4798451..a9fa48c5 100644 --- a/src/testdata/lottery_egld.abi.json +++ b/src/testdata/lottery-esdt.abi.json @@ -1,4 +1,21 @@ { + "buildInfo": { + "rustc": { + "version": "1.60.0-nightly", + "commitHash": "c5c610aad0a012a9228ecb83cc19e77111a52140", + "commitDate": "2022-02-14", + "channel": "Nightly", + "short": "rustc 1.60.0-nightly (c5c610aad 2022-02-14)" + }, + "contractCrate": { + "name": "lottery-esdt", + "version": "0.0.0" + }, + "framework": { + "name": "elrond-wasm", + "version": "0.30.0" + } + }, "name": "Lottery", "constructor": { "inputs": [], @@ -7,11 +24,16 @@ "endpoints": [ { "name": "start", + "mutability": "mutable", "inputs": [ { "name": "lottery_name", "type": "bytes" }, + { + "name": "token_identifier", + "type": "TokenIdentifier" + }, { "name": "ticket_price", "type": "BigUint" @@ -35,17 +57,27 @@ { "name": "opt_whitelist", "type": "Option>" + }, + { + "name": "opt_burn_percentage", + "type": "optional", + "multi_arg": true } ], "outputs": [] }, { "name": "createLotteryPool", + "mutability": "mutable", "inputs": [ { "name": "lottery_name", "type": "bytes" }, + { + "name": "token_identifier", + "type": "TokenIdentifier" + }, { "name": "ticket_price", "type": "BigUint" @@ -69,14 +101,20 @@ { "name": "opt_whitelist", "type": "Option>" + }, + { + "name": "opt_burn_percentage", + "type": "optional", + "multi_arg": true } ], "outputs": [] }, { "name": "buy_ticket", + "mutability": "mutable", "payableInTokens": [ - "EGLD" + "*" ], "inputs": [ { @@ -88,6 +126,7 @@ }, { "name": "determine_winner", + "mutability": "mutable", "inputs": [ { "name": "lottery_name", @@ -98,6 +137,7 @@ }, { "name": "status", + "mutability": "readonly", "inputs": [ { "name": "lottery_name", @@ -111,7 +151,8 @@ ] }, { - "name": "lotteryInfo", + "name": "getLotteryInfo", + "mutability": "readonly", "inputs": [ { "name": "lottery_name", @@ -123,12 +164,33 @@ "type": "LotteryInfo" } ] + }, + { + "name": "getLotteryWhitelist", + "mutability": "readonly", + "inputs": [ + { + "name": "lottery_name", + "type": "bytes" + } + ], + "outputs": [ + { + "type": "variadic
", + "multi_result": true + } + ] } ], + "hasCallback": false, "types": { "LotteryInfo": { "type": "struct", "fields": [ + { + "name": "token_identifier", + "type": "TokenIdentifier" + }, { "name": "ticket_price", "type": "BigUint" @@ -149,14 +211,6 @@ "name": "prize_distribution", "type": "bytes" }, - { - "name": "whitelist", - "type": "List
" - }, - { - "name": "current_ticket_number", - "type": "u32" - }, { "name": "prize_pool", "type": "BigUint" diff --git a/src/testdata/lottery-esdt.wasm b/src/testdata/lottery-esdt.wasm new file mode 100755 index 00000000..1d031ace Binary files /dev/null and b/src/testdata/lottery-esdt.wasm differ diff --git a/src/testdata/lottery_egld.wasm b/src/testdata/lottery_egld.wasm deleted file mode 100755 index 1fd83f0f..00000000 Binary files a/src/testdata/lottery_egld.wasm and /dev/null differ diff --git a/src/testutils/mockProvider.ts b/src/testutils/mockProvider.ts index ee6ff0d4..b46b230c 100644 --- a/src/testutils/mockProvider.ts +++ b/src/testutils/mockProvider.ts @@ -15,6 +15,10 @@ import { NetworkStatus } from "../networkStatus"; import { TypedEvent } from "../events"; import { BalanceBuilder } from "../balanceBuilder"; import BigNumber from "bignumber.js"; +import { SmartContractResultItem, SmartContractResults } from "../smartcontracts"; + +const DummyHyperblockNonce = new Nonce(42); +const DummyHyperblockHash = new Hash("a".repeat(32)); /** * A mock {@link IProvider}, used for tests only. @@ -27,7 +31,8 @@ export class MockProvider implements IProvider { private readonly transactions: Map; private readonly onTransactionSent: TypedEvent<{ transaction: Transaction }>; private readonly accounts: Map; - private readonly queryResponders: QueryResponder[] = []; + private readonly queryContractResponders: QueryContractResponder[] = []; + private readonly getTransactionResponders: GetTransactionResponder[] = []; constructor() { this.transactions = new Map(); @@ -78,13 +83,23 @@ export class MockProvider implements IProvider { this.transactions.set(hash.toString(), item); } - mockQueryResponseOnFunction(functionName: string, response: QueryResponse) { + mockQueryContractOnFunction(functionName: string, response: QueryResponse) { let predicate = (query: Query) => query.func.name == functionName; - this.queryResponders.push(new QueryResponder(predicate, response)); + this.queryContractResponders.push(new QueryContractResponder(predicate, response)); } - mockQueryResponse(predicate: (query: Query) => boolean, response: QueryResponse) { - this.queryResponders.push(new QueryResponder(predicate, response)); + mockGetTransactionWithAnyHashAsNotarizedWithOneResult(returnCodeAndData: string) { + let contractResult = new SmartContractResultItem({ nonce: new Nonce(1), data: returnCodeAndData }); + + let predicate = (_hash: TransactionHash) => true; + let response = new TransactionOnNetwork({ + status: new TransactionStatus("executed"), + hyperblockNonce: DummyHyperblockNonce, + hyperblockHash: DummyHyperblockHash, + results: new SmartContractResults([contractResult]) + }); + + this.getTransactionResponders.unshift(new GetTransactionResponder(predicate, response)); } async mockTransactionTimeline(transaction: Transaction, timelinePoints: any[]): Promise { @@ -97,7 +112,7 @@ export class MockProvider implements IProvider { return this.mockTransactionTimelineByHash(transaction.getHash(), timelinePoints); } - async nextTransactionSent(): Promise { + private async nextTransactionSent(): Promise { return new Promise((resolve, _reject) => { this.onTransactionSent.on((eventArgs) => resolve(eventArgs.transaction)); }); @@ -115,12 +130,8 @@ export class MockProvider implements IProvider { }); } else if (point instanceof MarkNotarized) { this.mockUpdateTransaction(hash, (transaction) => { - transaction.hyperblockNonce = new Nonce(42); - transaction.hyperblockHash = new Hash("a".repeat(32)); - }); - } else if (point instanceof AddImmediateResult) { - this.mockUpdateTransaction(hash, (transaction) => { - transaction.getSmartContractResults().getImmediate().data = point.data; + transaction.hyperblockNonce = DummyHyperblockNonce; + transaction.hyperblockHash = DummyHyperblockHash; }); } else if (point instanceof Wait) { await timeline.start(point.milliseconds); @@ -175,6 +186,14 @@ export class MockProvider implements IProvider { _hintSender?: Address, _withResults?: boolean ): Promise { + // At first, try to use a mock responder + for (const responder of this.getTransactionResponders) { + if (responder.matches(txHash)) { + return responder.response; + } + } + + // Then, try to use the local collection of transactions let transaction = this.transactions.get(txHash.toString()); if (transaction) { return transaction; @@ -184,12 +203,8 @@ export class MockProvider implements IProvider { } async getTransactionStatus(txHash: TransactionHash): Promise { - let transaction = this.transactions.get(txHash.toString()); - if (transaction) { - return transaction.status; - } - - throw new errors.ErrMock("Transaction not found"); + let transaction = await this.getTransaction(txHash); + return transaction.status; } async getNetworkConfig(): Promise { @@ -201,7 +216,7 @@ export class MockProvider implements IProvider { } async queryContract(query: Query): Promise { - for (const responder of this.queryResponders) { + for (const responder of this.queryContractResponders) { if (responder.matches(query)) { return responder.response; } @@ -221,20 +236,22 @@ export class Wait { export class MarkNotarized { } -export class AddImmediateResult { - readonly data: string; +class QueryContractResponder { + readonly matches: (query: Query) => boolean; + readonly response: QueryResponse; - constructor(data: string) { - this.data = data; + constructor(matches: (query: Query) => boolean, response: QueryResponse) { + this.matches = matches; + this.response = response; } } -class QueryResponder { - readonly matches: (query: Query) => boolean; - readonly response: QueryResponse; +class GetTransactionResponder { + readonly matches: (hash: TransactionHash) => boolean; + readonly response: TransactionOnNetwork; - constructor(matches: (query: Query) => boolean, response: QueryResponse) { - this.matches = matches || ((_) => true); - this.response = response || new QueryResponse(); + constructor(matches: (hash: TransactionHash) => boolean, response: TransactionOnNetwork) { + this.matches = matches; + this.response = response; } } diff --git a/src/testutils/wallets.ts b/src/testutils/wallets.ts index 2992600e..8dfefc90 100644 --- a/src/testutils/wallets.ts +++ b/src/testutils/wallets.ts @@ -82,6 +82,10 @@ export class TestWallet { this.account = new Account(this.address); } + getAddress(): Address { + return this.address; + } + async sync(provider: IProvider) { await this.account.sync(provider); return this; diff --git a/src/transaction.dev.net.spec.ts b/src/transaction.dev.net.spec.ts index 9ff8183f..fe34c2b6 100644 --- a/src/transaction.dev.net.spec.ts +++ b/src/transaction.dev.net.spec.ts @@ -48,19 +48,16 @@ describe("test transactions on devnet", function () { transactionOnProxy.hyperblockNonce = new Nonce(0); transactionOnProxy.hyperblockHash = new Hash(""); - let immediateContractResultOnAPI: SmartContractResultItem = (transactionOnAPI).results.immediate; - let contractResultsOnAPI: SmartContractResultItem[] = (transactionOnAPI).results.items; - let resultingCallsOnAPI: SmartContractResultItem[] = (transactionOnAPI).results.resultingCalls; - let allContractResults = [immediateContractResultOnAPI].concat(resultingCallsOnAPI).concat(contractResultsOnAPI); + let contractResultsOnAPI: SmartContractResultItem[] = transactionOnAPI.results.getAll(); // Important issue (existing bug)! When working with TransactionOnNetwork objects, SCRs cannot be parsed correctly from API, only from Proxy. // On API response, base64 decode "data" from smart contract results: - for (const item of allContractResults) { + for (const item of contractResultsOnAPI) { item.data = Buffer.from(item.data, "base64").toString(); } // On API response, convert "callType" of smart contract results to a number: - for (const item of allContractResults) { + for (const item of contractResultsOnAPI) { item.callType = Number(item.callType); } diff --git a/src/transactionLogs.ts b/src/transactionLogs.ts index 7c55831b..f1ef9cbf 100644 --- a/src/transactionLogs.ts +++ b/src/transactionLogs.ts @@ -20,14 +20,19 @@ export class TransactionLogs { return new TransactionLogs(address, events); } - findEventByIdentifier(identifier: string) { - let event = this.events.filter(event => event.identifier == identifier)[0]; + requireEventByIdentifier(identifier: string): TransactionEvent { + let event = this.findEventByIdentifier(identifier); if (event) { return event; } throw new ErrTransactionEventNotFound(identifier); } + + findEventByIdentifier(identifier: string): TransactionEvent | undefined { + let event = this.events.filter(event => event.identifier == identifier)[0]; + return event; + } } export class TransactionEvent { diff --git a/src/transactionOnNetwork.ts b/src/transactionOnNetwork.ts index 6d913145..4cd75c20 100644 --- a/src/transactionOnNetwork.ts +++ b/src/transactionOnNetwork.ts @@ -32,9 +32,10 @@ export class TransactionOnNetwork { hyperblockNonce: Nonce = new Nonce(0); hyperblockHash: Hash = Hash.empty(); - private receipt: Receipt = new Receipt(); - private results: SmartContractResults = SmartContractResults.empty(); - private logs: TransactionLogs = TransactionLogs.empty(); + // TODO: Check if "receipt" is still received from the API. + receipt: Receipt = new Receipt(); + results: SmartContractResults = SmartContractResults.empty(); + logs: TransactionLogs = TransactionLogs.empty(); constructor(init?: Partial) { Object.assign(this, init); @@ -91,18 +92,6 @@ export class TransactionOnNetwork { getDateTime(): Date { return new Date(this.timestamp * 1000); } - - getReceipt(): Receipt { - return this.receipt; - } - - getSmartContractResults(): SmartContractResults { - return this.results; - } - - getLogs(): TransactionLogs { - return this.logs; - } } /** @@ -116,6 +105,7 @@ export class TransactionOnNetworkType { } } +// TODO: Check if we still need this. export class Receipt { value: Balance = Balance.Zero(); sender: Address = new Address();