// 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 .
import {
EventEmitter,
GET_TX_ATTEMPTS,
Provider,
RPCMethod,
RPCResponse,
Signable,
TxBlockObj,
} from "@zilliqa-js/core";
import {
getAddressFromPublicKey,
normaliseAddress,
toChecksumAddress,
} from "@zilliqa-js/crypto";
import { BN, Long } from "@zilliqa-js/util";
import {
TxEventName,
TxIncluded,
TxParams,
TxReceipt,
TxStatus,
} from "./types";
import { encodeTransactionProto, sleep } from "./util";
// @ts-ignore
import { Buffer } from "buffer";
import hashjs from "hash.js";
/**
* Transaction
*
* Transaction is a functor. Its purpose is to encode the possible states a
* Transaction can be in: Confirmed, Rejected, Pending, or Initialised (i.e., not broadcasted).
*/
export class Transaction implements Signable {
/**
* confirm
*
* constructs an already-confirmed transaction.
*
* @static
* @param {BaseTx} params
*/
static confirm(params: TxParams, provider: Provider) {
return new Transaction(params, provider, TxStatus.Confirmed);
}
/**
* reject
*
* constructs an already-rejected transaction.
*
* @static
* @param {BaseTx} params
*/
static reject(params: TxParams, provider: Provider) {
return new Transaction(params, provider, TxStatus.Rejected);
}
provider: Provider;
eventEmitter: EventEmitter;
id?: string;
status: TxStatus;
toDS: boolean;
blockConfirmation?: number;
// parameters
private version: number;
private nonce?: number;
private toAddr: string;
private pubKey?: string;
private amount: BN;
private gasPrice: BN;
private gasLimit: Long;
private code: string = "";
private data: string = "";
private receipt?: TxReceipt;
private signature?: string;
/**
* to get hash or transaction id of this transaction
* this can be identical returned by zilliqa network while calling CreateTransaction
*/
get hash(): string {
const payload = this.bytes.toString("hex");
return hashjs.sha256().update(payload, "hex").digest("hex");
}
get bytes(): Buffer {
return encodeTransactionProto(this.txParams);
}
get senderAddress(): string {
if (!this.pubKey) {
return "0".repeat(40);
}
return getAddressFromPublicKey(this.pubKey);
}
get txParams(): TxParams {
return {
version: this.version,
toAddr: normaliseAddress(this.toAddr),
nonce: this.nonce,
pubKey: this.pubKey,
amount: this.amount,
gasPrice: this.gasPrice,
gasLimit: this.gasLimit,
code: this.code,
data: this.data,
signature: this.signature,
receipt: this.receipt,
};
}
get payload() {
return {
version: this.version,
toAddr: this.toAddr,
nonce: this.nonce,
pubKey: this.pubKey,
amount: this.amount.toString(),
gasPrice: this.gasPrice.toString(),
gasLimit: this.gasLimit.toString(),
code: this.code,
data: this.data,
signature: this.signature,
receipt: this.receipt,
};
}
constructor(
params: TxParams,
provider: Provider,
status: TxStatus = TxStatus.Initialised,
toDS: boolean = false,
enableSecureToAddress: boolean = true
) {
// private members
this.version = params.version;
this.toAddr = enableSecureToAddress
? normaliseAddress(params.toAddr)
: toChecksumAddress(params.toAddr);
this.nonce = params.nonce;
this.pubKey = params.pubKey;
this.amount = params.amount;
this.code = params.code || "";
this.data = params.data || "";
this.signature = params.signature;
this.gasPrice = params.gasPrice;
this.gasLimit = params.gasLimit;
this.receipt = params.receipt;
// public members
this.provider = provider;
this.status = status;
this.toDS = toDS;
this.blockConfirmation = 0;
this.eventEmitter = new EventEmitter();
}
/**
* isPending
*
* @returns {boolean}
*/
isPending(): boolean {
return this.status === TxStatus.Pending;
}
/**
* isInitialised
*
* @returns {boolean}
*/
isInitialised(): boolean {
return this.status === TxStatus.Initialised;
}
getReceipt(): TxReceipt | undefined {
return this.receipt;
}
/**
* isConfirmed
*
* @returns {boolean}
*/
isConfirmed(): boolean {
return this.status === TxStatus.Confirmed;
}
/**
* isRejected
*
* @returns {boolean}
*/
isRejected(): boolean {
return this.status === TxStatus.Rejected;
}
/**
* setProvider
*
* Sets the provider on this instance.
*
* @param {Provider} provider
*/
setProvider(provider: Provider) {
this.provider = provider;
}
/**
* setStatus
*
* Escape hatch to imperatively set the state of the transaction.
*
* @param {TxStatus} status
* @returns {undefined}
*/
setStatus(status: TxStatus) {
this.status = status;
return this;
}
observed(): EventEmitter {
return this.eventEmitter;
}
/**
* blockConfirm
*
* Use `RPCMethod.GetLatestBlock` to get latest blockNumber
* Use interval to get the latestBlockNumber
* After BlockNumber change, then we use `RPCMethod.GetTransaction` to get the receipt
*
* @param {string} txHash
* @param {number} maxblockCount
* @param {number} interval interval in milliseconds
* @returns {Promise}
*/
async blockConfirm(
txHash: string,
maxblockCount: number = 4,
interval: number = 1000
) {
this.status = TxStatus.Pending;
const blockStart: BN = await this.getBlockNumber();
let blockChecked = blockStart;
for (let attempt = 0; attempt < maxblockCount; attempt += 1) {
try {
const blockLatest: BN = await this.getBlockNumber();
const blockNext: BN = blockChecked.add(
new BN(attempt === 0 ? attempt : 1)
);
if (blockLatest.gte(blockNext)) {
blockChecked = blockLatest;
this.emit(TxEventName.Track, {
txHash,
attempt,
currentBlock: blockChecked.toString(),
});
if (await this.trackTx(txHash)) {
this.blockConfirmation = blockLatest.sub(blockStart).toNumber();
return this;
}
} else {
attempt = attempt - 1 >= 0 ? attempt - 1 : 0;
}
} catch (err) {
this.status = TxStatus.Rejected;
throw err;
}
if (attempt + 1 < maxblockCount) {
await sleep(interval);
}
}
// if failed
const blockFailed: BN = await this.getBlockNumber();
this.blockConfirmation = blockFailed.sub(blockStart).toNumber();
this.status = TxStatus.Rejected;
const errorMessage = `The transaction is still not confirmed after ${maxblockCount} blocks.`;
throw new Error(errorMessage);
}
/**
* confirmReceipt
*
* Similar to the Promise API. This sets the Transaction instance to a state
* of pending. Calling this function kicks off a passive loop that polls the
* lookup node for confirmation on the txHash.
*
* The polls are performed with a linear backoff:
*
* `const delay = interval * attempt`
*
* This is a low-level method that you should generally not have to use
* directly.
*
* @param {string} txHash
* @param {number} maxAttempts
* @param {number} initial interval in milliseconds
* @returns {Promise}
*/
async confirm(
txHash: string,
maxAttempts = GET_TX_ATTEMPTS,
interval = 1000
): Promise {
this.status = TxStatus.Pending;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
this.emit(TxEventName.Track, {
txHash,
attempt,
});
try {
if (await this.trackTx(txHash)) {
return this;
}
} catch (err) {
this.status = TxStatus.Rejected;
throw err;
}
if (attempt + 1 < maxAttempts) {
await sleep(interval * attempt);
}
}
this.status = TxStatus.Rejected;
const errorMessage = `The transaction is still not confirmed after ${maxAttempts} attempts.`;
throw new Error(errorMessage);
}
/**
* map
*
* maps over the transaction, allowing for manipulation.
*
* @param {(prev: TxParams) => TxParams} fn - mapper
* @returns {Transaction}
*/
map(fn: (prev: TxParams) => TxParams): Transaction {
const newParams = fn(this.txParams);
this.setParams(newParams);
return this;
}
private setParams(params: TxParams) {
this.version = params.version;
this.toAddr = normaliseAddress(params.toAddr);
this.nonce = params.nonce;
this.pubKey = params.pubKey;
this.amount = params.amount;
this.code = params.code || "";
this.data = params.data || "";
this.signature = params.signature;
this.gasPrice = params.gasPrice;
this.gasLimit = params.gasLimit;
this.receipt = params.receipt;
}
private async trackTx(txHash: string): Promise {
const res: RPCResponse = await this.provider.send(
RPCMethod.GetTransaction,
txHash
);
if (res.error) {
this.emit(TxEventName.Error, res.error);
return false;
}
this.id = res.result.ID;
this.receipt = {
...res.result.receipt,
cumulative_gas: parseInt(res.result.receipt.cumulative_gas, 10),
};
this.emit(TxEventName.Receipt, this.receipt);
this.status =
this.receipt && this.receipt.success
? TxStatus.Confirmed
: TxStatus.Rejected;
return true;
}
private async getBlockNumber(): Promise {
try {
const res: RPCResponse = await this.provider.send(
RPCMethod.GetLatestTxBlock
);
if (res.error === undefined && res.result.header.BlockNum) {
// if blockNumber is too high, we use BN to be safer
return new BN(res.result.header.BlockNum);
} else {
throw new Error("Can not get latest BlockNumber");
}
} catch (error) {
throw error;
}
}
private emit(event: TxEventName | string, txEvent: any) {
this.eventEmitter.emit(event, { ...txEvent, event });
}
}