@faast/ethereum-payments
Version:
Library to assist in processing ethereum payments, such as deriving addresses and sweeping funds
339 lines • 15.7 kB
JavaScript
import InputDataDecoder from 'ethereum-input-data-decoder';
import { BigNumber } from 'bignumber.js';
import Contract from 'web3-eth-contract';
import { deriveAddress } from './deriveAddress';
import { TransactionStatus, PaymentsError, PaymentsErrorCode, } from '@faast/payments-common';
import { MIN_CONFIRMATIONS, TOKEN_WALLET_ABI, TOKEN_WALLET_ABI_LEGACY, TOKEN_METHODS_ABI, DEPOSIT_KEY_INDEX, } from '../constants';
import { BaseEthereumPayments } from '../BaseEthereumPayments';
import * as SIGNATURE from './constants';
export class BaseErc20Payments extends BaseEthereumPayments {
constructor(config) {
super(config);
this.tokenAddress = config.tokenAddress.toLowerCase();
this.masterAddress = (config.masterAddress || '').toLowerCase();
this.depositKeyIndex = (typeof config.depositKeyIndex === 'undefined') ? DEPOSIT_KEY_INDEX : config.depositKeyIndex;
}
newContract(...args) {
const contract = new Contract(...args);
contract.setProvider(this.eth.currentProvider);
return contract;
}
async getBalance(resolveablePayport) {
const payport = await this.resolvePayport(resolveablePayport);
const contract = this.newContract(TOKEN_METHODS_ABI, this.tokenAddress);
const balance = await contract.methods.balanceOf(payport.address).call({});
const sweepable = await this.isSweepableBalance(this.toMainDenomination(balance));
return {
confirmedBalance: this.toMainDenomination(balance),
unconfirmedBalance: '0',
spendableBalance: this.toMainDenomination(balance),
sweepable,
requiresActivation: false,
};
}
async isSweepableBalance(balance) {
return new BigNumber(balance).isGreaterThan(0);
}
async createTransaction(from, to, amountMain, options = {}) {
this.logger.debug('createTransaction', from, to, amountMain);
const fromTo = await this.resolveFromTo(from, to);
const txFromAddress = fromTo.fromAddress.toLowerCase();
const amountBase = this.toBaseDenominationBigNumber(amountMain);
const contract = this.newContract(TOKEN_METHODS_ABI, this.tokenAddress);
const txData = contract.methods.transfer(fromTo.toAddress, `0x${amountBase.toString(16)}`).encodeABI();
const amountOfGas = await this.gasOptionOrEstimate(options, {
from: fromTo.fromAddress,
to: this.tokenAddress,
data: txData,
}, 'TOKEN_TRANSFER');
const feeOption = await this.resolveFeeOption(options, amountOfGas);
const feeBase = new BigNumber(feeOption.feeBase);
const nonce = options.sequenceNumber || await this.getNextSequenceNumber(txFromAddress);
let ethBalance = await this.getEthBaseBalance(fromTo.fromAddress);
if (feeBase.isGreaterThan(ethBalance)) {
throw new PaymentsError(PaymentsErrorCode.TxInsufficientBalance, `Insufficient ETH balance (${this.toMainDenominationEth(ethBalance)}) to pay transaction fee of ${feeOption.feeMain}`);
}
const transactionObject = {
from: fromTo.fromAddress.toLowerCase(),
to: this.tokenAddress,
data: txData,
value: '0x0',
gas: `0x${amountOfGas.toString(16)}`,
gasPrice: `0x${(new BigNumber(feeOption.gasPrice)).toString(16)}`,
nonce: `0x${(new BigNumber(nonce)).toString(16)}`,
};
this.logger.debug('transactionObject', transactionObject);
return {
status: TransactionStatus.Unsigned,
id: null,
fromAddress: fromTo.fromAddress.toLowerCase(),
toAddress: fromTo.toAddress.toLowerCase(),
toExtraId: null,
fromIndex: fromTo.fromIndex,
toIndex: fromTo.toIndex,
amount: amountMain,
fee: feeOption.feeMain,
targetFeeLevel: feeOption.targetFeeLevel,
targetFeeRate: feeOption.targetFeeRate,
targetFeeRateType: feeOption.targetFeeRateType,
sequenceNumber: nonce.toString(),
weight: amountOfGas,
data: transactionObject,
};
}
async createSweepTransaction(from, to, options = {}) {
this.logger.debug('createSweepTransaction', from, to);
if (from === 0) {
const { confirmedBalance } = await this.getBalance(from);
return this.createTransaction(from, to, confirmedBalance, options);
}
const { address: signerAddress } = await this.resolvePayport(this.depositKeyIndex);
const { address: toAddress } = await this.resolvePayport(to);
let txData;
let target;
let fromAddress;
if (typeof from === 'string') {
fromAddress = from.toLowerCase();
target = from.toLowerCase();
const contract = this.newContract(TOKEN_WALLET_ABI_LEGACY, from);
txData = contract.methods.sweep(this.tokenAddress, toAddress).encodeABI();
}
else {
fromAddress = (await this.getPayport(from)).address;
target = this.masterAddress;
const { confirmedBalance } = await this.getBalance(fromAddress);
const balance = this.toBaseDenomination(confirmedBalance);
const contract = this.newContract(TOKEN_WALLET_ABI, this.masterAddress);
const salt = this.getAddressSalt(from);
txData = contract.methods.proxyTransfer(salt, this.tokenAddress, toAddress, balance).encodeABI();
}
const amountOfGas = await this.gasOptionOrEstimate(options, {
from: signerAddress,
to: target,
data: txData
}, 'TOKEN_SWEEP');
const feeOption = await this.resolveFeeOption(options, amountOfGas);
const feeBase = new BigNumber(feeOption.feeBase);
let ethBalance = await this.getEthBaseBalance(signerAddress);
if (feeBase.isGreaterThan(ethBalance)) {
throw new PaymentsError(PaymentsErrorCode.TxInsufficientBalance, `Insufficient ETH balance (${this.toMainDenominationEth(ethBalance)}) at owner address ${signerAddress} `
+ `to sweep contract ${from} with fee of ${feeOption.feeMain} ETH`);
}
const { confirmedBalance: tokenBalanceMain } = await this.getBalance({ address: fromAddress });
const tokenBalanceBase = this.toBaseDenominationBigNumber(tokenBalanceMain);
if (tokenBalanceBase.isLessThan(0)) {
throw new PaymentsError(PaymentsErrorCode.TxInsufficientBalance, `Insufficient token balance (${tokenBalanceMain}) to sweep`);
}
const nonce = options.sequenceNumber || await this.getNextSequenceNumber(signerAddress);
const transactionObject = {
from: signerAddress,
to: target,
data: txData,
value: '0x0',
nonce: `0x${(new BigNumber(nonce)).toString(16)}`,
gasPrice: `0x${(new BigNumber(feeOption.gasPrice)).toString(16)}`,
gas: `0x${amountOfGas.toString(16)}`,
};
return {
status: TransactionStatus.Unsigned,
id: null,
fromAddress,
toAddress,
toExtraId: null,
fromIndex: this.depositKeyIndex,
toIndex: typeof to === 'number' ? to : null,
amount: tokenBalanceMain,
fee: feeOption.feeMain,
targetFeeLevel: feeOption.targetFeeLevel,
targetFeeRate: feeOption.targetFeeRate,
targetFeeRateType: feeOption.targetFeeRateType,
sequenceNumber: nonce.toString(),
weight: amountOfGas,
data: transactionObject,
};
}
getErc20TransferLogAmount(txReceipt) {
const transferLog = txReceipt.logs.find((log) => log.topics[0] === SIGNATURE.LOG_TOPIC0_ERC20_SWEEP);
if (!transferLog) {
this.logger.warn(`Transaction ${txReceipt.transactionHash} was an ERC20 sweep but cannot find log for Transfer event`);
return '0';
}
return this.toMainDenomination(transferLog.data);
}
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`);
}
if (!tx.input) {
throw new Error(`Transaction ${txid} has no input data so it can't be an ERC20 tx`);
}
const currentBlockNumber = await this.getCurrentBlockNumber();
let txReceipt = await this._retryDced(() => this.eth.getTransactionReceipt(txid));
let txBlock = null;
let isConfirmed = false;
let confirmationTimestamp = null;
let confirmations = 0;
if (tx.blockNumber) {
confirmations = currentBlockNumber - tx.blockNumber;
if (confirmations > minConfirmations) {
isConfirmed = true;
txBlock = await this._retryDced(() => this.eth.getBlock(tx.blockNumber));
confirmationTimestamp = new Date(txBlock.timestamp);
}
}
let status = TransactionStatus.Pending;
let isExecuted = false;
if (isConfirmed) {
status = TransactionStatus.Confirmed;
isExecuted = true;
if (txReceipt && txReceipt.hasOwnProperty('status')
&& (txReceipt.status === false || txReceipt.status.toString() === 'false')) {
status = TransactionStatus.Failed;
isExecuted = false;
}
}
let fromAddress = tx.from.toLowerCase();
let toAddress = '';
let amount = '';
if (tx.input.startsWith(SIGNATURE.ERC20_TRANSFER)) {
if ((tx.to || '').toLowerCase() !== this.tokenAddress.toLowerCase()) {
throw new Error(`Transaction ${txid} was sent to different contract: ${tx.to}, Expected: ${this.tokenAddress}`);
}
const tokenDecoder = new InputDataDecoder(TOKEN_METHODS_ABI);
const txData = tokenDecoder.decodeData(tx.input);
toAddress = this.web3.utils.toChecksumAddress(txData.inputs[0]).toLowerCase();
amount = this.toMainDenomination(txData.inputs[1].toString());
if (txReceipt) {
const actualAmount = this.getErc20TransferLogAmount(txReceipt);
if (isExecuted && amount !== actualAmount) {
this.logger.warn(`Transcation ${txid} tried to transfer ${amount} but only ${actualAmount} was actually transferred`);
}
}
}
else if (tx.input.startsWith(SIGNATURE.ERC20_SWEEP_CONTRACT_DEPLOY)
|| tx.input.startsWith(SIGNATURE.ERC20_SWEEP_CONTRACT_DEPLOY_LEGACY)) {
amount = '0';
}
else if (tx.input.startsWith(SIGNATURE.ERC20_PROXY)) {
amount = '0';
}
else if (tx.input.startsWith(SIGNATURE.ERC20_SWEEP)) {
const tokenDecoder = new InputDataDecoder(TOKEN_WALLET_ABI);
const txData = tokenDecoder.decodeData(tx.input);
if (txData.inputs.length !== 4) {
throw new Error(`Transaction ${txid} has not recognized number of inputs ${txData.inputs.length}`);
}
const sweepContractAddress = tx.to;
if (!sweepContractAddress) {
throw new Error(`Transaction ${txid} should have a to address destination`);
}
const addr = deriveAddress(this.masterAddress, `0x${txData.inputs[0].toString('hex')}`, true);
fromAddress = this.web3.utils.toChecksumAddress(addr).toLowerCase();
toAddress = this.web3.utils.toChecksumAddress(txData.inputs[2]).toLowerCase();
if (txReceipt) {
amount = this.getErc20TransferLogAmount(txReceipt);
}
else {
amount = this.toMainDenomination(txData.inputs[3].toString());
}
}
else if (tx.input.startsWith(SIGNATURE.ERC20_SWEEP_LEGACY)) {
const tokenDecoder = new InputDataDecoder(TOKEN_WALLET_ABI_LEGACY);
const txData = tokenDecoder.decodeData(tx.input);
if (txData.inputs.length !== 2) {
throw new Error(`Transaction ${txid} has not recognized number of inputs ${txData.inputs.length}`);
}
const sweepContractAddress = tx.to;
if (!sweepContractAddress) {
throw new Error(`Transaction ${txid} should have a to address destination`);
}
fromAddress = this.web3.utils.toChecksumAddress(sweepContractAddress).toLowerCase();
toAddress = this.web3.utils.toChecksumAddress(txData.inputs[1]).toLowerCase();
if (txReceipt) {
amount = this.getErc20TransferLogAmount(txReceipt);
}
}
else {
throw new Error(`Transaction ${txid} is not ERC20 transaction neither swap`);
}
if (!txReceipt) {
txReceipt = {
transactionHash: tx.hash,
from: tx.from || '',
to: toAddress,
status: true,
blockNumber: 0,
cumulativeGasUsed: 0,
gasUsed: 0,
transactionIndex: 0,
blockHash: '',
logs: [],
logsBloom: ''
};
return {
id: txid,
amount,
toAddress,
fromAddress: tx.from,
toExtraId: null,
fromIndex: null,
toIndex: null,
fee: this.toMainDenominationEth((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,
...txReceipt,
currentBlock: currentBlockNumber
},
};
}
return {
id: txid,
amount,
toAddress,
fromAddress,
toExtraId: null,
fromIndex: null,
toIndex: null,
fee: this.toMainDenominationEth((new BigNumber(tx.gasPrice)).multipliedBy(txReceipt.gasUsed)),
sequenceNumber: tx.nonce,
weight: txReceipt.gasUsed,
isExecuted,
isConfirmed,
confirmations,
confirmationId: tx.blockHash,
confirmationTimestamp,
status,
currentBlockNumber: currentBlockNumber,
data: {
...tx,
...txReceipt,
currentBlock: currentBlockNumber
},
};
}
async getNextSequenceNumber(payport) {
const resolvedPayport = await this.resolvePayport(payport);
const sequenceNumber = await this.gasStation.getNonce(resolvedPayport.address);
return sequenceNumber;
}
async getEthBaseBalance(address) {
const balanceBase = await this._retryDced(() => this.eth.getBalance(address));
return new BigNumber(balanceBase);
}
logTopicToAddress(value) {
return `0x${value.slice(value.length - 40)}`;
}
}
export default BaseErc20Payments;
//# sourceMappingURL=BaseErc20Payments.js.map