import * as TonWeb from 'tonweb'; import { type TonConnectUI } from '@tonconnect/ui'; import { BaseStepExecutor } from '../BaseStepExecutor.js'; import type { LiFiStepExtended, StepExecutorOptions } from '../types.js'; import { TON_NATIVE_ADDRESS } from '../../utils/constants.js'; import { parseTVMErrors } from './parseTVMErrors.js'; import { ChainId } from '../../types/base.js'; import type { RoutesRainbowParams } from '../../routes/Ton/Rainbow/types.js'; import { getRoutesRainbow } from '../../routes/Ton/Rainbow/index.js'; import type { RoutesSwapCoffeeParams } from '../../routes/Ton/SwapCoffee/types.js'; import { getRoutesSwapCoffee } from '../../routes/Ton/SwapCoffee/index.js'; export interface TVMStepExecutorOptions extends StepExecutorOptions { walletAdapter: TonConnectUI; } export class TVMStepExecutor extends BaseStepExecutor { private walletAdapter: TonConnectUI; private slippage?: string; constructor(options: TVMStepExecutorOptions) { super(options); this.walletAdapter = options.walletAdapter; this.slippage = options.executionOptions?.slippage; } async executeStep(step: LiFiStepExtended): Promise { step.execution = this.statusManager.initExecutionObject(step); const currentProcessType = 'SWAP'; let process = this.statusManager.findOrCreateProcess({ step, type: currentProcessType, }); try { process = this.statusManager.updateProcess(step, process.type, 'STARTED'); let messages = []; let inputUsd = 0; if (step.tool === 'swapcoffee') { const swapCoffeeRoutes = await getRoutesSwapCoffee({ fromToken: step.action.fromToken.address || '', toToken: step.action.toToken.address || '', fromAmount: step.action.fromAmount, isBuildTx: true, slippage: this.slippage, senderAddress: this.walletAdapter.account?.address!!, fromTokenDecimals: step.action.fromToken.decimals, } as RoutesSwapCoffeeParams).catch((e) => { console.error('>>> e', e); process = this.statusManager.updateProcess( step, process.type, 'FAILED', { error: { code: -1, message: e.message, }, }, ); }); for (const transaction of (swapCoffeeRoutes as any)?.swapMessages.data .transactions) { messages.push({ address: transaction.address, amount: transaction.value, payload: transaction.cell, }); } inputUsd = (swapCoffeeRoutes as any).inputUsd; } else if (step.tool === 'rainbow') { const rainbowRoutes = await getRoutesRainbow({ fromToken: step.action.fromToken.address, toToken: step.action.toToken.address, fromAmount: step.action.fromAmount, isBuildTx: true, slippage: this.slippage, senderAddress: this.walletAdapter.account?.address!!, } as RoutesRainbowParams).catch((e) => { console.error('>>> e', e); process = this.statusManager.updateProcess( step, process.type, 'FAILED', { error: { code: -1, message: e.message, }, }, ); }); messages = (rainbowRoutes as any).swapMessages; inputUsd = (rainbowRoutes as any).inputUsd; } const tonToken = await fetch( 'https://tokens.swap.coffee/api/v1/tokens/1/tokens', ); const tonData = await tonToken.json(); const nativeTokenPrice = tonData.find( (token: any) => token.address === TON_NATIVE_ADDRESS, ).price_usd; // Nimbus will take 1% volume fee const nimbusCommission = Math.floor( ((0.01 * inputUsd) / Number(nativeTokenPrice)) * 10 ** 9, ); messages.push({ address: 'UQC7j89MucLopXTiGSThTJKKfheY75rwvNadA2odSkIUIiki', // move to env later amount: nimbusCommission, }); process = this.statusManager.updateProcess(step, process.type, 'PENDING'); try { // just send the transaction to the user const txnResponse = await this.walletAdapter.sendTransaction({ validUntil: Date.now() + 5 * 60 * 1000, // 5 minutes messages: messages as any, }); const txHash = await bocToHash(txnResponse.boc); process = this.statusManager.updateProcess( step, process.type, 'PENDING', { chainId: ChainId.TON, txHash: txHash, txLink: `https://tonviewer.com/transaction/${txHash}`, }, ); await waitForTransaction(txHash, 90 * 1000); // wait up to 1 minute 30 seconds for transaction } catch (e: any) { const error = await parseTVMErrors(e, step, process); process = this.statusManager.updateProcess( step, process.type, 'FAILED', { error: { message: error?.cause?.message, code: error?.code, }, }, ); this.statusManager.updateExecution(step, 'FAILED'); throw error; } process = this.statusManager.updateProcess(step, process.type, 'DONE'); this.statusManager.updateExecution(step, 'DONE'); return step; } catch (e: any) { process = this.statusManager.updateProcess(step, process.type, 'FAILED', { error: { code: e?.cause?.code, message: e?.cause?.message, }, }); return step; } } } const bocToHash = async (boc: string) => { // @ts-ignore const bocCell = TonWeb.boc.Cell.oneFromBoc(TonWeb.utils.base64ToBytes(boc)); // @ts-ignore const hash = TonWeb.utils.bytesToBase64(await bocCell.hash()); return hash; }; // Function to fetch transaction status from tonapi async function getTransactionStatus(txId: string) { try { const response = await fetch( `https://tonapi.io/v2/blockchain/transactions/${txId}`, ); const data = await response.json(); return data; } catch (error) { console.error('Error fetching transaction status:', error); return null; } } async function waitForTransaction(txId: string, timeout: number) { return new Promise((resolve, reject) => { const intervalTime = 2000; // Check every 2 seconds const startTime = Date.now(); const interval = setInterval(async () => { try { const transaction = await getTransactionStatus(txId); if (transaction && transaction.success) { // Check if transaction is confirmed clearInterval(interval); resolve(transaction); } if (Date.now() - startTime >= timeout) { clearInterval(interval); reject(new Error('Transaction not confirmed within timeout period')); } } catch (error) { clearInterval(interval); reject(error); } }, intervalTime); }); }