// Copyright (C) 2018 Zilliqa // // This file is part of zilliqa-js // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . /* eslint-disable */ import { Transaction, util, Wallet } from "@zilliqa-js/account"; import { fromBech32Address } from "@zilliqa-js/crypto"; import { validation } from "@zilliqa-js/util"; import { BlockchainInfo, BlockList, DsBlockObj, GET_TX_ATTEMPTS, TransactionStatusObj, Provider, RPCMethod, RPCResponse, ShardingStructure, TransactionObj, MinerInfo, TxBlockObj, TxList, ZilliqaModule, sign, } from "@zilliqa-js/core"; /* eslint-enable */ import { toTxParams } from "./util"; const isBlockNumber = (blockNum: number) => Number.isFinite(blockNum) && Number.isInteger(blockNum) && blockNum >= 0; export class Blockchain implements ZilliqaModule { signer: Wallet; provider: Provider; pendingErrorMap: { [key: number]: string } = { 0: "Transaction not found", 1: "Pending - Dispatched", 2: "Pending - Soft-confirmed (awaiting Tx block generation)", 4: "Pending - Nonce is higher than expected", 5: "Pending - Microblock gas limit exceeded", 6: "Pending - Consensus failure in network", 3: "Confirmed", 10: "Rejected - Transaction caused math error", 11: "Rejected - Scilla invocation error", 12: "Rejected - Contract account initialization error", 13: "Rejected - Invalid source account", 14: "Rejected - Gas limit higher than shard gas limit", 15: "Rejected - Unknown transaction type", 16: "Rejected - Transaction sent to wrong shard", 17: "Rejected - Contract & source account cross-shard issue", 18: "Rejected - Code size exceeded limit", 19: "Rejected - Transaction verification failed", 20: "Rejected - Gas limit too low", 21: "Rejected - Insufficient balance", 22: "Rejected - Insufficient gas to invoke Scilla checker", 23: "Rejected - Duplicate transaction exists", 24: "Rejected - Transaction with same nonce but same/higher gas price exists", 25: "Rejected - Invalid destination address", 26: "Rejected - Failed to add contract account to state", 27: "Rejected - Nonce is lower than expected", 255: "Rejected - Internal error", }; transactionStatusMap: { [key: number]: { [key: number]: string } } = { 0: { 0: "Transaction not found", 1: " Pending - Dispatched" }, 1: { 2: "Pending - Soft-confirmed (awaiting Tx block generation)", 4: "Pending - Nonce is higher than expected", 5: "Pending - Microblock gas limit exceeded", 6: "Pending - Consensus failure in network", }, 2: { 3: "Confirmed", 10: "Rejected - Transaction caused math error", 11: "Rejected - Scilla invocation error", 12: "Rejected - Contract account initialization error", 13: "Rejected - Invalid source account", 14: "Rejected - Gas limit higher than shard gas limit", 15: "Rejected - Unknown transaction type", 16: "Rejected - Transaction sent to wrong shard", 17: "Rejected - Contract & source account cross-shard issue", 18: "Rejected - Code size exceeded limit", 19: "Rejected - Transaction verification failed", 20: "Rejected - Gas limit too low", 21: "Rejected - Insufficient balance", 22: "Rejected - Insufficient gas to invoke Scilla checker", 23: "Rejected - Duplicate transaction exists", 24: "Rejected - Transaction with higher gas price exists", 25: "Rejected - Invalid destination address", 26: "Rejected - Failed to add contract account to state", 27: "Rejected - Nonce is lower than expected", 255: "Rejected - Internal error", }, }; constructor(provider: Provider, signer: Wallet) { this.provider = provider; this.provider.middleware.request.use( util.formatOutgoingTx, RPCMethod.CreateTransaction ); this.signer = signer; } getBlockChainInfo(): Promise> { return this.provider.send(RPCMethod.GetBlockchainInfo); } getShardingStructure(): Promise> { return this.provider.send(RPCMethod.GetShardingStructure); } // Gets details of a Directory Service block by block number. getDSBlock(blockNum: number): Promise> { return this.provider.send(RPCMethod.GetDSBlock, blockNum.toString()); } // Gets details of the most recent Directory Service block. getLatestDSBlock(): Promise> { return this.provider.send(RPCMethod.GetLatestDSBlock); } // Gets the number of DS blocks that the network has processed. getNumDSBlocks(): Promise> { return this.provider.send(RPCMethod.GetNumDSBlocks); } // Gets the average rate of DS blocks processed per second getDSBlockRate(): Promise> { return this.provider.send(RPCMethod.GetDSBlockRate); } // Gets a paginated list of up to 10 Directory Service (DS) blocks // and their block hashes for a specified page. getDSBlockListing(max: number): Promise> { return this.provider.send(RPCMethod.DSBlockListing, max); } // Gets details of a Transaction block by block number. getTxBlock(blockNum: number): Promise> { return this.provider.send(RPCMethod.GetTxBlock, blockNum.toString()); } // Gets details of the most recent Transaction block. getLatestTxBlock(): Promise> { return this.provider.send(RPCMethod.GetLatestTxBlock); } // Gets the total number of TxBlocks. getNumTxBlocks(): Promise> { return this.provider.send(RPCMethod.GetNumTxBlocks); } // Gets the average number of Tx blocks per second. getTxBlockRate(): Promise> { return this.provider.send(RPCMethod.GetTxBlockRate); } // Get a paginated list of Transaction blocks. getTxBlockListing(max: number): Promise> { return this.provider.send(RPCMethod.TxBlockListing, max); } // Gets the number of transactions processed by the network so far. getNumTransactions(): Promise> { return this.provider.send(RPCMethod.GetNumTransactions); } // Gets the number of transactions processed per second getTransactionRate(): Promise> { return this.provider.send(RPCMethod.GetTransactionRate); } // Gets the current Tx Epoch. getCurrentMiniEpoch(): Promise> { return this.provider.send(RPCMethod.GetCurrentMiniEpoch); } // Gets the current DS Epoch. getCurrentDSEpoch(): Promise> { return this.provider.send(RPCMethod.GetCurrentDSEpoch); } // Gets shard difficulty for previous PoW round getPrevDifficulty(): Promise> { return this.provider.send(RPCMethod.GetPrevDifficulty); } // Gets DS difficulty for previous PoW round getPrevDSDifficulty(): Promise> { return this.provider.send(RPCMethod.GetPrevDSDifficulty); } // Returns the total supply (ZIL) of coins in the network. getTotalCoinSupply(): Promise> { return this.provider.send(RPCMethod.GetTotalCoinSupply); } // Returns the mining nodes (i.e., the members of the DS committee and shards) // at the specified DS block. getMinerInfo(dsBlockNumber: string): Promise> { return this.provider.send(RPCMethod.GetMinerInfo, dsBlockNumber); } // Creates a transaction and polls the lookup node for a transaction receipt. @sign async createTransaction( tx: Transaction, maxAttempts: number = GET_TX_ATTEMPTS, interval: number = 1000, blockConfirm: boolean = false ): Promise { try { const response = await this.provider.send(RPCMethod.CreateTransaction, { ...tx.txParams, priority: tx.toDS, }); if (response.error) { throw response.error; } if (blockConfirm) { return tx.blockConfirm(response.result.TranID, maxAttempts, interval); } return tx.confirm(response.result.TranID, maxAttempts, interval); } catch (err) { throw err; } } // used together with signed batch // this method waits for each txn to confirm // see @createBatchTransactionWithoutConfirm for transactions without confirmation async createBatchTransaction( signedTxList: Transaction[], maxAttempts: number = GET_TX_ATTEMPTS, interval: number = 1000, blockConfirm: boolean = false ): Promise { try { const txParamsList = []; for (const signedTx of signedTxList) { if (signedTx.txParams.signature === undefined) { throw new Error("The transaction is not signed."); } txParamsList.push({ ...signedTx.txParams, priority: signedTx.toDS, }); } const response = await this.provider.sendBatch( RPCMethod.CreateTransaction, txParamsList ); if (response.error) { throw response.error; } // retrieve batch result const batchResults = []; for (let i = 0; i < signedTxList.length; i++) { const tx = signedTxList[i]; const txRes = response.batch_result[i]; if (blockConfirm) { batchResults.push( await tx.blockConfirm(txRes.result.TranID, maxAttempts, interval) ); } else { batchResults.push( await tx.confirm(txRes.result.TranID, maxAttempts, interval) ); } } return batchResults; } catch (err) { throw err; } } // Create a transaction by using a exist signed transaction payload // This payload may come form some offline signing software like ledger // Currently we haven't supported convert a singed transaction back to transaction param, so we won't perform // confirm logic here. async createTransactionRaw(payload: string): Promise { try { const tx = JSON.parse(payload); const response = await this.provider.send( RPCMethod.CreateTransaction, tx ); if (response.error) { throw response.error; } return response.result.TranID; } catch (err) { throw err; } } @sign async createTransactionWithoutConfirm(tx: Transaction): Promise { try { const response = await this.provider.send(RPCMethod.CreateTransaction, { ...tx.txParams, priority: tx.toDS, }); if (response.error) { throw response.error; } tx.id = response.result.TranID; return tx; } catch (err) { throw err; } } // used together with signed batch async createBatchTransactionWithoutConfirm( signedTxList: Transaction[] ): Promise { try { const txParamsList = []; for (const signedTx of signedTxList) { if (signedTx.txParams.signature === undefined) { throw new Error("The transaction is not signed."); } txParamsList.push({ ...signedTx.txParams, priority: signedTx.toDS, }); } const response = await this.provider.sendBatch( RPCMethod.CreateTransaction, txParamsList ); if (response.error) { throw response.error; } const batchResults = []; for (let i = 0; i < signedTxList.length; i++) { const tx = signedTxList[i]; const txRes = response.batch_result[i]; tx.id = txRes.result.TranID; batchResults.push(tx); } return batchResults; } catch (err) { throw err; } } // Returns the details of a specified Transaction. async getTransaction(txHash: string): Promise { try { const response = await this.provider.send( RPCMethod.GetTransaction, txHash.replace("0x", "") ); if (response.error) { return Promise.reject(response.error); } return response.result.receipt.success ? Transaction.confirm(toTxParams(response), this.provider) : Transaction.reject(toTxParams(response), this.provider); } catch (err) { throw err; } } // Returns the status of a specified transaction. async getTransactionStatus(txHash: string): Promise { try { const response = await this.provider.send( RPCMethod.GetTransactionStatus, txHash.replace("0x", "") ); if (response.error) { return Promise.reject(response.error); } const modificationState = response.result.modificationState; const status = response.result.status; response.result.statusMessage = this.transactionStatusMap[modificationState][status]; return response.result; } catch (err) { throw err; } } // Gets a list of recent transactions getRecentTransactions(): Promise> { return this.provider.send(RPCMethod.GetRecentTransactions); } // Returns the validated transactions included // within a specified final transaction block as an array of // length i, where i is the number of shards plus the DS committee. getTransactionsForTxBlock( txBlock: number ): Promise> { return this.provider.send( RPCMethod.GetTransactionsForTxBlock, txBlock.toString() ); } // returns the transactions in batches (or pages) of 2,500. // This API behaves similar to GetTransactionsForTxBlock getTransactionsForTxBlockEx( txBlock: number ): Promise> { if (!isBlockNumber(txBlock)) { throw new Error("invalid txBlock"); } return this.provider.send( RPCMethod.GetTransactionsForTxBlockEx, txBlock.toString() ); } // Returns the validated transactions (in verbose form) // included within a specified final transaction block. getTxnBodiesForTxBlock( txBlock: number ): Promise> { return this.provider.send( RPCMethod.GetTxnBodiesForTxBlock, txBlock.toString() ); } // Returns the transactions in batches (or pages) of 2,500 // This API behaves similar to GetTxBodiesForTxBlock getTxnBodiesForTxBlockEx(txBlock: number): Promise> { if (!isBlockNumber(txBlock)) { throw new Error("invalid txBlock"); } return this.provider.send( RPCMethod.GetTxnBodiesForTxBlockEx, txBlock.toString() ); } // Gets the number of transactions procesed for a given Tx Epoch. getNumTxnsTxEpoch(epoch: number): Promise> { return this.provider.send(RPCMethod.GetNumTxnsTxEpoch, epoch); } // Gets the number of transactions procesed for a given DS Epoch. getNumTxnsDSEpoch(epoch: number): Promise> { return this.provider.send(RPCMethod.GetNumTxnsDSEpoch, epoch); } // Gets the numeric minimum gas price. getMinimumGasPrice() { return this.provider.send(RPCMethod.GetMinimumGasPrice); } // Gets the balance of an account by address. getBalance(addr: string): Promise> { const address = validation.isBech32(addr) ? fromBech32Address(addr) : addr; return this.provider.send( RPCMethod.GetBalance, address.replace("0x", "").toLowerCase() ); } // Returns the Scilla code associated with a smart contract address getSmartContractCode( addr: string ): Promise> { const address = validation.isBech32(addr) ? fromBech32Address(addr) : addr; return this.provider.send( RPCMethod.GetSmartContractCode, address.replace("0x", "").toLowerCase() ); } // Returns the initialization (immutable) parameters of // a given smart contract, represented in a JSON format. getSmartContractInit(addr: string): Promise> { const address = validation.isBech32(addr) ? fromBech32Address(addr) : addr; return this.provider.send( RPCMethod.GetSmartContractInit, address.replace("0x", "").toLowerCase() ); } // Retrieves the entire state of a smart contract. getSmartContractState(addr: string): Promise> { const address = validation.isBech32(addr) ? fromBech32Address(addr) : addr; return this.provider.send( RPCMethod.GetSmartContractState, address.replace("0x", "").toLowerCase() ); } // Queries the contract state, filtered by the variable names. getSmartContractSubState( addr: string, variableName: string, indices?: string[] ): Promise> { const address = validation.isBech32(addr) ? fromBech32Address(addr) : addr; if (!variableName) { throw new Error("Variable name required"); } return this.provider.send( RPCMethod.GetSmartContractSubState, address.replace("0x", "").toLowerCase(), variableName, indices === undefined ? [] : indices ); } // Queries the contract state using batch rpc. getSmartContractSubStateBatch(reqs: any[]): Promise> { return this.provider.sendBatch(RPCMethod.GetSmartContractSubState, reqs); } getSmartContracts(addr: string): Promise> { const address = validation.isBech32(addr) ? fromBech32Address(addr) : addr; return this.provider.send( RPCMethod.GetSmartContracts, address.replace("0x", "").toLowerCase() ); } getContractAddressFromTransactionID( txHash: string ): Promise> { return this.provider.send( RPCMethod.GetContractAddressFromTransactionID, txHash ); } // Returns the state proof for the corresponding TxBlock for a smart contract. getStateProof( contractAddress: string, sha256Hash: string, txBlock: number | string ): Promise> { const address = validation.isBech32(contractAddress) ? fromBech32Address(contractAddress) : contractAddress; const isLatestStr = txBlock === "latest"; const isValid = isLatestStr || isBlockNumber(Number(txBlock)); if (!isValid) { throw new Error("invalid txBlock"); } return this.provider.send( RPCMethod.GetStateProof, address.replace("0x", "").toLowerCase(), sha256Hash, txBlock.toString() ); } }