Skip to content

[Issue-4321] Implement Bridge-Swap process for cross-chain swap on EVM #4340

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
May 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,8 @@ export class SwapBaseHandler {

if (needEditAmount) {
bnSendingValue = BigN(selectedQuote.toAmount).multipliedBy(DEFAULT_EXCESS_AMOUNT_WEIGHT); // need to round
} else {
} else { // todo: remove
console.log('The code cannot run into here, if it runs into here, pls ask dev to check');
bnSendingValue = BigN(selectedQuote.toAmount);
}
}
Expand Down Expand Up @@ -440,7 +441,7 @@ export class SwapBaseHandler {
const isEvmAddress = isEthereumAddress(recipient);
const isEvmDestChain = _isChainEvmCompatible(swapToChain);

if ((isEvmAddress && !isEvmDestChain) || (!isEvmAddress && isEvmDestChain)) { // todo: update this condition
if (isEvmAddress !== isEvmDestChain) { // todo: update condition if support swap chain # EVM or Substrate
return [new TransactionError(SwapErrorType.INVALID_RECIPIENT)];
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { ChainType, ExtrinsicType } from '@subwallet/extension-base/background/K
import { validateTypedSignMessageDataV3V4 } from '@subwallet/extension-base/core/logic-validation';
import { estimateTxFee, getERC20Allowance, getERC20SpendingApprovalTx } from '@subwallet/extension-base/koni/api/contract-handler/evm/web3';
import { createAcrossBridgeExtrinsic, CreateXcmExtrinsicProps } from '@subwallet/extension-base/services/balance-service/transfer/xcm';
import { getAcrossQuote } from '@subwallet/extension-base/services/balance-service/transfer/xcm/acrossBridge';
import { AcrossQuote, getAcrossQuote } from '@subwallet/extension-base/services/balance-service/transfer/xcm/acrossBridge';
import { DEFAULT_EXCESS_AMOUNT_WEIGHT, FEE_RATE_MULTIPLIER } from '@subwallet/extension-base/services/swap-service/utils';
import TransactionService from '@subwallet/extension-base/services/transaction-service';
import { ApproveStepMetadata, BaseStepDetail, BaseSwapStepMetadata, BasicTxErrorType, CommonOptimalSwapPath, CommonStepFeeInfo, CommonStepType, DynamicSwapType, EvmFeeInfo, FeeOptionKey, GenSwapStepFuncV2, HandleYieldStepData, OptimalSwapPathParamsV2, PermitSwapData, SwapBaseTxData, SwapFeeType, SwapProviderId, SwapStepType, SwapSubmitParams, SwapSubmitStepData, TokenSpendingApprovalParams, ValidateSwapProcessParams } from '@subwallet/extension-base/types';
import { _reformatAddressWithChain } from '@subwallet/extension-base/utils';
Expand Down Expand Up @@ -198,7 +199,8 @@ export class UniswapHandler implements SwapBaseInterface {
const stepFuncList: GenSwapStepFuncV2[] = [];
/**
* approve - permit - swap or
* approve - permit - swap - approve - bridge
* approve - permit - swap - approve - bridge or
* approve - bridge - approve - permit - swap
*/

params.path.forEach((step) => {
Expand Down Expand Up @@ -228,12 +230,28 @@ export class UniswapHandler implements SwapBaseInterface {
}

async getApprovalStep (params: OptimalSwapPathParamsV2, stepIndex: number): Promise<[BaseStepDetail, CommonStepFeeInfo] | undefined> {
if (stepIndex === 0) {
/**
* Explain: All processes will go through one of below processes. If a step do not have, it returns undefined and
* the stepIndex is still counted up
*
* Processes:
* approve - permit - swap or
* approve - permit - swap - approve - bridge or
* approve - bridge - approve - permit - swap
*/
const actionList = JSON.stringify(params.path.map((step) => step.action));
const swap = actionList === JSON.stringify([DynamicSwapType.SWAP]);
const swapBridge = actionList === JSON.stringify([DynamicSwapType.SWAP, DynamicSwapType.BRIDGE]);
const bridgeSwap = actionList === JSON.stringify([DynamicSwapType.BRIDGE, DynamicSwapType.SWAP]);
const isApproveBridge = (stepIndex === 3 && swapBridge) || (stepIndex === 0 && bridgeSwap);
const isApproveSwap = (stepIndex === 0 && swap) || (stepIndex === 0 && swapBridge) || (stepIndex === 2 && bridgeSwap);

if (isApproveSwap) {
return this.getApproveSwap(params);
}

if (stepIndex === 3) {
return this.getApproveBridge(params);
if (isApproveBridge) {
return this.getApproveBridge(params, bridgeSwap);
}

return Promise.resolve(undefined);
Expand Down Expand Up @@ -333,23 +351,29 @@ export class UniswapHandler implements SwapBaseInterface {
return Promise.resolve([submitStep, feeInfo]);
}

async getApproveBridge (params: OptimalSwapPathParamsV2): Promise<[BaseStepDetail, CommonStepFeeInfo] | undefined> {
const quote = params.selectedQuote;
async getApproveBridge (params: OptimalSwapPathParamsV2, isBridgeFirst: boolean): Promise<[BaseStepDetail, CommonStepFeeInfo] | undefined> {
const { path, request, selectedQuote } = params;

if (!quote) {
if (!selectedQuote) {
return Promise.resolve(undefined);
}

console.log('params', params);
const sendingAmount = quote.toAmount;
const senderAddress = params.request.address;
const fromTokenInfo = this.chainService.getAssetBySlug(quote.pair.to);
const bridgePairInfo = path.find((action) => action.action === DynamicSwapType.BRIDGE);

if (!bridgePairInfo || !bridgePairInfo.pair) {
return Promise.resolve(undefined);
}

const _sendingAmount = isBridgeFirst ? request.fromAmount : selectedQuote.toAmount;
const sendingAmount = BigNumber(_sendingAmount).multipliedBy(2).toFixed(0, 1); // ensure approve enough amount
const senderAddress = request.address;
const fromTokenInfo = this.chainService.getAssetBySlug(bridgePairInfo.pair.from);
const fromChainInfo = this.chainService.getChainInfoByKey(_getAssetOriginChain(fromTokenInfo));
const fromChainId = _getEvmChainId(fromChainInfo);
const evmApi = this.chainService.getEvmApi(fromChainInfo.slug);
const tokenContract = _getContractAddressOfToken(fromTokenInfo);

const toTokenInfo = this.chainService.getAssetBySlug(params.request.pair.to);
const toTokenInfo = this.chainService.getAssetBySlug(bridgePairInfo.pair.to);
const toChainInfo = this.chainService.getChainInfoByKey(_getAssetOriginChain(toTokenInfo));

if (_isNativeToken(fromTokenInfo)) {
Expand All @@ -363,9 +387,9 @@ export class UniswapHandler implements SwapBaseInterface {
const inputData = {
destinationTokenInfo: toTokenInfo,
originTokenInfo: fromTokenInfo,
sendingValue: sendingAmount,
sendingValue: _sendingAmount,
sender: senderAddress,
recipient: senderAddress,
recipient: senderAddress, // todo: there's a case swap - bridge to another address
destinationChain: toChainInfo,
originChain: fromChainInfo
} as CreateXcmExtrinsicProps;
Expand Down Expand Up @@ -453,6 +477,11 @@ export class UniswapHandler implements SwapBaseInterface {
return Promise.resolve(undefined);
}

const actionList = JSON.stringify(path.map((step) => step.action));
const swapXcm = actionList === JSON.stringify([DynamicSwapType.SWAP, DynamicSwapType.BRIDGE]);
const sendingValue = swapXcm ? BigNumber(request.fromAmount).multipliedBy(DEFAULT_EXCESS_AMOUNT_WEIGHT).toFixed(0, 1) : request.fromAmount;
const expectedReceive = swapXcm ? BigNumber(selectedQuote.toAmount).multipliedBy(DEFAULT_EXCESS_AMOUNT_WEIGHT).toFixed(0, 1) : selectedQuote.toAmount;

const originTokenInfo = this.chainService.getAssetBySlug(selectedQuote.pair.from);
const destinationTokenInfo = this.chainService.getAssetBySlug(selectedQuote.pair.to);
const originChain = this.chainService.getChainInfoByKey(originTokenInfo.originChain);
Expand All @@ -463,8 +492,8 @@ export class UniswapHandler implements SwapBaseInterface {
type: SwapStepType.SWAP,
// @ts-ignore
metadata: {
sendingValue: request.fromAmount.toString(),
expectedReceive: selectedQuote.toAmount,
sendingValue,
expectedReceive,
originTokenInfo,
destinationTokenInfo,
sender: _reformatAddressWithChain(request.address, originChain),
Expand All @@ -478,6 +507,20 @@ export class UniswapHandler implements SwapBaseInterface {

async getBridgeStep (params: OptimalSwapPathParamsV2, stepIndex: number): Promise<[BaseStepDetail, CommonStepFeeInfo] | undefined> {
const { path, request, selectedQuote } = params;
/**
* Explain: All processes will go through one of below processes. If a step do not have, it returns undefined and
* the stepIndex is still counted up
*
* Processes:
* approve - permit - swap or
* approve - permit - swap - approve - bridge or
* approve - bridge - approve - permit - swap
*/
const actionList = JSON.stringify(path.map((step) => step.action));
const bridgeSwap = actionList === JSON.stringify([DynamicSwapType.BRIDGE, DynamicSwapType.SWAP]);
const swapBridge = actionList === JSON.stringify([DynamicSwapType.SWAP, DynamicSwapType.BRIDGE]);
const isBridgeFirst = stepIndex === 1 && bridgeSwap;
const isBridgeSecond = stepIndex === 4 && swapBridge;

// stepIndex is not corresponding index in path, because uniswap include approval and permit step
const bridgePairInfo = path.find((action) => action.action === DynamicSwapType.BRIDGE);
Expand All @@ -499,9 +542,19 @@ export class UniswapHandler implements SwapBaseInterface {
throw Error('Token or chain not found');
}

let receiverAddress;
let mockSendingValue;
const senderAddress = _reformatAddressWithChain(request.address, fromChainInfo);
const receiverAddress = _reformatAddressWithChain(request.recipient || request.address, toChainInfo);
const sendingValue = BigNumber(selectedQuote.toAmount).div(1.02).toFixed(0, 1);

if (isBridgeFirst) {
receiverAddress = _reformatAddressWithChain(request.address, toChainInfo);
mockSendingValue = BigNumber(selectedQuote.fromAmount).toFixed(0, 1);
} else if (isBridgeSecond) {
receiverAddress = _reformatAddressWithChain(request.recipient || request.address, toChainInfo);
mockSendingValue = BigNumber(selectedQuote.toAmount).toFixed(0, 1);
} else {
return undefined;
}

try {
const evmApi = await this.chainService.getEvmApi(fromChainInfo.slug).isReady;
Expand All @@ -515,25 +568,42 @@ export class UniswapHandler implements SwapBaseInterface {
evmApi,
feeInfo,
// Mock sending value to get payment info
sendingValue,
sendingValue: mockSendingValue,
sender: senderAddress,
recipient: receiverAddress
});

// // todo: wait until this ready to get destination fee. the real receiveAmount is deduce by this fee
// const acrossQuote = await getAcrossQuote({
// destinationChain: toChainInfo,
// destinationTokenInfo: toTokenInfo,
// originChain: fromChainInfo,
// originTokenInfo: fromTokenInfo,
// recipient: receiverAddress,
// sender: senderAddress,
// sendingValue,
// feeInfo
// });
const acrossQuote = await getAcrossQuote({
destinationChain: toChainInfo,
destinationTokenInfo: toTokenInfo,
originChain: fromChainInfo,
originTokenInfo: fromTokenInfo,
recipient: receiverAddress,
sender: senderAddress,
sendingValue: mockSendingValue,
feeInfo
});

const acrossQuoteMetadata = acrossQuote.metadata as AcrossQuote;

const estimatedBridgeFee = await estimateTxFee(tx, evmApi, feeInfo);
const expectedReceive = BigNumber(sendingValue).minus(estimatedBridgeFee).toFixed(0, 1);
const estimatedDestinationFee = BigNumber(mockSendingValue).minus(acrossQuoteMetadata.outputAmount).toFixed(0, 1); // todo: should better handle on backend and return desFee metadata instead of minus like this

let sendingValue;
let expectedReceive;

if (isBridgeFirst) {
expectedReceive = selectedQuote.fromAmount;
sendingValue = BigNumber(estimatedDestinationFee).multipliedBy(FEE_RATE_MULTIPLIER.medium).plus(selectedQuote.fromAmount).toFixed(0, 1);
} else if (isBridgeSecond) {
expectedReceive = selectedQuote.toAmount;
sendingValue = BigNumber(selectedQuote.toAmount).multipliedBy(DEFAULT_EXCESS_AMOUNT_WEIGHT).toFixed(0, 1);
} else {
return undefined;
}

console.log('[i] estimatedBridgeFee', estimatedBridgeFee);
console.log('[i] estimatedDestinationFee', estimatedDestinationFee);

const fee: CommonStepFeeInfo = {
feeComponent: [{
Expand Down Expand Up @@ -951,7 +1021,7 @@ export class UniswapHandler implements SwapBaseInterface {
return [new TransactionError(BasicTxErrorType.INTERNAL_ERROR)];
}

if (swapXcm && bridgeIndex <= -1) {
if ((swapXcm || xcmSwap) && bridgeIndex <= -1) {
return [new TransactionError(BasicTxErrorType.INTERNAL_ERROR)];
}

Expand All @@ -964,7 +1034,7 @@ export class UniswapHandler implements SwapBaseInterface {
}

if (xcmSwap) {
return [new TransactionError(BasicTxErrorType.INTERNAL_ERROR)];
return this.swapBaseHandler.validateXcmSwapProcess(params, swapIndex, bridgeIndex);
}

if (xcmSwapXcm) {
Expand Down
2 changes: 1 addition & 1 deletion packages/extension-base/src/services/swap-service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export class SwapService implements StoppableServiceInterface {
private async askProvidersForQuote (_request: SwapRequestV2) {
const availableQuotes: QuoteAskResponse[] = [];

// hotfix
// hotfix // todo: remove later
const request = {
..._request,
isSupportKyberVersion: true
Expand Down
9 changes: 8 additions & 1 deletion packages/subwallet-api-sdk/src/modules/swapApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ interface SwapRequestV2 {
feeToken?: string;
preferredProvider?: SwapProviderId; // allow user to designate a provider
isCrossChain?: boolean;
evmBridgeSwapVersion?: boolean; // hotfix todo: remove later
}

export interface HydrationRateRequest {
Expand Down Expand Up @@ -195,9 +196,15 @@ export class SwapApi {
}
}

async findAvailablePath (availablePathRequest: SwapRequestV2) {
async findAvailablePath (_availablePathRequest: SwapRequestV2) {
const url = `${this.baseUrl}/swap/find-available-path`;

// hotfix todo: remove later
const availablePathRequest = {
..._availablePathRequest,
evmBridgeSwapVersion: true
};

try {
const rawResponse = await fetch(url, {
method: 'POST',
Expand Down
Loading