diff --git a/src/account.ts b/src/account.ts index a5db19db..b7ed953f 100644 --- a/src/account.ts +++ b/src/account.ts @@ -114,9 +114,9 @@ export class AccountOnNetwork { result.address = new Address(payload["address"] || 0); result.nonce = new Nonce(payload["nonce"] || 0); - result.balance = Balance.fromString(payload["balance"]); - result.code = payload["code"]; - result.userName = payload["username"]; + result.balance = Balance.fromString(payload["balance"] || "0"); + result.code = payload["code"] || ""; + result.userName = payload["username"] || ""; return result; } diff --git a/src/constants.ts b/src/constants.ts index 7d1d2900..b582f0e1 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -10,6 +10,7 @@ export const ESDTNFT_TRANSFER_FUNCTION_NAME = "ESDTNFTTransfer"; export const MULTI_ESDTNFT_TRANSFER_FUNCTION_NAME = "MultiESDTNFTTransfer"; export const ESDT_TRANSFER_VALUE = "0"; +// TODO: Rename fo "AxiosDefaultConfig" (less ambiguous). export const defaultConfig = { timeout: 1000, // See: https://github.com/axios/axios/issues/983 regarding transformResponse diff --git a/src/errors.ts b/src/errors.ts index 54b783fa..5aa28431 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -272,6 +272,16 @@ 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/index.ts b/src/index.ts index 5e484ff1..b41dfbfb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,4 +30,5 @@ export * from "./nullSigner"; export * from "./smartcontracts"; +export * from "./networkProvider"; export * from "./dapp"; diff --git a/src/networkProvider/apiNetworkProvider.ts b/src/networkProvider/apiNetworkProvider.ts new file mode 100644 index 00000000..3d809404 --- /dev/null +++ b/src/networkProvider/apiNetworkProvider.ts @@ -0,0 +1,192 @@ +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 { 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"; + +// TODO: Find & remove duplicate code between "ProxyNetworkProvider" and "ApiNetworkProvider". +export class ApiNetworkProvider implements INetworkProvider { + private url: string; + private config: AxiosRequestConfig; + private backingProxyNetworkProvider; + + constructor(url: string, config?: AxiosRequestConfig) { + this.url = url; + this.config = { ...defaultConfig, ...config }; + this.backingProxyNetworkProvider = new ProxyNetworkProvider(url, config); + } + + async getNetworkConfig(): Promise { + return await this.backingProxyNetworkProvider.getNetworkConfig(); + } + + async getNetworkStatus(): Promise { + return await this.backingProxyNetworkProvider.getNetworkStatus(); + } + + async getNetworkStakeStatistics(): Promise { + let response = await this.doGetGeneric("stake"); + let networkStake = NetworkStake.fromHttpResponse(response) + return networkStake; + } + + async getNetworkGeneralStatistics(): Promise { + let response = await this.doGetGeneric("stats"); + let stats = Stats.fromHttpResponse(response) + return stats; + } + + async getAccount(address: Address): Promise { + let response = await this.doGetGeneric(`accounts/${address.bech32()}`); + let account = AccountOnNetwork.fromHttpResponse(response); + return account; + } + + async getFungibleTokensOfAccount(address: Address, pagination?: Pagination): Promise { + pagination = pagination || Pagination.default(); + + let url = `accounts/${address.bech32()}/tokens?${this.buildPaginationParams(pagination)}`; + let response: any[] = await this.doGetGeneric(url); + let tokens = response.map(item => FungibleTokenOfAccountOnNetwork.fromHttpResponse(item)); + + // TODO: Fix sorting + tokens.sort((a, b) => a.identifier.localeCompare(b.identifier)); + return tokens; + } + + async getNonFungibleTokensOfAccount(address: Address, pagination?: Pagination): Promise { + pagination = pagination || Pagination.default(); + + let url = `accounts/${address.bech32()}/nfts?${this.buildPaginationParams(pagination)}`; + let response: any[] = await this.doGetGeneric(url); + let tokens = response.map(item => NonFungibleTokenOfAccountOnNetwork.fromApiHttpResponse(item)); + + // TODO: Fix sorting + tokens.sort((a, b) => a.identifier.localeCompare(b.identifier)); + return tokens; + } + + async getFungibleTokenOfAccount(address: Address, 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 { + let response = await this.doGetGeneric(`accounts/${address.bech32()}/nfts/${collection}-${nonce.hex()}`); + let tokenData = NonFungibleTokenOfAccountOnNetwork.fromApiHttpResponse(response); + return tokenData; + } + + async getTransaction(txHash: TransactionHash): Promise { + let response = await this.doGetGeneric(`transactions/${txHash.toString()}`); + let transaction = TransactionOnNetwork.fromApiHttpResponse(txHash, response); + return transaction; + } + + async getTransactionStatus(txHash: TransactionHash): Promise { + let response = await this.doGetGeneric(`transactions/${txHash.toString()}?fields=status`); + let status = new TransactionStatus(response.status); + return status; + } + + async sendTransaction(tx: Transaction): Promise { + let response = await this.doPostGeneric("transactions", tx.toSendable()); + let hash = new TransactionHash(response.txHash); + return hash; + } + + async simulateTransaction(tx: Transaction): Promise { + return await this.backingProxyNetworkProvider.simulateTransaction(tx); + } + + async queryContract(query: Query): Promise { + let data = query.toHttpRequest(); + let response = await this.doPostGeneric("query", data); + let queryResponse = ContractQueryResponse.fromHttpResponse(response); + return queryResponse; + } + + async getDefinitionOfFungibleToken(tokenIdentifier: string): Promise { + let response = await this.doGetGeneric(`tokens/${tokenIdentifier}`); + let definition = DefinitionOfFungibleTokenOnNetwork.fromApiHttpResponse(response); + return definition; + } + + async getDefinitionOfTokenCollection(collection: string): Promise { + let response = await this.doGetGeneric(`collections/${collection}`); + let definition = DefinitionOfTokenCollectionOnNetwork.fromApiHttpResponse(response); + return definition; + } + + async getNonFungibleToken(collection: string, nonce: Nonce): Promise { + let response = await this.doGetGeneric(`nfts/${collection}-${nonce.hex()}`); + let token = NonFungibleTokenOfAccountOnNetwork.fromApiHttpResponse(response); + return token; + } + + async doGetGeneric(resourceUrl: string): Promise { + let response = await this.doGet(resourceUrl); + return response; + } + + async doPostGeneric(resourceUrl: string, payload: any): Promise { + let response = await this.doPost(resourceUrl, payload); + return response; + } + + private buildPaginationParams(pagination: Pagination) { + return `from=${pagination.from}&size=${pagination.size}`; + } + + private async doGet(resourceUrl: string): Promise { + try { + let url = `${this.url}/${resourceUrl}`; + let response = await axios.get(url, this.config); + return response.data; + } catch (error) { + this.handleApiError(error, resourceUrl); + } + } + + private async doPost(resourceUrl: string, payload: any): Promise { + try { + let url = `${this.url}/${resourceUrl}`; + let response = await axios.post(url, payload, { + ...this.config, + headers: { + "Content-Type": "application/json", + }, + }); + let responsePayload = response.data; + return responsePayload; + } catch (error) { + this.handleApiError(error, resourceUrl); + } + } + + private handleApiError(error: any, resourceUrl: string) { + if (!error.response) { + Logger.warn(error); + throw new ErrNetworkProvider(resourceUrl, error.toString(), error); + } + + let errorData = error.response.data; + let originalErrorMessage = errorData.error || errorData.message || JSON.stringify(errorData); + throw new ErrNetworkProvider(resourceUrl, originalErrorMessage, error); + } +} diff --git a/src/networkProvider/contractResults.ts b/src/networkProvider/contractResults.ts new file mode 100644 index 00000000..c4b0126a --- /dev/null +++ b/src/networkProvider/contractResults.ts @@ -0,0 +1,123 @@ +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 { ArgSerializer, EndpointDefinition, MaxUint64, ReturnCode, TypedValue } from "../smartcontracts"; +import { TransactionHash } from "../transaction"; + +export class ContractResults implements IContractResults { + readonly items: IContractResultItem[]; + + constructor(items: IContractResultItem[]) { + this.items = items; + + this.items.sort(function (a: IContractResultItem, b: IContractResultItem) { + return a.nonce.valueOf() - b.nonce.valueOf(); + }); + } + + static empty(): ContractResults { + return new ContractResults([]); + } + + static fromProxyHttpResponse(results: any[]): ContractResults { + let items = results.map(item => ContractResultItem.fromProxyHttpResponse(item)); + return new ContractResults(items); + } + + static fromApiHttpResponse(results: any[]): ContractResults { + let items = results.map(item => ContractResultItem.fromApiHttpResponse(item)); + return new ContractResults(items); + } +} + +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(); + data: string = ""; + previousHash: Hash = Hash.empty(); + originalHash: Hash = Hash.empty(); + gasLimit: GasLimit = new GasLimit(0); + gasPrice: GasPrice = new GasPrice(0); + callType: number = 0; + returnMessage: string = ""; + + static fromProxyHttpResponse(response: any): ContractResultItem { + let item = ContractResultItem.fromHttpResponse(response); + return item; + } + + static fromApiHttpResponse(response: any): ContractResultItem { + let item = ContractResultItem.fromHttpResponse(response); + + item.data = Buffer.from(item.data, "base64").toString(); + item.callType = Number(item.callType); + + return item; + } + + private static fromHttpResponse(response: any): ContractResultItem { + let item = new ContractResultItem(); + + 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.previousHash = new TransactionHash(response.prevTxHash); + item.originalHash = new TransactionHash(response.originalTxHash); + item.gasLimit = new GasLimit(response.gasLimit); + item.gasPrice = new GasPrice(response.gasPrice); + item.data = response.data || ""; + item.callType = response.callType; + item.returnMessage = response.returnMessage; + + return item; + } + + getOutputUntyped(): Buffer[] { + // TODO: Decide how to parse "data" (immediate results vs. other results). + throw new Error("Method not implemented."); + } + + getOutputTyped(_endpointDefinition: EndpointDefinition): TypedValue[] { + // TODO: Decide how to parse "data" (immediate results vs. other results). + throw new Error("Method not implemented."); + } +} + +export class ContractQueryResponse implements IContractQueryResponse { + returnData: string[] = []; + returnCode: ReturnCode = ReturnCode.None; + returnMessage: string = ""; + gasUsed: GasLimit = new GasLimit(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.returnMessage = payload["returnMessage"] || ""; + response.gasUsed = new GasLimit(MaxUint64.minus(gasRemaining).toNumber()); + + return response; + } + + getOutputUntyped(): Buffer[] { + let buffers = this.returnData.map((item) => Buffer.from(item || "", "base64")); + return buffers; + } + + getOutputTyped(endpointDefinition: EndpointDefinition): TypedValue[] { + let buffers = this.getOutputUntyped(); + let values = new ArgSerializer().buffersToValues(buffers, endpointDefinition!.output); + return values; + } +} diff --git a/src/networkProvider/index.ts b/src/networkProvider/index.ts new file mode 100644 index 00000000..0c4e6aa0 --- /dev/null +++ b/src/networkProvider/index.ts @@ -0,0 +1,5 @@ +export * from "./apiNetworkProvider"; +export * from "./proxyNetworkProvider"; + +// we do not export "./tokens" +// we do not export "./transactions" diff --git a/src/networkProvider/interface.ts b/src/networkProvider/interface.ts new file mode 100644 index 00000000..a5f9689c --- /dev/null +++ b/src/networkProvider/interface.ts @@ -0,0 +1,229 @@ +import { BigNumber } from "bignumber.js"; +import { Balance, GasLimit, GasPrice, Nonce, TransactionPayload } from ".."; +import { AccountOnNetwork } from "../account"; +import { Address } from "../address"; +import { Hash } from "../hash"; +import { NetworkConfig } from "../networkConfig"; +import { NetworkStake } from "../networkStake"; +import { NetworkStatus } from "../networkStatus"; +import { Signature } from "../signature"; +import { EndpointDefinition, Query, ReturnCode, TypedValue } from "../smartcontracts"; +import { Stats } from "../stats"; +import { Transaction, TransactionHash, TransactionStatus } from "../transaction"; +import { TransactionLogs } from "../transactionLogs"; + +/** + * An interface that defines the endpoints of an HTTP API Provider. + */ +export interface INetworkProvider { + /** + * Fetches the Network configuration. + */ + getNetworkConfig(): Promise; + + /** + * Fetches the Network status. + */ + getNetworkStatus(): Promise; + + /** + * Fetches stake statistics. + */ + getNetworkStakeStatistics(): Promise; + + /** + * Fetches general statistics. + */ + getNetworkGeneralStatistics(): Promise; + + /** + * Fetches the state of an {@link Account}. + */ + getAccount(address: Address): Promise; + + /** + * Fetches data about the fungible tokens held by an account. + */ + getFungibleTokensOfAccount(address: Address, pagination?: Pagination): Promise; + + /** + * Fetches data about the non-fungible tokens held by account. + */ + getNonFungibleTokensOfAccount(address: Address, pagination?: Pagination): Promise; + + /** + * Fetches data about a specific fungible token held by an account. + */ + getFungibleTokenOfAccount(address: Address, tokenIdentifier: string): Promise; + + /** + * Fetches data about a specific non-fungible token (instance) held by an account. + */ + getNonFungibleTokenOfAccount(address: Address, collection: string, nonce: Nonce): Promise; + + /** + * Fetches the state of a {@link Transaction}. + */ + getTransaction(txHash: TransactionHash): Promise; + + /** + * Queries the status of a {@link Transaction}. + */ + getTransactionStatus(txHash: TransactionHash): Promise; + + /** + * Broadcasts an already-signed {@link Transaction}. + */ + sendTransaction(tx: Transaction): Promise; + + /** + * Simulates the processing of an already-signed {@link Transaction}. + * + */ + simulateTransaction(tx: Transaction): Promise; + + /** + * Queries a Smart Contract - runs a pure function defined by the contract and returns its results. + */ + queryContract(query: Query): Promise; + + /** + * Fetches the definition of a fungible token. + * + */ + getDefinitionOfFungibleToken(tokenIdentifier: string): Promise; + + /** + * Fetches the definition of a SFT (including Meta ESDT) or NFT. + * + */ + getDefinitionOfTokenCollection(collection: string): Promise; + + /** + * Fetches data about a specific non-fungible token (instance). + */ + getNonFungibleToken(collection: string, nonce: Nonce): Promise; + + /** + * Performs a generic GET action against the provider (useful for new HTTP endpoints, not yet supported by erdjs). + */ + doGetGeneric(resourceUrl: string): Promise; + + /** + * Performs a generic POST action against the provider (useful for new HTTP endpoints, not yet supported by erdjs). + */ + doPostGeneric(resourceUrl: string, payload: any): Promise; +} + +export interface IFungibleTokenOfAccountOnNetwork { + identifier: string; + balance: BigNumber; +} + +export interface INonFungibleTokenOfAccountOnNetwork { + identifier: string; + collection: string; + attributes: Buffer; + balance: BigNumber; + nonce: Nonce; + creator: Address; + royalties: BigNumber; +} + + +export interface IDefinitionOfFungibleTokenOnNetwork { + identifier: string; + name: string; + ticker: string; + owner: Address; + decimals: number; + supply: BigNumber; + isPaused: boolean; + canUpgrade: boolean; + canMint: boolean; + canBurn: boolean; + canChangeOwner: boolean; + canPause: boolean; + canFreeze: boolean; + canWipe: boolean; + canAddSpecialRoles: boolean; +} + +export interface IDefinitionOfTokenCollectionOnNetwork { + collection: string; + type: string; + name: string; + ticker: string; + owner: Address; + decimals: number; + canPause: boolean; + canFreeze: boolean; + canWipe: boolean; + canTransferRole: boolean; + // 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; + + getOutputUntyped(): Buffer[]; + getOutputTyped(endpointDefinition: EndpointDefinition): TypedValue[]; +} + +export interface IContractQueryResponse { + returnData: string[]; + returnCode: ReturnCode; + returnMessage: string; + gasUsed: GasLimit; + + getOutputUntyped(): Buffer[]; + getOutputTyped(endpointDefinition: EndpointDefinition): TypedValue[]; +} + +export interface IContractSimulation { +} + +export class Pagination { + from: number = 0; + size: number = 100; + + static default(): Pagination { + return { from: 0, size: 100 }; + } +} diff --git a/src/networkProvider/providers.dev.net.spec.ts b/src/networkProvider/providers.dev.net.spec.ts new file mode 100644 index 00000000..7c953b5b --- /dev/null +++ b/src/networkProvider/providers.dev.net.spec.ts @@ -0,0 +1,264 @@ +import { assert } from "chai"; +import { ApiNetworkProvider, ProxyNetworkProvider } from "."; +import { Hash } from "../hash"; +import { INetworkProvider, ITransactionOnNetwork } from "./interface"; +import { Address } from "../address"; +import { loadTestWallets, TestWallet } from "../testutils"; +import { TransactionHash, TransactionStatus } from "../transaction"; +import { Nonce } from "../nonce"; +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"; + +describe("test network providers on devnet: Proxy and API", function () { + let apiProvider: INetworkProvider = new ApiNetworkProvider("https://devnet-api.elrond.com", { timeout: 10000 }); + let proxyProvider: INetworkProvider = new ProxyNetworkProvider("https://devnet-gateway.elrond.com", { timeout: 10000 }); + + let alice: TestWallet; + let bob: TestWallet; + let carol: TestWallet; + let dan: TestWallet; + + before(async function () { + ({ alice, bob, carol, dan } = await loadTestWallets()); + }); + + it("should have same response for getNetworkConfig()", async function () { + let apiResponse = await apiProvider.getNetworkConfig(); + let proxyResponse = await proxyProvider.getNetworkConfig(); + + assert.deepEqual(apiResponse, proxyResponse); + }); + + it("should have same response for getNetworkStatus()", async function () { + let apiResponse = await apiProvider.getNetworkStatus(); + let proxyResponse = await proxyProvider.getNetworkStatus(); + + assert.deepEqual(apiResponse, proxyResponse); + }); + + // TODO: Enable test after implementing ProxyNetworkProvider.getNetworkStakeStatistics(). + it.skip("should have same response for getNetworkStakeStatistics()", async function () { + let apiResponse = await apiProvider.getNetworkStakeStatistics(); + let proxyResponse = await proxyProvider.getNetworkStakeStatistics(); + + assert.deepEqual(apiResponse, proxyResponse); + }); + + // TODO: Enable test after implementing ProxyNetworkProvider.getNetworkGeneralStatistics(). + it.skip("should have same response for getNetworkGeneralStatistics()", async function () { + let apiResponse = await apiProvider.getNetworkGeneralStatistics(); + let proxyResponse = await proxyProvider.getNetworkGeneralStatistics(); + + assert.deepEqual(apiResponse, proxyResponse); + }); + + it("should have same response for getAccount()", async function () { + for (const user of [bob, carol, dan]) { + let apiResponse = await apiProvider.getAccount(user.address); + let proxyResponse = await proxyProvider.getAccount(user.address); + + assert.deepEqual(apiResponse, proxyResponse); + } + }); + + it("should have same response for getFungibleTokensOfAccount(), getFungibleTokenOfAccount()", async function () { + this.timeout(30000); + + for (const user of [carol, dan]) { + let apiResponse = await apiProvider.getFungibleTokensOfAccount(user.address); + let proxyResponse = await proxyProvider.getFungibleTokensOfAccount(user.address); + + assert.deepEqual(apiResponse.slice(0, 100), proxyResponse.slice(0, 100)); + + for (const item of apiResponse.slice(0, 5)) { + let apiResponse = await apiProvider.getFungibleTokenOfAccount(user.address, item.identifier); + let proxyResponse = await proxyProvider.getFungibleTokenOfAccount(user.address, item.identifier); + + assert.deepEqual(apiResponse, proxyResponse, `user: ${user.address.bech32()}, token: ${item.identifier}`); + } + } + }); + + it("should have same response for getNonFungibleTokensOfAccount(), getNonFungibleTokenOfAccount", async function () { + this.timeout(30000); + + for (const user of [alice, bob, carol, dan]) { + let apiResponse = await apiProvider.getNonFungibleTokensOfAccount(user.address); + let proxyResponse = await proxyProvider.getNonFungibleTokensOfAccount(user.address); + + assert.deepEqual(apiResponse.slice(0, 100), proxyResponse.slice(0, 100)); + + for (const item of apiResponse.slice(0, 5)) { + let apiResponse = await apiProvider.getNonFungibleTokenOfAccount(user.address, item.collection, item.nonce); + let proxyResponse = await proxyProvider.getNonFungibleTokenOfAccount(user.address, item.collection, item.nonce); + + assert.deepEqual(apiResponse, proxyResponse, `user: ${user.address.bech32()}, token: ${item.identifier}`); + } + } + }); + + it("should have same response for getTransaction()", async 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") + ]; + + for (const hash of hashes) { + let apiResponse = await apiProvider.getTransaction(hash); + let proxyResponse = await proxyProvider.getTransaction(hash); + + ignoreKnownTransactionDifferencesBetweenProviders(apiResponse, proxyResponse); + assert.deepEqual(apiResponse, proxyResponse, `transaction: ${hash}`); + } + }); + + // TODO: Strive to have as little differences as possible between Proxy and API. + function ignoreKnownTransactionDifferencesBetweenProviders(apiResponse: ITransactionOnNetwork, proxyResponse: ITransactionOnNetwork) { + // 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.epoch = 0; + proxyResponse.blockNonce = new Nonce(0); + proxyResponse.hyperblockNonce = new Nonce(0); + proxyResponse.hyperblockHash = new Hash(""); + } + + // TODO: Fix differences of "tx.status", then enable this test. + it.skip("should have same response for getTransactionStatus()", async 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") + ]; + + for (const hash of hashes) { + let apiResponse = await apiProvider.getTransactionStatus(hash); + let proxyResponse = await proxyProvider.getTransactionStatus(hash); + + assert.deepEqual(apiResponse, proxyResponse, `transaction: ${hash}`); + } + }); + + it("should have same response for getDefinitionOfFungibleToken()", async function () { + this.timeout(10000); + + let identifiers = ["MEX-b6bb7d", "WEGLD-88600a", "RIDE-482531", "USDC-a32906"]; + + for (const identifier of identifiers) { + let apiResponse = await apiProvider.getDefinitionOfFungibleToken(identifier); + + assert.equal(apiResponse.identifier, identifier); + + // TODO: Uncomment after implementing the function in the proxy provider. + // let proxyResponse = await proxyProvider.getDefinitionOfFungibleToken(identifier); + // assert.deepEqual(apiResponse, proxyResponse); + } + }); + + it("should have same response for getDefinitionOfTokenCollection()", async function () { + this.timeout(10000); + + let collections = ["LKMEX-9acade", "LKFARM-c20c1c", "MEXFARM-bab93a", "ART-264971", "MOS-ff0040"]; + + for (const collection of collections) { + let apiResponse = await apiProvider.getDefinitionOfTokenCollection(collection); + + assert.equal(apiResponse.collection, collection); + + // TODO: Uncomment after implementing the function in the proxy provider. + // let proxyResponse = await proxyProvider.getDefinitionOfTokenCollection(identifier); + // assert.deepEqual(apiResponse, proxyResponse); + } + }); + + it("should have same response for getNonFungibleToken()", async function () { + this.timeout(10000); + + let tokens = [{ id: "ERDJSNFT-4a5669", nonce: new Nonce(1) }]; + + for (const token of tokens) { + let apiResponse = await apiProvider.getNonFungibleToken(token.id, token.nonce); + + assert.equal(apiResponse.collection, token.id); + + // TODO: Uncomment after implementing the function in the proxy provider. + // let proxyResponse = await proxyProvider.getNonFungibleToken(token.id, token.nonce); + // assert.deepEqual(apiResponse, proxyResponse); + } + }); + + // TODO: enable when API fixes the imprecision around "gasRemaining". + // TODO: enable when API supports queries with "value". + it.skip("should have same response for queryContract()", async function () { + this.timeout(10000); + + // Query: get ultimate answer + let query = new Query({ + address: new Address("erd1qqqqqqqqqqqqqpgqggww7tjryk9saqzfpq09tw3vm06kl8h3396qqz277y"), + func: new ContractFunction("getUltimateAnswer"), + args: [] + }); + + let apiResponse = await apiProvider.queryContract(query); + let proxyResponse = await proxyProvider.queryContract(query); + + assert.deepEqual(apiResponse, proxyResponse); + assert.deepEqual(apiResponse.getOutputUntyped(), proxyResponse.getOutputUntyped()); + + // Query: increment counter + query = new Query({ + address: new Address("erd1qqqqqqqqqqqqqpgqz045rw74nthgzw2te9lytgah775n3l08396q3wt4qq"), + func: new ContractFunction("increment"), + args: [] + }); + + apiResponse = await apiProvider.queryContract(query); + proxyResponse = await proxyProvider.queryContract(query); + + assert.deepEqual(apiResponse, proxyResponse); + assert.deepEqual(apiResponse.getOutputUntyped(), proxyResponse.getOutputUntyped()); + + // Query: issue ESDT + query = new Query({ + address: new Address("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u"), + func: new ContractFunction("issue"), + value: Balance.egld(0.05), + args: [ + BytesValue.fromUTF8("FOO"), + BytesValue.fromUTF8("FOO"), + new BigUIntValue(new BigNumber("10000")), + new U32Value(18), + new VariadicValue(new VariadicType(new CompositeType(new BytesType(), new BooleanType())), []) + ] + }); + + apiResponse = await apiProvider.queryContract(query); + proxyResponse = await proxyProvider.queryContract(query); + + assert.deepEqual(apiResponse, proxyResponse); + assert.deepEqual(apiResponse.getOutputUntyped(), proxyResponse.getOutputUntyped()); + }); +}); diff --git a/src/networkProvider/proxyNetworkProvider.ts b/src/networkProvider/proxyNetworkProvider.ts new file mode 100644 index 00000000..57f5b238 --- /dev/null +++ b/src/networkProvider/proxyNetworkProvider.ts @@ -0,0 +1,204 @@ +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 { 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"; + +// TODO: Find & remove duplicate code between "ProxyNetworkProvider" and "ApiNetworkProvider". +export class ProxyNetworkProvider implements INetworkProvider { + private url: string; + private config: AxiosRequestConfig; + + constructor(url: string, config?: AxiosRequestConfig) { + this.url = url; + this.config = { ...defaultConfig, ...config }; + } + + async getNetworkConfig(): Promise { + let response = await this.doGetGeneric("network/config"); + let networkConfig = NetworkConfig.fromHttpResponse(response.config); + return networkConfig; + } + + async getNetworkStatus(): Promise { + let response = await this.doGetGeneric("network/status/4294967295"); + let networkStatus = NetworkStatus.fromHttpResponse(response.status); + return networkStatus; + } + + async getNetworkStakeStatistics(): Promise { + // TODO: Implement wrt.: + // https://github.com/ElrondNetwork/api.elrond.com/blob/main/src/endpoints/stake/stake.service.ts + throw new Error("Method not implemented."); + } + + async getNetworkGeneralStatistics(): Promise { + // TODO: Implement wrt. (full implementation may not be possible): + // https://github.com/ElrondNetwork/api.elrond.com/blob/main/src/endpoints/network/network.service.ts + throw new Error("Method not implemented."); + } + + async getAccount(address: Address): Promise { + let response = await this.doGetGeneric(`address/${address.bech32()}`); + let account = AccountOnNetwork.fromHttpResponse(response.account); + return account; + } + + async getFungibleTokensOfAccount(address: Address, _pagination?: Pagination): Promise { + let url = `address/${address.bech32()}/esdt`; + let response = await this.doGetGeneric(url); + let responseItems: any[] = Object.values(response.esdts); + // Skip NFTs / SFTs. + let responseItemsFiltered = responseItems.filter(item => !item.nonce); + let tokens = responseItemsFiltered.map(item => FungibleTokenOfAccountOnNetwork.fromHttpResponse(item)); + + // TODO: Fix sorting + tokens.sort((a, b) => a.identifier.localeCompare(b.identifier)); + return tokens; + } + + async getNonFungibleTokensOfAccount(address: Address, _pagination?: Pagination): Promise { + let url = `address/${address.bech32()}/esdt`; + let response = await this.doGetGeneric(url); + let responseItems: any[] = Object.values(response.esdts); + // Skip fungible tokens. + let responseItemsFiltered = responseItems.filter(item => item.nonce >= 0); + let tokens = responseItemsFiltered.map(item => NonFungibleTokenOfAccountOnNetwork.fromProxyHttpResponse(item)); + + // TODO: Fix sorting + tokens.sort((a, b) => a.identifier.localeCompare(b.identifier)); + return tokens; + } + + async getFungibleTokenOfAccount(address: Address, 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 { + 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 { + 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 { + let response = await this.doGetGeneric(`transaction/${txHash.toString()}/status`); + let status = new TransactionStatus(response.status); + return status; + } + + async sendTransaction(tx: Transaction): Promise { + let response = await this.doPostGeneric("transaction/send", tx.toSendable()); + let hash = new TransactionHash(response.txHash); + return hash; + } + + async simulateTransaction(tx: Transaction): Promise { + let response = await this.doPostGeneric("transaction/simulate", tx.toSendable()); + return response; + } + + async queryContract(query: Query): Promise { + let data = query.toHttpRequest(); + let response = await this.doPostGeneric("vm-values/query", data); + let queryResponse = ContractQueryResponse.fromHttpResponse(response.data); + return queryResponse; + } + + async getDefinitionOfFungibleToken(_tokenIdentifier: string): Promise { + // TODO: Implement wrt.: + // https://github.com/ElrondNetwork/api.elrond.com/blob/main/src/endpoints/esdt/esdt.service.ts#L221 + throw new Error("Method not implemented."); + } + + async getDefinitionOfTokenCollection(_collection: string): Promise { + // TODO: Implement wrt.: + // https://github.com/ElrondNetwork/api.elrond.com/blob/main/src/endpoints/collections/collection.service.ts + // https://docs.elrond.com/developers/esdt-tokens/#get-esdt-token-properties + throw new Error("Method not implemented."); + } + + async getNonFungibleToken(_collection: string, _nonce: Nonce): Promise { + throw new Error("Method not implemented."); + } + + async doGetGeneric(resourceUrl: string): Promise { + let response = await this.doGet(resourceUrl); + return response; + } + + async doPostGeneric(resourceUrl: string, payload: any): Promise { + let response = await this.doPost(resourceUrl, payload); + return response; + } + + private async doGet(resourceUrl: string): Promise { + try { + let url = `${this.url}/${resourceUrl}`; + let response = await axios.get(url, this.config); + let payload = response.data.data; + return payload; + } catch (error) { + this.handleApiError(error, resourceUrl); + } + } + + private async doPost(resourceUrl: string, payload: any): Promise { + try { + let url = `${this.url}/${resourceUrl}`; + let response = await axios.post(url, payload, { + ...this.config, + headers: { + "Content-Type": "application/json", + }, + }); + let responsePayload = response.data.data; + return responsePayload; + } catch (error) { + this.handleApiError(error, resourceUrl); + } + } + + private buildUrlWithQueryParameters(endpoint: string, params: Record): string { + let searchParams = new URLSearchParams(); + + for (let [key, value] of Object.entries(params)) { + if (value) { + searchParams.append(key, value); + } + } + + return `${endpoint}?${searchParams.toString()}`; + } + + private handleApiError(error: any, resourceUrl: string) { + if (!error.response) { + Logger.warn(error); + throw new ErrNetworkProvider(resourceUrl, error.toString(), error); + } + + let errorData = error.response.data; + let originalErrorMessage = errorData.error || errorData.message || JSON.stringify(errorData); + throw new ErrNetworkProvider(resourceUrl, originalErrorMessage, error); + } +} diff --git a/src/networkProvider/tokenDefinitions.ts b/src/networkProvider/tokenDefinitions.ts new file mode 100644 index 00000000..77b784e6 --- /dev/null +++ b/src/networkProvider/tokenDefinitions.ts @@ -0,0 +1,73 @@ +import { BigNumber } from "bignumber.js"; +import { Address } from "../address"; +import { IDefinitionOfFungibleTokenOnNetwork, IDefinitionOfTokenCollectionOnNetwork } from "./interface"; + +export class DefinitionOfFungibleTokenOnNetwork implements IDefinitionOfFungibleTokenOnNetwork { + identifier: string = ""; + name: string = ""; + ticker: string = ""; + owner: Address = new Address(); + decimals: number = 0; + supply: BigNumber = new BigNumber(0); + isPaused: boolean = false; + canUpgrade: boolean = false; + canMint: boolean = false; + canBurn: boolean = false; + canChangeOwner: boolean = false; + canPause: boolean = false; + canFreeze: boolean = false; + canWipe: boolean = false; + canAddSpecialRoles: boolean = false; + + static fromApiHttpResponse(payload: any): DefinitionOfFungibleTokenOnNetwork { + let result = new DefinitionOfFungibleTokenOnNetwork(); + + result.identifier = payload.identifier || ""; + result.name = payload.name || ""; + result.ticker = payload.ticker || ""; + result.owner = new Address(payload.owner || ""); + result.decimals = payload.decimals || 0; + result.supply = new BigNumber(payload.supply || "0"); + result.isPaused = payload.isPaused || false; + result.canUpgrade = payload.canUpgrade || false; + result.canMint = payload.canMint || false; + result.canBurn = payload.canBurn || false; + result.canChangeOwner = payload.canChangeOwner || false; + result.canPause = payload.canPause || false; + result.canFreeze = payload.canFreeze || false; + result.canWipe = payload.canWipe || false; + result.canAddSpecialRoles = payload.canAddSpecialRoles || false; + + return result; + } +} + +export class DefinitionOfTokenCollectionOnNetwork implements IDefinitionOfTokenCollectionOnNetwork { + collection: string = ""; + type: string = ""; + name: string = ""; + ticker: string = ""; + owner: Address = new Address(); + decimals: number = 0; + canPause: boolean = false; + canFreeze: boolean = false; + canWipe: boolean = false; + canTransferRole: boolean = false; + + static fromApiHttpResponse(payload: any): DefinitionOfTokenCollectionOnNetwork { + let result = new DefinitionOfTokenCollectionOnNetwork(); + + result.collection = payload.collection || ""; + result.type = payload.type || ""; + result.name = payload.name || ""; + result.ticker = payload.ticker || ""; + result.owner = new Address(payload.owner || ""); + result.decimals = payload.decimals || 0; + result.canPause = payload.canPause || false; + result.canFreeze = payload.canFreeze || false; + result.canWipe = payload.canWipe || false; + result.canTransferRole = payload.canTransferRole || false; + + return result; + } +} diff --git a/src/networkProvider/tokens.ts b/src/networkProvider/tokens.ts new file mode 100644 index 00000000..303fdb7c --- /dev/null +++ b/src/networkProvider/tokens.ts @@ -0,0 +1,75 @@ +import { BigNumber } from "bignumber.js"; +import { Address } from "../address"; +import { IFungibleTokenOfAccountOnNetwork, INonFungibleTokenOfAccountOnNetwork } from "./interface"; +import { Nonce } from "../nonce"; + +export class FungibleTokenOfAccountOnNetwork implements IFungibleTokenOfAccountOnNetwork { + identifier: string = ""; + balance: BigNumber = new BigNumber(0); + + static fromHttpResponse(payload: any): FungibleTokenOfAccountOnNetwork { + let result = new FungibleTokenOfAccountOnNetwork(); + + result.identifier = payload.tokenIdentifier || payload.identifier || ""; + result.balance = new BigNumber(payload.balance || 0); + + return result; + } +} + +export class NonFungibleTokenOfAccountOnNetwork implements INonFungibleTokenOfAccountOnNetwork { + identifier: string = ""; + collection: string = ""; + attributes: Buffer = Buffer.from([]); + balance: BigNumber = new BigNumber(0); + nonce: Nonce = new Nonce(0); + creator: Address = new Address(""); + royalties: BigNumber = new BigNumber(0); + + static fromProxyHttpResponse(payload: any): NonFungibleTokenOfAccountOnNetwork { + let result = NonFungibleTokenOfAccountOnNetwork.fromHttpResponse(payload); + + result.identifier = payload.tokenIdentifier || ""; + result.collection = NonFungibleTokenOfAccountOnNetwork.parseCollectionFromIdentifier(result.identifier); + result.royalties = new BigNumber(payload.royalties || 0).div(100); + + return result; + } + + static fromProxyHttpResponseByNonce(payload: any): NonFungibleTokenOfAccountOnNetwork { + let result = NonFungibleTokenOfAccountOnNetwork.fromHttpResponse(payload); + + result.identifier = `${payload.tokenIdentifier}-${result.nonce.hex()}`; + result.collection = payload.tokenIdentifier || ""; + result.royalties = new BigNumber(payload.royalties || 0).div(100); + + return result; + } + + static fromApiHttpResponse(payload: any): NonFungibleTokenOfAccountOnNetwork { + let result = NonFungibleTokenOfAccountOnNetwork.fromHttpResponse(payload); + + result.identifier = payload.identifier || ""; + result.collection = payload.collection || ""; + + return result; + } + + private static fromHttpResponse(payload: any): NonFungibleTokenOfAccountOnNetwork { + let result = new NonFungibleTokenOfAccountOnNetwork(); + + result.attributes = Buffer.from(payload.attributes || "", "base64"); + result.balance = new BigNumber(payload.balance || 1); + result.nonce = new Nonce(payload.nonce || 0); + result.creator = new Address(payload.creator || ""); + result.royalties = new BigNumber(payload.royalties || 0); + + return result; + } + + private static parseCollectionFromIdentifier(identifier: string): string { + let parts = identifier.split("-"); + let collection = parts.slice(0, 2).join("-"); + return collection; + } +} diff --git a/src/networkProvider/transactions.ts b/src/networkProvider/transactions.ts new file mode 100644 index 00000000..6fd3c906 --- /dev/null +++ b/src/networkProvider/transactions.ts @@ -0,0 +1,78 @@ +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 { ContractResults } from "./contractResults"; + + export class TransactionOnNetwork implements ITransactionOnNetwork { + hash: TransactionHash = new TransactionHash(""); + 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(); + + logs: TransactionLogs = TransactionLogs.empty(); + contractResults: IContractResults = ContractResults.empty(); + + static fromProxyHttpResponse(txHash: TransactionHash, response: any): TransactionOnNetwork { + let result = TransactionOnNetwork.fromHttpResponse(txHash, response); + result.contractResults = ContractResults.fromProxyHttpResponse(response.smartContractResults || []); + // TODO: uniformize transaction status + return result; + } + + static fromApiHttpResponse(txHash: TransactionHash, response: any): TransactionOnNetwork { + let result = TransactionOnNetwork.fromHttpResponse(txHash, response); + result.contractResults = ContractResults.fromApiHttpResponse(response.results || []); + // TODO: uniformize transaction status + return result; + } + + private static fromHttpResponse(txHash: TransactionHash, response: any): TransactionOnNetwork { + let result = new TransactionOnNetwork(); + + result.hash = txHash; + 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.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.logs = TransactionLogs.fromHttpResponse(response.logs || {}); + + return result; + } + + getDateTime(): Date { + return new Date(this.timestamp * 1000); + } +} + diff --git a/src/nonce.ts b/src/nonce.ts index 2355dd54..9c865d13 100644 --- a/src/nonce.ts +++ b/src/nonce.ts @@ -1,4 +1,5 @@ import * as errors from "./errors"; +import { numberToPaddedHex } from "./utils.codec"; /** * The nonce, as an immutable object. @@ -29,6 +30,10 @@ export class Nonce { return new Nonce(this.value + 1); } + hex(): string { + return numberToPaddedHex(this.value); + } + valueOf(): number { return this.value; } diff --git a/src/smartcontracts/returnCode.ts b/src/smartcontracts/returnCode.ts index 8d4dfe40..b537062e 100644 --- a/src/smartcontracts/returnCode.ts +++ b/src/smartcontracts/returnCode.ts @@ -1,5 +1,3 @@ -import { EndpointDefinition } from "./typesystem"; - export class ReturnCode { static None = new ReturnCode(""); static Ok = new ReturnCode("ok"); diff --git a/src/utils.codec.ts b/src/utils.codec.ts new file mode 100644 index 00000000..1e8d3cbb --- /dev/null +++ b/src/utils.codec.ts @@ -0,0 +1,10 @@ +export function numberToPaddedHex(value: number) { + let hex = value.toString(16); + let padding = "0"; + + if (hex.length % 2 == 1) { + hex = padding + hex; + } + + return hex; +}