import Web3 from 'web3' ; import { PaymentsUtils , Payport , createUnitConverters , AutoFeeLevels , FeeRate , FeeRateType , NetworkType , BalanceResult , TransactionStatus , BlockInfo , } from '@bitaccess/coinlib-common' ; import { Logger , DelegateLogger , assertType , isNull , Numeric , isUndefined , isNumber } from '@faast/ts-common' ; import BigNumber from 'bignumber.js' ; import { Transaction , TransactionReceipt } from 'web3-core' ; import { PACKAGE_NAME , ETH_DECIMAL_PLACES , ETH_NAME , ETH_SYMBOL , DEFAULT_ADDRESS_FORMAT , MIN_SWEEPABLE_WEI , MIN_CONFIRMATIONS , } from './constants' ; import { EthereumAddressFormat , EthereumAddressFormatT , EthereumPaymentsUtilsConfig , EthereumTransactionInfo } from './types' ; import { isValidXkey } from './bip44' ; import { NetworkData } from './NetworkData' ; import { retryIfDisconnected } from './utils' ; type UnitConverters = ReturnType < typeof createUnitConverters > ; export class EthereumPaymentsUtils implements PaymentsUtils { readonly networkType : NetworkType ; readonly coinSymbol : string ; readonly coinName : string ; readonly coinDecimals : number ; logger : Logger ; server : string | null ; web3 : Web3 ; eth : Web3 [ 'eth' ] ; gasStation : NetworkData ; constructor ( config ) { this . logger = new DelegateLogger ( config . logger , PACKAGE_NAME ) ; this . networkType = config . network || NetworkType . Mainnet ; this . coinName = config . name ?? ETH_NAME ; this . coinSymbol = config . symbol ?? ETH_SYMBOL ; this . coinDecimals = config . decimals ?? ETH_DECIMAL_PLACES ; this . server = config . fullNode || null ; let provider ; if ( config . web3 ) { this . web3 = config . web3 ; } else if ( isNull ( this . server ) ) { this . web3 = new Web3 ( ) ; } else if ( this . server . startsWith ( 'http' ) ) { provider = new Web3 . providers . HttpProvider ( this . server , config . providerOptions ) ; this . web3 = new Web3 ( provider ) ; } else if ( this . server . startsWith ( 'ws' ) ) { provider = new Web3 . providers . WebsocketProvider ( this . server , config . providerOptions ) ; this . web3 = new Web3 ( provider ) ; } else { throw new Error ( `Invalid ethereum payments fullNode, must start with http or ws: ${ this . server }` ) ; } if ( provider && process . env . NODE_DEBUG && process . env . NODE_DEBUG . includes ( 'ethereum-payments' ) ) { const send = provider . send ; provider . send = ( payload , cb ) => { this . logger . debug ( `web3 provider request ${ this . server }` , payload ) ; send . call ( provider , payload , ( error , result ) => { if ( error ) { this . logger . debug ( `web3 provider response error ${ this . server }` , error ) ; } else { this . logger . debug ( `web3 provider response result ${ this . server }` , result ) ; } cb ( error , result ) ; } ) ; } ; } this . eth = this . web3 . eth ; this . gasStation = new NetworkData ( this . eth , config . gasStation , config . parityNode , this . logger ) ; const unitConverters = createUnitConverters ( this . coinDecimals ) ; this . toMainDenominationBigNumber = unitConverters . toMainDenominationBigNumber ; this . toBaseDenominationBigNumber = unitConverters . toBaseDenominationBigNumber ; this . toMainDenomination = unitConverters . toMainDenominationString ; this . toBaseDenomination = unitConverters . toBaseDenominationString ; const ethUnitConverters = createUnitConverters ( ETH_DECIMAL_PLACES ) ; this . toMainDenominationBigNumberEth = ethUnitConverters . toMainDenominationBigNumber ; this . toBaseDenominationBigNumberEth = ethUnitConverters . toBaseDenominationBigNumber ; this . toMainDenominationEth = ethUnitConverters . toMainDenominationString ; this . toBaseDenominationEth = ethUnitConverters . toBaseDenominationString ; } async init ( ) { } async destroy ( ) { } toMainDenominationBigNumber : UnitConverters [ 'toMainDenominationBigNumber' ] ; toBaseDenominationBigNumber : UnitConverters [ 'toMainDenominationBigNumber' ] ; toMainDenomination : UnitConverters [ 'toMainDenominationString' ] ; toBaseDenomination : UnitConverters [ 'toBaseDenominationString' ] ; toMainDenominationBigNumberEth : UnitConverters [ 'toMainDenominationBigNumber' ] ; toBaseDenominationBigNumberEth : UnitConverters [ 'toMainDenominationBigNumber' ] ; toMainDenominationEth : UnitConverters [ 'toMainDenominationString' ] ; toBaseDenominationEth : UnitConverters [ 'toBaseDenominationString' ] ; isValidAddress ( address , options = { } ) { const { format } = options ; if ( format === EthereumAddressFormat . Lowercase ) { return this . web3 . utils . isAddress ( address ) && address === address . toLowerCase ( ) ; } else if ( format === EthereumAddressFormat . Checksum ) { return this . web3 . utils . checkAddressChecksum ( address ) ; } return this . web3 . utils . isAddress ( address ) ; } standardizeAddress ( address , options ? ) { if ( ! this . web3 . utils . isAddress ( address ) ) { return null ; } const format = assertType ( EthereumAddressFormatT , options ?. format ?? DEFAULT_ADDRESS_FORMAT , 'format' ) ; if ( format === EthereumAddressFormat . Lowercase ) { return address . toLowerCase ( ) ; } else { return this . web3 . utils . toChecksumAddress ( address ) ; } } isValidExtraId ( extraId ) { return false ; } isValidPayport ( payport ) { return Payport . is ( payport ) && ! this . _getPayportValidationMessage ( payport ) ; } validatePayport ( payport ) { const message = this . _getPayportValidationMessage ( payport ) ; if ( message ) { throw new Error ( message ) ; } } getPayportValidationMessage ( payport ) { try { payport = assertType ( Payport , payport , 'payport' ) ; } catch ( e ) { return e . message ; } return this . _getPayportValidationMessage ( payport ) ; } isValidXprv ( xprv ) { return isValidXkey ( xprv ) && xprv . substring ( 0 , 4 ) === 'xprv' ; } isValidXpub ( xpub ) { return isValidXkey ( xpub ) && xpub . substring ( 0 , 4 ) === 'xpub' ; } isValidPrivateKey ( prv ) { try { return Boolean ( this . web3 . eth . accounts . privateKeyToAccount ( prv ) ) ; } catch ( e ) { return false ; } } privateKeyToAddress ( prv ) { let key ; if ( prv . substring ( 0 , 2 ) === '0x' ) { key = prv ; } else { key = `0x${ prv }` ; } return this . web3 . eth . accounts . privateKeyToAccount ( key ) . address . toLowerCase ( ) ; } private _getPayportValidationMessage ( payport ) { try { const { address } = payport ; if ( ! ( this . isValidAddress ( address ) ) ) { return 'Invalid payport address' ; } } catch ( e ) { return 'Invalid payport address' ; } return undefined ; } async getFeeRateRecommendation ( level ) { const gasPrice = await this . gasStation . getGasPrice ( level ) ; return { feeRate : gasPrice , feeRateType : FeeRateType . BasePerWeight , } ; } async _retryDced < T > ( fn ) { return retryIfDisconnected ( fn , this . logger ) ; } async getCurrentBlockNumber ( ) { return this . _retryDced ( ( ) => this . eth . getBlockNumber ( ) ) ; } isAddressBalanceSweepable ( balanceEth ) { return this . toBaseDenominationBigNumberEth ( balanceEth ) . gt ( MIN_SWEEPABLE_WEI ) ; } async getAddressBalance ( address ) { const balance = await this . _retryDced ( ( ) => this . eth . getBalance ( address ) ) ; const confirmedBalance = this . toMainDenomination ( balance ) . toString ( ) ; const sweepable = this . isAddressBalanceSweepable ( confirmedBalance ) ; return { confirmedBalance , unconfirmedBalance : '0' , spendableBalance : confirmedBalance , sweepable , requiresActivation : false , } ; } async getAddressNextSequenceNumber ( address ) { return this . gasStation . getNonce ( address ) ; } async getAddressUtxos ( ) { return [ ] ; } 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` ) ; } const currentBlockNumber = await this . getCurrentBlockNumber ( ) ; let txInfo = await this . _retryDced ( ( ) => this . eth . getTransactionReceipt ( txid ) ) ; tx . from = tx . from ? tx . from . toLowerCase ( ) : '' ; tx . to = tx . to ? tx . to . toLowerCase ( ) : '' ; if ( ! txInfo ) { txInfo = { transactionHash : tx . hash , from : tx . from , to : tx . to , status : true , blockNumber : 0 , cumulativeGasUsed : 0 , gasUsed : 0 , transactionIndex : 0 , blockHash : '' , logs : [ ] , logsBloom : '' } ; return { id : txid , amount : this . toMainDenomination ( tx . value ) , toAddress : tx . to ? tx . to . toLowerCase ( ) : null , fromAddress : tx . from ? tx . from . toLowerCase ( ) : null , toExtraId : null , fromIndex : null , toIndex : null , fee : this . toMainDenomination ( ( 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 , ... txInfo , currentBlock : currentBlockNumber } , } ; } let isConfirmed = false ; let confirmationTimestamp = null ; let confirmations = 0 ; if ( tx . blockNumber ) { confirmations = currentBlockNumber - tx . blockNumber ; if ( confirmations > minConfirmations ) { isConfirmed = true ; const txBlock = await this . _retryDced ( ( ) => this . eth . getBlock ( tx . blockNumber ! ) ) ; confirmationTimestamp = new Date ( Number ( txBlock . timestamp ) * 1000 ) ; } } let status = TransactionStatus . Pending ; if ( isConfirmed ) { status = TransactionStatus . Confirmed ; if ( txInfo && ( txInfo ?. status === false || txInfo . status . toString ( ) === 'false' ) ) { status = TransactionStatus . Failed ; } } txInfo . from = tx . from ; txInfo . to = tx . to ; return { id : txid , amount : this . toMainDenomination ( tx . value ) , toAddress : tx . to ? tx . to . toLowerCase ( ) : null , fromAddress : tx . from ? tx . from . toLowerCase ( ) : null , toExtraId : null , fromIndex : null , toIndex : null , fee : this . toMainDenomination ( ( new BigNumber ( tx . gasPrice ) ) . multipliedBy ( txInfo . gasUsed ) ) , sequenceNumber : tx . nonce , weight : txInfo . gasUsed , isExecuted : status !== TransactionStatus . Failed , isConfirmed , confirmations , confirmationId : tx . blockHash , confirmationTimestamp , status , currentBlockNumber : currentBlockNumber , data : { ... tx , ... txInfo , currentBlock : currentBlockNumber } , } ; } async getBlock ( id ? ) { const raw = await this . eth . getBlock ( id ?? 'latest' ) ; return { id : raw . hash , height : raw . number , previousId : raw . parentHash , time : new Date ( isNumber ( raw . timestamp ) ? raw . timestamp * 1000 : raw . timestamp ) , raw : raw } ; } }