UNPKG

@faast/tron-payments

Version:

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

318 lines (293 loc) 10.4 kB
import { Transaction as TronTransaction } from 'tronweb' import { cloneDeep } from 'lodash' import { BalanceResult, BasePayments, TransactionStatus, FeeLevel, FeeOption, FeeRateType, FeeOptionCustom, ResolvedFeeOption, Payport, FromTo, ResolveablePayport, PaymentsError, PaymentsErrorCode, PayportOutput, } from '@faast/payments-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> { // You may notice that many function blocks are enclosed in a try/catch. // I had to do this because tronweb thinks it's a good idea to throw // strings instead of Errors and now we need to convert them all ourselves // to be consistent. constructor(config: Config) { super(config) } abstract getFullConfig(): Config abstract getPublicConfig(): Config abstract getAccountId(index: number): string abstract getAccountIds(): string[] abstract getPayport(index: number): Promise<Payport> abstract getPrivateKey(index: number): Promise<string> async init() {} async destroy() {} requiresBalanceMonitor() { return false } async getBalance(resolveablePayport: ResolveablePayport): Promise<BalanceResult> { const payport = await this.resolvePayport(resolveablePayport) return this.getAddressBalance(payport.address) } async resolveFeeOption(feeOption: FeeOption): Promise<ResolvedFeeOption> { let targetFeeLevel: FeeLevel 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: string, amountSun: number, fromAddress: string): Promise<TronTransaction> { 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(): Promise<null> { return null } async createSweepTransaction( from: number, to: ResolveablePayport, options: CreateTransactionOptions = {}, ): Promise<TronUnsignedTransaction> { 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: number, to: ResolveablePayport, amountTrx: string, options: CreateTransactionOptions = {}, ): Promise<TronUnsignedTransaction> { 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: TronUnsignedTransaction): Promise<TronSignedTransaction> { try { const fromPrivateKey = await this.getPrivateKey(unsignedTx.fromIndex) const unsignedRaw = cloneDeep(unsignedTx.data) as TronWebTransaction // tron modifies unsigned object const signedTx = await this.tronweb.trx.sign(unsignedRaw, fromPrivateKey) return { ...unsignedTx, status: TransactionStatus.Signed, data: signedTx, } } catch (e) { throw toError(e) } } async broadcastTransaction(tx: TronSignedTransaction): Promise<TronBroadcastResult> { /* * I’ve discovered that tron nodes like to “remember” every transaction you give it. * If you try broadcasting an invalid TX the first time you’ll get a `SIGERROR` but * every subsequent broadcast gives a `DUP_TRANSACTION_ERROR`. Which is the exact same * error you get after rebroadcasting a valid transaction. And to make things worse, * if you try to look up the invalid transaction by ID it tells you `Transaction not found`. * So in order to actually determine the status of a broadcast the logic becomes: * `success status` -> broadcast succeeded * `error status` -> broadcast failed * `(DUP_TRANSACTION_ERROR && Transaction found)` -> tx already broadcast * `(DUP_TRANASCTION_ERROR && Transaction not found)` -> tx was probably invalid? Maybe? Who knows… */ 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: string | undefined = 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: Numeric): boolean { return this.isAddressBalanceSweepable(balanceTrx) } usesSequenceNumber() { return false } async getNextSequenceNumber() { return null } usesUtxos() { return false } async getUtxos() { return [] } // HELPERS async resolvePayport(payport: ResolveablePayport): Promise<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 } } if (!this.isValidPayport(payport)) { throw new Error(`Invalid TRON payport: ${JSON.stringify(payport)}`) } return payport } async resolveFromTo(from: number, to: ResolveablePayport): Promise<FromTo> { 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: number, to: PayportOutput[], options: CreateTransactionOptions = {}, ): Promise<null> { return null } } export default BaseTronPayments