@faast/ethereum-payments
Version:
Library to assist in processing ethereum payments, such as deriving addresses and sweeping funds
277 lines • 11.1 kB
JavaScript
import Web3 from 'web3';
import { Payport, createUnitConverters, FeeRateType, NetworkType, TransactionStatus, } from '@faast/payments-common';
import { DelegateLogger, assertType, isNull } from '@faast/ts-common';
import BigNumber from 'bignumber.js';
import { PACKAGE_NAME, ETH_DECIMAL_PLACES, ETH_NAME, ETH_SYMBOL, DEFAULT_ADDRESS_FORMAT, MIN_SWEEPABLE_WEI, MIN_CONFIRMATIONS, } from './constants';
import { EthereumAddressFormat, EthereumAddressFormatT } from './types';
import { isValidXkey } from './bip44';
import { NetworkData } from './NetworkData';
import { retryIfDisconnected } from './utils';
export class EthereumPaymentsUtils {
constructor(config) {
var _a, _b, _c;
this.logger = new DelegateLogger(config.logger, PACKAGE_NAME);
this.networkType = config.network || NetworkType.Mainnet;
this.coinName = (_a = config.name) !== null && _a !== void 0 ? _a : ETH_NAME;
this.coinSymbol = (_b = config.symbol) !== null && _b !== void 0 ? _b : ETH_SYMBOL;
this.coinDecimals = (_c = config.decimals) !== null && _c !== void 0 ? _c : ETH_DECIMAL_PLACES;
this.server = config.fullNode || null;
let provider;
if (config.web3) {
this.web3 = config.web3;
}
else if (isNull(this.server)) {
this.web3 = new Web3();
}
else if (this.server.startsWith('http')) {
provider = new Web3.providers.HttpProvider(this.server, config.providerOptions);
this.web3 = new Web3(provider);
}
else if (this.server.startsWith('ws')) {
provider = new Web3.providers.WebsocketProvider(this.server, config.providerOptions);
this.web3 = new Web3(provider);
}
else {
throw new Error(`Invalid ethereum payments fullNode, must start with http or ws: ${this.server}`);
}
if (provider && process.env.NODE_DEBUG && process.env.NODE_DEBUG.includes('ethereum-payments')) {
const send = provider.send;
provider.send = (payload, cb) => {
this.logger.debug(`web3 provider request ${this.server}`, payload);
send.call(provider, payload, (error, result) => {
if (error) {
this.logger.debug(`web3 provider response error ${this.server}`, error);
}
else {
this.logger.debug(`web3 provider response result ${this.server}`, result);
}
cb(error, result);
});
};
}
this.eth = this.web3.eth;
this.gasStation = new NetworkData(this.eth, config.gasStation, config.parityNode, this.logger);
const unitConverters = createUnitConverters(this.coinDecimals);
this.toMainDenominationBigNumber = unitConverters.toMainDenominationBigNumber;
this.toBaseDenominationBigNumber = unitConverters.toBaseDenominationBigNumber;
this.toMainDenomination = unitConverters.toMainDenominationString;
this.toBaseDenomination = unitConverters.toBaseDenominationString;
const ethUnitConverters = createUnitConverters(ETH_DECIMAL_PLACES);
this.toMainDenominationBigNumberEth = ethUnitConverters.toMainDenominationBigNumber;
this.toBaseDenominationBigNumberEth = ethUnitConverters.toBaseDenominationBigNumber;
this.toMainDenominationEth = ethUnitConverters.toMainDenominationString;
this.toBaseDenominationEth = ethUnitConverters.toBaseDenominationString;
}
async init() { }
async destroy() { }
isValidAddress(address, options = {}) {
const { format } = options;
if (format === EthereumAddressFormat.Lowercase) {
return this.web3.utils.isAddress(address) &&
address === address.toLowerCase();
}
else if (format === EthereumAddressFormat.Checksum) {
return this.web3.utils.checkAddressChecksum(address);
}
return this.web3.utils.isAddress(address);
}
standardizeAddress(address, options) {
var _a;
if (!this.web3.utils.isAddress(address)) {
return null;
}
const format = assertType(EthereumAddressFormatT, (_a = options === null || options === void 0 ? void 0 : options.format) !== null && _a !== void 0 ? _a : DEFAULT_ADDRESS_FORMAT, 'format');
if (format === EthereumAddressFormat.Lowercase) {
return address.toLowerCase();
}
else {
return this.web3.utils.toChecksumAddress(address);
}
}
isValidExtraId(extraId) {
return false;
}
isValidPayport(payport) {
return Payport.is(payport) && !this._getPayportValidationMessage(payport);
}
validatePayport(payport) {
const message = this._getPayportValidationMessage(payport);
if (message) {
throw new Error(message);
}
}
getPayportValidationMessage(payport) {
try {
payport = assertType(Payport, payport, 'payport');
}
catch (e) {
return e.message;
}
return this._getPayportValidationMessage(payport);
}
isValidXprv(xprv) {
return isValidXkey(xprv) && xprv.substring(0, 4) === 'xprv';
}
isValidXpub(xpub) {
return isValidXkey(xpub) && xpub.substring(0, 4) === 'xpub';
}
isValidPrivateKey(prv) {
try {
return Boolean(this.web3.eth.accounts.privateKeyToAccount(prv));
}
catch (e) {
return false;
}
}
privateKeyToAddress(prv) {
let key;
if (prv.substring(0, 2) === '0x') {
key = prv;
}
else {
key = `0x${prv}`;
}
return this.web3.eth.accounts.privateKeyToAccount(key).address.toLowerCase();
}
_getPayportValidationMessage(payport) {
try {
const { address } = payport;
if (!(this.isValidAddress(address))) {
return 'Invalid payport address';
}
}
catch (e) {
return 'Invalid payport address';
}
return undefined;
}
async getFeeRateRecommendation(level) {
const gasPrice = await this.gasStation.getGasPrice(level);
return {
feeRate: gasPrice,
feeRateType: FeeRateType.BasePerWeight,
};
}
async _retryDced(fn) {
return retryIfDisconnected(fn, this.logger);
}
async getCurrentBlockNumber() {
return this._retryDced(() => this.eth.getBlockNumber());
}
isAddressBalanceSweepable(balanceEth) {
return this.toBaseDenominationBigNumberEth(balanceEth).gt(MIN_SWEEPABLE_WEI);
}
async getAddressBalance(address) {
const balance = await this._retryDced(() => this.eth.getBalance(address));
const confirmedBalance = this.toMainDenomination(balance).toString();
const sweepable = this.isAddressBalanceSweepable(confirmedBalance);
return {
confirmedBalance,
unconfirmedBalance: '0',
spendableBalance: confirmedBalance,
sweepable,
requiresActivation: false,
};
}
async getAddressNextSequenceNumber(address) {
return this.gasStation.getNonce(address);
}
async getAddressUtxos() {
return [];
}
async getTransactionInfo(txid) {
const minConfirmations = MIN_CONFIRMATIONS;
const tx = await this._retryDced(() => this.eth.getTransaction(txid));
if (!tx) {
throw new Error(`Transaction ${txid} not found`);
}
const currentBlockNumber = await this.getCurrentBlockNumber();
let txInfo = await this._retryDced(() => this.eth.getTransactionReceipt(txid));
tx.from = tx.from ? tx.from.toLowerCase() : '';
tx.to = tx.to ? tx.to.toLowerCase() : '';
if (!txInfo) {
txInfo = {
transactionHash: tx.hash,
from: tx.from,
to: tx.to,
status: true,
blockNumber: 0,
cumulativeGasUsed: 0,
gasUsed: 0,
transactionIndex: 0,
blockHash: '',
logs: [],
logsBloom: ''
};
return {
id: txid,
amount: this.toMainDenomination(tx.value),
toAddress: tx.to ? tx.to.toLowerCase() : null,
fromAddress: tx.from ? tx.from.toLowerCase() : null,
toExtraId: null,
fromIndex: null,
toIndex: null,
fee: this.toMainDenomination((new BigNumber(tx.gasPrice)).multipliedBy(tx.gas)),
sequenceNumber: tx.nonce,
weight: tx.gas,
isExecuted: false,
isConfirmed: false,
confirmations: 0,
confirmationId: null,
confirmationTimestamp: null,
currentBlockNumber: currentBlockNumber,
status: TransactionStatus.Pending,
data: {
...tx,
...txInfo,
currentBlock: currentBlockNumber
},
};
}
let isConfirmed = false;
let confirmationTimestamp = null;
let confirmations = 0;
if (tx.blockNumber) {
confirmations = currentBlockNumber - tx.blockNumber;
if (confirmations > minConfirmations) {
isConfirmed = true;
const txBlock = await this._retryDced(() => this.eth.getBlock(tx.blockNumber));
confirmationTimestamp = new Date(Number(txBlock.timestamp) * 1000);
}
}
let status = TransactionStatus.Pending;
if (isConfirmed) {
status = TransactionStatus.Confirmed;
if (txInfo.hasOwnProperty('status') && (txInfo.status === false || txInfo.status.toString() === 'false')) {
status = TransactionStatus.Failed;
}
}
txInfo.from = tx.from;
txInfo.to = tx.to;
return {
id: txid,
amount: this.toMainDenomination(tx.value),
toAddress: tx.to ? tx.to.toLowerCase() : null,
fromAddress: tx.from ? tx.from.toLowerCase() : null,
toExtraId: null,
fromIndex: null,
toIndex: null,
fee: this.toMainDenomination((new BigNumber(tx.gasPrice)).multipliedBy(txInfo.gasUsed)),
sequenceNumber: tx.nonce,
weight: txInfo.gasUsed,
isExecuted: status !== TransactionStatus.Failed,
isConfirmed,
confirmations,
confirmationId: tx.blockHash,
confirmationTimestamp,
status,
currentBlockNumber: currentBlockNumber,
data: {
...tx,
...txInfo,
currentBlock: currentBlockNumber
},
};
}
}
//# sourceMappingURL=EthereumPaymentsUtils.js.map