diff --git a/apps/kyberswap-interface/.env.production b/apps/kyberswap-interface/.env.production index 420c1fe2c9..5b646871c3 100644 --- a/apps/kyberswap-interface/.env.production +++ b/apps/kyberswap-interface/.env.production @@ -54,5 +54,5 @@ VITE_AFFILIATE_SERVICE=https://affiliate-service.kyberswap.com/api VITE_SOLANA_RPC=https://solana-rpc.kyberswap.com VITE_SMART_EXIT_API_URL=https://conditional-order.kyberswap.com/api -VITE_CROSSCHAIN_AGGREGATOR_API=https://crosschain-aggregator.kyberswap.com -# VITE_CROSSCHAIN_AGGREGATOR_API=https://pre-crosschain-aggregator.kyberengineering.io +# VITE_CROSSCHAIN_AGGREGATOR_API=https://crosschain-aggregator.kyberswap.com +VITE_CROSSCHAIN_AGGREGATOR_API=https://pre-crosschain-aggregator.kyberengineering.io diff --git a/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/AcrossAdapter/api.ts b/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/AcrossAdapter/api.ts new file mode 100644 index 0000000000..c7af3eb0c8 --- /dev/null +++ b/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/AcrossAdapter/api.ts @@ -0,0 +1,18 @@ +import axios from 'axios' + +import { AcrossDepositStatusResponse } from 'pages/CrossChainSwap/adapters/AcrossAdapter/types' + +const ACROSS_API_BASE_URL = 'https://app.across.to/api' + +export const getAcrossDepositStatus = async (depositTxnRef: string): Promise => { + const { data, status } = await axios.get(`${ACROSS_API_BASE_URL}/deposit/status`, { + params: { depositTxnRef }, + validateStatus: status => status < 500, + }) + + if (status >= 400 && !data?.error) { + throw new Error(`Across deposit status failed with HTTP ${status}`) + } + + return data +} diff --git a/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/AcrossAdapter.ts b/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/AcrossAdapter/index.ts similarity index 68% rename from apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/AcrossAdapter.ts rename to apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/AcrossAdapter/index.ts index e38d426252..a2d849d0ba 100644 --- a/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/AcrossAdapter.ts +++ b/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/AcrossAdapter/index.ts @@ -2,7 +2,8 @@ import { AcrossClient, createAcrossClient } from '@across-protocol/app-sdk' import { ChainId, Currency, Token } from '@kyberswap/ks-sdk-core' import { WalletAdapterProps } from '@solana/wallet-adapter-base' import { Connection } from '@solana/web3.js' -import { WalletClient, formatUnits } from 'viem' +import axios from 'axios' +import { type Address, WalletClient, formatUnits } from 'viem' import { arbitrum, base, @@ -21,6 +22,13 @@ import { import { CROSS_CHAIN_FEE_RECEIVER, ZERO_ADDRESS } from 'constants/index' import { NETWORKS_INFO } from 'hooks/useChainsConfig' +import { getAcrossDepositStatus } from 'pages/CrossChainSwap/adapters/AcrossAdapter/api' +import { + AcrossSuggestedFeesQuote, + AcrossSwapQuote, + AcrossWalletClient, +} from 'pages/CrossChainSwap/adapters/AcrossAdapter/types' +import { getAcrossFillTxHash, mapAcrossDepositStatus } from 'pages/CrossChainSwap/adapters/AcrossAdapter/utils' import { BaseSwapAdapter, Chain, @@ -39,6 +47,9 @@ import { isEvmChain } from 'utils' const API_URL = 'https://app.across.to/api/suggested-fees' +const getAcrossTokenAddress = (token: Currency): Address => + (token.isNative ? ZERO_ADDRESS : token.wrapped.address) as Address + export class AcrossAdapter extends BaseSwapAdapter { private acrossClient: AcrossClient @@ -119,12 +130,19 @@ export class AcrossAdapter extends BaseSwapAdapter { async getQuote(params: QuoteParams): Promise { try { - let res const isFromSol = params.fromChain === NonEvmChain.Solana + let rawQuote: AcrossSuggestedFeesQuote | AcrossSwapQuote + let outputAmount: bigint + let contractAddress: string + let timeEstimate: number + let gasFeeUsd = 0 + if (isFromSol && isEvmChain(params.toChain)) { + const fromToken = params.fromToken as SolanaToken + const toToken = params.toToken as Token const reqParams = new URLSearchParams({ - inputToken: (params.fromToken as SolanaToken).id, - outputToken: (params.toToken as Token).wrapped.address, + inputToken: fromToken.id, + outputToken: toToken.wrapped.address, destinationChainId: params.toChain.toString(), originChainId: '34268394551451', amount: params.amount, @@ -132,15 +150,19 @@ export class AcrossAdapter extends BaseSwapAdapter { allowUnmatchedDecimals: 'true', }) - res = await fetch(`${API_URL}?${reqParams}`).then(res => res.json()) + const { data } = await axios.get(API_URL, { params: reqParams }) + rawQuote = data + outputAmount = BigInt(data.outputAmount) + contractAddress = ZERO_ADDRESS + timeEstimate = data.estimatedFillTimeSec } else { - const p = params as EvmQuoteParams - res = await this.acrossClient.getSwapQuote({ + const quoteParams = params as EvmQuoteParams + const swapQuote = await this.acrossClient.getSwapQuote({ route: { originChainId: +params.fromChain, destinationChainId: +params.toChain, - inputToken: (p.fromToken.isNative ? ZERO_ADDRESS : p.fromToken.wrapped.address) as `0x${string}`, - outputToken: (p.toToken.isNative ? ZERO_ADDRESS : p.toToken.wrapped.address) as `0x${string}`, + inputToken: getAcrossTokenAddress(quoteParams.fromToken), + outputToken: getAcrossTokenAddress(quoteParams.toToken), }, amount: params.amount, appFee: params.feeBps / 10_000, @@ -148,6 +170,12 @@ export class AcrossAdapter extends BaseSwapAdapter { slippage: params.slippage / 10_000, // https://docs.across.to/reference/api-reference#get-swap-approval depositor: params.sender, }) + + rawQuote = swapQuote + outputAmount = BigInt(swapQuote.expectedOutputAmount) + contractAddress = swapQuote.checks.allowance.spender + timeEstimate = swapQuote.expectedFillTime + gasFeeUsd = Number(swapQuote.fees.originGas.amountUsd || 0) } // across only have bridge then we can treat token in and out price usd are the same in case price service is not supported @@ -161,7 +189,6 @@ export class AcrossAdapter extends BaseSwapAdapter { ? params.tokenInUsd : params.tokenOutUsd - const outputAmount = BigInt(isFromSol ? res.outputAmount : res.expectedOutputAmount) const formattedOutputAmount = formatUnits(outputAmount, params.toToken.decimals) const formattedInputAmount = formatUnits(BigInt(params.amount), params.fromToken.decimals) @@ -172,57 +199,59 @@ export class AcrossAdapter extends BaseSwapAdapter { quoteParams: params, outputAmount, formattedOutputAmount, - inputUsd: tokenInUsd * +formatUnits(BigInt(params.amount), params.fromToken.decimals), - outputUsd: tokenOutUsd * +formattedOutputAmount, + inputUsd, + outputUsd, rate: +formattedOutputAmount / +formattedInputAmount, - timeEstimate: isFromSol ? res.estimatedFillTimeSec : res.expectedFillTime, + timeEstimate, priceImpact: !inputUsd || !outputUsd ? NaN : ((inputUsd - outputUsd) * 100) / inputUsd, - // TODO: what is gas fee for across - gasFeeUsd: 0, - contractAddress: isFromSol ? ZERO_ADDRESS : res.checks.allowance.spender, - rawQuote: res, + gasFeeUsd, + contractAddress, + rawQuote, protocolFee: 0, platformFeePercent: (params.feeBps * 100) / 10_000, } - } catch (e) { - console.log('Across getQuote error', e) - throw e + } catch (error) { + console.log('Across getQuote error', error) + throw error } } async executeSwap( quote: Quote, walletClient: WalletClient, - _nearWalletClient?: any, + _nearWalletClient?: unknown, _sendBtcFn?: (params: { recipient: string; amount: string | number }) => Promise, _sendTransaction?: WalletAdapterProps['sendTransaction'], _connection?: Connection, ): Promise { + const normalizedQuote = quote.quote + const quoteParams = normalizedQuote.quoteParams + // For EVM chains, use the original implementation return new Promise((resolve, reject) => { this.acrossClient .executeSwapQuote({ - walletClient: walletClient as any, - swapQuote: quote.quote.rawQuote as any, + walletClient: walletClient as AcrossWalletClient, + swapQuote: normalizedQuote.rawQuote as AcrossSwapQuote, onProgress: progress => { if (progress.step === 'swap' && 'txHash' in progress) { resolve({ - sender: quote.quote.quoteParams.sender, + sender: quoteParams.sender, sourceTxHash: progress.txHash, adapter: this.getName(), id: progress.txHash, - sourceChain: quote.quote.quoteParams.fromChain, - targetChain: quote.quote.quoteParams.toChain, - inputAmount: quote.quote.quoteParams.amount, - outputAmount: quote.quote.outputAmount.toString(), - sourceToken: quote.quote.quoteParams.fromToken, - targetToken: quote.quote.quoteParams.toToken, + sourceChain: quoteParams.fromChain, + targetChain: quoteParams.toChain, + inputAmount: quoteParams.amount, + outputAmount: normalizedQuote.outputAmount.toString(), + sourceToken: quoteParams.fromToken, + targetToken: quoteParams.toToken, timestamp: new Date().getTime(), - amountInUsd: quote.quote.inputUsd, - amountOutUsd: quote.quote.outputUsd, - platformFeePercent: quote.quote.platformFeePercent, - recipient: quote.quote.quoteParams.recipient, + amountInUsd: normalizedQuote.inputUsd, + amountOutUsd: normalizedQuote.outputUsd, + platformFeePercent: normalizedQuote.platformFeePercent, + recipient: quoteParams.recipient, }) } }, @@ -232,12 +261,11 @@ export class AcrossAdapter extends BaseSwapAdapter { } async getTransactionStatus(params: NormalizedTxResponse): Promise { try { - const res = await fetch(`https://app.across.to/api/deposit/status?depositTxHash=${params.sourceTxHash}`).then( - res => res.json(), - ) + const res = await getAcrossDepositStatus(params.sourceTxHash) + return { - txHash: res.fillTx || '', - status: res.status === 'refunded' ? 'Refunded' : res.status === 'filled' ? 'Success' : 'Processing', + txHash: getAcrossFillTxHash(res), + status: mapAcrossDepositStatus(res, { txTimestamp: params.timestamp }), } } catch (error) { console.error('Error fetching transaction status:', error) diff --git a/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/AcrossAdapter/types.ts b/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/AcrossAdapter/types.ts new file mode 100644 index 0000000000..e8186a3177 --- /dev/null +++ b/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/AcrossAdapter/types.ts @@ -0,0 +1,27 @@ +import type { AcrossClient, ExecuteQuoteParams, ExecuteSwapQuoteParams } from '@across-protocol/app-sdk' + +export type AcrossDepositStatus = 'pending' | 'filled' | 'expired' | 'refunded' | 'slowFillRequested' + +export interface AcrossDepositStatusResponse { + status?: AcrossDepositStatus + fillTxnRef?: string + fillTx?: string + originChainId?: number + destinationChainId?: number + depositId?: string | number + depositTxnRef?: string + depositRefundTxnRef?: string + actionsSucceeded?: boolean + error?: string + message?: string +} + +export interface AcrossSuggestedFeesQuote { + outputAmount: string + estimatedFillTimeSec: number +} + +export type AcrossSwapQuote = Awaited> +export type AcrossWalletClient = ExecuteQuoteParams['walletClient'] +export type AcrossDeposit = ExecuteQuoteParams['deposit'] +export type AcrossSwapExecutionProgress = Parameters>[0] diff --git a/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/AcrossAdapter/utils.ts b/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/AcrossAdapter/utils.ts new file mode 100644 index 0000000000..17b30a908d --- /dev/null +++ b/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/AcrossAdapter/utils.ts @@ -0,0 +1,42 @@ +import { AcrossDepositStatusResponse } from 'pages/CrossChainSwap/adapters/AcrossAdapter/types' +import { SwapStatus } from 'pages/CrossChainSwap/adapters/BaseSwapAdapter' + +const ACROSS_STATUS_ERROR_GRACE_PERIOD = 2 * 60 * 60 * 1_000 + +interface AcrossDepositStatusOptions { + txTimestamp?: number + now?: number +} + +export const getAcrossFillTxHash = (statusResponse: AcrossDepositStatusResponse): string => { + return statusResponse.fillTxnRef || statusResponse.fillTx || '' +} + +export const mapAcrossDepositStatus = ( + statusResponse: AcrossDepositStatusResponse, + options: AcrossDepositStatusOptions = {}, +): SwapStatus['status'] => { + if (statusResponse.error) { + const { txTimestamp, now = Date.now() } = options + const isWithinIndexingGracePeriod = txTimestamp ? now - txTimestamp < ACROSS_STATUS_ERROR_GRACE_PERIOD : false + + if (isWithinIndexingGracePeriod) { + return 'Processing' + } + + return 'Failed' + } + + switch (statusResponse.status) { + case 'filled': + return 'Success' + case 'refunded': + return 'Refunded' + case 'expired': + return 'Failed' + case 'pending': + case 'slowFillRequested': + default: + return 'Processing' + } +} diff --git a/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberAcrossAdapter.ts b/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberAcrossAdapter.ts deleted file mode 100644 index 1ebe1c1ce1..0000000000 --- a/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberAcrossAdapter.ts +++ /dev/null @@ -1,777 +0,0 @@ -import { - AcrossClient, - createAcrossClient, - getIntegratorDataSuffix, - parseDepositLogs, - parseFillLogs, - waitForDepositTx, - waitForFillTx, -} from '@across-protocol/app-sdk' -import { ChainId, Currency } from '@kyberswap/ks-sdk-core' -import { WalletAdapterProps } from '@solana/wallet-adapter-base' -import { Connection } from '@solana/web3.js' -import { - type Address, - type Hash, - type Hex, - type TransactionReceipt, - type Chain as ViemChain, - WalletClient, - encodeFunctionData, - maxUint256, - parseAbi, -} from 'viem' -import { arbitrum, base, blast, bsc, linea, mainnet, optimism, polygon, scroll, unichain, zksync } from 'viem/chains' - -import { monad, plasma } from 'components/Web3Provider' -import { NETWORKS_INFO } from 'hooks/useChainsConfig' - -import { Quote } from '../registry' -import { - BaseSwapAdapter, - Chain, - NormalizedQuote, - NormalizedTxResponse, - QuoteParams, - SwapStatus, -} from './BaseSwapAdapter' - -// Integrator ID for Across tracking -const KYBERSWAP_INTEGRATOR_ID: Hex = '0x008a' - -// Chain ID to viem Chain mapping -const chainIdToViemChain: Record = { - [ChainId.MAINNET]: mainnet, - [ChainId.ARBITRUM]: arbitrum, - [ChainId.BSCMAINNET]: bsc, - [ChainId.OPTIMISM]: optimism, - [ChainId.LINEA]: linea, - [ChainId.MATIC]: polygon, - [ChainId.ZKSYNC]: zksync, - [ChainId.BASE]: base, - [ChainId.SCROLL]: scroll, - [ChainId.BLAST]: blast, - [ChainId.UNICHAIN]: unichain, - [ChainId.PLASMA]: plasma, - [ChainId.MONAD]: monad, -} - -// TransferType enum -export enum TransferType { - Approval = 0, - Transfer = 1, - Permit2Approval = 2, -} - -// Type definitions for SwapAndDepositData -export interface Fees { - amount: bigint - recipient: Address -} - -export interface BaseDepositData { - inputToken: Address - outputToken: `0x${string}` // bytes32 - outputAmount: bigint - depositor: Address - recipient: `0x${string}` // bytes32 - destinationChainId: bigint - exclusiveRelayer: `0x${string}` // bytes32 - quoteTimestamp: number - fillDeadline: number - exclusivityParameter: number - message: `0x${string}` -} - -export interface SwapAndDepositData { - submissionFees: Fees - depositData: BaseDepositData - swapToken: Address - exchange: Address - transferType: TransferType - swapTokenAmount: bigint - minExpectedInputTokenAmount: bigint - routerCalldata: `0x${string}` - enableProportionalAdjustment: boolean - spokePool: Address - nonce: bigint -} - -// ABI for SpokePoolPeriphery contract -export const spokePoolPeripheryAbi = [ - { - inputs: [{ internalType: 'contract IPermit2', name: '_permit2', type: 'address' }], - stateMutability: 'nonpayable', - type: 'constructor', - }, - { inputs: [], name: 'InvalidMinExpectedInputAmount', type: 'error' }, - { inputs: [], name: 'InvalidMsgValue', type: 'error' }, - { inputs: [], name: 'InvalidNonce', type: 'error' }, - { inputs: [], name: 'InvalidShortString', type: 'error' }, - { inputs: [], name: 'InvalidSignature', type: 'error' }, - { inputs: [], name: 'MinimumExpectedInputAmount', type: 'error' }, - { inputs: [{ internalType: 'string', name: 'str', type: 'string' }], name: 'StringTooLong', type: 'error' }, - { anonymous: false, inputs: [], name: 'EIP712DomainChanged', type: 'event' }, - { - anonymous: false, - inputs: [ - { indexed: false, internalType: 'address', name: 'exchange', type: 'address' }, - { indexed: false, internalType: 'bytes', name: 'exchangeCalldata', type: 'bytes' }, - { indexed: true, internalType: 'address', name: 'swapToken', type: 'address' }, - { indexed: true, internalType: 'address', name: 'acrossInputToken', type: 'address' }, - { indexed: false, internalType: 'uint256', name: 'swapTokenAmount', type: 'uint256' }, - { indexed: false, internalType: 'uint256', name: 'acrossInputAmount', type: 'uint256' }, - { indexed: true, internalType: 'bytes32', name: 'acrossOutputToken', type: 'bytes32' }, - { indexed: false, internalType: 'uint256', name: 'acrossOutputAmount', type: 'uint256' }, - ], - name: 'SwapBeforeBridge', - type: 'event', - }, - { - inputs: [ - { - components: [ - { - components: [ - { internalType: 'uint256', name: 'amount', type: 'uint256' }, - { internalType: 'address', name: 'recipient', type: 'address' }, - ], - internalType: 'struct SpokePoolPeripheryInterface.Fees', - name: 'submissionFees', - type: 'tuple', - }, - { - components: [ - { internalType: 'address', name: 'inputToken', type: 'address' }, - { internalType: 'bytes32', name: 'outputToken', type: 'bytes32' }, - { internalType: 'uint256', name: 'outputAmount', type: 'uint256' }, - { internalType: 'address', name: 'depositor', type: 'address' }, - { internalType: 'bytes32', name: 'recipient', type: 'bytes32' }, - { internalType: 'uint256', name: 'destinationChainId', type: 'uint256' }, - { internalType: 'bytes32', name: 'exclusiveRelayer', type: 'bytes32' }, - { internalType: 'uint32', name: 'quoteTimestamp', type: 'uint32' }, - { internalType: 'uint32', name: 'fillDeadline', type: 'uint32' }, - { internalType: 'uint32', name: 'exclusivityParameter', type: 'uint32' }, - { internalType: 'bytes', name: 'message', type: 'bytes' }, - ], - internalType: 'struct SpokePoolPeripheryInterface.BaseDepositData', - name: 'depositData', - type: 'tuple', - }, - { internalType: 'address', name: 'swapToken', type: 'address' }, - { internalType: 'address', name: 'exchange', type: 'address' }, - { internalType: 'enum SpokePoolPeripheryInterface.TransferType', name: 'transferType', type: 'uint8' }, - { internalType: 'uint256', name: 'swapTokenAmount', type: 'uint256' }, - { internalType: 'uint256', name: 'minExpectedInputTokenAmount', type: 'uint256' }, - { internalType: 'bytes', name: 'routerCalldata', type: 'bytes' }, - { internalType: 'bool', name: 'enableProportionalAdjustment', type: 'bool' }, - { internalType: 'address', name: 'spokePool', type: 'address' }, - { internalType: 'uint256', name: 'nonce', type: 'uint256' }, - ], - internalType: 'struct SpokePoolPeripheryInterface.SwapAndDepositData', - name: 'swapAndDepositData', - type: 'tuple', - }, - ], - name: 'swapAndBridge', - outputs: [], - stateMutability: 'payable', - type: 'function', - }, -] as const - -// Progress tracking types -type ProgressMeta = ApproveMeta | SwapAndBridgeMeta | FillMeta | undefined - -type ApproveMeta = { - approvalAmount: bigint - spender: Address -} - -type SwapAndBridgeMeta = { - swapAndDepositData: SwapAndDepositData -} - -type FillMeta = { - depositId: bigint -} - -export type SwapAndBridgeProgress = - | { - step: 'approve' - status: 'idle' - } - | { - step: 'approve' - status: 'txPending' - txHash: Hash - meta: ApproveMeta - } - | { - step: 'approve' - status: 'txSuccess' - txReceipt: TransactionReceipt - meta: ApproveMeta - } - | { - step: 'swapAndBridge' - status: 'simulationPending' - meta: SwapAndBridgeMeta - } - | { - step: 'swapAndBridge' - status: 'simulationSuccess' - txRequest: any - meta: SwapAndBridgeMeta - } - | { - step: 'swapAndBridge' - status: 'txPending' - txHash: Hash - txRequest?: any - meta: SwapAndBridgeMeta - } - | { - step: 'swapAndBridge' - status: 'txSuccess' - txReceipt: TransactionReceipt - depositId: bigint - depositLog: ReturnType - meta: SwapAndBridgeMeta - } - | { - step: 'fill' - status: 'pending' - meta: FillMeta - } - | { - step: 'fill' - status: 'txSuccess' - txReceipt: TransactionReceipt - fillTxTimestamp: bigint - actionSuccess: boolean | undefined - fillLog: ReturnType - meta: FillMeta - } - | { - step: 'approve' | 'swapAndBridge' | 'fill' - status: 'error' - error: Error - meta: ProgressMeta - } - -export interface ExecuteSwapAndBridgeParams { - // Wallet and clients - walletClient: WalletClient - originChain: ViemChain - destinationChain: ViemChain - // User address - userAddress: Address - // Swap and bridge data - swapAndDepositData: SwapAndDepositData - // Contract addresses - spokePoolPeripheryAddress: Address - destinationSpokePoolAddress: Address - // Options - isNative?: boolean - infiniteApproval?: boolean - skipAllowanceCheck?: boolean - throwOnError?: boolean - // Progress handler - onProgress?: (progress: SwapAndBridgeProgress) => void -} - -export interface ExecuteSwapAndBridgeResponse { - depositId?: bigint - swapAndBridgeTxReceipt?: TransactionReceipt - fillTxReceipt?: TransactionReceipt - error?: Error -} - -/** - * Transforms raw API quote data (with string values) to properly typed SwapAndDepositData (with bigint values) - * The API returns numeric values as strings, but the contract expects bigint types - */ -function transformSwapAndDepositData(raw: any): SwapAndDepositData { - return { - submissionFees: { - amount: BigInt(raw.submissionFees?.amount || '0'), - recipient: raw.submissionFees?.recipient as Address, - }, - depositData: { - inputToken: raw.depositData?.inputToken as Address, - outputToken: raw.depositData?.outputToken as `0x${string}`, - outputAmount: BigInt(raw.depositData?.outputAmount || '0'), - depositor: raw.depositData?.depositor as Address, - recipient: raw.depositData?.recipient as `0x${string}`, - destinationChainId: BigInt(raw.depositData?.destinationChainId || '0'), - exclusiveRelayer: raw.depositData?.exclusiveRelayer as `0x${string}`, - quoteTimestamp: Number(raw.depositData?.quoteTimestamp || 0), - fillDeadline: Number(raw.depositData?.fillDeadline || 0), - exclusivityParameter: Number(raw.depositData?.exclusivityParameter || 0), - message: raw.depositData?.message as `0x${string}`, - }, - swapToken: raw.swapToken as Address, - exchange: raw.exchange as Address, - transferType: Number(raw.transferType) as TransferType, - swapTokenAmount: BigInt(raw.swapTokenAmount || '0'), - minExpectedInputTokenAmount: BigInt(raw.minExpectedInputTokenAmount || '0'), - routerCalldata: raw.routerCalldata as `0x${string}`, - enableProportionalAdjustment: Boolean(raw.enableProportionalAdjustment), - spokePool: raw.spokePool as Address, - nonce: BigInt(raw.nonce || '0'), - } -} - -export class KyberAcrossAdapter extends BaseSwapAdapter { - private acrossClient: AcrossClient - - constructor() { - super() - this.acrossClient = createAcrossClient({ - integratorId: KYBERSWAP_INTEGRATOR_ID, - chains: [mainnet, arbitrum, bsc, optimism, linea, polygon, zksync, base, scroll, blast, unichain, plasma, monad], - rpcUrls: [ - ChainId.MAINNET, - ChainId.ARBITRUM, - ChainId.BSCMAINNET, - ChainId.OPTIMISM, - ChainId.LINEA, - ChainId.MATIC, - ChainId.ZKSYNC, - ChainId.BASE, - ChainId.SCROLL, - ChainId.BLAST, - ChainId.UNICHAIN, - ChainId.MONAD, - ].reduce((acc, cur) => { - return { ...acc, [cur]: NETWORKS_INFO[cur].defaultRpcUrl } - }, {}), - }) - } - - getName(): string { - return 'KyberAcross' - } - - getIcon(): string { - return 'https://i.ibb.co/fVLsZryT/kyberacross.jpg' - } - - // canSupport returns true for all cases - uses default implementation from BaseSwapAdapter - - getSupportedChains(): Chain[] { - return [ - ChainId.MAINNET, - ChainId.ARBITRUM, - ChainId.OPTIMISM, - ChainId.LINEA, - ChainId.MATIC, - ChainId.ZKSYNC, - ChainId.BASE, - ChainId.SCROLL, - ChainId.BLAST, - ChainId.UNICHAIN, - ChainId.BSCMAINNET, - ChainId.PLASMA, - ChainId.MONAD, - ] - } - - getSupportedTokens(_sourceChain: Chain, _destChain: Chain): Currency[] { - return [] - } - - // getQuote is empty - we use the stream API response for this provider - async getQuote(_params: QuoteParams): Promise { - throw new Error('KyberAcross does not support direct quote fetching. Use stream API response instead.') - } - - async executeSwap( - quote: Quote, - walletClient: WalletClient, - _nearWalletClient?: any, - _sendBtcFn?: (params: { recipient: string; amount: string | number }) => Promise, - _sendTransaction?: WalletAdapterProps['sendTransaction'], - _connection?: Connection, - ): Promise { - const rawQuote = quote.quote.rawQuote - - console.log('rawQuote ======== ', rawQuote) - - // Check if sourceSwap is null - if so, use executeQuote directly to SpokePool - if (!rawQuote.sourceSwap) { - return new Promise((resolve, reject) => { - this.acrossClient - .executeQuote({ - walletClient: walletClient as any, - deposit: rawQuote.bridge.deposit, - onProgress: progress => { - if (progress.step === 'deposit' && 'txHash' in progress) { - resolve({ - sender: quote.quote.quoteParams.sender, - sourceTxHash: progress.txHash, - adapter: this.getName(), - id: progress.txHash, - sourceChain: quote.quote.quoteParams.fromChain, - targetChain: quote.quote.quoteParams.toChain, - inputAmount: quote.quote.quoteParams.amount, - outputAmount: quote.quote.outputAmount.toString(), - sourceToken: quote.quote.quoteParams.fromToken, - targetToken: quote.quote.quoteParams.toToken, - timestamp: new Date().getTime(), - amountInUsd: quote.quote.inputUsd, - amountOutUsd: quote.quote.outputUsd, - platformFeePercent: quote.quote.platformFeePercent, - recipient: quote.quote.quoteParams.recipient, - }) - } - }, - }) - .catch(reject) - }) - } - - // Extract and transform swapAndDepositData from rawQuote - // The API returns numeric values as strings, so we need to convert them to bigints - const swapAndDepositData: SwapAndDepositData = transformSwapAndDepositData(rawQuote.swapAndDepositData) - - // Determine if this is a native token swap, explicitly set in rawQuote - const isNative: boolean = rawQuote.swapAndDepositData?.isNative - - // Get origin chain from quoteParams - const originChainId = quote.quote.quoteParams.fromChain as ChainId - const originChain = chainIdToViemChain[originChainId] - - if (!originChain) { - throw new Error(`Unsupported chain: ${originChainId}`) - } - - // Get destination chain from quoteParams - const destinationChainId = quote.quote.quoteParams.toChain as ChainId - const destinationChain = chainIdToViemChain[destinationChainId] - - if (!destinationChain) { - throw new Error(`Unsupported destination chain: ${destinationChainId}`) - } - - // Get spokePoolPeripheryAddress from rawQuote - const spokePoolPeripheryAddress: Address = rawQuote.spokePoolPeripheryAddress - if (!spokePoolPeripheryAddress) { - throw new Error(`No SpokePoolPeriphery address found for chain: ${originChainId}`) - } - - // Get destinationSpokePoolAddress from rawQuote - const destinationSpokePoolAddress: Address = rawQuote.destinationSpokePoolAddress - if (!destinationSpokePoolAddress) { - throw new Error(`No SpokePool address found for destination chain: ${destinationChainId}`) - } - - // Get user address from quote params - const userAddress = quote.quote.quoteParams.sender as Address - - // Get RPC URL for the origin chain - const rpcUrl = NETWORKS_INFO[originChainId]?.defaultRpcUrl - - if (!rpcUrl) { - throw new Error(`No RPC URL found for chain: ${originChainId}`) - } - - return new Promise((resolve, reject) => { - this.executeSwapAndBridge({ - walletClient, - originChain, - destinationChain, - userAddress, - swapAndDepositData, - spokePoolPeripheryAddress, - destinationSpokePoolAddress, - isNative, - infiniteApproval: false, - skipAllowanceCheck: false, - throwOnError: true, - onProgress: progress => { - if (progress.step === 'swapAndBridge' && 'txHash' in progress) { - resolve({ - sender: quote.quote.quoteParams.sender, - sourceTxHash: progress.txHash, - adapter: this.getName(), - id: progress.txHash, - sourceChain: quote.quote.quoteParams.fromChain, - targetChain: quote.quote.quoteParams.toChain, - inputAmount: quote.quote.quoteParams.amount, - outputAmount: quote.quote.outputAmount.toString(), - sourceToken: quote.quote.quoteParams.fromToken, - targetToken: quote.quote.quoteParams.toToken, - timestamp: new Date().getTime(), - amountInUsd: quote.quote.inputUsd, - amountOutUsd: quote.quote.outputUsd, - platformFeePercent: quote.quote.platformFeePercent, - recipient: quote.quote.quoteParams.recipient, - }) - } - }, - }).catch(reject) - }) - } - - /** - * Executes a swap-and-bridge transaction by: - * 1. Approving the SpokePoolPeriphery contract if necessary - * 2. Executing the swapAndBridge transaction - * 3. Parsing the deposit ID from transaction logs - */ - async executeSwapAndBridge(params: ExecuteSwapAndBridgeParams): Promise { - const { - walletClient, - originChain, - destinationChain, - userAddress, - swapAndDepositData, - spokePoolPeripheryAddress, - destinationSpokePoolAddress, - isNative = false, - infiniteApproval = false, - skipAllowanceCheck = false, - throwOnError = true, - onProgress, - } = params - - const onProgressHandler = onProgress || ((progress: SwapAndBridgeProgress) => console.log('Progress:', progress)) - - let currentProgress: SwapAndBridgeProgress = { - status: 'idle', - step: 'approve', - } - let currentProgressMeta: ProgressMeta - - try { - // Create public clients for reading blockchain state - const originClient = this.acrossClient.getPublicClient(originChain.id) - const destinationClient = this.acrossClient.getPublicClient(destinationChain.id) - - // Get user's nonce for replay protection - const nonce = await originClient.getTransactionCount({ - address: userAddress, - }) - - // Step 1: Check and handle approval if necessary (skip for native ETH) - if (!skipAllowanceCheck && !isNative) { - const allowance = await originClient.readContract({ - address: swapAndDepositData.swapToken, - abi: parseAbi(['function allowance(address owner, address spender) public view returns (uint256)']), - functionName: 'allowance', - args: [userAddress, spokePoolPeripheryAddress], - }) - - if (swapAndDepositData.swapTokenAmount > allowance) { - const approvalAmount = infiniteApproval ? maxUint256 : swapAndDepositData.swapTokenAmount - - currentProgressMeta = { - approvalAmount, - spender: spokePoolPeripheryAddress, - } - - // Execute approval - const approveCalldata = encodeFunctionData({ - abi: parseAbi(['function approve(address spender, uint256 value)']), - args: [spokePoolPeripheryAddress, approvalAmount], - }) - - const approveTxHash = await walletClient.sendTransaction({ - account: walletClient.account || ('0x0' as Address), - chain: originChain, - to: swapAndDepositData.swapToken, - data: approveCalldata, - }) - - currentProgress = { - step: 'approve', - status: 'txPending', - txHash: approveTxHash, - meta: currentProgressMeta, - } - onProgressHandler(currentProgress) - - // Wait for approval confirmation - const approveTxReceipt = await originClient.waitForTransactionReceipt({ - hash: approveTxHash, - }) - - currentProgress = { - step: 'approve', - status: 'txSuccess', - txReceipt: approveTxReceipt, - meta: currentProgressMeta, - } - onProgressHandler(currentProgress) - } - } - - // Step 2: Execute swapAndBridge - // 1. Simulate the swapAndBridge transaction - // 2. If successful, execute the swapAndBridge transaction - // 3. Wait for the transaction to be mined - currentProgressMeta = { - swapAndDepositData, - } - - // Report simulation pending status - currentProgress = { - step: 'swapAndBridge', - status: 'simulationPending', - meta: currentProgressMeta, - } - onProgressHandler(currentProgress) - - // Prepare the swapAndBridge args with updated nonce - const swapAndBridgeArgs = { ...swapAndDepositData, nonce: BigInt(nonce) } - - // Encode calldata for Tenderly simulation - const calldata = encodeFunctionData({ - abi: spokePoolPeripheryAbi, - functionName: 'swapAndBridge', - args: [{ ...swapAndBridgeArgs }] as any, - }) - const dataSuffix = getIntegratorDataSuffix(KYBERSWAP_INTEGRATOR_ID) - const fullCalldata = `${calldata}${dataSuffix.slice(2)}` as Hex // Remove 0x from suffix before concatenating - - // Log for Tenderly simulation - console.log('🔵 🔵 🔵 🔵 🔵 🔵 🔵') - console.log('Contract Address:', spokePoolPeripheryAddress) - console.log('Sender (from):', userAddress) - console.log('Value (wei):', isNative ? swapAndDepositData.swapTokenAmount.toString() : '0') - console.log('Calldata:', fullCalldata) - console.log('Chain ID:', originChain.id) - - // Simulate the transaction to catch revert errors with proper decoding - // and get the request object for execution - const { request: txRequest } = await originClient.simulateContract({ - address: spokePoolPeripheryAddress, - abi: spokePoolPeripheryAbi, - functionName: 'swapAndBridge', - args: [{ ...swapAndBridgeArgs }] as any, - account: walletClient.account, - value: isNative ? swapAndDepositData.swapTokenAmount : undefined, - dataSuffix: getIntegratorDataSuffix(KYBERSWAP_INTEGRATOR_ID), - }) - - // Report simulation success status - currentProgress = { - step: 'swapAndBridge', - status: 'simulationSuccess', - txRequest, - meta: currentProgressMeta, - } - onProgressHandler(currentProgress) - - // Execute the transaction using writeContract with the simulated request - const swapAndBridgeTxHash = await walletClient.writeContract(txRequest) - - currentProgress = { - step: 'swapAndBridge', - status: 'txPending', - txHash: swapAndBridgeTxHash, - txRequest, - meta: currentProgressMeta, - } - onProgressHandler(currentProgress) - - // Wait for deposit transaction and parse logs using SDK - const { depositId, depositTxReceipt } = await waitForDepositTx({ - originChainId: originChain.id, - transactionHash: swapAndBridgeTxHash, - publicClient: originClient, - }) - const depositLog = parseDepositLogs(depositTxReceipt.logs) - - currentProgress = { - step: 'swapAndBridge', - status: 'txSuccess', - txReceipt: depositTxReceipt, - depositId, - depositLog, - meta: currentProgressMeta, - } - onProgressHandler(currentProgress) - - // Step 3: Wait for fill on destination chain - currentProgressMeta = { - depositId, - } - currentProgress = { - step: 'fill', - status: 'pending', - meta: currentProgressMeta, - } - onProgressHandler(currentProgress) - - const destinationBlock = await destinationClient.getBlockNumber() - - const { fillTxReceipt, fillTxTimestamp, actionSuccess } = await waitForFillTx({ - deposit: { - originChainId: originChain.id, - destinationChainId: destinationChain.id, - destinationSpokePoolAddress: destinationSpokePoolAddress, - message: swapAndDepositData.depositData.message, - }, - depositId, - depositTxHash: depositTxReceipt.transactionHash, - destinationChainClient: destinationClient, - fromBlock: destinationBlock - 100n, - }) - - const fillLog = parseFillLogs(fillTxReceipt.logs) - - currentProgress = { - step: 'fill', - status: 'txSuccess', - txReceipt: fillTxReceipt, - fillTxTimestamp, - actionSuccess, - fillLog, - meta: currentProgressMeta, - } - onProgressHandler(currentProgress) - - return { - depositId, - swapAndBridgeTxReceipt: depositTxReceipt, - fillTxReceipt, - } - } catch (error) { - currentProgress = { - ...currentProgress, - status: 'error', - error: error as Error, - meta: currentProgressMeta, - } - onProgressHandler(currentProgress) - - if (!throwOnError) { - return { error: error as Error } - } - - throw error - } - } - - // getTransactionStatus is empty for now - will be added later - async getTransactionStatus(params: NormalizedTxResponse): Promise { - try { - const res = await fetch(`https://app.across.to/api/deposit/status?depositTxHash=${params.sourceTxHash}`).then( - res => res.json(), - ) - return { - txHash: res.fillTx || '', - status: res.status === 'refunded' ? 'Refunded' : res.status === 'filled' ? 'Success' : 'Processing', - } - } catch (error) { - console.error('Error fetching transaction status:', error) - return { - txHash: '', - status: 'Processing', - } - } - } -} diff --git a/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberAcrossAdapter/abi.ts b/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberAcrossAdapter/abi.ts new file mode 100644 index 0000000000..c08f2b9735 --- /dev/null +++ b/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberAcrossAdapter/abi.ts @@ -0,0 +1,81 @@ +export const spokePoolPeripheryAbi = [ + { + inputs: [{ internalType: 'contract IPermit2', name: '_permit2', type: 'address' }], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { inputs: [], name: 'InvalidMinExpectedInputAmount', type: 'error' }, + { inputs: [], name: 'InvalidMsgValue', type: 'error' }, + { inputs: [], name: 'InvalidNonce', type: 'error' }, + { inputs: [], name: 'InvalidShortString', type: 'error' }, + { inputs: [], name: 'InvalidSignature', type: 'error' }, + { inputs: [], name: 'MinimumExpectedInputAmount', type: 'error' }, + { inputs: [{ internalType: 'string', name: 'str', type: 'string' }], name: 'StringTooLong', type: 'error' }, + { anonymous: false, inputs: [], name: 'EIP712DomainChanged', type: 'event' }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: 'address', name: 'exchange', type: 'address' }, + { indexed: false, internalType: 'bytes', name: 'exchangeCalldata', type: 'bytes' }, + { indexed: true, internalType: 'address', name: 'swapToken', type: 'address' }, + { indexed: true, internalType: 'address', name: 'acrossInputToken', type: 'address' }, + { indexed: false, internalType: 'uint256', name: 'swapTokenAmount', type: 'uint256' }, + { indexed: false, internalType: 'uint256', name: 'acrossInputAmount', type: 'uint256' }, + { indexed: true, internalType: 'bytes32', name: 'acrossOutputToken', type: 'bytes32' }, + { indexed: false, internalType: 'uint256', name: 'acrossOutputAmount', type: 'uint256' }, + ], + name: 'SwapBeforeBridge', + type: 'event', + }, + { + inputs: [ + { + components: [ + { + components: [ + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + { internalType: 'address', name: 'recipient', type: 'address' }, + ], + internalType: 'struct SpokePoolPeripheryInterface.Fees', + name: 'submissionFees', + type: 'tuple', + }, + { + components: [ + { internalType: 'address', name: 'inputToken', type: 'address' }, + { internalType: 'bytes32', name: 'outputToken', type: 'bytes32' }, + { internalType: 'uint256', name: 'outputAmount', type: 'uint256' }, + { internalType: 'address', name: 'depositor', type: 'address' }, + { internalType: 'bytes32', name: 'recipient', type: 'bytes32' }, + { internalType: 'uint256', name: 'destinationChainId', type: 'uint256' }, + { internalType: 'bytes32', name: 'exclusiveRelayer', type: 'bytes32' }, + { internalType: 'uint32', name: 'quoteTimestamp', type: 'uint32' }, + { internalType: 'uint32', name: 'fillDeadline', type: 'uint32' }, + { internalType: 'uint32', name: 'exclusivityParameter', type: 'uint32' }, + { internalType: 'bytes', name: 'message', type: 'bytes' }, + ], + internalType: 'struct SpokePoolPeripheryInterface.BaseDepositData', + name: 'depositData', + type: 'tuple', + }, + { internalType: 'address', name: 'swapToken', type: 'address' }, + { internalType: 'address', name: 'exchange', type: 'address' }, + { internalType: 'enum SpokePoolPeripheryInterface.TransferType', name: 'transferType', type: 'uint8' }, + { internalType: 'uint256', name: 'swapTokenAmount', type: 'uint256' }, + { internalType: 'uint256', name: 'minExpectedInputTokenAmount', type: 'uint256' }, + { internalType: 'bytes', name: 'routerCalldata', type: 'bytes' }, + { internalType: 'bool', name: 'enableProportionalAdjustment', type: 'bool' }, + { internalType: 'address', name: 'spokePool', type: 'address' }, + { internalType: 'uint256', name: 'nonce', type: 'uint256' }, + ], + internalType: 'struct SpokePoolPeripheryInterface.SwapAndDepositData', + name: 'swapAndDepositData', + type: 'tuple', + }, + ], + name: 'swapAndBridge', + outputs: [], + stateMutability: 'payable', + type: 'function', + }, +] as const diff --git a/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberAcrossAdapter/constants.ts b/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberAcrossAdapter/constants.ts new file mode 100644 index 0000000000..2bcc65c795 --- /dev/null +++ b/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberAcrossAdapter/constants.ts @@ -0,0 +1,55 @@ +import { ChainId } from '@kyberswap/ks-sdk-core' +import { type Hex, type Chain as ViemChain } from 'viem' +import { arbitrum, base, blast, bsc, linea, mainnet, optimism, polygon, scroll, unichain, zksync } from 'viem/chains' + +import { monad, plasma } from 'components/Web3Provider' + +export const KYBERSWAP_INTEGRATOR_ID: Hex = '0x008a' + +export const kyberAcrossSupportedChains = [ + ChainId.MAINNET, + ChainId.ARBITRUM, + ChainId.OPTIMISM, + ChainId.LINEA, + ChainId.MATIC, + ChainId.ZKSYNC, + ChainId.BASE, + ChainId.SCROLL, + ChainId.BLAST, + ChainId.UNICHAIN, + ChainId.BSCMAINNET, + ChainId.PLASMA, + ChainId.MONAD, +] + +export const kyberAcrossViemChains = [ + mainnet, + arbitrum, + bsc, + optimism, + linea, + polygon, + zksync, + base, + scroll, + blast, + unichain, + plasma, + monad, +] + +export const chainIdToViemChain: Record = { + [ChainId.MAINNET]: mainnet, + [ChainId.ARBITRUM]: arbitrum, + [ChainId.BSCMAINNET]: bsc, + [ChainId.OPTIMISM]: optimism, + [ChainId.LINEA]: linea, + [ChainId.MATIC]: polygon, + [ChainId.ZKSYNC]: zksync, + [ChainId.BASE]: base, + [ChainId.SCROLL]: scroll, + [ChainId.BLAST]: blast, + [ChainId.UNICHAIN]: unichain, + [ChainId.PLASMA]: plasma, + [ChainId.MONAD]: monad, +} diff --git a/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberAcrossAdapter/index.ts b/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberAcrossAdapter/index.ts new file mode 100644 index 0000000000..f51004a4c9 --- /dev/null +++ b/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberAcrossAdapter/index.ts @@ -0,0 +1,243 @@ +import { AcrossClient, createAcrossClient } from '@across-protocol/app-sdk' +import { ChainId, Currency } from '@kyberswap/ks-sdk-core' +import { WalletAdapterProps } from '@solana/wallet-adapter-base' +import { Connection } from '@solana/web3.js' +import { type Address, type Hash, WalletClient } from 'viem' + +import { NETWORKS_INFO } from 'hooks/useChainsConfig' +import { getAcrossDepositStatus } from 'pages/CrossChainSwap/adapters/AcrossAdapter/api' +import { AcrossWalletClient } from 'pages/CrossChainSwap/adapters/AcrossAdapter/types' +import { getAcrossFillTxHash, mapAcrossDepositStatus } from 'pages/CrossChainSwap/adapters/AcrossAdapter/utils' +import { + BaseSwapAdapter, + Chain, + NormalizedQuote, + NormalizedTxResponse, + QuoteParams, + SwapStatus, +} from 'pages/CrossChainSwap/adapters/BaseSwapAdapter' +import { + KYBERSWAP_INTEGRATOR_ID, + chainIdToViemChain, + kyberAcrossSupportedChains, + kyberAcrossViemChains, +} from 'pages/CrossChainSwap/adapters/KyberAcrossAdapter/constants' +import { executeSwapAndBridge } from 'pages/CrossChainSwap/adapters/KyberAcrossAdapter/service' +import { + ExecuteSwapAndBridgeParams, + ExecuteSwapAndBridgeResponse, + KyberAcrossRawQuote, + SwapAndDepositData, +} from 'pages/CrossChainSwap/adapters/KyberAcrossAdapter/types' +import { transformSwapAndDepositData } from 'pages/CrossChainSwap/adapters/KyberAcrossAdapter/utils' +import { Quote } from 'pages/CrossChainSwap/registry' + +export class KyberAcrossAdapter extends BaseSwapAdapter { + private acrossClient: AcrossClient + + constructor() { + super() + this.acrossClient = createAcrossClient({ + integratorId: KYBERSWAP_INTEGRATOR_ID, + chains: kyberAcrossViemChains, + rpcUrls: [ + ChainId.MAINNET, + ChainId.ARBITRUM, + ChainId.BSCMAINNET, + ChainId.OPTIMISM, + ChainId.LINEA, + ChainId.MATIC, + ChainId.ZKSYNC, + ChainId.BASE, + ChainId.SCROLL, + ChainId.BLAST, + ChainId.UNICHAIN, + ChainId.MONAD, + ].reduce((acc, cur) => { + return { ...acc, [cur]: NETWORKS_INFO[cur].defaultRpcUrl } + }, {}), + }) + } + + getName(): string { + return 'KyberAcross' + } + + getIcon(): string { + return 'https://i.ibb.co/fVLsZryT/kyberacross.jpg' + } + + // canSupport returns true for all cases - uses default implementation from BaseSwapAdapter + + getSupportedChains(): Chain[] { + return kyberAcrossSupportedChains + } + + getSupportedTokens(_sourceChain: Chain, _destChain: Chain): Currency[] { + return [] + } + + // getQuote is empty - we use the stream API response for this provider + async getQuote(_params: QuoteParams): Promise { + throw new Error('KyberAcross does not support direct quote fetching. Use stream API response instead.') + } + + async executeSwap( + quote: Quote, + walletClient: WalletClient, + _nearWalletClient?: unknown, + _sendBtcFn?: (params: { recipient: string; amount: string | number }) => Promise, + _sendTransaction?: WalletAdapterProps['sendTransaction'], + _connection?: Connection, + ): Promise { + const normalizedQuote = quote.quote + const quoteParams = normalizedQuote.quoteParams + const rawQuote = normalizedQuote.rawQuote as KyberAcrossRawQuote + + // Check if sourceSwap is null - if so, use executeQuote directly to SpokePool + if (!rawQuote.sourceSwap) { + return new Promise((resolve, reject) => { + this.acrossClient + .executeQuote({ + walletClient: walletClient as AcrossWalletClient, + deposit: rawQuote.bridge.deposit, + onProgress: progress => { + if (progress.step === 'deposit' && 'txHash' in progress) { + resolve({ + sender: quoteParams.sender, + sourceTxHash: progress.txHash, + adapter: this.getName(), + id: progress.txHash, + sourceChain: quoteParams.fromChain, + targetChain: quoteParams.toChain, + inputAmount: quoteParams.amount, + outputAmount: normalizedQuote.outputAmount.toString(), + sourceToken: quoteParams.fromToken, + targetToken: quoteParams.toToken, + timestamp: new Date().getTime(), + amountInUsd: normalizedQuote.inputUsd, + amountOutUsd: normalizedQuote.outputUsd, + platformFeePercent: normalizedQuote.platformFeePercent, + recipient: quoteParams.recipient, + }) + } + }, + }) + .catch(reject) + }) + } + + // Extract and transform swapAndDepositData from rawQuote + // The API returns numeric values as strings, so we need to convert them to bigints + if (!rawQuote.swapAndDepositData) { + throw new Error('No swapAndDepositData found in KyberAcross quote') + } + + const swapAndDepositData: SwapAndDepositData = transformSwapAndDepositData(rawQuote.swapAndDepositData) + + const isNative = rawQuote.swapAndDepositData.isNative || false + + const originChainId = quoteParams.fromChain as ChainId + const originChain = chainIdToViemChain[originChainId] + + if (!originChain) { + throw new Error(`Unsupported chain: ${originChainId}`) + } + + const destinationChainId = quoteParams.toChain as ChainId + const destinationChain = chainIdToViemChain[destinationChainId] + + if (!destinationChain) { + throw new Error(`Unsupported destination chain: ${destinationChainId}`) + } + + const spokePoolPeripheryAddress = rawQuote.spokePoolPeripheryAddress + if (!spokePoolPeripheryAddress) { + throw new Error(`No SpokePoolPeriphery address found for chain: ${originChainId}`) + } + + const destinationSpokePoolAddress = rawQuote.destinationSpokePoolAddress + if (!destinationSpokePoolAddress) { + throw new Error(`No SpokePool address found for destination chain: ${destinationChainId}`) + } + + const userAddress = quoteParams.sender as Address + + const rpcUrl = NETWORKS_INFO[originChainId]?.defaultRpcUrl + + if (!rpcUrl) { + throw new Error(`No RPC URL found for chain: ${originChainId}`) + } + + return new Promise((resolve, reject) => { + this.executeSwapAndBridge({ + walletClient, + originChain, + destinationChain, + userAddress, + swapAndDepositData, + spokePoolPeripheryAddress, + destinationSpokePoolAddress, + isNative, + infiniteApproval: false, + skipAllowanceCheck: false, + throwOnError: true, + onProgress: progress => { + if (progress.step === 'swapAndBridge' && 'txHash' in progress) { + resolve({ + sender: quoteParams.sender, + sourceTxHash: progress.txHash, + adapter: this.getName(), + id: progress.txHash, + sourceChain: quoteParams.fromChain, + targetChain: quoteParams.toChain, + inputAmount: quoteParams.amount, + outputAmount: normalizedQuote.outputAmount.toString(), + sourceToken: quoteParams.fromToken, + targetToken: quoteParams.toToken, + timestamp: new Date().getTime(), + amountInUsd: normalizedQuote.inputUsd, + amountOutUsd: normalizedQuote.outputUsd, + platformFeePercent: normalizedQuote.platformFeePercent, + recipient: quoteParams.recipient, + }) + } + }, + }).catch(reject) + }) + } + + async executeSwapAndBridge(params: ExecuteSwapAndBridgeParams): Promise { + return executeSwapAndBridge(this.acrossClient, params) + } + + async getTransactionStatus(params: NormalizedTxResponse): Promise { + try { + const res = await getAcrossDepositStatus(params.sourceTxHash) + + return { + txHash: getAcrossFillTxHash(res), + status: mapAcrossDepositStatus(res, { txTimestamp: params.timestamp }), + } + } catch (error) { + console.error('Error fetching transaction status:', error) + + const publicClient = this.acrossClient.getPublicClient(params.sourceChain as number) + const receipt = await publicClient.getTransactionReceipt({ + hash: params.sourceTxHash as Hash, + }) + + if (receipt?.status === 'reverted') { + return { + txHash: '', + status: 'Failed', + } + } + + return { + txHash: '', + status: 'Processing', + } + } + } +} diff --git a/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberAcrossAdapter/service.ts b/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberAcrossAdapter/service.ts new file mode 100644 index 0000000000..7d875fb655 --- /dev/null +++ b/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberAcrossAdapter/service.ts @@ -0,0 +1,245 @@ +import { + AcrossClient, + getIntegratorDataSuffix, + parseDepositLogs, + parseFillLogs, + waitForDepositTx, + waitForFillTx, +} from '@across-protocol/app-sdk' +import { type Hex, encodeFunctionData, maxUint256, parseAbi } from 'viem' + +import { spokePoolPeripheryAbi } from 'pages/CrossChainSwap/adapters/KyberAcrossAdapter/abi' +import { KYBERSWAP_INTEGRATOR_ID } from 'pages/CrossChainSwap/adapters/KyberAcrossAdapter/constants' +import { + ExecuteSwapAndBridgeParams, + ExecuteSwapAndBridgeResponse, + ProgressMeta, + SwapAndBridgeProgress, +} from 'pages/CrossChainSwap/adapters/KyberAcrossAdapter/types' + +export async function executeSwapAndBridge( + acrossClient: AcrossClient, + params: ExecuteSwapAndBridgeParams, +): Promise { + const { + walletClient, + originChain, + destinationChain, + userAddress, + swapAndDepositData, + spokePoolPeripheryAddress, + destinationSpokePoolAddress, + isNative = false, + infiniteApproval = false, + skipAllowanceCheck = false, + throwOnError = true, + onProgress, + } = params + + const onProgressHandler = onProgress || ((progress: SwapAndBridgeProgress) => console.log('Progress:', progress)) + + let currentProgress: SwapAndBridgeProgress = { + status: 'idle', + step: 'approve', + } + let currentProgressMeta: ProgressMeta + + try { + const originClient = acrossClient.getPublicClient(originChain.id) + const destinationClient = acrossClient.getPublicClient(destinationChain.id) + + const nonce = await originClient.getTransactionCount({ + address: userAddress, + }) + + if (!skipAllowanceCheck && !isNative) { + const allowance = await originClient.readContract({ + address: swapAndDepositData.swapToken, + abi: parseAbi(['function allowance(address owner, address spender) public view returns (uint256)']), + functionName: 'allowance', + args: [userAddress, spokePoolPeripheryAddress], + }) + + if (swapAndDepositData.swapTokenAmount > allowance) { + const approvalAmount = infiniteApproval ? maxUint256 : swapAndDepositData.swapTokenAmount + + currentProgressMeta = { + approvalAmount, + spender: spokePoolPeripheryAddress, + } + + const approveCalldata = encodeFunctionData({ + abi: parseAbi(['function approve(address spender, uint256 value)']), + args: [spokePoolPeripheryAddress, approvalAmount], + }) + + if (!walletClient.account) { + throw new Error('Wallet account not connected') + } + + const approveTxHash = await walletClient.sendTransaction({ + account: walletClient.account, + chain: originChain, + to: swapAndDepositData.swapToken, + data: approveCalldata, + }) + + currentProgress = { + step: 'approve', + status: 'txPending', + txHash: approveTxHash, + meta: currentProgressMeta, + } + onProgressHandler(currentProgress) + + const approveTxReceipt = await originClient.waitForTransactionReceipt({ + hash: approveTxHash, + }) + + currentProgress = { + step: 'approve', + status: 'txSuccess', + txReceipt: approveTxReceipt, + meta: currentProgressMeta, + } + onProgressHandler(currentProgress) + } + } + + currentProgressMeta = { + swapAndDepositData, + } + + currentProgress = { + step: 'swapAndBridge', + status: 'simulationPending', + meta: currentProgressMeta, + } + onProgressHandler(currentProgress) + + const swapAndBridgeArgs = { ...swapAndDepositData, nonce: BigInt(nonce) } + + // Encode calldata for Tenderly simulation + const calldata = encodeFunctionData({ + abi: spokePoolPeripheryAbi, + functionName: 'swapAndBridge', + args: [{ ...swapAndBridgeArgs }], + }) + const dataSuffix = getIntegratorDataSuffix(KYBERSWAP_INTEGRATOR_ID) + const fullCalldata = `${calldata}${dataSuffix.slice(2)}` as Hex // Remove 0x from suffix before concatenating + + // Log for Tenderly simulation + console.log('🔵 🔵 🔵 🔵 🔵 🔵 🔵') + console.log('Contract Address:', spokePoolPeripheryAddress) + console.log('Sender (from):', userAddress) + console.log('Value (wei):', isNative ? swapAndDepositData.swapTokenAmount.toString() : '0') + console.log('Calldata:', fullCalldata) + console.log('Chain ID:', originChain.id) + + const { request: txRequest } = await originClient.simulateContract({ + address: spokePoolPeripheryAddress, + abi: spokePoolPeripheryAbi, + functionName: 'swapAndBridge', + args: [{ ...swapAndBridgeArgs }], + account: walletClient.account, + value: isNative ? swapAndDepositData.swapTokenAmount : undefined, + dataSuffix: getIntegratorDataSuffix(KYBERSWAP_INTEGRATOR_ID), + }) + + currentProgress = { + step: 'swapAndBridge', + status: 'simulationSuccess', + txRequest, + meta: currentProgressMeta, + } + onProgressHandler(currentProgress) + + const swapAndBridgeTxHash = await walletClient.writeContract(txRequest) + + currentProgress = { + step: 'swapAndBridge', + status: 'txPending', + txHash: swapAndBridgeTxHash, + txRequest, + meta: currentProgressMeta, + } + onProgressHandler(currentProgress) + + const { depositId, depositTxReceipt } = await waitForDepositTx({ + originChainId: originChain.id, + transactionHash: swapAndBridgeTxHash, + publicClient: originClient, + }) + const depositLog = parseDepositLogs(depositTxReceipt.logs) + + currentProgress = { + step: 'swapAndBridge', + status: 'txSuccess', + txReceipt: depositTxReceipt, + depositId, + depositLog, + meta: currentProgressMeta, + } + onProgressHandler(currentProgress) + + currentProgressMeta = { + depositId, + } + currentProgress = { + step: 'fill', + status: 'pending', + meta: currentProgressMeta, + } + onProgressHandler(currentProgress) + + const destinationBlock = await destinationClient.getBlockNumber() + + const { fillTxReceipt, fillTxTimestamp, actionSuccess } = await waitForFillTx({ + deposit: { + originChainId: originChain.id, + destinationChainId: destinationChain.id, + destinationSpokePoolAddress: destinationSpokePoolAddress, + message: swapAndDepositData.depositData.message, + }, + depositId, + depositTxHash: depositTxReceipt.transactionHash, + destinationChainClient: destinationClient, + fromBlock: destinationBlock - 100n, + }) + + const fillLog = parseFillLogs(fillTxReceipt.logs) + + currentProgress = { + step: 'fill', + status: 'txSuccess', + txReceipt: fillTxReceipt, + fillTxTimestamp, + actionSuccess, + fillLog, + meta: currentProgressMeta, + } + onProgressHandler(currentProgress) + + return { + depositId, + swapAndBridgeTxReceipt: depositTxReceipt, + fillTxReceipt, + } + } catch (error) { + const executeError = error instanceof Error ? error : new Error(String(error)) + + currentProgress = { + ...currentProgress, + status: 'error', + error: executeError, + meta: currentProgressMeta, + } + onProgressHandler(currentProgress) + + if (!throwOnError) { + return { error: executeError } + } + + throw error + } +} diff --git a/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberAcrossAdapter/types.ts b/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberAcrossAdapter/types.ts new file mode 100644 index 0000000000..8b6942e4a8 --- /dev/null +++ b/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberAcrossAdapter/types.ts @@ -0,0 +1,186 @@ +import { parseDepositLogs, parseFillLogs } from '@across-protocol/app-sdk' +import { type Address, type Hash, type Hex, type TransactionReceipt, type Chain as ViemChain, WalletClient } from 'viem' + +import { AcrossDeposit } from 'pages/CrossChainSwap/adapters/AcrossAdapter/types' + +export enum TransferType { + Approval = 0, + Transfer = 1, + Permit2Approval = 2, +} + +export interface Fees { + amount: bigint + recipient: Address +} + +export interface BaseDepositData { + inputToken: Address + outputToken: Hex + outputAmount: bigint + depositor: Address + recipient: Hex + destinationChainId: bigint + exclusiveRelayer: Hex + quoteTimestamp: number + fillDeadline: number + exclusivityParameter: number + message: Hex +} + +export interface SwapAndDepositData { + submissionFees: Fees + depositData: BaseDepositData + swapToken: Address + exchange: Address + transferType: TransferType + swapTokenAmount: bigint + minExpectedInputTokenAmount: bigint + routerCalldata: Hex + enableProportionalAdjustment: boolean + spokePool: Address + nonce: bigint +} + +export interface RawBaseDepositData { + inputToken?: Address + outputToken?: Hex + outputAmount?: string + depositor?: Address + recipient?: Hex + destinationChainId?: string + exclusiveRelayer?: Hex + quoteTimestamp?: string | number + fillDeadline?: string | number + exclusivityParameter?: string | number + message?: Hex +} + +export interface RawSwapAndDepositData { + submissionFees?: { + amount?: string + recipient?: Address + } + depositData?: RawBaseDepositData + swapToken?: Address + exchange?: Address + transferType?: TransferType | string | number + swapTokenAmount?: string + minExpectedInputTokenAmount?: string + routerCalldata?: Hex + enableProportionalAdjustment?: boolean + spokePool?: Address + nonce?: string + isNative?: boolean +} + +export interface KyberAcrossRawQuote { + sourceSwap?: unknown | null + bridge: { + deposit: AcrossDeposit + } + swapAndDepositData?: RawSwapAndDepositData + spokePoolPeripheryAddress?: Address + destinationSpokePoolAddress?: Address +} + +export type ProgressMeta = ApproveMeta | SwapAndBridgeMeta | FillMeta | undefined + +export type ApproveMeta = { + approvalAmount: bigint + spender: Address +} + +export type SwapAndBridgeMeta = { + swapAndDepositData: SwapAndDepositData +} + +export type FillMeta = { + depositId: bigint +} + +export type SwapAndBridgeProgress = + | { + step: 'approve' + status: 'idle' + } + | { + step: 'approve' + status: 'txPending' + txHash: Hash + meta: ApproveMeta + } + | { + step: 'approve' + status: 'txSuccess' + txReceipt: TransactionReceipt + meta: ApproveMeta + } + | { + step: 'swapAndBridge' + status: 'simulationPending' + meta: SwapAndBridgeMeta + } + | { + step: 'swapAndBridge' + status: 'simulationSuccess' + txRequest: unknown + meta: SwapAndBridgeMeta + } + | { + step: 'swapAndBridge' + status: 'txPending' + txHash: Hash + txRequest?: unknown + meta: SwapAndBridgeMeta + } + | { + step: 'swapAndBridge' + status: 'txSuccess' + txReceipt: TransactionReceipt + depositId: bigint + depositLog: ReturnType + meta: SwapAndBridgeMeta + } + | { + step: 'fill' + status: 'pending' + meta: FillMeta + } + | { + step: 'fill' + status: 'txSuccess' + txReceipt: TransactionReceipt + fillTxTimestamp: bigint + actionSuccess: boolean | undefined + fillLog: ReturnType + meta: FillMeta + } + | { + step: 'approve' | 'swapAndBridge' | 'fill' + status: 'error' + error: Error + meta: ProgressMeta + } + +export interface ExecuteSwapAndBridgeParams { + walletClient: WalletClient + originChain: ViemChain + destinationChain: ViemChain + userAddress: Address + swapAndDepositData: SwapAndDepositData + spokePoolPeripheryAddress: Address + destinationSpokePoolAddress: Address + isNative?: boolean + infiniteApproval?: boolean + skipAllowanceCheck?: boolean + throwOnError?: boolean + onProgress?: (progress: SwapAndBridgeProgress) => void +} + +export interface ExecuteSwapAndBridgeResponse { + depositId?: bigint + swapAndBridgeTxReceipt?: TransactionReceipt + fillTxReceipt?: TransactionReceipt + error?: Error +} diff --git a/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberAcrossAdapter/utils.ts b/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberAcrossAdapter/utils.ts new file mode 100644 index 0000000000..4239f39420 --- /dev/null +++ b/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberAcrossAdapter/utils.ts @@ -0,0 +1,38 @@ +import { type Address, type Hex } from 'viem' + +import { + RawSwapAndDepositData, + SwapAndDepositData, + TransferType, +} from 'pages/CrossChainSwap/adapters/KyberAcrossAdapter/types' + +export function transformSwapAndDepositData(raw: RawSwapAndDepositData): SwapAndDepositData { + return { + submissionFees: { + amount: BigInt(raw.submissionFees?.amount || '0'), + recipient: raw.submissionFees?.recipient as Address, + }, + depositData: { + inputToken: raw.depositData?.inputToken as Address, + outputToken: raw.depositData?.outputToken as Hex, + outputAmount: BigInt(raw.depositData?.outputAmount || '0'), + depositor: raw.depositData?.depositor as Address, + recipient: raw.depositData?.recipient as Hex, + destinationChainId: BigInt(raw.depositData?.destinationChainId || '0'), + exclusiveRelayer: raw.depositData?.exclusiveRelayer as Hex, + quoteTimestamp: Number(raw.depositData?.quoteTimestamp || 0), + fillDeadline: Number(raw.depositData?.fillDeadline || 0), + exclusivityParameter: Number(raw.depositData?.exclusivityParameter || 0), + message: raw.depositData?.message as Hex, + }, + swapToken: raw.swapToken as Address, + exchange: raw.exchange as Address, + transferType: Number(raw.transferType) as TransferType, + swapTokenAmount: BigInt(raw.swapTokenAmount || '0'), + minExpectedInputTokenAmount: BigInt(raw.minExpectedInputTokenAmount || '0'), + routerCalldata: raw.routerCalldata as Hex, + enableProportionalAdjustment: Boolean(raw.enableProportionalAdjustment), + spokePool: raw.spokePool as Address, + nonce: BigInt(raw.nonce || '0'), + } +} diff --git a/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberCrossChainAdapter.ts b/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberCrossChainAdapter.ts deleted file mode 100644 index 91815dac6f..0000000000 --- a/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberCrossChainAdapter.ts +++ /dev/null @@ -1,479 +0,0 @@ -import { parseDepositLogs, parseFillLogs, waitForDepositTx, waitForFillTx } from '@across-protocol/app-sdk' -import { ChainId, Currency } from '@kyberswap/ks-sdk-core' -import { WalletAdapterProps } from '@solana/wallet-adapter-base' -import { Connection } from '@solana/web3.js' -import { - type Address, - type Hash, - type Hex, - type TransactionReceipt, - type Chain as ViemChain, - WalletClient, - createPublicClient, - encodeFunctionData, - http, - maxUint256, - parseAbi, -} from 'viem' -import { arbitrum, base, blast, bsc, linea, mainnet, optimism, polygon, scroll, unichain, zksync } from 'viem/chains' - -import { monad, plasma } from 'components/Web3Provider' -import { NETWORKS_INFO } from 'hooks/useChainsConfig' - -import { Quote } from '../registry' -import { - BaseSwapAdapter, - Chain, - NormalizedQuote, - NormalizedTxResponse, - QuoteParams, - SwapStatus, -} from './BaseSwapAdapter' - -// Chain ID to viem Chain mapping -const chainIdToViemChain: Record = { - [ChainId.MAINNET]: mainnet, - [ChainId.ARBITRUM]: arbitrum, - [ChainId.BSCMAINNET]: bsc, - [ChainId.OPTIMISM]: optimism, - [ChainId.LINEA]: linea, - [ChainId.MATIC]: polygon, - [ChainId.ZKSYNC]: zksync, - [ChainId.BASE]: base, - [ChainId.SCROLL]: scroll, - [ChainId.BLAST]: blast, - [ChainId.UNICHAIN]: unichain, - [ChainId.PLASMA]: plasma, - [ChainId.MONAD]: monad, -} - -// ============================================ -// Progress Tracking Types -// ============================================ - -type ApproveMeta = { - approvalAmount: bigint - spender: Address -} - -export type CrossChainExecuteProgress = - | { step: 'approve'; status: 'checking' } - | { step: 'approve'; status: 'txPending'; txHash: Hash; meta: ApproveMeta } - | { step: 'approve'; status: 'txSuccess'; txReceipt: TransactionReceipt; meta: ApproveMeta } - | { step: 'ksExecute'; status: 'simulationPending' } - | { step: 'ksExecute'; status: 'simulationSuccess'; txRequest: any } - | { step: 'ksExecute'; status: 'txPending'; txHash: Hash } - | { step: 'ksExecute'; status: 'txSuccess'; txReceipt: TransactionReceipt; depositId: bigint; depositLog: any } - | { step: 'fill'; status: 'pending'; depositId: bigint } - | { - step: 'fill' - status: 'txSuccess' - txReceipt: TransactionReceipt - fillTxTimestamp: bigint - actionSuccess: boolean | undefined - fillLog: ReturnType - } - | { step: 'approve' | 'ksExecute' | 'fill'; status: 'error'; error: Error } - -// ============================================ -// Execute Parameters -// ============================================ - -export interface CrossChainExecuteResponse { - depositId?: bigint - swapAndBridgeTxReceipt?: TransactionReceipt - fillTxReceipt?: TransactionReceipt - error?: Error -} - -export interface ExecuteParams { - walletClient: WalletClient - originChain: ViemChain - destinationChain: ViemChain - userAddress: Address - to: Address - txData: Hex - value: bigint - destinationSpokePoolAddress: Address - message: Hex // Needed for fill monitoring - // Approval params - inputToken: Address - inputAmount: bigint - isNativeToken: boolean - infiniteApproval?: boolean - // Options - throwOnError?: boolean - onProgress?: (progress: CrossChainExecuteProgress) => void -} - -// ============================================ -// KyberCrossChainAdapter -// ============================================ - -export class KyberCrossChainAdapter extends BaseSwapAdapter { - getName(): string { - return 'KyberAcross' - } - - getIcon(): string { - return 'https://i.ibb.co/fVLsZryT/kyberacross.jpg' - } - - getSupportedChains(): Chain[] { - return [ - ChainId.MAINNET, - ChainId.ARBITRUM, - ChainId.OPTIMISM, - ChainId.LINEA, - ChainId.MATIC, - ChainId.ZKSYNC, - ChainId.BASE, - ChainId.SCROLL, - ChainId.BLAST, - ChainId.UNICHAIN, - ChainId.BSCMAINNET, - ChainId.PLASMA, - ChainId.MONAD, - ] - } - - getSupportedTokens(_sourceChain: Chain, _destChain: Chain): Currency[] { - return [] - } - - // getQuote is empty - we use the stream API response for this provider - async getQuote(_params: QuoteParams): Promise { - throw new Error('KyberCrossChain does not support direct quote fetching. Use stream API response instead.') - } - - async executeSwap( - quote: Quote, - walletClient: WalletClient, - _nearWalletClient?: any, - _sendBtcFn?: (params: { recipient: string; amount: string | number }) => Promise, - _sendTransaction?: WalletAdapterProps['sendTransaction'], - _connection?: Connection, - ): Promise { - const rawQuote = quote.quote.rawQuote - - console.log('KyberCrossChainAdapter rawQuote ======== ', rawQuote) - - // Validate new required fields from API - const to = rawQuote.to as Address - const txData = rawQuote.txData as Hex - const value = BigInt(rawQuote.value || '0') - const destinationSpokePoolAddress = rawQuote.destinationSpokePoolAddress as Address - - if (!to || !txData) { - throw new Error('Missing required transaction data (to, txData)') - } - - if (!destinationSpokePoolAddress) { - throw new Error('Missing destinationSpokePoolAddress') - } - - const originChainId = quote.quote.quoteParams.fromChain as ChainId - const originChain = chainIdToViemChain[originChainId] - if (!originChain) throw new Error(`Unsupported chain: ${originChainId}`) - - const destinationChainId = quote.quote.quoteParams.toChain as ChainId - const destinationChain = chainIdToViemChain[destinationChainId] - if (!destinationChain) throw new Error(`Unsupported destination chain: ${destinationChainId}`) - - // Extract message for fill monitoring (from bridge data if available) - const message = (rawQuote.bridge?.deposit?.message || '0x') as Hex - - // Get user address - const userAddress = quote.quote.quoteParams.sender as Address - - // Get input token info for approval - const fromToken = quote.quote.quoteParams.fromToken as any - const isNativeToken = rawQuote.isNativeToken || fromToken?.isNative || false - const inputToken = isNativeToken - ? ('0x0000000000000000000000000000000000000000' as Address) - : ((fromToken?.wrapped?.address || fromToken?.address) as Address) - const inputAmount = BigInt(quote.quote.quoteParams.amount) - - return new Promise((resolve, reject) => { - this.execute({ - walletClient, - originChain, - destinationChain, - userAddress, - to, - txData, - value, - destinationSpokePoolAddress, - message, - inputToken, - inputAmount, - isNativeToken, - infiniteApproval: false, - throwOnError: true, - onProgress: progress => { - if (progress.step === 'ksExecute' && 'txHash' in progress) { - resolve({ - sender: quote.quote.quoteParams.sender, - sourceTxHash: progress.txHash, - adapter: this.getName(), - id: progress.txHash, - sourceChain: quote.quote.quoteParams.fromChain, - targetChain: quote.quote.quoteParams.toChain, - inputAmount: quote.quote.quoteParams.amount, - outputAmount: quote.quote.outputAmount.toString(), - sourceToken: quote.quote.quoteParams.fromToken, - targetToken: quote.quote.quoteParams.toToken, - timestamp: new Date().getTime(), - amountInUsd: quote.quote.inputUsd, - amountOutUsd: quote.quote.outputUsd, - platformFeePercent: quote.quote.platformFeePercent, - recipient: quote.quote.quoteParams.recipient, - }) - } - }, - }).catch(reject) - }) - } - - /** - * Executes a cross-chain swap transaction - * Flow: - * 1. Checks and handles token approval to AllowanceHub (if ERC20) - * 2. Sends pre-encoded transaction to the target contract (AllowanceHub) - * 3. Monitors for fill on destination chain - * - * Note: Transaction data (to, txData, value) is pre-encoded by the backend API - */ - async execute(params: ExecuteParams): Promise { - const { - walletClient, - originChain, - destinationChain, - userAddress, - to, - txData, - value, - destinationSpokePoolAddress, - message, - inputToken, - inputAmount, - isNativeToken, - infiniteApproval = false, - throwOnError = false, - onProgress, - } = params - - const rpcUrl = NETWORKS_INFO[originChain.id as ChainId]?.defaultRpcUrl - if (!rpcUrl) { - throw new Error(`No RPC URL found for chain: ${originChain.id}`) - } - - const originClient = createPublicClient({ - chain: originChain, - transport: http(rpcUrl), - }) - - try { - // --- Step 1: Check and handle approval if necessary (skip for native tokens) --- - if (!isNativeToken) { - onProgress?.({ - step: 'approve', - status: 'checking', - }) - - const allowance = await originClient.readContract({ - address: inputToken, - abi: parseAbi(['function allowance(address owner, address spender) public view returns (uint256)']), - functionName: 'allowance', - args: [userAddress, to], // `to` is the AllowanceHub address - }) - - if (inputAmount > allowance) { - const approvalAmount = infiniteApproval ? maxUint256 : inputAmount - - if (!walletClient.account) { - throw new Error('Wallet account not connected') - } - - // Execute approval to AllowanceHub - const approveCalldata = encodeFunctionData({ - abi: parseAbi(['function approve(address spender, uint256 value)']), - args: [to, approvalAmount], // Approve AllowanceHub (the `to` address) - }) - - const approveTxHash = await walletClient.sendTransaction({ - account: walletClient.account, - chain: originChain, - to: inputToken, - data: approveCalldata, - }) - - onProgress?.({ - step: 'approve', - status: 'txPending', - txHash: approveTxHash, - meta: { approvalAmount, spender: to }, - }) - - // Wait for approval confirmation - const approveTxReceipt = await originClient.waitForTransactionReceipt({ - hash: approveTxHash, - }) - - onProgress?.({ - step: 'approve', - status: 'txSuccess', - txReceipt: approveTxReceipt, - meta: { approvalAmount, spender: to }, - }) - } - } - - // --- Step 2: Report simulation pending --- - onProgress?.({ - step: 'ksExecute', - status: 'simulationPending', - }) - - // --- Simulate transaction --- - await originClient.call({ - to, - data: txData, - value, - account: walletClient.account, - }) - - onProgress?.({ - step: 'ksExecute', - status: 'simulationSuccess', - txRequest: { to, data: txData, value }, - }) - - // --- Execute transaction --- - if (!walletClient.account) { - throw new Error('Wallet account not connected') - } - - const txHash = await walletClient.sendTransaction({ - account: walletClient.account, - to, - data: txData, - value, - chain: originChain, - }) - - onProgress?.({ - step: 'ksExecute', - status: 'txPending', - txHash, - }) - - // --- Wait for deposit tx and parse deposit from logs --- - const { depositId, depositTxReceipt } = await waitForDepositTx({ - transactionHash: txHash, - originChainId: originChain.id, - publicClient: originClient, - }) - - const depositLog = parseDepositLogs(depositTxReceipt.logs) - - onProgress?.({ - step: 'ksExecute', - status: 'txSuccess', - txReceipt: depositTxReceipt, - depositId, - depositLog, - }) - - // --- Step 3: Wait for fill on destination chain --- - onProgress?.({ - step: 'fill', - status: 'pending', - depositId, - }) - - const destRpcUrl = NETWORKS_INFO[destinationChain.id as ChainId]?.defaultRpcUrl - if (!destRpcUrl) { - throw new Error(`No RPC URL found for destination chain: ${destinationChain.id}`) - } - - const destClient = createPublicClient({ - chain: destinationChain, - transport: http(destRpcUrl), - }) - - const destinationBlock = await destClient.getBlockNumber() - - const { fillTxReceipt, fillTxTimestamp } = await waitForFillTx({ - deposit: { - originChainId: originChain.id, - destinationChainId: destinationChain.id, - destinationSpokePoolAddress, - message, - }, - depositId, - depositTxHash: depositTxReceipt.transactionHash, - destinationChainClient: destClient, - fromBlock: destinationBlock - 100n, - }) - - const fillLog = parseFillLogs(fillTxReceipt.logs) - - // Note: actionSuccess from SDK checks MulticallHandler events. - // Since we use our own BridgeAdapter (not MulticallHandler), - // we consider the action successful if the fill tx succeeded. - // The BridgeAdapter reverts on failure, so tx success = action success. - const actionSuccessOverride = true - - onProgress?.({ - step: 'fill', - status: 'txSuccess', - txReceipt: fillTxReceipt, - fillTxTimestamp, - actionSuccess: actionSuccessOverride, - fillLog, - }) - - return { - depositId, - swapAndBridgeTxReceipt: depositTxReceipt, - fillTxReceipt, - } - } catch (error: any) { - onProgress?.({ - step: 'ksExecute', - status: 'error', - error, - }) - - if (throwOnError) { - throw error - } - - return { - depositId: undefined, - swapAndBridgeTxReceipt: undefined, - fillTxReceipt: undefined, - error, - } - } - } - - async getTransactionStatus(params: NormalizedTxResponse): Promise { - try { - const res = await fetch(`https://app.across.to/api/deposit/status?depositTxHash=${params.sourceTxHash}`).then( - res => res.json(), - ) - return { - txHash: res.fillTx || '', - status: res.status === 'refunded' ? 'Refunded' : res.status === 'filled' ? 'Success' : 'Processing', - } - } catch (error) { - console.error('Error fetching transaction status:', error) - return { - txHash: '', - status: 'Processing', - } - } - } -} diff --git a/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberCrossChainAdapter/constants.ts b/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberCrossChainAdapter/constants.ts new file mode 100644 index 0000000000..c6b0edffbd --- /dev/null +++ b/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberCrossChainAdapter/constants.ts @@ -0,0 +1,37 @@ +import { ChainId } from '@kyberswap/ks-sdk-core' +import { type Chain as ViemChain } from 'viem' +import { arbitrum, base, blast, bsc, linea, mainnet, optimism, polygon, scroll, unichain, zksync } from 'viem/chains' + +import { monad, plasma } from 'components/Web3Provider' + +export const kyberCrossSupportedChains = [ + ChainId.MAINNET, + ChainId.ARBITRUM, + ChainId.OPTIMISM, + ChainId.LINEA, + ChainId.MATIC, + ChainId.ZKSYNC, + ChainId.BASE, + ChainId.SCROLL, + ChainId.BLAST, + ChainId.UNICHAIN, + ChainId.BSCMAINNET, + ChainId.PLASMA, + ChainId.MONAD, +] + +export const chainIdToViemChain: Record = { + [ChainId.MAINNET]: mainnet, + [ChainId.ARBITRUM]: arbitrum, + [ChainId.BSCMAINNET]: bsc, + [ChainId.OPTIMISM]: optimism, + [ChainId.LINEA]: linea, + [ChainId.MATIC]: polygon, + [ChainId.ZKSYNC]: zksync, + [ChainId.BASE]: base, + [ChainId.SCROLL]: scroll, + [ChainId.BLAST]: blast, + [ChainId.UNICHAIN]: unichain, + [ChainId.PLASMA]: plasma, + [ChainId.MONAD]: monad, +} diff --git a/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberCrossChainAdapter/index.ts b/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberCrossChainAdapter/index.ts new file mode 100644 index 0000000000..0dfdb414b2 --- /dev/null +++ b/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberCrossChainAdapter/index.ts @@ -0,0 +1,179 @@ +import { ChainId, Currency } from '@kyberswap/ks-sdk-core' +import { WalletAdapterProps } from '@solana/wallet-adapter-base' +import { Connection } from '@solana/web3.js' +import { type Address, type Hash, WalletClient, createPublicClient, http } from 'viem' + +import kyberswapIcon from 'assets/images/kyberswap.ico' +import { ZERO_ADDRESS } from 'constants/index' +import { NETWORKS_INFO } from 'hooks/useChainsConfig' +import { + BaseSwapAdapter, + Chain, + NormalizedQuote, + NormalizedTxResponse, + QuoteParams, + SwapProvider, + SwapStatus, +} from 'pages/CrossChainSwap/adapters/BaseSwapAdapter' +import { + chainIdToViemChain, + kyberCrossSupportedChains, +} from 'pages/CrossChainSwap/adapters/KyberCrossChainAdapter/constants' +import { executeKyberCross } from 'pages/CrossChainSwap/adapters/KyberCrossChainAdapter/service' +import { + CrossChainExecuteResponse, + ExecuteParams, + KyberCrossRawQuote, +} from 'pages/CrossChainSwap/adapters/KyberCrossChainAdapter/types' +import { + getKyberCrossTx, + getKyberCrossTxData, + getResponseData, + getRouteProvider, + normalizeProvider, +} from 'pages/CrossChainSwap/adapters/KyberCrossChainAdapter/utils' +import { Quote } from 'pages/CrossChainSwap/registry' + +// ============================================ +// KyberCrossChainAdapter +// ============================================ + +export class KyberCrossChainAdapter extends BaseSwapAdapter { + constructor(private readonly getAdapterByName?: (name?: string) => SwapProvider | undefined) { + super() + } + + getName(): string { + return 'KyberCross' + } + + getIcon(): string { + return kyberswapIcon + } + + getSupportedChains(): Chain[] { + return kyberCrossSupportedChains + } + + getSupportedTokens(_sourceChain: Chain, _destChain: Chain): Currency[] { + return [] + } + + // getQuote is empty - we use the stream API response for this provider + async getQuote(_params: QuoteParams): Promise { + throw new Error('KyberCross does not support direct quote fetching. Use stream API response instead.') + } + + async executeSwap( + quote: Quote, + walletClient: WalletClient, + _nearWalletClient?: unknown, + _sendBtcFn?: (params: { recipient: string; amount: string | number }) => Promise, + _sendTransaction?: WalletAdapterProps['sendTransaction'], + _connection?: Connection, + ): Promise { + const normalizedQuote = quote.quote + const quoteParams = normalizedQuote.quoteParams + const rawQuote = normalizedQuote.rawQuote as KyberCrossRawQuote + const responseData = getResponseData(rawQuote) + const routePlan = responseData?.route_plan + const routeProvider = getRouteProvider(rawQuote, responseData) + + const { to, txData, value } = getKyberCrossTxData(getKyberCrossTx(rawQuote, responseData)) + + const originChainId = quoteParams.fromChain as ChainId + const originChain = chainIdToViemChain[originChainId] + if (!originChain) throw new Error(`Unsupported chain: ${originChainId}`) + + const destinationChainId = quoteParams.toChain as ChainId + const destinationChain = chainIdToViemChain[destinationChainId] + if (!destinationChain) throw new Error(`Unsupported destination chain: ${destinationChainId}`) + + const userAddress = quoteParams.sender as Address + + const fromToken = quoteParams.fromToken as Currency + const isNativeToken = rawQuote.isNativeToken || fromToken.isNative + const inputToken = (isNativeToken ? ZERO_ADDRESS : fromToken.wrapped.address) as Address + const inputAmount = BigInt(quoteParams.amount) + + return new Promise((resolve, reject) => { + this.execute({ + walletClient, + originChain, + userAddress, + to, + txData, + value, + inputToken, + inputAmount, + isNativeToken, + infiniteApproval: false, + throwOnError: true, + onProgress: progress => { + if (progress.step === 'ksExecute' && 'txHash' in progress) { + resolve({ + sender: quoteParams.sender, + sourceTxHash: progress.txHash, + adapter: this.getName(), + id: progress.txHash, + sourceChain: quoteParams.fromChain, + targetChain: quoteParams.toChain, + inputAmount: quoteParams.amount, + outputAmount: normalizedQuote.outputAmount.toString(), + sourceToken: quoteParams.fromToken, + targetToken: quoteParams.toToken, + timestamp: new Date().getTime(), + amountInUsd: normalizedQuote.inputUsd, + amountOutUsd: normalizedQuote.outputUsd, + platformFeePercent: normalizedQuote.platformFeePercent, + recipient: quoteParams.recipient, + bridgeProvider: routeProvider, + routeId: routePlan?.route_id, + }) + } + }, + }).catch(reject) + }) + } + + async execute(params: ExecuteParams): Promise { + return executeKyberCross(params) + } + + async getTransactionStatus(params: NormalizedTxResponse): Promise { + const provider = normalizeProvider(params.bridgeProvider) + const adapter = this.getAdapterByName?.(provider) + + if (adapter && normalizeProvider(adapter.getName()) !== normalizeProvider(this.getName())) { + return adapter.getTransactionStatus({ + ...params, + adapter: adapter.getName(), + id: params.routeId || params.id, + }) + } + + const sourceChainId = params.sourceChain as ChainId + const sourceChain = chainIdToViemChain[sourceChainId] + const rpcUrl = NETWORKS_INFO[sourceChainId]?.defaultRpcUrl + + if (!sourceChain || !rpcUrl) { + return { txHash: '', status: 'Processing' } + } + + const publicClient = createPublicClient({ + chain: sourceChain, + transport: http(rpcUrl), + }) + + try { + const receipt = await publicClient.getTransactionReceipt({ hash: params.sourceTxHash as Hash }) + + return { + txHash: '', + status: receipt.status === 'reverted' ? 'Failed' : 'Processing', + } + } catch (error) { + return { txHash: '', status: 'Processing' } + } + } +} diff --git a/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberCrossChainAdapter/service.ts b/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberCrossChainAdapter/service.ts new file mode 100644 index 0000000000..adb2ce7a73 --- /dev/null +++ b/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberCrossChainAdapter/service.ts @@ -0,0 +1,153 @@ +import { ChainId } from '@kyberswap/ks-sdk-core' +import { createPublicClient, encodeFunctionData, http, maxUint256, parseAbi } from 'viem' + +import { NETWORKS_INFO } from 'hooks/useChainsConfig' +import { CrossChainExecuteResponse, ExecuteParams } from 'pages/CrossChainSwap/adapters/KyberCrossChainAdapter/types' + +export async function executeKyberCross(params: ExecuteParams): Promise { + const { + walletClient, + originChain, + userAddress, + to, + txData, + value, + inputToken, + inputAmount, + isNativeToken, + infiniteApproval = false, + throwOnError = false, + onProgress, + } = params + + const rpcUrl = NETWORKS_INFO[originChain.id as ChainId]?.defaultRpcUrl + if (!rpcUrl) { + throw new Error(`No RPC URL found for chain: ${originChain.id}`) + } + + const originClient = createPublicClient({ + chain: originChain, + transport: http(rpcUrl), + }) + + try { + if (!isNativeToken) { + onProgress?.({ + step: 'approve', + status: 'checking', + }) + + const allowance = await originClient.readContract({ + address: inputToken, + abi: parseAbi(['function allowance(address owner, address spender) public view returns (uint256)']), + functionName: 'allowance', + args: [userAddress, to], + }) + + if (inputAmount > allowance) { + const approvalAmount = infiniteApproval ? maxUint256 : inputAmount + + if (!walletClient.account) { + throw new Error('Wallet account not connected') + } + + const approveCalldata = encodeFunctionData({ + abi: parseAbi(['function approve(address spender, uint256 value)']), + args: [to, approvalAmount], + }) + + const approveTxHash = await walletClient.sendTransaction({ + account: walletClient.account, + chain: originChain, + to: inputToken, + data: approveCalldata, + }) + + onProgress?.({ + step: 'approve', + status: 'txPending', + txHash: approveTxHash, + meta: { approvalAmount, spender: to }, + }) + + const approveTxReceipt = await originClient.waitForTransactionReceipt({ + hash: approveTxHash, + }) + + onProgress?.({ + step: 'approve', + status: 'txSuccess', + txReceipt: approveTxReceipt, + meta: { approvalAmount, spender: to }, + }) + } + } + + onProgress?.({ + step: 'ksExecute', + status: 'simulationPending', + }) + + await originClient.call({ + to, + data: txData, + value, + account: walletClient.account, + }) + + onProgress?.({ + step: 'ksExecute', + status: 'simulationSuccess', + txRequest: { to, data: txData, value }, + }) + + if (!walletClient.account) { + throw new Error('Wallet account not connected') + } + + const txHash = await walletClient.sendTransaction({ + account: walletClient.account, + to, + data: txData, + value, + chain: originChain, + }) + + onProgress?.({ + step: 'ksExecute', + status: 'txPending', + txHash, + }) + + const txReceipt = await originClient.waitForTransactionReceipt({ + hash: txHash, + }) + + onProgress?.({ + step: 'ksExecute', + status: 'txSuccess', + txReceipt, + }) + + return { + txReceipt, + } + } catch (error) { + const executeError = error instanceof Error ? error : new Error(String(error)) + + onProgress?.({ + step: 'ksExecute', + status: 'error', + error: executeError, + }) + + if (throwOnError) { + throw error + } + + return { + txReceipt: undefined, + error: executeError, + } + } +} diff --git a/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberCrossChainAdapter/types.ts b/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberCrossChainAdapter/types.ts new file mode 100644 index 0000000000..187846f95d --- /dev/null +++ b/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberCrossChainAdapter/types.ts @@ -0,0 +1,71 @@ +import { type Address, type Hash, type Hex, type TransactionReceipt, type Chain as ViemChain, WalletClient } from 'viem' + +export type KyberCrossTx = { + to?: Address + data?: Hex + txData?: Hex + value?: string | number | bigint +} + +export type KyberCrossRoutePlan = { + route_id?: string + provider?: string +} + +export type KyberCrossResponseData = { + route_plan?: KyberCrossRoutePlan + build?: { + tx?: KyberCrossTx + } +} + +export type KyberCrossRawQuote = { + request_id?: string + data?: KyberCrossResponseData | Hex + steps?: { + provider?: string + }[] + build?: { + tx?: KyberCrossTx + } + tx?: KyberCrossTx + to?: Address + txData?: Hex + value?: string | number | bigint + isNativeToken?: boolean +} + +export type ApproveMeta = { + approvalAmount: bigint + spender: Address +} + +export type CrossChainExecuteProgress = + | { step: 'approve'; status: 'checking' } + | { step: 'approve'; status: 'txPending'; txHash: Hash; meta: ApproveMeta } + | { step: 'approve'; status: 'txSuccess'; txReceipt: TransactionReceipt; meta: ApproveMeta } + | { step: 'ksExecute'; status: 'simulationPending' } + | { step: 'ksExecute'; status: 'simulationSuccess'; txRequest: unknown } + | { step: 'ksExecute'; status: 'txPending'; txHash: Hash } + | { step: 'ksExecute'; status: 'txSuccess'; txReceipt: TransactionReceipt } + | { step: 'approve' | 'ksExecute'; status: 'error'; error: Error } + +export interface CrossChainExecuteResponse { + txReceipt?: TransactionReceipt + error?: Error +} + +export interface ExecuteParams { + walletClient: WalletClient + originChain: ViemChain + userAddress: Address + to: Address + txData: Hex + value: bigint + inputToken: Address + inputAmount: bigint + isNativeToken: boolean + infiniteApproval?: boolean + throwOnError?: boolean + onProgress?: (progress: CrossChainExecuteProgress) => void +} diff --git a/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberCrossChainAdapter/utils.ts b/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberCrossChainAdapter/utils.ts new file mode 100644 index 0000000000..fd285bc1e3 --- /dev/null +++ b/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/KyberCrossChainAdapter/utils.ts @@ -0,0 +1,30 @@ +import { type Address, type Hex } from 'viem' + +import { + KyberCrossRawQuote, + KyberCrossResponseData, + KyberCrossTx, +} from 'pages/CrossChainSwap/adapters/KyberCrossChainAdapter/types' + +export const getResponseData = (rawQuote: KyberCrossRawQuote): KyberCrossResponseData | undefined => + typeof rawQuote.data === 'object' ? rawQuote.data : undefined + +export const normalizeProvider = (provider?: string) => provider?.toLowerCase().replace(/\s+/g, '') + +export const getRouteProvider = (rawQuote: KyberCrossRawQuote, responseData?: KyberCrossResponseData) => + responseData?.route_plan?.provider || rawQuote.steps?.find(step => step.provider)?.provider + +export const getKyberCrossTx = (rawQuote: KyberCrossRawQuote, responseData?: KyberCrossResponseData): KyberCrossTx => + responseData?.build?.tx || rawQuote.build?.tx || rawQuote.tx || (rawQuote as KyberCrossTx) + +export const getKyberCrossTxData = (tx: KyberCrossTx) => { + const to = tx.to as Address + const txData = (typeof tx.data === 'string' ? tx.data : tx.txData) as Hex + const value = BigInt(tx.value || '0') + + if (!to || !txData) { + throw new Error('Missing required transaction data (to, txData)') + } + + return { to, txData, value } +} diff --git a/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/types.ts b/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/types.ts index b1d5f378bc..46f7c7fc19 100644 --- a/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/types.ts +++ b/apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/types.ts @@ -115,6 +115,8 @@ export interface NormalizedTxResponse { targetTxHash?: string timestamp: number status?: 'Processing' | 'Success' | 'Failed' | 'Refunded' + bridgeProvider?: string + routeId?: string // Enriched fields for data analysis amountInUsd: number amountOutUsd: number diff --git a/apps/kyberswap-interface/src/pages/CrossChainSwap/components/QuoteProviderName.tsx b/apps/kyberswap-interface/src/pages/CrossChainSwap/components/QuoteProviderName.tsx index 7b9a2088ab..bd0cb2f1ea 100644 --- a/apps/kyberswap-interface/src/pages/CrossChainSwap/components/QuoteProviderName.tsx +++ b/apps/kyberswap-interface/src/pages/CrossChainSwap/components/QuoteProviderName.tsx @@ -6,7 +6,8 @@ import { registry } from 'pages/CrossChainSwap/hooks/useCrossChainSwap' import { Quote } from 'pages/CrossChainSwap/registry' const getStepProviders = (quote: Quote): SwapProvider[] => { - if (quote.adapter.getName().toLowerCase() !== 'kyberacross') return [] + const adapterName = quote.adapter.getName().toLowerCase() + if (adapterName !== 'kyberacross' && adapterName !== 'kybercross') return [] const steps = quote.quote.rawQuote?.steps if (!Array.isArray(steps)) return [] diff --git a/apps/kyberswap-interface/src/pages/CrossChainSwap/factory.ts b/apps/kyberswap-interface/src/pages/CrossChainSwap/factory.ts index 0ab68f3952..3bf525c9b4 100644 --- a/apps/kyberswap-interface/src/pages/CrossChainSwap/factory.ts +++ b/apps/kyberswap-interface/src/pages/CrossChainSwap/factory.ts @@ -16,6 +16,8 @@ import { NearIntentsAdapter } from './adapters/NearIntentsAdapter' import { OptimexAdapter } from './adapters/OptimexAdapter' import { OrbiterAdapter } from './adapters/OrbiterAdapter' +const normalizeAdapterName = (name: string) => name.toLowerCase().replace(/\s+/g, '') + // Factory for creating swap provider instances export class CrossChainSwapFactory { // Singleton instances (lazy loaded) @@ -129,7 +131,9 @@ export class CrossChainSwapFactory { static getKyberCrossChainAdapter(): KyberCrossChainAdapter { if (!CrossChainSwapFactory.kyberCrossChainInstance) { - CrossChainSwapFactory.kyberCrossChainInstance = new KyberCrossChainAdapter() + CrossChainSwapFactory.kyberCrossChainInstance = new KyberCrossChainAdapter(name => + name ? CrossChainSwapFactory.getAdapterByName(name) : undefined, + ) } return CrossChainSwapFactory.kyberCrossChainInstance } @@ -149,21 +153,21 @@ export class CrossChainSwapFactory { CrossChainSwapFactory.getKsApdater(), // CrossChainSwapFactory.getOrbiterAdapter(), CrossChainSwapFactory.getBungeeAdapter(), - // CrossChainSwapFactory.getKyberAcrossAdapter(), - // CrossChainSwapFactory.getKyberCrossChainAdapter(), + CrossChainSwapFactory.getKyberAcrossAdapter(), + CrossChainSwapFactory.getKyberCrossChainAdapter(), ] } // Get adapter by name static getAdapterByName(name: string): SwapProvider | undefined { - switch (name.toLowerCase()) { + switch (normalizeAdapterName(name)) { case 'across': return CrossChainSwapFactory.getAcrossAdapter() case 'relay': return CrossChainSwapFactory.getRelayAdapter() case 'xyfinance': return CrossChainSwapFactory.getXyFinanceAdapter() - case 'near intents': + case 'nearintents': return CrossChainSwapFactory.getNearIntentsAdapter() case 'mayan': return CrossChainSwapFactory.getMayanAdapter() @@ -182,6 +186,8 @@ export class CrossChainSwapFactory { case 'bungee': return CrossChainSwapFactory.getBungeeAdapter() case 'kyberacross': + return CrossChainSwapFactory.getKyberAcrossAdapter() + case 'kybercross': return CrossChainSwapFactory.getKyberCrossChainAdapter() default: return undefined