// 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()
);
}
}