import { Transaction as TronTransaction } from 'tronweb' ; import { cloneDeep } from 'lodash' ; import { BalanceResult , BasePayments , TransactionStatus , FeeLevel , FeeOption , FeeRateType , FeeOptionCustom , ResolvedFeeOption , Payport , FromTo , ResolveablePayport , DerivablePayport , PaymentsError , PaymentsErrorCode , PayportOutput , } from '@bitaccess/coinlib-common' ; import { isType , Numeric } from '@faast/ts-common' ; import { TronTransactionInfo , TronUnsignedTransaction , TronSignedTransaction , TronBroadcastResult , CreateTransactionOptions , BaseTronPaymentsConfig , TronWebTransaction , } from './types' ; import { toBaseDenominationNumber , isValidAddress , toMainDenominationBigNumber } from './helpers' ; import { toError } from './utils' ; import { MIN_BALANCE_SUN , MIN_BALANCE_TRX , DEFAULT_FEE_LEVEL , EXPIRATION_FUDGE_MS , TX_EXPIRATION_EXTENSION_SECONDS , } from './constants' ; import { TronPaymentsUtils } from './TronPaymentsUtils' ; export abstract class BaseTronPayments < Config extends BaseTronPaymentsConfig > extends TronPaymentsUtils implements BasePayments < Config , TronUnsignedTransaction , TronSignedTransaction , TronBroadcastResult , TronTransactionInfo > { abstract getFullConfig ( ) ; abstract getPublicConfig ( ) ; abstract getAccountId ( index ) ; abstract getAccountIds ( ) ; abstract getPayport ( index ) ; abstract getPrivateKey ( index ) ; async init ( ) { } async destroy ( ) { } requiresBalanceMonitor ( ) { return false ; } async getBalance ( resolveablePayport ) { const payport = await this . resolvePayport ( resolveablePayport ) ; return this . getAddressBalance ( payport . address ) ; } async resolveFeeOption ( feeOption ) { let targetFeeLevel ; if ( isType ( FeeOptionCustom , feeOption ) ) { if ( feeOption . feeRate !== '0' ) { throw new Error ( 'tron-payments custom fees are unsupported' ) ; } targetFeeLevel = FeeLevel . Custom ; } else { targetFeeLevel = feeOption . feeLevel || DEFAULT_FEE_LEVEL ; } return { targetFeeLevel , targetFeeRate : '0' , targetFeeRateType : FeeRateType . Base , feeBase : '0' , feeMain : '0' , } ; } async buildUnsignedTx ( toAddress , amountSun , fromAddress ) { let tx = await this . tronweb . transactionBuilder . sendTrx ( toAddress , amountSun , fromAddress ) ; tx = await this . tronweb . transactionBuilder . extendExpiration ( tx , TX_EXPIRATION_EXTENSION_SECONDS ) ; return tx ; } async createServiceTransaction ( ) { return null ; } async createSweepTransaction ( from , to , options = { } ) { this . logger . debug ( 'createSweepTransaction' , from , to ) ; try { const { fromAddress , fromIndex , fromPayport , toAddress , toIndex } = await this . resolveFromTo ( from , to ) ; const { targetFeeLevel , targetFeeRate , targetFeeRateType , feeBase , feeMain } = await this . resolveFeeOption ( options ) ; const feeSun = Number . parseInt ( feeBase ) ; const { confirmedBalance : balanceTrx } = await this . getBalance ( fromPayport ) ; const balanceSun = toBaseDenominationNumber ( balanceTrx ) ; if ( ! this . canSweepBalanceSun ( balanceSun ) ) { throw new Error ( `Insufficient balance (${ balanceTrx }) to sweep with fee of ${ feeMain } ` + `while maintaining a minimum required balance of ${ MIN_BALANCE_TRX }` ) ; } const amountSun = balanceSun - feeSun - MIN_BALANCE_SUN ; const amountTrx = this . toMainDenomination ( amountSun ) ; const tx = await this . buildUnsignedTx ( toAddress , amountSun , fromAddress ) ; return { status : TransactionStatus . Unsigned , id : tx . txID , fromAddress , toAddress , toExtraId : null , fromIndex , toIndex , amount : amountTrx , fee : feeMain , targetFeeLevel , targetFeeRate , targetFeeRateType , sequenceNumber : null , data : tx , } ; } catch ( e ) { throw toError ( e ) ; } } async createTransaction ( from , to , amountTrx , options = { } ) { this . logger . debug ( 'createTransaction' , from , to , amountTrx ) ; try { const { fromAddress , fromIndex , fromPayport , toAddress , toIndex } = await this . resolveFromTo ( from , to ) ; const { targetFeeLevel , targetFeeRate , targetFeeRateType , feeBase , feeMain } = await this . resolveFeeOption ( options ) ; const feeSun = Number . parseInt ( feeBase ) ; const { confirmedBalance : balanceTrx } = await this . getBalance ( fromPayport ) ; const balanceSun = toBaseDenominationNumber ( balanceTrx ) ; const amountSun = toBaseDenominationNumber ( amountTrx ) ; if ( balanceSun - feeSun - MIN_BALANCE_SUN < amountSun ) { throw new Error ( `Insufficient balance (${ balanceTrx }) to send ${ amountTrx } including fee of ${ feeMain } ` + `while maintaining a minimum required balance of ${ MIN_BALANCE_TRX }` ) ; } const tx = await this . buildUnsignedTx ( toAddress , amountSun , fromAddress ) ; return { status : TransactionStatus . Unsigned , id : tx . txID , fromAddress , toAddress , toExtraId : null , fromIndex , toIndex , amount : amountTrx , fee : feeMain , targetFeeLevel , targetFeeRate , targetFeeRateType , sequenceNumber : null , data : tx , } ; } catch ( e ) { throw toError ( e ) ; } } async signTransaction ( unsignedTx ) { try { const fromPrivateKey = await this . getPrivateKey ( unsignedTx . fromIndex ! ) ; const unsignedRaw = cloneDeep ( unsignedTx . data ) as TronWebTransaction ; const signedTx = await this . tronweb . trx . sign ( unsignedRaw , fromPrivateKey ) ; return { ... unsignedTx , status : TransactionStatus . Signed , data : signedTx , } ; } catch ( e ) { throw toError ( e ) ; } } async broadcastTransaction ( tx ) { try { const status = await this . _retryDced ( ( ) => this . tronweb . trx . sendRawTransaction ( tx . data as TronWebTransaction ) ) ; let success = false ; let rebroadcast = false ; if ( status . result || status . code === 'SUCCESS' ) { success = true ; } else { try { await this . _retryDced ( ( ) => this . tronweb . trx . getTransaction ( tx . id ) ) ; success = true ; rebroadcast = true ; } catch ( e ) { const expiration = tx . data && ( tx . data as TronTransaction ) . raw_data . expiration ; if ( expiration && Date . now ( ) > expiration + EXPIRATION_FUDGE_MS ) { throw new PaymentsError ( PaymentsErrorCode . TxExpired , 'Transaction has expired' ) ; } } } if ( success ) { return { id : tx . id , rebroadcast , } ; } else { let statusCode = status . code ; if ( statusCode === 'TRANSACTION_EXPIRATION_ERROR' ) { throw new PaymentsError ( PaymentsErrorCode . TxExpired , `${ statusCode } ${ status . message || '' }` ) ; } if ( statusCode === 'DUP_TRANSACTION_ERROR' ) { statusCode = 'DUP_TX_BUT_TX_NOT_FOUND_SO_PROBABLY_INVALID_TX_ERROR' ; } this . logger . warn ( `Tron broadcast tx unsuccessful ${ tx . id }` , status ) ; throw new Error ( `Failed to broadcast transaction: ${ statusCode } ${ status . message }` ) ; } } catch ( e ) { throw toError ( e ) ; } } isSweepableBalance ( balanceTrx ) { return this . isAddressBalanceSweepable ( balanceTrx ) ; } usesSequenceNumber ( ) { return false ; } async getNextSequenceNumber ( ) { return null ; } usesUtxos ( ) { return false ; } async getUtxos ( ) { return [ ] ; } async resolvePayport ( payport ) { if ( typeof payport === 'number' ) { return this . getPayport ( payport ) ; } else if ( typeof payport === 'string' ) { if ( ! isValidAddress ( payport ) ) { throw new Error ( `Invalid TRON address: ${ payport }` ) ; } return { address : payport } ; } else if ( this . isValidPayport ( payport as any ) ) { return payport as Payport ; } throw new Error ( `Invalid TRON payport: ${ JSON . stringify ( payport ) }` ) ; } 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 createMultiOutputTransaction ( from , to , options = { } ) { return null ; } async createMultiInputTransaction ( from , to , options = { } ) { return null ; } } export default BaseTronPayments ;