diff --git a/CHANGELOG.md b/CHANGELOG.md index 138b78bb..50805122 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,24 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how ## [Unreleased] +- [Contract wrapper](https://github.com/ElrondNetwork/elrond-sdk-erdjs/pull/9) + - Added `ContractWrapper`, `SystemWrapper` - for more details check the pull request. + - Added support for constructors in ABIs + - Added ABIs: + - builtin functions + - ESDT system smart contract + - Added `BalanceBuilder` interface + - Added `NativeSerializer` + +Breaking changes: +- Changed how a provider is obtained: + - Removed `getDevnetProvider`, `getTestnetProvider`, `getMainnetProvider` + - Use added `chooseProvider` +- Renamed `ESDTToken` to `Token` +- Changed how test wallets (alice, bob, carol etc.) are obtained, added all 12 test wallets: + - Removed `TestWallets` + - Use added function `loadTestWallets` (or `loadInteractive`) + ## [6.5.2] - [Bugfix - corrected compute hash for transaction with options #44](https://github.com/ElrondNetwork/elrond-sdk-erdjs/pull/44) diff --git a/abi/builtinFunctions.abi.json b/abi/builtinFunctions.abi.json new file mode 100644 index 00000000..5fa21221 --- /dev/null +++ b/abi/builtinFunctions.abi.json @@ -0,0 +1,109 @@ +{ + "name": "builtinFunctions", + "endpoints": [ + { + "name": "ESDTTransfer", + "inputs": [ + { + "name": "tokenName", + "type": "bytes" + }, + { + "name": "value", + "type": "BigUint" + }, + { + "name": "functionAndArguments", + "type": "variadic", + "multi_arg": true + } + ], + "outputs": [] + }, + { + "name": "ESDTNFTTransfer", + "inputs": [ + { + "name": "tokenName", + "type": "bytes" + }, + { + "name": "nonce", + "type": "U64" + }, + { + "name": "quantity", + "type": "BigUint" + }, + { + "name": "destination", + "type": "Address" + }, + { + "name": "functionAndArguments", + "type": "variadic", + "multi_arg": true + } + ], + "outputs": [] + }, + { + "name": "ESDTNFTCreate", + "inputs": [ + { + "name": "tokenIdentifier", + "type": "bytes" + }, + { + "name": "initialQuantity", + "type": "BigUint" + }, + { + "name": "NFTName", + "type": "bytes" + }, + { + "name": "royalties", + "type": "BigUint" + }, + { + "name": "hash", + "type": "bytes" + }, + { + "name": "attributes", + "type": "bytes" + }, + { + "name": "URIs", + "type": "variadic", + "multi_arg": true + } + ], + "outputs": [ + { + "name": "tokenNonce", + "type": "BigUint" + } + ] + }, + { + "name": "ESDTNFTAddQuantity", + "inputs": [ + { + "name": "tokenIdentifier", + "type": "bytes" + }, + { + "name": "tokenNonce", + "type": "BigUint" + }, + { + "name": "amount", + "type": "BigUint" + } + ], + "outputs": [] + } + ] +} diff --git a/abi/esdtSystemContract.abi.json b/abi/esdtSystemContract.abi.json new file mode 100644 index 00000000..660b3971 --- /dev/null +++ b/abi/esdtSystemContract.abi.json @@ -0,0 +1,384 @@ +{ + "name": "esdt", + "endpoints": [ + { + "name": "issue", + "payableInTokens": [ + "EGLD" + ], + "inputs": [ + { + "name": "tokenName", + "type": "bytes" + }, + { + "name": "tickerName", + "type": "bytes" + }, + { + "name": "initialSupply", + "type": "BigUint" + }, + { + "name": "numOfDecimals", + "type": "u32" + }, + { + "name": "properties", + "type": "variadic>", + "multi_arg": true + } + ], + "outputs": [ + { + "name": "tokenIdentifier", + "type": "bytes" + } + ] + }, + { + "name": "issueSemiFungible", + "payableInTokens": [ + "EGLD" + ], + "inputs": [ + { + "name": "tokenName", + "type": "bytes" + }, + { + "name": "tickerName", + "type": "bytes" + }, + { + "name": "properties", + "type": "variadic>", + "multi_arg": true + } + ], + "outputs": [ + { + "name": "tokenIdentifier", + "type": "bytes" + } + ] + }, + { + "name": "issueNonFungible", + "payableInTokens": [ + "EGLD" + ], + "inputs": [ + { + "name": "tokenName", + "type": "bytes" + }, + { + "name": "tickerName", + "type": "bytes" + }, + { + "name": "properties", + "type": "variadic>", + "multi_arg": true + } + ], + "outputs": [ + { + "name": "tokenIdentifier", + "type": "bytes" + } + ] + }, + { + "name": "ESDTBurn", + "inputs": [ + { + "name": "tokenName", + "type": "bytes" + }, + { + "name": "burnAmount", + "type": "BigUint" + } + ], + "outputs": [] + }, + { + "name": "mint", + "inputs": [ + { + "name": "tokenName", + "type": "bytes" + }, + { + "name": "mintAmount", + "type": "BigUint" + }, + { + "name": "mintedTokensOwner", + "type": "optional
" + } + ], + "outputs": [] + }, + { + "name": "freeze", + "inputs": [ + { + "name": "tokenName", + "type": "bytes" + }, + { + "name": "addressToFreezeFor", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "unFreeze", + "inputs": [ + { + "name": "tokenName", + "type": "bytes" + }, + { + "name": "addressToUnFreezeFor", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "wipe", + "inputs": [ + { + "name": "tokenName", + "type": "bytes" + }, + { + "name": "addressToWipeFor", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "pause", + "inputs": [ + { + "name": "tokenName", + "type": "bytes" + } + ], + "outputs": [] + }, + { + "name": "unPause", + "inputs": [ + { + "name": "tokenName", + "type": "bytes" + } + ], + "outputs": [] + }, + { + "name": "claim", + "inputs": [], + "outputs": [] + }, + { + "name": "configChange", + "inputs": [ + { + "name": "ownerAddress", + "type": "Address" + }, + { + "name": "baseIssuingCost", + "type": "BigUint" + }, + { + "name": "minTokenNameLength", + "type": "u32" + }, + { + "name": "maxTokenNameLength", + "type": "u32" + } + ], + "outputs": [] + }, + { + "name": "controlChanges", + "inputs": [ + { + "name": "tokenName", + "type": "bytes" + }, + { + "name": "properties", + "type": "variadic>", + "multi_arg": true + } + ], + "outputs": [] + }, + { + "name": "transferOwnership", + "inputs": [ + { + "name": "tokenName", + "type": "bytes" + }, + { + "name": "newOwner", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "getTokenProperties", + "comment": "Properties have the format: Name-value, ex: NumDecimals-5, IsPaused-true", + "inputs": [ + { + "name": "tokenName", + "type": "bytes" + } + ], + "outputs": [ + { + "name": "tokenName", + "type": "bytes" + }, + { + "name": "tokenType", + "type": "bytes" + }, + { + "name": "ownerAddress", + "type": "Address" + }, + { + "name": "totalMinted", + "type": "bytes" + }, + { + "name": "totalBurned", + "type": "bytes" + }, + { + "name": "numDecimals", + "type": "bytes" + }, + { + "name": "isPaused", + "type": "bytes" + }, + { + "name": "canUpgrade", + "type": "bytes" + }, + { + "name": "canMint", + "type": "bytes" + }, + { + "name": "canBurn", + "type": "bytes" + }, + { + "name": "canChangeOwner", + "type": "bytes" + }, + { + "name": "canPause", + "type": "bytes" + }, + { + "name": "canFreeze", + "type": "bytes" + }, + { + "name": "canWipe", + "type": "bytes" + }, + { + "name": "canAddSpecialRoles", + "type": "bytes" + }, + { + "name": "canTransferNftCreateRole", + "type": "bytes" + }, + { + "name": "nftCreateStopped", + "type": "bytes" + }, + { + "name": "numWiped", + "type": "bytes" + } + ] + }, + { + "name": "getSpecialRoles", + "inputs": [ + { + "name": "tokenName", + "type": "bytes" + } + ], + "outputs": [ + { + "name": "addressesWithRoles", + "type": "variadic" + } + ] + }, + { + "name": "setSpecialRole", + "inputs": [ + { + "name": "tokenName", + "type": "bytes" + }, + { + "name": "addressToSetRolesFor", + "type": "Address" + }, + { + "name": "roles", + "type": "variadic", + "multi_arg": true + } + ], + "outputs": [] + }, + { + "name": "getContractConfig", + "inputs": [], + "outputs": [ + { + "name": "ownerAddress", + "type": "Address" + }, + { + "name": "baseIssuingCost", + "type": "BigUint" + }, + { + "name": "minTokenNameLength", + "type": "BigUint" + }, + { + "name": "maxTokenNameLength", + "type": "BigUint" + } + ] + } + ] +} diff --git a/abi/sendWrapper.abi.json b/abi/sendWrapper.abi.json new file mode 100644 index 00000000..200d9a42 --- /dev/null +++ b/abi/sendWrapper.abi.json @@ -0,0 +1,13 @@ +{ + "name": "send", + "endpoints": [ + { + "name": "", + "payableInTokens": [ + "*" + ], + "inputs": [], + "outputs": [] + } + ] +} diff --git a/package.json b/package.json index 69f4eb4a..bfc77ab2 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,11 @@ "compile-browser-min": "tsc -p tsconfig.json && browserify out/index.js -o out-browser/erdjs.min.js --standalone erdjs -p tinyify", "compile-browser-nowallet": "tsc -p tsconfig.json && browserify out/index.js -o out-browser/erdjs.nowallet.js --standalone erdjs --exclude=**/walletcore/**", "compile-browser-nowallet-min": "tsc -p tsconfig.json && browserify out/index.js -o out-browser/erdjs.nowallet.min.js --standalone erdjs --exclude=**/walletcore/** -p tinyify", - "compile": "tsc -p tsconfig.json", + "compile": "tsc -p tsconfig.json && npm run copy-files", "compile-proto": "npx pbjs -t static-module -w commonjs -o src/proto/compiled.js src/proto/transaction.proto && npx pbts -o src/proto/compiled.d.ts src/proto/compiled.js", + "copy-files": "mkdir -p out/testutils/ && cp -R src/testutils/testwallets out/testutils/ && mkdir -p out/abi/ && cp -R abi/ out/abi/", "browser-tests": "make clean && make browser-tests && http-server --port=9876 -o browser-tests/index.html", + "lint": "tslint --project .", "pretest": "npm run compile", "prepare": "npm run compile && npm run compile-browser && npm run compile-browser-min && npm run compile-browser-nowallet && npm run compile-browser-nowallet-min" }, diff --git a/src/account.ts b/src/account.ts index 88b7d484..eafdbc6c 100644 --- a/src/account.ts +++ b/src/account.ts @@ -2,120 +2,121 @@ import { IProvider } from "./interface"; import { Address } from "./address"; import { Nonce } from "./nonce"; import { Balance } from "./balance"; +import { Egld } from "./balanceBuilder"; /** * An abstraction representing an account (user or Smart Contract) on the Network. */ export class Account { - /** - * The address of the account. - */ - readonly address: Address = new Address(); - - /** - * The nonce of the account (the account sequence number). - */ - nonce: Nonce = new Nonce(0); - - /** - * The balance of the account. - */ - balance: Balance = new Balance("0"); - - private asOnNetwork: AccountOnNetwork = new AccountOnNetwork(); - - /** - * Creates an account object from an address - */ - constructor(address: Address) { - this.address = address; - } - - /** - * Queries the details of the account on the Network - * @param provider the Network provider - * @param cacheLocally whether to save the query response within the object, locally - */ - async getAsOnNetwork(provider: IProvider, cacheLocally: boolean = true): Promise { - this.address.assertNotEmpty(); - - let response = await provider.getAccount(this.address); - - if (cacheLocally) { - this.asOnNetwork = response; + /** + * The address of the account. + */ + readonly address: Address = new Address(); + + /** + * The nonce of the account (the account sequence number). + */ + nonce: Nonce = new Nonce(0); + + /** + * The balance of the account. + */ + balance: Balance = Egld("0"); + + private asOnNetwork: AccountOnNetwork = new AccountOnNetwork(); + + /** + * Creates an account object from an address + */ + constructor(address: Address) { + this.address = address; } - return response; - } - - /** - * Gets a previously saved query response - */ - getAsOnNetworkCached(): AccountOnNetwork { - return this.asOnNetwork; - } - - /** - * Synchronizes account properties (such as nonce, balance) with the ones queried from the Network - * @param provider the Network provider - */ - async sync(provider: IProvider) { - await this.getAsOnNetwork(provider, true); - this.nonce = this.asOnNetwork.nonce; - this.balance = this.asOnNetwork.balance; - } - - /** - * Increments (locally) the nonce (the account sequence number). - */ - incrementNonce() { - this.nonce = this.nonce.increment(); - } - - /** - * Gets then increments (locally) the nonce (the account sequence number). - */ - getNonceThenIncrement(): Nonce { - let nonce = this.nonce; - this.nonce = this.nonce.increment(); - return nonce; - } - - /** - * Converts the account to a pretty, plain JavaScript object. - */ - toJSON(): any { - return { - address: this.address.bech32(), - nonce: this.nonce.valueOf(), - balance: this.balance.toString(), - }; - } + /** + * Queries the details of the account on the Network + * @param provider the Network provider + * @param cacheLocally whether to save the query response within the object, locally + */ + async getAsOnNetwork(provider: IProvider, cacheLocally: boolean = true): Promise { + this.address.assertNotEmpty(); + + let response = await provider.getAccount(this.address); + + if (cacheLocally) { + this.asOnNetwork = response; + } + + return response; + } + + /** + * Gets a previously saved query response + */ + getAsOnNetworkCached(): AccountOnNetwork { + return this.asOnNetwork; + } + + /** + * Synchronizes account properties (such as nonce, balance) with the ones queried from the Network + * @param provider the Network provider + */ + async sync(provider: IProvider) { + await this.getAsOnNetwork(provider, true); + this.nonce = this.asOnNetwork.nonce; + this.balance = this.asOnNetwork.balance; + } + + /** + * Increments (locally) the nonce (the account sequence number). + */ + incrementNonce() { + this.nonce = this.nonce.increment(); + } + + /** + * Gets then increments (locally) the nonce (the account sequence number). + */ + getNonceThenIncrement(): Nonce { + let nonce = this.nonce; + this.nonce = this.nonce.increment(); + return nonce; + } + + /** + * Converts the account to a pretty, plain JavaScript object. + */ + toJSON(): any { + return { + address: this.address.bech32(), + nonce: this.nonce.valueOf(), + balance: this.balance.toString(), + }; + } } /** * A plain view of an account, as queried from the Network. */ export class AccountOnNetwork { - address: Address = new Address(); - nonce: Nonce = new Nonce(0); - balance: Balance = new Balance("0"); - code: string = ""; - userName: string = ""; - - constructor(init?: Partial) { - Object.assign(this, init); - } - - static fromHttpResponse(payload: any): AccountOnNetwork { - let result = new AccountOnNetwork(); - - result.address = new Address(payload["address"] || 0); - result.nonce = new Nonce(payload["nonce"] || 0); - result.balance = Balance.fromString(payload["balance"]); - result.code = payload["code"]; - result.userName = payload["username"]; - - return result; - } + address: Address = new Address(); + nonce: Nonce = new Nonce(0); + balance: Balance = Egld(0); + code: string = ""; + userName: string = ""; + + constructor(init?: Partial) { + Object.assign(this, init); + } + + static fromHttpResponse(payload: any): AccountOnNetwork { + let result = new AccountOnNetwork(); + + result.address = new Address(payload["address"] || 0); + result.nonce = new Nonce(payload["nonce"] || 0); + result.balance = Balance.fromString(payload["balance"]); + result.code = payload["code"]; + result.userName = payload["username"]; + + return result; + } } diff --git a/src/apiProvider.ts b/src/apiProvider.ts index d69d6461..5e3fbc7b 100644 --- a/src/apiProvider.ts +++ b/src/apiProvider.ts @@ -6,7 +6,7 @@ import { NetworkStake } from "./networkStake"; import { Stats } from "./stats"; import { TransactionHash } from "./transaction"; import { TransactionOnNetwork } from "./transactionOnNetwork"; -import { ESDTToken } from "./esdtToken"; +import { Token } from "./token"; import { NFTToken } from "./nftToken"; const JSONbig = require("json-bigint"); @@ -14,86 +14,86 @@ const JSONbig = require("json-bigint"); * This is a temporary change, this will be the only provider used, ProxyProvider will be deprecated */ export class ApiProvider implements IApiProvider { - private url: string; - private config: AxiosRequestConfig; + private url: string; + private config: AxiosRequestConfig; - /** - * Creates a new ApiProvider. - * @param url the URL of the Elrond Api - * @param config axios request config options - */ - constructor(url: string, config?: AxiosRequestConfig) { - this.url = url; - this.config = config || { - timeout: 1000, - }; -} + /** + * Creates a new ApiProvider. + * @param url the URL of the Elrond Api + * @param config axios request config options + */ + constructor(url: string, config?: AxiosRequestConfig) { + this.url = url; + this.config = config || { + timeout: 1000, + }; + } - /** - * Fetches the Network Stake. - */ - async getNetworkStake(): Promise { - return this.doGetGeneric("stake", (response) => NetworkStake.fromHttpResponse(response)); - } + /** + * Fetches the Network Stake. + */ + async getNetworkStake(): Promise { + return this.doGetGeneric("stake", (response) => NetworkStake.fromHttpResponse(response)); + } - /** - * Fetches the Network Stats. - */ - async getNetworkStats(): Promise { - return this.doGetGeneric("stats", (response) => Stats.fromHttpResponse(response)); - } + /** + * Fetches the Network Stats. + */ + async getNetworkStats(): Promise { + return this.doGetGeneric("stats", (response) => Stats.fromHttpResponse(response)); + } - /** - * Fetches the state of a {@link Transaction}. - */ - async getTransaction(txHash: TransactionHash): Promise { - return this.doGetGeneric(`transactions/${txHash.toString()}`, (response) => - TransactionOnNetwork.fromHttpResponse(response) - ); - } + /** + * Fetches the state of a {@link Transaction}. + */ + async getTransaction(txHash: TransactionHash): Promise { + return this.doGetGeneric(`transactions/${txHash.toString()}`, (response) => + TransactionOnNetwork.fromHttpResponse(response) + ); + } - async getESDTToken(tokenIdentifier: string): Promise { - return this.doGetGeneric(`tokens/${tokenIdentifier}`, (response) => ESDTToken.fromHttpResponse(response)); - } + async getToken(tokenIdentifier: string): Promise { + return this.doGetGeneric(`tokens/${tokenIdentifier}`, (response) => Token.fromHttpResponse(response)); + } - async getNFTToken(tokenIdentifier: string): Promise { - return this.doGetGeneric(`nfts/${tokenIdentifier}`, (response) => NFTToken.fromHttpResponse(response)); - } + async getNFTToken(tokenIdentifier: string): Promise { + return this.doGetGeneric(`nfts/${tokenIdentifier}`, (response) => NFTToken.fromHttpResponse(response)); + } - /** - * Get method that receives the resource url and on callback the method used to map the response. - */ - async doGetGeneric(resourceUrl: string, callback: (response: any) => any): Promise { - let response = await this.doGet(resourceUrl); - return callback(response); - } + /** + * Get method that receives the resource url and on callback the method used to map the response. + */ + async doGetGeneric(resourceUrl: string, callback: (response: any) => any): Promise { + let response = await this.doGet(resourceUrl); + return callback(response); + } - private async doGet(resourceUrl: string): Promise { - try { - let url = `${this.url}/${resourceUrl}`; - let response = await axios.get(url, this.config); + private async doGet(resourceUrl: string): Promise { + try { + let url = `${this.url}/${resourceUrl}`; + let response = await axios.get(url, this.config); - return response.data; - } catch (error) { - this.handleApiError(error, resourceUrl); + return response.data; + } catch (error) { + this.handleApiError(error, resourceUrl); + } } - } - private handleApiError(error: any, resourceUrl: string) { - if (!error.response) { - Logger.warn(error); - throw new errors.ErrApiProviderGet(resourceUrl, error.toString(), error); - } + private handleApiError(error: any, resourceUrl: string) { + if (!error.response) { + Logger.warn(error); + throw new errors.ErrApiProviderGet(resourceUrl, error.toString(), error); + } - let errorData = error.response.data; - let originalErrorMessage = errorData.error || errorData.message || JSON.stringify(errorData); - throw new errors.ErrApiProviderGet(resourceUrl, originalErrorMessage, error); - } + let errorData = error.response.data; + let originalErrorMessage = errorData.error || errorData.message || JSON.stringify(errorData); + throw new errors.ErrApiProviderGet(resourceUrl, originalErrorMessage, error); + } } // See: https://github.com/axios/axios/issues/983 axios.defaults.transformResponse = [ - function(data) { - return JSONbig.parse(data); - }, + function (data) { + return JSONbig.parse(data); + }, ]; diff --git a/src/balance.spec.ts b/src/balance.spec.ts index c51705df..327b5bb3 100644 --- a/src/balance.spec.ts +++ b/src/balance.spec.ts @@ -1,5 +1,6 @@ import { assert } from "chai"; import { Balance } from "./balance"; +import { Egld } from "./balanceBuilder"; describe("test balance", () => { it("should have desired precision", () => { @@ -27,7 +28,10 @@ describe("test balance", () => { assert.equal(Balance.egld("0.123456789123456789777777888888").toCurrencyString(), "0.123456789123456789 EGLD"); }); - it("should format as denominated", () => { - assert.equal(new Balance('1000000000').toDenominated(), "0.000000001000000000"); + it("test Egld builder", () => { + assert.equal(Egld(3.14).toDenominated(), "3.140000000000000000"); + assert.equal(Egld(0.01).toDenominated(), "0.010000000000000000"); + assert.equal(Egld.raw('5000000000000000042').toDenominated(), "5.000000000000000042"); + assert.equal(Egld.raw('1000000000').toDenominated(), "0.000000001000000000"); }); }); diff --git a/src/balance.ts b/src/balance.ts index 0e54cbc4..17a9407e 100644 --- a/src/balance.ts +++ b/src/balance.ts @@ -1,72 +1,62 @@ -import * as errors from "./errors"; import { BigNumber } from "bignumber.js"; +import { Token } from "./token"; +import { ErrInvalidArgument } from "./errors"; +import { Egld } from "./balanceBuilder"; /** - * The base used for toString methods to avoid exponential notation + * The number of decimals handled when working with EGLD or ESDT values. */ -const BASE_10 = 10; +const DEFAULT_BIGNUMBER_DECIMAL_PLACES = 18; -/** - * The number of decimals handled when working with EGLD values. - */ -const DENOMINATION = 18; - -/** - * One EGLD, in its big-integer form (as a string). - */ -const OneEGLDString = "1000000000000000000"; - -const EGLDTicker = "EGLD"; -BigNumber.set({ DECIMAL_PLACES: DENOMINATION, ROUNDING_MODE: 1 }); +BigNumber.set({ DECIMAL_PLACES: DEFAULT_BIGNUMBER_DECIMAL_PLACES, ROUNDING_MODE: 1 }); /** * Balance, as an immutable object. - * TODO: Re-design, perhaps as new Money(value, currency), with new Currency(denomination, ticker). */ export class Balance { + readonly token: Token; + private readonly nonce: BigNumber = new BigNumber(0); private readonly value: BigNumber = new BigNumber(0); /** * Creates a Balance object. */ - public constructor(value: string) { + public constructor(token: Token, nonce: BigNumber.Value, value: BigNumber.Value) { + this.token = token; + this.nonce = new BigNumber(nonce); this.value = new BigNumber(value); - - if (this.value.isNegative()) { - throw new errors.ErrBalanceInvalid(this.value); - } } /** * Creates a balance object from an EGLD value (denomination will be applied). */ - static egld(value: any): Balance { - let bigGold = new BigNumber(value); - let bigUnits = bigGold.multipliedBy(new BigNumber(OneEGLDString)); - let bigUnitsString = bigUnits.integerValue().toString(BASE_10); - - return new Balance(bigUnitsString); + static egld(value: BigNumber.Value): Balance { + return Egld(value); } /** * Creates a balance object from a string (with denomination included). */ static fromString(value: string): Balance { - return new Balance(value || "0"); + return Egld.raw(value || "0"); } /** - * Creates a zero-valued balance object. + * Creates a zero-valued EGLD balance object. */ static Zero(): Balance { - return new Balance('0'); + return Egld(0); } isZero(): boolean { return this.value.isZero(); } + isEgld(): boolean { + return this.token.isEgld(); + } + isSet(): boolean { return !this.isZero(); } @@ -75,22 +65,18 @@ export class Balance { * Returns the string representation of the value (as EGLD currency). */ toCurrencyString(): string { - let denominated = this.toDenominated(); - return `${denominated} ${EGLDTicker}`; + return `${this.toDenominated()} ${this.token.getTokenIdentifier()}`; } toDenominated(): string { - let padded = this.toString().padStart(DENOMINATION, "0"); - let decimals = padded.slice(-DENOMINATION); - let integer = padded.slice(0, padded.length - DENOMINATION) || 0; - return `${integer}.${decimals}`; + return this.value.shiftedBy(-this.token.decimals).toFixed(this.token.decimals); } /** * Returns the string representation of the value (its big-integer form). */ toString(): string { - return this.value.toString(BASE_10); + return this.value.toFixed(); } /** @@ -103,7 +89,35 @@ export class Balance { }; } + getNonce(): BigNumber { + return this.nonce; + } + valueOf(): BigNumber { return this.value; } + + plus(other: Balance) { + this.checkSameToken(other); + return new Balance(this.token, this.nonce, this.value.plus(other.value)); + } + + minus(other: Balance) { + this.checkSameToken(other); + return new Balance(this.token, this.nonce, this.value.minus(other.value)); + } + + isEqualTo(other: Balance) { + this.checkSameToken(other); + return this.value.isEqualTo(other.value); + } + + checkSameToken(other: Balance) { + if (this.token != other.token) { + throw new ErrInvalidArgument("Different token types"); + } + if (!this.nonce.isEqualTo(other.nonce)) { + throw new ErrInvalidArgument("Different nonces"); + } + } } diff --git a/src/balanceBuilder.ts b/src/balanceBuilder.ts new file mode 100644 index 00000000..d5c8abd6 --- /dev/null +++ b/src/balanceBuilder.ts @@ -0,0 +1,144 @@ +import BigNumber from "bignumber.js"; +import { Balance, ErrInvariantFailed } from "."; +import { ErrInvalidArgument } from "./errors"; +import { Token, TokenType } from "./token"; + +/** + * Creates balances for ESDTs (Fungible, Semi-Fungible (SFT) or Non-Fungible Tokens). + */ +export interface BalanceBuilder { + + /** + * Creates a balance. Identical to {@link BalanceBuilder.value} + */ + (value: BigNumber.Value): Balance; + + /** + * Creates a denominated balance. + * Note: For SFTs and NFTs this is equivalent to the raw balance, since SFTs and NFTs have 0 decimals. + */ + value(value: BigNumber.Value): Balance; + + /** + * Creates a balance. Does not apply denomination. + */ + raw(value: BigNumber.Value): Balance; + + /** + * Creates a new balance builder with the given nonce. + */ + nonce(nonce: BigNumber.Value): BalanceBuilder; + + /** + * Sets the nonce. Modifies the current instance. + */ + setNonce(nonce: BigNumber.Value): void; + + /* + * Get the nonce for an SFT or NFT builder. + */ + getNonce(): BigNumber; + + /* + * Returns true if the nonce was specified. + */ + hasNonce(): boolean; + + /* + * Get the token. + */ + getToken(): Token; + + /* + * Get the token identifier. + */ + getTokenIdentifier(): string; + + /** + * Creates a balance of value 1. Useful after specifying the nonce of an NFT. + */ + one(): Balance; +} + +class BalanceBuilderImpl { + readonly token: Token; + nonce_: BigNumber | null; + constructor(token: Token) { + this.token = token; + this.nonce_ = null; + if (token.isFungible()) { + this.setNonce(0); + } + } + + value(value: BigNumber.Value): Balance { + value = applyDenomination(value, this.token.decimals); + return new Balance(this.token, this.getNonce(), value); + } + + raw(value: BigNumber.Value): Balance { + return new Balance(this.token, this.getNonce(), value); + } + + nonce(nonce: BigNumber.Value): BalanceBuilder { + let builder = createBalanceBuilder(this.token); + builder.setNonce(nonce); + return builder; + } + + setNonce(nonce: BigNumber.Value): void { + this.nonce_ = new BigNumber(nonce); + } + + one(): Balance { + return this.value(1); + } + + hasNonce(): boolean { + return this.token.isFungible() || this.nonce_ != null; + } + + getNonce(): BigNumber.Value { + if (this.nonce_ == null) { + throw new ErrInvariantFailed("Nonce was not provided"); + } + return new BigNumber(this.nonce_); + } + + getToken(): Token { + return this.token; + } + + getTokenIdentifier(): string { + return this.getToken().getTokenIdentifier(); + } +} + +export function createBalanceBuilder(token: Token): BalanceBuilder { + let impl = new BalanceBuilderImpl(token); + let denominated = impl.value.bind(impl); + let others = { + value: impl.value.bind(impl), + raw: impl.raw.bind(impl), + nonce: impl.nonce.bind(impl), + setNonce: impl.setNonce.bind(impl), + one: impl.one.bind(impl), + hasNonce: impl.hasNonce.bind(impl), + getNonce: impl.getNonce.bind(impl), + getToken: impl.getToken.bind(impl), + getTokenIdentifier: impl.getTokenIdentifier.bind(impl) + }; + return Object.assign(denominated, others); +} + +/** + * Builder for an EGLD value. + */ +export const Egld = createBalanceBuilder(new Token({ identifier: "EGLD", name: "eGold", decimals: 18, type: TokenType.Fungible })); + +function applyDenomination(value: BigNumber.Value, decimals: number): BigNumber { + if (decimals < 0) { + throw new ErrInvalidArgument("The number of decimals must be positive"); + } + return new BigNumber(value).shiftedBy(decimals).decimalPlaces(0); +} diff --git a/src/dapp/walletConnectProvider.ts b/src/dapp/walletConnectProvider.ts index 27f48f82..66327144 100644 --- a/src/dapp/walletConnectProvider.ts +++ b/src/dapp/walletConnectProvider.ts @@ -78,7 +78,7 @@ export class WalletConnectProvider implements IDappProvider { } await this.walletConnector?.createSession({ chainId: WALLETCONNECT_ELROND_CHAIN_ID }); - if (!this.walletConnector?.uri) return ""; + if (!this.walletConnector?.uri) { return ""; } return this.walletConnector?.uri; } @@ -200,7 +200,7 @@ export class WalletConnectProvider implements IDappProvider { accounts: [account], } = params[0]; - this.loginAccount(account); + let _ = this.loginAccount(account); } private async onDisconnect(error: any) { diff --git a/src/dapp/walletProvider.ts b/src/dapp/walletProvider.ts index ad11a533..d8663ac7 100644 --- a/src/dapp/walletProvider.ts +++ b/src/dapp/walletProvider.ts @@ -1,4 +1,4 @@ -import {IDappProvider, IDappMessageEvent} from "./interface"; +import { IDappProvider, IDappMessageEvent } from "./interface"; import { DAPP_MESSAGE_INIT, DAPP_DEFAULT_TIMEOUT, @@ -9,10 +9,10 @@ import { DAPP_MESSAGE_SIGN_TRANSACTION_URL, DAPP_MESSAGE_LOG_OUT } from "./constants"; -import {mainFrameStyle} from "./dom"; -import {Transaction} from "../transaction"; -import {SignableMessage} from "../signableMessage"; -import {ErrNotImplemented} from "../errors"; +import { mainFrameStyle } from "./dom"; +import { Transaction } from "../transaction"; +import { SignableMessage } from "../signableMessage"; +import { ErrNotImplemented } from "../errors"; export class WalletProvider implements IDappProvider { walletUrl: string; @@ -25,7 +25,7 @@ export class WalletProvider implements IDappProvider { constructor(walletURL: string = '') { this.walletUrl = walletURL; this.attachMainFrame(); - this.init().then(); + let _ = this.init(); } /** @@ -58,7 +58,7 @@ export class WalletProvider implements IDappProvider { return false; } - const {contentWindow} = this.mainFrame; + const { contentWindow } = this.mainFrame; if (!contentWindow) { return false; } @@ -79,7 +79,7 @@ export class WalletProvider implements IDappProvider { return; } - const {data} = ev; + const { data } = ev; if (data.type !== DAPP_MESSAGE_IS_CONNECTED) { return; } @@ -96,12 +96,12 @@ export class WalletProvider implements IDappProvider { /** * Fetches the login hook url and redirects the client to the wallet login. */ - async login(options?:{callbackUrl?:string; token?:string}): Promise { + async login(options?: { callbackUrl?: string; token?: string }): Promise { if (!this.mainFrame) { return ''; } - const {contentWindow} = this.mainFrame; + const { contentWindow } = this.mainFrame; if (!contentWindow) { console.warn("something went wrong, main wallet iframe does not contain a contentWindow"); return ''; @@ -122,7 +122,7 @@ export class WalletProvider implements IDappProvider { return; } - const {data} = ev; + const { data } = ev; if (data.type !== DAPP_MESSAGE_CONNECT_URL) { return; } @@ -136,11 +136,11 @@ export class WalletProvider implements IDappProvider { }).then((connectionUrl: string) => { let callbackUrl = `callbackUrl=${window.location.href}`; if (options && options.callbackUrl) { - callbackUrl = `callbackUrl=${options.callbackUrl}`; + callbackUrl = `callbackUrl=${options.callbackUrl}`; } let token = ''; if (options && options.token) { - token = `&token=${options.token}`; + token = `&token=${options.token}`; } window.location.href = `${this.baseWalletUrl()}${connectionUrl}?${callbackUrl}${token}`; return window.location.href; @@ -149,15 +149,15 @@ export class WalletProvider implements IDappProvider { }); } - /** - * Fetches the logout hook url and redirects the client to the wallet logout. - */ + /** + * Fetches the logout hook url and redirects the client to the wallet logout. + */ async logout(): Promise { if (!this.mainFrame) { return false; } - const {contentWindow} = this.mainFrame; + const { contentWindow } = this.mainFrame; if (!contentWindow) { console.warn("something went wrong, main wallet iframe does not contain a contentWindow"); return false; @@ -178,7 +178,7 @@ export class WalletProvider implements IDappProvider { return; } - const {data} = ev; + const { data } = ev; if (data.type !== DAPP_MESSAGE_LOG_OUT) { return; } @@ -189,7 +189,7 @@ export class WalletProvider implements IDappProvider { }; window.addEventListener('message', logout); - }) + }); } /** @@ -200,7 +200,7 @@ export class WalletProvider implements IDappProvider { return ''; } - const {contentWindow} = this.mainFrame; + const { contentWindow } = this.mainFrame; if (!contentWindow) { return ''; } @@ -220,7 +220,7 @@ export class WalletProvider implements IDappProvider { return; } - const {data} = ev; + const { data } = ev; if (data.type !== DAPP_MESSAGE_GET_ADDRESS) { return; } @@ -240,12 +240,12 @@ export class WalletProvider implements IDappProvider { * @param transaction * @param options */ - async sendTransaction(transaction: Transaction, options?: {callbackUrl?: string}): Promise { + async sendTransaction(transaction: Transaction, options?: { callbackUrl?: string }): Promise { if (!this.mainFrame) { throw new Error("Wallet provider is not initialised, call init() first"); } - const {contentWindow} = this.mainFrame; + const { contentWindow } = this.mainFrame; if (!contentWindow) { throw new Error("Wallet provider is not initialised, call init() first"); } @@ -269,7 +269,7 @@ export class WalletProvider implements IDappProvider { return; } - const {data} = ev; + const { data } = ev; if (data.type !== DAPP_MESSAGE_SEND_TRANSACTION_URL) { return; } @@ -297,12 +297,12 @@ export class WalletProvider implements IDappProvider { * @param transaction * @param options */ - async signTransaction(transaction: Transaction, options?: {callbackUrl?: string}): Promise { + async signTransaction(transaction: Transaction, options?: { callbackUrl?: string }): Promise { if (!this.mainFrame) { throw new Error("Wallet provider is not initialised, call init() first"); } - const {contentWindow} = this.mainFrame; + const { contentWindow } = this.mainFrame; if (!contentWindow) { throw new Error("Wallet provider is not initialised, call init() first"); } @@ -326,7 +326,7 @@ export class WalletProvider implements IDappProvider { return; } - const {data} = ev; + const { data } = ev; if (data.type !== DAPP_MESSAGE_SIGN_TRANSACTION_URL) { return; } @@ -369,7 +369,7 @@ export class WalletProvider implements IDappProvider { return plainTransaction; } - private async waitForRemote(): Promise{ + private async waitForRemote(): Promise { return new Promise((resolve, reject) => { const timeout = setTimeout(_ => reject(false), DAPP_DEFAULT_TIMEOUT); const setConnected = (ev: IDappMessageEvent) => { @@ -380,7 +380,7 @@ export class WalletProvider implements IDappProvider { if (!this.isValidWalletSource(ev.origin)) { return; } - const {data} = ev; + const { data } = ev; if (data.type !== DAPP_MESSAGE_INIT) { return; } @@ -416,7 +416,7 @@ export class WalletProvider implements IDappProvider { } private baseWalletUrl(): string { - const pathArray = this.walletUrl.split( '/' ); + const pathArray = this.walletUrl.split('/'); const protocol = pathArray[0]; const host = pathArray[2]; return protocol + '//' + host; diff --git a/src/esdtToken.ts b/src/esdtToken.ts deleted file mode 100644 index b46ceaac..00000000 --- a/src/esdtToken.ts +++ /dev/null @@ -1,52 +0,0 @@ - -export class ESDTToken { - identifier: string = ''; - name: string = ''; - type: string = ''; - owner: string = ''; - minted: string = ''; - burnt: string = ''; - decimals: number = 18; - isPaused: boolean = false; - canUpgrade: boolean = false; - canMint: boolean = false; - canBurn: boolean = false; - canChangeOwner: boolean = false; - canPause: boolean = false; - canFreeze: boolean = false; - canWipe: boolean = false; - - constructor(init?: Partial) { - Object.assign(this, init); - } - - static fromHttpResponse(response: { - token: string, - name: string, - type: string, - owner: string, - minted: string, - burnt: string, - decimals: number, - isPaused: boolean, - canUpgrade: boolean, - canMint: boolean, - canBurn: boolean, - canChangeOwner: boolean, - canPause: boolean, - canFreeze: boolean, - canWipe: boolean - }) { - let esdtToken = new ESDTToken(response); - return esdtToken - } - - getTokenName() { - return this.name; - } - - getTokenIdentifier() { - return this.identifier; - } - -} diff --git a/src/index.ts b/src/index.ts index b0480e2b..81ceb920 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,8 @@ export * from "./nonce"; export * from "./transaction"; export * from "./transactionPayload"; export * from "./balance"; +export * from "./balanceBuilder"; +export * from "./interactive"; export * from "./networkConfig"; export * from "./networkStake"; export * from "./networkParams"; @@ -15,6 +17,7 @@ export * from "./proxyProvider"; export * from "./apiProvider"; export * from "./scArgumentsParser"; export * from "./esdtHelpers"; +export * from "./token"; export * from "./crypto"; export * from "./walletcore"; diff --git a/src/interactive.ts b/src/interactive.ts new file mode 100644 index 00000000..74c8d24f --- /dev/null +++ b/src/interactive.ts @@ -0,0 +1,33 @@ +import { BalanceBuilder, Egld, ErrInvalidArgument, IProvider, NetworkConfig, ProxyProvider, SystemWrapper } from "."; +import { loadAndSyncTestWallets, TestWallet } from "./testutils"; + +type InteractivePackage = { erdSys: SystemWrapper, Egld: BalanceBuilder, wallets: Record }; + +export async function setupInteractive(providerChoice: string): Promise { + let provider = chooseProvider(providerChoice); + return await setupInteractiveWithProvider(provider); +} + +export async function setupInteractiveWithProvider(provider: IProvider): Promise { + await NetworkConfig.getDefault().sync(provider); + let wallets = await loadAndSyncTestWallets(provider); + let erdSys = await SystemWrapper.load(provider); + return { erdSys, Egld, wallets }; +} + +export function getProviders(): Record { + return { + "local-testnet": new ProxyProvider("http://localhost:7950", { timeout: 5000 }), + "elrond-testnet": new ProxyProvider("https://testnet-gateway.elrond.com", { timeout: 5000 }), + "elrond-devnet": new ProxyProvider("https://devnet-gateway.elrond.com", { timeout: 5000 }), + "elrond-mainnet": new ProxyProvider("https://gateway.elrond.com", { timeout: 20000 }), + } +} + +export function chooseProvider(providerChoice: string): IProvider { + let providers = getProviders(); + if (providerChoice in providers) { + return providers[providerChoice]; + } + throw new ErrInvalidArgument(`providerChoice is not recognized (must be one of: ${Object.keys(providers)})`); +} diff --git a/src/interface.ts b/src/interface.ts index 30a2bfe7..6e77e955 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -9,143 +9,158 @@ import { NetworkStake } from "./networkStake"; import { Stats } from "./stats"; import { NetworkStatus } from "./networkStatus"; import { TransactionOnNetwork } from "./transactionOnNetwork"; -import { ESDTToken } from "./esdtToken"; -import {SignableMessage} from "./signableMessage"; +import { Token } from "./token"; +import BigNumber from "bignumber.js"; /** * An interface that defines the endpoints of an HTTP API Provider. */ export interface IProvider { - /** - * Fetches the Network configuration. - */ - getNetworkConfig(): Promise; - - /** - * Fetches the Network status. - */ - getNetworkStatus(): Promise; - - /** - * Fetches the state of an {@link Account}. - */ - getAccount(address: Address): Promise; - - /** - * Queries a Smart Contract - runs a pure function defined by the contract and returns its results. - */ - queryContract(query: Query): Promise; - - /** - * Broadcasts an already-signed {@link Transaction}. - */ - sendTransaction(tx: Transaction): Promise; - - /** - * Simulates the processing of an already-signed {@link Transaction}. - */ - simulateTransaction(tx: Transaction): Promise; - - /** - * Fetches the state of a {@link Transaction}. - */ - getTransaction(txHash: TransactionHash, hintSender?: Address, withResults?: boolean): Promise; - - /** - * Queries the status of a {@link Transaction}. - */ - getTransactionStatus(txHash: TransactionHash): Promise; - - /** - * Get method that receives the resource url and on callback the method used to map the response. - */ - doGetGeneric(resourceUrl: string, callback: (response: any) => any): Promise; - - /** - * Post method that receives the resource url, the post payload and on callback the method used to map the response. - */ - doPostGeneric(resourceUrl: string, payload: any, callback: (response: any) => any): Promise; + /** + * Fetches the Network configuration. + */ + getNetworkConfig(): Promise; + + /** + * Fetches the Network status. + */ + getNetworkStatus(): Promise; + + /** + * Fetches the state of an {@link Account}. + */ + getAccount(address: Address): Promise; + + /** + * Fetches the list of ESDT data for all the tokens of an address. + */ + getAddressEsdtList(address: Address): Promise; + + /** + * Fetches the ESDT data for a token of an address. + */ + getAddressEsdt(address: Address, tokenIdentifier: string): Promise; + + /** + * Fetches the NFT data for a token with a given nonce of an address. + */ + getAddressNft(address: Address, tokenIdentifier: string, nonce: BigNumber): Promise; + + /** + * Queries a Smart Contract - runs a pure function defined by the contract and returns its results. + */ + queryContract(query: Query): Promise; + + /** + * Broadcasts an already-signed {@link Transaction}. + */ + sendTransaction(tx: Transaction): Promise; + + /** + * Simulates the processing of an already-signed {@link Transaction}. + */ + simulateTransaction(tx: Transaction): Promise; + + /** + * Fetches the state of a {@link Transaction}. + */ + getTransaction(txHash: TransactionHash, hintSender?: Address, withResults?: boolean): Promise; + + /** + * Queries the status of a {@link Transaction}. + */ + getTransactionStatus(txHash: TransactionHash): Promise; + + /** + * Get method that receives the resource url and on callback the method used to map the response. + */ + doGetGeneric(resourceUrl: string, callback: (response: any) => any): Promise; + + /** + * Post method that receives the resource url, the post payload and on callback the method used to map the response. + */ + doPostGeneric(resourceUrl: string, payload: any, callback: (response: any) => any): Promise; } /** * An interface that defines the endpoints of an HTTP API Provider. */ export interface IApiProvider { - /** - * Fetches the Network Stake. - */ - getNetworkStake(): Promise; - /** - * Fetches the Network Stats. - */ - getNetworkStats(): Promise; - /** - * Fetches the state of a {@link Transaction}. - */ - getTransaction(txHash: TransactionHash): Promise; - - getESDTToken(tokenIdentifier: string): Promise; - - /** - * Get method that receives the resource url and on callback the method used to map the response. - */ - doGetGeneric(resourceUrl: string, callback: (response: any) => any): Promise; + /** + * Fetches the Network Stake. + */ + getNetworkStake(): Promise; + /** + * Fetches the Network Stats. + */ + getNetworkStats(): Promise; + /** + * Fetches the state of a {@link Transaction}. + */ + getTransaction(txHash: TransactionHash): Promise; + + getToken(tokenIdentifier: string): Promise; + + /** + * Get method that receives the resource url and on callback the method used to map the response. + */ + doGetGeneric(resourceUrl: string, callback: (response: any) => any): Promise; } /** * An interface that defines a signing-capable object. */ export interface ISigner { - /** - * Gets the {@link Address} of the signer. - */ - getAddress(): Address; - - /** - * Signs a message (e.g. a {@link Transaction}). - */ - sign(signable: ISignable): Promise; + /** + * Gets the {@link Address} of the signer. + */ + getAddress(): Address; + + /** + * Signs a message (e.g. a {@link Transaction}). + */ + sign(signable: ISignable): Promise; } export interface IVerifier { - verify(message: IVerifiable): boolean; + verify(message: IVerifiable): boolean; } /** * An interface that defines a signable object (e.g. a {@link Transaction}). */ export interface ISignable { - /** - * Returns the signable object in its raw form - a sequence of bytes to be signed. - */ - serializeForSigning(signedBy: Address): Buffer; - - /** - * Applies the computed signature on the object itself. - * - * @param signature The computed signature - * @param signedBy The address of the {@link Signer} - */ - applySignature(signature: Signature, signedBy: Address): void; + /** + * Returns the signable object in its raw form - a sequence of bytes to be signed. + */ + serializeForSigning(signedBy: Address): Buffer; + + /** + * Applies the computed signature on the object itself. + * + * @param signature The computed signature + * @param signedBy The address of the {@link Signer} + */ + applySignature(signature: Signature, signedBy: Address): void; } /** * Interface that defines a signed and verifiable object */ export interface IVerifiable { - /** - * Returns the signature that should be verified - */ - getSignature(): Signature; - /** - * Returns the signable object in its raw form - a sequence of bytes to be verified. - */ - serializeForSigning(signedBy?: Address): Buffer; + /** + * Returns the signature that should be verified + */ + getSignature(): Signature; + /** + * Returns the signable object in its raw form - a sequence of bytes to be verified. + */ + serializeForSigning(signedBy?: Address): Buffer; } /** * An interface that defines a disposable object. */ export interface Disposable { - dispose(): void; + dispose(): void; } diff --git a/src/networkParams.ts b/src/networkParams.ts index 1ff43ca8..c5560864 100644 --- a/src/networkParams.ts +++ b/src/networkParams.ts @@ -1,7 +1,7 @@ -import {TransactionPayload} from "./transactionPayload"; -import {NetworkConfig} from "./networkConfig"; +import { TransactionPayload } from "./transactionPayload"; +import { NetworkConfig } from "./networkConfig"; import * as errors from "./errors"; -import {Balance} from "./balance"; +import { Egld } from "./balanceBuilder"; import { TRANSACTION_OPTIONS_DEFAULT, TRANSACTION_OPTIONS_TX_HASH_SIGN, @@ -31,7 +31,7 @@ export class GasPrice { } toDenominated(): string { - let asBalance = new Balance(this.value.toString(10)); + let asBalance = Egld.raw(this.value.toString(10)); return asBalance.toDenominated(); } diff --git a/src/nftToken.ts b/src/nftToken.ts index 69f0c0c1..3132eeae 100644 --- a/src/nftToken.ts +++ b/src/nftToken.ts @@ -46,7 +46,7 @@ export class NFTToken { wiped: string }) { let nftToken = new NFTToken(response); - return nftToken + return nftToken; } getTokenName() { diff --git a/src/proto/serializer.spec.ts b/src/proto/serializer.spec.ts index 304dfcba..1c135e7e 100644 --- a/src/proto/serializer.spec.ts +++ b/src/proto/serializer.spec.ts @@ -1,16 +1,19 @@ import { assert } from "chai"; import { ProtoSerializer } from "./serializer"; import { Nonce } from "../nonce"; -import { Transaction, } from "../transaction"; -import { TestWallets } from "../testutils"; +import { Transaction } from "../transaction"; +import { loadTestWallets, TestWallet } from "../testutils"; import { Signature } from "../signature"; import { Balance } from "../balance"; import { ChainID, GasLimit, GasPrice, TransactionVersion } from "../networkParams"; import { TransactionPayload } from "../transactionPayload"; describe("serialize transactions", () => { - let wallets = new TestWallets(); + let wallets: Record; let serializer = new ProtoSerializer(); + before(async function () { + wallets = await loadTestWallets(); + }); it("with no data, no value", async () => { let transaction = new Transaction({ diff --git a/src/proxyProvider.ts b/src/proxyProvider.ts index 2c0b6dca..98166076 100644 --- a/src/proxyProvider.ts +++ b/src/proxyProvider.ts @@ -10,6 +10,7 @@ import { QueryResponse } from "./smartcontracts/queryResponse"; import { Logger } from "./logger"; import { NetworkStatus } from "./networkStatus"; import { TransactionOnNetwork } from "./transactionOnNetwork"; +import BigNumber from "bignumber.js"; const JSONbig = require("json-bigint"); /** @@ -40,6 +41,24 @@ export class ProxyProvider implements IProvider { ); } + async getAddressEsdtList(address: Address): Promise { + return this.doGetGeneric(`address/${address.bech32()}/esdt`, (response) => + response.esdts + ); + } + + async getAddressEsdt(address: Address, tokenIdentifier: string): Promise { + return this.doGetGeneric(`address/${address.bech32()}/esdt/${tokenIdentifier}`, (response) => + response.tokenData + ); + } + + async getAddressNft(address: Address, tokenIdentifier: string, nonce: BigNumber): Promise { + return this.doGetGeneric(`address/${address.bech32()}/nft/${tokenIdentifier}/nonce/${nonce}`, (response) => + response.tokenData + ); + } + /** * Queries a Smart Contract - runs a pure function defined by the contract and returns its results. */ @@ -183,7 +202,7 @@ export class ProxyProvider implements IProvider { // See: https://github.com/axios/axios/issues/983 axios.defaults.transformResponse = [ - function(data) { + function (data) { return JSONbig.parse(data); }, ]; diff --git a/src/signableMessage.spec.ts b/src/signableMessage.spec.ts index 83bac947..95287e2c 100644 --- a/src/signableMessage.spec.ts +++ b/src/signableMessage.spec.ts @@ -1,29 +1,31 @@ import { assert } from "chai"; import { SignableMessage } from "./signableMessage"; import { Signature } from "./signature"; -import {TestWallets} from "./testutils"; +import { loadTestWallets, TestWallet } from "./testutils"; describe("test signable message", () => { - let wallets = new TestWallets(); - let alice = wallets.alice; - it("should create signableMessage", async () => { - const sm = new SignableMessage({ - address: alice.address, - message: Buffer.from("test message", "ascii"), - signature: new Signature(Buffer.from("a".repeat(128), "hex"),), - signer: "ElrondWallet" + let alice: TestWallet; + before(async function () { + ({ alice } = await loadTestWallets()); }); + it("should create signableMessage", async () => { + const sm = new SignableMessage({ + address: alice.address, + message: Buffer.from("test message", "ascii"), + signature: new Signature(Buffer.from("a".repeat(128), "hex"),), + signer: "ElrondWallet" + }); - const jsonSM = sm.toJSON(); + const jsonSM = sm.toJSON(); - // We just test that the returned object contains what was passed and the hex values are prefixed with 0x - assert.deepEqual(jsonSM, { - address: 'erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th', - message: '0x74657374206d657373616765', - signature: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', - version: 1, - signer: 'ElrondWallet' - }, "invalid signable message returned"); - }); + // We just test that the returned object contains what was passed and the hex values are prefixed with 0x + assert.deepEqual(jsonSM, { + address: 'erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th', + message: '0x74657374206d657373616765', + signature: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + version: 1, + signer: 'ElrondWallet' + }, "invalid signable message returned"); + }); }); diff --git a/src/smartcontracts/abi.ts b/src/smartcontracts/abi.ts index ee81d744..cf91f535 100644 --- a/src/smartcontracts/abi.ts +++ b/src/smartcontracts/abi.ts @@ -1,4 +1,7 @@ +import { ErrInvariantFailed } from "../errors"; +import { loadAbiRegistry } from "../testutils"; import { guardValueIsSet } from "../utils"; +import { ContractFunction } from "./function"; import { AbiRegistry, EndpointDefinition } from "./typesystem"; import { ContractInterface } from "./typesystem/contractInterface"; @@ -9,9 +12,15 @@ export class SmartContractAbi { this.interfaces.push(...registry.getInterfaces(implementsInterfaces)); } + static async fromAbiPath(abiPath: string): Promise { + let abiRegistry = await loadAbiRegistry([abiPath]); + let interfaceNames = abiRegistry.interfaces.map(iface => iface.name); + return new SmartContractAbi(abiRegistry, interfaceNames); + } + getAllEndpoints(): EndpointDefinition[] { let endpoints = []; - + for (const iface of this.interfaces) { endpoints.push(...iface.endpoints); } @@ -19,9 +28,30 @@ export class SmartContractAbi { return endpoints; } - getEndpoint(name: string): EndpointDefinition { - let result = this.getAllEndpoints().find(item => item.name == name); + getEndpoint(name: string | ContractFunction): EndpointDefinition { + if (name instanceof ContractFunction) { + name = name.name; + } + let result = this.getAllEndpoints().find(item => item.name === name); guardValueIsSet("result", result); return result!; } + + getConstructorDefinition(): EndpointDefinition | null { + let constructors = []; + for (const iface of this.interfaces) { + let constructor_definition = iface.getConstructorDefinition(); + if (constructor_definition !== null) { + constructors.push(constructor_definition); + } + } + switch (constructors.length) { + case 0: + return null; + case 1: + return constructors[0]; + default: + throw new ErrInvariantFailed(`Found more than 1 constructor (found ${constructors.length})`); + } + } } diff --git a/src/smartcontracts/argumentErrorContext.ts b/src/smartcontracts/argumentErrorContext.ts new file mode 100644 index 00000000..a2d487b5 --- /dev/null +++ b/src/smartcontracts/argumentErrorContext.ts @@ -0,0 +1,33 @@ +import { EndpointParameterDefinition, Type } from "."; +import { ErrInvalidArgument } from ".."; + +export class ArgumentErrorContext { + endpointName: string; + argumentIndex: string; + parameterDefinition: EndpointParameterDefinition; + + constructor(endpointName: string, argumentIndex: string, parameterDefinition: EndpointParameterDefinition) { + this.endpointName = endpointName; + this.argumentIndex = argumentIndex; + this.parameterDefinition = parameterDefinition; + } + + throwError(specificError: string): never { + throw new ErrInvalidArgument(`Error when converting arguments for endpoint (endpoint name: ${this.endpointName}, argument index: ${this.argumentIndex}, name: ${this.parameterDefinition.name}, type: ${this.parameterDefinition.type})\nNested error: ${specificError}`); + } + + convertError(native: any, typeName: string): never { + this.throwError(`Can't convert argument (argument: ${native}, type ${typeof native}), wanted type: ${typeName})`); + } + + unhandledType(functionName: string, type: Type): never { + this.throwError(`Unhandled type (function: ${functionName}, type: ${type})`); + } + + guardSameLength(native: any[], valueTypes: Type[]) { + native = native || []; + if (native.length != valueTypes.length) { + this.throwError(`Incorrect composite type length: have ${native.length}, expected ${valueTypes.length} (argument: ${native})`); + } + } +} diff --git a/src/smartcontracts/function.ts b/src/smartcontracts/function.ts index c9884f3b..df0281ca 100644 --- a/src/smartcontracts/function.ts +++ b/src/smartcontracts/function.ts @@ -1,4 +1,4 @@ -import * as errors from "../errors"; +import * as errors from "../errors"; /** * A function of a Smart Contract, as an abstraction. @@ -17,7 +17,7 @@ export class ContractFunction { constructor(name: string) { this.name = name; - if (name.length == 0) { + if (name == null) { throw new errors.ErrInvalidFunctionName(); } } diff --git a/src/smartcontracts/index.ts b/src/smartcontracts/index.ts index 6846949a..50bcd518 100644 --- a/src/smartcontracts/index.ts +++ b/src/smartcontracts/index.ts @@ -12,6 +12,7 @@ export * from "./defaultRunner"; export * from "./function"; export * from "./interaction"; export * from "./interface"; +export * from "./nativeSerializer"; export * from "./query"; export * from "./queryResponse"; export * from "./returnCode"; @@ -20,3 +21,4 @@ export * from "./smartContractResults"; export * from "./strictChecker"; export * from "./transactionPayloadBuilders"; export * from "./typesystem"; +export * from "./wrapper"; diff --git a/src/smartcontracts/interaction.dev.net.spec.ts b/src/smartcontracts/interaction.dev.net.spec.ts index ca4def12..e15ef942 100644 --- a/src/smartcontracts/interaction.dev.net.spec.ts +++ b/src/smartcontracts/interaction.dev.net.spec.ts @@ -2,7 +2,7 @@ import { StrictChecker } from "./strictChecker"; import { DefaultInteractionRunner } from "./defaultRunner"; import { SmartContract } from "./smartContract"; import { BigUIntValue, OptionValue, TypedValue, U32Value } from "./typesystem"; -import { getDevnetProvider, loadAbiRegistry, loadContractCode, TestWallets } from "../testutils"; +import { loadAbiRegistry, loadContractCode, loadTestWallets, TestWallet } from "../testutils"; import { SmartContractAbi } from "./abi"; import { assert } from "chai"; import { Interaction } from "./interaction"; @@ -11,17 +11,19 @@ import { ReturnCode } from "./returnCode"; import { Balance } from "../balance"; import BigNumber from "bignumber.js"; import { NetworkConfig } from "../networkConfig"; -import { Account } from "../account"; import { BytesValue } from "./typesystem/bytes"; +import { chooseProvider } from "../interactive"; describe("test smart contract interactor", function () { - let wallets = new TestWallets(); let checker = new StrictChecker(); - let provider = getDevnetProvider(); - let alice = new Account(wallets.alice.address); - let aliceSigner = wallets.alice.signer; - let runner = new DefaultInteractionRunner(checker, aliceSigner, provider); + let provider = chooseProvider("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); @@ -45,9 +47,9 @@ describe("test smart contract interactor", function () { assert.isTrue(queryResponseBundle.returnCode.equals(ReturnCode.Ok)); // Execute, do not wait for execution - await runner.run(interaction.withNonce(alice.getNonceThenIncrement())); + await runner.run(interaction.withNonce(alice.account.getNonceThenIncrement())); // Execute, and wait for execution - let executionResultsBundle = await runner.runAwaitExecution(interaction.withNonce(alice.getNonceThenIncrement())); + let executionResultsBundle = await runner.runAwaitExecution(interaction.withNonce(alice.account.getNonceThenIncrement())); assert.lengthOf(executionResultsBundle.values, 1); assert.deepEqual(executionResultsBundle.firstValue.valueOf(), new BigNumber(42)); @@ -76,12 +78,12 @@ describe("test smart contract interactor", function () { assert.deepEqual(counterValue.valueOf(), new BigNumber(1)); // Increment, wait for execution. - let { firstValue: valueAfterIncrement } = await runner.runAwaitExecution(incrementInteraction.withNonce(alice.getNonceThenIncrement())); + 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.getNonceThenIncrement())); - let { firstValue: valueAfterDecrement } = await runner.runAwaitExecution(decrementInteraction.withNonce(alice.getNonceThenIncrement())) + 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)); }); @@ -117,18 +119,18 @@ describe("test smart contract interactor", function () { ]).withGasLimit(new GasLimit(15000000)); // start() - let { returnCode: startReturnCode, values: startReturnvalues } = await runner.runAwaitExecution(startInteraction.withNonce(alice.getNonceThenIncrement())) + let { returnCode: startReturnCode, values: startReturnvalues } = await runner.runAwaitExecution(startInteraction.withNonce(alice.account.getNonceThenIncrement())) assert.isTrue(startReturnCode.equals(ReturnCode.Ok)); assert.lengthOf(startReturnvalues, 0); // status() - let { returnCode: statusReturnCode, values: statusReturnValues, firstValue: statusFirstValue } = await runner.runAwaitExecution(lotteryStatusInteraction.withNonce(alice.getNonceThenIncrement())) + 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(), "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.getNonceThenIncrement())) + 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); @@ -159,8 +161,8 @@ describe("test smart contract interactor", function () { }); // In these tests, all contracts are deployed by Alice. - transactionDeploy.setNonce(alice.getNonceThenIncrement()); - await aliceSigner.sign(transactionDeploy); + 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 1d8756cb..0d87b196 100644 --- a/src/smartcontracts/interaction.spec.ts +++ b/src/smartcontracts/interaction.spec.ts @@ -2,7 +2,7 @@ import { StrictChecker } from "./strictChecker"; import { DefaultInteractionRunner } from "./defaultRunner"; import { SmartContract } from "./smartContract"; import { BigUIntValue, OptionValue, U32Value } from "./typesystem"; -import { AddImmediateResult, loadAbiRegistry, MarkNotarized, MockProvider, setupUnitTestWatcherTimeouts, TestWallets } from "../testutils"; +import { AddImmediateResult, loadAbiRegistry, loadTestWallets, MarkNotarized, MockProvider, setupUnitTestWatcherTimeouts, TestWallet } from "../testutils"; import { SmartContractAbi } from "./abi"; import { Address } from "../address"; import { assert } from "chai"; @@ -18,12 +18,15 @@ import BigNumber from "bignumber.js"; import { BytesValue } from "./typesystem/bytes"; describe("test smart contract interactor", function () { - let wallets = new TestWallets(); let dummyAddress = new Address("erd1qqqqqqqqqqqqqpgqak8zt22wl2ph4tswtyc39namqx6ysa2sd8ss4xmlj3"); let checker = new StrictChecker(); let provider = new MockProvider(); - let signer = wallets.alice.signer; - let runner = new DefaultInteractionRunner(checker, signer, provider); + let alice: TestWallet; + let runner: DefaultInteractionRunner; + before(async function () { + ({ alice } = await loadTestWallets()); + runner = new DefaultInteractionRunner(checker, alice.signer, provider); + }); it("should interact with 'answer'", async function () { setupUnitTestWatcherTimeouts(); @@ -34,7 +37,8 @@ describe("test smart contract interactor", function () { let interaction = contract.methods.getUltimateAnswer().withGasLimit(new GasLimit(543210)); assert.equal(interaction.getContract().getAddress(), dummyAddress); - assert.deepEqual(interaction.getFunction(), new ContractFunction("getUltimateAnswer")); + assert.deepEqual(interaction.getInterpretingFunction(), new ContractFunction("getUltimateAnswer")); + assert.deepEqual(interaction.getExecutingFunction(), new ContractFunction("getUltimateAnswer")); assert.lengthOf(interaction.getArguments(), 0); assert.deepEqual(interaction.getGasLimit(), new GasLimit(543210)); diff --git a/src/smartcontracts/interaction.ts b/src/smartcontracts/interaction.ts index 31f23a7a..4e74af68 100644 --- a/src/smartcontracts/interaction.ts +++ b/src/smartcontracts/interaction.ts @@ -10,6 +10,7 @@ import { SmartContract } from "./smartContract"; import { EndpointDefinition, TypedValue } from "./typesystem"; import { Nonce } from "../nonce"; import { ExecutionResultsBundle, QueryResponseBundle } from "./interface"; +import { ErrInvariantFailed } from "../errors"; /** * Interactions can be seen as mutable transaction & query builders. @@ -19,8 +20,10 @@ import { ExecutionResultsBundle, QueryResponseBundle } from "./interface"; */ export class Interaction { private readonly contract: SmartContract; - private readonly func: ContractFunction; + private readonly executingFunction: ContractFunction; + private readonly interpretingFunction: ContractFunction; private readonly args: TypedValue[]; + private readonly receiver?: Address; private nonce: Nonce = new Nonce(0); private value: Balance = Balance.Zero(); @@ -28,20 +31,28 @@ export class Interaction { constructor( contract: SmartContract, - func: ContractFunction, - args: TypedValue[] + executingFunction: ContractFunction, + interpretingFunction: ContractFunction, + args: TypedValue[], + receiver?: Address, ) { this.contract = contract; - this.func = func; + this.executingFunction = executingFunction; + this.interpretingFunction = interpretingFunction; this.args = args; + this.receiver = receiver; } getContract(): SmartContract { return this.contract; } - getFunction(): ContractFunction { - return this.func; + getInterpretingFunction(): ContractFunction { + return this.interpretingFunction; + } + + getExecutingFunction(): ContractFunction { + return this.executingFunction; } getArguments(): TypedValue[] { @@ -59,12 +70,13 @@ export class Interaction { buildTransaction(): Transaction { // TODO: create as "deploy" transaction if the function is "init" (or find a better pattern for deployments). let transaction = this.contract.call({ - func: this.func, + func: this.executingFunction, // GasLimit will be set using "withGasLimit()". gasLimit: this.gasLimit, args: this.args, // Value will be set using "withValue()". - value: this.value + value: this.value, + receiver: this.receiver, }); transaction.setNonce(this.nonce); @@ -74,7 +86,7 @@ export class Interaction { buildQuery(): Query { return new Query({ address: this.contract.getAddress(), - func: this.func, + func: this.executingFunction, args: this.args, // Value will be set using "withValue()". value: this.value, @@ -88,23 +100,7 @@ export class Interaction { * The outcome is structured such that it allows quick access to each level of detail. */ interpretExecutionResults(transactionOnNetwork: TransactionOnNetwork): ExecutionResultsBundle { - let smartContractResults = transactionOnNetwork.getSmartContractResults(); - let immediateResult = smartContractResults.getImmediate(); - let endpoint = this.getEndpointDefinition(); - - immediateResult.setEndpointDefinition(endpoint); - - let values = immediateResult.outputTyped(); - let returnCode = immediateResult.getReturnCode(); - - return { - transactionOnNetwork: transactionOnNetwork, - smartContractResults: smartContractResults, - immediateResult: immediateResult, - values: values, - firstValue: values[0], - returnCode: returnCode - }; + return interpretExecutionResults(this.getEndpoint(), transactionOnNetwork); } /** @@ -112,7 +108,7 @@ export class Interaction { * The outcome is structured such that it allows quick access to each level of detail. */ interpretQueryResponse(queryResponse: QueryResponse): QueryResponseBundle { - let endpoint = this.getEndpointDefinition(); + let endpoint = this.getEndpoint(); queryResponse.setEndpointDefinition(endpoint); let values = queryResponse.outputTyped(); @@ -141,11 +137,28 @@ export class Interaction { return this; } - getEndpointDefinition(): EndpointDefinition { - let abi = this.getContract().getAbi(); - let name = this.getFunction().toString(); - let endpoint = abi.getEndpoint(name); - - return endpoint; + getEndpoint(): EndpointDefinition { + return this.getContract().getAbi().getEndpoint(this.getInterpretingFunction()); } } + +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 + }; +} diff --git a/src/smartcontracts/interface.ts b/src/smartcontracts/interface.ts index b0d50b73..a0c9ec5c 100644 --- a/src/smartcontracts/interface.ts +++ b/src/smartcontracts/interface.ts @@ -9,7 +9,7 @@ import { ContractFunction } from "./function"; import { Interaction } from "./interaction"; import { QueryResponse } from "./queryResponse"; import { ReturnCode } from "./returnCode"; -import { ImmediateResult, SmartContractResults } from "./smartContractResults"; +import { SmartContractResults, TypedResult } from "./smartContractResults"; import { TypedValue } from "./typesystem"; /** @@ -24,35 +24,19 @@ export interface ISmartContract { /** * Creates a {@link Transaction} for deploying the Smart Contract to the Network. */ - deploy({ code, codeMetadata, initArguments, value, gasLimit } - : { code: Code, codeMetadata?: CodeMetadata, initArguments?: TypedValue[], value?: Balance, gasLimit: GasLimit }): Transaction; + deploy({ code, codeMetadata, initArguments, value, gasLimit }: DeployArguments): Transaction; /** * Creates a {@link Transaction} for upgrading the Smart Contract on the Network. */ - upgrade({ code, codeMetadata, initArguments, value, gasLimit } - : { code: Code, codeMetadata?: CodeMetadata, initArguments?: TypedValue[], value?: Balance, gasLimit: GasLimit }): Transaction; + upgrade({ code, codeMetadata, initArguments, value, gasLimit }: UpgradeArguments): Transaction; /** * Creates a {@link Transaction} for calling (a function of) the Smart Contract. - */ - call({ func, args, value, gasLimit } - : { func: ContractFunction, args?: TypedValue[], value?: Balance, gasLimit: GasLimit }): Transaction; + */ + call({ func, args, value, gasLimit }: CallArguments): Transaction; } -// export interface ERC20Client extends ISmartContract { -// name(): string; -// symbol(): string; -// decimals(): number; -// totalSupply(): Promise; -// balanceOf(address: string): Promise; -// transfer(receiver: string, value: bigint): Promise; -// transferFrom(sender: string, receiver: string, value: bigint): Promise; -// approve(spender: string, value: bigint): Promise; -// allowance(owner: string, spender: string): Promise; -// } - - export interface IInteractionRunner { run(interaction: Interaction): Promise; runAwaitExecution(interaction: Interaction): Promise; @@ -65,10 +49,42 @@ export interface IInteractionChecker { checkInteraction(interaction: Interaction): void; } +export interface DeployArguments { + code: Code; + codeMetadata?: CodeMetadata; + initArguments?: TypedValue[]; + value?: Balance; + gasLimit: GasLimit; +} + +export interface UpgradeArguments { + code: Code; + codeMetadata?: CodeMetadata; + initArguments?: TypedValue[]; + value?: Balance; + gasLimit: GasLimit; +} + +export interface CallArguments { + func: ContractFunction; + args?: TypedValue[]; + value?: Balance; + gasLimit: GasLimit; + receiver?: Address; +} + +export interface QueryArguments { + func: ContractFunction; + args?: TypedValue[]; + value?: Balance; + caller?: Address +} + export interface ExecutionResultsBundle { transactionOnNetwork: TransactionOnNetwork; smartContractResults: SmartContractResults; - immediateResult: ImmediateResult; + immediateResult: TypedResult; + resultingCalls: TypedResult[]; values: TypedValue[]; firstValue: TypedValue; returnCode: ReturnCode; diff --git a/src/smartcontracts/nativeSerializer.ts b/src/smartcontracts/nativeSerializer.ts new file mode 100644 index 00000000..8fc3c19a --- /dev/null +++ b/src/smartcontracts/nativeSerializer.ts @@ -0,0 +1,242 @@ +import BigNumber from "bignumber.js"; +import { AddressType, AddressValue, BigIntType, BigIntValue, BigUIntType, BigUIntValue, BooleanType, BooleanValue, BytesType, BytesValue, Code, CompositeType, CompositeValue, EndpointDefinition, EndpointParameterDefinition, I16Type, I16Value, I32Type, I32Value, I64Type, I64Value, I8Type, I8Value, List, ListType, NumericalType, OptionalType, OptionalValue, OptionType, OptionValue, PrimitiveType, TokenIdentifierType, TokenIdentifierValue, Type, TypedValue, U16Type, U16Value, U32Type, U32Value, U64Type, U64Value, U8Type, U8Value, VariadicType, VariadicValue } from "."; +import { ErrInvalidArgument, Address, BalanceBuilder } from ".."; +import { TestWallet } from "../testutils"; +import { ArgumentErrorContext } from "./argumentErrorContext"; +import { SmartContract } from "./smartContract"; +import { ContractWrapper } from "./wrapper/contractWrapper"; + +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 namespace NativeSerializer { + /** + * Interprets a set of native javascript values into a set of typed values, given parameter definitions. + */ + export function nativeToTypedValues(args: any[], endpoint: EndpointDefinition): TypedValue[] { + args = args || []; + args = handleVariadicArgsAndRePack(args, endpoint); + + let parameters = endpoint.input; + let values: TypedValue[] = []; + + for (let i in parameters) { + let parameter = parameters[i]; + let errorContext = new ArgumentErrorContext(endpoint.name, i, parameter); + let value = convertToTypedValue(args[i], parameter.type, errorContext); + values.push(value); + } + + return values; + } + + function handleVariadicArgsAndRePack(args: any[], endpoint: EndpointDefinition) { + let parameters = endpoint.input; + + let { min, max, variadic } = getArgumentsCardinality(parameters); + + if (!(min <= args.length && args.length <= max)) { + throw new ErrInvalidArgument(`Wrong number of arguments for endpoint ${endpoint.name}: expected between ${min} and ${max} arguments, have ${args.length}`); + } + + if (variadic) { + let lastArgIndex = parameters.length - 1; + let lastArg = args.slice(lastArgIndex); + if (lastArg.length > 0) { + args[lastArgIndex] = lastArg; + } + } + return args; + } + + + // A function may have one of the following formats: + // f(arg1, arg2, optional, optional) returns { min: 2, max: 4, variadic: false } + // f(arg1, variadic) returns { min: 1, max: Infinity, variadic: true } + // f(arg1, arg2, optional, arg4, optional, variadic) returns { min: 2, max: Infinity, variadic: true } + function getArgumentsCardinality(parameters: EndpointParameterDefinition[]): { min: number, max: number, variadic: boolean } { + let reversed = [...parameters].reverse(); // keep the original unchanged + let min = parameters.length; + let max = parameters.length; + let variadic = false; + if (reversed.length > 0 && reversed[0].type.getCardinality().isComposite()) { + max = Infinity; + variadic = true; + } + for (let parameter of reversed) { + if (parameter.type.getCardinality().isSingular()) { + break; + } + min -= 1; + } + return { min, max, variadic }; + } + + function convertToTypedValue(native: any, type: Type, errorContext: ArgumentErrorContext): TypedValue { + if (type instanceof OptionType) { + return toOptionValue(native, type, errorContext); + } + if (type instanceof OptionalType) { + return toOptionalValue(native, type, errorContext); + } + if (type instanceof VariadicType) { + return toVariadicValue(native, type, errorContext); + } + if (type instanceof CompositeType) { + return toCompositeValue(native, type, errorContext); + } + if (type instanceof ListType) { + return toListValue(native, type, errorContext); + } + if (type instanceof PrimitiveType) { + return toPrimitive(native, type, errorContext); + } + errorContext.throwError(`convertToTypedValue: unhandled type ${type}`); + } + + function toOptionValue(native: any, type: Type, errorContext: ArgumentErrorContext): TypedValue { + if (native == null) { + return OptionValue.newMissing(); + } + let converted = convertToTypedValue(native, type.getFirstTypeParameter(), errorContext); + return OptionValue.newProvided(converted); + } + + function toOptionalValue(native: any, type: Type, errorContext: ArgumentErrorContext): TypedValue { + if (native == null) { + return new OptionalValue(type); + } + let converted = convertToTypedValue(native, type.getFirstTypeParameter(), errorContext); + return new OptionalValue(type, converted); + } + + function toVariadicValue(native: any, type: Type, errorContext: ArgumentErrorContext): TypedValue { + if (native == null) { + native = []; + } + if (native.map === undefined) { + errorContext.convertError(native, "Variadic"); + } + let converted = native.map(function (item: any) { + return convertToTypedValue(item, type.getFirstTypeParameter(), errorContext); + }); + return new VariadicValue(type, converted); + } + + function toListValue(native: any, type: Type, errorContext: ArgumentErrorContext): TypedValue { + if (native.map === undefined) { + errorContext.convertError(native, "List"); + } + let converted = native.map(function (item: any) { + return convertToTypedValue(item, type.getFirstTypeParameter(), errorContext); + }); + return new List(type, converted); + } + + function toCompositeValue(native: any, type: Type, errorContext: ArgumentErrorContext): TypedValue { + let typedValues = []; + let typeParameters = type.getTypeParameters(); + errorContext.guardSameLength(native, typeParameters); + for (let i in typeParameters) { + typedValues.push(convertToTypedValue(native[i], typeParameters[i], errorContext)); + } + + return new CompositeValue(type, typedValues); + } + + function toPrimitive(native: any, type: Type, errorContext: ArgumentErrorContext): TypedValue { + if (type instanceof NumericalType) { + let number = new BigNumber(native); + return convertNumericalType(number, type, errorContext); + } + if (type instanceof BytesType) { + return convertNativeToBytesValue(native, errorContext); + } + if (type instanceof AddressType) { + return new AddressValue(convertNativeToAddress(native, errorContext)); + } + if (type instanceof BooleanType) { + return new BooleanValue(native); + } + if (type instanceof TokenIdentifierType) { + return new TokenIdentifierValue(convertNativeToBuffer(native, errorContext)); + } + errorContext.throwError(`(function: toPrimitive) unsupported type ${type}`); + } + + function convertNativeToBytesValue(native: NativeTypes.NativeBytes, errorContext: ArgumentErrorContext) { + if (native instanceof Code) { + return BytesValue.fromHex(native.toString()); + } + if (native instanceof Buffer) { + return new BytesValue(native); + } + if (typeof native === "string") { + return BytesValue.fromUTF8(native); + } + if (((native).getTokenIdentifier)) { + return BytesValue.fromUTF8(native.getTokenIdentifier()); + } + errorContext.convertError(native, "BytesValue"); + } + + function convertNativeToBuffer(native: NativeTypes.NativeBuffer, errorContext: ArgumentErrorContext): Buffer { + if (native instanceof Buffer) { + return native; + } + if (typeof native === "string") { + return Buffer.from(native); + } + if (((native).getTokenIdentifier)) { + return Buffer.from(native.getTokenIdentifier()); + } + errorContext.convertError(native, "Buffer"); + } + + export function convertNativeToAddress(native: NativeTypes.NativeAddress, errorContext: ArgumentErrorContext): Address { + 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"); + } + } + + function convertNumericalType(number: BigNumber, type: Type, errorContext: ArgumentErrorContext): TypedValue { + switch (type.constructor) { + case U8Type: + return new U8Value(number); + case I8Type: + return new I8Value(number); + case U16Type: + return new U16Value(number); + case I16Type: + return new I16Value(number); + case U32Type: + return new U32Value(number); + case I32Type: + return new I32Value(number); + case U64Type: + return new U64Value(number); + case I64Type: + return new I64Value(number); + case BigUIntType: + return new BigUIntValue(number); + case BigIntType: + return new BigIntValue(number); + default: + errorContext.unhandledType("convertNumericalType", type); + } + } +} diff --git a/src/smartcontracts/query.main.net.spec.ts b/src/smartcontracts/query.main.net.spec.ts index fbbe14b7..fa76b32d 100644 --- a/src/smartcontracts/query.main.net.spec.ts +++ b/src/smartcontracts/query.main.net.spec.ts @@ -1,13 +1,13 @@ import { assert } from "chai"; import { Address } from "../address"; import { ContractFunction } from "./function"; -import { getMainnetProvider } from "../testutils"; import { SmartContract } from "./smartContract"; import * as errors from "../errors"; import { AddressValue } from "./typesystem"; +import { chooseProvider } from "../interactive"; describe("test queries on mainnet", function () { - let provider = getMainnetProvider(); + let provider = chooseProvider("elrond-mainnet"); let delegationContract = new SmartContract({ address: new Address("erd1qqqqqqqqqqqqqpgqxwakt2g7u9atsnr03gqcgmhcv38pt7mkd94q6shuwt") }); it("delegation: should getTotalStakeByType", async () => { @@ -30,7 +30,7 @@ describe("test queries on mainnet", function () { assert.isAtMost(response.gasUsed.valueOf(), 50000000); }); - it("delegation: should getFullWaitingList", async function() { + it("delegation: should getFullWaitingList", async function () { this.timeout(20000); let response = await delegationContract.runQuery(provider, { @@ -41,7 +41,7 @@ describe("test queries on mainnet", function () { assert.isAtLeast(response.returnData.length, 20000); }); - it("delegation: should getClaimableRewards", async function() { + it("delegation: should getClaimableRewards", async function () { this.timeout(5000); // First, expect an error (bad arguments): diff --git a/src/smartcontracts/queryResponse.ts b/src/smartcontracts/queryResponse.ts index b13a627f..ad9f0565 100644 --- a/src/smartcontracts/queryResponse.ts +++ b/src/smartcontracts/queryResponse.ts @@ -1,13 +1,11 @@ import { GasLimit } from "../networkParams"; -import * as errors from "../errors"; import { EndpointDefinition, TypedValue } from "./typesystem"; import { MaxUint64 } from "./query"; import { ReturnCode } from "./returnCode"; -import { guardValueIsSet } from "../utils"; -import { ArgSerializer } from "./argSerializer"; import BigNumber from "bignumber.js"; +import { Result } from "./result"; -export class QueryResponse { +export class QueryResponse implements Result.IResult { /** * If available, will provide typed output arguments (with typed values). */ @@ -43,19 +41,28 @@ export class QueryResponse { }); } - assertSuccess() { - if (this.isSuccess()) { - return; - } + getEndpointDefinition(): EndpointDefinition | undefined { + return this.endpointDefinition; + } + getReturnCode(): ReturnCode { + return this.returnCode; + } + getReturnMessage(): string { + return this.returnMessage; + } + unpackOutput(): any { + return Result.unpackOutput(this); + } - throw new errors.ErrContract(`${this.returnCode}: ${this.returnMessage}`); + assertSuccess() { + Result.assertSuccess(this); } isSuccess(): boolean { - return this.returnCode.equals(ReturnCode.Ok); + return this.returnCode.isSuccess(); } - setEndpointDefinition(endpointDefinition: EndpointDefinition) { + setEndpointDefinition(endpointDefinition: EndpointDefinition): void { this.endpointDefinition = endpointDefinition; } @@ -67,12 +74,7 @@ export class QueryResponse { } outputTyped(): TypedValue[] { - this.assertSuccess(); - guardValueIsSet("endpointDefinition", this.endpointDefinition); - - let buffers = this.outputUntyped(); - let values = new ArgSerializer().buffersToValues(buffers, this.endpointDefinition!.output); - return values; + return Result.outputTyped(this); } /** diff --git a/src/smartcontracts/result.ts b/src/smartcontracts/result.ts new file mode 100644 index 00000000..3dfaf580 --- /dev/null +++ b/src/smartcontracts/result.ts @@ -0,0 +1,48 @@ +import { ArgSerializer, EndpointDefinition, ErrContract, guardValueIsSet, ReturnCode, TypedValue } from ".."; + +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/returnCode.ts b/src/smartcontracts/returnCode.ts index de5625d9..8d4dfe40 100644 --- a/src/smartcontracts/returnCode.ts +++ b/src/smartcontracts/returnCode.ts @@ -1,4 +1,7 @@ +import { EndpointDefinition } from "./typesystem"; + export class ReturnCode { + static None = new ReturnCode(""); static Ok = new ReturnCode("ok"); static FunctionNotFound = new ReturnCode("function not found"); static FunctionWrongSignature = new ReturnCode("wrong signature for function"); @@ -38,4 +41,8 @@ export class ReturnCode { return this.text == other.text; } + + isSuccess(): boolean { + return this.equals(ReturnCode.Ok) || this.equals(ReturnCode.None); + } } diff --git a/src/smartcontracts/smartContract.dev.net.spec.ts b/src/smartcontracts/smartContract.dev.net.spec.ts index 2d5a27e5..e032f984 100644 --- a/src/smartcontracts/smartContract.dev.net.spec.ts +++ b/src/smartcontracts/smartContract.dev.net.spec.ts @@ -2,25 +2,25 @@ import { SmartContract } from "./smartContract"; import { GasLimit } from "../networkParams"; import { TransactionWatcher } from "../transactionWatcher"; import { ContractFunction } from "./function"; -import { Account } from "../account"; import { NetworkConfig } from "../networkConfig"; -import { TestWallets } from "../testutils/wallets"; -import { getDevnetProvider, loadContractCode } from "../testutils"; +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 { decodeUnsignedNumber } from "./codec"; import { BytesValue } from "./typesystem/bytes"; +import { chooseProvider } from "../interactive"; describe("test on devnet (local)", function () { - let devnet = getDevnetProvider(); - let wallets = new TestWallets(); - let aliceWallet = wallets.alice; - let alice = new Account(aliceWallet.address); - let aliceSigner = aliceWallet.signer; + let devnet = chooseProvider("local-testnet"); + let alice: TestWallet, bob: TestWallet, carol: TestWallet; + before(async function () { + ({ alice, bob, carol } = await loadTestWallets()); + }); - it("counter: should deploy, then simulate transactions", async function() { + it("counter: should deploy, then simulate transactions", async function () { this.timeout(60000); TransactionWatcher.DefaultPollingInterval = 5000; @@ -36,10 +36,10 @@ describe("test on devnet (local)", function () { gasLimit: new GasLimit(3000000) }); - transactionDeploy.setNonce(alice.nonce); - await aliceSigner.sign(transactionDeploy); + transactionDeploy.setNonce(alice.account.nonce); + await alice.signer.sign(transactionDeploy); - alice.incrementNonce(); + alice.account.incrementNonce(); // ++ let transactionIncrement = contract.call({ @@ -47,10 +47,10 @@ describe("test on devnet (local)", function () { gasLimit: new GasLimit(3000000) }); - transactionIncrement.setNonce(alice.nonce); - await aliceSigner.sign(transactionIncrement); + transactionIncrement.setNonce(alice.account.nonce); + await alice.signer.sign(transactionIncrement); - alice.incrementNonce(); + alice.account.incrementNonce(); // Now, let's build a few transactions, to be simulated let simulateOne = contract.call({ @@ -63,11 +63,11 @@ describe("test on devnet (local)", function () { gasLimit: new GasLimit(500000) }); - simulateOne.setNonce(alice.nonce); - simulateTwo.setNonce(alice.nonce); + simulateOne.setNonce(alice.account.nonce); + simulateTwo.setNonce(alice.account.nonce); - await aliceSigner.sign(simulateOne); - await aliceSigner.sign(simulateTwo); + await alice.signer.sign(simulateOne); + await alice.signer.sign(simulateTwo); // Broadcast & execute await transactionDeploy.send(devnet); @@ -81,7 +81,7 @@ describe("test on devnet (local)", function () { Logger.trace(JSON.stringify(await simulateTwo.simulate(devnet), null, 4)); }); - it("counter: should deploy, call and query contract", async function() { + it("counter: should deploy, call and query contract", async function () { this.timeout(80000); TransactionWatcher.DefaultPollingInterval = 5000; @@ -97,10 +97,10 @@ describe("test on devnet (local)", function () { gasLimit: new GasLimit(3000000) }); - transactionDeploy.setNonce(alice.nonce); - await aliceSigner.sign(transactionDeploy); + transactionDeploy.setNonce(alice.account.nonce); + await alice.signer.sign(transactionDeploy); - alice.incrementNonce(); + alice.account.incrementNonce(); // ++ let transactionIncrementFirst = contract.call({ @@ -108,10 +108,10 @@ describe("test on devnet (local)", function () { gasLimit: new GasLimit(2000000) }); - transactionIncrementFirst.setNonce(alice.nonce); - await aliceSigner.sign(transactionIncrementFirst); + transactionIncrementFirst.setNonce(alice.account.nonce); + await alice.signer.sign(transactionIncrementFirst); - alice.incrementNonce(); + alice.account.incrementNonce(); // ++ let transactionIncrementSecond = contract.call({ @@ -119,10 +119,10 @@ describe("test on devnet (local)", function () { gasLimit: new GasLimit(2000000) }); - transactionIncrementSecond.setNonce(alice.nonce); - await aliceSigner.sign(transactionIncrementSecond); + transactionIncrementSecond.setNonce(alice.account.nonce); + await alice.signer.sign(transactionIncrementSecond); - alice.incrementNonce(); + alice.account.incrementNonce(); // Broadcast & execute await transactionDeploy.send(devnet); @@ -138,7 +138,7 @@ describe("test on devnet (local)", function () { assert.equal(3, decodeUnsignedNumber(queryResponse.outputUntyped()[0])); }); - it("erc20: should deploy, call and query contract", async function() { + it("erc20: should deploy, call and query contract", async function () { this.timeout(60000); TransactionWatcher.DefaultPollingInterval = 5000; @@ -157,31 +157,31 @@ describe("test on devnet (local)", function () { // The deploy transaction should be signed, so that the address of the contract // (required for the subsequent transactions) is computed. - transactionDeploy.setNonce(alice.nonce); - await aliceSigner.sign(transactionDeploy); - alice.incrementNonce(); + transactionDeploy.setNonce(alice.account.nonce); + await alice.signer.sign(transactionDeploy); + alice.account.incrementNonce(); // Minting let transactionMintBob = contract.call({ func: new ContractFunction("transferToken"), gasLimit: new GasLimit(9000000), - args: [new AddressValue(wallets.bob.address), new U32Value(1000)] + args: [new AddressValue(bob.address), new U32Value(1000)] }); let transactionMintCarol = contract.call({ func: new ContractFunction("transferToken"), gasLimit: new GasLimit(9000000), - args: [new AddressValue(wallets.carol.address), new U32Value(1500)] + args: [new AddressValue(carol.address), new U32Value(1500)] }); // Apply nonces and sign the remaining transactions - transactionMintBob.setNonce(alice.nonce); - alice.incrementNonce(); - transactionMintCarol.setNonce(alice.nonce); - alice.incrementNonce(); + transactionMintBob.setNonce(alice.account.nonce); + alice.account.incrementNonce(); + transactionMintCarol.setNonce(alice.account.nonce); + alice.account.incrementNonce(); - await aliceSigner.sign(transactionMintBob); - await aliceSigner.sign(transactionMintCarol); + await alice.signer.sign(transactionMintBob); + await alice.signer.sign(transactionMintCarol); // Broadcast & execute await transactionDeploy.send(devnet); @@ -200,24 +200,24 @@ describe("test on devnet (local)", function () { queryResponse = await contract.runQuery(devnet, { func: new ContractFunction("balanceOf"), - args: [new AddressValue(wallets.alice.address)] + args: [new AddressValue(alice.address)] }); assert.equal(7500, decodeUnsignedNumber(queryResponse.outputUntyped()[0])); - + queryResponse = await contract.runQuery(devnet, { func: new ContractFunction("balanceOf"), - args: [new AddressValue(wallets.bob.address)] + args: [new AddressValue(bob.address)] }); assert.equal(1000, decodeUnsignedNumber(queryResponse.outputUntyped()[0])); - + queryResponse = await contract.runQuery(devnet, { func: new ContractFunction("balanceOf"), - args: [new AddressValue(wallets.carol.address)] + args: [new AddressValue(carol.address)] }); assert.equal(1500, decodeUnsignedNumber(queryResponse.outputUntyped()[0])); }); - it("lottery: should deploy, call and query contract", async function() { + it("lottery: should deploy, call and query contract", async function () { this.timeout(60000); TransactionWatcher.DefaultPollingInterval = 5000; @@ -236,9 +236,9 @@ describe("test on devnet (local)", function () { // The deploy transaction should be signed, so that the address of the contract // (required for the subsequent transactions) is computed. - transactionDeploy.setNonce(alice.nonce); - await aliceSigner.sign(transactionDeploy); - alice.incrementNonce(); + transactionDeploy.setNonce(alice.account.nonce); + await alice.signer.sign(transactionDeploy); + alice.account.incrementNonce(); // Start let transactionStart = contract.call({ @@ -256,9 +256,9 @@ describe("test on devnet (local)", function () { }); // Apply nonces and sign the remaining transactions - transactionStart.setNonce(alice.nonce); + transactionStart.setNonce(alice.account.nonce); - await aliceSigner.sign(transactionStart); + await alice.signer.sign(transactionStart); // Broadcast & execute await transactionDeploy.send(devnet); diff --git a/src/smartcontracts/smartContract.spec.ts b/src/smartcontracts/smartContract.spec.ts index 30a7dc18..7d189da1 100644 --- a/src/smartcontracts/smartContract.spec.ts +++ b/src/smartcontracts/smartContract.spec.ts @@ -4,21 +4,19 @@ import { Code } from "./code"; import { Nonce } from "../nonce"; import { SmartContract } from "./smartContract"; import { GasLimit } from "../networkParams"; -import { MockProvider, setupUnitTestWatcherTimeouts, Wait } from "../testutils"; +import { loadTestWallets, MockProvider, setupUnitTestWatcherTimeouts, TestWallet, Wait } from "../testutils"; import { TransactionStatus } from "../transaction"; import { ContractFunction } from "./function"; -import { Account } from "../account"; -import { TestWallets } from "../testutils"; import { U32Value } from "./typesystem"; import { BytesValue } from "./typesystem/bytes"; describe("test contract", () => { let provider = new MockProvider(); - let wallets = new TestWallets(); - let aliceWallet = wallets.alice; - let alice = new Account(aliceWallet.address); - let aliceSigner = aliceWallet.signer; + let alice: TestWallet; + before(async function () { + ({ alice } = await loadTestWallets()); + }); it("should compute contract address", async () => { let owner = new Address("93ee6143cdc10ce79f15b2a6c2ad38e9b6021c72a1779051f47154fd54cfbd5e"); @@ -46,14 +44,14 @@ describe("test contract", () => { }); await alice.sync(provider); - deployTransaction.setNonce(alice.nonce); + deployTransaction.setNonce(alice.account.nonce); assert.equal(deployTransaction.getData().valueOf().toString(), "01020304@0500@0100"); assert.equal(deployTransaction.getGasLimit().valueOf(), 1000000); assert.equal(deployTransaction.getNonce().valueOf(), 42); // Sign transaction, then check contract address (should be computed upon signing) - aliceSigner.sign(deployTransaction); + alice.signer.sign(deployTransaction); assert.equal(contract.getOwner().bech32(), alice.address.bech32()); assert.equal(contract.getAddress().bech32(), "erd1qqqqqqqqqqqqqpgq3ytm9m8dpeud35v3us20vsafp77smqghd8ss4jtm0q"); @@ -88,9 +86,9 @@ describe("test contract", () => { }); await alice.sync(provider); - callTransactionOne.setNonce(alice.nonce); - alice.incrementNonce(); - callTransactionTwo.setNonce(alice.nonce); + callTransactionOne.setNonce(alice.account.nonce); + alice.account.incrementNonce(); + callTransactionTwo.setNonce(alice.account.nonce); assert.equal(callTransactionOne.getNonce().valueOf(), 42); assert.equal(callTransactionOne.getData().valueOf().toString(), "helloEarth@05@0123"); @@ -100,8 +98,8 @@ describe("test contract", () => { assert.equal(callTransactionTwo.getGasLimit().valueOf(), 1500000); // Sign transactions, broadcast them - aliceSigner.sign(callTransactionOne); - aliceSigner.sign(callTransactionTwo); + alice.signer.sign(callTransactionOne); + alice.signer.sign(callTransactionTwo); let hashOne = await callTransactionOne.send(provider); let hashTwo = await callTransactionTwo.send(provider); diff --git a/src/smartcontracts/smartContract.ts b/src/smartcontracts/smartContract.ts index e3494970..d4d636b9 100644 --- a/src/smartcontracts/smartContract.ts +++ b/src/smartcontracts/smartContract.ts @@ -1,11 +1,10 @@ import { Balance } from "../balance"; import { Address } from "../address"; -import { GasLimit } from "../networkParams"; import { Transaction } from "../transaction"; import { TransactionPayload } from "../transactionPayload"; import { Code } from "./code"; import { CodeMetadata } from "./codeMetadata"; -import { ISmartContract as ISmartContract } from "./interface"; +import { CallArguments, DeployArguments, ISmartContract as ISmartContract, QueryArguments, UpgradeArguments } from "./interface"; import { ArwenVirtualMachine } from "./transactionPayloadBuilders"; import { Nonce } from "../nonce"; import { ContractFunction } from "./function"; @@ -62,7 +61,7 @@ export class SmartContract implements ISmartContract { // and returns a prepared contract interaction. this.methods[functionName] = function (args: TypedValue[]) { let func = new ContractFunction(functionName); - let interaction = new Interaction(contract, func, args || []); + let interaction = new Interaction(contract, func, func, args || []); return interaction; }; } @@ -79,7 +78,6 @@ export class SmartContract implements ISmartContract { * Gets the address, as on Network. */ getAddress(): Address { - this.address.assertNotEmpty(); return this.address; } @@ -120,9 +118,7 @@ export class SmartContract implements ISmartContract { /** * Creates a {@link Transaction} for deploying the Smart Contract to the Network. */ - deploy({ code, codeMetadata, initArguments, value, gasLimit } - : { code: Code, codeMetadata?: CodeMetadata, initArguments?: TypedValue[], value?: Balance, gasLimit: GasLimit } - ): Transaction { + deploy({ code, codeMetadata, initArguments, value, gasLimit }: DeployArguments): Transaction { codeMetadata = codeMetadata || new CodeMetadata(); initArguments = initArguments || []; value = value || Balance.Zero(); @@ -159,16 +155,15 @@ export class SmartContract implements ISmartContract { /** * Creates a {@link Transaction} for upgrading the Smart Contract on the Network. */ - upgrade({ code, codeMetadata, initArgs, value, gasLimit } - : { code: Code, codeMetadata?: CodeMetadata, initArgs?: TypedValue[], value?: Balance, gasLimit: GasLimit }): Transaction { + upgrade({ code, codeMetadata, initArguments, value, gasLimit }: UpgradeArguments): Transaction { codeMetadata = codeMetadata || new CodeMetadata(); - initArgs = initArgs || []; + initArguments = initArguments || []; value = value || Balance.Zero(); let payload = TransactionPayload.contractUpgrade() .setCode(code) .setCodeMetadata(codeMetadata) - .setInitArgs(initArgs) + .setInitArgs(initArguments) .build(); let transaction = new Transaction({ @@ -192,8 +187,7 @@ export class SmartContract implements ISmartContract { /** * Creates a {@link Transaction} for calling (a function of) the Smart Contract. */ - call({ func, args, value, gasLimit } - : { func: ContractFunction, args?: TypedValue[], value?: Balance, gasLimit: GasLimit }): Transaction { + call({ func, args, value, gasLimit, receiver }: CallArguments): Transaction { args = args || []; value = value || Balance.Zero(); @@ -203,7 +197,7 @@ export class SmartContract implements ISmartContract { .build(); let transaction = new Transaction({ - receiver: this.getAddress(), + receiver: receiver ? receiver : this.getAddress(), value: value, gasLimit: gasLimit, data: payload @@ -220,7 +214,7 @@ export class SmartContract implements ISmartContract { async runQuery( provider: IProvider, - { func, args, value, caller }: { func: ContractFunction, args?: TypedValue[], value?: Balance, caller?: Address }) + { func, args, value, caller }: QueryArguments) : Promise { let query = new Query({ address: this.address, diff --git a/src/smartcontracts/smartContractResults.dev.net.spec.ts b/src/smartcontracts/smartContractResults.dev.net.spec.ts index f2e70161..b5993dd7 100644 --- a/src/smartcontracts/smartContractResults.dev.net.spec.ts +++ b/src/smartcontracts/smartContractResults.dev.net.spec.ts @@ -1,17 +1,17 @@ -import { Account } from "../account"; import { NetworkConfig } from "../networkConfig"; -import { getDevnetProvider, loadContractCode, TestWallets } from "../testutils"; +import { loadContractCode, loadTestWallets, TestWallet } from "../testutils"; import { TransactionWatcher } from "../transactionWatcher"; import { ContractFunction, SmartContract } from "."; import { GasLimit } from "../networkParams"; import { assert } from "chai"; +import { chooseProvider } from "../interactive"; describe("fetch transactions from devnet", function () { - let devnet = getDevnetProvider(); - let wallets = new TestWallets(); - let aliceWallet = wallets.alice; - let alice = new Account(aliceWallet.address); - let aliceSigner = aliceWallet.signer; + let devnet = chooseProvider("local-testnet");; + let alice: TestWallet; + before(async function () { + ({ alice } = await loadTestWallets()); + }); it("counter smart contract", async function () { this.timeout(60000); @@ -29,10 +29,10 @@ describe("fetch transactions from devnet", function () { gasLimit: new GasLimit(3000000) }); - transactionDeploy.setNonce(alice.nonce); - await aliceSigner.sign(transactionDeploy); + transactionDeploy.setNonce(alice.account.nonce); + await alice.signer.sign(transactionDeploy); - alice.incrementNonce(); + alice.account.incrementNonce(); // ++ let transactionIncrement = contract.call({ @@ -40,10 +40,10 @@ describe("fetch transactions from devnet", function () { gasLimit: new GasLimit(3000000) }); - transactionIncrement.setNonce(alice.nonce); - await aliceSigner.sign(transactionIncrement); + transactionIncrement.setNonce(alice.account.nonce); + await alice.signer.sign(transactionIncrement); - alice.incrementNonce(); + alice.account.incrementNonce(); // Broadcast & execute await transactionDeploy.send(devnet); diff --git a/src/smartcontracts/smartContractResults.ts b/src/smartcontracts/smartContractResults.ts index c787dfbd..a91152b9 100644 --- a/src/smartcontracts/smartContractResults.ts +++ b/src/smartcontracts/smartContractResults.ts @@ -1,4 +1,3 @@ -import * as errors from "../errors"; import { Address } from "../address"; import { Balance } from "../balance"; import { Hash } from "../hash"; @@ -7,19 +6,22 @@ import { Nonce } from "../nonce"; import { TransactionHash } from "../transaction"; import { ArgSerializer } from "./argSerializer"; import { EndpointDefinition, TypedValue } from "./typesystem"; -import { guardValueIsSet } from "../utils"; import { ReturnCode } from "./returnCode"; +import { Result } from "./result"; export class SmartContractResults { private readonly items: SmartContractResultItem[] = []; - private readonly immediate: ImmediateResult = new ImmediateResult(); - private readonly resultingCalls: ResultingCall[] = []; + private readonly immediate: TypedResult = new TypedResult(); + private readonly resultingCalls: TypedResult[] = []; constructor(items: SmartContractResultItem[]) { this.items = items; if (this.items.length > 0) { - this.immediate = this.findImmediateResult(); + let immediateResult = this.findImmediateResult(); + if (immediateResult) { + this.immediate = immediateResult; + } this.resultingCalls = this.findResultingCalls(); } } @@ -33,25 +35,35 @@ export class SmartContractResults { return new SmartContractResults(items); } - private findImmediateResult(): ImmediateResult { - let immediateItem = this.items[0]; - guardValueIsSet("immediateItem", immediateItem); - return new ImmediateResult(immediateItem); + private findImmediateResult(): TypedResult | undefined { + let immediateItem = this.items.filter(item => isImmediateResult(item))[0]; + if (immediateItem) { + return new TypedResult(immediateItem); + } + return undefined; } - private findResultingCalls(): ResultingCall[] { - let otherItems = this.items.slice(1); - let resultingCalls = otherItems.map(item => new ResultingCall(item)); + private findResultingCalls(): TypedResult[] { + let otherItems = this.items.filter(item => !isImmediateResult(item)); + let resultingCalls = otherItems.map(item => new TypedResult(item)); return resultingCalls; } - getImmediate(): ImmediateResult { + getImmediate(): TypedResult { return this.immediate; } - getResultingCalls(): ResultingCall[] { + 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 { @@ -101,16 +113,15 @@ export class SmartContractResultItem { } getDataTokens(): Buffer[] { - let serializer = new ArgSerializer(); - return serializer.stringToBuffers(this.data); + return new ArgSerializer().stringToBuffers(this.data); } } -export class ImmediateResult extends SmartContractResultItem { +export class TypedResult extends SmartContractResultItem implements Result.IResult { /** - * If available, will provide typed output arguments (with typed values). - */ - private endpointDefinition?: EndpointDefinition; + * If available, will provide typed output arguments (with typed values). + */ + endpointDefinition?: EndpointDefinition; constructor(init?: Partial) { super(); @@ -118,52 +129,46 @@ export class ImmediateResult extends SmartContractResultItem { } assertSuccess() { - if (this.isSuccess()) { - return; - } - - throw new errors.ErrContract(`${this.getReturnCode()}: ${this.returnMessage}`); + Result.assertSuccess(this); } isSuccess(): boolean { - return this.getReturnCode().equals(ReturnCode.Ok); + return this.getReturnCode().isSuccess(); } getReturnCode(): ReturnCode { - // Question for review: is this correct? Is the first parameter of a SCR always void and unused? E.g.: @6f6b@2b. - let returnCodeToken = this.getDataTokens()[1]; + let tokens = this.getDataTokens(); + if (tokens.length < 2) { + return ReturnCode.None; + } + let returnCodeToken = tokens[1]; return ReturnCode.fromBuffer(returnCodeToken); } - outputUntyped(): Buffer[] { this.assertSuccess(); - // Question for review: is this correct? Is the first parameter of a SCR always void and unused? E.g.: @6f6b@2b. + // Skip the first 2 SCRs (eg. the @6f6b from @6f6b@2b). return this.getDataTokens().slice(2); } - outputTyped(): TypedValue[] { - this.assertSuccess(); - - guardValueIsSet("endpointDefinition", this.endpointDefinition); + setEndpointDefinition(endpointDefinition: EndpointDefinition) { + this.endpointDefinition = endpointDefinition; + } - let buffers = this.outputUntyped(); - let values = new ArgSerializer().buffersToValues(buffers, this.endpointDefinition!.output); - return values; + getEndpointDefinition(): EndpointDefinition | undefined { + return this.endpointDefinition; } - setEndpointDefinition(endpointDefinition: EndpointDefinition) { - this.endpointDefinition = endpointDefinition; + getReturnMessage(): string { + return this.returnMessage; } -} -export class ResultingCall extends SmartContractResultItem { - constructor(init?: Partial) { - super(); - Object.assign(this, init); + outputTyped(): TypedValue[] { + return Result.outputTyped(this); } - // TODO: Get as contract call (function, arguments). - // TODO: Tests for ESDT, which call a built-in function. + unpackOutput(): any { + return Result.unpackOutput(this); + } } diff --git a/src/smartcontracts/strictChecker.spec.ts b/src/smartcontracts/strictChecker.spec.ts index 27e39a19..2395cdf2 100644 --- a/src/smartcontracts/strictChecker.spec.ts +++ b/src/smartcontracts/strictChecker.spec.ts @@ -2,7 +2,7 @@ import * as errors from "../errors"; import { StrictChecker as StrictInteractionChecker } from "./strictChecker"; import { SmartContract } from "./smartContract"; import { BigUIntValue, OptionValue, U64Value } from "./typesystem"; -import { loadAbiRegistry, MockProvider, TestWallets } from "../testutils"; +import { loadAbiRegistry } from "../testutils"; import { SmartContractAbi } from "./abi"; import { Address } from "../address"; import { assert } from "chai"; @@ -12,11 +12,8 @@ import BigNumber from "bignumber.js"; import { BytesValue } from "./typesystem/bytes"; describe("integration tests: test checker within interactor", function () { - let wallets = new TestWallets(); let dummyAddress = new Address("erd1qqqqqqqqqqqqqpgqak8zt22wl2ph4tswtyc39namqx6ysa2sd8ss4xmlj3"); let checker = new StrictInteractionChecker(); - let provider = new MockProvider(); - let signer = wallets.alice.signer; it("should detect errors for 'ultimate answer'", async function () { let abiRegistry = await loadAbiRegistry(["src/testdata/answer.abi.json"]); diff --git a/src/smartcontracts/strictChecker.ts b/src/smartcontracts/strictChecker.ts index 4c80fe9a..928bd8db 100644 --- a/src/smartcontracts/strictChecker.ts +++ b/src/smartcontracts/strictChecker.ts @@ -12,7 +12,7 @@ import { IInteractionChecker } from "./interface"; */ export class StrictChecker implements IInteractionChecker { checkInteraction(interaction: Interaction): void { - let definition = interaction.getEndpointDefinition(); + let definition = interaction.getEndpoint(); this.checkPayable(interaction, definition); this.checkArguments(interaction, definition); @@ -45,7 +45,7 @@ export class StrictChecker implements IInteractionChecker { let actualType = argument.getType(); // isAssignableFrom() is responsible to handle covariance and contravariance (depending on the class that overrides it). let ok = expectedType.isAssignableFrom(actualType); - + if (!ok) { throw new errors.ErrContractInteraction(`type mismatch at index ${i}, expected: ${expectedType}, got: ${actualType}`); } diff --git a/src/smartcontracts/transactionPayloadBuilders.ts b/src/smartcontracts/transactionPayloadBuilders.ts index edc265c2..7f4727e0 100644 --- a/src/smartcontracts/transactionPayloadBuilders.ts +++ b/src/smartcontracts/transactionPayloadBuilders.ts @@ -129,7 +129,7 @@ export class ContractCallPayloadBuilder { /** * Sets the function to be called (executed). */ - setFunction (contractFunction: ContractFunction): ContractCallPayloadBuilder { + setFunction(contractFunction: ContractFunction): ContractCallPayloadBuilder { this.contractFunction = contractFunction; return this; } @@ -168,7 +168,6 @@ function appendArgumentsToString(to: string, values: TypedValue[]) { return to; } - let serializer = new ArgSerializer(); - let argumentsString = serializer.valuesToString(values); + let argumentsString = new ArgSerializer().valuesToString(values); return `${to}@${argumentsString}`; } diff --git a/src/smartcontracts/typesystem/abiRegistry.spec.ts b/src/smartcontracts/typesystem/abiRegistry.spec.ts index fbc64508..e8e6ebab 100644 --- a/src/smartcontracts/typesystem/abiRegistry.spec.ts +++ b/src/smartcontracts/typesystem/abiRegistry.spec.ts @@ -26,7 +26,7 @@ describe("test abi registry", () => { assert.lengthOf(registry.interfaces, 3); assert.lengthOf(registry.customTypes, 2); - assert.lengthOf(registry.getInterface("Lottery").endpoints, 7); + assert.lengthOf(registry.getInterface("Lottery").endpoints, 6); assert.lengthOf(registry.getStruct("LotteryInfo").fields, 8); assert.lengthOf(registry.getEnum("Status").variants, 3); }); diff --git a/src/smartcontracts/typesystem/abiRegistry.ts b/src/smartcontracts/typesystem/abiRegistry.ts index 0291fe92..68a08831 100644 --- a/src/smartcontracts/typesystem/abiRegistry.ts +++ b/src/smartcontracts/typesystem/abiRegistry.ts @@ -16,7 +16,7 @@ export class AbiRegistry { * Convenience factory function to load ABIs (from files or URLs). * This function will also remap ABI types to know types (on best-efforts basis). */ - static async load(json: { files?: string[]; urls?: string[] }) { + static async load(json: { files?: string[]; urls?: string[] }): Promise { let registry = new AbiRegistry(); for (const file of json.files || []) { await registry.extendFromFile(file); @@ -117,15 +117,10 @@ export class AbiRegistry { for (const iface of this.interfaces) { let newEndpoints: EndpointDefinition[] = []; for (const endpoint of iface.endpoints) { - let newInput = endpoint.input.map( - (e) => new EndpointParameterDefinition(e.name, e.description, mapper.mapType(e.type)) - ); - let newOutput = endpoint.output.map( - (e) => new EndpointParameterDefinition(e.name, e.description, mapper.mapType(e.type)) - ); - newEndpoints.push(new EndpointDefinition(endpoint.name, newInput, newOutput, endpoint.modifiers)); + newEndpoints.push(mapEndpoint(endpoint, mapper)); } - newInterfaces.push(new ContractInterface(iface.name, newEndpoints)); + let newConstructor = iface.constructorDefinition ? mapEndpoint(iface.constructorDefinition, mapper) : null; + newInterfaces.push(new ContractInterface(iface.name, newConstructor, newEndpoints)); } // Now return the new registry, with all types remapped to known types let newRegistry = new AbiRegistry(); @@ -135,3 +130,13 @@ export class AbiRegistry { return newRegistry; } } + +function mapEndpoint(endpoint: EndpointDefinition, mapper: TypeMapper): EndpointDefinition { + let newInput = endpoint.input.map( + (e) => new EndpointParameterDefinition(e.name, e.description, mapper.mapType(e.type)) + ); + let newOutput = endpoint.output.map( + (e) => new EndpointParameterDefinition(e.name, e.description, mapper.mapType(e.type)) + ); + return new EndpointDefinition(endpoint.name, newInput, newOutput, endpoint.modifiers); +} diff --git a/src/smartcontracts/typesystem/contractInterface.ts b/src/smartcontracts/typesystem/contractInterface.ts index 0a1bb81e..4c23e6ee 100644 --- a/src/smartcontracts/typesystem/contractInterface.ts +++ b/src/smartcontracts/typesystem/contractInterface.ts @@ -8,19 +8,26 @@ const NamePlaceholder = "?"; */ export class ContractInterface { readonly name: string; + readonly constructorDefinition: EndpointDefinition | null; readonly endpoints: EndpointDefinition[] = []; - constructor(name: string, endpoints: EndpointDefinition[]) { + constructor(name: string, constructor_definition: EndpointDefinition | null, endpoints: EndpointDefinition[]) { this.name = name; + this.constructorDefinition = constructor_definition; this.endpoints = endpoints; } - static fromJSON(json: { name: string, endpoints: any[] }): ContractInterface { + static fromJSON(json: { name: string, constructor: any, endpoints: any[] }): ContractInterface { json.name = json.name || NamePlaceholder; json.endpoints = json.endpoints || []; + let constructorDefinition = constructorFromJSON(json); let endpoints = json.endpoints.map(item => EndpointDefinition.fromJSON(item)); - return new ContractInterface(json.name, endpoints); + return new ContractInterface(json.name, constructorDefinition, endpoints); + } + + getConstructorDefinition(): EndpointDefinition | null { + return this.constructorDefinition; } getEndpoint(name: string): EndpointDefinition { @@ -29,3 +36,14 @@ export class ContractInterface { return result!; } } + +function constructorFromJSON(json: any): EndpointDefinition | null { + if (json.constructor.inputs === undefined || json.constructor.outputs === undefined) { + return null; + } + + // the name will be missing, so we add it manually + let constructorWithName = { name: "constructor", ...json.constructor }; + + return EndpointDefinition.fromJSON(constructorWithName); +} diff --git a/src/smartcontracts/typesystem/endpoint.ts b/src/smartcontracts/typesystem/endpoint.ts index 94f9cc7b..280c2333 100644 --- a/src/smartcontracts/typesystem/endpoint.ts +++ b/src/smartcontracts/typesystem/endpoint.ts @@ -1,5 +1,4 @@ import { TypeExpressionParser } from "./typeExpressionParser"; -import { TypeMapper } from "./typeMapper"; import { Type } from "./types"; const NamePlaceholder = "?"; @@ -18,6 +17,10 @@ export class EndpointDefinition { this.modifiers = modifiers; } + isConstructor(): boolean { + return this.name == "constructor"; + } + static fromJSON(json: { name: string, storageModifier: string, @@ -25,7 +28,7 @@ export class EndpointDefinition { inputs: any[], outputs: [] }): EndpointDefinition { - json.name = json.name || NamePlaceholder; + json.name = json.name == null ? NamePlaceholder : json.name; json.payableInTokens = json.payableInTokens || []; json.inputs = json.inputs || []; json.outputs = json.outputs || []; @@ -67,6 +70,10 @@ export class EndpointModifiers { return false; } + isPayable() { + return this.payableInTokens.length != 0; + } + isReadonly() { return this.storageModifier == "readonly"; } diff --git a/src/smartcontracts/typesystem/index.ts b/src/smartcontracts/typesystem/index.ts index 4665cd2c..ed9615b9 100644 --- a/src/smartcontracts/typesystem/index.ts +++ b/src/smartcontracts/typesystem/index.ts @@ -16,6 +16,7 @@ export * from "./generic"; export * from "./h256"; export * from "./matchers"; export * from "./numerical"; +export * from "./string"; export * from "./struct"; export * from "./tuple"; export * from "./tokenIdentifier"; diff --git a/src/smartcontracts/typesystem/numerical.ts b/src/smartcontracts/typesystem/numerical.ts index 4c6cc214..7baf39f7 100644 --- a/src/smartcontracts/typesystem/numerical.ts +++ b/src/smartcontracts/typesystem/numerical.ts @@ -93,11 +93,11 @@ export class NumericalValue extends PrimitiveValue { constructor(type: NumericalType, value: BigNumber) { super(type); guardType("type", NumericalType, type, false); - + if (!(value instanceof BigNumber)) { throw new errors.ErrInvalidArgument("value", value, "not a big number"); } - + this.value = value; this.sizeInBytes = type.sizeInBytes; this.withSign = type.withSign; diff --git a/src/smartcontracts/typesystem/string.ts b/src/smartcontracts/typesystem/string.ts new file mode 100644 index 00000000..dec860bb --- /dev/null +++ b/src/smartcontracts/typesystem/string.ts @@ -0,0 +1,46 @@ +import { PrimitiveType, PrimitiveValue } from "./types"; + +export class StringType extends PrimitiveType { + constructor() { + super("utf-8 string"); + } +} + +export class StringValue extends PrimitiveValue { + private readonly value: string; + + constructor(value: string) { + super(new StringType()); + this.value = value; + } + + /** + * Creates a StringValue from a utf-8 string. + */ + static fromUTF8(value: string): StringValue { + return new StringValue(value); + } + + /** + * Creates a StringValue from a hex-encoded string. + */ + static fromHex(value: string): StringValue { + let decodedValue = Buffer.from(value, "hex").toString(); + return new StringValue(decodedValue); + } + + getLength(): number { + return this.value.length; + } + + /** + * Returns whether two objects have the same value. + */ + equals(other: StringValue): boolean { + return this.value === other.value; + } + + valueOf(): string { + return this.value; + } +} diff --git a/src/smartcontracts/typesystem/typeExpressionParser.spec.ts b/src/smartcontracts/typesystem/typeExpressionParser.spec.ts index 2faf55b1..cf92dd09 100644 --- a/src/smartcontracts/typesystem/typeExpressionParser.spec.ts +++ b/src/smartcontracts/typesystem/typeExpressionParser.spec.ts @@ -4,198 +4,227 @@ import { Type } from "./types"; import { TypeExpressionParser } from "./typeExpressionParser"; describe("test parser", () => { - let parser = new TypeExpressionParser(); + let parser = new TypeExpressionParser(); - it("should parse expression", () => { - let type: Type; + it("should parse expression", () => { + let type: Type; - type = parser.parse("u32"); - assert.deepEqual(type.toJSON(), { - name: "u32", - typeParameters: [], - }); + type = parser.parse("u32"); + assert.deepEqual(type.toJSON(), { + name: "u32", + typeParameters: [], + }); - type = parser.parse("List"); - assert.deepEqual(type.toJSON(), { - name: "List", - typeParameters: [ - { - name: "u32", - typeParameters: [], - }, - ], - }); + type = parser.parse("List"); + assert.deepEqual(type.toJSON(), { + name: "List", + typeParameters: [ + { + name: "u32", + typeParameters: [], + }, + ], + }); - type = parser.parse("Option>"); - assert.deepEqual(type.toJSON(), { - name: "Option", - typeParameters: [ - { - name: "List", - typeParameters: [ - { - name: "Address", - typeParameters: [], - }, - ], - }, - ], - }); + type = parser.parse("Option>"); + assert.deepEqual(type.toJSON(), { + name: "Option", + typeParameters: [ + { + name: "List", + typeParameters: [ + { + name: "Address", + typeParameters: [], + }, + ], + }, + ], + }); - type = parser.parse("VarArgs>"); - assert.deepEqual(type.toJSON(), { - name: "VarArgs", - typeParameters: [ - { - name: "MultiArg", - typeParameters: [ - { - name: "bytes", - typeParameters: [], - }, - { - name: "Address", - typeParameters: [], - }, - ], - }, - ], - }); + type = parser.parse("VarArgs>"); + assert.deepEqual(type.toJSON(), { + name: "VarArgs", + typeParameters: [ + { + name: "MultiArg", + typeParameters: [ + { + name: "bytes", + typeParameters: [], + }, + { + name: "Address", + typeParameters: [], + }, + ], + }, + ], + }); - type = parser.parse("MultiResultVec>"); - assert.deepEqual(type.toJSON(), { - name: "MultiResultVec", - typeParameters: [ - { - name: "MultiResult", - typeParameters: [ - { - name: "Address", - typeParameters: [], - }, - { - name: "u64", - typeParameters: [], - }, - ], - }, - ], - }); + type = parser.parse("MultiResultVec>"); + assert.deepEqual(type.toJSON(), { + name: "MultiResultVec", + typeParameters: [ + { + name: "MultiResult", + typeParameters: [ + { + name: "Address", + typeParameters: [], + }, + { + name: "u64", + typeParameters: [], + }, + ], + }, + ], + }); + + type = parser.parse("MultiResultVec>"); + assert.deepEqual(type.toJSON(), { + name: "MultiResultVec", + typeParameters: [ + { + name: "MultiResult", + typeParameters: [ + { + name: "i32", + typeParameters: [], + }, + { + name: "bytes", + typeParameters: [], + }, + ], + }, + ], + }); - type = parser.parse("MultiResultVec>"); - assert.deepEqual(type.toJSON(), { - name: "MultiResultVec", - typeParameters: [ - { - name: "MultiResult", - typeParameters: [ - { - name: "i32", - typeParameters: [], - }, - { - name: "bytes", - typeParameters: [], - }, - ], - }, - ], + // TODO: In a future PR, replace the JSON-based parsing logic with a better one and enable this test, + // which currently fails. + + // type = parser.parse("MultiArg, List>"); + // assert.deepEqual(type.toJSON(), { + // "name": "MultiArg", + // "typeParameters": [ + // { + // "name": "Option", + // "typeParameters": [ + // { + // "name": "u8", + // "typeParameters": [] + // } + // ] + // }, + // { + // "name": "List", + // "typeParameters": [ + // { + // "name": "u16", + // "typeParameters": [] + // } + // ] + // } + // ] + // }); }); - // TODO: In a future PR, replace the JSON-based parsing logic with a better one and enable this test, - // which currently fails. - - // type = parser.parse("MultiArg, List>"); - // assert.deepEqual(type.toJSON(), { - // "name": "MultiArg", - // "typeParameters": [ - // { - // "name": "Option", - // "typeParameters": [ - // { - // "name": "u8", - // "typeParameters": [] - // } - // ] - // }, - // { - // "name": "List", - // "typeParameters": [ - // { - // "name": "u16", - // "typeParameters": [] - // } - // ] - // } - // ] - // }); - }); - - it("should parse expression: tuples", () => { - let type: Type; - - type = parser.parse("tuple2"); - assert.deepEqual(type.toJSON(), { - name: "tuple2", - typeParameters: [ - { - name: "i32", - typeParameters: [], - }, - { - name: "bytes", - typeParameters: [], - }, - ], + it("should parse expression: tuples", () => { + let type: Type; + + type = parser.parse("tuple2"); + assert.deepEqual(type.toJSON(), { + name: "tuple2", + typeParameters: [ + { + name: "i32", + typeParameters: [], + }, + { + name: "bytes", + typeParameters: [], + }, + ], + }); + + type = parser.parse("tuple3>"); + assert.deepEqual(type.toJSON(), { + name: "tuple3", + typeParameters: [ + { + name: "i32", + typeParameters: [], + }, + { + name: "bytes", + typeParameters: [], + }, + { + name: "Option", + typeParameters: [ + { + name: "i64", + typeParameters: [], + }, + ], + }, + ], + }); + + // TODO: In a future PR, replace the JSON-based parsing logic with a better one and enable this test. + // This test currently fails because JSON key de-duplication takes place: i32 is incorrectly de-duplicated by the parser. + type = parser.parse("tuple2"); + assert.deepEqual(type.toJSON(), { + name: "tuple2", + typeParameters: [ + { + name: "i32", + typeParameters: [], + }, + { + name: "i32", + typeParameters: [], + }, + ], + }); }); - type = parser.parse("tuple3>"); - assert.deepEqual(type.toJSON(), { - name: "tuple3", - typeParameters: [ - { - name: "i32", - typeParameters: [], - }, - { - name: "bytes", - typeParameters: [], - }, - { - name: "Option", - typeParameters: [ - { - name: "i64", - typeParameters: [], - }, - ], - }, - ], + it("should handle utf-8 string types which contain spaces", () => { + let type: Type; + + type = parser.parse("tuple3>"); + assert.deepEqual(type.toJSON(), { + name: "tuple3", + typeParameters: [ + { + name: "utf-8 string", + typeParameters: [], + }, + { + name: "bytes", + typeParameters: [], + }, + { + name: "Option", + typeParameters: [ + { + name: "utf-8 string", + typeParameters: [], + }, + ], + }, + ], + }); + }); - // TODO: In a future PR, replace the JSON-based parsing logic with a better one and enable this test. - // This test currently fails because JSON key de-duplication takes place: i32 is incorrectly de-duplicated by the parser. - type = parser.parse("tuple2"); - assert.deepEqual(type.toJSON(), { - name: "tuple2", - typeParameters: [ - { - name: "i32", - typeParameters: [], - }, - { - name: "i32", - typeParameters: [], - }, - ], + it("should not parse expression", () => { + assert.throw(() => parser.parse("<>"), errors.ErrTypingSystem); + assert.throw(() => parser.parse("<"), errors.ErrTypingSystem); + // TODO: In a future PR replace Json Parsing logic with a better one and enable this test + //assert.throw(() => parser.parse("MultiResultVec"), errors.ErrTypingSystem); + assert.throw(() => parser.parse("a, b"), errors.ErrTypingSystem); }); - }); - - it("should not parse expression", () => { - assert.throw(() => parser.parse("<>"), errors.ErrTypingSystem); - assert.throw(() => parser.parse("<"), errors.ErrTypingSystem); - // TODO: In a future PR replace Json Parsing logic with a better one and enable this test - //assert.throw(() => parser.parse("MultiResultVec"), errors.ErrTypingSystem); - assert.throw(() => parser.parse("a, b"), errors.ErrTypingSystem); - }); }); diff --git a/src/smartcontracts/typesystem/typeExpressionParser.ts b/src/smartcontracts/typesystem/typeExpressionParser.ts index d0201e85..ab0ee6e6 100644 --- a/src/smartcontracts/typesystem/typeExpressionParser.ts +++ b/src/smartcontracts/typesystem/typeExpressionParser.ts @@ -3,83 +3,84 @@ import { Type } from "./types"; var jsonHandler = require("json-duplicate-key-handle"); export class TypeExpressionParser { - parse(expression: string): Type { - let root = this.doParse(expression); - let rootKeys = Object.keys(root); + parse(expression: string): Type { + let root = this.doParse(expression); + let rootKeys = Object.keys(root); - if (rootKeys.length != 1) { - throw new errors.ErrTypingSystem(`bad type expression: ${expression}`); - } + if (rootKeys.length != 1) { + throw new errors.ErrTypingSystem(`bad type expression: ${expression}`); + } - let name = rootKeys[0]; - let type = this.nodeToType(name, root[name]); - return type; - } + let name = rootKeys[0]; + let type = this.nodeToType(name, root[name]); + return type; + } - private doParse(expression: string): any { - let jsoned = this.getJsonedString(expression); + private doParse(expression: string): any { + let jsoned = this.getJsonedString(expression); - try { - return jsonHandler.parse(jsoned); - } catch (error) { - throw new errors.ErrTypingSystem(`cannot parse type expression: ${expression}. internal json: ${jsoned}.`); + try { + return jsonHandler.parse(jsoned); + } catch (error) { + throw new errors.ErrTypingSystem(`cannot parse type expression: ${expression}. internal json: ${jsoned}.`); + } } - } - /** - * Converts a raw type expression to a JSON, parsing-friendly format. - * This is a workaround, so that the parser implementation is simpler (thus we actually rely on the JSON parser). - * - * @param expression a string such as: - * - * ``` - * - Option> - * - VarArgs> - * - MultiResultVec - * ``` - */ - private getJsonedString(expression: string) { - let jsoned = ""; + /** + * Converts a raw type expression to a JSON, parsing-friendly format. + * This is a workaround, so that the parser implementation is simpler (thus we actually rely on the JSON parser). + * + * @param expression a string such as: + * + * ``` + * - Option> + * - VarArgs> + * - MultiResultVec + * ``` + */ + private getJsonedString(expression: string) { + let jsoned = ""; - for (var i = 0; i < expression.length; i++) { - let char = expression.charAt(i); - let previousChar = expression.charAt(i - 1); - let nextChar = expression.charAt(i + 1); + for (var i = 0; i < expression.length; i++) { + let char = expression.charAt(i); + let previousChar = expression.charAt(i - 1); + let nextChar = expression.charAt(i + 1); - if (char == "<") { - jsoned += ": {"; - } else if (char == ">") { - if (previousChar != ">") { - jsoned += ": {} }"; - } else { - jsoned += "}"; - } - } else if (char == ",") { - if (nextChar == ">") { - // Skip superfluous comma - } else { - jsoned += ": {},"; + if (char == "<") { + jsoned += ": {"; + } else if (char == ">") { + if (previousChar != ">") { + jsoned += ": {} }"; + } else { + jsoned += "}"; + } + } else if (char == ",") { + if (nextChar == ">") { + // Skip superfluous comma + } else { + jsoned += ": {},"; + } + } else { + jsoned += char; + } } - } else { - jsoned += char; - } - } - let symbolsRegex = /(:|\{|\}|,|\s)/; - let tokens = jsoned.split(symbolsRegex).filter((token) => token); - jsoned = tokens.map((token) => (symbolsRegex.test(token) ? token : `"${token}"`)).join(""); + // Split by the delimiters, but exclude the spaces that are found in the middle of "utf-8 string" + let symbolsRegex = /(:|\{|\}|,|(? token); + jsoned = tokens.map((token) => (symbolsRegex.test(token) ? token : `"${token}"`)).join(""); - if (tokens.length == 1) { - // Workaround for simple, non-generic types. - return `{${jsoned}: {}}`; - } + if (tokens.length == 1) { + // Workaround for simple, non-generic types. + return `{${jsoned}: {}}`; + } - return `{${jsoned}}`; - } + return `{${jsoned}}`; + } - private nodeToType(name: string, node: any): Type { - if (name.charAt(name.length - 1) === "1") name = name.slice(0, -1); - let typeParameters = Object.keys(node).map((key) => this.nodeToType(key, node[key])); - return new Type(name, typeParameters); - } + private nodeToType(name: string, node: any): Type { + if (name.charAt(name.length - 1) === "1") { name = name.slice(0, -1); } + let typeParameters = Object.keys(node).map((key) => this.nodeToType(key, node[key])); + return new Type(name, typeParameters); + } } diff --git a/src/smartcontracts/typesystem/typeMapper.ts b/src/smartcontracts/typesystem/typeMapper.ts index cf751c85..c57e83aa 100644 --- a/src/smartcontracts/typesystem/typeMapper.ts +++ b/src/smartcontracts/typesystem/typeMapper.ts @@ -23,7 +23,7 @@ import { TokenIdentifierType } from "./tokenIdentifier"; import { Type, CustomType } from "./types"; import { VariadicType } from "./variadic"; import { OptionalType } from "./algebraic"; -import { TupleType } from "."; +import { StringType, TupleType } from "."; type TypeConstructor = new (...typeParameters: Type[]) => Type; @@ -42,6 +42,7 @@ export class TypeMapper { ["OptionalArg", OptionalType], ["optional", OptionalType], ["OptionalResult", OptionalType], + ["multi", CompositeType], ["MultiArg", CompositeType], ["MultiResult", CompositeType], ["multi", CompositeType], @@ -62,16 +63,19 @@ export class TypeMapper { ["u16", new U16Type()], ["u32", new U32Type()], ["u64", new U64Type()], + ["U64", new U64Type()], ["BigUint", new BigUIntType()], ["i8", new I8Type()], ["i16", new I16Type()], ["i32", new I32Type()], ["i64", new I64Type()], ["Bigint", new BigIntType()], + ["BigInt", new BigIntType()], ["bool", new BooleanType()], ["bytes", new BytesType()], ["Address", new AddressType()], ["H256", new H256Type()], + ["utf-8 string", new StringType()], ["TokenIdentifier", new TokenIdentifierType()], ]); diff --git a/src/smartcontracts/typesystem/types.spec.ts b/src/smartcontracts/typesystem/types.spec.ts index 94b33134..ffafb693 100644 --- a/src/smartcontracts/typesystem/types.spec.ts +++ b/src/smartcontracts/typesystem/types.spec.ts @@ -1,7 +1,7 @@ import * as errors from "../../errors"; import { assert } from "chai"; -import { NumericalValue } from "."; -import { I64Type, U16Type, U32Type, U32Value } from "./numerical"; +import { NumericalValue, StringType } from "."; +import { I64Type, U16Type, U32Type, U32Value, U8Type } from "./numerical"; import { PrimitiveType, Type } from "./types"; import { BooleanType } from "./boolean"; import { AddressType } from "./address"; @@ -11,7 +11,7 @@ import BigNumber from "bignumber.js"; describe("test types", () => { let parser = new TypeExpressionParser(); - + it("for numeric values, should throw error when invalid input", () => { assert.throw(() => new U32Value(new BigNumber(-42)), errors.ErrInvalidArgument); assert.throw(() => new NumericalValue(new U16Type(), Number(42)), errors.ErrInvalidArgument); @@ -42,5 +42,6 @@ describe("test types", () => { assert.isTrue(parser.parse("MultiResultVec").equals(parser.parse("MultiResultVec"))); assert.isFalse(parser.parse("MultiResultVec").equals(parser.parse("MultiResultVec"))); assert.isTrue(parser.parse("Option").equals(new OptionType(new U32Type()))); + assert.isTrue(parser.parse("utf-8 string").equals(new StringType())); }); }); diff --git a/src/smartcontracts/typesystem/types.ts b/src/smartcontracts/typesystem/types.ts index d149f3f7..3c133111 100644 --- a/src/smartcontracts/typesystem/types.ts +++ b/src/smartcontracts/typesystem/types.ts @@ -101,9 +101,6 @@ export class Type { }; } - // Question for review: though nice and might help us in the future, this concept (assigning a fixed or variable value-cardinality to each type) - // isn't extremly useful at this moment (except for some checks in the codecs - e.g. "only types with cardinality are directly encodable" - non-composite, non-variadic). - // Keep it or remove it? getCardinality(): TypeCardinality { return this.cardinality; } diff --git a/src/smartcontracts/wrapper/chainSendContext.ts b/src/smartcontracts/wrapper/chainSendContext.ts new file mode 100644 index 00000000..c5dc7011 --- /dev/null +++ b/src/smartcontracts/wrapper/chainSendContext.ts @@ -0,0 +1,35 @@ +import { Balance, ContractLogger, SendContext } from "../.."; +import { TestWallet } from "../../testutils"; + +export class ChainSendContext { + readonly context: SendContext; + + constructor(context: SendContext) { + this.context = context; + } + + sender(caller: TestWallet): this { + this.context.sender(caller); + return this; + } + + gas(gas: number): this { + this.context.gas(gas); + return this; + } + + autoGas(baseGas: number): this { + this.context.autoGas(baseGas); + return this; + } + + value(value: Balance): this { + this.context.value(value); + return this; + } + + logger(logger: ContractLogger | null): this { + this.context.logger(logger); + return this; + } +} diff --git a/src/smartcontracts/wrapper/contractLogger.ts b/src/smartcontracts/wrapper/contractLogger.ts new file mode 100644 index 00000000..9cf9c552 --- /dev/null +++ b/src/smartcontracts/wrapper/contractLogger.ts @@ -0,0 +1,53 @@ +import { Address, NetworkConfig, Query, QueryResponse, SmartContractResults, Transaction, TypedResult } from "../.."; + +/** + * Provides a simple interface in order to easily call or query the smart contract's methods. + */ +export class ContractLogger { + + synchronizedNetworkConfig(networkConfig: NetworkConfig) { + console.log(`Synchronized network config - chainID: ${networkConfig.ChainID.valueOf()}`); + } + + transactionCreated(transaction: Transaction) { + console.log(`Tx ${transaction.getHash()} created. Sending...`); + } + + deployComplete(transaction: Transaction, smartContractResults: SmartContractResults, smartContractAddress: Address) { + logReturnMessages(transaction, smartContractResults); + console.log(`done. (address: ${smartContractAddress.bech32()} )`); + } + + transactionSent(_transaction: Transaction) { + console.log(`awaiting results...`); + } + + transactionComplete(_result: any, _resultData: string, transaction: Transaction, smartContractResults: SmartContractResults) { + logReturnMessages(transaction, smartContractResults); + console.log(`done.`); + } + + queryCreated(_query: Query) { + console.log(`Query created. Sending...`); + } + + queryComplete(_result: any, _response: QueryResponse) { + console.log(`done.`); + } +} + +function logReturnMessages(transaction: Transaction, smartContractResults: SmartContractResults) { + let immediate = smartContractResults.getImmediate(); + logSmartContractResultIfMessage("(immediate)", transaction, immediate); + + let resultingCalls = smartContractResults.getResultingCalls(); + 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}`); + } +} diff --git a/src/smartcontracts/wrapper/contractWrapper.dev.net.spec.ts b/src/smartcontracts/wrapper/contractWrapper.dev.net.spec.ts new file mode 100644 index 00000000..65178678 --- /dev/null +++ b/src/smartcontracts/wrapper/contractWrapper.dev.net.spec.ts @@ -0,0 +1,72 @@ +import { SystemWrapper, Balance, setupInteractive } from "../.."; +import { assert } from "chai"; +import { BigNumber } from "bignumber.js"; +import { TestWallet } from "../../testutils"; + + +describe("test smart contract interactor", function () { + let erdSys: SystemWrapper; + let alice: TestWallet; + + before(async function () { + ({ erdSys, wallets: { alice } } = await setupInteractive("local-testnet")); + }); + + it("should interact with 'answer' (local testnet)", async function () { + // Currently, this has to be called before creating any Interaction objects, + // because the Transaction objects created under the hood point to the "default" NetworkConfig. + this.timeout(60000); + + let answer = await erdSys.loadWrapper("src/testdata", "answer"); + + await answer.sender(alice).gas(3_000_000).call.deploy(); + + // Query + let queryResponse = await answer.query.getUltimateAnswer(); + assert.deepEqual(queryResponse, new BigNumber(42)); + + // Call + let callResponse = await answer.call.getUltimateAnswer(); + assert.deepEqual(callResponse, new BigNumber(42)); + }); + + it("should interact with 'counter' (local testnet)", async function () { + this.timeout(120000); + + let counter = await erdSys.loadWrapper("src/testdata", "counter"); + + await counter.sender(alice).gas(3_000_000).call.deploy(); + assert.deepEqual(await counter.query.get(), new BigNumber(1)); + assert.deepEqual(await counter.call.increment(), new BigNumber(2)); + assert.deepEqual(await counter.call.decrement(), new BigNumber(1)); + assert.deepEqual(await counter.call.decrement(), new BigNumber(0)); + }); + + it("should interact with 'lottery_egld' (local testnet)", async function () { + this.timeout(120000); + + let lottery = await erdSys.loadWrapper("src/testdata", "lottery_egld"); + + await lottery.sender(alice).gas(100_000_000).call.deploy(); + + lottery.gas(15_000_000); + await lottery.call.start("lucky", Balance.egld(1), null, null, 1, null, null); + + let status = await lottery.query.status("lucky"); + assert.equal(status.valueOf(), "Running"); + + let info = await lottery.query.lotteryInfo("lucky"); + // Ignore "deadline" field in our test + delete info.deadline; + + assert.deepEqual(info, { + ticket_price: new BigNumber("1000000000000000000"), + 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 new file mode 100644 index 00000000..17a215ec --- /dev/null +++ b/src/smartcontracts/wrapper/contractWrapper.spec.ts @@ -0,0 +1,96 @@ +import { AddImmediateResult, MarkNotarized, MockProvider, setupUnitTestWatcherTimeouts, TestWallet } from "../../testutils"; +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"; +import { Egld, setupInteractiveWithProvider } from "../.."; + +describe("test smart contract wrapper", async function () { + let dummyAddress = new Address("erd1qqqqqqqqqqqqqpgqak8zt22wl2ph4tswtyc39namqx6ysa2sd8ss4xmlj3"); + let erdSys: SystemWrapper; + let provider = new MockProvider(); + let alice: TestWallet; + before(async function () { + ({ erdSys, wallets: { alice } } = await setupInteractiveWithProvider(provider)); + }); + + it("should interact with 'answer'", async function () { + setupUnitTestWatcherTimeouts(); + + let answer = await erdSys.loadWrapper("src/testdata", "answer"); + answer.address(dummyAddress).sender(alice).gas(500_000); + + mockQuery(provider, "getUltimateAnswer", "Kg=="); + + let queryResult = await answer.query.getUltimateAnswer(); + assert.deepEqual(queryResult, new BigNumber(42)); + + let callResult = await mockCall(provider, "@6f6b@2b", answer.call.getUltimateAnswer()); + assert.deepEqual(callResult, new BigNumber(43)); + }); + + it("should interact with 'counter'", async function () { + setupUnitTestWatcherTimeouts(); + + let counter = await erdSys.loadWrapper("src/testdata", "counter"); + counter.address(dummyAddress).sender(alice).gas(500_000); + + // For "get()", return fake 7 + mockQuery(provider, "get", "Bw=="); + + let counterValue = await counter.query.get(); + assert.deepEqual(counterValue, new BigNumber(7)); + + // Return fake 8 + let valueAfterIncrement = await mockCall(provider, "@6f6b@08", counter.call.increment()); + assert.deepEqual(valueAfterIncrement, new BigNumber(8)); + + // Decrement. Return fake 7. + let decrementResult = await mockCall(provider, "@6f6b@07", counter.call.decrement()); + assert.deepEqual(decrementResult, new BigNumber(7)); + }); + + it("should interact with 'lottery_egld'", async function () { + setupUnitTestWatcherTimeouts(); + + let lottery = await erdSys.loadWrapper("src/testdata", "lottery_egld"); + lottery.address(dummyAddress).sender(alice).gas(5_000_000); + + await mockCall(provider, "@6f6b", lottery.call.start("lucky", Egld(1), null, null, 1, null, null)); + + let status = await mockCall(provider, "@6f6b@01", lottery.call.status("lucky")); + assert.equal(status, "Running"); + + let info = await mockCall( + provider, + "@6f6b@000000080de0b6b3a764000000000320000000006012a806000000010000000164000000000000000000000000", + lottery.call.lotteryInfo("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), + prize_distribution: Buffer.from([0x64]), + whitelist: [], + current_ticket_number: new BigNumber(0), + prize_pool: new BigNumber("0") + }); + }); +}); + +function mockQuery(provider: MockProvider, functionName: string, mockedResult: string) { + provider.mockQueryResponseOnFunction(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; +} diff --git a/src/smartcontracts/wrapper/contractWrapper.ts b/src/smartcontracts/wrapper/contractWrapper.ts new file mode 100644 index 00000000..cc164b17 --- /dev/null +++ b/src/smartcontracts/wrapper/contractWrapper.ts @@ -0,0 +1,319 @@ +import path from "path"; +import fs from "fs"; +import { SmartContract, SmartContractAbi, ExecutionResultsBundle, Code, EndpointDefinition, Interaction, NativeTypes, NativeSerializer, EndpointParameterDefinition, AddressType } from ".."; +import { ChainSendContext } from "./chainSendContext"; +import { generateMethods, Methods } from "./generateMethods"; +import { formatEndpoint, FormattedCall } from "./formattedCall"; +import { ArgumentErrorContext } from "../argumentErrorContext"; +import { Address, Balance, Egld, Err, ErrContract, ErrInvalidArgument, IProvider, Transaction } from "../.."; +import { PreparedCall } from "./preparedCall"; +import { TransactionOnNetwork } from "../../transactionOnNetwork"; +import { ContractLogger, SendContext } from "."; +import { loadContractCode } from "../../testutils"; + +/** + * Provides a simple interface in order to easily call or query the smart contract's methods. + */ +export class ContractWrapper extends ChainSendContext { + readonly context: SendContext; + private readonly smartContract: SmartContract; + private readonly wasmPath: string | null; + private readonly abi: SmartContractAbi; + private readonly builtinFunctions: ContractWrapper; + readonly call: Methods>; + readonly results: Methods>; + readonly query: Methods>; + readonly format: Methods; + + private constructor( + smartContract: SmartContract, + abi: SmartContractAbi, + wasmPath: string | null, + context: SendContext, + builtinFunctions: ContractWrapper | null, + ) { + super(context); + this.context = context; + this.smartContract = smartContract; + this.abi = abi; + this.wasmPath = wasmPath; + this.builtinFunctions = builtinFunctions || this; + + + this.call = generateMethods(this, this.abi, this.handleCall); + + this.results = generateMethods(this, this.abi, this.handleResults); + + this.query = generateMethods(this, this.abi, this.handleQuery); + + this.format = generateMethods(this, this.abi, this.handleFormat); + + let constructor = this.abi.getConstructorDefinition(); + if (constructor !== null) { + this.call.deploy = this.handleDeployCall.bind(this, constructor); + this.format.deploy = this.handleFormat.bind(this, constructor); + } + } + + address(address: NativeTypes.NativeAddress): ContractWrapper { + let typedAddress = NativeSerializer.convertNativeToAddress(address, new ArgumentErrorContext("address", "0", new EndpointParameterDefinition("address", "", new AddressType()))); + this.smartContract.setAddress(typedAddress); + return this; + } + + getAddress(): Address { + return this.smartContract.getAddress(); + } + + getAbi(): SmartContractAbi { + return this.abi; + } + + getSmartContract(): SmartContract { + return this.smartContract; + } + + async getCode(): Promise { + if (this.wasmPath == null) { + throw new Err("contract wasm path not configured"); + } + return await loadContractCode(this.wasmPath); + } + + private async buildDeployTransaction(constructorDefinition: EndpointDefinition, args: any[]): Promise { + let contractCode = await this.getCode(); + + let convertedArgs = formatEndpoint(constructorDefinition, constructorDefinition, ...args).toTypedValues(); + let transaction = this.smartContract.deploy({ + code: contractCode, + gasLimit: this.context.getGasLimit(), + initArguments: convertedArgs + }); + return transaction; + } + + private async handleDeployCall(constructorDefinition: EndpointDefinition, ...args: any[]): Promise { + let transaction = await this.buildDeployTransaction(constructorDefinition, args); + + let transactionOnNetwork = await this.processTransaction(transaction); + + let smartContractResults = transactionOnNetwork.getSmartContractResults(); + let immediateResult = smartContractResults.getImmediate(); + immediateResult.assertSuccess(); + let logger = this.context.getLogger(); + logger?.deployComplete(transaction, smartContractResults, this.smartContract.getAddress()); + } + + static async loadProject(provider: IProvider, builtinFunctions: ContractWrapper | null, projectPath: string, filenameHint?: string, sendContext?: SendContext): Promise { + let { abiPath, wasmPath } = await expandProjectPath(projectPath, filenameHint); + let abi = await SmartContractAbi.fromAbiPath(abiPath); + let smartContract = new SmartContract({ abi: abi }); + + sendContext = sendContext || new SendContext(provider).logger(new ContractLogger()); + return new ContractWrapper(smartContract, abi, wasmPath, sendContext, builtinFunctions); + } + + async handleQuery(endpoint: EndpointDefinition, ...args: any[]): Promise { + let preparedCall = await this.prepareCallWithPayment(endpoint, args); + let interaction = this.convertPreparedCallToInteraction(preparedCall); + let provider = this.context.getProvider(); + let logger = this.context.getLogger(); + + let query = interaction.buildQuery(); + logger?.queryCreated(query); + let optionalSender = this.context.getSenderOptional(); + if (optionalSender != null) { + query.caller = optionalSender.address; + } + let response = await provider.queryContract(query); + let queryResponseBundle = interaction.interpretQueryResponse(response); + let result = queryResponseBundle.queryResponse.unpackOutput(); + logger?.queryComplete(result, response); + + return result; + } + + async handleCall(endpoint: EndpointDefinition, ...args: any[]): Promise { + let { transaction, interaction } = this.buildTransactionAndInteraction(endpoint, args); + let { result } = await this.processTransactionAndInterpretResults({ transaction, interaction }); + return result; + } + + async handleResults(endpoint: EndpointDefinition, ...args: any[]): Promise { + let { transaction, interaction } = this.buildTransactionAndInteraction(endpoint, args); + let { executionResultsBundle } = await this.processTransactionAndInterpretResults({ transaction, interaction }); + return executionResultsBundle; + } + + async processTransactionAndInterpretResults({ transaction, interaction }: { + transaction: Transaction, + interaction: Interaction + }): Promise<{ executionResultsBundle: ExecutionResultsBundle, result: any }> { + let transactionOnNetwork = await this.processTransaction(transaction); + let executionResultsBundle = interaction.interpretExecutionResults(transactionOnNetwork); + let { smartContractResults, immediateResult } = executionResultsBundle; + let result = immediateResult?.unpackOutput(); + let logger = this.context.getLogger(); + logger?.transactionComplete(result, immediateResult?.data, transaction, smartContractResults); + return { executionResultsBundle, result }; + } + + async processTransaction(transaction: Transaction): Promise { + let provider = this.context.getProvider(); + let sender = this.context.getSender(); + transaction.setNonce(sender.account.nonce); + await sender.signer.sign(transaction); + + let logger = this.context.getLogger(); + logger?.transactionCreated(transaction); + await transaction.send(provider); + + // increment the nonce only after the transaction is sent + // since an exception thrown by the provider means we will have to re-use the same nonce + // otherwise the next transactions will hang (and never complete) + sender.account.incrementNonce(); + + logger?.transactionSent(transaction); + await transaction.awaitExecuted(provider); + let transactionOnNetwork = await transaction.getAsOnNetwork(provider, true, false, true); + if (transaction.getStatus().isFailed()) { + // TODO: extract the error messages + //let results = transactionOnNetwork.getSmartContractResults().getAllResults(); + //let messages = results.map((result) => console.log(result)); + throw new ErrContract(`Transaction status failed: [${transaction.getStatus().toString()}].`);// Return messages:\n${messages}`); + } + return transactionOnNetwork; + } + + handleFormat(endpoint: EndpointDefinition, ...args: any[]): FormattedCall { + let { formattedCall } = this.prepareCallWithPayment(endpoint, args); + return formattedCall; + } + + buildTransactionAndInteraction(endpoint: EndpointDefinition, args: any[]): { transaction: Transaction, interaction: Interaction } { + let preparedCall = this.prepareCallWithPayment(endpoint, args); + let interaction = this.convertPreparedCallToInteraction(preparedCall); + interaction.withGasLimit(this.context.getGasLimit()); + let transaction = interaction.buildTransaction(); + return { transaction, interaction }; + } + + prepareCallWithPayment(endpoint: EndpointDefinition, args: any[]): PreparedCall { + let value = this.context.getAndResetValue(); + if (value == null && endpoint.modifiers.isPayable()) { + throw new Err("Did not provide any value for a payable method"); + } + if (value != null && !endpoint.modifiers.isPayable()) { + throw new Err("A value was provided for a non-payable method"); + } + if (value != null && !endpoint.modifiers.isPayableInToken(value.token.getTokenIdentifier())) { + throw new Err(`Token ${value.token.getTokenIdentifier()} is not accepted by payable method. Accepted tokens: ${endpoint.modifiers.payableInTokens}`); + } + let formattedCall = formatEndpoint(endpoint, endpoint, ...args); + let preparedCall = new PreparedCall(this.smartContract.getAddress(), Egld(0), formattedCall); + this.applyValueModfiers(value, preparedCall); + return preparedCall; + } + + convertPreparedCallToInteraction(preparedCall: PreparedCall): Interaction { + 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); + interaction.withValue(preparedCall.egldValue); + return interaction; + } + + applyValueModfiers(value: Balance | null, preparedCall: PreparedCall) { + if (value == null) { + return; + } + if (value.token.isEgld()) { + preparedCall.egldValue = value; + return; + } + if (value.token.isFungible()) { + preparedCall.wrap( + this.builtinFunctions.format.ESDTTransfer( + value.token.getTokenIdentifier(), + value.valueOf(), + preparedCall.formattedCall + ) + ); + } else { + preparedCall.receiver = this.context.getSender().address; + preparedCall.wrap( + this.builtinFunctions.format.ESDTNFTTransfer( + value.token.getTokenIdentifier(), + value.getNonce(), + value.valueOf(), + this.smartContract, + preparedCall.formattedCall + ) + ); + } + } +} + + +function filterByExtension(fileList: string[], extension: string): string[] { + return fileList.filter(name => name.endsWith(extension)); +} + +function filterByFilename(fileList: string[], filename: string): string[] { + return fileList.filter(name => name == filename); +} + +// Compiling creates a temporary file which sometimes doesn't get deleted. It should be ignored. +function ignoreTemporaryWasmFiles(fileList: string[]) { + let temporaryWasmFiles = filterByExtension(fileList, "_wasm.wasm"); + let difference = fileList.filter(file => temporaryWasmFiles.indexOf(file) === -1); + return difference; +} + +function filterWithHint(fileList: string[], extension: string, filenameHint?: string): { pattern: string, filteredFileList: string[] } { + if (filenameHint) { + let pattern = filenameHint + extension; + return { + pattern, + filteredFileList: filterByFilename(fileList, pattern) + }; + } + return { + pattern: "*" + extension, + filteredFileList: filterByExtension(fileList, extension) + }; +} + +function getFileByExtension(fileList: string[], folderPath: string, extension: string, filenameHint?: string): string { + let { pattern, filteredFileList } = filterWithHint(fileList, extension, filenameHint); + if (filteredFileList.length != 1) { + throw new ErrInvalidArgument(`Expected a single ${pattern} file in ${folderPath} (found ${filteredFileList.length})`); + } + return path.join(folderPath, filteredFileList[0]); +} + +async function getAbiAndWasmPaths(outputPath: string, filenameHint?: string) { + let filesInOutput = await fs.promises.readdir(outputPath); + filesInOutput = ignoreTemporaryWasmFiles(filesInOutput); + + let abiPath = getFileByExtension(filesInOutput, outputPath, ".abi.json", filenameHint); + let wasmPath: string | null; + try { + wasmPath = getFileByExtension(filesInOutput, outputPath, ".wasm", filenameHint); + } catch (_) { + wasmPath = null; + } + return { abiPath, wasmPath }; +} + +async function expandProjectPath(projectPath: string, filenameHint?: string): Promise<{ abiPath: string, wasmPath: string | null }> { + projectPath = path.resolve(projectPath); + try { + return await getAbiAndWasmPaths(projectPath, filenameHint); + } + catch (_) { + let outputPath = path.join(projectPath, "output"); + return await getAbiAndWasmPaths(outputPath, filenameHint); + } +} diff --git a/src/smartcontracts/wrapper/debug.ts b/src/smartcontracts/wrapper/debug.ts new file mode 100644 index 00000000..4f49413d --- /dev/null +++ b/src/smartcontracts/wrapper/debug.ts @@ -0,0 +1,11 @@ +import { ScArgumentsParser } from "../../scArgumentsParser"; + +export function debugTxData(data: string) { + let { functionName, args } = ScArgumentsParser.parseSmartContractCallDataField(data); + let parsedArgs = args.map((rawHex) => { + let asNumber = parseInt(rawHex, 16); + let asString = Buffer.from(rawHex, "hex").toString(); + return [asString, asNumber, rawHex]; + }); + return { functionName, parsedArgs }; +} diff --git a/src/smartcontracts/wrapper/esdt.spec.ts b/src/smartcontracts/wrapper/esdt.spec.ts new file mode 100644 index 00000000..dd010ad0 --- /dev/null +++ b/src/smartcontracts/wrapper/esdt.spec.ts @@ -0,0 +1,54 @@ +import { Address, ContractWrapper, createBalanceBuilder, Egld, Token, SystemWrapper, TokenType, setupInteractiveWithProvider } from "../.."; +import { MockProvider, setupUnitTestWatcherTimeouts, TestWallet } from "../../testutils"; +import { assert } from "chai"; + +describe("test ESDT transfers via the smart contract wrapper", async function () { + let dummyAddress = new Address("erd1qqqqqqqqqqqqqpgqak8zt22wl2ph4tswtyc39namqx6ysa2sd8ss4xmlj3"); + let provider = new MockProvider(); + let erdSys: SystemWrapper; + let alice: TestWallet; + let market: ContractWrapper; + before(async function () { + ({ erdSys, wallets: { alice } } = await setupInteractiveWithProvider(provider)); + market = await erdSys.loadWrapper("src/testdata", "esdt-nft-marketplace"); + market.address(dummyAddress).sender(alice).gas(500_000); + }); + + it("calling ", async function () { + setupUnitTestWatcherTimeouts(); + + let minBid = 100; + let maxBid = 500; + let deadline = 1_000_000; + let acceptedToken = "TEST-1234"; + let acceptedTokenNonce = 5_000; + + let egldCallBuffers = market.value(Egld(0.5)).format.auctionToken(minBid, maxBid, deadline, acceptedToken, acceptedTokenNonce).toCallBuffers(); + assert.deepEqual(callBuffersToStrings(egldCallBuffers), ["auctionToken", "64", "01f4", "0f4240", "544553542d31323334", "1388"]); + + let MyNFT = createBalanceBuilder(new Token({ identifier: "TEST-1234", decimals: 0, type: TokenType.Nonfungible })); + let nonFungibleCallBuffers = market.value(MyNFT.nonce(1000).one()).format.auctionToken(minBid, maxBid, deadline, acceptedToken, acceptedTokenNonce).toCallBuffers(); + assert.deepEqual(callBuffersToStrings(nonFungibleCallBuffers), [ + "ESDTNFTTransfer", + "544553542d31323334", + "03e8", + "01", + "00000000000000000500ed8e25a94efa837aae0e593112cfbb01b448755069e1", + "61756374696f6e546f6b656e", + "64", + "01f4", + "0f4240", + "544553542d31323334", + "1388" + ]); + }); +}); + +function callBuffersToStrings(values: Buffer[]): string[] { + let [func, ...args] = values; + return [func.toString(), ...argBuffersToStrings(args)]; +} + +function argBuffersToStrings(values: Buffer[]): string[] { + return values.map(buffer => buffer.toString("hex")); +} diff --git a/src/smartcontracts/wrapper/formattedCall.ts b/src/smartcontracts/wrapper/formattedCall.ts new file mode 100644 index 00000000..5688bf17 --- /dev/null +++ b/src/smartcontracts/wrapper/formattedCall.ts @@ -0,0 +1,72 @@ +import { ArgSerializer, ContractFunction, EndpointDefinition, NativeSerializer, TypedValue } from ".."; + +/** + * Creates a FormattedCall from the given endpoint and args. + */ +export function formatEndpoint(executingEndpoint: EndpointDefinition, interpretingEndpoint: EndpointDefinition, ...args: any[]): FormattedCall { + return new FormattedCall(executingEndpoint, interpretingEndpoint, args); +} + +/** + * Formats and validates the arguments of a bound call. + * A bound call is represented by a function and its arguments packed together. + * A function is defined as something that has an EndpointDefinition and may be: + * - a smart contract method + * - a built-in function (such as an ESDT transfer) + */ +export class FormattedCall { + readonly executingEndpoint: EndpointDefinition; + interpretingEndpoint: EndpointDefinition; + readonly args: any[]; + + constructor(executingEndpoint: EndpointDefinition, interpretingEndpoint: EndpointDefinition, args: any[]) { + this.executingEndpoint = executingEndpoint; + this.interpretingEndpoint = interpretingEndpoint; + this.args = args; + } + + getExecutingFunction(): ContractFunction { + return new ContractFunction(this.executingEndpoint.name); + } + + getInterpretingFunction(): ContractFunction { + return new ContractFunction(this.interpretingEndpoint.name); + } + + /** + * Takes the given arguments, and converts them to typed values, validating them against the given endpoint in the process. + */ + toTypedValues(): TypedValue[] { + let expandedArgs = this.getExpandedArgs(); + return NativeSerializer.nativeToTypedValues(expandedArgs, this.executingEndpoint); + } + + toArgBuffers(): Buffer[] { + let typedValues = this.toTypedValues(); + return new ArgSerializer().valuesToBuffers(typedValues); + } + + /** + * Formats the function name and its arguments as an array of buffers. + * This is useful for nested calls (for the multisig smart contract or for ESDT transfers). + * A formatted deploy call does not return the function name. + */ + toCallBuffers(): Buffer[] { + if (this.executingEndpoint.isConstructor()) { + return this.toArgBuffers(); + } + return [Buffer.from(this.executingEndpoint.name), ...this.toArgBuffers()]; + } + + private getExpandedArgs(): any[] { + let expanded: any[] = []; + for (let value of this.args) { + if (value instanceof FormattedCall) { + expanded = expanded.concat(value.toCallBuffers()); + } else { + expanded.push(value); + } + } + return expanded; + } +} diff --git a/src/smartcontracts/wrapper/generateMethods.ts b/src/smartcontracts/wrapper/generateMethods.ts new file mode 100644 index 00000000..69aa625f --- /dev/null +++ b/src/smartcontracts/wrapper/generateMethods.ts @@ -0,0 +1,18 @@ +import { SmartContractAbi } from "../abi"; +import { EndpointDefinition } from "../typesystem"; + +export type EndpointHandler = (this: ThisType, endpoint: EndpointDefinition, ...args: any[]) => ReturnType; +export type Method = (...args: any[]) => ReturnType; +export type Methods = Record>; + +export function generateMethods( + this_: ThisType, + abi: SmartContractAbi, + endpointHandler: EndpointHandler +): Methods { + let generated: Methods = {}; + for (const endpoint of abi.getAllEndpoints()) { + generated[endpoint.name] = endpointHandler.bind(this_, endpoint); + } + return generated; +} diff --git a/src/smartcontracts/wrapper/index.ts b/src/smartcontracts/wrapper/index.ts new file mode 100644 index 00000000..f2927313 --- /dev/null +++ b/src/smartcontracts/wrapper/index.ts @@ -0,0 +1,12 @@ +/** + * @packageDocumentation + * @module wrapper + */ + +export * from "./chainSendContext"; +export * from "./contractLogger"; +export * from "./contractWrapper"; +export * from "./debug"; +export * from "./sendContext"; +export * from "./systemWrapper"; +export * from "./utils"; diff --git a/src/smartcontracts/wrapper/preparedCall.ts b/src/smartcontracts/wrapper/preparedCall.ts new file mode 100644 index 00000000..fdba1cf4 --- /dev/null +++ b/src/smartcontracts/wrapper/preparedCall.ts @@ -0,0 +1,28 @@ +import { Address, Balance } from "../.."; +import { FormattedCall } from "./formattedCall"; + +/** + * Keeps track of part of the context necessary for making a call to a smart contract method. + */ +export class PreparedCall { + // Usually the address of the called smart contract, although not always (eg. it may be the system contract address for any ESDTNFTTransfer) + receiver: Address; + + // The EGLD amount to be transfered (if any) + egldValue: Balance; + + // The function or method to be called and its arguments + // Note: May contain NFT transfers on top of the usual smart contract method call + formattedCall: FormattedCall; + + constructor(receiver: Address, egldValue: Balance, formattedCall: FormattedCall) { + this.receiver = receiver; + this.egldValue = egldValue; + this.formattedCall = formattedCall; + } + + wrap(wrappedCall: FormattedCall) { + wrappedCall.interpretingEndpoint = this.formattedCall.interpretingEndpoint; + this.formattedCall = wrappedCall; + } +} diff --git a/src/smartcontracts/wrapper/sendContext.ts b/src/smartcontracts/wrapper/sendContext.ts new file mode 100644 index 00000000..99390b9f --- /dev/null +++ b/src/smartcontracts/wrapper/sendContext.ts @@ -0,0 +1,91 @@ +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"; +import { Err } from "../../errors"; +import { getGasFromValue } from "./systemWrapper"; + +/** + * Stores contextual information which is needed when preparing a transaction. + */ +export class SendContext { + private sender_: TestWallet | null; + private provider_: IProvider; + private gas_: GasLimit | null; + private logger_: ContractLogger | null; + private value_: Balance | null; + readonly checker: IInteractionChecker; + + constructor(provider: IProvider) { + this.sender_ = null; + this.provider_ = provider; + this.gas_ = null; + this.logger_ = null; + this.value_ = null; + this.checker = new StrictChecker(); + } + + provider(provider: IProvider): this { + this.provider_ = provider; + return this; + } + + sender(sender: TestWallet): this { + this.sender_ = sender; + return this; + } + + gas(gas: number): this { + this.gas_ = new GasLimit(gas); + return this; + } + + autoGas(baseGas: number): this { + return this.gas(getGasFromValue(baseGas, this.value_)); + } + + logger(logger: ContractLogger | null): this { + this.logger_ = logger; + return this; + } + + value(value: Balance): this { + this.value_ = value; + return this; + } + + getAndResetValue(): Balance | null { + let value = this.value_; + this.value_ = null; + return value; + } + + getSender(): TestWallet { + if (this.sender_) { + return this.sender_; + } + throw new Err("sender not set"); + } + + getSenderOptional(): TestWallet | null { + return this.sender_; + } + + getProvider(): IProvider { + return this.provider_; + } + + getGasLimit(): GasLimit { + if (this.gas_) { + return this.gas_; + } + throw new Err("gas limit not set"); + } + + getLogger(): ContractLogger | null { + return this.logger_; + } +} diff --git a/src/smartcontracts/wrapper/systemWrapper.ts b/src/smartcontracts/wrapper/systemWrapper.ts new file mode 100644 index 00000000..a3c02205 --- /dev/null +++ b/src/smartcontracts/wrapper/systemWrapper.ts @@ -0,0 +1,169 @@ +import BigNumber from "bignumber.js"; +import path from "path"; +import { ContractWrapper, SendContext, ContractLogger, EndpointParameterDefinition, AddressType } from ".."; +import { Address, Balance, IProvider, BalanceBuilder, Token, createBalanceBuilder, Egld, EsdtHelpers } from "../.."; +import { MockProvider, TestWallet } from "../../testutils"; +import { NativeSerializer, NativeTypes } from "../nativeSerializer"; +import { ArgumentErrorContext } from "../argumentErrorContext"; +import { ChainSendContext } from "./chainSendContext"; + +export namespace SystemConstants { + export let SYSTEM_ABI_PATH = path.join(__dirname, "../../../abi"); + export let ESDT_CONTRACT_ADDRESS = new Address("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u"); + + export let MIN_TRANSACTION_GAS = 50_000; + export let ESDT_ISSUE_GAS_LIMIT = 60_000_000; + export let ESDT_TRANSFER_GAS_LIMIT = 500_000; + export let ESDT_NFT_TRANSFER_GAS_LIMIT = 1_000_000; + export let ESDT_BASE_GAS_LIMIT = 6_000_000; +} + +export class SystemWrapper extends ChainSendContext { + private readonly provider: IProvider; + private readonly builtinFunctions: ContractWrapper; + readonly esdtSystemContract: ContractWrapper; + readonly issueCost: Balance; + private readonly sendWrapper: ContractWrapper; + + private constructor(provider: IProvider, context: SendContext, sendContract: ContractWrapper, esdtSystemContract: ContractWrapper, issueCost: Balance, builtinFunctions: ContractWrapper) { + super(context); + this.provider = provider; + this.sendWrapper = sendContract; + this.esdtSystemContract = esdtSystemContract; + this.issueCost = issueCost; + this.builtinFunctions = builtinFunctions; + } + + async loadWrapper(projectPath: string, filenameHint?: string, context?: SendContext): Promise { + return await ContractWrapper.loadProject(this.provider, this.builtinFunctions, projectPath, filenameHint, context); + } + + static async getEsdtContractConfig(esdtSystemContract: ContractWrapper): Promise { + let [ownerAddress, baseIssuingCost, minTokenNameLength, maxTokenNameLength] = await esdtSystemContract.query.getContractConfig(); + return { ownerAddress, baseIssuingCost: Egld.raw(baseIssuingCost), minTokenNameLength, maxTokenNameLength }; + } + + static async load(provider: IProvider): Promise { + let context = new SendContext(provider).logger(new ContractLogger()); + let builtinFunctions = await ContractWrapper.loadProject(provider, null, SystemConstants.SYSTEM_ABI_PATH, "builtinFunctions", context); + let sendWrapper = await ContractWrapper.loadProject(provider, builtinFunctions, SystemConstants.SYSTEM_ABI_PATH, "sendWrapper", context); + let esdtSystemContract = await ContractWrapper.loadProject(provider, builtinFunctions, SystemConstants.SYSTEM_ABI_PATH, "esdtSystemContract", context); + esdtSystemContract.address(SystemConstants.ESDT_CONTRACT_ADDRESS); + let issueCost: Balance; + if (provider instanceof MockProvider) { + issueCost = Balance.Zero(); + } else { + let contractConfig = await this.getEsdtContractConfig(esdtSystemContract); + issueCost = contractConfig.baseIssuingCost; + } + return new SystemWrapper(provider, context, sendWrapper, esdtSystemContract, issueCost, builtinFunctions); + } + + async send(receiver: string | Buffer | Address | TestWallet): Promise { + let address = NativeSerializer.convertNativeToAddress(receiver, new ArgumentErrorContext("send", "0", new EndpointParameterDefinition("receiver", "", new AddressType()))); + await this.sendWrapper.address(address).autoGas(0).call[""](); + } + + async issueFungible(...args: any[]): Promise { + let { resultingCalls: [issueResult] } = await this.esdtSystemContract + .gas(SystemConstants.ESDT_ISSUE_GAS_LIMIT) + .value(this.issueCost) + .results.issue(...args); + let { tokenIdentifier } = EsdtHelpers.extractFieldsFromEsdtTransferDataField(issueResult.data); + tokenIdentifier = Buffer.from(tokenIdentifier, "hex").toString(); + return this.recallToken(tokenIdentifier); + } + + async issueSemiFungible(...args: any[]): Promise { + let tokenIdentifier = (await this.esdtSystemContract + .gas(SystemConstants.ESDT_ISSUE_GAS_LIMIT) + .value(this.issueCost) + .call.issueSemiFungible(...args) + ).toString(); + return this.recallToken(tokenIdentifier); + } + + async issueNonFungible(...args: any[]): Promise { + let tokenIdentifier = (await this.esdtSystemContract + .gas(SystemConstants.ESDT_ISSUE_GAS_LIMIT) + .value(this.issueCost) + .call.issueNonFungible(...args) + ).toString(); + return this.recallToken(tokenIdentifier); + } + + async esdtNftCreate(balanceBuilder: BalanceBuilder, ...args: any[]): Promise { + let nonce = await this.builtinFunctions + .address(this.context.getSender()) + .gas(SystemConstants.ESDT_BASE_GAS_LIMIT) + .call + .ESDTNFTCreate(balanceBuilder, ...args); + return balanceBuilder.nonce(nonce); + } + + async recallToken(tokenIdentifier: string): Promise { + let tokenProperties = await this.esdtSystemContract.query.getTokenProperties(tokenIdentifier); + let token = Token.fromTokenProperties(tokenIdentifier, tokenProperties); + return createBalanceBuilder(token); + } + + async getBalance(address: NativeTypes.NativeAddress, balanceBuilder: BalanceBuilder): Promise { + let typedAddress = NativeSerializer.convertNativeToAddress(address, new ArgumentErrorContext("getBalance", "0", new EndpointParameterDefinition("account", "", new AddressType()))); + if (balanceBuilder.getToken().isEgld()) { + return await this.provider.getAccount(typedAddress).then((account) => account.balance); + } + let tokenData = await this.getTokenData(typedAddress, balanceBuilder); + return balanceBuilder.raw(tokenData.balance); + } + + async getBalanceList(address: NativeTypes.NativeAddress, balanceBuilder: BalanceBuilder) { + let typedAddress = NativeSerializer.convertNativeToAddress(address, new ArgumentErrorContext("getBalanceList", "0", new EndpointParameterDefinition("account", "", new AddressType()))); + if (balanceBuilder.getToken().isNft() && balanceBuilder.hasNonce()) { + return [await this.getBalance(typedAddress, balanceBuilder)]; + } + + return await this.provider.getAddressEsdtList(typedAddress).then((esdtList) => { + let tokenBalances: Balance[] = []; + let filterIdentifier = balanceBuilder.getTokenIdentifier() + '-'; + for (let [identifier, details] of Object.entries(esdtList)) { + if (identifier.startsWith(filterIdentifier)) { + tokenBalances.push(balanceBuilder.nonce(details.nonce).raw(details.balance)); + } + } + return tokenBalances; + }); + } + + async getTokenData(address: Address, balanceBuilder: BalanceBuilder): Promise { + let tokenIdentifier = balanceBuilder.getTokenIdentifier(); + if (balanceBuilder.getToken().isFungible()) { + return await this.provider.getAddressEsdt(address, tokenIdentifier); + } else { + return await this.provider.getAddressNft(address, tokenIdentifier, balanceBuilder.getNonce()); + } + } + + async currentNonce(): Promise { + let networkStatus = await this.provider.getNetworkStatus(); + return networkStatus.Nonce; + } + +} + +export type EsdtContractConfig = { + ownerAddress: Address; + baseIssuingCost: Balance; + minTokenNameLength: BigNumber; + maxTokenNameLength: BigNumber; +}; + +export function getGasFromValue(baseGas: number, value: Balance | null): number { + if (!value || value.isEgld()) { + return Math.max(baseGas, SystemConstants.MIN_TRANSACTION_GAS); + } + if (value.token.isFungible()) { + return baseGas + SystemConstants.ESDT_TRANSFER_GAS_LIMIT; + } else { + return baseGas + SystemConstants.ESDT_NFT_TRANSFER_GAS_LIMIT; + } +} diff --git a/src/smartcontracts/wrapper/utils.ts b/src/smartcontracts/wrapper/utils.ts new file mode 100644 index 00000000..711062aa --- /dev/null +++ b/src/smartcontracts/wrapper/utils.ts @@ -0,0 +1,29 @@ +import { Balance } from "../.."; + +export function print(balance: Balance) { + let nonceString = balance.token.isFungible() ? '' : ` nonce: ${balance.getNonce()}`; + console.log(`${balance.toCurrencyString()}${nonceString}`); +} + +export function printList(balanceList: Balance[]) { + balanceList.forEach((balance) => print(balance)); +} + +export function minutesToNonce(minutes: number): number { + // the nonce is incremented every 6 seconds - in a minute the nonce increases by 10 + return minutes * 10; +} + +export function now(): number { + return Math.floor(Date.now() / 1000); +} + +export function hours(hours: number): number { + let asMinutes = hours * 60; + return minutes(asMinutes); +} + +export function minutes(minutes: number): number { + let seconds = minutes * 60; + return seconds; +} diff --git a/src/testdata/answer.abi.json b/src/testdata/answer.abi.json index 73e397a7..d68ea2bd 100644 --- a/src/testdata/answer.abi.json +++ b/src/testdata/answer.abi.json @@ -1,5 +1,9 @@ { "name": "answer", + "constructor": { + "inputs": [], + "outputs": [] + }, "endpoints": [ { "name": "getUltimateAnswer", diff --git a/src/testdata/counter.abi.json b/src/testdata/counter.abi.json index b565e128..d70c7783 100644 --- a/src/testdata/counter.abi.json +++ b/src/testdata/counter.abi.json @@ -1,5 +1,9 @@ { "name": "counter", + "constructor": { + "inputs": [], + "outputs": [] + }, "endpoints": [ { "name": "increment", diff --git a/src/testdata/esdt-nft-marketplace.abi.json b/src/testdata/esdt-nft-marketplace.abi.json new file mode 100644 index 00000000..ea395cc3 --- /dev/null +++ b/src/testdata/esdt-nft-marketplace.abi.json @@ -0,0 +1,291 @@ +{ + "name": "EsdtNftMarketplace", + "constructor": { + "inputs": [ + { + "name": "bid_cut_percentage", + "type": "u64" + } + ], + "outputs": [] + }, + "endpoints": [ + { + "name": "setCutPercentage", + "inputs": [ + { + "name": "new_cut_percentage", + "type": "u64" + } + ], + "outputs": [] + }, + { + "name": "auctionToken", + "payableInTokens": [ + "*" + ], + "inputs": [ + { + "name": "min_bid", + "type": "BigUint" + }, + { + "name": "max_bid", + "type": "BigUint" + }, + { + "name": "deadline", + "type": "u64" + }, + { + "name": "accepted_payment_token", + "type": "TokenIdentifier" + }, + { + "name": "opt_accepted_payment_token_nonce", + "type": "optional", + "multi_arg": true + } + ], + "outputs": [] + }, + { + "name": "bid", + "payableInTokens": [ + "*" + ], + "inputs": [ + { + "name": "nft_type", + "type": "TokenIdentifier" + }, + { + "name": "nft_nonce", + "type": "u64" + } + ], + "outputs": [] + }, + { + "name": "endAuction", + "inputs": [ + { + "name": "nft_type", + "type": "TokenIdentifier" + }, + { + "name": "nft_nonce", + "type": "u64" + } + ], + "outputs": [] + }, + { + "name": "withdraw", + "inputs": [ + { + "name": "nft_type", + "type": "TokenIdentifier" + }, + { + "name": "nft_nonce", + "type": "u64" + } + ], + "outputs": [] + }, + { + "name": "isAlreadyUpForAuction", + "inputs": [ + { + "name": "nft_type", + "type": "TokenIdentifier" + }, + { + "name": "nft_nonce", + "type": "u64" + } + ], + "outputs": [ + { + "type": "bool" + } + ] + }, + { + "name": "getPaymentTokenForAuctionedNft", + "inputs": [ + { + "name": "nft_type", + "type": "TokenIdentifier" + }, + { + "name": "nft_nonce", + "type": "u64" + } + ], + "outputs": [ + { + "type": "Option" + } + ] + }, + { + "name": "getMinMaxBid", + "inputs": [ + { + "name": "nft_type", + "type": "TokenIdentifier" + }, + { + "name": "nft_nonce", + "type": "u64" + } + ], + "outputs": [ + { + "type": "Option>" + } + ] + }, + { + "name": "getDeadline", + "inputs": [ + { + "name": "nft_type", + "type": "TokenIdentifier" + }, + { + "name": "nft_nonce", + "type": "u64" + } + ], + "outputs": [ + { + "type": "Option" + } + ] + }, + { + "name": "getOriginalOwner", + "inputs": [ + { + "name": "nft_type", + "type": "TokenIdentifier" + }, + { + "name": "nft_nonce", + "type": "u64" + } + ], + "outputs": [ + { + "type": "Option
" + } + ] + }, + { + "name": "getCurrentWinningBid", + "inputs": [ + { + "name": "nft_type", + "type": "TokenIdentifier" + }, + { + "name": "nft_nonce", + "type": "u64" + } + ], + "outputs": [ + { + "type": "Option" + } + ] + }, + { + "name": "getCurrentWinner", + "inputs": [ + { + "name": "nft_type", + "type": "TokenIdentifier" + }, + { + "name": "nft_nonce", + "type": "u64" + } + ], + "outputs": [ + { + "type": "Option
" + } + ] + }, + { + "name": "getFullAuctionData", + "inputs": [ + { + "name": "nft_type", + "type": "TokenIdentifier" + }, + { + "name": "nft_nonce", + "type": "u64" + } + ], + "outputs": [ + { + "type": "Option" + } + ] + } + ], + "types": { + "Auction": { + "type": "struct", + "fields": [ + { + "name": "payment_token", + "type": "EsdtToken" + }, + { + "name": "min_bid", + "type": "BigUint" + }, + { + "name": "max_bid", + "type": "BigUint" + }, + { + "name": "deadline", + "type": "u64" + }, + { + "name": "original_owner", + "type": "Address" + }, + { + "name": "current_bid", + "type": "BigUint" + }, + { + "name": "current_winner", + "type": "Address" + } + ] + }, + "EsdtToken": { + "type": "struct", + "fields": [ + { + "name": "token_type", + "type": "TokenIdentifier" + }, + { + "name": "nonce", + "type": "u64" + } + ] + } + } +} \ No newline at end of file diff --git a/src/testdata/lottery_egld.abi.json b/src/testdata/lottery_egld.abi.json index fcbd08a5..e4798451 100644 --- a/src/testdata/lottery_egld.abi.json +++ b/src/testdata/lottery_egld.abi.json @@ -1,11 +1,10 @@ { "name": "Lottery", + "constructor": { + "inputs": [], + "outputs": [] + }, "endpoints": [ - { - "name": "init", - "inputs": [], - "outputs": [] - }, { "name": "start", "inputs": [ diff --git a/src/testutils/mockProvider.ts b/src/testutils/mockProvider.ts index 1fd44470..ee6ff0d4 100644 --- a/src/testutils/mockProvider.ts +++ b/src/testutils/mockProvider.ts @@ -13,214 +13,228 @@ import { QueryResponse } from "../smartcontracts/queryResponse"; import { Hash } from "../hash"; import { NetworkStatus } from "../networkStatus"; import { TypedEvent } from "../events"; +import { BalanceBuilder } from "../balanceBuilder"; +import BigNumber from "bignumber.js"; /** * A mock {@link IProvider}, used for tests only. */ export class MockProvider implements IProvider { - static AddressOfAlice = new Address("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - static AddressOfBob = new Address("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); - static AddressOfCarol = new Address("erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8"); - - private readonly transactions: Map; - private readonly onTransactionSent: TypedEvent<{ transaction: Transaction }>; - private readonly accounts: Map; - private readonly queryResponders: QueryResponder[] = []; - - constructor() { - this.transactions = new Map(); - this.onTransactionSent = new TypedEvent(); - this.accounts = new Map(); - - this.accounts.set( - MockProvider.AddressOfAlice.bech32(), - new AccountOnNetwork({ nonce: new Nonce(0), balance: Balance.egld(1000) }) - ); - this.accounts.set( - MockProvider.AddressOfBob.bech32(), - new AccountOnNetwork({ nonce: new Nonce(5), balance: Balance.egld(500) }) - ); - this.accounts.set( - MockProvider.AddressOfCarol.bech32(), - new AccountOnNetwork({ nonce: new Nonce(42), balance: Balance.egld(300) }) - ); - } - doPostGeneric(resourceUrl: string, payload: any, callback: (response: any) => any): Promise { - resourceUrl; - payload; - callback; - throw new Error("Method not implemented."); - } - - doGetGeneric(resourceUrl: string, callback: (response: any) => any): Promise { - resourceUrl; - callback; - throw new Error("Method not implemented."); - } - - mockUpdateAccount(address: Address, mutate: (item: AccountOnNetwork) => void) { - let account = this.accounts.get(address.bech32()); - if (account) { - mutate(account); - } - } - - mockUpdateTransaction(hash: TransactionHash, mutate: (item: TransactionOnNetwork) => void) { - let transaction = this.transactions.get(hash.toString()); - if (transaction) { - mutate(transaction); - } - } - - mockPutTransaction(hash: TransactionHash, item: TransactionOnNetwork) { - this.transactions.set(hash.toString(), item); - } - - mockQueryResponseOnFunction(functionName: string, response: QueryResponse) { - let predicate = (query: Query) => query.func.name == functionName; - this.queryResponders.push(new QueryResponder(predicate, response)); - } - - mockQueryResponse(predicate: (query: Query) => boolean, response: QueryResponse) { - this.queryResponders.push(new QueryResponder(predicate, response)); - } - - async mockTransactionTimeline(transaction: Transaction, timelinePoints: any[]): Promise { - await transaction.awaitHashed(); - return this.mockTransactionTimelineByHash(transaction.getHash(), timelinePoints); - } - - async mockNextTransactionTimeline(timelinePoints: any[]): Promise { - let transaction = await this.nextTransactionSent(); - return this.mockTransactionTimelineByHash(transaction.getHash(), timelinePoints); - } - - async nextTransactionSent(): Promise { - return new Promise((resolve, _reject) => { - this.onTransactionSent.on((eventArgs) => resolve(eventArgs.transaction)); - }); - } - - async mockTransactionTimelineByHash(hash: TransactionHash, timelinePoints: any[]): Promise { - let timeline = new AsyncTimer(`mock timeline of ${hash}`); - - await timeline.start(0); - - for (const point of timelinePoints) { - if (point instanceof TransactionStatus) { - this.mockUpdateTransaction(hash, (transaction) => { - transaction.status = point; - }); - } 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; + static AddressOfAlice = new Address("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + static AddressOfBob = new Address("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + static AddressOfCarol = new Address("erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8"); + + private readonly transactions: Map; + private readonly onTransactionSent: TypedEvent<{ transaction: Transaction }>; + private readonly accounts: Map; + private readonly queryResponders: QueryResponder[] = []; + + constructor() { + this.transactions = new Map(); + this.onTransactionSent = new TypedEvent(); + this.accounts = new Map(); + + this.accounts.set( + MockProvider.AddressOfAlice.bech32(), + new AccountOnNetwork({ nonce: new Nonce(0), balance: Balance.egld(1000) }) + ); + this.accounts.set( + MockProvider.AddressOfBob.bech32(), + new AccountOnNetwork({ nonce: new Nonce(5), balance: Balance.egld(500) }) + ); + this.accounts.set( + MockProvider.AddressOfCarol.bech32(), + new AccountOnNetwork({ nonce: new Nonce(42), balance: Balance.egld(300) }) + ); + } + + getAccountEsdtBalance(_address: Address, _tokenBalanceBuilder: BalanceBuilder): Promise { + throw new Error("Method not implemented."); + } + + doPostGeneric(_resourceUrl: string, _payload: any, _callback: (response: any) => any): Promise { + throw new Error("Method not implemented."); + } + + doGetGeneric(_resourceUrl: string, _callback: (response: any) => any): Promise { + throw new Error("Method not implemented."); + } + + mockUpdateAccount(address: Address, mutate: (item: AccountOnNetwork) => void) { + let account = this.accounts.get(address.bech32()); + if (account) { + mutate(account); + } + } + + mockUpdateTransaction(hash: TransactionHash, mutate: (item: TransactionOnNetwork) => void) { + let transaction = this.transactions.get(hash.toString()); + if (transaction) { + mutate(transaction); + } + } + + mockPutTransaction(hash: TransactionHash, item: TransactionOnNetwork) { + this.transactions.set(hash.toString(), item); + } + + mockQueryResponseOnFunction(functionName: string, response: QueryResponse) { + let predicate = (query: Query) => query.func.name == functionName; + this.queryResponders.push(new QueryResponder(predicate, response)); + } + + mockQueryResponse(predicate: (query: Query) => boolean, response: QueryResponse) { + this.queryResponders.push(new QueryResponder(predicate, response)); + } + + async mockTransactionTimeline(transaction: Transaction, timelinePoints: any[]): Promise { + await transaction.awaitHashed(); + return this.mockTransactionTimelineByHash(transaction.getHash(), timelinePoints); + } + + async mockNextTransactionTimeline(timelinePoints: any[]): Promise { + let transaction = await this.nextTransactionSent(); + return this.mockTransactionTimelineByHash(transaction.getHash(), timelinePoints); + } + + async nextTransactionSent(): Promise { + return new Promise((resolve, _reject) => { + this.onTransactionSent.on((eventArgs) => resolve(eventArgs.transaction)); }); - } else if (point instanceof Wait) { - await timeline.start(point.milliseconds); - } } - } - async getAccount(address: Address): Promise { - let account = this.accounts.get(address.bech32()); - if (account) { - return account; + async mockTransactionTimelineByHash(hash: TransactionHash, timelinePoints: any[]): Promise { + let timeline = new AsyncTimer(`mock timeline of ${hash}`); + + await timeline.start(0); + + for (const point of timelinePoints) { + if (point instanceof TransactionStatus) { + this.mockUpdateTransaction(hash, (transaction) => { + transaction.status = point; + }); + } 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; + }); + } else if (point instanceof Wait) { + await timeline.start(point.milliseconds); + } + } } - return new AccountOnNetwork(); - } + async getAccount(address: Address): Promise { + let account = this.accounts.get(address.bech32()); + if (account) { + return account; + } - async sendTransaction(transaction: Transaction): Promise { - this.mockPutTransaction( - transaction.getHash(), - new TransactionOnNetwork({ - nonce: transaction.getNonce(), - sender: transaction.getSender(), - receiver: transaction.getReceiver(), - data: transaction.getData(), - status: new TransactionStatus("pending"), - }) - ); + return new AccountOnNetwork(); + } - this.onTransactionSent.emit({ transaction: transaction }); + async getAddressEsdt(_address: Address, _tokenIdentifier: string): Promise { + return {}; + } - return transaction.getHash(); - } + async getAddressEsdtList(_address: Address): Promise { + return {}; + } - async simulateTransaction(_transaction: Transaction): Promise { - return {}; - } + async getAddressNft(_address: Address, _tokenIdentifier: string, _nonce: BigNumber): Promise { + return {}; + } - async getTransaction( - txHash: TransactionHash, - _hintSender?: Address, - _withResults?: boolean - ): Promise { - let transaction = this.transactions.get(txHash.toString()); - if (transaction) { - return transaction; + async sendTransaction(transaction: Transaction): Promise { + this.mockPutTransaction( + transaction.getHash(), + new TransactionOnNetwork({ + nonce: transaction.getNonce(), + sender: transaction.getSender(), + receiver: transaction.getReceiver(), + data: transaction.getData(), + status: new TransactionStatus("pending"), + }) + ); + + this.onTransactionSent.emit({ transaction: transaction }); + + return transaction.getHash(); } - throw new errors.ErrMock("Transaction not found"); - } + async simulateTransaction(_transaction: Transaction): Promise { + return {}; + } - async getTransactionStatus(txHash: TransactionHash): Promise { - let transaction = this.transactions.get(txHash.toString()); - if (transaction) { - return transaction.status; + async getTransaction( + txHash: TransactionHash, + _hintSender?: Address, + _withResults?: boolean + ): Promise { + let transaction = this.transactions.get(txHash.toString()); + if (transaction) { + return transaction; + } + + throw new errors.ErrMock("Transaction not found"); } - throw new errors.ErrMock("Transaction not found"); - } + async getTransactionStatus(txHash: TransactionHash): Promise { + let transaction = this.transactions.get(txHash.toString()); + if (transaction) { + return transaction.status; + } - async getNetworkConfig(): Promise { - return new NetworkConfig(); - } + throw new errors.ErrMock("Transaction not found"); + } - async getNetworkStatus(): Promise { - return new NetworkStatus(); - } + async getNetworkConfig(): Promise { + return new NetworkConfig(); + } - async queryContract(query: Query): Promise { - for (const responder of this.queryResponders) { - if (responder.matches(query)) { - return responder.response; - } + async getNetworkStatus(): Promise { + return new NetworkStatus(); } - return new QueryResponse(); - } + async queryContract(query: Query): Promise { + for (const responder of this.queryResponders) { + if (responder.matches(query)) { + return responder.response; + } + } + + return new QueryResponse(); + } } export class Wait { - readonly milliseconds: number; + readonly milliseconds: number; - constructor(milliseconds: number) { - this.milliseconds = milliseconds; - } + constructor(milliseconds: number) { + this.milliseconds = milliseconds; + } } -export class MarkNotarized {} +export class MarkNotarized { } export class AddImmediateResult { - readonly data: string; + readonly data: string; - constructor(data: string) { - this.data = data; - } + constructor(data: string) { + this.data = data; + } } class QueryResponder { - readonly matches: (query: Query) => boolean; - readonly response: QueryResponse; + readonly matches: (query: Query) => boolean; + readonly response: QueryResponse; - constructor(matches: (query: Query) => boolean, response: QueryResponse) { - this.matches = matches || ((_) => true); - this.response = response || new QueryResponse(); - } + constructor(matches: (query: Query) => boolean, response: QueryResponse) { + this.matches = matches || ((_) => true); + this.response = response || new QueryResponse(); + } } diff --git a/src/testutils/testwallets/alice.json b/src/testutils/testwallets/alice.json new file mode 100644 index 00000000..9e83170c --- /dev/null +++ b/src/testutils/testwallets/alice.json @@ -0,0 +1,22 @@ +{ + "version": 4, + "id": "0dc10c02-b59b-4bac-9710-6b2cfa4284ba", + "address": "0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1", + "bech32": "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + "crypto": { + "ciphertext": "4c41ef6fdfd52c39b1585a875eb3c86d30a315642d0e35bb8205b6372c1882f135441099b11ff76345a6f3a930b5665aaf9f7325a32c8ccd60081c797aa2d538", + "cipherparams": { + "iv": "033182afaa1ebaafcde9ccc68a5eac31" + }, + "cipher": "aes-128-ctr", + "kdf": "scrypt", + "kdfparams": { + "dklen": 32, + "salt": "4903bd0e7880baa04fc4f886518ac5c672cdc745a6bd13dcec2b6c12e9bffe8d", + "n": 4096, + "r": 8, + "p": 1 + }, + "mac": "5b4a6f14ab74ba7ca23db6847e28447f0e6a7724ba9664cf425df707a84f5a8b" + } +} diff --git a/src/testutils/testwallets/alice.pem b/src/testutils/testwallets/alice.pem new file mode 100644 index 00000000..d27bb68b --- /dev/null +++ b/src/testutils/testwallets/alice.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY for erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th----- +NDEzZjQyNTc1ZjdmMjZmYWQzMzE3YTc3ODc3MTIxMmZkYjgwMjQ1ODUwOTgxZTQ4 +YjU4YTRmMjVlMzQ0ZThmOTAxMzk0NzJlZmY2ODg2NzcxYTk4MmYzMDgzZGE1ZDQy +MWYyNGMyOTE4MWU2Mzg4ODIyOGRjODFjYTYwZDY5ZTE= +-----END PRIVATE KEY for erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th----- \ No newline at end of file diff --git a/src/testutils/testwallets/bob.json b/src/testutils/testwallets/bob.json new file mode 100644 index 00000000..439b394a --- /dev/null +++ b/src/testutils/testwallets/bob.json @@ -0,0 +1,22 @@ +{ + "version": 4, + "id": "85fdc8a7-7119-479d-b7fb-ab4413ed038d", + "address": "8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8", + "bech32": "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx", + "crypto": { + "ciphertext": "c2664a31350aaf6a00525560db75c254d0aea65dc466441356c1dd59253cceb9e83eb05730ef3f42a11573c9a0e33dd952d488f00535b35357bb41d127b1eb82", + "cipherparams": { + "iv": "18378411e31f6c4e99f1435d9ab82831" + }, + "cipher": "aes-128-ctr", + "kdf": "scrypt", + "kdfparams": { + "dklen": 32, + "salt": "18304455ac2dbe2a2018bda162bd03ef95b81622e99d8275c34a6d5e6932a68b", + "n": 4096, + "r": 8, + "p": 1 + }, + "mac": "23756172195ac483fa29025dc331bc7aa2c139533922a8dc08642eb0a677541f" + } +} diff --git a/src/testutils/testwallets/bob.pem b/src/testutils/testwallets/bob.pem new file mode 100644 index 00000000..00b5bc4e --- /dev/null +++ b/src/testutils/testwallets/bob.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY for erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx----- +YjhjYTZmODIwM2ZiNGI1NDVhOGU4M2M1Mzg0ZGEwMzNjNDE1ZGIxNTViNTNmYjVi +OGViYTdmZjVhMDM5ZDYzOTgwNDlkNjM5ZTVhNjk4MGQxY2QyMzkyYWJjY2U0MTAy +OWNkYTc0YTE1NjM1MjNhMjAyZjA5NjQxY2MyNjE4Zjg= +-----END PRIVATE KEY for erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx----- \ No newline at end of file diff --git a/src/testutils/testwallets/carol.json b/src/testutils/testwallets/carol.json new file mode 100644 index 00000000..3614a5ba --- /dev/null +++ b/src/testutils/testwallets/carol.json @@ -0,0 +1,22 @@ +{ + "version": 4, + "id": "65894f35-d142-41d2-9335-6ad02e0ed0be", + "address": "b2a11555ce521e4944e09ab17549d85b487dcd26c84b5017a39e31a3670889ba", + "bech32": "erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8", + "crypto": { + "ciphertext": "bdfb984a1e7c7460f0a289749609730cdc99d7ce85b59305417c2c0f007b2a6aaa7203dd94dbf27315bced39b0b281769fbc70b01e6e57f89ae2f2a9e9100007", + "cipherparams": { + "iv": "258ed2b4dc506b4dc9d274b0449b0eb0" + }, + "cipher": "aes-128-ctr", + "kdf": "scrypt", + "kdfparams": { + "dklen": 32, + "salt": "4f2f5530ce28dc0210962589b908f52714f75c8fb79ff18bdd0024c43c7a220b", + "n": 4096, + "r": 8, + "p": 1 + }, + "mac": "f8de52e2627024eaa33f2ee5eadcd3d3815e10dd274ea966dc083d000cc8b258" + } +} diff --git a/src/testutils/testwallets/carol.pem b/src/testutils/testwallets/carol.pem new file mode 100644 index 00000000..5551c9c0 --- /dev/null +++ b/src/testutils/testwallets/carol.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY for erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8----- +ZTI1M2E1NzFjYTE1M2RjMmFlZTg0NTgxOWY3NGJjYzk3NzNiMDU4NmVkZWFkMTVh +OTRjYjcyMzVhNTAyNzQzNmIyYTExNTU1Y2U1MjFlNDk0NGUwOWFiMTc1NDlkODVi +NDg3ZGNkMjZjODRiNTAxN2EzOWUzMWEzNjcwODg5YmE= +-----END PRIVATE KEY for erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8----- \ No newline at end of file diff --git a/src/testutils/testwallets/dan.json b/src/testutils/testwallets/dan.json new file mode 100644 index 00000000..15eb9f79 --- /dev/null +++ b/src/testutils/testwallets/dan.json @@ -0,0 +1,22 @@ +{ + "version": 4, + "id": "fc8b9b89-2227-41ec-afd1-5e6853feb7b2", + "address": "b13a017423c366caff8cecfb77a12610a130f4888134122c7937feae0d6d7d17", + "bech32": "erd1kyaqzaprcdnv4luvanah0gfxzzsnpaygsy6pytrexll2urtd05ts9vegu7", + "crypto": { + "ciphertext": "b2a6fffd935ea3c9bc8ac136fdb548554faf0c5e4b78b347305499c29a4ae71dfca565297bfcf85df6faf850d2a42368178937b69a0e95ee62d8f112f98ad918", + "cipherparams": { + "iv": "46e398d62d85faa06d2f424303772ea3" + }, + "cipher": "aes-128-ctr", + "kdf": "scrypt", + "kdfparams": { + "dklen": 32, + "salt": "58b3e16f0fa600ca004bddd68c6487e63beb0df42d050e2eea3c838d9c664224", + "n": 4096, + "r": 8, + "p": 1 + }, + "mac": "141fc44faa4376187278cef7e6a5b8b1a3d5d4932f0590bac9f0772bf21dd358" + } +} diff --git a/src/testutils/testwallets/dan.pem b/src/testutils/testwallets/dan.pem new file mode 100644 index 00000000..f112d8f6 --- /dev/null +++ b/src/testutils/testwallets/dan.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY for erd1kyaqzaprcdnv4luvanah0gfxzzsnpaygsy6pytrexll2urtd05ts9vegu7----- +NTRlNDc5NzQzZTUxMjhlMGUyOWRiNGQ0YTIxZDc1OWI2OTAxMjQxZmFjNzdjNTAy +YzhlZjcwZGM3OWExMmE4YmIxM2EwMTc0MjNjMzY2Y2FmZjhjZWNmYjc3YTEyNjEw +YTEzMGY0ODg4MTM0MTIyYzc5MzdmZWFlMGQ2ZDdkMTc= +-----END PRIVATE KEY for erd1kyaqzaprcdnv4luvanah0gfxzzsnpaygsy6pytrexll2urtd05ts9vegu7----- \ No newline at end of file diff --git a/src/testutils/testwallets/eve.json b/src/testutils/testwallets/eve.json new file mode 100644 index 00000000..b95f2582 --- /dev/null +++ b/src/testutils/testwallets/eve.json @@ -0,0 +1,22 @@ +{ + "version": 4, + "id": "e770c455-a23b-4dcd-a7a5-0e22375dc233", + "address": "3af8d9c9423b2577c6252722c1d90212a4111f7203f9744f76fcfa1d0a310033", + "bech32": "erd18tudnj2z8vjh0339yu3vrkgzz2jpz8mjq0uhgnmklnap6z33qqeszq2yn4", + "crypto": { + "ciphertext": "ebabff99b80b867154cfc24726d47a58a6dfc2bf056e9fe0257841287f2f496c443533583d2cbf28e796f38186b3145774007876e93eca83d815b396cfe7f89a", + "cipherparams": { + "iv": "abc1ada087b472a0154e869f16ad3b2f" + }, + "cipher": "aes-128-ctr", + "kdf": "scrypt", + "kdfparams": { + "dklen": 32, + "salt": "d1a57ea919706bdad2916bc4ebf5bc072843b1333818808a424d34b8cd851491", + "n": 4096, + "r": 8, + "p": 1 + }, + "mac": "55b8bd2cada5766704ac9b70742cace95f5faa66a7f5edf7c9e3b31755710caf" + } +} diff --git a/src/testutils/testwallets/eve.pem b/src/testutils/testwallets/eve.pem new file mode 100644 index 00000000..dcbe731f --- /dev/null +++ b/src/testutils/testwallets/eve.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY for erd18tudnj2z8vjh0339yu3vrkgzz2jpz8mjq0uhgnmklnap6z33qqeszq2yn4----- +NjliODJmMzlhMTQ2MWI0MzFmOGZhMjY3Zjg3YTBiZjc5NDAyN2NlYTM3ZGM2NjE5 +MGU2NzQwYTZlNGNjY2RmMDNhZjhkOWM5NDIzYjI1NzdjNjI1MjcyMmMxZDkwMjEy +YTQxMTFmNzIwM2Y5NzQ0Zjc2ZmNmYTFkMGEzMTAwMzM= +-----END PRIVATE KEY for erd18tudnj2z8vjh0339yu3vrkgzz2jpz8mjq0uhgnmklnap6z33qqeszq2yn4----- \ No newline at end of file diff --git a/src/testutils/testwallets/frank.json b/src/testutils/testwallets/frank.json new file mode 100644 index 00000000..afb4a0f6 --- /dev/null +++ b/src/testutils/testwallets/frank.json @@ -0,0 +1,22 @@ +{ + "version": 4, + "id": "df70f3ef-bb40-4afd-8751-77b26b29356d", + "address": "b37f5d130beb8885b90ab574a8bfcdd894ca531a7d3d1f3431158d77d6185fbb", + "bech32": "erd1kdl46yctawygtwg2k462307dmz2v55c605737dp3zkxh04sct7asqylhyv", + "crypto": { + "ciphertext": "50e0992457078c3b1b5512e003ab911065fb64429dbc0dbec4d425670ecf410807de7ac5558568f70fac0c003ee9d090831e4e0801add602b89f0163735d11e8", + "cipherparams": { + "iv": "264b13aee12e96a25ad5dc91518cdfdb" + }, + "cipher": "aes-128-ctr", + "kdf": "scrypt", + "kdfparams": { + "dklen": 32, + "salt": "fc05efc44099f837d3fdf1741f86f97abe394ed6c9882ff26d35bc6deab72a53", + "n": 4096, + "r": 8, + "p": 1 + }, + "mac": "5308637cdac70085b775f4c7e55229530b8139aef4c3f26b7855cbc96317584c" + } +} diff --git a/src/testutils/testwallets/frank.pem b/src/testutils/testwallets/frank.pem new file mode 100644 index 00000000..6d04639d --- /dev/null +++ b/src/testutils/testwallets/frank.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY for erd1kdl46yctawygtwg2k462307dmz2v55c605737dp3zkxh04sct7asqylhyv----- +NzgyMGE5MTE4OWM3MjdjMDc1YjQ5OWM1MjNmMGRjNDAzZmVkMzc5OTBlYzhkMzdm +NGJkZWNiOGI2OGZkMmE5N2IzN2Y1ZDEzMGJlYjg4ODViOTBhYjU3NGE4YmZjZGQ4 +OTRjYTUzMWE3ZDNkMWYzNDMxMTU4ZDc3ZDYxODVmYmI= +-----END PRIVATE KEY for erd1kdl46yctawygtwg2k462307dmz2v55c605737dp3zkxh04sct7asqylhyv----- \ No newline at end of file diff --git a/src/testutils/testwallets/grace.json b/src/testutils/testwallets/grace.json new file mode 100644 index 00000000..81d640dc --- /dev/null +++ b/src/testutils/testwallets/grace.json @@ -0,0 +1,22 @@ +{ + "version": 4, + "id": "9aff338f-f504-403c-86cd-5e623bc81c42", + "address": "1e8a8b6b49de5b7be10aaa158a5a6a4abb4b56cc08f524bb5e6cd5f211ad3e13", + "bech32": "erd1r69gk66fmedhhcg24g2c5kn2f2a5k4kvpr6jfw67dn2lyydd8cfswy6ede", + "crypto": { + "ciphertext": "b8f4fe8790a980fb78e5b126c52e81009910101210420da1da0d558313a45eb8803b57825bc19a2a8b92d7af147f97a0bf261ce152bc61cccd6e34df4aa9ab09", + "cipherparams": { + "iv": "0be21668939564c5b2ac122a71ce97a4" + }, + "cipher": "aes-128-ctr", + "kdf": "scrypt", + "kdfparams": { + "dklen": 32, + "salt": "7c5b1a6f91d257a11b327b9745a27bb4708867e17a1cce942cd9a3a3d74e7bf0", + "n": 4096, + "r": 8, + "p": 1 + }, + "mac": "a3c251303a253d82ae2be6535a65d0584f6bb92a84199d7f0c6eff249b46ce9a" + } +} diff --git a/src/testutils/testwallets/grace.pem b/src/testutils/testwallets/grace.pem new file mode 100644 index 00000000..7b0836a5 --- /dev/null +++ b/src/testutils/testwallets/grace.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY for erd1r69gk66fmedhhcg24g2c5kn2f2a5k4kvpr6jfw67dn2lyydd8cfswy6ede----- +ODY5Yzk4ZGYwYjExZmRhMDgwNGQ3ODM1NGVjYTQzMWY4N2E2ZTkxMGZhZTE0MDgx +ZjczOGMzMTZhMjVlYTQwOTFlOGE4YjZiNDlkZTViN2JlMTBhYWExNThhNWE2YTRh +YmI0YjU2Y2MwOGY1MjRiYjVlNmNkNWYyMTFhZDNlMTM= +-----END PRIVATE KEY for erd1r69gk66fmedhhcg24g2c5kn2f2a5k4kvpr6jfw67dn2lyydd8cfswy6ede----- \ No newline at end of file diff --git a/src/testutils/testwallets/heidi.json b/src/testutils/testwallets/heidi.json new file mode 100644 index 00000000..7608b981 --- /dev/null +++ b/src/testutils/testwallets/heidi.json @@ -0,0 +1,22 @@ +{ + "version": 4, + "id": "1b55836f-946f-4dc3-946d-3c27e5096873", + "address": "6e224118d9068ae626878a1cfbebcb6a95a4715db86d1b51e06a04226cf30fd6", + "bech32": "erd1dc3yzxxeq69wvf583gw0h67td226gu2ahpk3k50qdgzzym8npltq7ndgha", + "crypto": { + "ciphertext": "04d8c15a32a8f5917980f8a1cc16adae29c1241ac42c917d154dbaced47c3208d372378eaa39e201785db891dfe397e33639cf23abcbeddd7e2ce38944e7e30c", + "cipherparams": { + "iv": "053caa5b1b236e786af178421fc0b829" + }, + "cipher": "aes-128-ctr", + "kdf": "scrypt", + "kdfparams": { + "dklen": 32, + "salt": "ec9bbfb8bceb60545b4500cb7990897aaf1ab51fa0154e7a2917c30759a4cfe5", + "n": 4096, + "r": 8, + "p": 1 + }, + "mac": "b326a517fea8e38af156e2c7d256144ada0f8ab6740d04faf2a8f98a54dff6c9" + } +} diff --git a/src/testutils/testwallets/heidi.pem b/src/testutils/testwallets/heidi.pem new file mode 100644 index 00000000..a8a693f2 --- /dev/null +++ b/src/testutils/testwallets/heidi.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY for erd1dc3yzxxeq69wvf583gw0h67td226gu2ahpk3k50qdgzzym8npltq7ndgha----- +YjMzODcyMjMzMjFjNTg3YzM1MTE5OGFkYzI1MzFjODIwYTM3OGQzNzQ4OTU2YjNk +YWUyZTBmNWExZGFhN2NhMTZlMjI0MTE4ZDkwNjhhZTYyNjg3OGExY2ZiZWJjYjZh +OTVhNDcxNWRiODZkMWI1MWUwNmEwNDIyNmNmMzBmZDY= +-----END PRIVATE KEY for erd1dc3yzxxeq69wvf583gw0h67td226gu2ahpk3k50qdgzzym8npltq7ndgha----- \ No newline at end of file diff --git a/src/testutils/testwallets/ivan.json b/src/testutils/testwallets/ivan.json new file mode 100644 index 00000000..ba0f7f34 --- /dev/null +++ b/src/testutils/testwallets/ivan.json @@ -0,0 +1,22 @@ +{ + "version": 4, + "id": "0b80d732-d4e6-4145-b5bb-698bdd323b3c", + "address": "899451b361a83e89d73b4096d3c90c209b27874c9c7cb01bb08b0bb4dc15693d", + "bech32": "erd13x29rvmp4qlgn4emgztd8jgvyzdj0p6vn37tqxas3v9mfhq4dy7shalqrx", + "crypto": { + "ciphertext": "2c7b56ea9ebec244382018818065a78f19cec328596b28eca2fa51defd22098f3c0164fbcb63b144f0c416fbefbf147e4df17c41eaed7caba2feb7fcdb7ec4e5", + "cipherparams": { + "iv": "f20e0d3fa965ef6c3084fdb712c7fcd9" + }, + "cipher": "aes-128-ctr", + "kdf": "scrypt", + "kdfparams": { + "dklen": 32, + "salt": "a12bab6dd6df4eb7b841214ab26ad7724b8be3a52873f7a3565ba65b85831028", + "n": 4096, + "r": 8, + "p": 1 + }, + "mac": "0a1c6c7a17c39b4882d00910b4bca532dd215b1e0b914f9624920ae1ac8b38c6" + } +} diff --git a/src/testutils/testwallets/ivan.pem b/src/testutils/testwallets/ivan.pem new file mode 100644 index 00000000..e9cdb61a --- /dev/null +++ b/src/testutils/testwallets/ivan.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY for erd13x29rvmp4qlgn4emgztd8jgvyzdj0p6vn37tqxas3v9mfhq4dy7shalqrx----- +MDhjNjE1NTUyMjFmMWYxMjE0M2Q3MWJiZWQ4MzFkMjk0N2U0OTRhMjJjZmQxN2I1 +NzQxMzhiY2M5ODQ1MzZkMzg5OTQ1MWIzNjFhODNlODlkNzNiNDA5NmQzYzkwYzIw +OWIyNzg3NGM5YzdjYjAxYmIwOGIwYmI0ZGMxNTY5M2Q= +-----END PRIVATE KEY for erd13x29rvmp4qlgn4emgztd8jgvyzdj0p6vn37tqxas3v9mfhq4dy7shalqrx----- \ No newline at end of file diff --git a/src/testutils/testwallets/judy.json b/src/testutils/testwallets/judy.json new file mode 100644 index 00000000..286c9ffd --- /dev/null +++ b/src/testutils/testwallets/judy.json @@ -0,0 +1,22 @@ +{ + "version": 4, + "id": "a3108434-5d2c-4dca-9f94-29e822697e20", + "address": "4a101a0f8f95f1218683900801cd971c6028b1597a771b2ed367d1ede09d9d2a", + "bech32": "erd1fggp5ru0jhcjrp5rjqyqrnvhr3sz3v2e0fm3ktknvlg7mcyan54qzccnan", + "crypto": { + "ciphertext": "8ae4807bc0d836606bf015ffde34274ddc454a61448bff6fc7a250a8530c5d478bc3988b8c3a3c631eb657e7fc5289cdfdcbb8f77be6faefb32ca77179636c73", + "cipherparams": { + "iv": "41f1833a14d4675cab19dd96bca5eb44" + }, + "cipher": "aes-128-ctr", + "kdf": "scrypt", + "kdfparams": { + "dklen": 32, + "salt": "6f5f765b00ece2f9dc654f008a21f7458d11aa53b56d119c90a29a9cc46c64f6", + "n": 4096, + "r": 8, + "p": 1 + }, + "mac": "01314543ab6866bba660952b697bc4555bc040770537be7409ceff83057b170f" + } +} diff --git a/src/testutils/testwallets/judy.pem b/src/testutils/testwallets/judy.pem new file mode 100644 index 00000000..23808c01 --- /dev/null +++ b/src/testutils/testwallets/judy.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY for erd1fggp5ru0jhcjrp5rjqyqrnvhr3sz3v2e0fm3ktknvlg7mcyan54qzccnan----- +MDgzZmQ2ZjAxYzRjMGZiMjUzY2QzOGMxMTE5OThiNGEzYjUzZDQyOTAwOGI4Mjhl +NzZhYzk3ZGE2ZjU4NTgyYTRhMTAxYTBmOGY5NWYxMjE4NjgzOTAwODAxY2Q5NzFj +NjAyOGIxNTk3YTc3MWIyZWQzNjdkMWVkZTA5ZDlkMmE= +-----END PRIVATE KEY for erd1fggp5ru0jhcjrp5rjqyqrnvhr3sz3v2e0fm3ktknvlg7mcyan54qzccnan----- \ No newline at end of file diff --git a/src/testutils/testwallets/mallory.json b/src/testutils/testwallets/mallory.json new file mode 100644 index 00000000..1416c2f1 --- /dev/null +++ b/src/testutils/testwallets/mallory.json @@ -0,0 +1,22 @@ +{ + "version": 4, + "id": "6756aa76-5934-4dc6-90c7-c261db98fe44", + "address": "1454931ffa758ab35654a5206b28e9dbf1fb8df8f9ced093bb2887eb39f7e7af", + "bech32": "erd1z32fx8l6wk9tx4j555sxk28fm0clhr0cl88dpyam9zr7kw0hu7hsx2j524", + "crypto": { + "ciphertext": "b05925b7f3c3eaec07258987848346355f66be347e748defb7064b34f3104a625e1f0b0046ec89618dcf70caa75a5c62a1a8b10563a0854bb068d99427697d4a", + "cipherparams": { + "iv": "673d0b58985d74e061288a41e9a5c0d8" + }, + "cipher": "aes-128-ctr", + "kdf": "scrypt", + "kdfparams": { + "dklen": 32, + "salt": "a313394c586b66d5fb1fad55fb02528113e2cce9f5663a9af4ca8fb794ad7ddf", + "n": 4096, + "r": 8, + "p": 1 + }, + "mac": "54039bd973c972e7d21b87589fc6cb5758e72a73afcc92944d5d14ed403a895c" + } +} diff --git a/src/testutils/testwallets/mallory.pem b/src/testutils/testwallets/mallory.pem new file mode 100644 index 00000000..be452cd0 --- /dev/null +++ b/src/testutils/testwallets/mallory.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY for erd1z32fx8l6wk9tx4j555sxk28fm0clhr0cl88dpyam9zr7kw0hu7hsx2j524----- +OTVjMWMwMzRlM2NmNzczNzdmMTU3NTFhYWZjZmZiMmEyZWIyZDE2NDAxYzY3MTQ0 +NDIyZTFkMDNlYzIwYWM1MjE0NTQ5MzFmZmE3NThhYjM1NjU0YTUyMDZiMjhlOWRi +ZjFmYjhkZjhmOWNlZDA5M2JiMjg4N2ViMzlmN2U3YWY= +-----END PRIVATE KEY for erd1z32fx8l6wk9tx4j555sxk28fm0clhr0cl88dpyam9zr7kw0hu7hsx2j524----- \ No newline at end of file diff --git a/src/testutils/testwallets/mike.json b/src/testutils/testwallets/mike.json new file mode 100644 index 00000000..d46a9e7b --- /dev/null +++ b/src/testutils/testwallets/mike.json @@ -0,0 +1,22 @@ +{ + "version": 4, + "id": "3f6adbc3-1215-4c31-9a61-a049b430e6f7", + "address": "e32afedc904fe1939746ad973beb383563cf63642ba669b3040f9b9428a5ed60", + "bech32": "erd1uv40ahysflse896x4ktnh6ecx43u7cmy9wnxnvcyp7deg299a4sq6vaywa", + "crypto": { + "ciphertext": "39a3ee873e39b30dd486dfe36fb9f8d8fb7bdaa83b30a35ac823acb0f69fb97413ff916afc6184888b60ffbe068a99c9109bb0291e21eae8c9556527263bbde7", + "cipherparams": { + "iv": "e99cc4008d995919b0da66aa94f95991" + }, + "cipher": "aes-128-ctr", + "kdf": "scrypt", + "kdfparams": { + "dklen": 32, + "salt": "44f6da2344cf56a23c7ebdc22033caab21adf0ff18144ddf513b5a56f7f04eb5", + "n": 4096, + "r": 8, + "p": 1 + }, + "mac": "eba324243f326ac2fd78c339962822147b8f01cd13c40fe019cf3b7834657d31" + } +} diff --git a/src/testutils/testwallets/mike.pem b/src/testutils/testwallets/mike.pem new file mode 100644 index 00000000..b2c01e67 --- /dev/null +++ b/src/testutils/testwallets/mike.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY for erd1uv40ahysflse896x4ktnh6ecx43u7cmy9wnxnvcyp7deg299a4sq6vaywa----- +Y2JkMjYzNThmMDg0YWZlMDA0ZDUzZjQ2NzMwM2Q3MTMzZjU1MzBiYjhkODE2NzUx +MWI3YzEyNjg1YWJjNTY4N2UzMmFmZWRjOTA0ZmUxOTM5NzQ2YWQ5NzNiZWIzODM1 +NjNjZjYzNjQyYmE2NjliMzA0MGY5Yjk0MjhhNWVkNjA= +-----END PRIVATE KEY for erd1uv40ahysflse896x4ktnh6ecx43u7cmy9wnxnvcyp7deg299a4sq6vaywa----- \ No newline at end of file diff --git a/src/testutils/testwallets/mnemonic.txt b/src/testutils/testwallets/mnemonic.txt new file mode 100644 index 00000000..a6c1bd69 --- /dev/null +++ b/src/testutils/testwallets/mnemonic.txt @@ -0,0 +1 @@ +moral volcano peasant pass circle pen over picture flat shop clap goat never lyrics gather prepare woman film husband gravity behind test tiger improve \ No newline at end of file diff --git a/src/testutils/testwallets/password.txt b/src/testutils/testwallets/password.txt new file mode 100644 index 00000000..7aa311ad --- /dev/null +++ b/src/testutils/testwallets/password.txt @@ -0,0 +1 @@ +password \ No newline at end of file diff --git a/src/testutils/utils.ts b/src/testutils/utils.ts index d5c17f65..7e8b637d 100644 --- a/src/testutils/utils.ts +++ b/src/testutils/utils.ts @@ -1,22 +1,8 @@ import { PathLike } from "fs"; -import { ProxyProvider } from "../proxyProvider"; import { Code } from "../smartcontracts/code"; import { AbiRegistry } from "../smartcontracts/typesystem"; import { TransactionWatcher } from "../transactionWatcher"; -// TODO: Adjust with respect to current terminology (localnet instead of devnet). -export function getDevnetProvider(): ProxyProvider { - return new ProxyProvider("http://localhost:7950", {timeout: 5000}); -} - -export function getTestnetProvider(): ProxyProvider { - return new ProxyProvider("https://testnet-gateway.elrond.com", {timeout: 5000}); -} - -export function getMainnetProvider(): ProxyProvider { - return new ProxyProvider("https://gateway.elrond.com", {timeout: 20000}); -} - export async function loadContractCode(path: PathLike): Promise { if (isBrowser()) { return Code.fromUrl(path.toString()); @@ -27,7 +13,7 @@ export async function loadContractCode(path: PathLike): Promise { export async function loadAbiRegistry(paths: PathLike[]): Promise { let sources = paths.map(e => e.toString()); - + if (isBrowser()) { return AbiRegistry.load({ urls: sources }); } @@ -37,7 +23,7 @@ export async function loadAbiRegistry(paths: PathLike[]): Promise { export async function extendAbiRegistry(registry: AbiRegistry, path: PathLike): Promise { let source = path.toString(); - + if (isBrowser()) { return registry.extendFromUrl(source); } diff --git a/src/testutils/wallets.ts b/src/testutils/wallets.ts index c591f7cf..15ff0edc 100644 --- a/src/testutils/wallets.ts +++ b/src/testutils/wallets.ts @@ -1,110 +1,54 @@ +import * as fs from "fs"; +import * as path from "path"; +import { Account } from "../account"; import { Address } from "../address"; -import { ISigner } from "../interface"; +import { IProvider, ISigner } from "../interface"; import { UserSecretKey } from "../walletcore"; import { UserSigner } from "../walletcore/userSigner"; -export class TestWallets { - mnemonic: string; - password: string; - alice: TestWallet; - bob: TestWallet; - carol: TestWallet; - - constructor() { - this.mnemonic = "moral volcano peasant pass circle pen over picture flat shop clap goat never lyrics gather prepare woman film husband gravity behind test tiger improve"; - this.password = "password"; - - let aliceKeyFile = { - "version": 4, - "id": "0dc10c02-b59b-4bac-9710-6b2cfa4284ba", - "address": "0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1", - "bech32": "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", - "crypto": { - "ciphertext": "4c41ef6fdfd52c39b1585a875eb3c86d30a315642d0e35bb8205b6372c1882f135441099b11ff76345a6f3a930b5665aaf9f7325a32c8ccd60081c797aa2d538", - "cipherparams": { - "iv": "033182afaa1ebaafcde9ccc68a5eac31" - }, - "cipher": "aes-128-ctr", - "kdf": "scrypt", - "kdfparams": { - "dklen": 32, - "salt": "4903bd0e7880baa04fc4f886518ac5c672cdc745a6bd13dcec2b6c12e9bffe8d", - "n": 4096, - "r": 8, - "p": 1 - }, - "mac": "5b4a6f14ab74ba7ca23db6847e28447f0e6a7724ba9664cf425df707a84f5a8b" - } - }; +export async function loadAndSyncTestWallets(provider: IProvider): Promise> { + let wallets = await loadTestWallets(); + await syncTestWallets(wallets, provider); + return wallets; +} - let bobKeyFile = { - "version": 4, - "id": "85fdc8a7-7119-479d-b7fb-ab4413ed038d", - "address": "8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8", - "bech32": "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx", - "crypto": { - "ciphertext": "c2664a31350aaf6a00525560db75c254d0aea65dc466441356c1dd59253cceb9e83eb05730ef3f42a11573c9a0e33dd952d488f00535b35357bb41d127b1eb82", - "cipherparams": { - "iv": "18378411e31f6c4e99f1435d9ab82831" - }, - "cipher": "aes-128-ctr", - "kdf": "scrypt", - "kdfparams": { - "dklen": 32, - "salt": "18304455ac2dbe2a2018bda162bd03ef95b81622e99d8275c34a6d5e6932a68b", - "n": 4096, - "r": 8, - "p": 1 - }, - "mac": "23756172195ac483fa29025dc331bc7aa2c139533922a8dc08642eb0a677541f" - } - }; +export async function syncTestWallets(wallets: Record, provider: IProvider) { + await Promise.all(Object.values(wallets).map(async (wallet) => wallet.sync(provider))); +} - let carolKeyFile = { - "version": 4, - "id": "65894f35-d142-41d2-9335-6ad02e0ed0be", - "address": "b2a11555ce521e4944e09ab17549d85b487dcd26c84b5017a39e31a3670889ba", - "bech32": "erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8", - "crypto": { - "ciphertext": "bdfb984a1e7c7460f0a289749609730cdc99d7ce85b59305417c2c0f007b2a6aaa7203dd94dbf27315bced39b0b281769fbc70b01e6e57f89ae2f2a9e9100007", - "cipherparams": { - "iv": "258ed2b4dc506b4dc9d274b0449b0eb0" - }, - "cipher": "aes-128-ctr", - "kdf": "scrypt", - "kdfparams": { - "dklen": 32, - "salt": "4f2f5530ce28dc0210962589b908f52714f75c8fb79ff18bdd0024c43c7a220b", - "n": 4096, - "r": 8, - "p": 1 - }, - "mac": "f8de52e2627024eaa33f2ee5eadcd3d3815e10dd274ea966dc083d000cc8b258" - } - }; +export async function loadTestWallets(): Promise> { + let walletNames = ["alice", "bob", "carol", "dan", "eve", "frank", "grace", "heidi", "ivan", "judy", "mallory", "mike"]; + let wallets = await Promise.all(walletNames.map(async name => await loadTestWallet(name))); + let walletMap: Record = {}; + for (let i in walletNames) { + walletMap[walletNames[i]] = wallets[i]; + } + return walletMap; +} - let alicePEM = `-----BEGIN PRIVATE KEY for erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th----- -NDEzZjQyNTc1ZjdmMjZmYWQzMzE3YTc3ODc3MTIxMmZkYjgwMjQ1ODUwOTgxZTQ4 -YjU4YTRmMjVlMzQ0ZThmOTAxMzk0NzJlZmY2ODg2NzcxYTk4MmYzMDgzZGE1ZDQy -MWYyNGMyOTE4MWU2Mzg4ODIyOGRjODFjYTYwZDY5ZTE= ------END PRIVATE KEY for erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th-----`; +export async function loadMnemonic(): Promise { + return await readTestWalletFileContents("mnemonic.txt"); +} - let bobPEM = `-----BEGIN PRIVATE KEY for erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx----- -YjhjYTZmODIwM2ZiNGI1NDVhOGU4M2M1Mzg0ZGEwMzNjNDE1ZGIxNTViNTNmYjVi -OGViYTdmZjVhMDM5ZDYzOTgwNDlkNjM5ZTVhNjk4MGQxY2QyMzkyYWJjY2U0MTAy -OWNkYTc0YTE1NjM1MjNhMjAyZjA5NjQxY2MyNjE4Zjg= ------END PRIVATE KEY for erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx-----`; +export async function loadPassword(): Promise { + return await readTestWalletFileContents("password.txt"); +} - let carolPEM = `-----BEGIN PRIVATE KEY for erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8----- -ZTI1M2E1NzFjYTE1M2RjMmFlZTg0NTgxOWY3NGJjYzk3NzNiMDU4NmVkZWFkMTVh -OTRjYjcyMzVhNTAyNzQzNmIyYTExNTU1Y2U1MjFlNDk0NGUwOWFiMTc1NDlkODVi -NDg3ZGNkMjZjODRiNTAxN2EzOWUzMWEzNjcwODg5YmE= ------END PRIVATE KEY for erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8-----`; +export async function loadTestWallet(name: string): Promise { + let jsonContents = JSON.parse(await readTestWalletFileContents(name + ".json")); + let pemContents = await readTestWalletFileContents(name + ".pem"); + let pemKey = UserSecretKey.fromPem(pemContents); + return new TestWallet( + new Address(jsonContents.address), + pemKey.hex(), + jsonContents, + pemContents); +} - this.alice = new TestWallet(new Address("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9", aliceKeyFile, alicePEM); - this.bob = new TestWallet(new Address("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"), "b8ca6f8203fb4b545a8e83c5384da033c415db155b53fb5b8eba7ff5a039d639", bobKeyFile, bobPEM); - this.carol = new TestWallet(new Address("erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8"), "e253a571ca153dc2aee845819f74bcc9773b0586edead15a94cb7235a5027436", carolKeyFile, carolPEM); - } +async function readTestWalletFileContents(name: string): Promise { + let basePath = path.join(__dirname, "testwallets"); + let filePath = path.join(basePath, name); + return await fs.promises.readFile(filePath, { encoding: "utf8" }); } export class TestWallet { @@ -114,6 +58,7 @@ export class TestWallet { readonly signer: ISigner; readonly keyFileObject: any; readonly pemFileText: any; + readonly account: Account; constructor(address: Address, secretKeyHex: string, keyFileObject: any, pemFileText: any) { this.address = address; @@ -122,5 +67,11 @@ export class TestWallet { this.signer = new UserSigner(UserSecretKey.fromString(secretKeyHex)); this.keyFileObject = keyFileObject; this.pemFileText = pemFileText; + this.account = new Account(this.address); + } + + async sync(provider: IProvider) { + await this.account.sync(provider); + return this; } } diff --git a/src/token.ts b/src/token.ts new file mode 100644 index 00000000..116f2439 --- /dev/null +++ b/src/token.ts @@ -0,0 +1,143 @@ +import BigNumber from "bignumber.js"; +import { Address } from "./address"; + +export enum TokenType { + Fungible, + Semifungible, + Nonfungible +} + +export class Token { + identifier: string = ''; // Token identifier (ticker + random string, eg. MYTOKEN-12345) + name: string = ''; // Token name (eg. MyTokenName123) + type: TokenType = TokenType.Fungible; + owner: Address = new Address(); + minted: BigNumber = new BigNumber(0); + burnt: BigNumber = new BigNumber(0); + decimals: number = 18; + isPaused: boolean = false; + canUpgrade: boolean = false; + canMint: boolean = false; + canBurn: boolean = false; + canChangeOwner: boolean = false; + canPause: boolean = false; + canFreeze: boolean = false; + canWipe: boolean = false; + canAddSpecialRoles: boolean = false; + canTransferNftCreateRole: boolean = false; + nftCreateStopped: boolean = false; + wiped: boolean = false; + + constructor(init?: Partial) { + Object.assign(this, init); + } + + static fromHttpResponse(response: { + token: string, + name: string, + type: string, + owner: string, + minted: string, + burnt: string, + decimals: number, + isPaused: boolean, + canUpgrade: boolean, + canMint: boolean, + canBurn: boolean, + canChangeOwner: boolean, + canPause: boolean, + canFreeze: boolean, + canWipe: boolean + }): Token { + return new Token({ + identifier: response.token, + name: response.name, + type: TokenType[response.type as keyof typeof TokenType], + owner: new Address(response.owner), + minted: new BigNumber(response.minted), + burnt: new BigNumber(response.burnt), + decimals: response.decimals, + isPaused: response.isPaused, + canUpgrade: response.canUpgrade, + canMint: response.canMint, + canBurn: response.canBurn, + canChangeOwner: response.canChangeOwner, + canPause: response.canPause, + canFreeze: response.canFreeze, + canWipe: response.canWipe, + }); + } + + static fromTokenProperties(tokenIdentifier: string, results: any[]): Token { + let [tokenName, tokenType, owner, totalMinted, totalBurned, ...propertiesBuffers] = results; + let properties = parseTokenProperties(propertiesBuffers); + return new Token({ + identifier: tokenIdentifier, + type: TokenType[tokenType.toString() as keyof typeof TokenType], + name: tokenName.toString(), + owner, + minted: new BigNumber(totalMinted), + burnt: new BigNumber(totalBurned), + decimals: properties.NumDecimals.toNumber(), + isPaused: properties.IsPaused, + canUpgrade: properties.CanUpgrade, + canMint: properties.CanMint, + canBurn: properties.CanBurn, + canChangeOwner: properties.CanChangeOwner, + canPause: properties.CanPause, + canFreeze: properties.CanFreeze, + canWipe: properties.CanWipe, + canAddSpecialRoles: properties.CanAddSpecialRoles, + canTransferNftCreateRole: properties.CanTransferNFTCreateRole, + nftCreateStopped: properties.NFTCreateStopped, + wiped: properties.NumWiped + }); + } + + getTokenName(): string { + return this.name; + } + + typeAsString(): string { + return TokenType[this.type]; + } + + getTokenIdentifier(): string { + return this.identifier; + } + + isEgld(): boolean { + return this.getTokenIdentifier() == "EGLD"; + } + + isFungible(): boolean { + return !this.isNft(); + } + + isNft(): boolean { + switch (this.type) { + case TokenType.Fungible: + return false; + case TokenType.Semifungible: + case TokenType.Nonfungible: + return true; + } + } +} + +function parseValue(value: string): any { + switch (value) { + case "true": return true; + case "false": return false; + default: return new BigNumber(value); + } +} + +function parseTokenProperties(propertiesBuffers: Buffer[]): Record { + let properties: Record = {}; + for (let buffer of propertiesBuffers) { + let [name, value] = buffer.toString().split("-"); + properties[name] = parseValue(value); + } + return properties; +} diff --git a/src/transaction.dev.net.spec.ts b/src/transaction.dev.net.spec.ts index aaff8c95..25442494 100644 --- a/src/transaction.dev.net.spec.ts +++ b/src/transaction.dev.net.spec.ts @@ -1,32 +1,29 @@ import { Transaction } from "./transaction"; import { GasLimit } from "./networkParams"; -import { Account } from "./account"; import { TransactionPayload } from "./transactionPayload"; import { NetworkConfig } from "./networkConfig"; import { Balance } from "./balance"; -import { TestWallets } from "./testutils"; -import { getDevnetProvider } from "./testutils"; +import { loadTestWallets, TestWallet } from "./testutils"; import { Logger } from "./logger"; import { assert } from "chai"; +import { chooseProvider } from "./interactive"; describe("test transaction", function () { - let wallets = new TestWallets(); - let aliceWallet = wallets.alice; - let alice = new Account(aliceWallet.address); - let aliceSigner = aliceWallet.signer; - let bobWallet = wallets.bob; - let bob = new Account(bobWallet.address); - - it("should send transactions", async function() { + let alice: TestWallet, bob: TestWallet; + before(async function () { + ({ alice, bob } = await loadTestWallets()); + }); + + it("should send transactions", async function () { this.timeout(20000); - let devnet = getDevnetProvider(); + let devnet = chooseProvider("local-testnet"); await NetworkConfig.getDefault().sync(devnet); await alice.sync(devnet); await bob.sync(devnet); - let initialBalanceOfBob = bob.balance; + let initialBalanceOfBob = bob.account.balance; let transactionOne = new Transaction({ receiver: bob.address, @@ -38,12 +35,12 @@ describe("test transaction", function () { value: Balance.egld(43) }); - transactionOne.setNonce(alice.nonce); - alice.incrementNonce(); - transactionTwo.setNonce(alice.nonce); + transactionOne.setNonce(alice.account.nonce); + alice.account.incrementNonce(); + transactionTwo.setNonce(alice.account.nonce); - await aliceSigner.sign(transactionOne); - await aliceSigner.sign(transactionTwo); + await alice.signer.sign(transactionOne); + await alice.signer.sign(transactionTwo); await transactionOne.send(devnet); await transactionTwo.send(devnet); @@ -52,15 +49,15 @@ describe("test transaction", function () { await transactionTwo.awaitExecuted(devnet); await bob.sync(devnet); - let newBalanceOfBob = bob.balance; + let newBalanceOfBob = bob.account.balance; assert.deepEqual(Balance.egld(85).valueOf(), newBalanceOfBob.valueOf().minus(initialBalanceOfBob.valueOf())); }); - it("should simulate transactions", async function() { + it("should simulate transactions", async function () { this.timeout(20000); - let devnet = getDevnetProvider(); + let devnet = chooseProvider("local-testnet"); await NetworkConfig.getDefault().sync(devnet); await alice.sync(devnet); @@ -79,11 +76,11 @@ describe("test transaction", function () { value: Balance.egld(1000000) }); - transactionOne.setNonce(alice.nonce); - transactionTwo.setNonce(alice.nonce); + transactionOne.setNonce(alice.account.nonce); + transactionTwo.setNonce(alice.account.nonce); - await aliceSigner.sign(transactionOne); - await aliceSigner.sign(transactionTwo); + await alice.signer.sign(transactionOne); + await alice.signer.sign(transactionTwo); Logger.trace(JSON.stringify(await transactionOne.simulate(devnet), null, 4)); Logger.trace(JSON.stringify(await transactionTwo.simulate(devnet), null, 4)); diff --git a/src/transaction.spec.ts b/src/transaction.spec.ts index ac1ae7ed..f40593ad 100644 --- a/src/transaction.spec.ts +++ b/src/transaction.spec.ts @@ -5,7 +5,7 @@ import { Nonce } from "./nonce"; import { ChainID, GasLimit, GasPrice, GasPriceModifier, TransactionOptions, TransactionVersion } from "./networkParams"; import { TransactionPayload } from "./transactionPayload"; import { Balance } from "./balance"; -import { TestWallets } from "./testutils"; +import { loadTestWallets, TestWallet } from "./testutils"; import { NetworkConfig } from "./networkConfig"; import { Address } from "./address"; @@ -31,7 +31,10 @@ describe("test transaction", () => { }); describe("test transaction construction", async () => { - let wallets = new TestWallets(); + let wallets: Record; + before(async function () { + wallets = await loadTestWallets(); + }); it("with no data, no value", async () => { let transaction = new Transaction({ diff --git a/src/transaction.ts b/src/transaction.ts index 29962191..6364f154 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -151,7 +151,7 @@ export class Transaction implements ISignable { * }); * * tx.setNonce(alice.nonce); - * await aliceSigner.sign(tx); + * await alice.signer.sign(tx); * ``` */ setNonce(nonce: Nonce) { @@ -327,19 +327,20 @@ export class Transaction implements ISignable { * * @param provider The provider to use * @param cacheLocally Whether to cache the response locally, on the transaction object + * @param awaitNotarized Whether to wait for the transaction to be notarized + * @param withResults Whether to wait for the transaction results */ - async getAsOnNetwork(provider: IProvider, cacheLocally: boolean = true): Promise { + async getAsOnNetwork(provider: IProvider, cacheLocally = true, awaitNotarized = true, withResults = true): Promise { if (this.hash.isEmpty()) { throw new errors.ErrTransactionHashUnknown(); } // For Smart Contract transactions, wait for their full execution & notarization before returning. let isSmartContractTransaction = this.receiver.isContractAddress(); - if (isSmartContractTransaction) { + if (isSmartContractTransaction && awaitNotarized) { await this.awaitNotarized(provider); } - let withResults = isSmartContractTransaction; let response = await provider.getTransaction(this.hash, this.sender, withResults); if (cacheLocally) { diff --git a/src/transactionPayload.ts b/src/transactionPayload.ts index aea88713..e92588e0 100644 --- a/src/transactionPayload.ts +++ b/src/transactionPayload.ts @@ -50,6 +50,14 @@ export class TransactionPayload { return this.data.toString(); } + getEncodedArguments(): string[] { + return this.toString().split("@"); + } + + getRawArguments(): Buffer[] { + return this.getEncodedArguments().map(argument => Buffer.from(argument, "hex")); + } + /** * Returns the length of the data. */ diff --git a/src/transactionWatcher.ts b/src/transactionWatcher.ts index 1e5510e8..f703d70e 100644 --- a/src/transactionWatcher.ts +++ b/src/transactionWatcher.ts @@ -15,7 +15,7 @@ export class TransactionWatcher { static DefaultPollingInterval: number = 6000; static DefaultTimeout: number = TransactionWatcher.DefaultPollingInterval * 15; - static NoopOnStatusReceived = (_: TransactionStatus) => {}; + static NoopOnStatusReceived = (_: TransactionStatus) => { }; private readonly hash: TransactionHash; private readonly provider: IProvider; @@ -79,7 +79,7 @@ export class TransactionWatcher { return this.awaitConditionally( isNotarized, doFetch, - (_) => {}, + (_) => { }, errorProvider ); } @@ -96,7 +96,7 @@ export class TransactionWatcher { let stop = false; let fetchedData: TData | undefined = undefined; - timeoutTimer.start(this.timeout).finally(() => { + let _ = timeoutTimer.start(this.timeout).finally(() => { timeoutTimer.stop(); stop = true; }); diff --git a/src/walletcore/pem.spec.ts b/src/walletcore/pem.spec.ts index 591e379b..89fb7646 100644 --- a/src/walletcore/pem.spec.ts +++ b/src/walletcore/pem.spec.ts @@ -1,33 +1,33 @@ import * as errors from "../errors"; import { assert } from "chai"; -import { TestWallets } from "../testutils"; +import { loadTestWallets, TestWallet } from "../testutils"; import { parse, parseUserKey, parseValidatorKey } from "./pem"; import { BLS } from "."; import { Buffer } from "buffer"; describe("test PEMs", () => { - let wallets = new TestWallets(); - let alice = wallets.alice; - let bob = wallets.bob; - let carol = wallets.carol; + let alice: TestWallet, bob: TestWallet, carol: TestWallet; + before(async function () { + ({ alice, bob, carol } = await loadTestWallets()); + }); it("should parseUserKey", () => { let aliceKey = parseUserKey(alice.pemFileText); - + assert.equal(aliceKey.hex(), alice.secretKeyHex); assert.equal(aliceKey.generatePublicKey().toAddress().bech32(), alice.address.bech32()); }); it("should parseValidatorKey", async () => { await BLS.initIfNecessary(); - + let pem = `-----BEGIN PRIVATE KEY for e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208----- N2NmZjk5YmQ2NzE1MDJkYjdkMTViYzhhYmMwYzlhODA0ZmI5MjU0MDZmYmRkNTBm MWU0YzE3YTRjZDc3NDI0Nw== -----END PRIVATE KEY for e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208-----`; let validatorKey = parseValidatorKey(pem); - + assert.equal(validatorKey.hex(), "7cff99bd671502db7d15bc8abc0c9a804fb925406fbdd50f1e4c17a4cd774247"); assert.equal(validatorKey.generatePublicKey().hex(), "e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208"); }); @@ -55,7 +55,7 @@ ${payloadCarol} -----END PRIVATE KEY for carol `; - assert.deepEqual(parse(trivialContent, 64), expected); + assert.deepEqual(parse(trivialContent, 64), expected); let contentWithWhitespaces = ` -----BEGIN PRIVATE KEY for alice diff --git a/src/walletcore/pem.ts b/src/walletcore/pem.ts index 45724efc..546ab0cd 100644 --- a/src/walletcore/pem.ts +++ b/src/walletcore/pem.ts @@ -38,7 +38,7 @@ export function parse(text: string, expectedLength: number): Buffer[] { let asBytes = Buffer.from(asHex, "hex"); if (asBytes.length != expectedLength) { - throw new errors.ErrBadPEM("incorrect key length"); + throw new errors.ErrBadPEM(`incorrect key length: expected ${expectedLength}, found ${asBytes.length}`); } buffers.push(asBytes); diff --git a/src/walletcore/users.spec.ts b/src/walletcore/users.spec.ts index d42a4215..27b64670 100644 --- a/src/walletcore/users.spec.ts +++ b/src/walletcore/users.spec.ts @@ -1,6 +1,6 @@ import * as errors from "../errors"; import { assert } from "chai"; -import { TestWallets } from "../testutils"; +import { loadMnemonic, loadPassword, loadTestWallets, TestWallet } from "../testutils"; import { UserSecretKey } from "./userKeys"; import { Mnemonic } from "./mnemonic"; import { UserWallet } from "./userWallet"; @@ -12,15 +12,16 @@ import { Nonce } from "../nonce"; import { Balance } from "../balance"; import { ChainID, GasLimit, GasPrice } from "../networkParams"; import { TransactionPayload } from "../transactionPayload"; -import {UserVerifier} from "./userVerifier"; -import {SignableMessage} from "../signableMessage"; +import { UserVerifier } from "./userVerifier"; +import { SignableMessage } from "../signableMessage"; describe("test user wallets", () => { - let wallets = new TestWallets(); - let alice = wallets.alice; - let bob = wallets.bob; - let carol = wallets.carol; - let password = wallets.password; + let alice: TestWallet, bob: TestWallet, carol: TestWallet; + let password: string; + before(async function () { + ({ alice, bob, carol } = await loadTestWallets()); + password = await loadPassword(); + }); it("should generate mnemonic", () => { let mnemonic = Mnemonic.generate(); @@ -28,8 +29,8 @@ describe("test user wallets", () => { assert.lengthOf(words, 24); }); - it("should derive keys", () => { - let mnemonic = Mnemonic.fromString(wallets.mnemonic); + it("should derive keys", async () => { + let mnemonic = Mnemonic.fromString(await loadMnemonic()); assert.equal(mnemonic.deriveKey(0).hex(), alice.secretKeyHex); assert.equal(mnemonic.deriveKey(1).hex(), bob.secretKeyHex); @@ -37,7 +38,7 @@ describe("test user wallets", () => { }); it("should create secret key", () => { - let keyHex = wallets.alice.secretKeyHex; + let keyHex = alice.secretKeyHex; let fromBuffer = new UserSecretKey(Buffer.from(keyHex, "hex")); let fromHex = UserSecretKey.fromString(keyHex); @@ -162,7 +163,7 @@ describe("test user wallets", () => { it("should sign transactions using PEM files", async () => { let signer = UserSigner.fromPem(alice.pemFileText); - + let transaction = new Transaction({ nonce: new Nonce(0), value: Balance.Zero(), @@ -177,7 +178,7 @@ describe("test user wallets", () => { assert.equal(transaction.getSignature().hex(), "c0bd2b3b33a07b9cc5ee7435228acb0936b3829c7008aacabceea35163e555e19a34def2c03a895cf36b0bcec30a7e11215c11efc0da29294a11234eb2b3b906"); }); - it("signs a general message", function() { + it("signs a general message", function () { let signer = new UserSigner(UserSecretKey.fromString("1a927e2af5306a9bb2ea777f73e06ecc0ac9aaa72fb4ea3fecf659451394cccf")); let verifier = new UserVerifier(UserSecretKey.fromString("1a927e2af5306a9bb2ea777f73e06ecc0ac9aaa72fb4ea3fecf659451394cccf").generatePublicKey()); const message = new SignableMessage({ diff --git a/src/walletcore/validators.spec.ts b/src/walletcore/validators.spec.ts index e737e2f5..609098cc 100644 --- a/src/walletcore/validators.spec.ts +++ b/src/walletcore/validators.spec.ts @@ -1,9 +1,7 @@ import { assert } from "chai"; -import { TestWallets } from "../testutils"; import { BLS, ValidatorSecretKey } from "./validatorKeys"; describe("test validator keys", () => { - let wallets = new TestWallets(); it("should create secret key and sign a message", async () => { await BLS.initIfNecessary(); diff --git a/tslint.json b/tslint.json index 837830ab..9e0c3972 100644 --- a/tslint.json +++ b/tslint.json @@ -1,15 +1,16 @@ { - "rules": { - "no-string-throw": true, - "no-unused-expression": true, - "no-duplicate-variable": true, - "curly": true, - "class-name": true, - "semicolon": [ - true, - "always" - ], - "triple-equals": false - }, - "defaultSeverity": "warning" -} \ No newline at end of file + "rules": { + "no-string-throw": true, + "no-unused-expression": true, + "no-duplicate-variable": true, + "no-floating-promises": true, + "curly": true, + "class-name": true, + "semicolon": [ + true, + "always" + ], + "triple-equals": false + }, + "defaultSeverity": "warning" +}