UNPKG

@faast/ethereum-payments

Version:

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

175 lines (151 loc) 5.27 kB
import * as request from 'request-promise-native' import { BigNumber } from 'bignumber.js' import Web3 from 'web3' import { Logger, DelegateLogger } from '@faast/ts-common' import type { TransactionConfig } from 'web3-core' import { AutoFeeLevels } from '@faast/payments-common' type Eth = Web3['eth'] import { DEFAULT_GAS_PRICE_IN_WEI, GAS_STATION_URL, GAS_STATION_FEE_SPEED, MAXIMUM_GAS, GAS_ESTIMATE_MULTIPLIER, ETHEREUM_TRANSFER_COST, } from './constants' import { EthTxType } from './types' import { retryIfDisconnected } from './utils' export class NetworkData { private gasStationUrl: string | undefined private parityUrl: string | undefined private eth: Eth private logger: Logger constructor(eth: Eth, gasStationUrl: string = GAS_STATION_URL, parityUrl?: string, logger?: Logger | null) { this.eth = eth this.gasStationUrl = gasStationUrl this.parityUrl = parityUrl this.logger = new DelegateLogger(logger, 'NetworkData') } async getNetworkData( txType: EthTxType, speed: AutoFeeLevels, from: string, to: string, data?: string, ): Promise<{ pricePerGasUnit: string, nonce: string, amountOfGas: number, }> { const pricePerGasUnit = await this.getGasPrice(speed) const nonce = await this.getNonce(from) const amountOfGas = await this.estimateGas({ from, to, data }, txType) return { pricePerGasUnit, amountOfGas, nonce, } } async getNonce(address: string): Promise<string> { const web3Nonce = await this.getWeb3Nonce(address) || '0' const parityNonce = await this.getParityNonce(address) || '0' const nonce = BigNumber.maximum(web3Nonce, parityNonce) return nonce.toNumber() ? nonce.toString() : '0' } async getGasPrice(speed: AutoFeeLevels): Promise<string> { let gasPrice = await this.getGasStationGasPrice(speed) if (gasPrice) return gasPrice gasPrice = await this.getWeb3GasPrice() if (gasPrice) return gasPrice return DEFAULT_GAS_PRICE_IN_WEI } async estimateGas(txObject: TransactionConfig, txType: EthTxType): Promise<number> { try { // estimateGas mutates txObject so must pass in a clone let gas = await this._retryDced(() => this.eth.estimateGas({ ...txObject })) if (gas > 21000) { // No need for multiplier for regular ethereum transfers gas = gas * GAS_ESTIMATE_MULTIPLIER } const maxGas = MAXIMUM_GAS[txType] if (gas > maxGas) { gas = maxGas } const result = Math.ceil(gas) this.logger.debug(`Estimated gas limit of ${result} for ${txType}`) return result } catch (e) { this.logger.warn(`Failed to estimate gas for ${txType} -- ${e}`) return MAXIMUM_GAS[txType] } } private async getWeb3Nonce(address: string): Promise<string> { try { const nonce = await this._retryDced(() => this.eth.getTransactionCount(address, 'pending')) return (new BigNumber(nonce)).toString() } catch (e) { return '' } } private async getParityNonce(address: string): Promise<string> { const data = { method: 'parity_nextNonce', params: [address], id: 1, jsonrpc: '2.0' } const options = { url: this.parityUrl || '', json: data } let body: { [key: string]: string } try { body = await request.post(options) } catch (e) { this.logger.warn('Failed to retrieve nonce from parity - ', e.toString()) return '' } if (!body || !body.result) { this.logger.warn('Bad result or missing fields in parity nextNonce response', body) return '' } return (new BigNumber(body.result, 16)).toString() } private async getGasStationGasPrice(level: AutoFeeLevels): Promise<string> { const hasKey = /\?api-key=/.test(this.gasStationUrl || '') const options = { url: hasKey ? `${this.gasStationUrl}` : `${this.gasStationUrl}/json/ethgasAPI.json`, json: true, timeout: 5000 } let body: { [key: string]: number } try { body = await this._retryDced(() => request.get(options)) } catch (e) { this.logger.warn('Failed to retrieve gas price from ethgasstation - ', e.toString()) return '' } const speed = GAS_STATION_FEE_SPEED[level] if (!(body && body.blockNum && body[speed])) { this.logger.warn('Bad result or missing fields in ethgasstation response', body) return '' } const price10xGwei = body[speed] const gwei = new BigNumber(price10xGwei).dividedBy(10) this.logger.log(`Retrieved gas price of ${gwei} Gwei from ethgasstation using speed ${speed}`) return gwei.multipliedBy(1e9).dp(0, BigNumber.ROUND_DOWN).toFixed() } private async getWeb3GasPrice(): Promise<string> { try { const wei = new BigNumber(await this._retryDced(() => this.eth.getGasPrice())) this.logger.log(`Retrieved gas price of ${wei.div(1e9)} Gwei from web3`) return wei.dp(0, BigNumber.ROUND_DOWN).toFixed() } catch (e) { this.logger.warn('Failed to retrieve gas price from web3 - ', e.toString()) return '' } } async _retryDced<T>(fn: () => Promise<T>): Promise<T> { return retryIfDisconnected(fn, this.logger) } }