@faast/ethereum-payments
Version:
Library to assist in processing ethereum payments, such as deriving addresses and sweeping funds
285 lines • 13 kB
JavaScript
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