@faast/tron-payments
Version:
Library to assist in processing tron payments, such as deriving addresses and sweeping funds
242 lines (206 loc) • 7.24 kB
text/typescript
import {
PaymentsUtils, NetworkType, Payport, AutoFeeLevels, FeeRate, FeeRateType, BalanceResult, TransactionStatus
} from '@faast/payments-common'
import { Logger, DelegateLogger, isNil, assertType, Numeric } from '@faast/ts-common'
import TronWeb, { Transaction as TronTransaction } from 'tronweb'
import {
toMainDenominationString,
toBaseDenominationString,
isValidXprv,
isValidXpub,
isValidAddress,
isValidExtraId,
isValidPrivateKey,
privateKeyToAddress,
toMainDenominationBigNumber,
} from './helpers'
import {
COIN_NAME,
COIN_SYMBOL,
DECIMAL_PLACES,
DEFAULT_EVENT_SERVER,
DEFAULT_FULL_NODE,
DEFAULT_SOLIDITY_NODE,
MIN_BALANCE_SUN,
MIN_BALANCE_TRX,
PACKAGE_NAME,
} from './constants'
import { BaseTronPaymentsConfig, TronTransactionInfo } from './types'
import { retryIfDisconnected, toError } from './utils'
import BigNumber from 'bignumber.js'
import { pick } from 'lodash'
export class TronPaymentsUtils implements PaymentsUtils {
readonly coinSymbol = COIN_SYMBOL
readonly coinName = COIN_NAME
readonly coinDecimals = DECIMAL_PLACES
readonly networkType: NetworkType
logger: Logger
fullNode: string
solidityNode: string
eventServer: string
tronweb: TronWeb
constructor(config: BaseTronPaymentsConfig = {}) {
assertType(BaseTronPaymentsConfig, config)
this.networkType = config.network || NetworkType.Mainnet
this.logger = new DelegateLogger(config.logger, PACKAGE_NAME)
this.fullNode = config.fullNode || DEFAULT_FULL_NODE
this.solidityNode = config.solidityNode || DEFAULT_SOLIDITY_NODE
this.eventServer = config.eventServer || DEFAULT_EVENT_SERVER
this.tronweb = new TronWeb(this.fullNode, this.solidityNode, this.eventServer)
}
async init() {}
async destroy() {}
isValidExtraId(extraId: string): boolean {
return isValidExtraId(extraId)
}
isValidAddress(address: string): boolean {
return isValidAddress(address)
}
standardizeAddress(address: string): string | null {
if (!isValidAddress(address)) {
return null
}
return address
}
private async _getPayportValidationMessage(payport: Payport): Promise<string | undefined> {
const { address, extraId } = payport
if (!isValidAddress(address)) {
return 'Invalid payport address'
}
if (!isNil(extraId) && !isValidExtraId(extraId)) {
return 'Invalid payport extraId'
}
}
async getPayportValidationMessage(payport: Payport): Promise<string | undefined> {
try {
payport = assertType(Payport, payport, 'payport')
} catch (e) {
return e.message
}
return this._getPayportValidationMessage(payport)
}
async validatePayport(payport: Payport): Promise<void> {
payport = assertType(Payport, payport, 'payport')
const message = await this._getPayportValidationMessage(payport)
if (message) {
throw new Error(message)
}
}
async isValidPayport(payport: Payport): Promise<boolean> {
return Payport.is(payport) && !(await this._getPayportValidationMessage(payport))
}
toMainDenomination(amount: string | number): string {
return toMainDenominationString(amount)
}
toBaseDenomination(amount: string | number): string {
return toBaseDenominationString(amount)
}
isValidXprv = isValidXprv
isValidXpub = isValidXpub
isValidPrivateKey = isValidPrivateKey
privateKeyToAddress = privateKeyToAddress
getFeeRateRecommendation(level: AutoFeeLevels): FeeRate {
return { feeRate: '0', feeRateType: FeeRateType.Base }
}
async _retryDced<T>(fn: () => Promise<T>): Promise<T> {
return retryIfDisconnected(fn, this.logger)
}
getCurrentBlockNumber() {
return this._retryDced(async () => (await this.tronweb.trx.getCurrentBlock()).block_header.raw_data.number)
}
async getAddressUtxos() {
return []
}
async getAddressNextSequenceNumber() {
return null
}
protected canSweepBalanceSun(balanceSun: number): boolean {
return balanceSun > MIN_BALANCE_SUN
}
isAddressBalanceSweepable(balanceTrx: Numeric): boolean {
return new BigNumber(balanceTrx).gt(MIN_BALANCE_TRX)
}
async getAddressBalance(address: string): Promise<BalanceResult> {
try {
const balanceSun = await this._retryDced(() => this.tronweb.trx.getBalance(address))
const sweepable = this.canSweepBalanceSun(balanceSun)
const confirmedBalance = toMainDenominationBigNumber(balanceSun)
const spendableBalance = BigNumber.max(0, confirmedBalance.minus(MIN_BALANCE_TRX))
return {
confirmedBalance: confirmedBalance.toString(),
unconfirmedBalance: '0',
spendableBalance: spendableBalance.toString(),
sweepable,
requiresActivation: false,
minimumBalance: String(MIN_BALANCE_TRX),
}
} catch (e) {
throw toError(e)
}
}
private extractTxFields(tx: TronTransaction) {
const contractParam = tx.raw_data?.contract?.[0]?.parameter?.value ?? null
if (!(contractParam && typeof contractParam.amount === 'number')) {
throw new Error('Unable to get transaction')
}
const amountSun = contractParam.amount || 0
const amountTrx = this.toMainDenomination(amountSun)
const toAddress = this.tronweb.address.fromHex(contractParam.to_address)
const fromAddress = this.tronweb.address.fromHex(contractParam.owner_address)
return {
amountTrx,
amountSun,
toAddress,
fromAddress,
}
}
async getTransactionInfo(txid: string): Promise<TronTransactionInfo> {
try {
const [tx, txInfo, currentBlock] = await Promise.all([
this._retryDced(() => this.tronweb.trx.getTransaction(txid)),
this._retryDced(() => this.tronweb.trx.getTransactionInfo(txid)),
this._retryDced(() => this.tronweb.trx.getCurrentBlock()),
])
const { amountTrx, fromAddress, toAddress } = this.extractTxFields(tx)
const contractRet = tx.ret?.[0]?.contractRet
const isExecuted = contractRet === 'SUCCESS'
const block = txInfo.blockNumber || null
const feeTrx = this.toMainDenomination(txInfo.fee || 0)
const currentBlockNumber = currentBlock.block_header?.raw_data?.number ?? 0
const confirmations = currentBlockNumber && block ? currentBlockNumber - block : 0
const isConfirmed = confirmations > 0
const confirmationTimestamp = txInfo.blockTimeStamp ? new Date(txInfo.blockTimeStamp) : null
let status: TransactionStatus = TransactionStatus.Pending
if (isConfirmed) {
if (!isExecuted) {
status = TransactionStatus.Failed
}
status = TransactionStatus.Confirmed
}
return {
id: tx.txID,
amount: amountTrx,
toAddress,
fromAddress,
toExtraId: null,
fromIndex: null,
toIndex: null,
fee: feeTrx,
sequenceNumber: null,
isExecuted,
isConfirmed,
confirmations,
confirmationId: block ? String(block) : null,
confirmationTimestamp,
status,
data: {
...tx,
...txInfo,
currentBlock: pick(currentBlock, 'block_header', 'blockID'),
},
}
} catch (e) {
throw toError(e)
}
}
}