Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,11 @@
"@noble/curves": "^1.9.6",
"@solana/wallet-adapter-base": "^0.9.27",
"@solana/web3.js": "^1.98.4",
"@tronweb3/tronwallet-abstract-adapter": "^1.1.9",
"bech32": "^2.0.0",
"bitcoinjs-lib": "^7.0.0-rc.0",
"bs58": "^6.0.0",
"tronweb": "^6.0.4",
"viem": "^2.33.3"
},
"devDependencies": {
Expand Down
376 changes: 374 additions & 2 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

46 changes: 46 additions & 0 deletions src/core/Tron/Tron.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { ChainType, Token } from '@lifi/types'
import type { StepExecutor, StepExecutorOptions } from '../types.js'
import { getTronBalance } from './getTronBalance.js'
import { resolveTronAddress } from './resolveTronAddress.js'
import { TronStepExecutor } from './TronStepExecutor.js'
import type { TronProvider, TronProviderOptions } from './types.js'
import { isValidTronAddress } from './utils.js'

export function Tron(options?: TronProviderOptions): TronProvider {
const _options: TronProviderOptions = options ?? {}
return {
get type() {
return 'TVM' as ChainType
},
isAddress: isValidTronAddress,
resolveAddress: resolveTronAddress,
getBalance: async (walletAddress: string, tokens: Token[]) => {
const balance = await getTronBalance(walletAddress)
return tokens.map((token) => ({
...token,
amount: BigInt(balance),
blockNumber: 0n,
}))
},
async getStepExecutor(options: StepExecutorOptions): Promise<StepExecutor> {
if (!_options.getWallet) {
throw new Error('getWallet is not provided.')
}

const wallet = await _options.getWallet()

const executor = new TronStepExecutor({
wallet,
routeId: options.routeId,
executionOptions: {
...options.executionOptions,
},
})

return executor
},
setOptions(options: TronProviderOptions) {
Object.assign(_options, options)
},
}
}
191 changes: 191 additions & 0 deletions src/core/Tron/TronStepExecutor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import type { Adapter } from '@tronweb3/tronwallet-abstract-adapter'
import { TronWeb } from 'tronweb'
import { config } from '../../config.js'
import { LiFiErrorCode } from '../../errors/constants.js'
import { TransactionError } from '../../errors/errors.js'
import { getStepTransaction } from '../../services/api.js'
import { BaseStepExecutor } from '../BaseStepExecutor.js'
import { checkBalance } from '../checkBalance.js'
import { stepComparison } from '../stepComparison.js'
import type { LiFiStepExtended, TransactionParameters } from '../types.js'
import { waitForDestinationChainTransaction } from '../waitForDestinationChainTransaction.js'
import type { TronStepExecutorOptions } from './types.js'
export class TronStepExecutor extends BaseStepExecutor {
private wallet: Adapter
private tronWeb: TronWeb

constructor(options: TronStepExecutorOptions) {
super(options)
this.wallet = options.wallet
// Initialize TronWeb with default nodes
this.tronWeb = new TronWeb({
fullNode: 'https://api.trongrid.io',
})
}

checkWallet = (step: LiFiStepExtended) => {
// Prevent execution of the quote by wallet different from the one which requested the quote
if (this.wallet.address !== step.action.fromAddress) {
throw new TransactionError(
LiFiErrorCode.WalletChangedDuringExecution,
'The wallet address that requested the quote does not match the wallet address attempting to sign the transaction.'
)
}
}

executeStep = async (step: LiFiStepExtended): Promise<LiFiStepExtended> => {
step.execution = this.statusManager.initExecutionObject(step)

const fromChain = await config.getChainById(step.action.fromChainId)
const toChain = await config.getChainById(step.action.toChainId)

const isBridgeExecution = fromChain.id !== toChain.id
const currentProcessType = isBridgeExecution ? 'CROSS_CHAIN' : 'SWAP'

let process = this.statusManager.findOrCreateProcess({
step,
type: currentProcessType,
chainId: fromChain.id,
})

if (process.status !== 'DONE') {
try {
process = this.statusManager.updateProcess(
step,
process.type,
'STARTED'
)

// Check balance
await checkBalance(step.action.fromAddress!, step)

// Create new transaction
if (!step.transactionRequest) {
// biome-ignore lint/correctness/noUnusedVariables: destructuring
const { execution, ...stepBase } = step
const updatedStep = await getStepTransaction(stepBase)
const comparedStep = await stepComparison(
this.statusManager,
step,
updatedStep,
this.allowUserInteraction,
this.executionOptions
)
Object.assign(step, {
...comparedStep,
execution: step.execution,
})
}

if (!step.transactionRequest?.data) {
throw new TransactionError(
LiFiErrorCode.TransactionUnprepared,
'Unable to prepare transaction.'
)
}

process = this.statusManager.updateProcess(
step,
process.type,
'ACTION_REQUIRED'
)

if (!this.allowUserInteraction) {
return step
}

let transactionRequest: TransactionParameters = {
data: step.transactionRequest.data,
}

if (this.executionOptions?.updateTransactionRequestHook) {
const customizedTransactionRequest: TransactionParameters =
await this.executionOptions.updateTransactionRequestHook({
requestType: 'transaction',
...transactionRequest,
})

transactionRequest = {
...transactionRequest,
...customizedTransactionRequest,
}
}

const transactionRequestData = transactionRequest.data

if (!transactionRequestData) {
throw new TransactionError(
LiFiErrorCode.TransactionUnprepared,
'Unable to prepare transaction.'
)
}

this.checkWallet(step)

// We give users 2 minutes to sign the transaction
const signedTx = await this.wallet.signTransaction(
transactionRequestData // TODO: map formats
)

process = this.statusManager.updateProcess(
step,
process.type,
'PENDING'
)

// Broadcast the signed transaction
const result = await this.tronWeb.trx.sendRawTransaction(signedTx)

if (!result.result) {
throw new TransactionError(
LiFiErrorCode.TransactionFailed,
`Transaction failed: ${result.message || 'Unknown error'}`
)
}

// Transaction has been confirmed and we can update the process
process = this.statusManager.updateProcess(
step,
process.type,
'PENDING',
{
txHash: result.txid,
txLink: `${fromChain.metamask.blockExplorerUrls[0]}tx/${result.txid}`,
}
)

if (isBridgeExecution) {
process = this.statusManager.updateProcess(step, process.type, 'DONE')
}
} catch (e: any) {
const error = new TransactionError(
LiFiErrorCode.TransactionFailed,
`Transaction failed: ${e.message || 'Unknown error'}`
)
process = this.statusManager.updateProcess(
step,
process.type,
'FAILED',
{
error: {
message: error.message,
code: error.code,
},
}
)
this.statusManager.updateExecution(step, 'FAILED')
throw error
}
}

await waitForDestinationChainTransaction(
step,
process,
fromChain,
toChain,
this.statusManager
)

return step
}
}
20 changes: 20 additions & 0 deletions src/core/Tron/getTronBalance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { TronWeb } from 'tronweb'

/**
* Get the balance of a Tron address
* @param address - The Tron address to get balance for
* @returns The balance in sun (smallest unit)
*/
export async function getTronBalance(address: string): Promise<number> {
try {
const tronWeb = new TronWeb({
fullNode: 'https://api.trongrid.io',
})

// https://tronweb.network/docu/docs/API%20List/trx/getBalance/
return await tronWeb.trx.getBalance(address)
} catch (error) {
console.error('Error getting Tron balance:', error)
return 0
}
}
13 changes: 13 additions & 0 deletions src/core/Tron/resolveTronAddress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { isValidTronAddress } from './utils.js'

/**
* Resolve a Tron address - validates and returns the address
* @param address - The address to resolve
* @returns The resolved address
*/
export async function resolveTronAddress(address: string): Promise<string> {
if (!isValidTronAddress(address)) {
throw new Error(`Invalid Tron address: ${address}`)
}
return address
}
19 changes: 19 additions & 0 deletions src/core/Tron/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { ChainType } from '@lifi/types'
import type { Adapter } from '@tronweb3/tronwallet-abstract-adapter'
import type { SDKProvider, StepExecutorOptions } from '../types.js'

export interface TronProviderOptions {
getWallet?: () => Promise<Adapter>
}

export interface TronProvider extends SDKProvider {
setOptions: (options: TronProviderOptions) => void
}

export function isTron(provider: SDKProvider): provider is TronProvider {
return provider.type === ('TVM' as ChainType)
}

export interface TronStepExecutorOptions extends StepExecutorOptions {
wallet: Adapter
}
50 changes: 50 additions & 0 deletions src/core/Tron/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { TronWeb } from 'tronweb'

/**
* Check if the given address is a valid Tron address
* @param address - The address to validate
* @returns True if the address is valid, false otherwise
*/
export function isValidTronAddress(address: string): boolean {
try {
return TronWeb.isAddress(address)
} catch {
return false
}
}

/**
* Convert a Tron address to its hex format
* @param address - The Tron address to convert
* @returns The hex format of the address
*/
export function tronAddressToHex(address: string): string {
return TronWeb.address.toHex(address)
}

/**
* Convert a hex address to Tron address format
* @param hexAddress - The hex address to convert
* @returns The Tron address format
*/
export function hexToTronAddress(hexAddress: string): string {
return TronWeb.address.fromHex(hexAddress)
}

/**
* Get the base58 address from a hex address
* @param hexAddress - The hex address
* @returns The base58 address
*/
export function getBase58Address(hexAddress: string): string {
return TronWeb.address.fromHex(hexAddress)
}

/**
* Get the hex address from a base58 address
* @param base58Address - The base58 address
* @returns The hex address
*/
export function getHexAddress(base58Address: string): string {
return TronWeb.address.toHex(base58Address)
}
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ export { StatusManager } from './core/StatusManager.js'
export { Sui } from './core/Sui/Sui.js'
export type { SuiProvider, SuiProviderOptions } from './core/Sui/types.js'
export { isSui } from './core/Sui/types.js'
export { Tron } from './core/Tron/Tron.js'
export type { TronProvider, TronProviderOptions } from './core/Tron/types.js'
export { isTron } from './core/Tron/types.js'
export type {
AcceptExchangeRateUpdateHook,
AcceptSlippageUpdateHook,
Expand Down
4 changes: 2 additions & 2 deletions tests/setup.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { createConfig } from '../src/createConfig.js'
import { EVM, Solana, Sui, UTXO } from '../src/index.js'
import { EVM, Solana, Sui, UTXO, Tron } from '../src/index.js'

export const setupTestEnvironment = () => {
createConfig({
integrator: 'lifi-sdk',
providers: [EVM(), Solana(), UTXO(), Sui()],
providers: [EVM(), Solana(), UTXO(), Sui(), Tron()],
})
}