UNPKG

@faast/ethereum-payments

Version:

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

285 lines 13 kB
import { BigNumber } from 'bignumber.js'; import { Transaction as Tx } from 'ethereumjs-tx'; import { cloneDeep } from 'lodash'; import { TransactionStatus, FeeLevel, FeeRateType, FeeOptionCustom, PaymentsError, PaymentsErrorCode, NetworkType, DEFAULT_MAX_FEE_PERCENT, } from '@faast/payments-common'; import { isType, isMatchingError } from '@faast/ts-common'; import request from 'request-promise-native'; import { DEFAULT_FEE_LEVEL, ETHEREUM_TRANSFER_COST, TOKEN_WALLET_DATA, DEPOSIT_KEY_INDEX, TOKEN_PROXY_DATA, } from './constants'; import { EthereumPaymentsUtils } from './EthereumPaymentsUtils'; export class BaseEthereumPayments extends EthereumPaymentsUtils { constructor(config) { super(config); this.config = config; this.depositKeyIndex = (typeof config.depositKeyIndex === 'undefined') ? DEPOSIT_KEY_INDEX : config.depositKeyIndex; } getFullConfig() { return this.config; } async resolvePayport(payport) { if (typeof payport === 'number') { return this.getPayport(payport); } else if (typeof payport === 'string') { if (!this.isValidAddress(payport)) { throw new Error(`Invalid Ethereum address: ${payport}`); } return { address: payport.toLowerCase() }; } if (!this.isValidPayport(payport)) { throw new Error(`Invalid Ethereum payport: ${JSON.stringify(payport)}`); } else { if (!this.isValidAddress(payport.address)) { throw new Error(`Invalid Ethereum payport: ${JSON.stringify(payport)}`); } } return { ...payport, address: payport.address.toLowerCase() }; } async resolveFromTo(from, to) { const fromPayport = await this.getPayport(from); const toPayport = await this.resolvePayport(to); return { fromAddress: fromPayport.address, fromIndex: from, fromExtraId: fromPayport.extraId, fromPayport, toAddress: toPayport.address, toIndex: typeof to === 'number' ? to : null, toExtraId: toPayport.extraId, toPayport, }; } async resolveFeeOption(feeOption, amountOfGas = ETHEREUM_TRANSFER_COST) { if (new BigNumber(amountOfGas).dp() > 0) { throw new Error(`Amount of gas must be a whole number ${amountOfGas}`); } return isType(FeeOptionCustom, feeOption) ? this.resolveCustomFeeOption(feeOption, amountOfGas) : this.resolveLeveledFeeOption(feeOption.feeLevel, amountOfGas); } resolveCustomFeeOption(feeOption, amountOfGas) { const { feeRate, feeRateType } = feeOption; let gasPrice; if (feeRateType === FeeRateType.BasePerWeight) { gasPrice = new BigNumber(feeRate); } else { const feeRateBase = feeRateType === FeeRateType.Main ? this.toBaseDenominationBigNumberEth(feeRate) : new BigNumber(feeRate); gasPrice = feeRateBase.dividedBy(amountOfGas); } gasPrice = gasPrice.dp(0, BigNumber.ROUND_DOWN); const feeBase = gasPrice.multipliedBy(amountOfGas); const feeMain = this.toMainDenominationBigNumberEth(feeBase); return { targetFeeRate: feeOption.feeRate, targetFeeLevel: FeeLevel.Custom, targetFeeRateType: feeOption.feeRateType, feeBase: feeBase.toFixed(), feeMain: feeMain.toFixed(), gasPrice: gasPrice.toFixed(), }; } async resolveLeveledFeeOption(feeLevel = DEFAULT_FEE_LEVEL, amountOfGas) { const gasPrice = new BigNumber(await this.gasStation.getGasPrice(feeLevel)); const feeBase = gasPrice.multipliedBy(amountOfGas).toFixed(); return { targetFeeRate: gasPrice.toFixed(), targetFeeLevel: feeLevel, targetFeeRateType: FeeRateType.BasePerWeight, feeBase, feeMain: this.toMainDenominationEth(feeBase), gasPrice: gasPrice.toFixed(), }; } requiresBalanceMonitor() { return false; } async getAvailableUtxos() { return []; } async getUtxos() { return []; } usesSequenceNumber() { return true; } usesUtxos() { return false; } async getBalance(resolveablePayport) { const payport = await this.resolvePayport(resolveablePayport); return this.getAddressBalance(payport.address); } async isSweepableBalance(balance) { return this.isAddressBalanceSweepable(balance); } async getNextSequenceNumber(payport) { const resolvedPayport = await this.resolvePayport(payport); return this.getAddressNextSequenceNumber(resolvedPayport.address); } async createTransaction(from, to, amountEth, options = {}) { this.logger.debug('createTransaction', from, to, amountEth); return this.createTransactionObject(from, to, amountEth, options); } async createServiceTransaction(from = this.depositKeyIndex, options = {}) { this.logger.debug('createDepositTransaction', from); return this.createTransactionObject(from, undefined, '', options); } async createSweepTransaction(from, to, options = {}) { this.logger.debug('createSweepTransaction', from, to); return this.createTransactionObject(from, to, 'max', options); } async createMultiOutputTransaction(from, to, options = {}) { return null; } async signTransaction(unsignedTx) { const fromPrivateKey = await this.getPrivateKey(unsignedTx.fromIndex); const payport = await this.getPayport(unsignedTx.fromIndex); const unsignedRaw = cloneDeep(unsignedTx.data); const extraParam = this.networkType === NetworkType.Testnet ? { chain: 'ropsten' } : undefined; const tx = new Tx(unsignedRaw, extraParam); const key = Buffer.from(fromPrivateKey.slice(2), 'hex'); tx.sign(key); const result = { ...unsignedTx, id: `0x${tx.hash().toString('hex')}`, status: TransactionStatus.Signed, data: { hex: `0x${tx.serialize().toString('hex')}` } }; this.logger.debug('signTransaction result', result); return result; } sendTransactionWithoutConfirmation(txHex) { return this._retryDced(() => new Promise((resolve, reject) => this.eth.sendSignedTransaction(txHex) .on('transactionHash', resolve) .on('error', reject))); } async broadcastTransaction(tx) { if (tx.status !== TransactionStatus.Signed) { throw new Error(`Tx ${tx.id} has not status ${TransactionStatus.Signed}`); } try { if (this.config.blockbookNode) { const url = `${this.config.blockbookNode}/api/sendtx/${tx.data.hex}`; request .get(url, { json: true }) .then((res) => this.logger.log(`Successful secondary broadcast to blockbook ethereum ${res.result}`)) .catch((e) => this.logger.log(`Failed secondary broadcast to blockbook ethereum ${tx.id}: ${url} - ${e}`)); } const txId = await this.sendTransactionWithoutConfirmation(tx.data.hex); return { id: txId, }; } catch (e) { if (isMatchingError(e, ['already known'])) { this.logger.log(`Ethereum broadcast tx already known ${tx.id}`); return { id: tx.id }; } this.logger.warn(`Ethereum broadcast tx unsuccessful ${tx.id}: ${e.message}`); if (isMatchingError(e, ['nonce too low'])) { throw new PaymentsError(PaymentsErrorCode.TxSequenceCollision, e.message); } throw new Error(`Ethereum broadcast tx unsuccessful: ${tx.id} ${e.message}`); } } async gasOptionOrEstimate(options, txObject, txType) { if (options.gas) { return new BigNumber(options.gas).dp(0, BigNumber.ROUND_UP).toNumber(); } return this.gasStation.estimateGas(txObject, txType); } async createTransactionObject(from, to, amountEth, options = {}) { var _a; const serviceFlag = (amountEth === '' && typeof to === 'undefined'); const sweepFlag = amountEth === 'max'; const txType = serviceFlag ? 'CONTRACT_DEPLOY' : 'ETHEREUM_TRANSFER'; const fromPayport = await this.getPayport(from); const toPayport = serviceFlag ? { address: '' } : await this.resolvePayport(to); const toIndex = typeof to === 'number' ? to : null; const txConfig = { from: fromPayport.address }; if (serviceFlag) { if (options.data) { txConfig.data = options.data; } else if (options.proxyAddress) { txConfig.data = TOKEN_PROXY_DATA .replace(/<address to proxy>/g, options.proxyAddress.replace('0x', '').toLowerCase()); } else { txConfig.data = TOKEN_WALLET_DATA; } } if (toPayport.address) { txConfig.to = toPayport.address; } const amountOfGas = await this.gasOptionOrEstimate(options, txConfig, txType); const feeOption = await this.resolveFeeOption(options, amountOfGas); const { confirmedBalance: balanceEth } = await this.getBalance(fromPayport); const nonce = options.sequenceNumber || await this.getNextSequenceNumber(fromPayport.address); const { feeMain, feeBase } = feeOption; const feeWei = new BigNumber(feeBase); const maxFeePercent = new BigNumber((_a = options.maxFeePercent) !== null && _a !== void 0 ? _a : DEFAULT_MAX_FEE_PERCENT); const balanceWei = this.toBaseDenominationBigNumberEth(balanceEth); let amountWei = new BigNumber(0); if (balanceWei.eq(0)) { throw new PaymentsError(PaymentsErrorCode.TxInsufficientBalance, `${fromPayport.address} No balance available (${balanceEth})`); } if (sweepFlag) { amountWei = balanceWei.minus(feeWei); if (balanceWei.isLessThan(feeWei)) { throw new PaymentsError(PaymentsErrorCode.TxFeeTooHigh, `${fromPayport.address} Insufficient balance (${balanceEth}) to pay sweep fee of ${feeMain}`); } if (feeWei.gt(maxFeePercent.times(balanceWei))) { throw new PaymentsError(PaymentsErrorCode.TxFeeTooHigh, `${fromPayport.address} Sweep fee (${feeMain}) exceeds max fee percent (${maxFeePercent}%) of address balance (${balanceEth})`); } } else if (!sweepFlag && !serviceFlag) { amountWei = this.toBaseDenominationBigNumberEth(amountEth); if (amountWei.plus(feeWei).isGreaterThan(balanceWei)) { throw new PaymentsError(PaymentsErrorCode.TxInsufficientBalance, `${fromPayport.address} Insufficient balance (${balanceEth}) to send ${amountEth} including fee of ${feeOption.feeMain}`); } if (feeWei.gt(maxFeePercent.times(amountWei))) { throw new PaymentsError(PaymentsErrorCode.TxFeeTooHigh, `${fromPayport.address} Sweep fee (${feeMain}) exceeds max fee percent (${maxFeePercent}%) of send amount (${amountEth})`); } } else { if (balanceWei.isLessThan(feeWei)) { throw new PaymentsError(PaymentsErrorCode.TxFeeTooHigh, `${fromPayport.address} Insufficient balance (${balanceEth}) to pay contract deploy fee of ${feeOption.feeMain}`); } } const result = { id: null, status: TransactionStatus.Unsigned, fromAddress: fromPayport.address, fromIndex: from, toAddress: serviceFlag ? '' : toPayport.address, toIndex, toExtraId: null, amount: serviceFlag ? '' : this.toMainDenomination(amountWei), fee: feeOption.feeMain, targetFeeLevel: feeOption.targetFeeLevel, targetFeeRate: feeOption.targetFeeRate, targetFeeRateType: feeOption.targetFeeRateType, weight: amountOfGas, sequenceNumber: nonce.toString(), data: { ...txConfig, value: `0x${amountWei.toString(16)}`, gas: `0x${amountOfGas.toString(16)}`, gasPrice: `0x${(new BigNumber(feeOption.gasPrice)).toString(16)}`, nonce: `0x${(new BigNumber(nonce)).toString(16)}`, }, }; this.logger.debug('createTransactionObject result', result); return result; } } export default BaseEthereumPayments; //# sourceMappingURL=BaseEthereumPayments.js.map