Skip to content

Improve XCM fee with Paraspell's updates #4369

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 9 commits into from
May 29, 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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@
"@polkadot/util": "^13.4.3",
"@polkadot/util-crypto": "^13.4.3",
"@polkadot/x-global": "^13.4.3",
"@subwallet/chain-list": "0.2.104",
"@subwallet/chain-list": "0.2.105-beta.20",
"@subwallet/keyring": "^0.1.12",
"@subwallet/react-ui": "5.1.2-b79",
"@subwallet/ui-keyring": "^0.1.12",
Expand Down
2 changes: 1 addition & 1 deletion packages/extension-base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"@reduxjs/toolkit": "^1.9.1",
"@sora-substrate/type-definitions": "^1.17.7",
"@substrate/connect": "^0.8.9",
"@subwallet/chain-list": "0.2.104",
"@subwallet/chain-list": "0.2.105-beta.20",
"@subwallet/extension-base": "^1.3.39-0",
"@subwallet/extension-chains": "^1.3.39-0",
"@subwallet/extension-dapp": "^1.3.39-0",
Expand Down
22 changes: 16 additions & 6 deletions packages/extension-base/src/koni/background/handlers/Extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import { _isAcrossChainBridge, AcrossQuote, getAcrossQuote } from '@subwallet/ex
import { getClaimTxOnAvail, getClaimTxOnEthereum, isAvailChainBridge } from '@subwallet/extension-base/services/balance-service/transfer/xcm/availBridge';
import { _isPolygonChainBridge, getClaimPolygonBridge, isClaimedPolygonBridge } from '@subwallet/extension-base/services/balance-service/transfer/xcm/polygonBridge';
import { _isPosChainBridge, getClaimPosBridge } from '@subwallet/extension-base/services/balance-service/transfer/xcm/posBridge';
import { DryRunInfo } from '@subwallet/extension-base/services/balance-service/transfer/xcm/utils';
import { estimateXcmFee } from '@subwallet/extension-base/services/balance-service/transfer/xcm/utils';
import { _DEFAULT_MANTA_ZK_CHAIN, _MANTA_ZK_CHAIN_GROUP, _ZK_ASSET_PREFIX } from '@subwallet/extension-base/services/chain-service/constants';
import { _ChainApiStatus, _ChainConnectionStatus, _ChainState, _NetworkUpsertParams, _ValidateCustomAssetRequest, _ValidateCustomAssetResponse, EnableChainParams, EnableMultiChainParams } from '@subwallet/extension-base/services/chain-service/types';
import { _getAssetDecimals, _getAssetSymbol, _getChainNativeTokenBasicInfo, _getContractAddressOfToken, _getEvmChainId, _isAssetSmartContractNft, _isChainEnabled, _isChainEvmCompatible, _isChainSubstrateCompatible, _isCustomAsset, _isLocalToken, _isMantaZkAsset, _isNativeToken, _isNativeTokenBySlug, _isPureEvmChain, _isTokenEvmSmartContract, _isTokenTransferredByCardano, _isTokenTransferredByEvm, _isTokenTransferredByTon } from '@subwallet/extension-base/services/chain-service/utils';
Expand Down Expand Up @@ -1687,12 +1687,18 @@ export default class KoniExtension {
};

extrinsic = await funcCreateExtrinsic(params);
let dryRunInfo: DryRunInfo;

if (isSubstrateXcm) {
dryRunInfo = await dryRunXcmExtrinsicV2(params);
const xcmFeeInfo = await estimateXcmFee({
fromChainInfo: params.originChain,
fromTokenInfo: params.originTokenInfo,
toChainInfo: params.destinationChain,
recipient: params.recipient,
sender: params.sender,
value: params.sendingValue
});

xcmFeeDryRun = dryRunInfo.fee;
xcmFeeDryRun = xcmFeeInfo?.origin.fee || '0';
}

if (isAcrossBridgeTransfer) {
Expand Down Expand Up @@ -1767,8 +1773,12 @@ export default class KoniExtension {
warning.length && inputTransaction.warnings.push(...warning);
error.length && inputTransaction.errors.push(...error);

if (isSubstrateXcm && !dryRunInfo.success) {
inputTransaction.errors.push(new TransactionError(BasicTxErrorType.UNABLE_TO_SEND, 'Unable to perform transaction. Select another token or destination chain and try again'));
if (isSubstrateXcm) {
const isDryRunSuccess = await dryRunXcmExtrinsicV2(params);

if (!isDryRunSuccess) {
inputTransaction.errors.push(new TransactionError(BasicTxErrorType.UNABLE_TO_SEND, 'Unable to perform transaction. Select another token or destination chain and try again'));
}
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,18 @@
// SPDX-License-Identifier: Apache-2.0

import { _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types';
import { XCM_MIN_AMOUNT_RATIO } from '@subwallet/extension-base/constants';
import { _isAcrossBridgeXcm, _isPolygonBridgeXcm, _isPosBridgeXcm, _isSnowBridgeXcm } from '@subwallet/extension-base/core/substrate/xcm-parser';
import { getAvailBridgeExtrinsicFromAvail, getAvailBridgeTxFromEth } from '@subwallet/extension-base/services/balance-service/transfer/xcm/availBridge';
import { getExtrinsicByPolkadotXcmPallet } from '@subwallet/extension-base/services/balance-service/transfer/xcm/polkadotXcm';
import { _createPolygonBridgeL1toL2Extrinsic, _createPolygonBridgeL2toL1Extrinsic } from '@subwallet/extension-base/services/balance-service/transfer/xcm/polygonBridge';
import { getSnowBridgeEvmTransfer } from '@subwallet/extension-base/services/balance-service/transfer/xcm/snowBridge';
import { buildXcm, DryRunInfo, dryRunXcmV2, isChainNotSupportDryRun, isChainNotSupportPolkadotApi } from '@subwallet/extension-base/services/balance-service/transfer/xcm/utils';
import { buildXcm, DryRunNodeResult, dryRunXcm, isChainNotSupportDryRun, isChainNotSupportPolkadotApi } from '@subwallet/extension-base/services/balance-service/transfer/xcm/utils';
import { getExtrinsicByXcmPalletPallet } from '@subwallet/extension-base/services/balance-service/transfer/xcm/xcmPallet';
import { getExtrinsicByXtokensPallet } from '@subwallet/extension-base/services/balance-service/transfer/xcm/xTokens';
import { _XCM_CHAIN_GROUP } from '@subwallet/extension-base/services/chain-service/constants';
import { _EvmApi, _SubstrateApi } from '@subwallet/extension-base/services/chain-service/types';
import { _isNativeToken } from '@subwallet/extension-base/services/chain-service/utils';
import { EvmEIP1559FeeOption, EvmFeeInfo, FeeInfo, RuntimeDispatchInfo, TransactionFee } from '@subwallet/extension-base/types';
import { EvmEIP1559FeeOption, EvmFeeInfo, FeeInfo, TransactionFee } from '@subwallet/extension-base/types';
import { combineEthFee } from '@subwallet/extension-base/utils';
import subwalletApiSdk from '@subwallet/subwallet-api-sdk';
import { TransactionConfig } from 'web3-core';
Expand Down Expand Up @@ -64,6 +63,7 @@ export const createSnowBridgeExtrinsic = async ({ destinationChain,
return getSnowBridgeEvmTransfer(originTokenInfo, originChain, destinationChain, sender, recipient, sendingValue, evmApi, feeInfo, feeCustom, feeOption);
};

// deprecated
export const createXcmExtrinsic = async ({ destinationChain,
originChain,
originTokenInfo,
Expand Down Expand Up @@ -161,44 +161,31 @@ export const createXcmExtrinsicV2 = async (request: CreateXcmExtrinsicProps): Pr
return await buildXcm(request);
} catch (e) {
console.log('createXcmExtrinsicV2 error: ', e);
const errorMessage = e instanceof Error ? e.message : 'Unknown error occurred';

if (isChainNotSupportPolkadotApi(errorMessage)) {
return createXcmExtrinsic(request);
}

return undefined;
}
};

export const dryRunXcmExtrinsicV2 = async (request: CreateXcmExtrinsicProps): Promise<DryRunInfo> => {
export const dryRunXcmExtrinsicV2 = async (request: CreateXcmExtrinsicProps): Promise<boolean> => {
try {
return await dryRunXcmV2(request);
} catch (e) {
const errorMessage = e instanceof Error ? e.message : 'Unknown error occurred';
const dryRunResult = await dryRunXcm(request);
const originDryRunResult = dryRunResult.origin;

if (isChainNotSupportDryRun(errorMessage) || isChainNotSupportPolkadotApi(errorMessage)) {
const xcmTransfer = await createXcmExtrinsicV2(request);
if (originDryRunResult.success) {
const destinationDryRunResult = dryRunResult.destination as DryRunNodeResult;

if (!xcmTransfer) {
return {
success: false
};
if (destinationDryRunResult.success) {
return true;
}

const _xcmFeeInfo = await xcmTransfer.paymentInfo(request.sender);
const xcmFeeInfo = _xcmFeeInfo.toPrimitive() as unknown as RuntimeDispatchInfo;

// skip dry run in this case
return {
success: true,
fee: Math.round(xcmFeeInfo.partialFee * XCM_MIN_AMOUNT_RATIO).toString()
};
// pass dry-run in these cases
return isChainNotSupportDryRun(destinationDryRunResult.failureReason) || isChainNotSupportPolkadotApi(destinationDryRunResult.failureReason);
}

return {
success: false
};
// pass dry-run in these cases
return isChainNotSupportDryRun(originDryRunResult.failureReason) || isChainNotSupportPolkadotApi(originDryRunResult.failureReason);
} catch (e) {
return false;
}
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,56 @@
// Copyright 2019-2022 @subwallet/extension-base
// SPDX-License-Identifier: Apache-2.0

import { _ChainInfo } from '@subwallet/chain-list/types';
import { TransactionError } from '@subwallet/extension-base/background/errors/TransactionError';
import { _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types';
import { fetchParaSpellChainMap } from '@subwallet/extension-base/constants/paraspell-chain-map';
import { CreateXcmExtrinsicProps } from '@subwallet/extension-base/services/balance-service/transfer/xcm/index';
import { BasicTxErrorType } from '@subwallet/extension-base/types';

import { ApiPromise } from '@polkadot/api';
import { SubmittableExtrinsic } from '@polkadot/api/types';
import { Call, ExtrinsicPayload } from '@polkadot/types/interfaces';
import { assert, compactToU8a, isHex, u8aConcat, u8aEq } from '@polkadot/util';

export interface DryRunInfo {
success: boolean,
fee?: string // has fee in case dry run success
export type DryRunNodeFailure = {
success: false,
failureReason: string
}

export type DryRunNodeSuccess = {
success: true
fee: string
forwardedXcms: any
// destParaId?: number
// currency: string
}

export type DryRunNodeResult = DryRunNodeSuccess | DryRunNodeFailure;

export type DryRunResult = {
origin: DryRunNodeResult
destination?: DryRunNodeResult
}

interface GetXcmFeeRequest {
sender: string,
recipient: string,
value: string,
fromChainInfo: _ChainInfo,
toChainInfo: _ChainInfo,
fromTokenInfo: _ChainAsset
}

export type XcmFeeType = 'dryRun' | 'paymentInfo'

export interface XcmFeeDetail {
fee: string
currency: string
feeType: XcmFeeType
dryRunError?: string
}

export type GetXcmFeeResult = {
origin: XcmFeeDetail
destination: XcmFeeDetail
}

interface ParaSpellCurrency {
Expand All @@ -28,11 +64,12 @@ interface ParaSpellError {
statusCode: number
}

const paraSpellEndpoint = 'https://api.lightspell.xyz';
const paraSpellEndpoint = 'https://api.lightspell.xyz/v3';

const paraSpellApi = {
buildXcm: `${paraSpellEndpoint}/x-transfer`,
dryRunXcm: `${paraSpellEndpoint}/dry-run`
dryRunXcm: `${paraSpellEndpoint}/dry-run`,
feeXcm: `${paraSpellEndpoint}/xcm-fee`
};

const paraSpellKey = process.env.PARASPELL_API_KEY || '';
Expand Down Expand Up @@ -104,7 +141,7 @@ export async function buildXcm (request: CreateXcmExtrinsicProps) {
const { destinationChain, originChain, originTokenInfo, recipient, sendingValue, substrateApi } = request;

if (!substrateApi) {
return Promise.reject(new Error('Substrate API is not available'));
throw new Error('Substrate API is not available');
}

const psAssetType = originTokenInfo.metadata?.paraSpellAssetType;
Expand Down Expand Up @@ -145,69 +182,69 @@ export async function buildXcm (request: CreateXcmExtrinsicProps) {
return txHexToSubmittableExtrinsic(chainApi.api, extrinsicHex);
}

// dry run can fail due to sender address & amount token
export async function dryRunXcm (request: CreateXcmExtrinsicProps) {
const { destinationChain, originChain, originTokenInfo, recipient, sender, sendingValue } = request;
const paraSpellChainMap = await fetchParaSpellChainMap();
const psAssetType = originTokenInfo.metadata?.paraSpellAssetType;
const psAssetValue = originTokenInfo.metadata?.paraSpellValue;

if (!psAssetType || !psAssetValue) {
throw new Error('Token is not support XCM at this time'); // todo: content
throw new Error('Token is not support XCM at this time');
}

let dryRunInfo: DryRunInfo | undefined;
const bodyData = {
senderAddress: sender,
address: recipient,
from: paraSpellChainMap[originChain.slug],
to: paraSpellChainMap[destinationChain.slug],
currency: createParaSpellCurrency(psAssetType, psAssetValue, sendingValue)
};

try {
const bodyData = {
senderAddress: sender,
address: recipient,
from: paraSpellChainMap[originChain.slug],
to: paraSpellChainMap[destinationChain.slug],
currency: createParaSpellCurrency(psAssetType, psAssetValue, sendingValue)
};

const response = await fetch(paraSpellApi.dryRunXcm, {
method: 'POST',
body: JSON.stringify(bodyData),
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
'X-API-KEY': paraSpellKey
}
});
const response = await fetch(paraSpellApi.dryRunXcm, {
method: 'POST',
body: JSON.stringify(bodyData),
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
'X-API-KEY': paraSpellKey
}
});

dryRunInfo = await response.json() as DryRunInfo;
} catch (e) {
console.error('Unable to dry run', e);
}
if (!response.ok) {
const error = await response.json() as ParaSpellError;

if (!dryRunInfo || !dryRunInfo.success) {
throw new TransactionError(BasicTxErrorType.UNABLE_TO_SEND, 'Unable to perform transaction. Select another token or destination chain and try again');
return {
origin: {
success: false,
failureReason: error.message
}
} as DryRunResult;
}

return dryRunInfo;
return await response.json() as DryRunResult;
}

export async function dryRunXcmV2 (request: CreateXcmExtrinsicProps) {
const { destinationChain, originChain, originTokenInfo, recipient, sender, sendingValue } = request;
export async function estimateXcmFee (request: GetXcmFeeRequest) {
const { fromChainInfo, fromTokenInfo, recipient, sender, toChainInfo, value } = request;
const paraSpellChainMap = await fetchParaSpellChainMap();
const psAssetType = originTokenInfo.metadata?.paraSpellAssetType;
const psAssetValue = originTokenInfo.metadata?.paraSpellValue;
const psAssetType = fromTokenInfo.metadata?.paraSpellAssetType;
const psAssetValue = fromTokenInfo.metadata?.paraSpellValue;

if (!psAssetType || !psAssetValue) {
throw new Error('Token is not support XCM at this time');
console.error('Lack of paraspell metadata');

return undefined;
}

const bodyData = {
senderAddress: sender,
address: recipient,
from: paraSpellChainMap[originChain.slug],
to: paraSpellChainMap[destinationChain.slug],
currency: createParaSpellCurrency(psAssetType, psAssetValue, sendingValue)
from: paraSpellChainMap[fromChainInfo.slug],
to: paraSpellChainMap[toChainInfo.slug],
currency: createParaSpellCurrency(psAssetType, psAssetValue, value)
};

const response = await fetch(paraSpellApi.dryRunXcm, {
const response = await fetch(paraSpellApi.feeXcm, {
method: 'POST',
body: JSON.stringify(bodyData),
headers: {
Expand All @@ -218,16 +255,17 @@ export async function dryRunXcmV2 (request: CreateXcmExtrinsicProps) {
});

if (!response.ok) {
const error = await response.json() as ParaSpellError;
console.error('Failed to request estimate fee');

throw new Error(error.message);
return undefined;
}

return await response.json() as DryRunInfo;
return await response.json() as GetXcmFeeResult;
}

function createParaSpellCurrency (assetType: string, assetValue: string, amount: string): ParaSpellCurrency {
// todo: handle complex conditions for asset has same symbol in a chain: Id, Multi-location, ...
// todo: or update all asset to use multi-location
return {
[assetType]: assetValue,
amount
Expand Down
Loading
Loading