import { SuiClient } from '@mysten/sui/client'; import { BaseStepExecutor } from '../BaseStepExecutor.js'; import type { LiFiStepExtended, StepExecutorOptions } from '../types.js'; import { parseMVMErrors } from './parseMVMErrors.js'; import { nimbusGetPrice } from '../../services/nimbus.js'; import { MOVE_NATIVE_ADDRESS } from '../../utils/constants.js'; import type { NimbusPricePrice } from '../../types/nimbus.js'; import { ChainId } from '../../types/base.js'; import { MVM_RPC_URL, SHIO_DEFAULT_RPC_URL } from '../../utils/mvm.js'; import { EstimateFee, AppendCoinToTip, ShioFastRpcUrl } from 'shio-fast-sdk'; import type { Transaction } from '@mysten/sui/transactions'; import type { RoutesAftermathParams } from '../../routes/Sui/Aftermath/types.js'; import { getRoutesAftermath } from '../../routes/Sui/Aftermath/index.js'; import type { RoutesCetusParams } from '../../routes/Sui/Cetus/index.js'; import { getRoutesCetus } from '../../routes/Sui/Cetus/index.js'; import type { RoutesNaviParams } from '../../routes/Sui/Navi/types.js'; import { getRoutesNavi } from '../../routes/Sui/Navi/index.js'; import type { RoutesSeventKParams } from '../../routes/Sui/SeventK/types.js'; import { getRoutesSeventK } from '../../routes/Sui/SeventK/index.js'; import type { RoutesFlowXParams } from '../../routes/Sui/FlowX/types.js'; import { getRoutesFlowX } from '../../routes/Sui/FlowX/index.js'; // import type { RoutesHopParams } from '../../routes/Sui/Hop/types.js'; // import { getRoutesHop } from '../../routes/Sui/Hop/index.js'; export interface MVMStepExecutorOptions extends StepExecutorOptions { walletAdapter: any; mevProtection?: boolean; transactionMode?: 'default' | 'fast'; customGasPrice?: string; maxGasCap?: string; } const WALLET_ADDRESS = '0x4b8e9932393d1a73a9afeb253e6bdc6ea6f40cb72fc3ea174c233912043c1e36'; export class MVMStepExecutor extends BaseStepExecutor { private walletAdapter: any; private commissionBps?: Partial>; private commissionBpsSDK?: Partial>; private slippage?: string; private mevProtection?: boolean; private transactionMode?: 'default' | 'fast'; private customGasPrice?: string; private maxGasCap?: string; constructor(options: MVMStepExecutorOptions) { super(options); this.walletAdapter = options.walletAdapter; this.commissionBps = options.executionOptions?.commissionBps; this.commissionBpsSDK = options.executionOptions?.commissionBpsSDK; this.slippage = options.executionOptions?.slippage; this.mevProtection = options.mevProtection; this.transactionMode = options.transactionMode; this.customGasPrice = options.customGasPrice; this.maxGasCap = options.maxGasCap; } async executeStep(step: LiFiStepExtended): Promise { step.execution = this.statusManager.initExecutionObject(step); const currentProcessType = 'SWAP'; const commissionBpsSDK = this.commissionBpsSDK?.[ChainId.MOVE]; const commissionBps = this.commissionBps?.[ChainId.MOVE]; let process = this.statusManager.findOrCreateProcess({ step, type: currentProcessType, }); try { process = this.statusManager.updateProcess(step, process.type, 'STARTED'); const rpcUrl = !this.mevProtection ? MVM_RPC_URL : this.transactionMode === 'default' ? SHIO_DEFAULT_RPC_URL : ShioFastRpcUrl; const client = new SuiClient({ url: rpcUrl, }); const fromToken = step.action.fromToken; const toToken = step.action.toToken; const inputAmount = step.action.fromAmount; const amountInUsd = (Number(step.action.fromAmount) / 10 ** step.action.fromToken.decimals) * Number(step.action.fromToken.priceUSD); const account = this.walletAdapter.account; let tx: Transaction | undefined; if (step.tool === 'flowx') { const tradeTx = await getRoutesFlowX({ fromToken: fromToken?.address || '', toToken: toToken?.address || '', fromAmount: inputAmount, isBuildTx: true, senderAddress: account.address, slippage: Number(this.slippage ? this.slippage : 1 / 100) * 1e6, } as RoutesFlowXParams).catch((e) => { console.error('>>> e', e); process = this.statusManager.updateProcess( step, process.type, 'FAILED', { error: { code: -1, message: e.message, }, }, ); }); if (tradeTx) { tx = tradeTx as any; } } else if (step.tool === '7k') { const tradeTx = await getRoutesSeventK({ fromToken: fromToken?.address || '', toToken: toToken?.address || '', fromAmount: inputAmount, isBuildTx: true, senderAddress: account.address, slippage: Number(this.slippage || 0.01), commission: this.commissionBpsSDK?.[ChainId.MOVE] ? Number(this.commissionBpsSDK?.[ChainId.MOVE]) : 0, } as RoutesSeventKParams).catch((e) => { console.error('>>> e', e); process = this.statusManager.updateProcess( step, process.type, 'FAILED', { error: { code: -1, message: e.message, }, }, ); }); if (tradeTx) { tx = tradeTx as any; } } else if (step.tool === 'navi') { const tradeTx = await getRoutesNavi({ fromToken: fromToken?.address || '', toToken: toToken?.address || '', fromAmount: inputAmount, isBuildTx: true, senderAddress: account.address, slippage: Number(this.slippage || 0.01), fromTokenDecimals: step.action.fromToken.decimals, } as RoutesNaviParams).catch((e) => { console.error('>>> e', e); process = this.statusManager.updateProcess( step, process.type, 'FAILED', { error: { code: -1, message: e.message, }, }, ); }); if (tradeTx) { tx = tradeTx as any; } } else if (step.tool === 'cetus') { const tradeTx = await getRoutesCetus({ fromToken: fromToken?.address || '', toToken: toToken?.address || '', fromAmount: inputAmount, isBuildTx: true, senderAddress: account.address, slippage: Number(this.slippage || 0.01), } as RoutesCetusParams).catch((e) => { console.error('>>> e', e); process = this.statusManager.updateProcess( step, process.type, 'FAILED', { error: { code: -1, message: e.message, }, }, ); }); if (tradeTx) { tx = tradeTx as any; } } else if (step.tool === 'aftermath') { const tradeTx = await getRoutesAftermath({ fromToken: fromToken?.address || '', toToken: toToken?.address || '', fromAmount: inputAmount, isBuildTx: true, senderAddress: account.address, slippage: Number(this.slippage || 0.01), } as RoutesAftermathParams).catch((e) => { console.error('>>> e', e); process = this.statusManager.updateProcess( step, process.type, 'FAILED', { error: { code: -1, message: e.message, }, }, ); }); if (tradeTx) { tx = tradeTx as any; } } else if (step.tool === 'hop') { // const tradeTx = await getRoutesHop({ // fromToken: fromToken.address, // toToken: toToken.address, // fromAmount: inputAmount, // isBuildTx: true, // senderAddress: account.address, // slippage: Number(this.slippage || 1) * 100, // commission: this.commissionBpsSDK?.[ChainId.MOVE] // ? Number(this.commissionBpsSDK?.[ChainId.MOVE]) // : 0, // } as RoutesHopParams).catch((e) => { // console.error('>>> e', e); // process = this.statusManager.updateProcess( // step, // process.type, // 'FAILED', // { // error: { // code: -1, // message: e.message, // }, // }, // ); // }); // if (tradeTx) { // tx = tradeTx as any; // } } // initial native token price incase of error let nativeTokenPrice: NimbusPricePrice = { data: { [MOVE_NATIVE_ADDRESS]: { price: 1, name: 'SUI', contract_address: MOVE_NATIVE_ADDRESS, symbol: 'SUI', timestamp: Date.now(), decimals: 9, source: 'Nimbus', }, }, }; try { nativeTokenPrice = await nimbusGetPrice({ chain: 'SUI', tokens: [MOVE_NATIVE_ADDRESS], }); } catch (e) { console.error('>>> error fetching native token price', e); } if ( ['cetus', 'flowx', 'aftermath', 'navi'].includes(step.tool) && nativeTokenPrice && commissionBpsSDK && tx && tx.gas && amountInUsd > 0 && nativeTokenPrice?.data?.[MOVE_NATIVE_ADDRESS]?.price > 0 ) { const nimbusCommission = Math.floor( (((commissionBpsSDK / 10000) * amountInUsd) / nativeTokenPrice?.data?.[MOVE_NATIVE_ADDRESS]?.price) * 10 ** 9, ); if (nimbusCommission > 0) { let nimbusCommissionCoin; try { [nimbusCommissionCoin] = tx.splitCoins(tx.gas, [nimbusCommission]) || []; if (nimbusCommissionCoin) { tx?.transferObjects([nimbusCommissionCoin || ``], WALLET_ADDRESS); } else { console.error('Failed to split coins for commission'); } } catch (err) { console.error('Error splitting coins for commission:', err); } } } if ( nativeTokenPrice && commissionBps && tx && tx.gas && amountInUsd > 0 && nativeTokenPrice?.data?.[MOVE_NATIVE_ADDRESS]?.price > 0 ) { const nimbusCommission = Math.floor( (((commissionBps / 10000) * amountInUsd) / nativeTokenPrice?.data?.[MOVE_NATIVE_ADDRESS]?.price) * 10 ** 9, ); if (nimbusCommission > 0) { let nimbusCommissionCoin; try { [nimbusCommissionCoin] = tx.splitCoins(tx.gas, [nimbusCommission]) || []; if (nimbusCommissionCoin) { tx.transferObjects([nimbusCommissionCoin], WALLET_ADDRESS); } else { console.error('Failed to split coins for commission'); } } catch (err) { console.error('Error splitting coins for commission:', err); } } } // Fast mode Shio flow if ( tx && account && account?.address && this.mevProtection && this.transactionMode === 'fast' && this.customGasPrice && this.maxGasCap ) { tx.setGasPrice(Number(this.customGasPrice)); const estimatedFee = await EstimateFee({ sender: account.address, transaction: tx as any, client: client as any, }); const formatMaxGasCapToMIST = Number(this.maxGasCap) * 10 ** 9; tx.setGasBudget( Math.min(formatMaxGasCapToMIST, estimatedFee.gasBudget), ); let tipCoins: any[] = []; if (estimatedFee.tipAmount > 0 && tx && tx.gas) { try { tipCoins = tx.splitCoins(tx.gas, [estimatedFee.tipAmount]) || []; if (tipCoins.length === 0 || !tipCoins[0]) { console.error('Failed to split coins for tip'); } else { AppendCoinToTip(tx as any, tipCoins[0], estimatedFee.tipAmount); } } catch (err) { console.error('Error splitting coins for tip:', err); } } } const resData = await this.walletAdapter.signAndExecuteTransaction({ transaction: tx, }); process = this.statusManager.updateProcess(step, process.type, 'PENDING'); try { process = this.statusManager.updateProcess( step, process.type, 'PENDING', { chainId: ChainId.MOVE, txHash: resData.digest, txLink: `https://suiscan.xyz/mainnet/tx/${resData.digest}`, }, ); // Wait for the transaction to be confirmed await client.waitForTransaction({ digest: resData.digest, }); // Check if the transaction is successful const txStatus = await client.getTransactionBlock({ digest: resData.digest, options: { showEffects: true }, }); if (txStatus.effects?.status.status === 'success') { process = this.statusManager.updateProcess( step, process.type, 'DONE', ); this.statusManager.updateExecution(step, 'DONE'); } else { throw new Error('Transaction failed'); } } catch (e: any) { const error = await parseMVMErrors(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; } return step; } catch (e: any) { console.error('>>> e', e); process = this.statusManager.updateProcess(step, process.type, 'FAILED', { error: { code: e.code, message: e.message, }, }); return step; } } }