UNPKG

@faast/ethereum-payments

Version:

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

339 lines 15.7 kB
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