import InputDataDecoder from 'ethereum-input-data-decoder' ; import { BigNumber } from 'bignumber.js' ; import type { TransactionReceipt , Transaction } from 'web3-core' ; import Contract from 'web3-eth-contract' ; import { deriveAddress } from './deriveAddress' ; import { BalanceResult , TransactionStatus , ResolveablePayport , Payport , PaymentsError , PaymentsErrorCode , } from '@bitaccess/coinlib-common' ; import { BaseErc20PaymentsConfig , EthereumUnsignedTransaction , EthereumTransactionOptions , EthereumTransactionInfo , } from '../types' ; 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' ; import { Numeric } from '@faast/ts-common' ; export abstract class BaseErc20Payments < Config extends BaseErc20PaymentsConfig > extends BaseEthereumPayments < Config > { public tokenAddress : string ; public depositKeyIndex : number ; public masterAddress : string ; 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 ; } abstract getAddressSalt ( index ) ; abstract getPayport ( index ) ; private 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 as number , 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 ) ; const 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 as number ) ; 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 ) ; const 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 , } ; } private 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 ?. 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 ; } private async getEthBaseBalance ( address ) { const balanceBase = await this . _retryDced ( ( ) => this . eth . getBalance ( address ) ) ; return new BigNumber ( balanceBase ) ; } private logTopicToAddress ( value ) { return `0x${ value . slice ( value . length - 40 ) }` ; } } export default BaseErc20Payments ;