UNPKG

@faast/ethereum-payments

Version:

Library to assist in processing ethereum payments, such as deriving addresses and sweeping funds

277 lines 11.1 kB
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