diff --git a/package-lock.json b/package-lock.json index b2fdf5979..069e62ec1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2091,9 +2091,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.103", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.103.tgz", - "integrity": "sha512-c/uKWR1Z/W30Wy/sx3dkZoj4BijbXX85QKWu9jJfjho3LBAXNEGAEW3oWiGb+dotA6C6BzCTxL2/aLes7jlUeg==", + "version": "1.4.104", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.104.tgz", + "integrity": "sha512-2kjoAyiG7uMyGRM9mx25s3HAzmQG2ayuYXxsFmYugHSDcwxREgLtscZvbL1JcW9S/OemeQ3f/SG6JhDwpnCclQ==", "dev": true }, "node_modules/elliptic": { @@ -7456,9 +7456,9 @@ } }, "electron-to-chromium": { - "version": "1.4.103", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.103.tgz", - "integrity": "sha512-c/uKWR1Z/W30Wy/sx3dkZoj4BijbXX85QKWu9jJfjho3LBAXNEGAEW3oWiGb+dotA6C6BzCTxL2/aLes7jlUeg==", + "version": "1.4.104", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.104.tgz", + "integrity": "sha512-2kjoAyiG7uMyGRM9mx25s3HAzmQG2ayuYXxsFmYugHSDcwxREgLtscZvbL1JcW9S/OemeQ3f/SG6JhDwpnCclQ==", "dev": true }, "elliptic": { diff --git a/src/apiProvider.ts b/src/apiProvider.ts index c8c2466c3..e52d19866 100644 --- a/src/apiProvider.ts +++ b/src/apiProvider.ts @@ -1,14 +1,14 @@ import axios, { AxiosRequestConfig } from "axios"; -import { IApiProvider } from "./interface"; +import { IApiProvider, IHash } from "./interface"; import * as errors from "./errors"; import { Logger } from "./logger"; import { NetworkStake } from "./networkStake"; import { Stats } from "./stats"; -import { TransactionHash, TransactionStatus } from "./transaction"; -import { TransactionOnNetwork } from "./transactionOnNetwork"; import { Token } from "./token"; import { NFTToken } from "./nftToken"; import { defaultConfig } from "./constants"; +import { ApiNetworkProvider } from "./networkProvider/apiNetworkProvider"; +import { ITransactionOnNetwork, ITransactionStatus } from "./interfaceOfNetwork"; /** * This is a temporary change, this will be the only provider used, ProxyProvider will be deprecated @@ -16,6 +16,10 @@ import { defaultConfig } from "./constants"; export class ApiProvider implements IApiProvider { private url: string; private config: AxiosRequestConfig; + /** + * @deprecated used only for preparatory refactoring (unifying network providers) + */ + private readonly backingProvider: ApiNetworkProvider; /** * Creates a new ApiProvider. @@ -25,6 +29,7 @@ export class ApiProvider implements IApiProvider { constructor(url: string, config?: AxiosRequestConfig) { this.url = url; this.config = {...defaultConfig, ...config}; + this.backingProvider = new ApiNetworkProvider(url, config); } /** @@ -44,19 +49,15 @@ export class ApiProvider implements IApiProvider { /** * Fetches the state of a {@link Transaction}. */ - async getTransaction(txHash: TransactionHash): Promise { - return this.doGetGeneric(`transactions/${txHash.toString()}`, (response) => - TransactionOnNetwork.fromHttpResponse(txHash, response) - ); + async getTransaction(txHash: IHash): Promise { + return await this.backingProvider.getTransaction(txHash); } /** * Queries the status of a {@link Transaction}. */ - async getTransactionStatus(txHash: TransactionHash): Promise { - return this.doGetGeneric(`transactions/${txHash.toString()}?fields=status`, (response) => - new TransactionStatus(response.status) - ); + async getTransactionStatus(txHash: IHash): Promise { + return await this.backingProvider.getTransactionStatus(txHash); } async getToken(tokenIdentifier: string): Promise { diff --git a/src/boundaryAdapters.ts b/src/boundaryAdapters.ts index bd1d13e1b..6278ad21f 100644 --- a/src/boundaryAdapters.ts +++ b/src/boundaryAdapters.ts @@ -1,12 +1,12 @@ import { Address } from "./address"; import { ErrInvariantFailed } from "./errors"; -import { IAddressOfExternalSigner, ISignatureOfExternalSigner } from "./interface"; +import { IBech32Address, ISignature } from "./interface"; import { Signature } from "./signature"; /** * Adapts a signature created by other components (e.g. erdjs-walletcore, erdjs-hw-provider) to one understood by erdjs. */ -export function adaptToSignature(obj: ISignatureOfExternalSigner): Signature { +export function adaptToSignature(obj: ISignature): Signature { if (!obj.hex || typeof obj.hex() !== "string") { throw new ErrInvariantFailed("adaptToSignature: bad or missing hex()") } @@ -17,7 +17,7 @@ export function adaptToSignature(obj: ISignatureOfExternalSigner): Signature { /** * Adapts an address created by other components (e.g. erdjs-walletcore, erdjs-hw-provider) to one understood by erdjs. */ -export function adaptToAddress(obj: IAddressOfExternalSigner): Address { +export function adaptToAddress(obj: IBech32Address): Address { if (!obj.bech32 || typeof obj.bech32() !== "string") { throw new ErrInvariantFailed("adaptToSignature: bad or missing bech32()") } diff --git a/src/errors.ts b/src/errors.ts index 8a50ca538..1e06a71b9 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -111,7 +111,7 @@ export class ErrInvariantFailed extends Err { /** * Signals an unexpected condition. */ - export class ErrUnexpectedCondition extends Err { +export class ErrUnexpectedCondition extends Err { public constructor(message: string) { super(`Unexpected condition: [${message}]`); } @@ -272,16 +272,6 @@ export class ErrInvalidFunctionName extends Err { } } -/** - * Signals an error that happened during a request against the Network. - */ - export class ErrNetworkProvider extends Err { - public constructor(url: string, error: string, inner?: Error) { - let message = `Request error on url [${url}]: [${error}]`; - super(message, inner); - } -} - /** * Signals an error that happened during a HTTP GET request. */ diff --git a/src/hash.ts b/src/hash.ts index 25ed7b40c..5f7095a76 100644 --- a/src/hash.ts +++ b/src/hash.ts @@ -35,6 +35,10 @@ export class Hash { } toString(): string { + return this.hex(); + } + + hex(): string { return this.hash.toString("hex"); } diff --git a/src/interface.ts b/src/interface.ts index b1a260db0..b4ccaf9e8 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -1,30 +1,26 @@ -import { Transaction, TransactionHash, TransactionStatus } from "./transaction"; +import { Transaction } from "./transaction"; import { NetworkConfig } from "./networkConfig"; import { Signature } from "./signature"; -import { Address } from "./address"; import { AccountOnNetwork, TokenOfAccountOnNetwork } from "./account"; import { Query } from "./smartcontracts"; import { QueryResponse } from "./smartcontracts"; import { NetworkStake } from "./networkStake"; import { Stats } from "./stats"; import { NetworkStatus } from "./networkStatus"; -import { TransactionOnNetwork } from "./transactionOnNetwork"; import { Token } from "./token"; import BigNumber from "bignumber.js"; +import { ITransactionOnNetwork, ITransactionStatus } from "./interfaceOfNetwork"; -/** - * @deprecated This interface will be removed in a future release, upon merging {@link IProvider} and {@link IApiProvider}. - */ export interface ITransactionFetcher { /** * Fetches the state of a {@link Transaction}. */ - getTransaction(txHash: TransactionHash, hintSender?: Address, withResults?: boolean): Promise; + getTransaction(txHash: IHash, hintSender?: IBech32Address, withResults?: boolean): Promise; /** * Queries the status of a {@link Transaction}. */ - getTransactionStatus(txHash: TransactionHash): Promise; + getTransactionStatus(txHash: IHash): Promise; } /** @@ -44,22 +40,22 @@ export interface IProvider extends ITransactionFetcher { /** * Fetches the state of an {@link Account}. */ - getAccount(address: Address): Promise; + getAccount(address: IBech32Address): Promise; /** * Fetches the list of ESDT data for all the tokens of an address. */ - getAddressEsdtList(address: Address): Promise; + getAddressEsdtList(address: IBech32Address): Promise; /** * Fetches the ESDT data for a token of an address. */ - getAddressEsdt(address: Address, tokenIdentifier: string): Promise; + getAddressEsdt(address: IBech32Address, 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; + getAddressNft(address: IBech32Address, tokenIdentifier: string, nonce: BigNumber): Promise; /** * Queries a Smart Contract - runs a pure function defined by the contract and returns its results. @@ -69,12 +65,12 @@ export interface IProvider extends ITransactionFetcher { /** * Broadcasts an already-signed {@link Transaction}. */ - sendTransaction(tx: Transaction): Promise; + sendTransaction(tx: Transaction): Promise; /** * Simulates the processing of an already-signed {@link Transaction}. */ - simulateTransaction(tx: Transaction): Promise; + simulateTransaction(tx: Transaction): Promise; /** * Get method that receives the resource url and on callback the method used to map the response. @@ -115,7 +111,7 @@ export interface ISignable { /** * Returns the signable object in its raw form - a sequence of bytes to be signed. */ - serializeForSigning(signedBy: IAddressOfExternalSigner): Buffer; + serializeForSigning(signedBy: IBech32Address): Buffer; /** * Applies the computed signature on the object itself. @@ -123,7 +119,7 @@ export interface ISignable { * @param signature The computed signature * @param signedBy The address of the signer */ - applySignature(signature: ISignatureOfExternalSigner, signedBy: IAddressOfExternalSigner): void; + applySignature(signature: ISignature, signedBy: IBech32Address): void; } /** @@ -137,7 +133,7 @@ export interface IVerifiable { /** * Returns the signable object in its raw form - a sequence of bytes to be verified. */ - serializeForSigning(signedBy?: Address): Buffer; + serializeForSigning(signedBy?: IBech32Address): Buffer; } /** @@ -147,18 +143,9 @@ export interface Disposable { dispose(): void; } -/** - * An interface that defines the signature, as computed by an external (to erdjs) signer. - * Implementations are outside of erdjs. - */ -export interface ISignatureOfExternalSigner { - hex(): string; -} - -/** - * An interface that defines the address of an external (to erdjs) signer. - * Implementations are outside of erdjs. - */ -export interface IAddressOfExternalSigner { - bech32(): string; -} +export interface ISignature { hex(): string; } +export interface IHash { hex(): string; } +export interface IBech32Address { bech32(): string; } +export interface ITransactionValue { toString(): string; } +export interface ITransactionPayload { encoded(): string; } +export interface INonce { valueOf(): number; } diff --git a/src/interfaceOfNetwork.ts b/src/interfaceOfNetwork.ts new file mode 100644 index 000000000..2d366e1d6 --- /dev/null +++ b/src/interfaceOfNetwork.ts @@ -0,0 +1,65 @@ +import { IBech32Address, IHash, INonce, ITransactionPayload, ITransactionValue } from "./interface"; + +export interface ITransactionOnNetwork { + hash: IHash; + type: string; + value: ITransactionValue; + receiver: IBech32Address; + sender: IBech32Address; + data: ITransactionPayload; + status: ITransactionStatus; + receipt: ITransactionReceipt; + contractResults: IContractResults; + logs: ITransactionLogs; + + isCompleted(): boolean; + getAllEvents(): ITransactionEvent[]; +} + +export interface ITransactionStatus { + isPending(): boolean; + isFailed(): boolean; + isInvalid(): boolean; + isExecuted(): boolean; +} + +export interface ITransactionReceipt { + data: string; +} + +export interface IContractResults { + items: IContractResultItem[]; +} + +export interface IContractResultItem { + hash: IHash; + nonce: INonce; + receiver: IBech32Address; + sender: IBech32Address; + data: string; + returnMessage: string; + logs: ITransactionLogs; +} + +export interface ITransactionLogs { + events: ITransactionEvent[]; + + findSingleOrNoneEvent(identifier: string, predicate?: (event: ITransactionEvent) => boolean): ITransactionEvent | undefined; + findFirstOrNoneEvent(identifier: string, predicate?: (event: ITransactionEvent) => boolean): ITransactionEvent | undefined; + findEvents(identifier: string, predicate?: (event: ITransactionEvent) => boolean): ITransactionEvent[]; +} + +export interface ITransactionEvent { + readonly address: IBech32Address; + readonly identifier: string; + readonly topics: ITransactionEventTopic[]; + readonly data: string; + + findFirstOrNoneTopic(predicate: (topic: ITransactionEventTopic) => boolean): ITransactionEventTopic | undefined; + getLastTopic(): ITransactionEventTopic; +} + +export interface ITransactionEventTopic { + toString(): string; + hex(): string; +} diff --git a/src/networkProvider/apiNetworkProvider.ts b/src/networkProvider/apiNetworkProvider.ts index 3d809404a..6bed3490c 100644 --- a/src/networkProvider/apiNetworkProvider.ts +++ b/src/networkProvider/apiNetworkProvider.ts @@ -1,22 +1,20 @@ import axios, { AxiosRequestConfig } from "axios"; import { AccountOnNetwork } from "../account"; -import { Address } from "../address"; -import { defaultConfig } from "../constants"; -import { ErrNetworkProvider } from "../errors"; -import { IContractQueryResponse, IDefinitionOfFungibleTokenOnNetwork, IDefinitionOfTokenCollectionOnNetwork, IFungibleTokenOfAccountOnNetwork, INetworkProvider, INonFungibleTokenOfAccountOnNetwork, ITransactionOnNetwork, Pagination } from "./interface"; -import { Logger } from "../logger"; +import { IAddress, IContractQueryResponse, IDefinitionOfFungibleTokenOnNetwork, IDefinitionOfTokenCollectionOnNetwork, IFungibleTokenOfAccountOnNetwork, IHash, INetworkProvider, INonce, INonFungibleTokenOfAccountOnNetwork, ITransaction, Pagination } from "./interface"; import { NetworkConfig } from "../networkConfig"; import { NetworkStake } from "../networkStake"; import { NetworkStatus } from "../networkStatus"; -import { Nonce } from "../nonce"; import { Query } from "../smartcontracts"; import { Stats } from "../stats"; -import { Transaction, TransactionHash, TransactionStatus } from "../transaction"; import { ContractQueryResponse } from "./contractResults"; import { ProxyNetworkProvider } from "./proxyNetworkProvider"; import { DefinitionOfFungibleTokenOnNetwork, DefinitionOfTokenCollectionOnNetwork } from "./tokenDefinitions"; import { FungibleTokenOfAccountOnNetwork, NonFungibleTokenOfAccountOnNetwork } from "./tokens"; import { TransactionOnNetwork } from "./transactions"; +import { TransactionStatus } from "./transactionStatus"; +import { Hash } from "./primitives"; +import { ErrNetworkProvider } from "./errors"; +import { defaultAxiosConfig } from "./config"; // TODO: Find & remove duplicate code between "ProxyNetworkProvider" and "ApiNetworkProvider". export class ApiNetworkProvider implements INetworkProvider { @@ -26,7 +24,7 @@ export class ApiNetworkProvider implements INetworkProvider { constructor(url: string, config?: AxiosRequestConfig) { this.url = url; - this.config = { ...defaultConfig, ...config }; + this.config = { ...defaultAxiosConfig, ...config }; this.backingProxyNetworkProvider = new ProxyNetworkProvider(url, config); } @@ -50,13 +48,13 @@ export class ApiNetworkProvider implements INetworkProvider { return stats; } - async getAccount(address: Address): Promise { + async getAccount(address: IAddress): Promise { let response = await this.doGetGeneric(`accounts/${address.bech32()}`); let account = AccountOnNetwork.fromHttpResponse(response); return account; } - async getFungibleTokensOfAccount(address: Address, pagination?: Pagination): Promise { + async getFungibleTokensOfAccount(address: IAddress, pagination?: Pagination): Promise { pagination = pagination || Pagination.default(); let url = `accounts/${address.bech32()}/tokens?${this.buildPaginationParams(pagination)}`; @@ -68,7 +66,7 @@ export class ApiNetworkProvider implements INetworkProvider { return tokens; } - async getNonFungibleTokensOfAccount(address: Address, pagination?: Pagination): Promise { + async getNonFungibleTokensOfAccount(address: IAddress, pagination?: Pagination): Promise { pagination = pagination || Pagination.default(); let url = `accounts/${address.bech32()}/nfts?${this.buildPaginationParams(pagination)}`; @@ -80,37 +78,37 @@ export class ApiNetworkProvider implements INetworkProvider { return tokens; } - async getFungibleTokenOfAccount(address: Address, tokenIdentifier: string): Promise { + async getFungibleTokenOfAccount(address: IAddress, tokenIdentifier: string): Promise { let response = await this.doGetGeneric(`accounts/${address.bech32()}/tokens/${tokenIdentifier}`); let tokenData = FungibleTokenOfAccountOnNetwork.fromHttpResponse(response); return tokenData; } - async getNonFungibleTokenOfAccount(address: Address, collection: string, nonce: Nonce): Promise { + async getNonFungibleTokenOfAccount(address: IAddress, collection: string, nonce: INonce): Promise { let response = await this.doGetGeneric(`accounts/${address.bech32()}/nfts/${collection}-${nonce.hex()}`); let tokenData = NonFungibleTokenOfAccountOnNetwork.fromApiHttpResponse(response); return tokenData; } - async getTransaction(txHash: TransactionHash): Promise { + async getTransaction(txHash: IHash): Promise { let response = await this.doGetGeneric(`transactions/${txHash.toString()}`); let transaction = TransactionOnNetwork.fromApiHttpResponse(txHash, response); return transaction; } - async getTransactionStatus(txHash: TransactionHash): Promise { + async getTransactionStatus(txHash: IHash): Promise { let response = await this.doGetGeneric(`transactions/${txHash.toString()}?fields=status`); let status = new TransactionStatus(response.status); return status; } - async sendTransaction(tx: Transaction): Promise { + async sendTransaction(tx: ITransaction): Promise { let response = await this.doPostGeneric("transactions", tx.toSendable()); - let hash = new TransactionHash(response.txHash); + let hash = new Hash(response.txHash); return hash; } - async simulateTransaction(tx: Transaction): Promise { + async simulateTransaction(tx: ITransaction): Promise { return await this.backingProxyNetworkProvider.simulateTransaction(tx); } @@ -133,7 +131,7 @@ export class ApiNetworkProvider implements INetworkProvider { return definition; } - async getNonFungibleToken(collection: string, nonce: Nonce): Promise { + async getNonFungibleToken(collection: string, nonce: INonce): Promise { let response = await this.doGetGeneric(`nfts/${collection}-${nonce.hex()}`); let token = NonFungibleTokenOfAccountOnNetwork.fromApiHttpResponse(response); return token; @@ -181,7 +179,7 @@ export class ApiNetworkProvider implements INetworkProvider { private handleApiError(error: any, resourceUrl: string) { if (!error.response) { - Logger.warn(error); + console.warn(error); throw new ErrNetworkProvider(resourceUrl, error.toString(), error); } diff --git a/src/networkProvider/config.ts b/src/networkProvider/config.ts new file mode 100644 index 000000000..9d352c9d1 --- /dev/null +++ b/src/networkProvider/config.ts @@ -0,0 +1,11 @@ +const JSONbig = require("json-bigint"); + +export const defaultAxiosConfig = { + timeout: 1000, + // See: https://github.com/axios/axios/issues/983 regarding transformResponse + transformResponse: [ + function (data: any) { + return JSONbig.parse(data); + } + ] +}; diff --git a/src/networkProvider/contractResults.ts b/src/networkProvider/contractResults.ts index 8cebbb3e0..3d9b4299e 100644 --- a/src/networkProvider/contractResults.ts +++ b/src/networkProvider/contractResults.ts @@ -1,20 +1,16 @@ import { BigNumber } from "bignumber.js"; -import { Address } from "../address"; -import { Balance } from "../balance"; -import { Hash } from "../hash"; -import { IContractQueryResponse, IContractResultItem, IContractResults } from "./interface"; -import { GasLimit, GasPrice } from "../networkParams"; -import { Nonce } from "../nonce"; -import { MaxUint64, ReturnCode } from "../smartcontracts"; -import { TransactionHash } from "../transaction"; - -export class ContractResults implements IContractResults { - readonly items: IContractResultItem[]; - - constructor(items: IContractResultItem[]) { +import { IAddress, IContractQueryResponse, IContractReturnCode, IGasLimit, IGasPrice, IHash, INonce } from "./interface"; +import { TransactionLogs } from "./transactionLogs"; +import { MaxUint64 } from "../smartcontracts/query"; +import { Address, ContractReturnCode, Hash, Nonce, TransactionValue } from "./primitives"; + +export class ContractResults { + readonly items: ContractResultItem[]; + + constructor(items: ContractResultItem[]) { this.items = items; - this.items.sort(function (a: IContractResultItem, b: IContractResultItem) { + this.items.sort(function (a: ContractResultItem, b: ContractResultItem) { return a.nonce.valueOf() - b.nonce.valueOf(); }); } @@ -34,19 +30,24 @@ export class ContractResults implements IContractResults { } } -export class ContractResultItem implements IContractResultItem { - hash: Hash = Hash.empty(); - nonce: Nonce = new Nonce(0); - value: Balance = Balance.Zero(); - receiver: Address = new Address(); - sender: Address = new Address(); +export class ContractResultItem { + hash: IHash = new Hash("") + nonce: INonce = new Nonce(0); + value: TransactionValue = new TransactionValue(""); + receiver: IAddress = new Address(""); + sender: IAddress = new Address(""); data: string = ""; - previousHash: Hash = Hash.empty(); - originalHash: Hash = Hash.empty(); - gasLimit: GasLimit = new GasLimit(0); - gasPrice: GasPrice = new GasPrice(0); + previousHash: Hash = new Hash(""); + originalHash: Hash = new Hash(""); + gasLimit: IGasLimit = 0; + gasPrice: IGasPrice = 0; callType: number = 0; returnMessage: string = ""; + logs: TransactionLogs = TransactionLogs.empty(); + + constructor(init?: Partial) { + Object.assign(this, init); + } static fromProxyHttpResponse(response: any): ContractResultItem { let item = ContractResultItem.fromHttpResponse(response); @@ -65,37 +66,39 @@ export class ContractResultItem implements IContractResultItem { private static fromHttpResponse(response: any): ContractResultItem { let item = new ContractResultItem(); - item.hash = new TransactionHash(response.hash); + item.hash = new Hash(response.hash); item.nonce = new Nonce(response.nonce || 0); - item.value = Balance.fromString(response.value); + item.value = new TransactionValue((response.value || 0).toString()); item.receiver = new Address(response.receiver); item.sender = new Address(response.sender); - item.previousHash = new TransactionHash(response.prevTxHash); - item.originalHash = new TransactionHash(response.originalTxHash); - item.gasLimit = new GasLimit(response.gasLimit); - item.gasPrice = new GasPrice(response.gasPrice); + item.previousHash = new Hash(response.prevTxHash); + item.originalHash = new Hash(response.originalTxHash); + item.gasLimit = Number(response.gasLimit || 0); + item.gasPrice = Number(response.gasPrice || 0); item.data = response.data || ""; item.callType = response.callType; item.returnMessage = response.returnMessage; + item.logs = TransactionLogs.fromHttpResponse(response.logs || {}); + return item; } } export class ContractQueryResponse implements IContractQueryResponse { returnData: string[] = []; - returnCode: ReturnCode = ReturnCode.None; + returnCode: IContractReturnCode = new ContractReturnCode(""); returnMessage: string = ""; - gasUsed: GasLimit = new GasLimit(0); + gasUsed: IGasLimit = 0; static fromHttpResponse(payload: any): ContractQueryResponse { let response = new ContractQueryResponse(); let gasRemaining = new BigNumber(payload["gasRemaining"] || payload["GasRemaining"] || 0); response.returnData = payload["returnData"] || []; - response.returnCode = payload["returnCode"] || ""; + response.returnCode = new ContractReturnCode(payload["returnCode"] || ""); response.returnMessage = payload["returnMessage"] || ""; - response.gasUsed = new GasLimit(MaxUint64.minus(gasRemaining).toNumber()); + response.gasUsed = MaxUint64.minus(gasRemaining).toNumber(); return response; } diff --git a/src/networkProvider/errors.ts b/src/networkProvider/errors.ts new file mode 100644 index 000000000..586e5cf83 --- /dev/null +++ b/src/networkProvider/errors.ts @@ -0,0 +1,30 @@ +/** + * The base class for exceptions (errors). + */ +export class Err extends Error { + inner: Error | undefined = undefined; + + public constructor(message: string, inner?: Error) { + super(message); + this.inner = inner; + } +} + +/** + * Signals an unexpected condition. + */ +export class ErrUnexpectedCondition extends Err { + public constructor(message: string) { + super(`Unexpected condition: [${message}]`); + } +} + +/** + * Signals an error that happened during a request against the Network. + */ +export class ErrNetworkProvider extends Err { + public constructor(url: string, error: string, inner?: Error) { + let message = `Request error on url [${url}]: [${error}]`; + super(message, inner); + } +} diff --git a/src/networkProvider/interface.ts b/src/networkProvider/interface.ts index 4f21dbe49..5d4a2c453 100644 --- a/src/networkProvider/interface.ts +++ b/src/networkProvider/interface.ts @@ -1,19 +1,12 @@ import { BigNumber } from "bignumber.js"; import { AccountOnNetwork } from "../account"; -import { Address } from "../address"; -import { Balance } from "../balance"; -import { Hash } from "../hash"; import { NetworkConfig } from "../networkConfig"; -import { GasLimit, GasPrice } from "../networkParams"; import { NetworkStake } from "../networkStake"; import { NetworkStatus } from "../networkStatus"; -import { Nonce } from "../nonce"; -import { Signature } from "../signature"; -import { Query, ReturnCode } from "../smartcontracts"; +import { Query } from "../smartcontracts"; import { Stats } from "../stats"; -import { Transaction, TransactionHash, TransactionStatus } from "../transaction"; -import { TransactionLogs } from "../transactionLogs"; -import { TransactionPayload } from "../transactionPayload"; +import { TransactionOnNetwork } from "./transactions"; +import { TransactionStatus } from "./transactionStatus"; /** * An interface that defines the endpoints of an HTTP API Provider. @@ -42,48 +35,48 @@ export interface INetworkProvider { /** * Fetches the state of an {@link Account}. */ - getAccount(address: Address): Promise; + getAccount(address: IAddress): Promise; /** * Fetches data about the fungible tokens held by an account. */ - getFungibleTokensOfAccount(address: Address, pagination?: Pagination): Promise; + getFungibleTokensOfAccount(address: IAddress, pagination?: Pagination): Promise; /** * Fetches data about the non-fungible tokens held by account. */ - getNonFungibleTokensOfAccount(address: Address, pagination?: Pagination): Promise; + getNonFungibleTokensOfAccount(address: IAddress, pagination?: Pagination): Promise; /** * Fetches data about a specific fungible token held by an account. */ - getFungibleTokenOfAccount(address: Address, tokenIdentifier: string): Promise; + getFungibleTokenOfAccount(address: IAddress, tokenIdentifier: string): Promise; /** * Fetches data about a specific non-fungible token (instance) held by an account. */ - getNonFungibleTokenOfAccount(address: Address, collection: string, nonce: Nonce): Promise; + getNonFungibleTokenOfAccount(address: IAddress, collection: string, nonce: INonce): Promise; /** * Fetches the state of a {@link Transaction}. */ - getTransaction(txHash: TransactionHash): Promise; + getTransaction(txHash: IHash): Promise; /** * Queries the status of a {@link Transaction}. */ - getTransactionStatus(txHash: TransactionHash): Promise; + getTransactionStatus(txHash: IHash): Promise; /** * Broadcasts an already-signed {@link Transaction}. */ - sendTransaction(tx: Transaction): Promise; + sendTransaction(tx: ITransaction): Promise; /** * Simulates the processing of an already-signed {@link Transaction}. * */ - simulateTransaction(tx: Transaction): Promise; + simulateTransaction(tx: ITransaction): Promise; /** * Queries a Smart Contract - runs a pure function defined by the contract and returns its results. @@ -105,7 +98,7 @@ export interface INetworkProvider { /** * Fetches data about a specific non-fungible token (instance). */ - getNonFungibleToken(collection: string, nonce: Nonce): Promise; + getNonFungibleToken(collection: string, nonce: INonce): Promise; /** * Performs a generic GET action against the provider (useful for new HTTP endpoints, not yet supported by erdjs). @@ -128,8 +121,8 @@ export interface INonFungibleTokenOfAccountOnNetwork { collection: string; attributes: Buffer; balance: BigNumber; - nonce: Nonce; - creator: Address; + nonce: INonce; + creator: IAddress; royalties: BigNumber; } @@ -138,7 +131,7 @@ export interface IDefinitionOfFungibleTokenOnNetwork { identifier: string; name: string; ticker: string; - owner: Address; + owner: IAddress; decimals: number; supply: BigNumber; isPaused: boolean; @@ -157,7 +150,7 @@ export interface IDefinitionOfTokenCollectionOnNetwork { type: string; name: string; ticker: string; - owner: Address; + owner: IAddress; decimals: number; canPause: boolean; canFreeze: boolean; @@ -166,55 +159,19 @@ export interface IDefinitionOfTokenCollectionOnNetwork { // TODO: add "assets", "roles" } -export interface ITransactionOnNetwork { - hash: TransactionHash; - nonce: Nonce; - round: number; - epoch: number; - value: Balance; - receiver: Address; - sender: Address; - gasPrice: GasPrice; - gasLimit: GasLimit; - data: TransactionPayload; - signature: Signature; - status: TransactionStatus; - timestamp: number; - blockNonce: Nonce; - hyperblockNonce: Nonce; - hyperblockHash: Hash; - logs: TransactionLogs; - contractResults: IContractResults; -} - -export interface IContractResults { - items: IContractResultItem[]; -} - -export interface IContractResultItem { - hash: Hash; - nonce: Nonce; - value: Balance; - receiver: Address; - sender: Address; - data: string; - previousHash: Hash; - originalHash: Hash; - gasLimit: GasLimit; - gasPrice: GasPrice; - callType: number; - returnMessage: string; -} - export interface IContractQueryResponse { returnData: string[]; - returnCode: ReturnCode; + returnCode: IContractReturnCode; returnMessage: string; - gasUsed: GasLimit; - + gasUsed: IGasLimit; + getReturnDataParts(): Buffer[]; } +export interface IContractReturnCode { + toString(): string; +} + export interface IContractSimulation { } @@ -226,3 +183,15 @@ export class Pagination { return { from: 0, size: 100 }; } } + +export interface ITransaction { + toSendable(): any; +} + +export interface IHexable { hex(): string } +export interface IHash extends IHexable { } +export interface IAddress { bech32(): string; } +export interface INonce extends IHexable { valueOf(): number; } +export interface ITransactionPayload { encoded(): string; } +export interface IGasLimit { valueOf(): number; } +export interface IGasPrice { valueOf(): number; } diff --git a/src/networkProvider/primitives.ts b/src/networkProvider/primitives.ts new file mode 100644 index 000000000..2f77a8769 --- /dev/null +++ b/src/networkProvider/primitives.ts @@ -0,0 +1,102 @@ +import { IAddress, IHash, INonce, ITransactionPayload } from "./interface"; + +export class Hash implements IHash { + private readonly value: string; + + constructor(value: string) { + this.value = value; + } + + hex(): string { + return this.value; + } +} + +export class Address implements IAddress { + private readonly value: string; + + constructor(value: string) { + this.value = value; + } + + bech32(): string { + return this.value; + } +} + +export class Nonce implements INonce { + private readonly value: number; + + constructor(value: number) { + this.value = value; + } + + valueOf(): number { + return this.value; + } + + hex(): string { + return numberToPaddedHex(this.value); + } +} + +export class TransactionValue { + private readonly value: string; + + constructor(value: string) { + this.value = value; + } + + toString(): string { + return this.value; + } +} + +export class TransactionPayload implements ITransactionPayload { + private readonly decoded: Buffer; + + constructor(encoded: string) { + this.decoded = Buffer.from(encoded || "", "base64"); + } + + encoded(): string { + return this.decoded.toString("base64"); + } + + toString() { + return this.decoded.toString(); + } +} + +export class ContractReturnCode { + private readonly value: string; + + constructor(value: string) { + this.value = value; + } + + toString() { + return this.value; + } +} + +export function numberToPaddedHex(value: number) { + let hex = value.toString(16); + return zeroPadStringIfOddLength(hex); +} + +export function isPaddedHex(input: string) { + input = input || ""; + let decodedThenEncoded = Buffer.from(input, "hex").toString("hex"); + return input.toUpperCase() == decodedThenEncoded.toUpperCase(); +} + +export function zeroPadStringIfOddLength(input: string): string { + input = input || ""; + + if (input.length % 2 == 1) { + return "0" + input; + } + + return input; +} diff --git a/src/networkProvider/providers.dev.net.spec.ts b/src/networkProvider/providers.dev.net.spec.ts index affbd8d0b..0b21855a5 100644 --- a/src/networkProvider/providers.dev.net.spec.ts +++ b/src/networkProvider/providers.dev.net.spec.ts @@ -1,16 +1,15 @@ import { assert } from "chai"; -import { Hash } from "../hash"; -import { INetworkProvider, ITransactionOnNetwork } from "./interface"; -import { Address } from "../address"; +import { INetworkProvider } from "./interface"; import { loadTestWallets, TestWallet } from "../testutils"; -import { TransactionHash, TransactionStatus } from "../transaction"; -import { Nonce } from "../nonce"; +import { TransactionHash } from "../transaction"; import { ContractFunction, Query } from "../smartcontracts"; import { BigUIntValue, U32Value, BytesValue, VariadicValue, VariadicType, CompositeType, BytesType, BooleanType } from "../smartcontracts/typesystem"; import { BigNumber } from "bignumber.js"; -import { Balance } from "../balance"; import { ApiNetworkProvider } from "./apiNetworkProvider"; import { ProxyNetworkProvider } from "./proxyNetworkProvider"; +import { TransactionOnNetwork } from "./transactions"; +import { TransactionStatus } from "./transactionStatus"; +import { Address, Nonce, TransactionValue } from "./primitives"; describe("test network providers on devnet: Proxy and API", function () { let apiProvider: INetworkProvider = new ApiNetworkProvider("https://devnet-api.elrond.com", { timeout: 10000 }); @@ -104,17 +103,12 @@ describe("test network providers on devnet: Proxy and API", function () { this.timeout(20000); let hashes = [ - new TransactionHash("b41f5fc39e96b1f194d07761c6efd6cb92278b95f5012ab12cbc910058ca8b54"), - new TransactionHash("7757397a59378e9d0f6d5f08cc934c260e33a50ae0d73fdf869f7c02b6b47b33"), - new TransactionHash("b87238089e81527158a6daee520280324bc7e5322ba54d1b3c9a5678abe953ea"), - new TransactionHash("b45dd5e598bc85ba71639f2cbce8c5dff2fbe93159e637852fddeb16c0e84a48"), - new TransactionHash("83db780e98d4d3c917668c47b33ba51445591efacb0df2a922f88e7dfbb5fc7d"), - new TransactionHash("c2eb62b28cc7320da2292d87944c5424a70e1f443323c138c1affada7f6e9705"), - // TODO: Uncomment once the Gateway returns all SCRs in this case, as well. - // new TransactionHash("98e913c2a78cafdf4fa7f0113c1285fb29c2409bd7a746bb6f5506ad76841d54"), - new TransactionHash("5b05945be8ba2635e7c13d792ad727533494358308b5fcf36a816e52b5b272b8"), - new TransactionHash("47b089b5f0220299a017359003694a01fd75d075100166b8072c418d5143fe06"), - new TransactionHash("85021f20b06662240d8302d62f68031bbf7261bacb53b84e3dc9346c0f10a8e7") + new TransactionHash("a069c663831002651fd542479869cc61103465f3284dace772e7480f81429fa8"), + new TransactionHash("de3bc87f3e057e28ea6a625acd6d6d332e24f35ea73e820462b71256c8ecffb7"), + new TransactionHash("dbefa0299fe6b2336eb0bc3123fa623845c276e5c6e2a175adf1a562d5e77718"), + new TransactionHash("2a8ccbd91b7d9460a86174b5a8d4e6aa073b38674d1ee8107e728980a66f0676"), + // TODO: uncomment after fix (SCR missing on API) + // new TransactionHash("be7914b1eb4c6bd352ba1d86991959b443e446e0ad49fb796be3495c287b2472") ]; for (const hash of hashes) { @@ -127,15 +121,16 @@ describe("test network providers on devnet: Proxy and API", function () { }); // TODO: Strive to have as little differences as possible between Proxy and API. - function ignoreKnownTransactionDifferencesBetweenProviders(apiResponse: ITransactionOnNetwork, proxyResponse: ITransactionOnNetwork) { + function ignoreKnownTransactionDifferencesBetweenProviders(apiResponse: TransactionOnNetwork, proxyResponse: TransactionOnNetwork) { // TODO: Remove this once "tx.status" is uniformized. apiResponse.status = proxyResponse.status = new TransactionStatus("ignore"); // Ignore fields which are not present on API response: + proxyResponse.type = ""; proxyResponse.epoch = 0; - proxyResponse.blockNonce = new Nonce(0); - proxyResponse.hyperblockNonce = new Nonce(0); - proxyResponse.hyperblockHash = new Hash(""); + proxyResponse.blockNonce = 0; + proxyResponse.hyperblockNonce = 0; + proxyResponse.hyperblockHash = ""; } // TODO: Fix differences of "tx.status", then enable this test. @@ -143,16 +138,11 @@ describe("test network providers on devnet: Proxy and API", function () { this.timeout(20000); let hashes = [ - new TransactionHash("b41f5fc39e96b1f194d07761c6efd6cb92278b95f5012ab12cbc910058ca8b54"), - new TransactionHash("7757397a59378e9d0f6d5f08cc934c260e33a50ae0d73fdf869f7c02b6b47b33"), - new TransactionHash("b87238089e81527158a6daee520280324bc7e5322ba54d1b3c9a5678abe953ea"), - new TransactionHash("b45dd5e598bc85ba71639f2cbce8c5dff2fbe93159e637852fddeb16c0e84a48"), - new TransactionHash("83db780e98d4d3c917668c47b33ba51445591efacb0df2a922f88e7dfbb5fc7d"), - new TransactionHash("c2eb62b28cc7320da2292d87944c5424a70e1f443323c138c1affada7f6e9705"), - new TransactionHash("98e913c2a78cafdf4fa7f0113c1285fb29c2409bd7a746bb6f5506ad76841d54"), - new TransactionHash("5b05945be8ba2635e7c13d792ad727533494358308b5fcf36a816e52b5b272b8"), - new TransactionHash("47b089b5f0220299a017359003694a01fd75d075100166b8072c418d5143fe06"), - new TransactionHash("85021f20b06662240d8302d62f68031bbf7261bacb53b84e3dc9346c0f10a8e7") + new TransactionHash("a069c663831002651fd542479869cc61103465f3284dace772e7480f81429fa8"), + new TransactionHash("de3bc87f3e057e28ea6a625acd6d6d332e24f35ea73e820462b71256c8ecffb7"), + new TransactionHash("dbefa0299fe6b2336eb0bc3123fa623845c276e5c6e2a175adf1a562d5e77718"), + new TransactionHash("2a8ccbd91b7d9460a86174b5a8d4e6aa073b38674d1ee8107e728980a66f0676"), + new TransactionHash("be7914b1eb4c6bd352ba1d86991959b443e446e0ad49fb796be3495c287b2472") ]; for (const hash of hashes) { @@ -166,7 +156,7 @@ describe("test network providers on devnet: Proxy and API", function () { it("should have same response for getDefinitionOfFungibleToken()", async function () { this.timeout(10000); - let identifiers = ["MEX-b6bb7d", "WEGLD-88600a", "RIDE-482531", "USDC-a32906"]; + let identifiers = ["FOO-b6f543", "BAR-c80d29", "COUNTER-b7401d"]; for (const identifier of identifiers) { let apiResponse = await apiProvider.getDefinitionOfFungibleToken(identifier); @@ -182,7 +172,7 @@ describe("test network providers on devnet: Proxy and API", function () { it("should have same response for getDefinitionOfTokenCollection()", async function () { this.timeout(10000); - let collections = ["LKMEX-9acade", "LKFARM-c20c1c", "MEXFARM-bab93a", "ART-264971", "MOS-ff0040"]; + let collections = ["ERDJS-38f249"]; for (const collection of collections) { let apiResponse = await apiProvider.getDefinitionOfTokenCollection(collection); @@ -198,7 +188,7 @@ describe("test network providers on devnet: Proxy and API", function () { it("should have same response for getNonFungibleToken()", async function () { this.timeout(10000); - let tokens = [{ id: "ERDJSNFT-4a5669", nonce: new Nonce(1) }]; + let tokens = [{ id: "ERDJS-38f249", nonce: new Nonce(1) }]; for (const token of tokens) { let apiResponse = await apiProvider.getNonFungibleToken(token.id, token.nonce); @@ -246,7 +236,7 @@ describe("test network providers on devnet: Proxy and API", function () { query = new Query({ address: new Address("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u"), func: new ContractFunction("issue"), - value: Balance.egld(0.05), + value: new TransactionValue("42"), args: [ BytesValue.fromUTF8("FOO"), BytesValue.fromUTF8("FOO"), diff --git a/src/networkProvider/proxyNetworkProvider.ts b/src/networkProvider/proxyNetworkProvider.ts index 57f5b238b..00201c284 100644 --- a/src/networkProvider/proxyNetworkProvider.ts +++ b/src/networkProvider/proxyNetworkProvider.ts @@ -1,20 +1,18 @@ import axios, { AxiosRequestConfig } from "axios"; import { AccountOnNetwork } from "../account"; -import { Address } from "../address"; -import { defaultConfig } from "../constants"; -import { ErrNetworkProvider } from "../errors"; -import { IContractQueryResponse, IDefinitionOfFungibleTokenOnNetwork, IDefinitionOfTokenCollectionOnNetwork, IFungibleTokenOfAccountOnNetwork, INetworkProvider, INonFungibleTokenOfAccountOnNetwork, ITransactionOnNetwork, Pagination } from "./interface"; -import { Logger } from "../logger"; +import { IAddress, IContractQueryResponse, IDefinitionOfFungibleTokenOnNetwork, IDefinitionOfTokenCollectionOnNetwork, IFungibleTokenOfAccountOnNetwork, IHash, INetworkProvider, INonce, INonFungibleTokenOfAccountOnNetwork, ITransaction, Pagination } from "./interface"; import { NetworkConfig } from "../networkConfig"; import { NetworkStake } from "../networkStake"; import { NetworkStatus } from "../networkStatus"; -import { Nonce } from "../nonce"; import { Query } from "../smartcontracts"; import { Stats } from "../stats"; -import { Transaction, TransactionHash, TransactionStatus } from "../transaction"; import { ContractQueryResponse } from "./contractResults"; import { FungibleTokenOfAccountOnNetwork, NonFungibleTokenOfAccountOnNetwork } from "./tokens"; import { TransactionOnNetwork } from "./transactions"; +import { TransactionStatus } from "./transactionStatus"; +import { Hash } from "./primitives"; +import { ErrNetworkProvider } from "./errors"; +import { defaultAxiosConfig } from "./config"; // TODO: Find & remove duplicate code between "ProxyNetworkProvider" and "ApiNetworkProvider". export class ProxyNetworkProvider implements INetworkProvider { @@ -23,7 +21,7 @@ export class ProxyNetworkProvider implements INetworkProvider { constructor(url: string, config?: AxiosRequestConfig) { this.url = url; - this.config = { ...defaultConfig, ...config }; + this.config = { ...defaultAxiosConfig, ...config }; } async getNetworkConfig(): Promise { @@ -50,13 +48,13 @@ export class ProxyNetworkProvider implements INetworkProvider { throw new Error("Method not implemented."); } - async getAccount(address: Address): Promise { + async getAccount(address: IAddress): Promise { let response = await this.doGetGeneric(`address/${address.bech32()}`); let account = AccountOnNetwork.fromHttpResponse(response.account); return account; } - async getFungibleTokensOfAccount(address: Address, _pagination?: Pagination): Promise { + async getFungibleTokensOfAccount(address: IAddress, _pagination?: Pagination): Promise { let url = `address/${address.bech32()}/esdt`; let response = await this.doGetGeneric(url); let responseItems: any[] = Object.values(response.esdts); @@ -69,7 +67,7 @@ export class ProxyNetworkProvider implements INetworkProvider { return tokens; } - async getNonFungibleTokensOfAccount(address: Address, _pagination?: Pagination): Promise { + async getNonFungibleTokensOfAccount(address: IAddress, _pagination?: Pagination): Promise { let url = `address/${address.bech32()}/esdt`; let response = await this.doGetGeneric(url); let responseItems: any[] = Object.values(response.esdts); @@ -82,38 +80,38 @@ export class ProxyNetworkProvider implements INetworkProvider { return tokens; } - async getFungibleTokenOfAccount(address: Address, tokenIdentifier: string): Promise { + async getFungibleTokenOfAccount(address: IAddress, tokenIdentifier: string): Promise { let response = await this.doGetGeneric(`address/${address.bech32()}/esdt/${tokenIdentifier}`); let tokenData = FungibleTokenOfAccountOnNetwork.fromHttpResponse(response.tokenData); return tokenData; } - async getNonFungibleTokenOfAccount(address: Address, collection: string, nonce: Nonce): Promise { + async getNonFungibleTokenOfAccount(address: IAddress, collection: string, nonce: INonce): Promise { let response = await this.doGetGeneric(`address/${address.bech32()}/nft/${collection}/nonce/${nonce.valueOf()}`); let tokenData = NonFungibleTokenOfAccountOnNetwork.fromProxyHttpResponseByNonce(response.tokenData); return tokenData; } - async getTransaction(txHash: TransactionHash): Promise { + async getTransaction(txHash: IHash): Promise { let url = this.buildUrlWithQueryParameters(`transaction/${txHash.toString()}`, { withResults: "true" }); let response = await this.doGetGeneric(url); let transaction = TransactionOnNetwork.fromProxyHttpResponse(txHash, response.transaction); return transaction; } - async getTransactionStatus(txHash: TransactionHash): Promise { + async getTransactionStatus(txHash: IHash): Promise { let response = await this.doGetGeneric(`transaction/${txHash.toString()}/status`); let status = new TransactionStatus(response.status); return status; } - async sendTransaction(tx: Transaction): Promise { + async sendTransaction(tx: ITransaction): Promise { let response = await this.doPostGeneric("transaction/send", tx.toSendable()); - let hash = new TransactionHash(response.txHash); + let hash = new Hash(response.txHash); return hash; } - async simulateTransaction(tx: Transaction): Promise { + async simulateTransaction(tx: ITransaction): Promise { let response = await this.doPostGeneric("transaction/simulate", tx.toSendable()); return response; } @@ -138,7 +136,7 @@ export class ProxyNetworkProvider implements INetworkProvider { throw new Error("Method not implemented."); } - async getNonFungibleToken(_collection: string, _nonce: Nonce): Promise { + async getNonFungibleToken(_collection: string, _nonce: INonce): Promise { throw new Error("Method not implemented."); } @@ -193,7 +191,7 @@ export class ProxyNetworkProvider implements INetworkProvider { private handleApiError(error: any, resourceUrl: string) { if (!error.response) { - Logger.warn(error); + console.warn(error); throw new ErrNetworkProvider(resourceUrl, error.toString(), error); } diff --git a/src/networkProvider/tokenDefinitions.ts b/src/networkProvider/tokenDefinitions.ts index 77b784e66..c10644854 100644 --- a/src/networkProvider/tokenDefinitions.ts +++ b/src/networkProvider/tokenDefinitions.ts @@ -1,12 +1,12 @@ import { BigNumber } from "bignumber.js"; -import { Address } from "../address"; -import { IDefinitionOfFungibleTokenOnNetwork, IDefinitionOfTokenCollectionOnNetwork } from "./interface"; +import { Address } from "./primitives"; +import { IAddress, IDefinitionOfFungibleTokenOnNetwork, IDefinitionOfTokenCollectionOnNetwork } from "./interface"; export class DefinitionOfFungibleTokenOnNetwork implements IDefinitionOfFungibleTokenOnNetwork { identifier: string = ""; name: string = ""; ticker: string = ""; - owner: Address = new Address(); + owner: IAddress = new Address(""); decimals: number = 0; supply: BigNumber = new BigNumber(0); isPaused: boolean = false; @@ -47,7 +47,7 @@ export class DefinitionOfTokenCollectionOnNetwork implements IDefinitionOfTokenC type: string = ""; name: string = ""; ticker: string = ""; - owner: Address = new Address(); + owner: IAddress = new Address(""); decimals: number = 0; canPause: boolean = false; canFreeze: boolean = false; diff --git a/src/networkProvider/tokens.ts b/src/networkProvider/tokens.ts index 303fdb7ca..3223f178a 100644 --- a/src/networkProvider/tokens.ts +++ b/src/networkProvider/tokens.ts @@ -1,7 +1,6 @@ import { BigNumber } from "bignumber.js"; -import { Address } from "../address"; -import { IFungibleTokenOfAccountOnNetwork, INonFungibleTokenOfAccountOnNetwork } from "./interface"; -import { Nonce } from "../nonce"; +import { Address, Nonce } from "./primitives"; +import { IAddress, IFungibleTokenOfAccountOnNetwork, INonce, INonFungibleTokenOfAccountOnNetwork } from "./interface"; export class FungibleTokenOfAccountOnNetwork implements IFungibleTokenOfAccountOnNetwork { identifier: string = ""; @@ -22,8 +21,8 @@ export class NonFungibleTokenOfAccountOnNetwork implements INonFungibleTokenOfAc collection: string = ""; attributes: Buffer = Buffer.from([]); balance: BigNumber = new BigNumber(0); - nonce: Nonce = new Nonce(0); - creator: Address = new Address(""); + nonce: INonce = new Nonce(0); + creator: IAddress = new Address(""); royalties: BigNumber = new BigNumber(0); static fromProxyHttpResponse(payload: any): NonFungibleTokenOfAccountOnNetwork { diff --git a/src/transactionCompletionStrategy.ts b/src/networkProvider/transactionCompletionStrategy.ts similarity index 83% rename from src/transactionCompletionStrategy.ts rename to src/networkProvider/transactionCompletionStrategy.ts index 6573d5f00..60de758ed 100644 --- a/src/transactionCompletionStrategy.ts +++ b/src/networkProvider/transactionCompletionStrategy.ts @@ -1,8 +1,7 @@ -import { Nonce } from "./nonce"; -import { TransactionStatus } from "./transaction"; +import { TransactionStatus } from "./transactionStatus"; +import { ITransactionPayload } from "./interface"; import { TransactionLogs } from "./transactionLogs"; -import { TransactionPayload } from "./transactionPayload"; -import { isPaddedHex } from "./utils.codec"; +import { isPaddedHex } from "./primitives"; /** * Internal interface: a transaction, as seen from the perspective of a {@link TransactionCompletionStrategy}. @@ -10,8 +9,8 @@ import { isPaddedHex } from "./utils.codec"; interface ITransactionOnNetwork { logs: TransactionLogs; status: TransactionStatus; - hyperblockNonce: Nonce; - data: TransactionPayload; + hyperblockNonce: number; + data: ITransactionPayload; } const WellKnownCompletionEvents = ["completedTxEvent", "SCDeploy", "signalError"]; @@ -40,7 +39,7 @@ export class TransactionCompletionStrategy { return transaction.status.isExecuted(); } - let hyperblockNonce = transaction.hyperblockNonce.valueOf(); + let hyperblockNonce = transaction.hyperblockNonce; // Imprecise condition, uncertain completion (usually sufficient, though). // This is WRONG when (at least): timeOf(block with execution at destination is notarized) < timeOf(the "completedTxEvent" occurs). @@ -57,7 +56,7 @@ export class TransactionCompletionStrategy { let prefix = parts[0]; let otherParts = parts.slice(1); let emptyPrefix = !prefix; - let somePartsAreNotValidArguments = !otherParts.every(this.looksLikeValidArgument); + let somePartsAreNotValidArguments = !otherParts.every(part => this.looksLikeValidArgument(part)); return emptyPrefix || somePartsAreNotValidArguments; } diff --git a/src/networkProvider/transactionEvents.ts b/src/networkProvider/transactionEvents.ts new file mode 100644 index 000000000..3ff2a5c4e --- /dev/null +++ b/src/networkProvider/transactionEvents.ts @@ -0,0 +1,58 @@ +import { IAddress } from "./interface"; +import { Address } from "./primitives"; + +export class TransactionEvent { + readonly address: IAddress; + readonly identifier: string; + readonly topics: TransactionEventTopic[]; + readonly data: string; + + constructor(address: IAddress, identifier: string, topics: TransactionEventTopic[], data: string) { + this.address = address; + this.identifier = identifier; + this.topics = topics; + this.data = data; + } + + static fromHttpResponse(responsePart: { + address: string, + identifier: string, + topics: string[], + data: string + }): TransactionEvent { + let topics = (responsePart.topics || []).map(topic => new TransactionEventTopic(topic)); + let address = new Address(responsePart.address); + let identifier = responsePart.identifier || ""; + let data = Buffer.from(responsePart.data || "", "base64").toString(); + let event = new TransactionEvent(address, identifier, topics, data); + return event; + } + + findFirstOrNoneTopic(predicate: (topic: TransactionEventTopic) => boolean): TransactionEventTopic | undefined { + return this.topics.filter(topic => predicate(topic))[0]; + } + + getLastTopic(): TransactionEventTopic { + return this.topics[this.topics.length - 1]; + } +} + +export class TransactionEventTopic { + private readonly raw: Buffer; + + constructor(topic: string) { + this.raw = Buffer.from(topic || "", "base64"); + } + + toString(): string { + return this.raw.toString("utf8"); + } + + hex(): string { + return this.raw.toString("hex"); + } + + valueOf(): Buffer { + return this.raw; + } +} diff --git a/src/networkProvider/transactionLogs.ts b/src/networkProvider/transactionLogs.ts new file mode 100644 index 000000000..91586ef00 --- /dev/null +++ b/src/networkProvider/transactionLogs.ts @@ -0,0 +1,48 @@ +import { ErrUnexpectedCondition } from "./errors"; +import { IAddress } from "./interface"; +import { Address } from "./primitives"; +import { TransactionEvent } from "./transactionEvents"; + +export class TransactionLogs { + readonly address: IAddress; + readonly events: TransactionEvent[]; + + constructor(address: IAddress, events: TransactionEvent[]) { + this.address = address; + this.events = events; + } + + static empty(): TransactionLogs { + return new TransactionLogs(new Address(""), []); + } + + static fromHttpResponse(logs: any): TransactionLogs { + let address = new Address(logs.address); + let events = (logs.events || []).map((event: any) => TransactionEvent.fromHttpResponse(event)); + return new TransactionLogs(address, events); + } + + findSingleOrNoneEvent(identifier: string, predicate?: (event: TransactionEvent) => boolean): TransactionEvent | undefined { + let events = this.findEvents(identifier, predicate); + + if (events.length > 1) { + throw new ErrUnexpectedCondition(`more than one event of type ${identifier}`); + } + + return events[0]; + } + + findFirstOrNoneEvent(identifier: string, predicate?: (event: TransactionEvent) => boolean): TransactionEvent | undefined { + return this.findEvents(identifier, predicate)[0]; + } + + findEvents(identifier: string, predicate?: (event: TransactionEvent) => boolean): TransactionEvent[] { + let events = this.events.filter(event => event.identifier == identifier); + + if (predicate) { + events = events.filter(event => predicate(event)); + } + + return events; + } +} diff --git a/src/networkProvider/transactionReceipt.ts b/src/networkProvider/transactionReceipt.ts new file mode 100644 index 000000000..358661692 --- /dev/null +++ b/src/networkProvider/transactionReceipt.ts @@ -0,0 +1,25 @@ +import { IAddress, IHash } from "./interface"; +import { Address, Hash, TransactionValue } from "./primitives"; + +export class TransactionReceipt { + value: TransactionValue = new TransactionValue(""); + sender: IAddress = new Address(""); + data: string = ""; + hash: IHash = new Hash(""); + + static fromHttpResponse(response: { + value: string, + sender: string, + data: string, + txHash: string + }): TransactionReceipt { + let receipt = new TransactionReceipt(); + + receipt.value = new TransactionValue(response.value); + receipt.sender = new Address(response.sender); + receipt.data = response.data; + receipt.hash = new Hash(response.txHash); + + return receipt; + } +} diff --git a/src/networkProvider/transactionStatus.ts b/src/networkProvider/transactionStatus.ts new file mode 100644 index 000000000..bb509cbcf --- /dev/null +++ b/src/networkProvider/transactionStatus.ts @@ -0,0 +1,86 @@ +/** + * An abstraction for handling and interpreting the "status" field of a transaction. + */ +export class TransactionStatus { + /** + * The raw status, as fetched from the Network. + */ + readonly status: string; + + /** + * Creates a new TransactionStatus object. + */ + constructor(status: string) { + this.status = (status || "").toLowerCase(); + } + + /** + * Creates an unknown status. + */ + static createUnknown(): TransactionStatus { + return new TransactionStatus("unknown"); + } + + /** + * Returns whether the transaction is pending (e.g. in mempool). + */ + isPending(): boolean { + return ( + this.status == "received" || + this.status == "pending" + ); + } + + /** + * Returns whether the transaction has been executed (not necessarily with success). + */ + isExecuted(): boolean { + return this.isSuccessful() || this.isFailed() || this.isInvalid(); + } + + /** + * Returns whether the transaction has been executed successfully. + */ + isSuccessful(): boolean { + return ( + this.status == "executed" || + this.status == "success" || + this.status == "successful" + ); + } + + /** + * Returns whether the transaction has been executed, but with a failure. + */ + isFailed(): boolean { + return ( + this.status == "fail" || + this.status == "failed" || + this.status == "unsuccessful" || + this.isInvalid() + ); + } + + /** + * Returns whether the transaction has been executed, but marked as invalid (e.g. due to "insufficient funds"). + */ + isInvalid(): boolean { + return this.status == "invalid"; + } + + toString(): string { + return this.status; + } + + valueOf(): string { + return this.status; + } + + equals(other: TransactionStatus) { + if (!other) { + return false; + } + + return this.status == other.status; + } +} diff --git a/src/networkProvider/transactions.ts b/src/networkProvider/transactions.ts index 6fd3c906c..4cd351cb3 100644 --- a/src/networkProvider/transactions.ts +++ b/src/networkProvider/transactions.ts @@ -1,71 +1,80 @@ -import { Address } from "../address"; -import { Balance } from "../balance"; -import { Hash } from "../hash"; -import { IContractResults, ITransactionOnNetwork } from "./interface"; -import { GasLimit, GasPrice } from "../networkParams"; -import { Nonce } from "../nonce"; -import { Signature } from "../signature"; -import { TransactionHash, TransactionStatus } from "../transaction"; -import { TransactionLogs } from "../transactionLogs"; -import { TransactionPayload } from "../transactionPayload"; +import { TransactionStatus } from "./transactionStatus"; import { ContractResults } from "./contractResults"; +import { Address, Hash, Nonce, TransactionValue, TransactionPayload } from "./primitives"; +import { IAddress, IGasLimit, IGasPrice, IHash, INonce, ITransactionPayload } from "./interface"; +import { TransactionCompletionStrategy } from "./transactionCompletionStrategy"; +import { TransactionEvent } from "./transactionEvents"; +import { TransactionLogs } from "./transactionLogs"; +import { TransactionReceipt } from "./transactionReceipt"; - export class TransactionOnNetwork implements ITransactionOnNetwork { - hash: TransactionHash = new TransactionHash(""); - nonce: Nonce = new Nonce(0); + export class TransactionOnNetwork { + hash: IHash = new Hash(""); + type: string = ""; + nonce: INonce = new Nonce(0); round: number = 0; epoch: number = 0; - value: Balance = Balance.Zero(); - receiver: Address = new Address(); - sender: Address = new Address(); - gasPrice: GasPrice = new GasPrice(0); - gasLimit: GasLimit = new GasLimit(0); - data: TransactionPayload = new TransactionPayload(); - signature: Signature = Signature.empty(); + value: TransactionValue = new TransactionValue(""); + receiver: IAddress = new Address(""); + sender: IAddress = new Address(""); + gasLimit: IGasLimit = 0; + gasPrice: IGasPrice = 0; + data: ITransactionPayload = new TransactionPayload(""); + signature: string = ""; status: TransactionStatus = TransactionStatus.createUnknown(); timestamp: number = 0; - blockNonce: Nonce = new Nonce(0); - hyperblockNonce: Nonce = new Nonce(0); - hyperblockHash: Hash = Hash.empty(); + blockNonce: number = 0; + hyperblockNonce: number = 0; + hyperblockHash: string = ""; + pendingResults: boolean = false; + receipt: TransactionReceipt = new TransactionReceipt(); + contractResults: ContractResults = ContractResults.empty(); logs: TransactionLogs = TransactionLogs.empty(); - contractResults: IContractResults = ContractResults.empty(); - static fromProxyHttpResponse(txHash: TransactionHash, response: any): TransactionOnNetwork { + constructor(init?: Partial) { + Object.assign(this, init); + } + + static fromProxyHttpResponse(txHash: IHash, response: any): TransactionOnNetwork { let result = TransactionOnNetwork.fromHttpResponse(txHash, response); result.contractResults = ContractResults.fromProxyHttpResponse(response.smartContractResults || []); - // TODO: uniformize transaction status + // TODO: uniformize transaction status. + // TODO: Use specific completion detection strategy. return result; } - static fromApiHttpResponse(txHash: TransactionHash, response: any): TransactionOnNetwork { + static fromApiHttpResponse(txHash: IHash, response: any): TransactionOnNetwork { let result = TransactionOnNetwork.fromHttpResponse(txHash, response); result.contractResults = ContractResults.fromApiHttpResponse(response.results || []); - // TODO: uniformize transaction status + // TODO: uniformize transaction status. + // TODO: Use specific completion detection strategy. return result; } - private static fromHttpResponse(txHash: TransactionHash, response: any): TransactionOnNetwork { + private static fromHttpResponse(txHash: IHash, response: any): TransactionOnNetwork { let result = new TransactionOnNetwork(); result.hash = txHash; + result.type = response.type || ""; result.nonce = new Nonce(response.nonce || 0); result.round = response.round; result.epoch = response.epoch || 0; - result.value = Balance.fromString(response.value); - result.sender = Address.fromBech32(response.sender); - result.receiver = Address.fromBech32(response.receiver); - result.gasPrice = new GasPrice(response.gasPrice); - result.gasLimit = new GasLimit(response.gasLimit); - result.data = TransactionPayload.fromEncoded(response.data); + result.value = new TransactionValue((response.value || 0).toString()); + result.sender = new Address(response.sender); + result.receiver = new Address(response.receiver); + result.gasPrice = response.gasPrice || 0; + result.gasLimit = response.gasLimit || 0; + result.data = new TransactionPayload(response.data); result.status = new TransactionStatus(response.status); result.timestamp = response.timestamp || 0; - result.blockNonce = new Nonce(response.blockNonce || 0); - result.hyperblockNonce = new Nonce(response.hyperblockNonce || 0); - result.hyperblockHash = new Hash(response.hyperblockHash); + result.blockNonce = response.blockNonce || 0; + result.hyperblockNonce = response.hyperblockNonce || 0; + result.hyperblockHash = response.hyperblockHash || ""; + result.pendingResults = response.pendingResults || false; + result.receipt = TransactionReceipt.fromHttpResponse(response.receipt || {}); result.logs = TransactionLogs.fromHttpResponse(response.logs || {}); return result; @@ -74,5 +83,20 @@ import { ContractResults } from "./contractResults"; getDateTime(): Date { return new Date(this.timestamp * 1000); } + + isCompleted(): boolean { + // TODO: use different transaction completion strategies - API / Proxy. + return new TransactionCompletionStrategy().isCompleted(this); + } + + getAllEvents(): TransactionEvent[] { + let result = [...this.logs.events]; + + for (const resultItem of this.contractResults.items) { + result.push(...resultItem.logs.events); + } + + return result; + } } diff --git a/src/proxyProvider.ts b/src/proxyProvider.ts index 2650e25e1..e2df595c3 100644 --- a/src/proxyProvider.ts +++ b/src/proxyProvider.ts @@ -2,7 +2,7 @@ import axios, { AxiosRequestConfig } from "axios"; import BigNumber from "bignumber.js"; import { IProvider } from "./interface"; -import { Transaction, TransactionHash, TransactionStatus } from "./transaction"; +import { Transaction, TransactionHash } from "./transaction"; import { NetworkConfig } from "./networkConfig"; import { Address } from "./address"; import * as errors from "./errors"; @@ -11,8 +11,9 @@ import { Query } from "./smartcontracts/query"; import { QueryResponse } from "./smartcontracts/queryResponse"; import { Logger } from "./logger"; import { NetworkStatus } from "./networkStatus"; -import { TransactionOnNetwork } from "./transactionOnNetwork"; import { defaultConfig } from "./constants"; +import { ProxyNetworkProvider } from "./networkProvider/proxyNetworkProvider"; +import { ITransactionOnNetwork, ITransactionStatus } from "./interfaceOfNetwork"; /** * This will be deprecated once all the endpoints move to ApiProvider @@ -22,6 +23,10 @@ import { defaultConfig } from "./constants"; export class ProxyProvider implements IProvider { private url: string; private config: AxiosRequestConfig; + /** + * @deprecated used only for preparatory refactoring (unifying network providers) + */ + private readonly backingProvider: ProxyNetworkProvider; /** * Creates a new ProxyProvider. @@ -31,6 +36,7 @@ export class ProxyProvider implements IProvider { constructor(url: string, config?: AxiosRequestConfig) { this.url = url; this.config = {...defaultConfig, ...config}; + this.backingProvider = new ProxyNetworkProvider(url, config); } /** @@ -97,26 +103,16 @@ export class ProxyProvider implements IProvider { * Fetches the state of a {@link Transaction}. */ async getTransaction( - txHash: TransactionHash, - hintSender?: Address, - withResults?: boolean - ): Promise { - let url = this.buildUrlWithQueryParameters(`transaction/${txHash.toString()}`, { - withSender: hintSender ? hintSender.bech32() : "", - withResults: withResults ? "true" : "", - }); - - return this.doGetGeneric(url, (response) => TransactionOnNetwork.fromHttpResponse(txHash, response.transaction)); + txHash: TransactionHash + ): Promise { + return await this.backingProvider.getTransaction(txHash); } /** * Queries the status of a {@link Transaction}. */ - async getTransactionStatus(txHash: TransactionHash): Promise { - return this.doGetGeneric( - `transaction/${txHash.toString()}/status`, - (response) => new TransactionStatus(response.status) - ); + async getTransactionStatus(txHash: TransactionHash): Promise { + return await this.backingProvider.getTransactionStatus(txHash); } /** diff --git a/src/signableMessage.ts b/src/signableMessage.ts index 67967e2ea..43b484e41 100644 --- a/src/signableMessage.ts +++ b/src/signableMessage.ts @@ -1,4 +1,4 @@ -import { ISignable, ISignatureOfExternalSigner } from "./interface"; +import { ISignable, ISignature } from "./interface"; import { Signature } from "./signature"; import { Address } from "./address"; import { adaptToSignature } from "./boundaryAdapters"; @@ -58,7 +58,7 @@ export class SignableMessage implements ISignable { return this.signature; } - applySignature(signature: ISignatureOfExternalSigner): void { + applySignature(signature: ISignature): void { this.signature = adaptToSignature(signature); } diff --git a/src/smartcontracts/index.ts b/src/smartcontracts/index.ts index 3e08174ad..f167a04be 100644 --- a/src/smartcontracts/index.ts +++ b/src/smartcontracts/index.ts @@ -19,7 +19,6 @@ export * from "./queryResponse"; export * from "./resultsParser"; export * from "./returnCode"; export * from "./smartContract"; -export * from "./smartContractResults"; export * from "./transactionPayloadBuilders"; export * from "./typesystem"; export * from "./wrapper"; diff --git a/src/smartcontracts/interface.ts b/src/smartcontracts/interface.ts index c6d1c80a7..c8fae891f 100644 --- a/src/smartcontracts/interface.ts +++ b/src/smartcontracts/interface.ts @@ -1,8 +1,8 @@ import { Address } from "../address"; import { Balance } from "../balance"; +import { ITransactionOnNetwork } from "../interfaceOfNetwork"; import { ChainID, GasLimit, GasPrice } from "../networkParams"; import { Transaction } from "../transaction"; -import { TransactionOnNetwork } from "../transactionOnNetwork"; import { Code } from "./code"; import { CodeMetadata } from "./codeMetadata"; import { ContractFunction } from "./function"; @@ -89,8 +89,8 @@ export interface UntypedOutcomeBundle { } export interface ISmartContractController { - deploy(transaction: Transaction): Promise<{ transactionOnNetwork: TransactionOnNetwork, bundle: UntypedOutcomeBundle }>; - execute(interaction: Interaction, transaction: Transaction): Promise<{ transactionOnNetwork: TransactionOnNetwork, bundle: TypedOutcomeBundle }>; + deploy(transaction: Transaction): Promise<{ transactionOnNetwork: ITransactionOnNetwork, bundle: UntypedOutcomeBundle }>; + execute(interaction: Interaction, transaction: Transaction): Promise<{ transactionOnNetwork: ITransactionOnNetwork, bundle: TypedOutcomeBundle }>; query(interaction: Interaction): Promise; } @@ -102,6 +102,6 @@ export interface IResultsParser { parseQueryResponse(queryResponse: QueryResponse, endpoint: EndpointDefinition): TypedOutcomeBundle; parseUntypedQueryResponse(queryResponse: QueryResponse): UntypedOutcomeBundle; - parseOutcome(transaction: TransactionOnNetwork, endpoint: EndpointDefinition): TypedOutcomeBundle; - parseUntypedOutcome(transaction: TransactionOnNetwork): UntypedOutcomeBundle; + parseOutcome(transaction: ITransactionOnNetwork, endpoint: EndpointDefinition): TypedOutcomeBundle; + parseUntypedOutcome(transaction: ITransactionOnNetwork): UntypedOutcomeBundle; } diff --git a/src/smartcontracts/query.spec.ts b/src/smartcontracts/query.spec.ts index 2500ecf3e..baedc6126 100644 --- a/src/smartcontracts/query.spec.ts +++ b/src/smartcontracts/query.spec.ts @@ -42,12 +42,4 @@ describe("test smart contract queries", () => { assert.equal(request["args"][2], "abba"); assert.equal(request["args"][3], "314dc6448d9338c15b0a00000000"); }); - - it("should throw if missing required", async () => { - assert.throw(() => new Query(), errors.ErrAddressEmpty); - assert.throw(() => new Query({ - address: new Address("erd1qqqqqqqqqqqqqpgq3ytm9m8dpeud35v3us20vsafp77smqghd8ss4jtm0q"), - func: undefined - }), errors.ErrInvariantFailed); - }); }); diff --git a/src/smartcontracts/query.ts b/src/smartcontracts/query.ts index b6b3be6ec..bb3d95fdf 100644 --- a/src/smartcontracts/query.ts +++ b/src/smartcontracts/query.ts @@ -5,15 +5,16 @@ import { guardValueIsSet } from "../utils"; import { TypedValue } from "./typesystem"; import { ArgSerializer } from "./argSerializer"; import BigNumber from "bignumber.js"; +import { IBech32Address, ITransactionValue } from "../interface"; export const MaxUint64 = new BigNumber("18446744073709551615"); export class Query { caller: Address; - address: Address; + address: IBech32Address; func: ContractFunction; args: TypedValue[]; - value: Balance; + value: ITransactionValue; constructor(init?: Partial) { this.caller = new Address(); @@ -27,7 +28,6 @@ export class Query { guardValueIsSet("address", this.address); guardValueIsSet("func", this.func); - this.address.assertNotEmpty(); this.args = this.args || []; this.caller = this.caller || new Address(); this.value = this.value || Balance.Zero(); diff --git a/src/smartcontracts/resultsParser.spec.ts b/src/smartcontracts/resultsParser.spec.ts index e87e1fa87..2274ab92f 100644 --- a/src/smartcontracts/resultsParser.spec.ts +++ b/src/smartcontracts/resultsParser.spec.ts @@ -6,13 +6,14 @@ import { BytesType, BytesValue } from "./typesystem/bytes"; import { QueryResponse } from "./queryResponse"; import { ReturnCode } from "./returnCode"; import { ResultsParser } from "./resultsParser"; -import { TransactionOnNetwork } from "../transactionOnNetwork"; -import { SmartContractResultItem, SmartContractResults } from "./smartContractResults"; import { Nonce } from "../nonce"; import { TransactionHash } from "../transaction"; -import { TransactionEvent, TransactionEventTopic, TransactionLogs } from "../transactionLogs"; import { Address } from "../address"; import { Logger, LogLevel } from "../logger"; +import { TransactionOnNetwork } from "../networkProvider/transactions"; +import { ContractResultItem, ContractResults } from "../networkProvider/contractResults"; +import { TransactionLogs } from "../networkProvider/transactionLogs"; +import { TransactionEvent, TransactionEventTopic } from "../networkProvider/transactionEvents"; const KnownReturnCodes: string[] = [ ReturnCode.None.valueOf(), @@ -70,8 +71,8 @@ describe("test smart contract results parser", () => { let endpoint = new EndpointDefinition("foo", [], outputParameters, endpointModifiers); let transactionOnNetwork = new TransactionOnNetwork({ - results: new SmartContractResults([ - new SmartContractResultItem({ nonce: new Nonce(7), data: "@6f6b@2a@abba" }) + contractResults: new ContractResults([ + new ContractResultItem({ nonce: new Nonce(7), data: "@6f6b@2a@abba" }) ]) }); @@ -85,8 +86,8 @@ describe("test smart contract results parser", () => { it("should parse contract outcome, on easily found result with return data", async () => { let transaction = new TransactionOnNetwork({ - results: new SmartContractResults([ - new SmartContractResultItem({ + contractResults: new ContractResults([ + new ContractResultItem({ nonce: new Nonce(42), data: "@6f6b@03", returnMessage: "foobar" @@ -181,7 +182,7 @@ describe("test smart contract results parser", () => { let jsonContent: string = fs.readFileSync(filePath, { encoding: "utf8" }); let json = JSON.parse(jsonContent); let payload = json["data"]["transaction"]; - let transaction = TransactionOnNetwork.fromHttpResponse(txHash, payload); + let transaction = TransactionOnNetwork.fromProxyHttpResponse(txHash, payload); samples.push([transaction, jsonContent]); } diff --git a/src/smartcontracts/resultsParser.ts b/src/smartcontracts/resultsParser.ts index 96e3d7a65..03caec9a1 100644 --- a/src/smartcontracts/resultsParser.ts +++ b/src/smartcontracts/resultsParser.ts @@ -2,14 +2,14 @@ import { TransactionDecoder, TransactionMetadata } from "@elrondnetwork/transact import { Address } from "../address"; import { ErrCannotParseContractResults } from "../errors"; import { Logger } from "../logger"; -import { TransactionLogs } from "../transactionLogs"; -import { TransactionOnNetwork } from "../transactionOnNetwork"; +import { IContractResults, ITransactionLogs, ITransactionOnNetwork } from "../interfaceOfNetwork"; import { ArgSerializer } from "./argSerializer"; import { TypedOutcomeBundle, IResultsParser, UntypedOutcomeBundle } from "./interface"; import { QueryResponse } from "./queryResponse"; import { ReturnCode } from "./returnCode"; -import { SmartContractResults } from "./smartContractResults"; import { EndpointDefinition } from "./typesystem"; +import { adaptToAddress } from "../boundaryAdapters"; +import { IBech32Address } from "../interface"; enum WellKnownEvents { OnTransactionCompleted = "completedTxEvent", @@ -48,7 +48,7 @@ export class ResultsParser implements IResultsParser { }; } - parseOutcome(transaction: TransactionOnNetwork, endpoint: EndpointDefinition): TypedOutcomeBundle { + parseOutcome(transaction: ITransactionOnNetwork, endpoint: EndpointDefinition): TypedOutcomeBundle { let untypedBundle = this.parseUntypedOutcome(transaction); let values = new ArgSerializer().buffersToValues(untypedBundle.values, endpoint.output); @@ -62,7 +62,7 @@ export class ResultsParser implements IResultsParser { }; } - parseUntypedOutcome(transaction: TransactionOnNetwork): UntypedOutcomeBundle { + parseUntypedOutcome(transaction: ITransactionOnNetwork): UntypedOutcomeBundle { let bundle: UntypedOutcomeBundle | null; let transactionMetadata = this.parseTransactionMetadata(transaction); @@ -79,7 +79,7 @@ export class ResultsParser implements IResultsParser { return bundle; } - bundle = this.createBundleOnEasilyFoundResultWithReturnData(transaction.results); + bundle = this.createBundleOnEasilyFoundResultWithReturnData(transaction.contractResults); if (bundle) { Logger.trace("parseUntypedOutcome(): on easily found result with return data"); return bundle; @@ -118,18 +118,18 @@ export class ResultsParser implements IResultsParser { throw new ErrCannotParseContractResults(`transaction ${transaction.hash.toString()}`); } - private parseTransactionMetadata(transaction: TransactionOnNetwork): TransactionMetadata { + private parseTransactionMetadata(transaction: ITransactionOnNetwork): TransactionMetadata { return new TransactionDecoder().getTransactionMetadata({ sender: transaction.sender.bech32(), receiver: transaction.receiver.bech32(), data: transaction.data.encoded(), value: transaction.value.toString(), - type: transaction.type.value - }) + type: transaction.type + }); } - private createBundleOnSimpleMoveBalance(transaction: TransactionOnNetwork): UntypedOutcomeBundle | null { - let noResults = transaction.results.getAll().length == 0; + private createBundleOnSimpleMoveBalance(transaction: ITransactionOnNetwork): UntypedOutcomeBundle | null { + let noResults = transaction.contractResults.items.length == 0; let noLogs = transaction.logs.events.length == 0; if (noResults && noLogs) { @@ -143,7 +143,7 @@ export class ResultsParser implements IResultsParser { return null; } - private createBundleOnInvalidTransaction(transaction: TransactionOnNetwork): UntypedOutcomeBundle | null { + private createBundleOnInvalidTransaction(transaction: ITransactionOnNetwork): UntypedOutcomeBundle | null { if (transaction.status.isInvalid()) { if (transaction.receipt.data) { return { @@ -159,8 +159,8 @@ export class ResultsParser implements IResultsParser { return null; } - private createBundleOnEasilyFoundResultWithReturnData(results: SmartContractResults): UntypedOutcomeBundle | null { - let resultItemWithReturnData = results.getAll().find(item => item.nonce.valueOf() != 0 && item.data.startsWith("@")); + private createBundleOnEasilyFoundResultWithReturnData(results: IContractResults): UntypedOutcomeBundle | null { + let resultItemWithReturnData = results.items.find(item => item.nonce.valueOf() != 0 && item.data.startsWith("@")); if (!resultItemWithReturnData) { return null; } @@ -175,7 +175,7 @@ export class ResultsParser implements IResultsParser { }; } - private createBundleOnSignalError(logs: TransactionLogs): UntypedOutcomeBundle | null { + private createBundleOnSignalError(logs: ITransactionLogs): UntypedOutcomeBundle | null { let eventSignalError = logs.findSingleOrNoneEvent(WellKnownEvents.OnSignalError); if (!eventSignalError) { return null; @@ -192,7 +192,7 @@ export class ResultsParser implements IResultsParser { }; } - private createBundleOnTooMuchGasWarning(logs: TransactionLogs): UntypedOutcomeBundle | null { + private createBundleOnTooMuchGasWarning(logs: ITransactionLogs): UntypedOutcomeBundle | null { let eventTooMuchGas = logs.findSingleOrNoneEvent( WellKnownEvents.OnWriteLog, event => event.findFirstOrNoneTopic(topic => topic.toString().startsWith(WellKnownTopics.TooMuchGas)) != undefined @@ -213,10 +213,12 @@ export class ResultsParser implements IResultsParser { }; } - private createBundleOnWriteLogWhereFirstTopicEqualsAddress(logs: TransactionLogs, address: Address): UntypedOutcomeBundle | null { + private createBundleOnWriteLogWhereFirstTopicEqualsAddress(logs: ITransactionLogs, address: IBech32Address): UntypedOutcomeBundle | null { + let hexAddress = adaptToAddress(address).hex(); + let eventWriteLogWhereTopicIsSender = logs.findSingleOrNoneEvent( WellKnownEvents.OnWriteLog, - event => event.findFirstOrNoneTopic(topic => topic.hex() == address.hex()) != undefined + event => event.findFirstOrNoneTopic(topic => topic.hex() == hexAddress) != undefined ); if (!eventWriteLogWhereTopicIsSender) { @@ -236,17 +238,17 @@ export class ResultsParser implements IResultsParser { /** * Override this method (in a subclass of {@link ResultsParser}) if the basic heuristics of the parser are not sufficient. */ - protected createBundleWithCustomHeuristics(_transaction: TransactionOnNetwork, _transactionMetadata: TransactionMetadata): UntypedOutcomeBundle | null { + protected createBundleWithCustomHeuristics(_transaction: ITransactionOnNetwork, _transactionMetadata: TransactionMetadata): UntypedOutcomeBundle | null { return null; } - private createBundleWithFallbackHeuristics(transaction: TransactionOnNetwork, transactionMetadata: TransactionMetadata): UntypedOutcomeBundle | null { + private createBundleWithFallbackHeuristics(transaction: ITransactionOnNetwork, transactionMetadata: TransactionMetadata): UntypedOutcomeBundle | null { let contractAddress = new Address(transactionMetadata.receiver); // Search the nested logs for matching events (writeLog): - for (const resultItem of transaction.results.getAll()) { + for (const resultItem of transaction.contractResults.items) { let writeLogWithReturnData = resultItem.logs.findSingleOrNoneEvent(WellKnownEvents.OnWriteLog, event => { - let addressIsSender = event.address.equals(transaction.sender); + let addressIsSender = event.address.bech32() == transaction.sender.bech32(); let firstTopicIsContract = event.topics[0]?.hex() == contractAddress.hex(); return addressIsSender && firstTopicIsContract; }); diff --git a/src/smartcontracts/smartContract.spec.ts b/src/smartcontracts/smartContract.spec.ts index 62fbc8455..e657aede8 100644 --- a/src/smartcontracts/smartContract.spec.ts +++ b/src/smartcontracts/smartContract.spec.ts @@ -5,11 +5,11 @@ import { Nonce } from "../nonce"; import { SmartContract } from "./smartContract"; import { ChainID, GasLimit } from "../networkParams"; import { InHyperblock, loadTestWallets, MockProvider, setupUnitTestWatcherTimeouts, TestWallet, Wait } from "../testutils"; -import { TransactionStatus } from "../transaction"; import { ContractFunction } from "./function"; import { U32Value } from "./typesystem"; import { BytesValue } from "./typesystem/bytes"; import { TransactionWatcher } from "../transactionWatcher"; +import { TransactionStatus } from "../networkProvider/transactionStatus"; describe("test contract", () => { diff --git a/src/smartcontracts/smartContractController.ts b/src/smartcontracts/smartContractController.ts index 4e026679e..d24162d3d 100644 --- a/src/smartcontracts/smartContractController.ts +++ b/src/smartcontracts/smartContractController.ts @@ -1,7 +1,6 @@ import { IProvider } from "../interface"; import { Interaction } from "./interaction"; import { Transaction } from "../transaction"; -import { TransactionOnNetwork } from "../transactionOnNetwork"; import { TypedOutcomeBundle, IInteractionChecker, IResultsParser, ISmartContractController, UntypedOutcomeBundle } from "./interface"; import { ContractFunction } from "./function"; import { ResultsParser } from "./resultsParser"; @@ -9,6 +8,7 @@ import { InteractionChecker, NullInteractionChecker } from "./interactionChecker import { EndpointDefinition } from "./typesystem"; import { Logger } from "../logger"; import { TransactionWatcher } from "../transactionWatcher"; +import { ITransactionOnNetwork } from "../interfaceOfNetwork"; /** * Internal interface: the smart contract ABI, as seen from the perspective of a {@link SmartContractController}. @@ -49,7 +49,7 @@ export class SmartContractController implements ISmartContractController { this.transactionCompletionAwaiter = transactionWatcher; } - async deploy(transaction: Transaction): Promise<{ transactionOnNetwork: TransactionOnNetwork, bundle: UntypedOutcomeBundle }> { + async deploy(transaction: Transaction): Promise<{ transactionOnNetwork: ITransactionOnNetwork, bundle: UntypedOutcomeBundle }> { Logger.info(`SmartContractController.deploy [begin]: transaction = ${transaction.getHash()}`); await this.provider.sendTransaction(transaction); @@ -67,7 +67,7 @@ export class SmartContractController implements ISmartContractController { * @param interaction The interaction used to build the {@link transaction} * @param transaction The interaction transaction, which must be signed beforehand */ - async execute(interaction: Interaction, transaction: Transaction): Promise<{ transactionOnNetwork: TransactionOnNetwork, bundle: TypedOutcomeBundle }> { + async execute(interaction: Interaction, transaction: Transaction): Promise<{ transactionOnNetwork: ITransactionOnNetwork, bundle: TypedOutcomeBundle }> { Logger.info(`SmartContractController.execute [begin]: function = ${interaction.getFunction()}, transaction = ${transaction.getHash()}`); let endpoint = this.getEndpoint(interaction); diff --git a/src/smartcontracts/smartContractResults.ts b/src/smartcontracts/smartContractResults.ts deleted file mode 100644 index e6896accb..000000000 --- a/src/smartcontracts/smartContractResults.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { Address } from "../address"; -import { Balance } from "../balance"; -import { Hash } from "../hash"; -import { GasLimit, GasPrice } from "../networkParams"; -import { Nonce } from "../nonce"; -import { TransactionHash } from "../transaction"; -import { TransactionLogs } from "../transactionLogs"; - -export class SmartContractResults { - private readonly items: SmartContractResultItem[] = []; - - constructor(items: SmartContractResultItem[]) { - this.items = items; - } - - static empty(): SmartContractResults { - return new SmartContractResults([]); - } - - static fromHttpResponse(smartContractResults: any[]): SmartContractResults { - let items = (smartContractResults || []).map((item: any) => SmartContractResultItem.fromHttpResponse(item)); - return new SmartContractResults(items); - } - - getAll(): SmartContractResultItem[] { - return this.items; - } -} - -export class SmartContractResultItem { - constructor(init?: Partial) { - Object.assign(this, init); - } - - hash: Hash = Hash.empty(); - nonce: Nonce = new Nonce(0); - value: Balance = Balance.Zero(); - receiver: Address = new Address(); - sender: Address = new Address(); - data: string = ""; - returnMessage: string = ""; - previousHash: Hash = Hash.empty(); - originalHash: Hash = Hash.empty(); - gasLimit: GasLimit = new GasLimit(0); - gasPrice: GasPrice = new GasPrice(0); - callType: number = 0; - logs: TransactionLogs = TransactionLogs.empty(); - - static fromHttpResponse(response: { - hash: string, - nonce: number, - value: string, - receiver: string, - sender: string, - data: string, - prevTxHash: string, - originalTxHash: string, - gasLimit: number, - gasPrice: number, - callType: number, - returnMessage: string, - logs: any[] - }): SmartContractResultItem { - let item = new SmartContractResultItem(); - - item.hash = new TransactionHash(response.hash); - item.nonce = new Nonce(response.nonce || 0); - item.value = Balance.fromString(response.value); - item.receiver = new Address(response.receiver); - item.sender = new Address(response.sender); - item.data = response.data || ""; - item.returnMessage = response.returnMessage || ""; - item.previousHash = new TransactionHash(response.prevTxHash); - item.originalHash = new TransactionHash(response.originalTxHash); - item.gasLimit = new GasLimit(response.gasLimit); - item.gasPrice = new GasPrice(response.gasPrice); - item.callType = response.callType; - - item.logs = TransactionLogs.fromHttpResponse(response.logs || {}); - - return item; - } -} diff --git a/src/smartcontracts/wrapper/contractLogger.ts b/src/smartcontracts/wrapper/contractLogger.ts index ea7a6302d..be789374c 100644 --- a/src/smartcontracts/wrapper/contractLogger.ts +++ b/src/smartcontracts/wrapper/contractLogger.ts @@ -1,9 +1,9 @@ import { Address } from "../../address"; import { NetworkConfig } from "../../networkConfig"; +import { IContractResults } from "../../interfaceOfNetwork"; import { Transaction } from "../../transaction"; import { Query } from "../query"; import { QueryResponse } from "../queryResponse"; -import { SmartContractResults } from "../smartContractResults"; import { findImmediateResult, findResultingCalls, TypedResult } from "./deprecatedContractResults"; /** @@ -19,7 +19,7 @@ export class ContractLogger { console.log(`Tx ${transaction.getHash()} created. Sending...`); } - deployComplete(transaction: Transaction, smartContractResults: SmartContractResults, smartContractAddress: Address) { + deployComplete(transaction: Transaction, smartContractResults: IContractResults, smartContractAddress: Address) { logReturnMessages(transaction, smartContractResults); console.log(`done. (address: ${smartContractAddress.bech32()} )`); } @@ -28,7 +28,7 @@ export class ContractLogger { console.log(`awaiting results...`); } - transactionComplete(_result: any, _resultData: string, transaction: Transaction, smartContractResults: SmartContractResults) { + transactionComplete(_result: any, _resultData: string, transaction: Transaction, smartContractResults: IContractResults) { logReturnMessages(transaction, smartContractResults); console.log(`done.`); } @@ -42,7 +42,7 @@ export class ContractLogger { } } -function logReturnMessages(transaction: Transaction, smartContractResults: SmartContractResults) { +function logReturnMessages(transaction: Transaction, smartContractResults: IContractResults) { let immediate = findImmediateResult(smartContractResults)!; logSmartContractResultIfMessage("(immediate)", transaction, immediate); diff --git a/src/smartcontracts/wrapper/contractWrapper.ts b/src/smartcontracts/wrapper/contractWrapper.ts index c02869d72..9ef3f6f00 100644 --- a/src/smartcontracts/wrapper/contractWrapper.ts +++ b/src/smartcontracts/wrapper/contractWrapper.ts @@ -5,7 +5,6 @@ import { generateMethods, Methods } from "./generateMethods"; import { formatEndpoint, FormattedCall } from "./formattedCall"; import { ArgumentErrorContext } from "../argumentErrorContext"; import { PreparedCall } from "./preparedCall"; -import { TransactionOnNetwork } from "../../transactionOnNetwork"; import { ContractLogger } from "./contractLogger"; import { SendContext } from "./sendContext"; import { loadContractCode } from "../../testutils"; @@ -24,6 +23,7 @@ import { Balance } from "../../balance"; import { ExecutionResultsBundle, findImmediateResult, interpretExecutionResults } from "./deprecatedContractResults"; import { Result } from "./result"; import { TransactionWatcher } from "../../transactionWatcher"; +import { ITransactionOnNetwork } from "../../interfaceOfNetwork"; /** * Provides a simple interface in order to easily call or query the smart contract's methods. @@ -112,7 +112,7 @@ export class ContractWrapper extends ChainSendContext { let transactionOnNetwork = await this.processTransaction(transaction); - let smartContractResults = transactionOnNetwork.results; + let smartContractResults = transactionOnNetwork.contractResults; let immediateResult = findImmediateResult(smartContractResults)!; immediateResult.assertSuccess(); let logger = this.context.getLogger(); @@ -172,7 +172,7 @@ export class ContractWrapper extends ChainSendContext { return { executionResultsBundle, result }; } - async processTransaction(transaction: Transaction): Promise { + async processTransaction(transaction: Transaction): Promise { let provider = this.context.getProvider(); let sender = this.context.getSender(); transaction.setNonce(sender.account.nonce); diff --git a/src/smartcontracts/wrapper/deprecatedContractResults.ts b/src/smartcontracts/wrapper/deprecatedContractResults.ts index 04f9b8df3..5c480ffcd 100644 --- a/src/smartcontracts/wrapper/deprecatedContractResults.ts +++ b/src/smartcontracts/wrapper/deprecatedContractResults.ts @@ -7,11 +7,11 @@ * @module */ -import { TransactionOnNetwork } from "../../transactionOnNetwork"; +import { ContractResultItem } from "../../networkProvider/contractResults"; +import { IContractResultItem, IContractResults, ITransactionOnNetwork } from "../../interfaceOfNetwork"; import { ArgSerializer } from "../argSerializer"; import { QueryResponse } from "../queryResponse"; import { ReturnCode } from "../returnCode"; -import { SmartContractResultItem, SmartContractResults } from "../smartContractResults"; import { EndpointDefinition, TypedValue } from "../typesystem"; import { Result } from "./result"; @@ -19,8 +19,8 @@ import { Result } from "./result"; * @deprecated The concept of immediate results / resulting calls does not exist in the Protocol / in the API. * The SCRs are more alike a graph. */ -export function interpretExecutionResults(endpoint: EndpointDefinition, transactionOnNetwork: TransactionOnNetwork): ExecutionResultsBundle { - let smartContractResults = transactionOnNetwork.results; +export function interpretExecutionResults(endpoint: EndpointDefinition, transactionOnNetwork: ITransactionOnNetwork): ExecutionResultsBundle { + let smartContractResults = transactionOnNetwork.contractResults; let immediateResult = findImmediateResult(smartContractResults)!; let resultingCalls = findResultingCalls(smartContractResults); @@ -43,7 +43,7 @@ export function interpretExecutionResults(endpoint: EndpointDefinition, transact * The SCRs are more alike a graph. */ export interface ExecutionResultsBundle { - smartContractResults: SmartContractResults; + smartContractResults: IContractResults; immediateResult: TypedResult; /** * @deprecated Most probably, we should use logs & events instead @@ -65,8 +65,8 @@ export interface QueryResponseBundle { * @deprecated The concept of immediate results / resulting calls does not exist in the Protocol / in the API. * The SCRs are more like a graph. */ -export function findImmediateResult(results: SmartContractResults): TypedResult | undefined { - let immediateItem = results.getAll().filter(item => isImmediateResult(item))[0]; +export function findImmediateResult(results: IContractResults): TypedResult | undefined { + let immediateItem = results.items.filter(item => isImmediateResult(item))[0]; if (immediateItem) { return new TypedResult(immediateItem); } @@ -77,8 +77,8 @@ export function findImmediateResult(results: SmartContractResults): TypedResult * @deprecated The concept of immediate results / resulting calls does not exist in the Protocol / in the API. * The SCRs are more like a graph. */ -export function findResultingCalls(results: SmartContractResults): TypedResult[] { - let otherItems = results.getAll().filter(item => !isImmediateResult(item)); +export function findResultingCalls(results: IContractResults): TypedResult[] { + let otherItems = results.items.filter(item => !isImmediateResult(item)); let resultingCalls = otherItems.map(item => new TypedResult(item)); return resultingCalls; } @@ -87,7 +87,7 @@ export function findResultingCalls(results: SmartContractResults): TypedResult[] * @deprecated The concept of immediate results / resulting calls does not exist in the Protocol / in the API. * The SCRs are more like a graph. */ -function isImmediateResult(item: SmartContractResultItem): boolean { +function isImmediateResult(item: IContractResultItem): boolean { return item.nonce.valueOf() != 0; } @@ -95,13 +95,13 @@ function isImmediateResult(item: SmartContractResultItem): boolean { * @deprecated getReturnCode(), outputUntyped are a bit fragile. * They are not necessarily applicable to SCRs, in general (only in particular). */ -export class TypedResult extends SmartContractResultItem implements Result.IResult { +export class TypedResult extends ContractResultItem implements Result.IResult { /** * If available, will provide typed output arguments (with typed values). */ endpointDefinition?: EndpointDefinition; - constructor(init?: Partial) { + constructor(init?: Partial) { super(); Object.assign(this, init); } diff --git a/src/testutils/mockProvider.ts b/src/testutils/mockProvider.ts index af28887a7..d9bf8d357 100644 --- a/src/testutils/mockProvider.ts +++ b/src/testutils/mockProvider.ts @@ -1,6 +1,5 @@ -import { IProvider } from "../interface"; -import { Transaction, TransactionHash, TransactionStatus } from "../transaction"; -import { TransactionOnNetwork } from "../transactionOnNetwork"; +import { IBech32Address, IHash, IProvider } from "../interface"; +import { Transaction, TransactionHash } from "../transaction"; import { NetworkConfig } from "../networkConfig"; import { Address } from "../address"; import { Nonce } from "../nonce"; @@ -10,15 +9,17 @@ import { Balance } from "../balance"; import * as errors from "../errors"; import { Query } from "../smartcontracts/query"; 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"; -import { SmartContractResultItem, SmartContractResults } from "../smartcontracts"; +import { ContractResultItem, ContractResults } from "../networkProvider/contractResults"; +import { TransactionOnNetwork } from "../networkProvider/transactions"; +import { ITransactionOnNetwork, ITransactionStatus } from "../interfaceOfNetwork"; +import { TransactionStatus } from "../networkProvider/transactionStatus"; -const DummyHyperblockNonce = new Nonce(42); -const DummyHyperblockHash = new Hash("a".repeat(32)); +const DummyHyperblockNonce = 42; +const DummyHyperblockHash = "a".repeat(32); /** * A mock {@link IProvider}, used for tests only. @@ -89,14 +90,14 @@ export class MockProvider implements IProvider { } mockGetTransactionWithAnyHashAsNotarizedWithOneResult(returnCodeAndData: string) { - let contractResult = new SmartContractResultItem({ nonce: new Nonce(1), data: returnCodeAndData }); + let contractResult = new ContractResultItem({ nonce: new Nonce(1), data: returnCodeAndData }); - let predicate = (_hash: TransactionHash) => true; + let predicate = (_hash: IHash) => true; let response = new TransactionOnNetwork({ status: new TransactionStatus("executed"), hyperblockNonce: DummyHyperblockNonce, hyperblockHash: DummyHyperblockHash, - results: new SmartContractResults([contractResult]) + contractResults: new ContractResults([contractResult]) }); this.getTransactionResponders.unshift(new GetTransactionResponder(predicate, response)); @@ -139,7 +140,7 @@ export class MockProvider implements IProvider { } } - async getAccount(address: Address): Promise { + async getAccount(address: IBech32Address): Promise { let account = this.accounts.get(address.bech32()); if (account) { return account; @@ -148,15 +149,15 @@ export class MockProvider implements IProvider { return new AccountOnNetwork(); } - async getAddressEsdt(_address: Address, _tokenIdentifier: string): Promise { + async getAddressEsdt(_address: IBech32Address, _tokenIdentifier: string): Promise { return {}; } - async getAddressEsdtList(_address: Address): Promise { + async getAddressEsdtList(_address: IBech32Address): Promise { return {}; } - async getAddressNft(_address: Address, _tokenIdentifier: string, _nonce: BigNumber): Promise { + async getAddressNft(_address: IBech32Address, _tokenIdentifier: string, _nonce: BigNumber): Promise { return {}; } @@ -182,10 +183,10 @@ export class MockProvider implements IProvider { } async getTransaction( - txHash: TransactionHash, - _hintSender?: Address, + txHash: IHash, + _hintSender?: IBech32Address, _withResults?: boolean - ): Promise { + ): Promise { // At first, try to use a mock responder for (const responder of this.getTransactionResponders) { if (responder.matches(txHash)) { @@ -202,7 +203,7 @@ export class MockProvider implements IProvider { throw new errors.ErrMock("Transaction not found"); } - async getTransactionStatus(txHash: TransactionHash): Promise { + async getTransactionStatus(txHash: TransactionHash): Promise { let transaction = await this.getTransaction(txHash); return transaction.status; } @@ -247,10 +248,10 @@ class QueryContractResponder { } class GetTransactionResponder { - readonly matches: (hash: TransactionHash) => boolean; + readonly matches: (hash: IHash) => boolean; readonly response: TransactionOnNetwork; - constructor(matches: (hash: TransactionHash) => boolean, response: TransactionOnNetwork) { + constructor(matches: (hash: IHash) => boolean, response: TransactionOnNetwork) { this.matches = matches; this.response = response; } diff --git a/src/transaction.dev.net.spec.ts b/src/transaction.dev.net.spec.ts deleted file mode 100644 index fe34c2b69..000000000 --- a/src/transaction.dev.net.spec.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { assert } from "chai"; -import { chooseApiProvider, chooseProxyProvider } from "./interactive"; -import { Hash } from "./hash"; -import { TransactionOnNetwork, TransactionOnNetworkType } from "./transactionOnNetwork"; -import { TransactionHash, TransactionStatus } from "./transaction"; -import { Address } from "./address"; -import { Nonce } from "./nonce"; -import { SmartContractResultItem } from "./smartcontracts"; - -describe("test transactions on devnet", function () { - it("should get transaction from Proxy & from API", async function () { - this.timeout(20000); - - let sender = new Address("erd1testnlersh4z0wsv8kjx39me4rmnvjkwu8dsaea7ukdvvc9z396qykv7z7"); - let proxyProvider = chooseProxyProvider("elrond-devnet"); - let apiProvider = chooseApiProvider("elrond-devnet"); - - let hashes = [ - new TransactionHash("b41f5fc39e96b1f194d07761c6efd6cb92278b95f5012ab12cbc910058ca8b54"), - new TransactionHash("7757397a59378e9d0f6d5f08cc934c260e33a50ae0d73fdf869f7c02b6b47b33"), - new TransactionHash("b87238089e81527158a6daee520280324bc7e5322ba54d1b3c9a5678abe953ea"), - new TransactionHash("b45dd5e598bc85ba71639f2cbce8c5dff2fbe93159e637852fddeb16c0e84a48"), - new TransactionHash("83db780e98d4d3c917668c47b33ba51445591efacb0df2a922f88e7dfbb5fc7d"), - new TransactionHash("c2eb62b28cc7320da2292d87944c5424a70e1f443323c138c1affada7f6e9705") - ] - - for (const hash of hashes) { - let transactionOnProxy = await proxyProvider.getTransaction(hash, sender, true); - let transactionOnAPI = await apiProvider.getTransaction(hash); - - ignoreKnownDifferencesBetweenProviders(transactionOnProxy, transactionOnAPI); - assert.deepEqual(transactionOnProxy, transactionOnAPI); - } - }); - - // TODO: Strive to have as little differences as possible between Proxy and API. - // ... On client-side (erdjs), try to handle the differences in ProxyProvider & ApiProvider, or in TransactionOnNetwork. - // ... Merging the providers (in the future) should solve this as well. - function ignoreKnownDifferencesBetweenProviders(transactionOnProxy: TransactionOnNetwork, transactionOnAPI: TransactionOnNetwork) { - // Ignore status, since it differs between Proxy and API (for smart contract calls): - transactionOnProxy.status = new TransactionStatus("unknown"); - transactionOnAPI.status = new TransactionStatus("unknown"); - - // Ignore fields which are not present on API response: - transactionOnProxy.epoch = 0; - transactionOnProxy.type = new TransactionOnNetworkType(); - transactionOnProxy.blockNonce = new Nonce(0); - transactionOnProxy.hyperblockNonce = new Nonce(0); - transactionOnProxy.hyperblockHash = new Hash(""); - - let contractResultsOnAPI: SmartContractResultItem[] = transactionOnAPI.results.getAll(); - - // Important issue (existing bug)! When working with TransactionOnNetwork objects, SCRs cannot be parsed correctly from API, only from Proxy. - // On API response, base64 decode "data" from smart contract results: - for (const item of contractResultsOnAPI) { - item.data = Buffer.from(item.data, "base64").toString(); - } - - // On API response, convert "callType" of smart contract results to a number: - for (const item of contractResultsOnAPI) { - item.callType = Number(item.callType); - } - - // On API response, sort contract results by nonce: - contractResultsOnAPI.sort(function (a: SmartContractResultItem, b: SmartContractResultItem) { - return a.nonce.valueOf() - b.nonce.valueOf(); - }); - } -}); diff --git a/src/transaction.ts b/src/transaction.ts index f206d2090..82bee8dea 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -1,5 +1,5 @@ import { BigNumber } from "bignumber.js"; -import { IAddressOfExternalSigner, ISignable, ISignatureOfExternalSigner } from "./interface"; +import { IBech32Address, ISignable, ISignature } from "./interface"; import { Address } from "./address"; import { Balance } from "./balance"; import { @@ -226,7 +226,7 @@ export class Transaction implements ISignable { * * @param signedBy The address of the future signer */ - serializeForSigning(signedBy: IAddressOfExternalSigner): Buffer { + serializeForSigning(signedBy: IBech32Address): Buffer { let adaptedSignedBy = adaptToAddress(signedBy); // TODO: for appropriate tx.version, interpret tx.options accordingly and sign using the content / data hash @@ -294,7 +294,7 @@ export class Transaction implements ISignable { * @param signature The signature, as computed by a signer. * @param signedBy The address of the signer. */ - applySignature(signature: ISignatureOfExternalSigner, signedBy: IAddressOfExternalSigner) { + applySignature(signature: ISignature, signedBy: IBech32Address) { let adaptedSignature = adaptToSignature(signature); let adaptedSignedBy = adaptToAddress(signedBy); @@ -387,90 +387,3 @@ export class TransactionHash extends Hash { } } -/** - * An abstraction for handling and interpreting the "status" field of a {@link Transaction}. - */ -export class TransactionStatus { - /** - * The raw status, as fetched from the Network. - */ - readonly status: string; - - /** - * Creates a new TransactionStatus object. - */ - constructor(status: string) { - this.status = (status || "").toLowerCase(); - } - - /** - * Creates an unknown status. - */ - static createUnknown(): TransactionStatus { - return new TransactionStatus("unknown"); - } - - /** - * Returns whether the transaction is pending (e.g. in mempool). - */ - isPending(): boolean { - return ( - this.status == "received" || - this.status == "pending" || - this.status == "partially-executed" - ); - } - - /** - * Returns whether the transaction has been executed (not necessarily with success). - */ - isExecuted(): boolean { - return this.isSuccessful() || this.isFailed() || this.isInvalid(); - } - - /** - * Returns whether the transaction has been executed successfully. - */ - isSuccessful(): boolean { - return ( - this.status == "executed" || - this.status == "success" || - this.status == "successful" - ); - } - - /** - * Returns whether the transaction has been executed, but with a failure. - */ - isFailed(): boolean { - return ( - this.status == "fail" || - this.status == "failed" || - this.status == "unsuccessful" || - this.isInvalid() - ); - } - - /** - * Returns whether the transaction has been executed, but marked as invalid (e.g. due to "insufficient funds"). - */ - isInvalid(): boolean { - return this.status == "invalid"; - } - - toString(): string { - return this.status; - } - - valueOf(): string { - return this.status; - } - - equals(other: TransactionStatus) { - if (!other) { - return false; - } - - return this.status == other.status; - } -} diff --git a/src/transactionLogs.ts b/src/transactionLogs.ts deleted file mode 100644 index bdc5a01b8..000000000 --- a/src/transactionLogs.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { Address } from "./address"; -import { ErrTransactionEventNotFound, ErrUnexpectedCondition } from "./errors"; - -export class TransactionLogs { - readonly address: Address; - readonly events: TransactionEvent[]; - - constructor(address: Address, events: TransactionEvent[]) { - this.address = address; - this.events = events; - } - - static empty(): TransactionLogs { - return new TransactionLogs(new Address(), []); - } - - static fromHttpResponse(logs: any): TransactionLogs { - let address = new Address(logs.address); - let events = (logs.events || []).map((event: any) => TransactionEvent.fromHttpResponse(event)); - return new TransactionLogs(address, events); - } - - findSingleOrNoneEvent(identifier: string, predicate?: (event: TransactionEvent) => boolean): TransactionEvent | undefined { - let events = this.findEvents(identifier, predicate); - - if (events.length > 1) { - throw new ErrUnexpectedCondition(`more than one event of type ${identifier}`); - } - - return events[0]; - } - - findFirstOrNoneEvent(identifier: string, predicate?: (event: TransactionEvent) => boolean): TransactionEvent | undefined { - return this.findEvents(identifier, predicate)[0]; - } - - findEvents(identifier: string, predicate?: (event: TransactionEvent) => boolean): TransactionEvent[] { - let events = this.events.filter(event => event.identifier == identifier); - - if (predicate) { - events = events.filter(event => predicate(event)); - } - - return events; - } -} - -export class TransactionEvent { - readonly address: Address; - readonly identifier: string; - readonly topics: TransactionEventTopic[]; - readonly data: string; - - constructor(address: Address, identifier: string, topics: TransactionEventTopic[], data: string) { - this.address = address; - this.identifier = identifier; - this.topics = topics; - this.data = data; - } - - static fromHttpResponse(responsePart: { - address: string, - identifier: string, - topics: string[], - data: string - }): TransactionEvent { - let topics = (responsePart.topics || []).map(topic => new TransactionEventTopic(topic)); - let address = new Address(responsePart.address); - let identifier = responsePart.identifier || ""; - let data = Buffer.from(responsePart.data || "", "base64").toString(); - let event = new TransactionEvent(address, identifier, topics, data); - return event; - } - - findFirstOrNoneTopic(predicate: (topic: TransactionEventTopic) => boolean): TransactionEventTopic | undefined { - return this.topics.filter(topic => predicate(topic))[0]; - } - - getLastTopic(): TransactionEventTopic { - return this.topics[this.topics.length - 1]; - } -} - -export class TransactionEventTopic { - private readonly raw: Buffer; - - constructor(topic: string) { - this.raw = Buffer.from(topic || "", "base64"); - } - - toString() { - return this.raw.toString("utf8"); - } - - hex() { - return this.raw.toString("hex"); - } - - valueOf(): Buffer { - return this.raw; - } -} diff --git a/src/transactionOnNetwork.ts b/src/transactionOnNetwork.ts deleted file mode 100644 index 50c918aca..000000000 --- a/src/transactionOnNetwork.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { Address } from "./address"; -import { Balance } from "./balance"; -import { GasPrice, GasLimit } from "./networkParams"; -import { Nonce } from "./nonce"; -import { Signature } from "./signature"; -import { TransactionPayload } from "./transactionPayload"; -import { Hash } from "./hash"; -import { TransactionHash, TransactionStatus } from "./transaction"; -import { SmartContractResults } from "./smartcontracts/smartContractResults"; -import { TransactionEvent, TransactionLogs } from "./transactionLogs"; -import { TransactionCompletionStrategy } from "./transactionCompletionStrategy"; - -/** - * A plain view of a transaction, as queried from the Network. - */ -export class TransactionOnNetwork { - hash: TransactionHash = new TransactionHash(""); - type: TransactionOnNetworkType = new TransactionOnNetworkType(); - nonce: Nonce = new Nonce(0); - round: number = 0; - epoch: number = 0; - value: Balance = Balance.Zero(); - receiver: Address = new Address(); - sender: Address = new Address(); - gasPrice: GasPrice = new GasPrice(0); - gasLimit: GasLimit = new GasLimit(0); - data: TransactionPayload = new TransactionPayload(); - signature: Signature = Signature.empty(); - status: TransactionStatus = TransactionStatus.createUnknown(); - timestamp: number = 0; - - blockNonce: Nonce = new Nonce(0); - hyperblockNonce: Nonce = new Nonce(0); - hyperblockHash: Hash = Hash.empty(); - pendingResults: boolean = false; - - // TODO: Check if "receipt" is still received from the API. - receipt: Receipt = new Receipt(); - results: SmartContractResults = SmartContractResults.empty(); - logs: TransactionLogs = TransactionLogs.empty(); - - constructor(init?: Partial) { - Object.assign(this, init); - } - - static fromHttpResponse(txHash: TransactionHash, response: { - type: string, - nonce: number, - round: number, - epoch: number, - value: string, - sender: string, - receiver: string, - gasPrice: number, - gasLimit: number, - data: string, - status: string, - pendingResults: boolean, - timestamp: number, - blockNonce: number; - hyperblockNonce: number, - hyperblockHash: string, - receipt: any, - results: any[], - smartContractResults: any[], - logs: any[] - }): TransactionOnNetwork { - let transactionOnNetwork = new TransactionOnNetwork(); - - transactionOnNetwork.hash = txHash; - transactionOnNetwork.type = new TransactionOnNetworkType(response.type || ""); - transactionOnNetwork.nonce = new Nonce(response.nonce || 0); - transactionOnNetwork.round = response.round; - transactionOnNetwork.epoch = response.epoch || 0; - transactionOnNetwork.value = Balance.fromString(response.value); - transactionOnNetwork.sender = Address.fromBech32(response.sender); - transactionOnNetwork.receiver = Address.fromBech32(response.receiver); - transactionOnNetwork.gasPrice = new GasPrice(response.gasPrice); - transactionOnNetwork.gasLimit = new GasLimit(response.gasLimit); - transactionOnNetwork.data = TransactionPayload.fromEncoded(response.data); - transactionOnNetwork.status = new TransactionStatus(response.status); - transactionOnNetwork.timestamp = response.timestamp || 0; - - transactionOnNetwork.blockNonce = new Nonce(response.blockNonce || 0); - transactionOnNetwork.hyperblockNonce = new Nonce(response.hyperblockNonce || 0); - transactionOnNetwork.hyperblockHash = new Hash(response.hyperblockHash); - // TODO: Take this into consideration, as well. - // Currently, erdjs' transaction completion detection works only for ProxyProvider. - // When adding separate constructors "fromAPIHttpResponse" / "fromGatewayHttpResponse", - // we will also use different completion detection strategies. - // (not done right now, in order to avoid further code workarounds). - transactionOnNetwork.pendingResults = response.pendingResults || false; - - transactionOnNetwork.receipt = Receipt.fromHttpResponse(response.receipt || {}); - transactionOnNetwork.results = SmartContractResults.fromHttpResponse(response.results || response.smartContractResults || []); - transactionOnNetwork.logs = TransactionLogs.fromHttpResponse(response.logs || {}); - - return transactionOnNetwork; - } - - getDateTime(): Date { - return new Date(this.timestamp * 1000); - } - - isCompleted(): boolean { - // TODO: When using separate constructors of TransactionOnNetwork (for API response vs. for Gateway response, see package "networkProvider"), - // we will be able to use different transaction completion strategies. - return new TransactionCompletionStrategy().isCompleted(this); - } - - getAllEvents(): TransactionEvent[] { - let result = [...this.logs.events]; - - for (const resultItem of this.results.getAll()) { - result.push(...resultItem.logs.events); - } - - return result; - } -} - -/** - * Not yet implemented. - */ -export class TransactionOnNetworkType { - readonly value: string; - - constructor(value?: string) { - this.value = value || "unknown"; - } -} - -// TODO: Check if we still need this. -export class Receipt { - value: Balance = Balance.Zero(); - sender: Address = new Address(); - data: string = ""; - hash: TransactionHash = TransactionHash.empty(); - - static fromHttpResponse(response: { - value: string, - sender: string, - data: string, - txHash: string - }): Receipt { - let receipt = new Receipt(); - - receipt.value = Balance.fromString(response.value); - receipt.sender = new Address(response.sender); - receipt.data = response.data; - receipt.hash = new TransactionHash(response.txHash); - - return receipt; - } -} diff --git a/src/transactionWatcher.spec.ts b/src/transactionWatcher.spec.ts index d56eaecd5..b2da4fa4f 100644 --- a/src/transactionWatcher.spec.ts +++ b/src/transactionWatcher.spec.ts @@ -1,9 +1,10 @@ import { assert } from "chai"; import { TransactionWatcher } from "./transactionWatcher"; -import { TransactionHash, TransactionStatus } from "./transaction"; +import { TransactionHash } from "./transaction"; import { MockProvider, InHyperblock, Wait } from "./testutils"; import { Nonce } from "./nonce"; -import { TransactionOnNetwork } from "./transactionOnNetwork"; +import { TransactionOnNetwork } from "./networkProvider/transactions"; +import { TransactionStatus } from "./networkProvider/transactionStatus"; describe("test transactionWatcher", () => { diff --git a/src/transactionWatcher.ts b/src/transactionWatcher.ts index bd33c8146..754a0cb08 100644 --- a/src/transactionWatcher.ts +++ b/src/transactionWatcher.ts @@ -1,18 +1,17 @@ -import { ITransactionFetcher } from "./interface"; +import { ITransactionFetcher, IHash } from "./interface"; import { AsyncTimer } from "./asyncTimer"; -import { TransactionHash, TransactionStatus } from "./transaction"; -import { TransactionOnNetwork } from "./transactionOnNetwork"; import { Logger } from "./logger"; import { Err, ErrExpectedTransactionEventsNotFound, ErrExpectedTransactionStatusNotReached } from "./errors"; import { Address } from "./address"; +import { ITransactionOnNetwork, ITransactionStatus } from "./interfaceOfNetwork"; -export type PredicateIsAwaitedStatus = (status: TransactionStatus) => boolean; +export type PredicateIsAwaitedStatus = (status: ITransactionStatus) => boolean; /** * Internal interface: a transaction, as seen from the perspective of a {@link TransactionWatcher}. */ interface ITransaction { - getHash(): TransactionHash; + getHash(): IHash; } /** @@ -22,7 +21,7 @@ export class TransactionWatcher { static DefaultPollingInterval: number = 6000; static DefaultTimeout: number = TransactionWatcher.DefaultPollingInterval * 15; - static NoopOnStatusReceived = (_: TransactionStatus) => { }; + static NoopOnStatusReceived = (_: ITransactionStatus) => { }; private readonly fetcher: ITransactionFetcher; private readonly pollingInterval: number; @@ -48,11 +47,11 @@ export class TransactionWatcher { * Waits until the transaction reaches the "pending" status. */ public async awaitPending(transaction: ITransaction): Promise { - let isPending = (status: TransactionStatus) => status.isPending(); + let isPending = (status: ITransactionStatus) => status.isPending(); let doFetch = async () => await this.fetcher.getTransactionStatus(transaction.getHash()); let errorProvider = () => new ErrExpectedTransactionStatusNotReached(); - return this.awaitConditionally( + return this.awaitConditionally( isPending, doFetch, errorProvider @@ -63,11 +62,11 @@ export class TransactionWatcher { * Waits until the transaction is completely processed. */ public async awaitCompleted(transaction: ITransaction): Promise { - let isCompleted = (transactionOnNetwork: TransactionOnNetwork) => transactionOnNetwork.isCompleted(); + let isCompleted = (transactionOnNetwork: ITransactionOnNetwork) => transactionOnNetwork.isCompleted(); let doFetch = async () => await this.fetcher.getTransaction(transaction.getHash(), undefined, true); let errorProvider = () => new ErrExpectedTransactionStatusNotReached(); - return this.awaitConditionally( + return this.awaitConditionally( isCompleted, doFetch, errorProvider @@ -75,7 +74,7 @@ export class TransactionWatcher { } public async awaitAllEvents(transaction: ITransaction, events: string[]): Promise { - let foundAllEvents = (transactionOnNetwork: TransactionOnNetwork) => { + let foundAllEvents = (transactionOnNetwork: ITransactionOnNetwork) => { let allEventIdentifiers = transactionOnNetwork.getAllEvents().map(event => event.identifier); let allAreFound = events.every(event => allEventIdentifiers.includes(event)); return allAreFound; @@ -84,7 +83,7 @@ export class TransactionWatcher { let doFetch = async () => await this.fetcher.getTransaction(transaction.getHash(), undefined, true); let errorProvider = () => new ErrExpectedTransactionEventsNotFound(); - return this.awaitConditionally( + return this.awaitConditionally( foundAllEvents, doFetch, errorProvider @@ -92,7 +91,7 @@ export class TransactionWatcher { } public async awaitAnyEvent(transaction: ITransaction, events: string[]): Promise { - let foundAnyEvent = (transactionOnNetwork: TransactionOnNetwork) => { + let foundAnyEvent = (transactionOnNetwork: ITransactionOnNetwork) => { let allEventIdentifiers = transactionOnNetwork.getAllEvents().map(event => event.identifier); let anyIsFound = events.find(event => allEventIdentifiers.includes(event)) != undefined; return anyIsFound; @@ -101,18 +100,18 @@ export class TransactionWatcher { let doFetch = async () => await this.fetcher.getTransaction(transaction.getHash(), undefined, true); let errorProvider = () => new ErrExpectedTransactionEventsNotFound(); - return this.awaitConditionally( + return this.awaitConditionally( foundAnyEvent, doFetch, errorProvider ); } - public async awaitOnCondition(transaction: ITransaction, condition: (data: TransactionOnNetwork) => boolean): Promise { + public async awaitOnCondition(transaction: ITransaction, condition: (data: ITransactionOnNetwork) => boolean): Promise { let doFetch = async () => await this.fetcher.getTransaction(transaction.getHash(), undefined, true); let errorProvider = () => new ErrExpectedTransactionStatusNotReached(); - return this.awaitConditionally( + return this.awaitConditionally( condition, doFetch, errorProvider @@ -172,12 +171,12 @@ class TransactionFetcherWithTracing implements ITransactionFetcher { this.fetcher = fetcher; } - async getTransaction(txHash: TransactionHash, hintSender?: Address, withResults?: boolean): Promise { + async getTransaction(txHash: IHash, hintSender?: Address, withResults?: boolean): Promise { Logger.debug(`transactionWatcher, getTransaction(${txHash.toString()})`); return await this.fetcher.getTransaction(txHash, hintSender, withResults); } - async getTransactionStatus(txHash: TransactionHash): Promise { + async getTransactionStatus(txHash: IHash): Promise { Logger.debug(`transactionWatcher, getTransactionStatus(${txHash.toString()})`); return await this.fetcher.getTransactionStatus(txHash); }