From 7b5d943481792f02def653e51c2739adc1427ae3 Mon Sep 17 00:00:00 2001 From: ron Date: Fri, 14 Nov 2025 00:28:19 +0800 Subject: [PATCH 01/13] Support multiple assets transfer --- web/packages/api/src/assets_v2.ts | 17 + .../api/src/toEthereumSnowbridgeV2.ts | 473 +++++++----------- .../src/transfers/toEthereum/erc20FromAH.ts | 170 +++---- .../toEthereum/erc20FromParachain.ts | 163 +++--- .../api/src/transfers/toEthereum/pnaFromAH.ts | 85 ++-- .../transfers/toEthereum/pnaFromParachain.ts | 94 ++-- .../transfers/toEthereum/transferInterface.ts | 22 +- web/packages/api/src/xcmBuilder.ts | 35 +- .../src/xcmbuilders/toEthereum/erc20FromAH.ts | 132 +++-- .../toEthereum/erc20FromParachain.ts | 383 +++++--------- .../erc20FromParachainWithNativeAsFee.ts | 103 ++-- .../toEthereum/pnaFromParachain.ts | 61 --- 12 files changed, 741 insertions(+), 997 deletions(-) diff --git a/web/packages/api/src/assets_v2.ts b/web/packages/api/src/assets_v2.ts index 911733e7e..5054637f3 100644 --- a/web/packages/api/src/assets_v2.ts +++ b/web/packages/api/src/assets_v2.ts @@ -111,6 +111,23 @@ export type TransferLocation = { ethChain?: EthereumChain } +export type AggregatedAsset = { + tokenErcMetadata: ERC20Metadata + ahAssetMetadata: Asset + sourceAssetMetadata: Asset + amount: bigint +} + +export type ConcreteAsset = { + id: Asset + amount: bigint +} + +export type ConcreteToken = { + address: string + amount: bigint +} + export const ETHER_TOKEN_ADDRESS = "0x0000000000000000000000000000000000000000" export async function buildRegistry(options: RegistryOptions): Promise { diff --git a/web/packages/api/src/toEthereumSnowbridgeV2.ts b/web/packages/api/src/toEthereumSnowbridgeV2.ts index 8fb01306e..8740f66d8 100644 --- a/web/packages/api/src/toEthereumSnowbridgeV2.ts +++ b/web/packages/api/src/toEthereumSnowbridgeV2.ts @@ -1,18 +1,21 @@ import { ApiPromise } from "@polkadot/api" import { SubmittableExtrinsic } from "@polkadot/api/types" import { Codec, ISubmittableResult } from "@polkadot/types/types" -import { AssetRegistry, ContractCall } from "@snowbridge/base-types" +import { + Asset, + AssetRegistry, + ContractCall, + ERC20Metadata, + Parachain, +} from "@snowbridge/base-types" import { CallDryRunEffects, XcmDryRunApiError, XcmDryRunEffects } from "@polkadot/types/interfaces" import { Result } from "@polkadot/types" import { - DeliveryFee, dryRunBridgeHub, resolveInputs, - Transfer, ValidationKind, ValidationLog, ValidationReason, - ValidationResult, } from "./toEthereum_v2" import { PNAFromAH } from "./transfers/toEthereum/pnaFromAH" import { TransferInterface } from "./transfers/toEthereum/transferInterface" @@ -31,13 +34,72 @@ import { BN, hexToU8a } from "@polkadot/util" import { padFeeByPercentage } from "./utils" import { paraImplementation } from "./parachains" import { Context } from "./index" -import { ETHER_TOKEN_ADDRESS, getAssetHubConversionPalletSwap } from "./assets_v2" -import { getOperatingStatus } from "./status" +import { + AggregatedAsset, + ConcreteToken, + ETHER_TOKEN_ADDRESS, + getAssetHubConversionPalletSwap, +} from "./assets_v2" +import { getOperatingStatus, OperationStatus } from "./status" import { AbstractProvider, ethers, Wallet, TransactionReceipt } from "ethers" import { CreateAgent } from "./registration/agent/createAgent" export { ValidationKind, signAndSend } from "./toEthereum_v2" +export type DeliveryFeeV2 = { + snowbridgeDeliveryFeeDOT: bigint + bridgeHubDeliveryFeeDOT: bigint + assetHubExecutionFeeDOT: bigint + returnToSenderExecutionFeeDOT: bigint + returnToSenderDeliveryFeeDOT: bigint + totalFeeInDot: bigint + localExecutionFeeDOT?: bigint + localDeliveryFeeDOT?: bigint + ethereumExecutionFee?: bigint + feeLocation?: any + totalFeeInNative?: bigint + assetHubExecutionFeeNative?: bigint + returnToSenderExecutionFeeNative?: bigint + localExecutionFeeInNative?: bigint + localDeliveryFeeInNative?: bigint + ethereumExecutionFeeInNative?: bigint +} + +export type TransferV2 = { + input: { + registry: AssetRegistry + sourceAccount: string + beneficiaryAccount: any + tokens: ConcreteToken[] + fee: DeliveryFeeV2 + contractCall?: ContractCall + } + computed: { + sourceParaId: number + sourceParachain: Parachain + sourceAccountHex: string + aggregatedAssets: AggregatedAsset[] + messageId?: string + } + tx: SubmittableExtrinsic<"promise", ISubmittableResult> +} + +export type ValidationResultV2 = { + logs: ValidationLog[] + success: boolean + data: { + bridgeStatus: OperationStatus + nativeBalance: bigint + dotBalance?: bigint + sourceExecutionFee: bigint + tokenBalances?: ConcreteToken[] + sourceDryRunError: any + assetHubDryRunError: any + bridgeHubDryRunError?: any + } + transfer: TransferV2 +} + export function createTransferImplementation( sourceParaId: number, registry: AssetRegistry, @@ -212,34 +274,32 @@ export type DeliveryXcm = { localXcm: any forwardedXcmToBH: any forwardXcmToAH?: any - returnToSenderXcm?: any } export const estimateEthereumExecutionFee = async ( context: Context, registry: AssetRegistry, sourceParaId: number, - tokenAddress: string, + tokenAddresses: string[], contractCall?: ContractCall, ): Promise => { const ethereum = await context.ethereum() - const { tokenErcMetadata } = resolveInputs(registry, tokenAddress, sourceParaId) - - // Calculate execution cost on ethereum - let ethereumChain = registry.ethereumChains[registry.ethChainId.toString()] - let feeData = await ethereum.getFeeData() - let ethereumExecutionFee = - (feeData.gasPrice ?? 2_000_000_000n) * - ((tokenErcMetadata.deliveryGas ?? 80_000n) + - (ethereumChain.baseDeliveryGas ?? 120_000n) + - (contractCall?.gas ?? 0n)) + const ethereumChain = registry.ethereumChains[registry.ethChainId.toString()] + const feeData = await ethereum.getFeeData() + const gasPrice = feeData.gasPrice ?? 2_000_000_000n + let ethereumExecutionFee: bigint = ethereumChain.baseDeliveryGas ?? 120_000n + for (let tokenAddress of tokenAddresses) { + const { tokenErcMetadata } = resolveInputs(registry, tokenAddress, sourceParaId) + ethereumExecutionFee += gasPrice * (tokenErcMetadata.deliveryGas ?? 80_000n) + } + ethereumExecutionFee += contractCall?.gas ?? 0n return ethereumExecutionFee } export const estimateFeesFromAssetHub = async ( context: Context, registry: AssetRegistry, - tokenAddress: string, + tokenAddresses: string[], deliveryXcm: DeliveryXcm, options?: { padPercentage?: bigint @@ -248,7 +308,7 @@ export const estimateFeesFromAssetHub = async ( feeTokenLocation?: any contractCall?: ContractCall }, -): Promise => { +): Promise => { const assetHub = await context.parachain(registry.assetHubParaId) const assetHubImpl = await paraImplementation(assetHub) @@ -289,7 +349,7 @@ export const estimateFeesFromAssetHub = async ( context, registry, registry.assetHubParaId, - tokenAddress, + tokenAddresses, options?.contractCall, ) @@ -338,7 +398,7 @@ export const estimateFeesFromParachains = async ( context: Context, sourceParaId: number, registry: AssetRegistry, - tokenAddress: string, + tokenAddresses: string[], deliveryXcm: DeliveryXcm, options?: { padPercentage?: bigint @@ -347,7 +407,7 @@ export const estimateFeesFromParachains = async ( feeTokenLocation?: any contractCall?: ContractCall }, -): Promise => { +): Promise => { const sourceParachain = registry.parachains[sourceParaId.toString()] const sourceParachainImpl = await paraImplementation(await context.parachain(sourceParaId)) @@ -380,10 +440,6 @@ export const estimateFeesFromParachains = async ( ), feePadPercentage, ) - returnToSenderExecutionFeeDOT = padFeeByPercentage( - await sourceParachainImpl.calculateXcmFee(deliveryXcm.returnToSenderXcm, DOT_LOCATION), - feePadPercentage, - ) } else { localExecutionFeeInNative = padFeeByPercentage( await sourceParachainImpl.calculateXcmFee(deliveryXcm.localXcm, HERE_LOCATION), @@ -396,16 +452,8 @@ export const estimateFeesFromParachains = async ( ), feePadPercentage, ) - returnToSenderExecutionFeeNative = padFeeByPercentage( - await sourceParachainImpl.calculateXcmFee(deliveryXcm.returnToSenderXcm, HERE_LOCATION), - feePadPercentage, - ) } - returnToSenderDeliveryFeeDOT = await assetHubImpl.calculateDeliveryFeeInDOT( - sourceParaId, - deliveryXcm.returnToSenderXcm, - ) assetHubExecutionFeeDOT = padFeeByPercentage( await assetHubImpl.calculateXcmFee(deliveryXcm.forwardXcmToAH, DOT_LOCATION), feePadPercentage, @@ -434,7 +482,7 @@ export const estimateFeesFromParachains = async ( context, registry, sourceParaId, - tokenAddress, + tokenAddresses, options?.contractCall, ) @@ -511,188 +559,16 @@ export const estimateFeesFromParachains = async ( } } -export const validateTransferFromAssetHub = async ( +export const validateTransfer = async ( context: Context, - transfer: Transfer, -): Promise => { - const { registry, fee, tokenAddress, amount } = transfer.input - const { sourceAccountHex, sourceParaId, sourceAssetMetadata } = transfer.computed - const { tx } = transfer - - const { sourceParachain, gateway, ethereum, bridgeHub } = - context instanceof Context - ? { - sourceParachain: await context.parachain(sourceParaId), - gateway: context.gateway(), - ethereum: context.ethereum(), - bridgeHub: await context.bridgeHub(), - } - : context - - const logs: ValidationLog[] = [] - const sourceParachainImpl = await paraImplementation(sourceParachain) - const nativeBalance = await sourceParachainImpl.getNativeBalance(sourceAccountHex) - let dotBalance = await sourceParachainImpl.getDotBalance(sourceAccountHex) - let tokenBalance: any - let isNativeBalance = false - // For DOT on AH, get it from the native balance pallet. - if ( - transfer.computed.sourceAssetMetadata.location && - isRelaychainLocation(transfer.computed.sourceAssetMetadata.location) - ) { - tokenBalance = await sourceParachainImpl.getNativeBalance(sourceAccountHex) - isNativeBalance = true - } else { - tokenBalance = await sourceParachainImpl.getTokenBalance( - sourceAccountHex, - registry.ethChainId, - tokenAddress, - sourceAssetMetadata, - ) - } - if (isNativeBalance && fee.totalFeeInNative) { - if (amount + fee.totalFeeInNative > tokenBalance) { - logs.push({ - kind: ValidationKind.Error, - reason: ValidationReason.InsufficientTokenBalance, - message: "Insufficient token balance to submit transaction.", - }) - } - } else { - if (amount > tokenBalance) { - logs.push({ - kind: ValidationKind.Error, - reason: ValidationReason.InsufficientTokenBalance, - message: "Insufficient token balance to submit transaction.", - }) - } - } - - // No fee specified means that the fee.ethereumExecutionFee is paid in Ether on source chain. - if (!fee.feeLocation) { - let etherBalance = await sourceParachainImpl.getTokenBalance( - sourceAccountHex, - registry.ethChainId, - ETHER_TOKEN_ADDRESS, - ) - - if (fee.ethereumExecutionFee! > etherBalance) { - logs.push({ - kind: ValidationKind.Error, - reason: ValidationReason.InsufficientEtherBalance, - message: "Insufficient ether balance to submit transaction.", - }) - } - } - let contractCall = transfer.input.contractCall - if (contractCall) { - try { - await checkContractAddress(ethereum, contractCall.target) - } catch (error) { - logs.push({ - kind: ValidationKind.Error, - reason: ValidationReason.ContractCallInvalidTarget, - message: - "Contract call with invalid target address: " + - contractCall.target + - " error: " + - String(error), - }) - } - } - - let sourceDryRunError - let assetHubDryRunError - let bridgeHubDryRunError - // do the dry run, get the forwarded xcm and dry run that - const dryRunResultAssetHub = await dryRunOnSourceParachain( - sourceParachain, - registry.assetHubParaId, - registry.bridgeHubParaId, - transfer.tx, - sourceAccountHex, - ) - if (dryRunResultAssetHub.success && dryRunResultAssetHub.bridgeHubForwarded) { - const dryRunResultBridgeHub = await dryRunBridgeHub( - bridgeHub, - registry.assetHubParaId, - dryRunResultAssetHub.bridgeHubForwarded[1][0], - ) - if (!dryRunResultBridgeHub.success) { - logs.push({ - kind: ValidationKind.Error, - reason: ValidationReason.DryRunFailed, - message: "Dry run failed on Bridge Hub.", - }) - bridgeHubDryRunError = dryRunResultBridgeHub.errorMessage - } - } else { - logs.push({ - kind: ValidationKind.Error, - reason: ValidationReason.DryRunFailed, - message: "Dry run call failed on Asset Hub.", - }) - assetHubDryRunError = dryRunResultAssetHub.error - } - - const paymentInfo = await tx.paymentInfo(sourceAccountHex) - const sourceExecutionFee = paymentInfo["partialFee"].toBigInt() - - // recheck total after fee estimation - if (isNativeBalance && fee.totalFeeInNative) { - if (amount + fee.totalFeeInNative + sourceExecutionFee > tokenBalance) { - logs.push({ - kind: ValidationKind.Error, - reason: ValidationReason.InsufficientTokenBalance, - message: "Insufficient token balance to submit transaction.", - }) - } - } - if (sourceExecutionFee + fee.totalFeeInDot > dotBalance) { - logs.push({ - kind: ValidationKind.Error, - reason: ValidationReason.InsufficientDotFee, - message: "Insufficient DOT balance to submit transaction on the source parachain.", - }) - } - const bridgeStatus = await getOperatingStatus({ gateway, bridgeHub }) - if (bridgeStatus.toEthereum.outbound !== "Normal") { - logs.push({ - kind: ValidationKind.Error, - reason: ValidationReason.BridgeStatusNotOperational, - message: "Bridge operations have been paused by onchain governance.", - }) - } - - const success = logs.find((l) => l.kind === ValidationKind.Error) === undefined - - return { - logs, - success, - data: { - bridgeStatus, - nativeBalance, - dotBalance, - sourceExecutionFee, - tokenBalance, - sourceDryRunError, - assetHubDryRunError, - bridgeHubDryRunError, - }, - transfer, - } -} - -export const validateTransferFromParachain = async ( - context: Context, - transfer: Transfer, -): Promise => { - const { registry, fee, tokenAddress, amount } = transfer.input + transfer: TransferV2, +): Promise => { + const { registry } = transfer.input const { sourceAccountHex, sourceParaId, + aggregatedAssets, sourceParachain: source, - sourceAssetMetadata, } = transfer.computed const { tx } = transfer @@ -710,62 +586,24 @@ export const validateTransferFromParachain = async ( const logs: ValidationLog[] = [] const sourceParachainImpl = await paraImplementation(sourceParachain) const nativeBalance = await sourceParachainImpl.getNativeBalance(sourceAccountHex) - let dotBalance: bigint | undefined = undefined - if (source.features.hasDotBalance) { - dotBalance = await sourceParachainImpl.getDotBalance(sourceAccountHex) - } - let tokenBalance: any - let isNativeBalance = false - - isNativeBalance = - sourceAssetMetadata.decimals === source.info.tokenDecimals && - sourceAssetMetadata.symbol == source.info.tokenSymbols - if (isNativeBalance) { - tokenBalance = await sourceParachainImpl.getNativeBalance(sourceAccountHex) - } else { - tokenBalance = await sourceParachainImpl.getTokenBalance( - sourceAccountHex, - registry.ethChainId, - tokenAddress, - sourceAssetMetadata, - ) - } - - if (isNativeBalance && fee.totalFeeInNative) { - if (amount + fee.totalFeeInNative > tokenBalance) { - logs.push({ - kind: ValidationKind.Error, - reason: ValidationReason.InsufficientTokenBalance, - message: "Insufficient token balance to submit transaction.", - }) - } - } else { - if (amount > tokenBalance) { - logs.push({ - kind: ValidationKind.Error, - reason: ValidationReason.InsufficientTokenBalance, - message: "Insufficient token balance to submit transaction.", - }) - } - } - - if (!fee.feeLocation) { - let etherBalance = await sourceParachainImpl.getTokenBalance( - sourceAccountHex, - registry.ethChainId, - ETHER_TOKEN_ADDRESS, - ) - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - if (fee.ethereumExecutionFee! > etherBalance) { - logs.push({ - kind: ValidationKind.Error, - reason: ValidationReason.InsufficientEtherBalance, - message: "Insufficient ether balance to submit transaction.", + let dotBalance = await sourceParachainImpl.getDotBalance(sourceAccountHex) + let tokenBalances: ConcreteToken[] = [] + for (let asset of aggregatedAssets) { + let tokenAmount: bigint = 0n + if (!isRelaychainLocation(asset.sourceAssetMetadata.location)) { + // tokenBalance = await sourceParachainImpl.getDotBalance(sourceAccountHex) + tokenAmount = await sourceParachainImpl.getTokenBalance( + sourceAccountHex, + registry.ethChainId, + asset.sourceAssetMetadata.token, + asset.sourceAssetMetadata, + ) + tokenBalances.push({ + address: asset.sourceAssetMetadata.token, + amount: tokenAmount, }) } } - let contractCall = transfer.input.contractCall if (contractCall) { try { @@ -786,59 +624,92 @@ export const validateTransferFromParachain = async ( let sourceDryRunError let assetHubDryRunError let bridgeHubDryRunError - if (source.features.hasDryRunApi) { - // do the dry run, get the forwarded xcm and dry run that - const dryRunSource = await dryRunOnSourceParachain( - sourceParachain, + // do the dry run, get the forwarded xcm and dry run that + if (sourceParaId == registry.assetHubParaId) { + const dryRunResultAssetHub = await dryRunOnSourceParachain( + assetHub, registry.assetHubParaId, registry.bridgeHubParaId, transfer.tx, sourceAccountHex, ) - if (!dryRunSource.success) { + if (dryRunResultAssetHub.success && dryRunResultAssetHub.bridgeHubForwarded) { + const dryRunResultBridgeHub = await dryRunBridgeHub( + bridgeHub, + registry.assetHubParaId, + dryRunResultAssetHub.bridgeHubForwarded[1][0], + ) + if (!dryRunResultBridgeHub.success) { + logs.push({ + kind: ValidationKind.Error, + reason: ValidationReason.DryRunFailed, + message: "Dry run failed on Bridge Hub.", + }) + bridgeHubDryRunError = dryRunResultBridgeHub.errorMessage + } + } else { logs.push({ kind: ValidationKind.Error, reason: ValidationReason.DryRunFailed, - message: "Dry run call on source failed.", + message: "Dry run call failed on Asset Hub.", }) - sourceDryRunError = dryRunSource.error + assetHubDryRunError = dryRunResultAssetHub.error } - - if (dryRunSource.success) { - if (!dryRunSource.assetHubForwarded) { + } else { + if (source.features.hasDryRunApi) { + // do the dry run, get the forwarded xcm and dry run that + const dryRunSource = await dryRunOnSourceParachain( + sourceParachain, + registry.assetHubParaId, + registry.bridgeHubParaId, + transfer.tx, + sourceAccountHex, + ) + if (!dryRunSource.success) { logs.push({ kind: ValidationKind.Error, reason: ValidationReason.DryRunFailed, - message: "Dry run call did not provide a forwarded xcm.", + message: "Dry run call on source failed.", }) - } else { - const dryRunResultAssetHub = await dryRunAssetHub( - assetHub, - sourceParaId, - registry.bridgeHubParaId, - dryRunSource.assetHubForwarded[1][0], - ) - if (dryRunResultAssetHub.success && dryRunResultAssetHub.bridgeHubForwarded) { - const dryRunResultBridgeHub = await dryRunBridgeHub( - bridgeHub, - registry.assetHubParaId, - dryRunResultAssetHub.bridgeHubForwarded[1][0], + sourceDryRunError = dryRunSource.error + } + + if (dryRunSource.success) { + if (!dryRunSource.assetHubForwarded) { + logs.push({ + kind: ValidationKind.Error, + reason: ValidationReason.DryRunFailed, + message: "Dry run call did not provide a forwarded xcm.", + }) + } else { + const dryRunResultAssetHub = await dryRunAssetHub( + assetHub, + sourceParaId, + registry.bridgeHubParaId, + dryRunSource.assetHubForwarded[1][0], ) - if (!dryRunResultBridgeHub.success) { + if (dryRunResultAssetHub.success && dryRunResultAssetHub.bridgeHubForwarded) { + const dryRunResultBridgeHub = await dryRunBridgeHub( + bridgeHub, + registry.assetHubParaId, + dryRunResultAssetHub.bridgeHubForwarded[1][0], + ) + if (!dryRunResultBridgeHub.success) { + logs.push({ + kind: ValidationKind.Error, + reason: ValidationReason.DryRunFailed, + message: "Dry run failed on Bridge Hub.", + }) + bridgeHubDryRunError = dryRunResultBridgeHub.errorMessage + } + } else { logs.push({ kind: ValidationKind.Error, reason: ValidationReason.DryRunFailed, - message: "Dry run failed on Bridge Hub.", + message: "Dry run failed on Asset Hub.", }) - bridgeHubDryRunError = dryRunResultBridgeHub.errorMessage + assetHubDryRunError = dryRunResultAssetHub.errorMessage } - } else { - logs.push({ - kind: ValidationKind.Error, - reason: ValidationReason.DryRunFailed, - message: "Dry run failed on Asset Hub.", - }) - assetHubDryRunError = dryRunResultAssetHub.errorMessage } } } @@ -866,7 +737,7 @@ export const validateTransferFromParachain = async ( nativeBalance, dotBalance, sourceExecutionFee, - tokenBalance, + tokenBalances, sourceDryRunError, assetHubDryRunError, bridgeHubDryRunError, @@ -886,7 +757,7 @@ export async function buildContractCallHex(context: Context, contractCall: Contr return "0x00" + callHex.toHex().slice(2) } -export const mockDeliveryFee: DeliveryFee = { +export const mockDeliveryFee: DeliveryFeeV2 = { localExecutionFeeDOT: 1n, snowbridgeDeliveryFeeDOT: 1n, assetHubExecutionFeeDOT: 1n, diff --git a/web/packages/api/src/transfers/toEthereum/erc20FromAH.ts b/web/packages/api/src/transfers/toEthereum/erc20FromAH.ts index 11ccdf504..a84d4b719 100644 --- a/web/packages/api/src/transfers/toEthereum/erc20FromAH.ts +++ b/web/packages/api/src/transfers/toEthereum/erc20FromAH.ts @@ -3,21 +3,12 @@ import { SubmittableExtrinsic } from "@polkadot/api/types" import { ISubmittableResult } from "@polkadot/types/types" import { isHex, u8aToHex } from "@polkadot/util" import { decodeAddress } from "@polkadot/util-crypto" -import { isRelaychainLocation } from "../../xcmBuilder" import { buildExportXcm, buildTransferXcmFromAssetHub, } from "../../xcmbuilders/toEthereum/erc20FromAH" -import { buildTransferXcmFromAssetHubWithDOTAsFee } from "../../xcmbuilders/toEthereum/erc20FromAHWithDotAsFee" -import { Asset, AssetRegistry, ContractCall } from "@snowbridge/base-types" -import { paraImplementation } from "../../parachains" -import { - buildMessageId, - DeliveryFee, - resolveInputs, - Transfer, - ValidationResult, -} from "../../toEthereum_v2" +import { AssetRegistry, ContractCall } from "@snowbridge/base-types" +import { buildMessageId, resolveInputs } from "../../toEthereum_v2" import { Context } from "../.." import { TransferInterface } from "./transferInterface" import { @@ -25,14 +16,19 @@ import { estimateFeesFromAssetHub, MaxWeight, mockDeliveryFee, - validateTransferFromAssetHub, + validateTransfer, + DeliveryFeeV2, + TransferV2, + ValidationResultV2, } from "../../toEthereumSnowbridgeV2" +import { AggregatedAsset, ConcreteAsset, ConcreteToken } from "../../assets_v2" export class ERC20FromAH implements TransferInterface { async getDeliveryFee( - source: { sourceParaId: number; context: Context }, + context: Context, + sourceParaId: number, registry: AssetRegistry, - tokenAddress: string, + tokenAddresses: string[], options?: { padPercentage?: bigint slippagePadPercentage?: bigint @@ -40,15 +36,18 @@ export class ERC20FromAH implements TransferInterface { feeTokenLocation?: any contractCall?: ContractCall }, - ): Promise { - const { assetHub } = - "sourceParaId" in source - ? { - assetHub: await source.context.assetHub(), - } - : source + ): Promise { + const assetHub = await context.assetHub() - const { sourceAssetMetadata } = resolveInputs(registry, tokenAddress, source.sourceParaId) + let concreteAssets: ConcreteAsset[] = [] + + for (const tokenAddress of tokenAddresses) { + const { sourceAssetMetadata } = resolveInputs(registry, tokenAddress, sourceParaId) + concreteAssets.push({ + id: sourceAssetMetadata, + amount: 1n, + }) + } let localXcm = buildTransferXcmFromAssetHub( assetHub.registry, @@ -56,26 +55,24 @@ export class ERC20FromAH implements TransferInterface { "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", - sourceAssetMetadata, - 1n, + concreteAssets, mockDeliveryFee, ) let forwardedXcmToBH = buildExportXcm( assetHub.registry, registry.ethChainId, - sourceAssetMetadata, "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", - 1n, - 1n, + concreteAssets, + mockDeliveryFee, ) const fees = await estimateFeesFromAssetHub( - source.context, + context, registry, - tokenAddress, + tokenAddresses, { localXcm, forwardedXcmToBH, @@ -86,50 +83,64 @@ export class ERC20FromAH implements TransferInterface { } async createTransfer( - source: { sourceParaId: number; context: Context }, + context: Context, + sourceParaId: number, registry: AssetRegistry, sourceAccount: string, beneficiaryAccount: string, - tokenAddress: string, - amount: bigint, - fee: DeliveryFee, + tokens: ConcreteToken[], + fee: DeliveryFeeV2, options?: { claimerLocation?: any contractCall?: ContractCall }, - ): Promise { + ): Promise { const { ethChainId } = registry let sourceAccountHex = sourceAccount if (!isHex(sourceAccountHex)) { sourceAccountHex = u8aToHex(decodeAddress(sourceAccount)) } - const { parachain } = - "sourceParaId" in source - ? { parachain: await source.context.parachain(source.sourceParaId) } - : source + const assetHub = await context.assetHub() - const sourceParachainImpl = await paraImplementation(parachain) - const { tokenErcMetadata, sourceParachain, ahAssetMetadata, sourceAssetMetadata } = - resolveInputs(registry, tokenAddress, sourceParachainImpl.parachainId) + let sourceParachain = registry.parachains[sourceParaId.toString()] + let concreteAssets: ConcreteAsset[] = [], + aggregatedAssets: AggregatedAsset[] = [] - let messageId: string | undefined = await buildMessageId( - parachain, - sourceParachainImpl.parachainId, + for (const token of tokens) { + const { tokenErcMetadata, ahAssetMetadata, sourceAssetMetadata } = resolveInputs( + registry, + token.address, + sourceParaId, + ) + concreteAssets.push({ + id: sourceAssetMetadata, + amount: token.amount, + }) + aggregatedAssets.push({ + tokenErcMetadata, + ahAssetMetadata, + sourceAssetMetadata, + amount: token.amount, + }) + } + + let messageId = await buildMessageId( + assetHub, + sourceParaId, sourceAccountHex, - tokenAddress, + tokens[0].address, beneficiaryAccount, - amount, + tokens[0].amount, ) let tx: SubmittableExtrinsic<"promise", ISubmittableResult> = await this.createTx( - source.context, - parachain, + context, + assetHub, ethChainId, sourceAccount, beneficiaryAccount, - ahAssetMetadata, - amount, messageId, + concreteAssets, fee, options, ) @@ -139,26 +150,23 @@ export class ERC20FromAH implements TransferInterface { registry, sourceAccount, beneficiaryAccount, - tokenAddress, - amount, + tokens, fee, contractCall: options?.contractCall, }, computed: { - sourceParaId: sourceParachainImpl.parachainId, + sourceParaId: sourceParaId, sourceAccountHex, - tokenErcMetadata, + aggregatedAssets, sourceParachain, - ahAssetMetadata, - sourceAssetMetadata, messageId, }, tx, } } - async validateTransfer(context: Context, transfer: Transfer): Promise { - return validateTransferFromAssetHub(context, transfer) + async validateTransfer(context: Context, transfer: TransferV2): Promise { + return validateTransfer(context, transfer) } async createTx( @@ -167,10 +175,9 @@ export class ERC20FromAH implements TransferInterface { ethChainId: number, sourceAccount: string, beneficiaryAccount: string, - asset: Asset, - amount: bigint, messageId: string, - fee: DeliveryFee, + concreteAssets: ConcreteAsset[], + fee: DeliveryFeeV2, options?: { claimerLocation?: any contractCall?: ContractCall @@ -180,37 +187,16 @@ export class ERC20FromAH implements TransferInterface { if (options?.contractCall) { callHex = await buildContractCallHex(context, options.contractCall) } - let xcm: any - // If there is no fee specified, we assume that Ether is available in user's wallet on source chain, - // thus no swap required on Asset Hub. - if (!fee.feeLocation) { - xcm = buildTransferXcmFromAssetHub( - parachain.registry, - ethChainId, - sourceAccount, - beneficiaryAccount, - messageId, - asset, - amount, - fee, - callHex, - ) - } // If the fee asset is in DOT, we need to swap it to Ether on Asset Hub. - else if (isRelaychainLocation(fee.feeLocation)) { - xcm = buildTransferXcmFromAssetHubWithDOTAsFee( - parachain.registry, - ethChainId, - sourceAccount, - beneficiaryAccount, - messageId, - asset, - amount, - fee, - callHex, - ) - } else { - throw new Error(`Fee token as ${fee.feeLocation} is not supported yet.`) - } + const xcm = buildTransferXcmFromAssetHub( + parachain.registry, + ethChainId, + sourceAccount, + beneficiaryAccount, + messageId, + concreteAssets, + fee, + callHex, + ) console.log("xcm on AH:", xcm.toHuman()) return parachain.tx.polkadotXcm.execute(xcm, MaxWeight) } diff --git a/web/packages/api/src/transfers/toEthereum/erc20FromParachain.ts b/web/packages/api/src/transfers/toEthereum/erc20FromParachain.ts index fbac3a55a..88e42c788 100644 --- a/web/packages/api/src/transfers/toEthereum/erc20FromParachain.ts +++ b/web/packages/api/src/transfers/toEthereum/erc20FromParachain.ts @@ -7,20 +7,13 @@ import { DOT_LOCATION, isRelaychainLocation, isParachainNative } from "../../xcm import { buildExportXcm } from "../../xcmbuilders/toEthereum/erc20FromAH" import { buildResultXcmAssetHubERC20TransferFromParachain, - buildParachainERC20ReceivedXcmOnDestination, buildTransferXcmFromParachain, } from "../../xcmbuilders/toEthereum/erc20FromParachain" import { buildTransferXcmFromParachainWithDOTAsFee } from "../../xcmbuilders/toEthereum/erc20FromParachainWithDotAsFee" import { buildTransferXcmFromParachainWithNativeAssetFee } from "../../xcmbuilders/toEthereum/erc20FromParachainWithNativeAsFee" import { Asset, AssetRegistry, ContractCall } from "@snowbridge/base-types" import { paraImplementation } from "../../parachains" -import { - buildMessageId, - DeliveryFee, - resolveInputs, - Transfer, - ValidationResult, -} from "../../toEthereum_v2" +import { buildMessageId, resolveInputs } from "../../toEthereum_v2" import { Context } from "../.." import { TransferInterface } from "./transferInterface" import { @@ -28,14 +21,19 @@ import { estimateFeesFromParachains, MaxWeight, mockDeliveryFee, - validateTransferFromParachain, + validateTransfer, + DeliveryFeeV2, + TransferV2, + ValidationResultV2, } from "../../toEthereumSnowbridgeV2" +import { AggregatedAsset, ConcreteAsset, ConcreteToken } from "src/assets_v2" export class ERC20FromParachain implements TransferInterface { async getDeliveryFee( - source: { sourceParaId: number; context: Context }, + context: Context, + sourceParaId: number, registry: AssetRegistry, - tokenAddress: string, + tokenAddresses: string[], options?: { padPercentage?: bigint slippagePadPercentage?: bigint @@ -44,44 +42,29 @@ export class ERC20FromParachain implements TransferInterface { claimerLocation?: any contractCall?: ContractCall }, - ): Promise { - const { assetHub, parachain } = - "sourceParaId" in source - ? { - assetHub: await source.context.assetHub(), - parachain: await source.context.parachain(source.sourceParaId), - } - : source + ): Promise { + const assetHub = await context.assetHub() - const sourceParachainImpl = await paraImplementation(parachain) - const { sourceAssetMetadata } = resolveInputs(registry, tokenAddress, source.sourceParaId) + let concreteAssets: ConcreteAsset[] = [] + for (const tokenAddress of tokenAddresses) { + const { sourceAssetMetadata } = resolveInputs(registry, tokenAddress, sourceParaId) + concreteAssets.push({ + id: sourceAssetMetadata, + amount: 1n, + }) + } - let forwardXcmToAH: any, forwardedXcmToBH: any, returnToSenderXcm: any, localXcm: any + let forwardXcmToAH: any, forwardedXcmToBH: any, localXcm: any forwardXcmToAH = buildResultXcmAssetHubERC20TransferFromParachain( assetHub.registry, registry.ethChainId, + sourceParaId, "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - 1n, - 1n, - 1n, - sourceParachainImpl.parachainId, - 1n, - DOT_LOCATION, - DOT_LOCATION, - ) - - returnToSenderXcm = buildParachainERC20ReceivedXcmOnDestination( - parachain.registry, - registry.ethChainId, - "0x0000000000000000000000000000000000000000", - 1n, - 1n, - "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", + concreteAssets, + mockDeliveryFee, ) localXcm = buildTransferXcmFromParachain( @@ -89,36 +72,33 @@ export class ERC20FromParachain implements TransferInterface { registry.environment, registry.ethChainId, registry.assetHubParaId, - sourceParachainImpl.parachainId, + sourceParaId, "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", - sourceAssetMetadata, - 1n, + concreteAssets, mockDeliveryFee, ) forwardedXcmToBH = buildExportXcm( assetHub.registry, registry.ethChainId, - sourceAssetMetadata, "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", - 1n, - 1n, + concreteAssets, + mockDeliveryFee, ) const fees = await estimateFeesFromParachains( - source.context, - source.sourceParaId, + context, + sourceParaId, registry, - tokenAddress, + tokenAddresses, { localXcm, forwardXcmToAH, forwardedXcmToBH, - returnToSenderXcm, }, options, ) @@ -126,53 +106,67 @@ export class ERC20FromParachain implements TransferInterface { } async createTransfer( - source: { sourceParaId: number; context: Context }, + context: Context, + sourceParaId: number, registry: AssetRegistry, sourceAccount: string, beneficiaryAccount: string, - tokenAddress: string, - amount: bigint, - fee: DeliveryFee, + tokens: ConcreteToken[], + fee: DeliveryFeeV2, options?: { claimerLocation?: any contractCall?: ContractCall }, - ): Promise { + ): Promise { const { ethChainId, assetHubParaId, environment } = registry let sourceAccountHex = sourceAccount if (!isHex(sourceAccountHex)) { sourceAccountHex = u8aToHex(decodeAddress(sourceAccount)) } - const { parachain } = - "sourceParaId" in source - ? { parachain: await source.context.parachain(source.sourceParaId) } - : source + const parachain = await context.parachain(sourceParaId) + let sourceParachain = registry.parachains[sourceParaId.toString()] + let concreteAssets: ConcreteAsset[] = [], + aggregatedAssets: AggregatedAsset[] = [] - const sourceParachainImpl = await paraImplementation(parachain) - const { tokenErcMetadata, sourceParachain, ahAssetMetadata, sourceAssetMetadata } = - resolveInputs(registry, tokenAddress, sourceParachainImpl.parachainId) + for (const token of tokens) { + const { tokenErcMetadata, ahAssetMetadata, sourceAssetMetadata } = resolveInputs( + registry, + token.address, + sourceParaId, + ) + concreteAssets.push({ + id: sourceAssetMetadata, + amount: token.amount, + }) + aggregatedAssets.push({ + tokenErcMetadata, + ahAssetMetadata, + sourceAssetMetadata, + amount: token.amount, + }) + } - let messageId: string | undefined = await buildMessageId( + let messageId = await buildMessageId( parachain, - sourceParachainImpl.parachainId, + sourceParaId, sourceAccountHex, - tokenAddress, + tokens[0].address, beneficiaryAccount, - amount, + tokens[0].amount, ) + let tx: SubmittableExtrinsic<"promise", ISubmittableResult> = await this.createTx( - source.context, + context, parachain, environment, ethChainId, assetHubParaId, - sourceParachainImpl.parachainId, + sourceParaId, sourceAccountHex, beneficiaryAccount, - sourceAssetMetadata, - amount, messageId, + concreteAssets, fee, options, ) @@ -182,26 +176,23 @@ export class ERC20FromParachain implements TransferInterface { registry, sourceAccount, beneficiaryAccount, - tokenAddress, - amount, + tokens, fee, contractCall: options?.contractCall, }, computed: { - sourceParaId: sourceParachainImpl.parachainId, + sourceParaId, sourceAccountHex, - tokenErcMetadata, sourceParachain, - ahAssetMetadata, - sourceAssetMetadata, + aggregatedAssets, messageId, }, tx, } } - async validateTransfer(context: Context, transfer: Transfer): Promise { - return validateTransferFromParachain(context, transfer) + async validateTransfer(context: Context, transfer: TransferV2): Promise { + return validateTransfer(context, transfer) } async createTx( @@ -213,10 +204,9 @@ export class ERC20FromParachain implements TransferInterface { sourceParachainId: number, sourceAccount: string, beneficiaryAccount: string, - asset: Asset, - amount: bigint, messageId: string, - fee: DeliveryFee, + concreteAssets: ConcreteAsset[], + fee: DeliveryFeeV2, options?: { claimerLocation?: any contractCall?: ContractCall @@ -239,15 +229,14 @@ export class ERC20FromParachain implements TransferInterface { sourceAccount, beneficiaryAccount, messageId, - asset, - amount, + concreteAssets, fee, claimerLocation, callHex, ) } // One swap from DOT to Ether on Asset Hub. else if (isRelaychainLocation(fee.feeLocation)) { - xcm = buildTransferXcmFromParachainWithDOTAsFee( + xcm = buildTransferXcmFromParachain( parachain.registry, envName, ethChainId, @@ -256,8 +245,7 @@ export class ERC20FromParachain implements TransferInterface { sourceAccount, beneficiaryAccount, messageId, - asset, - amount, + concreteAssets, fee, claimerLocation, callHex, @@ -274,8 +262,7 @@ export class ERC20FromParachain implements TransferInterface { sourceAccount, beneficiaryAccount, messageId, - asset, - amount, + concreteAssets, fee, claimerLocation, callHex, diff --git a/web/packages/api/src/transfers/toEthereum/pnaFromAH.ts b/web/packages/api/src/transfers/toEthereum/pnaFromAH.ts index 6daa93f01..d7f96b7e1 100644 --- a/web/packages/api/src/transfers/toEthereum/pnaFromAH.ts +++ b/web/packages/api/src/transfers/toEthereum/pnaFromAH.ts @@ -11,13 +11,7 @@ import { import { buildTransferXcmFromAssetHubWithDOTAsFee } from "../../xcmbuilders/toEthereum/pnaFromAHWithDotAsFee" import { Asset, AssetRegistry, ContractCall } from "@snowbridge/base-types" import { paraImplementation } from "../../parachains" -import { - buildMessageId, - DeliveryFee, - resolveInputs, - Transfer, - ValidationResult, -} from "../../toEthereum_v2" +import { buildMessageId, resolveInputs } from "../../toEthereum_v2" import { Context } from "../.." import { TransferInterface } from "./transferInterface" import { @@ -25,14 +19,19 @@ import { estimateFeesFromAssetHub, MaxWeight, mockDeliveryFee, - validateTransferFromAssetHub, + validateTransfer, + DeliveryFeeV2, + TransferV2, + ValidationResultV2, } from "../../toEthereumSnowbridgeV2" +import { ConcreteToken } from "src/assets_v2" export class PNAFromAH implements TransferInterface { async getDeliveryFee( - source: { sourceParaId: number; context: Context }, + context: Context, + sourceParaId: number, registry: AssetRegistry, - tokenAddress: string, + tokenAddresses: string[], options?: { padPercentage?: bigint slippagePadPercentage?: bigint @@ -40,21 +39,10 @@ export class PNAFromAH implements TransferInterface { feeTokenLocation?: any contractCall?: ContractCall }, - ): Promise { - const { assetHub, parachain } = - "sourceParaId" in source - ? { - assetHub: await source.context.assetHub(), - parachain: await source.context.parachain(source.sourceParaId), - } - : source + ): Promise { + const assetHub = await context.assetHub() - const sourceParachainImpl = await paraImplementation(parachain) - const { sourceAssetMetadata } = resolveInputs( - registry, - tokenAddress, - sourceParachainImpl.parachainId, - ) + const { sourceAssetMetadata } = resolveInputs(registry, tokenAddresses[0], sourceParaId) let forwardedXcmToBH, localXcm: any @@ -81,9 +69,9 @@ export class PNAFromAH implements TransferInterface { ) const fees = await estimateFeesFromAssetHub( - source.context, + context, registry, - tokenAddress, + tokenAddresses, { localXcm, forwardedXcmToBH, @@ -94,49 +82,46 @@ export class PNAFromAH implements TransferInterface { } async createTransfer( - source: { sourceParaId: number; context: Context }, + context: Context, + sourceParaId: number, registry: AssetRegistry, sourceAccount: string, beneficiaryAccount: string, - tokenAddress: string, - amount: bigint, - fee: DeliveryFee, + tokens: ConcreteToken[], + fee: DeliveryFeeV2, options?: { claimerLocation?: any contractCall?: ContractCall }, - ): Promise { + ): Promise { const { ethChainId } = registry let sourceAccountHex = sourceAccount if (!isHex(sourceAccountHex)) { sourceAccountHex = u8aToHex(decodeAddress(sourceAccount)) } - const { parachain } = - "sourceParaId" in source - ? { parachain: await source.context.parachain(source.sourceParaId) } - : source + const parachain = await context.parachain(sourceParaId) const sourceParachainImpl = await paraImplementation(parachain) const { tokenErcMetadata, sourceParachain, ahAssetMetadata, sourceAssetMetadata } = - resolveInputs(registry, tokenAddress, sourceParachainImpl.parachainId) + resolveInputs(registry, tokens[0].address, sourceParachainImpl.parachainId) let messageId: string | undefined = await buildMessageId( parachain, sourceParachainImpl.parachainId, sourceAccountHex, - tokenAddress, + tokens[0].address, beneficiaryAccount, - amount, + tokens[0].amount, ) let tx: SubmittableExtrinsic<"promise", ISubmittableResult> = await this.createTx( - source.context, + context, parachain, ethChainId, sourceAccount, beneficiaryAccount, ahAssetMetadata, - amount, + tokens[0].amount, messageId, fee, options, @@ -147,26 +132,30 @@ export class PNAFromAH implements TransferInterface { registry, sourceAccount, beneficiaryAccount, - tokenAddress, - amount, + tokens, fee, contractCall: options?.contractCall, }, computed: { sourceParaId: sourceParachainImpl.parachainId, sourceAccountHex, - tokenErcMetadata, sourceParachain, - ahAssetMetadata, - sourceAssetMetadata, + aggregatedAssets: [ + { + tokenErcMetadata, + ahAssetMetadata, + sourceAssetMetadata, + amount: tokens[0].amount, + }, + ], messageId, }, tx, } } - async validateTransfer(context: Context, transfer: Transfer): Promise { - return validateTransferFromAssetHub(context, transfer) + async validateTransfer(context: Context, transfer: TransferV2): Promise { + return validateTransfer(context, transfer) } async createTx( @@ -178,7 +167,7 @@ export class PNAFromAH implements TransferInterface { asset: Asset, amount: bigint, messageId: string, - fee: DeliveryFee, + fee: DeliveryFeeV2, options?: { claimerLocation?: any contractCall?: ContractCall diff --git a/web/packages/api/src/transfers/toEthereum/pnaFromParachain.ts b/web/packages/api/src/transfers/toEthereum/pnaFromParachain.ts index 57d834847..839fed93a 100644 --- a/web/packages/api/src/transfers/toEthereum/pnaFromParachain.ts +++ b/web/packages/api/src/transfers/toEthereum/pnaFromParachain.ts @@ -7,20 +7,13 @@ import { isRelaychainLocation, isParachainNative } from "../../xcmBuilder" import { buildExportXcm } from "../../xcmbuilders/toEthereum/pnaFromAH" import { buildResultXcmAssetHubPNATransferFromParachain, - buildParachainPNAReceivedXcmOnDestination, buildTransferXcmFromParachain, } from "../../xcmbuilders/toEthereum/pnaFromParachain" import { buildTransferXcmFromParachainWithDOTAsFee } from "../../xcmbuilders/toEthereum/pnaFromParachainWithDotAsFee" import { buildTransferXcmFromParachainWithNativeAssetFee } from "../../xcmbuilders/toEthereum/pnaFromParachainWithNativeAsFee" import { Asset, AssetRegistry, ContractCall } from "@snowbridge/base-types" import { paraImplementation } from "../../parachains" -import { - buildMessageId, - DeliveryFee, - resolveInputs, - Transfer, - ValidationResult, -} from "../../toEthereum_v2" +import { buildMessageId, resolveInputs } from "../../toEthereum_v2" import { Context } from "../.." import { TransferInterface } from "./transferInterface" import { @@ -28,14 +21,19 @@ import { estimateFeesFromParachains, MaxWeight, mockDeliveryFee, - validateTransferFromParachain, + validateTransfer, + DeliveryFeeV2, + TransferV2, + ValidationResultV2, } from "../../toEthereumSnowbridgeV2" +import { ConcreteToken } from "../../assets_v2" export class PNAFromParachain implements TransferInterface { async getDeliveryFee( - source: { sourceParaId: number; context: Context }, + context: Context, + sourceParaId: number, registry: AssetRegistry, - tokenAddress: string, + tokenAddresses: string[], options?: { padPercentage?: bigint slippagePadPercentage?: bigint @@ -44,19 +42,14 @@ export class PNAFromParachain implements TransferInterface { claimerLocation?: any contractCall?: ContractCall }, - ): Promise { - const { assetHub, parachain } = - "sourceParaId" in source - ? { - assetHub: await source.context.assetHub(), - parachain: await source.context.parachain(source.sourceParaId), - } - : source + ): Promise { + const assetHub = await context.assetHub() + const parachain = await context.parachain(sourceParaId) const sourceParachainImpl = await paraImplementation(parachain) const { sourceAssetMetadata } = resolveInputs( registry, - tokenAddress, + tokenAddresses[0], sourceParachainImpl.parachainId, ) @@ -75,15 +68,6 @@ export class PNAFromParachain implements TransferInterface { 340282366920938463463374607431768211455n, ) - returnToSenderXcm = buildParachainPNAReceivedXcmOnDestination( - parachain.registry, - sourceAssetMetadata.location, - 340282366920938463463374607431768211455n, - 340282366920938463463374607431768211455n, - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - ) - localXcm = buildTransferXcmFromParachain( assetHub.registry, registry.environment, @@ -110,15 +94,14 @@ export class PNAFromParachain implements TransferInterface { ) const fees = await estimateFeesFromParachains( - source.context, - source.sourceParaId, + context, + sourceParaId, registry, - tokenAddress, + tokenAddresses, { localXcm, forwardXcmToAH, forwardedXcmToBH, - returnToSenderXcm, }, options, ) @@ -126,43 +109,40 @@ export class PNAFromParachain implements TransferInterface { } async createTransfer( - source: { sourceParaId: number; context: Context }, + context: Context, + sourceParaId: number, registry: AssetRegistry, sourceAccount: string, beneficiaryAccount: string, - tokenAddress: string, - amount: bigint, - fee: DeliveryFee, + tokens: ConcreteToken[], + fee: DeliveryFeeV2, options?: { claimerLocation?: any contractCall?: ContractCall }, - ): Promise { + ): Promise { const { ethChainId, assetHubParaId, environment } = registry let sourceAccountHex = sourceAccount if (!isHex(sourceAccountHex)) { sourceAccountHex = u8aToHex(decodeAddress(sourceAccount)) } - const { parachain } = - "sourceParaId" in source - ? { parachain: await source.context.parachain(source.sourceParaId) } - : source + const parachain = await context.parachain(sourceParaId) const sourceParachainImpl = await paraImplementation(parachain) const { tokenErcMetadata, sourceParachain, ahAssetMetadata, sourceAssetMetadata } = - resolveInputs(registry, tokenAddress, sourceParachainImpl.parachainId) + resolveInputs(registry, tokens[0].address, sourceParachainImpl.parachainId) let messageId: string | undefined = await buildMessageId( parachain, sourceParachainImpl.parachainId, sourceAccountHex, - tokenAddress, + tokens[0].address, beneficiaryAccount, - amount, + tokens[0].amount, ) let tx: SubmittableExtrinsic<"promise", ISubmittableResult> = await this.createTx( - source.context, + context, parachain, environment, ethChainId, @@ -171,7 +151,7 @@ export class PNAFromParachain implements TransferInterface { sourceAccountHex, beneficiaryAccount, sourceAssetMetadata, - amount, + tokens[0].amount, messageId, fee, options, @@ -182,26 +162,30 @@ export class PNAFromParachain implements TransferInterface { registry, sourceAccount, beneficiaryAccount, - tokenAddress, - amount, + tokens, fee, contractCall: options?.contractCall, }, computed: { sourceParaId: sourceParachainImpl.parachainId, sourceAccountHex, - tokenErcMetadata, sourceParachain, - ahAssetMetadata, - sourceAssetMetadata, + aggregatedAssets: [ + { + tokenErcMetadata, + ahAssetMetadata, + sourceAssetMetadata, + amount: tokens[0].amount, + }, + ], messageId, }, tx, } } - async validateTransfer(context: Context, transfer: Transfer): Promise { - return validateTransferFromParachain(context, transfer) + async validateTransfer(context: Context, transfer: TransferV2): Promise { + return validateTransfer(context, transfer) } async createTx( @@ -216,7 +200,7 @@ export class PNAFromParachain implements TransferInterface { asset: Asset, amount: bigint, messageId: string, - fee: DeliveryFee, + fee: DeliveryFeeV2, options?: { claimerLocation?: any contractCall?: ContractCall diff --git a/web/packages/api/src/transfers/toEthereum/transferInterface.ts b/web/packages/api/src/transfers/toEthereum/transferInterface.ts index 6c49cec1e..312dbabc1 100644 --- a/web/packages/api/src/transfers/toEthereum/transferInterface.ts +++ b/web/packages/api/src/transfers/toEthereum/transferInterface.ts @@ -1,12 +1,14 @@ import { AssetRegistry, ContractCall } from "@snowbridge/base-types" -import { DeliveryFee, Transfer, ValidationResult } from "../../toEthereum_v2" +import { DeliveryFeeV2, TransferV2, ValidationResultV2 } from "../../toEthereumSnowbridgeV2" import { Context } from "../../index" +import { ConcreteToken } from "../../assets_v2" export interface TransferInterface { getDeliveryFee( - source: { sourceParaId: number; context: Context }, + context: Context, + sourceParaId: number, registry: AssetRegistry, - tokenAddress: string, + tokenAddresses: string[], options?: { padPercentage?: bigint slippagePadPercentage?: bigint @@ -15,21 +17,21 @@ export interface TransferInterface { claimerLocation?: any contractCall?: ContractCall }, - ): Promise + ): Promise createTransfer( - source: { sourceParaId: number; context: Context }, + context: Context, + sourceParaId: number, registry: AssetRegistry, sourceAccount: string, beneficiaryAccount: string, - tokenAddress: string, - amount: bigint, - fee: DeliveryFee, + tokens: ConcreteToken[], + fee: DeliveryFeeV2, options?: { claimerLocation?: any contractCall?: ContractCall }, - ): Promise + ): Promise - validateTransfer(context: Context, transfer: Transfer): Promise + validateTransfer(context: Context, transfer: TransferV2): Promise } diff --git a/web/packages/api/src/xcmBuilder.ts b/web/packages/api/src/xcmBuilder.ts index b4f0380ea..5580cfac0 100644 --- a/web/packages/api/src/xcmBuilder.ts +++ b/web/packages/api/src/xcmBuilder.ts @@ -1,6 +1,6 @@ import { Registry } from "@polkadot/types/types" import { beneficiaryMultiAddress } from "./utils" -import { ETHER_TOKEN_ADDRESS } from "./assets_v2" +import { ConcreteAsset, ETHER_TOKEN_ADDRESS } from "./assets_v2" export const HERE_LOCATION = { parents: 0, interior: "Here" } export const DOT_LOCATION = { parents: 1, interior: "Here" } @@ -1401,7 +1401,7 @@ export function isEthereumAsset(location: any): boolean { } export function isRelaychainLocation(location: any) { - return location.parents == DOT_LOCATION.parents && location.interior == DOT_LOCATION.interior + return location?.parents == DOT_LOCATION.parents && location?.interior == DOT_LOCATION.interior } export function isParachainNative(location: any, parachainId: number) { @@ -1847,7 +1847,7 @@ export function buildEthereumInstructions( depositAsset: { assets: { wild: { - allCounted: 3, + allCounted: 8, }, }, beneficiary: { @@ -1873,3 +1873,32 @@ export function buildEthereumInstructions( }) return remoteXcm } + +export function containsEther(ethChainId: number, concreteAssets: ConcreteAsset[]): boolean { + for (const asset of concreteAssets) { + const tokenLocation = erc20Location(ethChainId, asset.id.token) + if (isEthereumNative(tokenLocation, ethChainId)) { + return true + } + } + return false +} + +export function splitEtherAsset( + ethChainId: number, + concreteAssets: ConcreteAsset[], +): { etherAsset?: ConcreteAsset; otherAssets: ConcreteAsset[] } { + let etherAsset: ConcreteAsset | undefined + const otherAssets: ConcreteAsset[] = [] + + for (const asset of concreteAssets) { + const tokenLocation = erc20Location(ethChainId, asset.id.token) + if (isEthereumNative(tokenLocation, ethChainId)) { + etherAsset = asset + } else { + otherAssets.push(asset) + } + } + + return { etherAsset, otherAssets } +} diff --git a/web/packages/api/src/xcmbuilders/toEthereum/erc20FromAH.ts b/web/packages/api/src/xcmbuilders/toEthereum/erc20FromAH.ts index 4bfc9f2ed..4e088df88 100644 --- a/web/packages/api/src/xcmbuilders/toEthereum/erc20FromAH.ts +++ b/web/packages/api/src/xcmbuilders/toEthereum/erc20FromAH.ts @@ -6,31 +6,41 @@ import { erc20LocationReanchored, accountToLocation, HERE_LOCATION, - isEthereumNative, buildEthereumInstructions, + containsEther, + splitEtherAsset, } from "../../xcmBuilder" -import { Asset } from "@snowbridge/base-types" -import { DeliveryFee } from "../../toEthereum_v2" +import { ConcreteAsset } from "../../assets_v2" +import { DeliveryFeeV2 } from "../../toEthereumSnowbridgeV2" export function buildExportXcm( registry: Registry, ethChainId: number, - asset: Asset, sender: string, beneficiary: string, topic: string, - transferAmount: bigint, - feeInEther: bigint, + concreteAssets: ConcreteAsset[], + deliveryFee: DeliveryFeeV2, ) { let senderLocation = accountToLocation(sender) let beneficiaryLocation = accountToLocation(beneficiary) + let assets = [] + for (const asset of concreteAssets) { + const tokenLocation = erc20LocationReanchored(asset.id.token) + assets.push({ + id: tokenLocation, + fun: { + Fungible: asset.amount, + }, + }) + } let exportXcm: any[] = [ { withdrawAsset: [ { id: HERE_LOCATION, fun: { - Fungible: feeInEther, + Fungible: deliveryFee.ethereumExecutionFee!, }, }, ], @@ -40,20 +50,13 @@ export function buildExportXcm( asset: { id: HERE_LOCATION, fun: { - Fungible: feeInEther, + Fungible: deliveryFee.ethereumExecutionFee!, }, }, }, }, { - withdrawAsset: [ - { - id: erc20LocationReanchored(asset.token), - fun: { - Fungible: transferAmount, - }, - }, - ], + withdrawAsset: assets, }, { aliasOrigin: { @@ -109,49 +112,104 @@ export function buildTransferXcmFromAssetHub( sourceAccount: string, beneficiary: string, topic: string, - asset: Asset, - tokenAmount: bigint, - fee: DeliveryFee, + concreteAssets: ConcreteAsset[], + fee: DeliveryFeeV2, callHex?: string, ) { let beneficiaryLocation = accountToLocation(beneficiary) let sourceLocation = accountToLocation(sourceAccount) - let tokenLocation = erc20Location(ethChainId, asset.token) let totalDOTFeeAmount = fee.totalFeeInDot let remoteEtherFeeAmount = fee.ethereumExecutionFee! - let assets = [] + let assets = [], + assetInstructions = [] + assets.push({ id: DOT_LOCATION, fun: { Fungible: totalDOTFeeAmount, }, }) - if (isEthereumNative(tokenLocation, ethChainId)) { + if (!containsEther(ethChainId, concreteAssets)) { assets.push({ id: bridgeLocation(ethChainId), fun: { - Fungible: tokenAmount + remoteEtherFeeAmount, + Fungible: remoteEtherFeeAmount, }, }) + for (const asset of concreteAssets) { + const tokenLocation = erc20Location(ethChainId, asset.id.token) + const tokenAmount = asset.amount + assets.push({ + id: tokenLocation, + fun: { + Fungible: tokenAmount, + }, + }) + } } else { + const { etherAsset, otherAssets } = splitEtherAsset(ethChainId, concreteAssets) assets.push({ id: bridgeLocation(ethChainId), fun: { - Fungible: remoteEtherFeeAmount, + Fungible: etherAsset!.amount + remoteEtherFeeAmount, }, }) - assets.push({ - id: tokenLocation, - fun: { - Fungible: tokenAmount, + for (const asset of otherAssets) { + const tokenLocation = erc20Location(ethChainId, asset.id.token) + const tokenAmount = asset.amount + assets.push({ + id: tokenLocation, + fun: { + Fungible: tokenAmount, + }, + }) + } + } + for (const asset of concreteAssets) { + const tokenLocation = erc20Location(ethChainId, asset.id.token) + const tokenAmount = asset.amount + assetInstructions.push({ + reserveWithdraw: { + definite: [ + { + id: tokenLocation, + fun: { + Fungible: tokenAmount, + }, + }, + ], }, }) } let remoteXcm = buildEthereumInstructions(beneficiaryLocation, topic, callHex) + let exchangeInstruction = fee.feeLocation + ? { + exchangeAsset: { + give: { + Wild: { + AllOf: { + id: fee.feeLocation, + fun: "Fungible", + }, + }, + }, + want: [ + { + id: bridgeLocation(ethChainId), + fun: { + Fungible: remoteEtherFeeAmount, + }, + }, + ], + maximal: false, + }, + } + : undefined + let instructions: any[] = [ { withdrawAsset: assets, @@ -175,7 +233,7 @@ export function buildTransferXcmFromAssetHub( depositAsset: { assets: { wild: { - allCounted: 2, + allCounted: 8, }, }, beneficiary: { @@ -186,6 +244,7 @@ export function buildTransferXcmFromAssetHub( }, ], }, + ...(exchangeInstruction ? [exchangeInstruction] : []), { initiateTransfer: { destination: bridgeLocation(ethChainId), @@ -202,20 +261,7 @@ export function buildTransferXcmFromAssetHub( }, }, preserveOrigin: true, - assets: [ - { - reserveWithdraw: { - definite: [ - { - id: tokenLocation, - fun: { - Fungible: tokenAmount, - }, - }, - ], - }, - }, - ], + assets: assetInstructions, remoteXcm: remoteXcm, }, }, diff --git a/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachain.ts b/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachain.ts index 0d9d62d53..d2ad3532c 100644 --- a/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachain.ts +++ b/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachain.ts @@ -10,37 +10,42 @@ import { isEthereumNative, buildAppendixInstructions, buildEthereumInstructions, + containsEther, + splitEtherAsset, } from "../../xcmBuilder" -import { Asset } from "@snowbridge/base-types" -import { DeliveryFee } from "../../toEthereum_v2" +import { ConcreteAsset } from "../../assets_v2" +import { DeliveryFeeV2 } from "../../toEthereumSnowbridgeV2" -export function buildParachainERC20ReceivedXcmOnDestination( +export function buildResultXcmAssetHubERC20TransferFromParachain( registry: Registry, ethChainId: number, - tokenAddress: string, - transferAmount: bigint, - feeInDot: bigint, + sourceParachainId: number, + sourceAccount: string, beneficiary: string, topic: string, + concreteAssets: ConcreteAsset[], + deliveryFee: DeliveryFeeV2, ) { - let beneficiaryLocation = accountToLocation(beneficiary) + let assets: any[] = [ + { + id: DOT_LOCATION, + fun: { + Fungible: deliveryFee.totalFeeInDot!, + }, + }, + ] + for (const asset of concreteAssets) { + assets.push({ + id: erc20Location(ethChainId, asset.id.token), + fun: { + Fungible: asset.amount, + }, + }) + } return registry.createType("XcmVersionedXcm", { v5: [ { - reserveAssetDeposited: [ - { - id: DOT_LOCATION, - fun: { - Fungible: feeInDot, - }, - }, - { - id: erc20Location(ethChainId, tokenAddress), - fun: { - Fungible: transferAmount, - }, - }, - ], + withdrawAsset: assets, }, { clearOrigin: null }, { @@ -48,222 +53,54 @@ export function buildParachainERC20ReceivedXcmOnDestination( fees: { id: DOT_LOCATION, fun: { - Fungible: feeInDot, + Fungible: deliveryFee.assetHubExecutionFeeDOT!, }, }, weightLimit: "Unlimited", }, }, { - depositAsset: { + initiateReserveWithdraw: { assets: { - wild: { - allCounted: 2, + Wild: { + AllOf: { id: bridgeLocation(ethChainId), fun: "Fungible" }, }, }, - beneficiary: { - parents: 0, - interior: { x1: [beneficiaryLocation] }, - }, - }, - }, - { setTopic: topic }, - ], - }) -} - -function buildAssetHubXcmFromParachain( - ethChainId: number, - sourceAccount: string, - beneficiary: string, - tokenAddress: string, - topic: string, - sourceParachainId: number, - destinationFee: bigint, - feeAssetId: any, -) { - let { - hexAddress, - address: { kind }, - } = beneficiaryMultiAddress(sourceAccount) - let sourceAccountLocation - switch (kind) { - case 1: - // 32 byte addresses - sourceAccountLocation = { accountId32: { id: hexAddress } } - break - case 2: - // 20 byte addresses - sourceAccountLocation = { accountKey20: { key: hexAddress } } - break - default: - throw Error(`Could not parse source address ${sourceAccount}`) - } - return [ - // Error Handling, return everything to sender on source parachain - { - setAppendix: [ - { - depositReserveAsset: { - assets: { - wild: "All", - }, - dest: { parents: 1, interior: { x1: [{ parachain: sourceParachainId }] } }, - xcm: [ - { - buyExecution: { - fees: { - id: feeAssetId, - fun: { - fungible: destinationFee, - }, + reserve: bridgeLocation(ethChainId), + xcm: [ + { + buyExecution: { + fees: { + id: bridgeLocation(ethChainId), // CAUTION: Must use reanchored locations. + fun: { + Fungible: "1", // Offering 1 unit as fee, but it is returned to the beneficiary address. }, - weightLimit: "Unlimited", }, + weight_limit: "Unlimited", }, - { - depositAsset: { - assets: { - wild: "All", - }, - beneficiary: { - parents: 0, - interior: { x1: [sourceAccountLocation] }, + }, + { + depositAsset: { + assets: { + Wild: { + AllCounted: 1, }, }, - }, - { setTopic: topic }, - ], - }, - }, - ], - }, - // Initiate the bridged transfer - { - initiateReserveWithdraw: { - assets: { - Wild: { - AllOf: { id: erc20Location(ethChainId, tokenAddress), fun: "Fungible" }, - }, - }, - reserve: bridgeLocation(ethChainId), - xcm: [ - { - buyExecution: { - fees: { - id: erc20LocationReanchored(tokenAddress), // CAUTION: Must use reanchored locations. - fun: { - Fungible: "1", // Offering 1 unit as fee, but it is returned to the beneficiary address. - }, - }, - weight_limit: "Unlimited", - }, - }, - { - depositAsset: { - assets: { - Wild: { - AllCounted: 1, + beneficiary: { + parents: 0, + interior: { x1: [{ AccountKey20: { key: beneficiary } }] }, }, }, - beneficiary: { - parents: 0, - interior: { x1: [{ AccountKey20: { key: beneficiary } }] }, - }, - }, - }, - { - setTopic: topic, - }, - ], - }, - }, - { - setTopic: topic, - }, - ] -} - -export function buildAssetHubERC20TransferFromParachain( - registry: Registry, - ethChainId: number, - sourceAccount: string, - beneficiary: string, - tokenAddress: string, - topic: string, - sourceParachainId: number, - returnToSenderFee: bigint, - feeAssetId: any, -) { - return registry.createType("XcmVersionedXcm", { - v5: buildAssetHubXcmFromParachain( - ethChainId, - sourceAccount, - beneficiary, - tokenAddress, - topic, - sourceParachainId, - returnToSenderFee, - feeAssetId, - ), - }) -} - -export function buildResultXcmAssetHubERC20TransferFromParachain( - registry: Registry, - ethChainId: number, - sourceAccount: string, - beneficiary: string, - tokenAddress: string, - topic: string, - transferAmount: bigint, - totalFee: bigint, - destinationFee: bigint, - sourceParachainId: number, - returnToSenderFee: bigint, - feeAssetId: any, - feeAssetIdReanchored: any, -) { - return registry.createType("XcmVersionedXcm", { - v5: [ - { - withdrawAsset: [ - { - id: feeAssetIdReanchored, - fun: { - Fungible: totalFee, }, - }, - { - id: erc20Location(ethChainId, tokenAddress), - fun: { - Fungible: transferAmount, + { + setTopic: topic, }, - }, - ], + ], + }, }, - { clearOrigin: null }, { - buyExecution: { - fees: { - id: feeAssetIdReanchored, - fun: { - Fungible: destinationFee, - }, - }, - weightLimit: "Unlimited", - }, + setTopic: topic, }, - ...buildAssetHubXcmFromParachain( - ethChainId, - sourceAccount, - beneficiary, - tokenAddress, - topic, - sourceParachainId, - returnToSenderFee, - feeAssetId, - ), ], }) } @@ -277,15 +114,13 @@ export function buildTransferXcmFromParachain( sourceAccount: string, beneficiary: string, topic: string, - asset: Asset, - tokenAmount: bigint, - fee: DeliveryFee, + concreteAssets: ConcreteAsset[], + fee: DeliveryFeeV2, claimerLocation?: any, callHex?: string, ) { let beneficiaryLocation = accountToLocation(beneficiary) let sourceLocation = accountToLocation(sourceAccount) - let tokenLocation = erc20Location(ethChainId, asset.token) let localDOTFeeAmount: bigint = fee.localExecutionFeeDOT! + fee.localDeliveryFeeDOT! + fee.returnToSenderExecutionFeeDOT @@ -293,31 +128,63 @@ export function buildTransferXcmFromParachain( let remoteEtherFeeAmount: bigint = fee.ethereumExecutionFee! let assets = [] - assets.push({ id: DOT_LOCATION, fun: { Fungible: totalDOTFeeAmount, }, }) - if (isEthereumNative(tokenLocation, ethChainId)) { + if (!containsEther(ethChainId, concreteAssets)) { assets.push({ id: bridgeLocation(ethChainId), fun: { - Fungible: remoteEtherFeeAmount + tokenAmount, + Fungible: remoteEtherFeeAmount, }, }) + for (const asset of concreteAssets) { + const tokenLocation = erc20Location(ethChainId, asset.id.token) + const tokenAmount = asset.amount + assets.push({ + id: tokenLocation, + fun: { + Fungible: tokenAmount, + }, + }) + } } else { + const { etherAsset, otherAssets } = splitEtherAsset(ethChainId, concreteAssets) assets.push({ id: bridgeLocation(ethChainId), fun: { - Fungible: remoteEtherFeeAmount, + Fungible: etherAsset!.amount + remoteEtherFeeAmount, }, }) - assets.push({ - id: tokenLocation, - fun: { - Fungible: tokenAmount, + for (const asset of otherAssets) { + const tokenLocation = erc20Location(ethChainId, asset.id.token) + const tokenAmount = asset.amount + assets.push({ + id: tokenLocation, + fun: { + Fungible: tokenAmount, + }, + }) + } + } + + let assetInstructions = [] + for (const asset of concreteAssets) { + const tokenLocation = erc20Location(ethChainId, asset.id.token) + const tokenAmount = asset.amount + assetInstructions.push({ + reserveWithdraw: { + definite: [ + { + id: tokenLocation, + fun: { + Fungible: tokenAmount, + }, + }, + ], }, }) } @@ -331,10 +198,35 @@ export function buildTransferXcmFromParachain( let remoteXcm = buildEthereumInstructions(beneficiaryLocation, topic, callHex) + let exchangeInstruction = fee.feeLocation + ? { + exchangeAsset: { + give: { + Wild: { + AllOf: { + id: fee.feeLocation, + fun: "Fungible", + }, + }, + }, + want: [ + { + id: bridgeLocation(ethChainId), + fun: { + Fungible: remoteEtherFeeAmount, + }, + }, + ], + maximal: false, + }, + } + : undefined + let remoteInstructionsOnAH: any[] = [ { setAppendix: appendixInstructions, }, + ...(exchangeInstruction ? [exchangeInstruction] : []), { initiateTransfer: { destination: bridgeLocation(ethChainId), @@ -351,20 +243,7 @@ export function buildTransferXcmFromParachain( }, }, preserveOrigin: true, - assets: [ - { - reserveWithdraw: { - definite: [ - { - id: tokenLocation, - fun: { - Fungible: tokenAmount, - }, - }, - ], - }, - }, - ], + assets: assetInstructions, remoteXcm: remoteXcm, }, }, @@ -396,7 +275,7 @@ export function buildTransferXcmFromParachain( depositAsset: { assets: { wild: { - allCounted: 3, + allCounted: 8, }, }, beneficiary: { @@ -416,7 +295,10 @@ export function buildTransferXcmFromParachain( { id: DOT_LOCATION, fun: { - Fungible: totalDOTFeeAmount - localDOTFeeAmount, + Fungible: + totalDOTFeeAmount - + localDOTFeeAmount - + remoteEtherFeeAmount, }, }, ], @@ -436,18 +318,7 @@ export function buildTransferXcmFromParachain( ], }, }, - { - reserveWithdraw: { - definite: [ - { - id: tokenLocation, - fun: { - Fungible: tokenAmount, - }, - }, - ], - }, - }, + ...assetInstructions, ], remoteXcm: remoteInstructionsOnAH, }, diff --git a/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachainWithNativeAsFee.ts b/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachainWithNativeAsFee.ts index bc4e32298..44a9643d2 100644 --- a/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachainWithNativeAsFee.ts +++ b/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachainWithNativeAsFee.ts @@ -8,9 +8,11 @@ import { HERE_LOCATION, buildAppendixInstructions, buildEthereumInstructions, + containsEther, + splitEtherAsset, } from "../../xcmBuilder" -import { Asset } from "@snowbridge/base-types" import { DeliveryFee } from "../../toEthereum_v2" +import { ConcreteAsset } from "../../assets_v2" export function buildTransferXcmFromParachainWithNativeAssetFee( registry: Registry, @@ -21,15 +23,13 @@ export function buildTransferXcmFromParachainWithNativeAssetFee( sourceAccount: string, beneficiary: string, topic: string, - asset: Asset, - tokenAmount: bigint, + concreteAssets: ConcreteAsset[], fee: DeliveryFee, claimerLocation?: any, callHex?: string, ) { let beneficiaryLocation = accountToLocation(beneficiary) let sourceLocation = accountToLocation(sourceAccount) - let tokenLocation = erc20Location(ethChainId, asset.token) let localNativeFeeAmount = fee.localExecutionFeeInNative! + @@ -39,20 +39,67 @@ export function buildTransferXcmFromParachainWithNativeAssetFee( let remoteEtherFeeAmount = fee.ethereumExecutionFee! let remoteEtherFeeNativeAmount = fee.ethereumExecutionFeeInNative! - let assets = [ - { - id: HERE_LOCATION, + let assets = [] + assets.push({ + id: HERE_LOCATION, + fun: { + Fungible: totalNativeFeeAmount, + }, + }) + if (!containsEther(ethChainId, concreteAssets)) { + assets.push({ + id: bridgeLocation(ethChainId), fun: { - Fungible: totalNativeFeeAmount, + Fungible: remoteEtherFeeAmount, }, - }, - { - id: tokenLocation, + }) + for (const asset of concreteAssets) { + const tokenLocation = erc20Location(ethChainId, asset.id.token) + const tokenAmount = asset.amount + assets.push({ + id: tokenLocation, + fun: { + Fungible: tokenAmount, + }, + }) + } + } else { + const { etherAsset, otherAssets } = splitEtherAsset(ethChainId, concreteAssets) + assets.push({ + id: bridgeLocation(ethChainId), fun: { - Fungible: tokenAmount, + Fungible: etherAsset!.amount + remoteEtherFeeAmount, }, - }, - ] + }) + for (const asset of otherAssets) { + const tokenLocation = erc20Location(ethChainId, asset.id.token) + const tokenAmount = asset.amount + assets.push({ + id: tokenLocation, + fun: { + Fungible: tokenAmount, + }, + }) + } + } + + let assetInstructions = [] + for (const asset of concreteAssets) { + const tokenLocation = erc20Location(ethChainId, asset.id.token) + const tokenAmount = asset.amount + assetInstructions.push({ + reserveWithdraw: { + definite: [ + { + id: tokenLocation, + fun: { + Fungible: tokenAmount, + }, + }, + ], + }, + }) + } let appendixInstructions = buildAppendixInstructions( envName, @@ -130,20 +177,7 @@ export function buildTransferXcmFromParachainWithNativeAssetFee( }, }, preserveOrigin: true, - assets: [ - { - reserveWithdraw: { - definite: [ - { - id: tokenLocation, - fun: { - Fungible: tokenAmount, - }, - }, - ], - }, - }, - ], + assets: assetInstructions, remoteXcm: remoteXcm, }, }, @@ -219,18 +253,7 @@ export function buildTransferXcmFromParachainWithNativeAssetFee( ], }, }, - { - reserveWithdraw: { - definite: [ - { - id: tokenLocation, - fun: { - Fungible: tokenAmount, - }, - }, - ], - }, - }, + ...assetInstructions, ], remoteXcm: remoteInstructionsOnAH, }, diff --git a/web/packages/api/src/xcmbuilders/toEthereum/pnaFromParachain.ts b/web/packages/api/src/xcmbuilders/toEthereum/pnaFromParachain.ts index 626d504b4..ec67e2653 100644 --- a/web/packages/api/src/xcmbuilders/toEthereum/pnaFromParachain.ts +++ b/web/packages/api/src/xcmbuilders/toEthereum/pnaFromParachain.ts @@ -122,67 +122,6 @@ function buildAssetHubXcmForPNAFromParachain( ] } -export function buildParachainPNAReceivedXcmOnDestination( - registry: Registry, - assetLocation: any, - transferAmount: bigint, - feeInDot: bigint, - beneficiary: string, - topic: string, -) { - let beneficiaryLocation = accountToLocation(beneficiary) - return registry.createType("XcmVersionedXcm", { - v5: [ - { - reserveAssetDeposited: [ - { - id: DOT_LOCATION, - fun: { - Fungible: feeInDot, - }, - }, - ], - }, - { - buyExecution: { - fees: { - id: DOT_LOCATION, - fun: { - Fungible: feeInDot, - }, - }, - weightLimit: "Unlimited", - }, - }, - { - receiveTeleportedAsset: [ - { - id: assetLocation, - fun: { - Fungible: transferAmount, - }, - }, - ], - }, - { clearOrigin: null }, - { - depositAsset: { - assets: { - wild: { - allCounted: 2, - }, - }, - beneficiary: { - parents: 0, - interior: { x1: [beneficiaryLocation] }, - }, - }, - }, - { setTopic: topic }, - ], - }) -} - export function buildAssetHubPNATransferFromParachain( registry: Registry, ethChainId: number, From 45b57257a64d770eb9bfaa1772f62bcfe5928f36 Mon Sep 17 00:00:00 2001 From: ron Date: Sun, 16 Nov 2025 09:37:32 +0800 Subject: [PATCH 02/13] Cleanup --- .../api/src/toEthereumSnowbridgeV2.ts | 22 +- web/packages/api/src/toEthereum_v2.ts | 3 +- .../toEthereum/erc20FromParachain.ts | 6 +- .../api/src/transfers/toEthereum/pnaFromAH.ts | 43 +-- .../transfers/toEthereum/pnaFromParachain.ts | 3 +- .../src/xcmbuilders/toEthereum/erc20FromAH.ts | 14 +- .../toEthereum/erc20FromAHWithDotAsFee.ts | 144 ---------- .../toEthereum/erc20FromParachain.ts | 78 ++--- .../erc20FromParachainWithDotAsFee.ts | 223 --------------- .../erc20FromParachainWithNativeAsFee.ts | 58 +--- .../src/xcmbuilders/toEthereum/pnaFromAH.ts | 79 ++++-- .../toEthereum/pnaFromAHWithDotAsFee.ts | 152 ---------- .../toEthereum/pnaFromParachain.ts | 266 +++++------------- .../pnaFromParachainWithDotAsFee.ts | 212 -------------- .../pnaFromParachainWithNativeAsFee.ts | 9 +- .../operations/src/transfer_to_ethereum_v2.ts | 15 +- 16 files changed, 217 insertions(+), 1110 deletions(-) delete mode 100644 web/packages/api/src/xcmbuilders/toEthereum/erc20FromAHWithDotAsFee.ts delete mode 100644 web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachainWithDotAsFee.ts delete mode 100644 web/packages/api/src/xcmbuilders/toEthereum/pnaFromAHWithDotAsFee.ts delete mode 100644 web/packages/api/src/xcmbuilders/toEthereum/pnaFromParachainWithDotAsFee.ts diff --git a/web/packages/api/src/toEthereumSnowbridgeV2.ts b/web/packages/api/src/toEthereumSnowbridgeV2.ts index 8740f66d8..0769239af 100644 --- a/web/packages/api/src/toEthereumSnowbridgeV2.ts +++ b/web/packages/api/src/toEthereumSnowbridgeV2.ts @@ -50,8 +50,6 @@ export type DeliveryFeeV2 = { snowbridgeDeliveryFeeDOT: bigint bridgeHubDeliveryFeeDOT: bigint assetHubExecutionFeeDOT: bigint - returnToSenderExecutionFeeDOT: bigint - returnToSenderDeliveryFeeDOT: bigint totalFeeInDot: bigint localExecutionFeeDOT?: bigint localDeliveryFeeDOT?: bigint @@ -59,7 +57,6 @@ export type DeliveryFeeV2 = { feeLocation?: any totalFeeInNative?: bigint assetHubExecutionFeeNative?: bigint - returnToSenderExecutionFeeNative?: bigint localExecutionFeeInNative?: bigint localDeliveryFeeInNative?: bigint ethereumExecutionFeeInNative?: bigint @@ -103,9 +100,9 @@ export type ValidationResultV2 = { export function createTransferImplementation( sourceParaId: number, registry: AssetRegistry, - tokenAddress: string, + tokenAddresses: string[], ): TransferInterface { - const { sourceAssetMetadata } = resolveInputs(registry, tokenAddress, sourceParaId) + const { sourceAssetMetadata } = resolveInputs(registry, tokenAddresses[0], sourceParaId) let transferImpl if (sourceParaId == registry.assetHubParaId) { @@ -318,7 +315,6 @@ export const estimateFeesFromAssetHub = async ( let localExecutionFeeDOT = 0n let assetHubExecutionFeeDOT = 0n let returnToSenderExecutionFeeDOT = 0n - let returnToSenderDeliveryFeeDOT = 0n let bridgeHubDeliveryFeeDOT = 0n let snowbridgeDeliveryFeeDOT = 0n @@ -341,8 +337,6 @@ export const estimateFeesFromAssetHub = async ( localExecutionFeeDOT + snowbridgeDeliveryFeeDOT + assetHubExecutionFeeDOT + - returnToSenderExecutionFeeDOT + - returnToSenderDeliveryFeeDOT + bridgeHubDeliveryFeeDOT let ethereumExecutionFee = await estimateEthereumExecutionFee( @@ -381,13 +375,10 @@ export const estimateFeesFromAssetHub = async ( snowbridgeDeliveryFeeDOT, assetHubExecutionFeeDOT, bridgeHubDeliveryFeeDOT, - returnToSenderDeliveryFeeDOT, - returnToSenderExecutionFeeDOT, totalFeeInDot, ethereumExecutionFee, feeLocation, assetHubExecutionFeeNative, - returnToSenderExecutionFeeNative, ethereumExecutionFeeInNative, localExecutionFeeInNative, totalFeeInNative, @@ -420,8 +411,6 @@ export const estimateFeesFromParachains = async ( let localExecutionFeeDOT = 0n let localDeliveryFeeDOT = 0n let assetHubExecutionFeeDOT = 0n - let returnToSenderExecutionFeeDOT = 0n - let returnToSenderDeliveryFeeDOT = 0n let bridgeHubDeliveryFeeDOT = 0n let snowbridgeDeliveryFeeDOT = 0n @@ -474,8 +463,6 @@ export const estimateFeesFromParachains = async ( localDeliveryFeeDOT + snowbridgeDeliveryFeeDOT + assetHubExecutionFeeDOT + - returnToSenderExecutionFeeDOT + - returnToSenderDeliveryFeeDOT + bridgeHubDeliveryFeeDOT let ethereumExecutionFee = await estimateEthereumExecutionFee( @@ -545,13 +532,10 @@ export const estimateFeesFromParachains = async ( snowbridgeDeliveryFeeDOT, assetHubExecutionFeeDOT, bridgeHubDeliveryFeeDOT, - returnToSenderDeliveryFeeDOT, - returnToSenderExecutionFeeDOT, totalFeeInDot, ethereumExecutionFee, feeLocation, assetHubExecutionFeeNative, - returnToSenderExecutionFeeNative, ethereumExecutionFeeInNative, localExecutionFeeInNative, localDeliveryFeeInNative, @@ -762,8 +746,6 @@ export const mockDeliveryFee: DeliveryFeeV2 = { snowbridgeDeliveryFeeDOT: 1n, assetHubExecutionFeeDOT: 1n, bridgeHubDeliveryFeeDOT: 1n, - returnToSenderDeliveryFeeDOT: 1n, - returnToSenderExecutionFeeDOT: 1n, totalFeeInDot: 10n, ethereumExecutionFee: 1n, } diff --git a/web/packages/api/src/toEthereum_v2.ts b/web/packages/api/src/toEthereum_v2.ts index 6d8f64f12..e1108327c 100644 --- a/web/packages/api/src/toEthereum_v2.ts +++ b/web/packages/api/src/toEthereum_v2.ts @@ -41,6 +41,7 @@ import { paraImplementation } from "./parachains" import { padFeeByPercentage } from "./utils" import { Context } from "./index" import { ParachainBase } from "./parachains/parachainBase" +import { TransferV2 } from "./toEthereumSnowbridgeV2" export type Transfer = { input: { @@ -799,7 +800,7 @@ export type MessageReceipt = { export async function signAndSend( context: Context | { sourceParachain: ApiPromise }, - transfer: Transfer, + transfer: Transfer | TransferV2, account: AddressOrPair, options: Partial, ): Promise { diff --git a/web/packages/api/src/transfers/toEthereum/erc20FromParachain.ts b/web/packages/api/src/transfers/toEthereum/erc20FromParachain.ts index 88e42c788..1e21b100d 100644 --- a/web/packages/api/src/transfers/toEthereum/erc20FromParachain.ts +++ b/web/packages/api/src/transfers/toEthereum/erc20FromParachain.ts @@ -3,16 +3,14 @@ import { SubmittableExtrinsic } from "@polkadot/api/types" import { ISubmittableResult } from "@polkadot/types/types" import { isHex, u8aToHex } from "@polkadot/util" import { decodeAddress } from "@polkadot/util-crypto" -import { DOT_LOCATION, isRelaychainLocation, isParachainNative } from "../../xcmBuilder" +import { isRelaychainLocation, isParachainNative } from "../../xcmBuilder" import { buildExportXcm } from "../../xcmbuilders/toEthereum/erc20FromAH" import { buildResultXcmAssetHubERC20TransferFromParachain, buildTransferXcmFromParachain, } from "../../xcmbuilders/toEthereum/erc20FromParachain" -import { buildTransferXcmFromParachainWithDOTAsFee } from "../../xcmbuilders/toEthereum/erc20FromParachainWithDotAsFee" import { buildTransferXcmFromParachainWithNativeAssetFee } from "../../xcmbuilders/toEthereum/erc20FromParachainWithNativeAsFee" -import { Asset, AssetRegistry, ContractCall } from "@snowbridge/base-types" -import { paraImplementation } from "../../parachains" +import { AssetRegistry, ContractCall } from "@snowbridge/base-types" import { buildMessageId, resolveInputs } from "../../toEthereum_v2" import { Context } from "../.." import { TransferInterface } from "./transferInterface" diff --git a/web/packages/api/src/transfers/toEthereum/pnaFromAH.ts b/web/packages/api/src/transfers/toEthereum/pnaFromAH.ts index d7f96b7e1..4fe8db9dd 100644 --- a/web/packages/api/src/transfers/toEthereum/pnaFromAH.ts +++ b/web/packages/api/src/transfers/toEthereum/pnaFromAH.ts @@ -8,7 +8,6 @@ import { buildExportXcm, buildTransferXcmFromAssetHub, } from "../../xcmbuilders/toEthereum/pnaFromAH" -import { buildTransferXcmFromAssetHubWithDOTAsFee } from "../../xcmbuilders/toEthereum/pnaFromAHWithDotAsFee" import { Asset, AssetRegistry, ContractCall } from "@snowbridge/base-types" import { paraImplementation } from "../../parachains" import { buildMessageId, resolveInputs } from "../../toEthereum_v2" @@ -177,37 +176,17 @@ export class PNAFromAH implements TransferInterface { if (options?.contractCall) { callHex = await buildContractCallHex(context, options.contractCall) } - let xcm: any - // If there is no fee specified, we assume that Ether is available in user's wallet on source chain, - // thus no swap required on Asset Hub. - if (!fee.feeLocation) { - xcm = buildTransferXcmFromAssetHub( - parachain.registry, - ethChainId, - sourceAccount, - beneficiaryAccount, - messageId, - asset, - amount, - fee, - callHex, - ) - } // If the fee asset is in DOT, we need to swap it to Ether on Asset Hub. - else if (isRelaychainLocation(fee.feeLocation)) { - xcm = buildTransferXcmFromAssetHubWithDOTAsFee( - parachain.registry, - ethChainId, - sourceAccount, - beneficiaryAccount, - messageId, - asset, - amount, - fee, - callHex, - ) - } else { - throw new Error(`Fee token as ${fee.feeLocation} is not supported yet.`) - } + const xcm = buildTransferXcmFromAssetHub( + parachain.registry, + ethChainId, + sourceAccount, + beneficiaryAccount, + messageId, + asset, + amount, + fee, + callHex, + ) console.log("xcm on AH:", xcm.toHuman()) return parachain.tx.polkadotXcm.execute(xcm, MaxWeight) } diff --git a/web/packages/api/src/transfers/toEthereum/pnaFromParachain.ts b/web/packages/api/src/transfers/toEthereum/pnaFromParachain.ts index 839fed93a..53b1fa63f 100644 --- a/web/packages/api/src/transfers/toEthereum/pnaFromParachain.ts +++ b/web/packages/api/src/transfers/toEthereum/pnaFromParachain.ts @@ -9,7 +9,6 @@ import { buildResultXcmAssetHubPNATransferFromParachain, buildTransferXcmFromParachain, } from "../../xcmbuilders/toEthereum/pnaFromParachain" -import { buildTransferXcmFromParachainWithDOTAsFee } from "../../xcmbuilders/toEthereum/pnaFromParachainWithDotAsFee" import { buildTransferXcmFromParachainWithNativeAssetFee } from "../../xcmbuilders/toEthereum/pnaFromParachainWithNativeAsFee" import { Asset, AssetRegistry, ContractCall } from "@snowbridge/base-types" import { paraImplementation } from "../../parachains" @@ -231,7 +230,7 @@ export class PNAFromParachain implements TransferInterface { ) } // One swap from DOT to Ether on Asset Hub. else if (isRelaychainLocation(fee.feeLocation)) { - xcm = buildTransferXcmFromParachainWithDOTAsFee( + xcm = buildTransferXcmFromParachain( parachain.registry, envName, ethChainId, diff --git a/web/packages/api/src/xcmbuilders/toEthereum/erc20FromAH.ts b/web/packages/api/src/xcmbuilders/toEthereum/erc20FromAH.ts index 4e088df88..7409e5ee1 100644 --- a/web/packages/api/src/xcmbuilders/toEthereum/erc20FromAH.ts +++ b/web/packages/api/src/xcmbuilders/toEthereum/erc20FromAH.ts @@ -132,12 +132,14 @@ export function buildTransferXcmFromAssetHub( }, }) if (!containsEther(ethChainId, concreteAssets)) { - assets.push({ - id: bridgeLocation(ethChainId), - fun: { - Fungible: remoteEtherFeeAmount, - }, - }) + if (!fee.feeLocation) { + assets.push({ + id: bridgeLocation(ethChainId), + fun: { + Fungible: remoteEtherFeeAmount, + }, + }) + } for (const asset of concreteAssets) { const tokenLocation = erc20Location(ethChainId, asset.id.token) const tokenAmount = asset.amount diff --git a/web/packages/api/src/xcmbuilders/toEthereum/erc20FromAHWithDotAsFee.ts b/web/packages/api/src/xcmbuilders/toEthereum/erc20FromAHWithDotAsFee.ts deleted file mode 100644 index b069c15c5..000000000 --- a/web/packages/api/src/xcmbuilders/toEthereum/erc20FromAHWithDotAsFee.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { Registry } from "@polkadot/types/types" -import { - bridgeLocation, - DOT_LOCATION, - erc20Location, - accountToLocation, - buildEthereumInstructions, -} from "../../xcmBuilder" -import { Asset } from "@snowbridge/base-types" -import { DeliveryFee } from "../../toEthereum_v2" - -export function buildTransferXcmFromAssetHubWithDOTAsFee( - registry: Registry, - ethChainId: number, - sourceAccount: string, - beneficiary: string, - topic: string, - asset: Asset, - tokenAmount: bigint, - fee: DeliveryFee, - callHex?: string, -) { - let beneficiaryLocation = accountToLocation(beneficiary) - let sourceLocation = accountToLocation(sourceAccount) - let tokenLocation = erc20Location(ethChainId, asset.token) - - let localDOTFeeAmount = - fee.localExecutionFeeDOT! + fee.bridgeHubDeliveryFeeDOT + fee.snowbridgeDeliveryFeeDOT - let totalDOTFeeAmount = fee.totalFeeInDot! - let remoteEtherFeeAmount = fee.ethereumExecutionFee! - - let assets = [] - - assets.push({ - id: DOT_LOCATION, - fun: { - Fungible: totalDOTFeeAmount, - }, - }) - assets.push({ - id: tokenLocation, - fun: { - Fungible: tokenAmount, - }, - }) - - let remoteXcm = buildEthereumInstructions(beneficiaryLocation, topic, callHex) - - let instructions: any[] = [ - { - withdrawAsset: assets, - }, - { - payfees: { - asset: { - id: DOT_LOCATION, - fun: { - Fungible: localDOTFeeAmount, - }, - }, - }, - }, - { - setAppendix: [ - { - refundSurplus: null, - }, - { - depositAsset: { - assets: { - wild: { - allCounted: 2, - }, - }, - beneficiary: { - parents: 0, - interior: { x1: [sourceLocation] }, - }, - }, - }, - ], - }, - { - exchangeAsset: { - give: { - Wild: { - AllOf: { - id: DOT_LOCATION, - fun: "Fungible", - }, - }, - }, - want: [ - { - id: bridgeLocation(ethChainId), - fun: { - Fungible: remoteEtherFeeAmount, - }, - }, - ], - maximal: false, - }, - }, - { - initiateTransfer: { - destination: bridgeLocation(ethChainId), - remote_fees: { - reserveWithdraw: { - definite: [ - { - id: bridgeLocation(ethChainId), - fun: { - Fungible: remoteEtherFeeAmount, - }, - }, - ], - }, - }, - preserveOrigin: true, - assets: [ - { - reserveWithdraw: { - definite: [ - { - id: tokenLocation, - fun: { - Fungible: tokenAmount, - }, - }, - ], - }, - }, - ], - remoteXcm: remoteXcm, - }, - }, - { - setTopic: topic, - }, - ] - return registry.createType("XcmVersionedXcm", { - v5: instructions, - }) -} diff --git a/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachain.ts b/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachain.ts index d2ad3532c..0c168e4f8 100644 --- a/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachain.ts +++ b/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachain.ts @@ -1,13 +1,10 @@ import { Registry } from "@polkadot/types/types" -import { beneficiaryMultiAddress } from "../../utils" import { bridgeLocation, DOT_LOCATION, erc20Location, - erc20LocationReanchored, parachainLocation, accountToLocation, - isEthereumNative, buildAppendixInstructions, buildEthereumInstructions, containsEther, @@ -122,10 +119,10 @@ export function buildTransferXcmFromParachain( let beneficiaryLocation = accountToLocation(beneficiary) let sourceLocation = accountToLocation(sourceAccount) - let localDOTFeeAmount: bigint = - fee.localExecutionFeeDOT! + fee.localDeliveryFeeDOT! + fee.returnToSenderExecutionFeeDOT + let localDOTFeeAmount: bigint = fee.localExecutionFeeDOT! + fee.localDeliveryFeeDOT! let totalDOTFeeAmount: bigint = fee.totalFeeInDot! let remoteEtherFeeAmount: bigint = fee.ethereumExecutionFee! + let remoteEtherFeeInDOTAmount: bigint = fee.ethereumExecutionFeeInNative! let assets = [] assets.push({ @@ -135,12 +132,14 @@ export function buildTransferXcmFromParachain( }, }) if (!containsEther(ethChainId, concreteAssets)) { - assets.push({ - id: bridgeLocation(ethChainId), - fun: { - Fungible: remoteEtherFeeAmount, - }, - }) + if (!fee.feeLocation) { + assets.push({ + id: bridgeLocation(ethChainId), + fun: { + Fungible: remoteEtherFeeAmount, + }, + }) + } for (const asset of concreteAssets) { const tokenLocation = erc20Location(ethChainId, asset.id.token) const tokenAmount = asset.amount @@ -171,24 +170,6 @@ export function buildTransferXcmFromParachain( } } - let assetInstructions = [] - for (const asset of concreteAssets) { - const tokenLocation = erc20Location(ethChainId, asset.id.token) - const tokenAmount = asset.amount - assetInstructions.push({ - reserveWithdraw: { - definite: [ - { - id: tokenLocation, - fun: { - Fungible: tokenAmount, - }, - }, - ], - }, - }) - } - let appendixInstructions = buildAppendixInstructions( envName, sourceParachainId, @@ -222,6 +203,24 @@ export function buildTransferXcmFromParachain( } : undefined + let assetInstructions = [] + for (const asset of concreteAssets) { + const tokenLocation = erc20Location(ethChainId, asset.id.token) + const tokenAmount = asset.amount + assetInstructions.push({ + reserveWithdraw: { + definite: [ + { + id: tokenLocation, + fun: { + Fungible: tokenAmount, + }, + }, + ], + }, + }) + } + let remoteInstructionsOnAH: any[] = [ { setAppendix: appendixInstructions, @@ -298,7 +297,7 @@ export function buildTransferXcmFromParachain( Fungible: totalDOTFeeAmount - localDOTFeeAmount - - remoteEtherFeeAmount, + remoteEtherFeeInDOTAmount, }, }, ], @@ -309,12 +308,19 @@ export function buildTransferXcmFromParachain( { reserveWithdraw: { definite: [ - { - id: bridgeLocation(ethChainId), - fun: { - Fungible: remoteEtherFeeAmount, - }, - }, + remoteEtherFeeInDOTAmount > 0 + ? { + id: DOT_LOCATION, + fun: { + Fungible: remoteEtherFeeInDOTAmount, + }, + } + : { + id: bridgeLocation(ethChainId), + fun: { + Fungible: remoteEtherFeeAmount, + }, + }, ], }, }, diff --git a/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachainWithDotAsFee.ts b/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachainWithDotAsFee.ts deleted file mode 100644 index d15571b63..000000000 --- a/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachainWithDotAsFee.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { Registry } from "@polkadot/types/types" -import { - bridgeLocation, - DOT_LOCATION, - erc20Location, - parachainLocation, - accountToLocation, - isEthereumNative, - buildAppendixInstructions, - buildEthereumInstructions, -} from "../../xcmBuilder" -import { Asset } from "@snowbridge/base-types" -import { DeliveryFee } from "../../toEthereum_v2" - -export function buildTransferXcmFromParachainWithDOTAsFee( - registry: Registry, - envName: string, - ethChainId: number, - assetHubParaId: number, - sourceParachainId: number, - sourceAccount: string, - beneficiary: string, - topic: string, - asset: Asset, - tokenAmount: bigint, - fee: DeliveryFee, - claimerLocation?: any, - callHex?: string, -) { - let beneficiaryLocation = accountToLocation(beneficiary) - let sourceLocation = accountToLocation(sourceAccount) - let tokenLocation = erc20Location(ethChainId, asset.token) - - let localDOTFeeAmount: bigint = - fee.localExecutionFeeDOT! + fee.localDeliveryFeeDOT! + fee.returnToSenderExecutionFeeDOT - let totalDOTFeeAmount: bigint = fee.totalFeeInDot - let remoteEtherFeeAmount: bigint = fee.ethereumExecutionFee! - let remoteEtherFeeInDOTAmount: bigint = fee.ethereumExecutionFeeInNative! - - let assets = [] - - assets.push({ - id: DOT_LOCATION, - fun: { - Fungible: totalDOTFeeAmount, - }, - }) - if (isEthereumNative(tokenLocation, ethChainId)) { - assets.push({ - id: bridgeLocation(ethChainId), - fun: { - Fungible: remoteEtherFeeAmount + tokenAmount, - }, - }) - } else { - assets.push({ - id: tokenLocation, - fun: { - Fungible: tokenAmount, - }, - }) - } - - let appendixInstructions = buildAppendixInstructions( - envName, - sourceParachainId, - sourceAccount, - claimerLocation, - ) - - let remoteXcm = buildEthereumInstructions(beneficiaryLocation, topic, callHex) - - let remoteInstructionsOnAH: any[] = [ - { - setAppendix: appendixInstructions, - }, - { - exchangeAsset: { - give: { - Wild: { - AllOf: { - id: DOT_LOCATION, - fun: "Fungible", - }, - }, - }, - want: [ - { - id: bridgeLocation(ethChainId), - fun: { - Fungible: remoteEtherFeeAmount, - }, - }, - ], - maximal: false, - }, - }, - { - initiateTransfer: { - destination: bridgeLocation(ethChainId), - remote_fees: { - reserveWithdraw: { - definite: [ - { - id: bridgeLocation(ethChainId), - fun: { - Fungible: remoteEtherFeeAmount, - }, - }, - ], - }, - }, - preserveOrigin: true, - assets: [ - { - reserveWithdraw: { - definite: [ - { - id: tokenLocation, - fun: { - Fungible: tokenAmount, - }, - }, - ], - }, - }, - ], - remoteXcm: remoteXcm, - }, - }, - { - setTopic: topic, - }, - ] - return registry.createType("XcmVersionedXcm", { - v5: [ - { - withdrawAsset: assets, - }, - { - payfees: { - asset: { - id: DOT_LOCATION, - fun: { - Fungible: localDOTFeeAmount, - }, - }, - }, - }, - { - setAppendix: [ - { - refundSurplus: null, - }, - { - depositAsset: { - assets: { - wild: { - allCounted: 3, - }, - }, - beneficiary: { - parents: 0, - interior: { x1: [sourceLocation] }, - }, - }, - }, - ], - }, - { - initiateTransfer: { - destination: parachainLocation(assetHubParaId), - remote_fees: { - reserveWithdraw: { - definite: [ - { - id: DOT_LOCATION, - fun: { - Fungible: - totalDOTFeeAmount - - localDOTFeeAmount - - remoteEtherFeeInDOTAmount, - }, - }, - ], - }, - }, - preserveOrigin: true, - assets: [ - { - reserveWithdraw: { - definite: [ - { - id: DOT_LOCATION, - fun: { - Fungible: remoteEtherFeeInDOTAmount, - }, - }, - ], - }, - }, - { - reserveWithdraw: { - definite: [ - { - id: tokenLocation, - fun: { - Fungible: tokenAmount, - }, - }, - ], - }, - }, - ], - remoteXcm: remoteInstructionsOnAH, - }, - }, - { - setTopic: topic, - }, - ], - }) -} diff --git a/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachainWithNativeAsFee.ts b/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachainWithNativeAsFee.ts index 44a9643d2..22aa1f022 100644 --- a/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachainWithNativeAsFee.ts +++ b/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachainWithNativeAsFee.ts @@ -8,10 +8,8 @@ import { HERE_LOCATION, buildAppendixInstructions, buildEthereumInstructions, - containsEther, - splitEtherAsset, } from "../../xcmBuilder" -import { DeliveryFee } from "../../toEthereum_v2" +import { DeliveryFeeV2 } from "../../toEthereumSnowbridgeV2" import { ConcreteAsset } from "../../assets_v2" export function buildTransferXcmFromParachainWithNativeAssetFee( @@ -24,69 +22,35 @@ export function buildTransferXcmFromParachainWithNativeAssetFee( beneficiary: string, topic: string, concreteAssets: ConcreteAsset[], - fee: DeliveryFee, + fee: DeliveryFeeV2, claimerLocation?: any, callHex?: string, ) { let beneficiaryLocation = accountToLocation(beneficiary) let sourceLocation = accountToLocation(sourceAccount) - let localNativeFeeAmount = - fee.localExecutionFeeInNative! + - fee.localDeliveryFeeInNative! + - fee.returnToSenderExecutionFeeNative! + let localNativeFeeAmount = fee.localExecutionFeeInNative! + fee.localDeliveryFeeInNative! let totalNativeFeeAmount = fee.totalFeeInNative! let remoteEtherFeeAmount = fee.ethereumExecutionFee! let remoteEtherFeeNativeAmount = fee.ethereumExecutionFeeInNative! - let assets = [] + let assets = [], + assetInstructions = [] assets.push({ id: HERE_LOCATION, fun: { Fungible: totalNativeFeeAmount, }, }) - if (!containsEther(ethChainId, concreteAssets)) { - assets.push({ - id: bridgeLocation(ethChainId), - fun: { - Fungible: remoteEtherFeeAmount, - }, - }) - for (const asset of concreteAssets) { - const tokenLocation = erc20Location(ethChainId, asset.id.token) - const tokenAmount = asset.amount - assets.push({ - id: tokenLocation, - fun: { - Fungible: tokenAmount, - }, - }) - } - } else { - const { etherAsset, otherAssets } = splitEtherAsset(ethChainId, concreteAssets) + for (const asset of concreteAssets) { + const tokenLocation = erc20Location(ethChainId, asset.id.token) + const tokenAmount = asset.amount assets.push({ - id: bridgeLocation(ethChainId), + id: tokenLocation, fun: { - Fungible: etherAsset!.amount + remoteEtherFeeAmount, + Fungible: tokenAmount, }, }) - for (const asset of otherAssets) { - const tokenLocation = erc20Location(ethChainId, asset.id.token) - const tokenAmount = asset.amount - assets.push({ - id: tokenLocation, - fun: { - Fungible: tokenAmount, - }, - }) - } - } - - let assetInstructions = [] - for (const asset of concreteAssets) { - const tokenLocation = erc20Location(ethChainId, asset.id.token) - const tokenAmount = asset.amount assetInstructions.push({ reserveWithdraw: { definite: [ @@ -210,7 +174,7 @@ export function buildTransferXcmFromParachainWithNativeAssetFee( depositAsset: { assets: { wild: { - allCounted: 3, + allCounted: 8, }, }, beneficiary: { diff --git a/web/packages/api/src/xcmbuilders/toEthereum/pnaFromAH.ts b/web/packages/api/src/xcmbuilders/toEthereum/pnaFromAH.ts index 424caeaaf..1a1ffa251 100644 --- a/web/packages/api/src/xcmbuilders/toEthereum/pnaFromAH.ts +++ b/web/packages/api/src/xcmbuilders/toEthereum/pnaFromAH.ts @@ -8,7 +8,7 @@ import { buildEthereumInstructions, } from "../../xcmBuilder" import { Asset } from "@snowbridge/base-types" -import { DeliveryFee } from "../../toEthereum_v2" +import { DeliveryFeeV2 } from "../../toEthereumSnowbridgeV2" export function buildExportXcm( registry: Registry, @@ -109,14 +109,16 @@ export function buildTransferXcmFromAssetHub( topic: string, asset: Asset, tokenAmount: bigint, - fee: DeliveryFee, + fee: DeliveryFeeV2, callHex?: string, ) { let beneficiaryLocation = accountToLocation(beneficiary) let sourceLocation = accountToLocation(sourceAccount) let tokenLocation = asset.location - let totalDOTFeeAmount = fee.totalFeeInDot + let localDOTFeeAmount = + fee.localExecutionFeeDOT! + fee.bridgeHubDeliveryFeeDOT + fee.snowbridgeDeliveryFeeDOT + let totalDOTFeeAmount = fee.totalFeeInDot! let remoteEtherFeeAmount = fee.ethereumExecutionFee! let assets = [] @@ -127,12 +129,14 @@ export function buildTransferXcmFromAssetHub( Fungible: totalDOTFeeAmount + tokenAmount, }, }) - assets.push({ - id: bridgeLocation(ethChainId), - fun: { - Fungible: remoteEtherFeeAmount, - }, - }) + if (!fee.feeLocation) { + assets.push({ + id: bridgeLocation(ethChainId), + fun: { + Fungible: remoteEtherFeeAmount, + }, + }) + } } else { // native asset first if (tokenLocation.parents == 0) { @@ -148,12 +152,14 @@ export function buildTransferXcmFromAssetHub( Fungible: totalDOTFeeAmount, }, }) - assets.push({ - id: bridgeLocation(ethChainId), - fun: { - Fungible: remoteEtherFeeAmount, - }, - }) + if (!fee.feeLocation) { + assets.push({ + id: bridgeLocation(ethChainId), + fun: { + Fungible: remoteEtherFeeAmount, + }, + }) + } } // Parachain assets or KSM assets else if (tokenLocation.parents == 1 || tokenLocation.parents == 2) { assets.push({ @@ -168,16 +174,42 @@ export function buildTransferXcmFromAssetHub( Fungible: tokenAmount, }, }) - assets.push({ - id: bridgeLocation(ethChainId), - fun: { - Fungible: remoteEtherFeeAmount, - }, - }) + if (!fee.feeLocation) { + assets.push({ + id: bridgeLocation(ethChainId), + fun: { + Fungible: remoteEtherFeeAmount, + }, + }) + } } } let remoteXcm = buildEthereumInstructions(beneficiaryLocation, topic, callHex) + let exchangeInstruction = fee.feeLocation + ? { + exchangeAsset: { + give: { + Wild: { + AllOf: { + id: fee.feeLocation, + fun: "Fungible", + }, + }, + }, + want: [ + { + id: bridgeLocation(ethChainId), + fun: { + Fungible: remoteEtherFeeAmount, + }, + }, + ], + maximal: false, + }, + } + : undefined + let instructions: any[] = [ { withdrawAsset: assets, @@ -187,7 +219,7 @@ export function buildTransferXcmFromAssetHub( asset: { id: DOT_LOCATION, fun: { - Fungible: totalDOTFeeAmount, + Fungible: localDOTFeeAmount, }, }, }, @@ -201,7 +233,7 @@ export function buildTransferXcmFromAssetHub( depositAsset: { assets: { wild: { - allCounted: 2, + allCounted: 8, }, }, beneficiary: { @@ -212,6 +244,7 @@ export function buildTransferXcmFromAssetHub( }, ], }, + ...(exchangeInstruction ? [exchangeInstruction] : []), { initiateTransfer: { destination: bridgeLocation(ethChainId), diff --git a/web/packages/api/src/xcmbuilders/toEthereum/pnaFromAHWithDotAsFee.ts b/web/packages/api/src/xcmbuilders/toEthereum/pnaFromAHWithDotAsFee.ts deleted file mode 100644 index add8895dd..000000000 --- a/web/packages/api/src/xcmbuilders/toEthereum/pnaFromAHWithDotAsFee.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { Registry } from "@polkadot/types/types" -import { - bridgeLocation, - DOT_LOCATION, - accountToLocation, - isRelaychainLocation, - buildEthereumInstructions, -} from "../../xcmBuilder" -import { Asset } from "@snowbridge/base-types" -import { DeliveryFee } from "../../toEthereum_v2" - -export function buildTransferXcmFromAssetHubWithDOTAsFee( - registry: Registry, - ethChainId: number, - sourceAccount: string, - beneficiary: string, - topic: string, - asset: Asset, - tokenAmount: bigint, - fee: DeliveryFee, - callHex?: string, -) { - let beneficiaryLocation = accountToLocation(beneficiary) - let sourceLocation = accountToLocation(sourceAccount) - let tokenLocation = asset.location - - let localDOTFeeAmount = - fee.localExecutionFeeDOT! + fee.bridgeHubDeliveryFeeDOT + fee.snowbridgeDeliveryFeeDOT - let totalDOTFeeAmount = fee.totalFeeInDot! - let remoteEtherFeeAmount = fee.ethereumExecutionFee! - - let assets = [] - if (isRelaychainLocation(tokenLocation)) { - assets.push({ - id: DOT_LOCATION, - fun: { - Fungible: totalDOTFeeAmount + tokenAmount, - }, - }) - } else { - assets.push({ - id: tokenLocation, - fun: { - Fungible: tokenAmount, - }, - }) - assets.push({ - id: DOT_LOCATION, - fun: { - Fungible: totalDOTFeeAmount, - }, - }) - } - - let remoteXcm = buildEthereumInstructions(beneficiaryLocation, topic, callHex) - - let instructions: any[] = [ - { - withdrawAsset: assets, - }, - { - payfees: { - asset: { - id: DOT_LOCATION, - fun: { - Fungible: localDOTFeeAmount, - }, - }, - }, - }, - { - setAppendix: [ - { - refundSurplus: null, - }, - { - depositAsset: { - assets: { - wild: { - allCounted: 2, - }, - }, - beneficiary: { - parents: 0, - interior: { x1: [sourceLocation] }, - }, - }, - }, - ], - }, - { - exchangeAsset: { - give: { - Wild: { - AllOf: { - id: DOT_LOCATION, - fun: "Fungible", - }, - }, - }, - want: [ - { - id: bridgeLocation(ethChainId), - fun: { - Fungible: remoteEtherFeeAmount, - }, - }, - ], - maximal: false, - }, - }, - { - initiateTransfer: { - destination: bridgeLocation(ethChainId), - remote_fees: { - reserveWithdraw: { - definite: [ - { - id: bridgeLocation(ethChainId), - fun: { - Fungible: remoteEtherFeeAmount, - }, - }, - ], - }, - }, - preserveOrigin: true, - assets: [ - { - reserveDeposit: { - definite: [ - { - id: tokenLocation, - fun: { - Fungible: tokenAmount, - }, - }, - ], - }, - }, - ], - remoteXcm: remoteXcm, - }, - }, - { - setTopic: topic, - }, - ] - return registry.createType("XcmVersionedXcm", { - v5: instructions, - }) -} diff --git a/web/packages/api/src/xcmbuilders/toEthereum/pnaFromParachain.ts b/web/packages/api/src/xcmbuilders/toEthereum/pnaFromParachain.ts index ec67e2653..612906885 100644 --- a/web/packages/api/src/xcmbuilders/toEthereum/pnaFromParachain.ts +++ b/web/packages/api/src/xcmbuilders/toEthereum/pnaFromParachain.ts @@ -9,7 +9,7 @@ import { buildEthereumInstructions, } from "../../xcmBuilder" import { Asset } from "@snowbridge/base-types" -import { DeliveryFee } from "../../toEthereum_v2" +import { DeliveryFeeV2 } from "../../toEthereumSnowbridgeV2" export function buildResultXcmAssetHubPNATransferFromParachain( registry: Registry, @@ -57,209 +57,48 @@ export function buildResultXcmAssetHubPNATransferFromParachain( ], }, { clearOrigin: null }, - ...buildAssetHubXcmForPNAFromParachain( - ethChainId, - beneficiary, - assetLocationOnAH, - assetLocationOnEthereum, - topic, - ), - ], - }) -} - -function buildAssetHubXcmForPNAFromParachain( - ethChainId: number, - beneficiary: string, - assetLocationOnAH: any, - assetLocationOnEthereum: any, - topic: string, -) { - return [ - // Initiate the bridged transfer - { - depositReserveAsset: { - assets: { - Wild: { - AllOf: { id: assetLocationOnAH, fun: "Fungible" }, - }, - }, - dest: bridgeLocation(ethChainId), - xcm: [ - { - buyExecution: { - fees: { - id: assetLocationOnEthereum, // CAUTION: Must use reanchored locations. - fun: { - Fungible: "1", // Offering 1 unit as fee, but it is returned to the beneficiary address. - }, - }, - weight_limit: "Unlimited", - }, - }, - { - depositAsset: { - assets: { - Wild: { - AllCounted: 1, - }, - }, - beneficiary: { - parents: 0, - interior: { x1: [{ AccountKey20: { key: beneficiary } }] }, - }, - }, - }, - { - setTopic: topic, - }, - ], - }, - }, - { - setTopic: topic, - }, - ] -} - -export function buildAssetHubPNATransferFromParachain( - registry: Registry, - ethChainId: number, - beneficiary: string, - assetLocationOnAH: any, - assetLocationOnEthereum: any, - topic: string, -) { - return registry.createType("XcmVersionedXcm", { - v5: buildAssetHubXcmForPNAFromParachain( - ethChainId, - beneficiary, - assetLocationOnAH, - assetLocationOnEthereum, - topic, - ), - }) -} - -export function buildParachainPNAReceivedXcmOnAssetHub( - registry: Registry, - ethChainId: number, - assetLocationOnAH: any, - destinationParaId: number, - transferAmount: bigint, - totalFeeInDot: bigint, - destinationFeeInDot: bigint, - beneficiary: string, - topic: string, -) { - let beneficiaryLocation = accountToLocation(beneficiary) - return registry.createType("XcmVersionedXcm", { - v5: [ { - receiveTeleportedAsset: [ - { - id: DOT_LOCATION, - fun: { - Fungible: totalFeeInDot, - }, - }, - ], - }, - { - buyExecution: { - fees: { - id: DOT_LOCATION, - fun: { - Fungible: totalFeeInDot, - }, - }, - weightLimit: "Unlimited", - }, - }, - { - descendOrigin: { x1: [{ PalletInstance: 80 }] }, - }, - { - universalOrigin: ethereumNetwork(ethChainId), - }, - { - withdrawAsset: [ - { - id: assetLocationOnAH, - fun: { - Fungible: transferAmount, - }, - }, - ], - }, - { clearOrigin: null }, - { - setAppendix: [ - { - depositAsset: { - assets: { - wild: { - allCounted: 2, - }, - }, - beneficiary: bridgeLocation(ethChainId), - }, - }, - ], - }, - { - reserveAssetDeposited: [ - { - id: DOT_LOCATION, - fun: { - Fungible: destinationFeeInDot, - }, - }, - ], - }, - { - initiateTeleport: { + depositReserveAsset: { assets: { - definite: [ - { - id: assetLocationOnAH, - fun: { - Fungible: transferAmount, - }, - }, - ], + Wild: { + AllOf: { id: assetLocationOnAH, fun: "Fungible" }, + }, }, - dest: { parents: 1, interior: { x1: [{ parachain: destinationParaId }] } }, + dest: bridgeLocation(ethChainId), xcm: [ { buyExecution: { fees: { - id: DOT_LOCATION, + id: assetLocationOnEthereum, // CAUTION: Must use reanchored locations. fun: { - Fungible: destinationFeeInDot, + Fungible: "1", // Offering 1 unit as fee, but it is returned to the beneficiary address. }, }, - weightLimit: "Unlimited", + weight_limit: "Unlimited", }, }, { depositAsset: { assets: { - wild: { - allCounted: 2, + Wild: { + AllCounted: 1, }, }, beneficiary: { parents: 0, - interior: { x1: [beneficiaryLocation] }, + interior: { x1: [{ AccountKey20: { key: beneficiary } }] }, }, }, }, - { setTopic: topic }, + { + setTopic: topic, + }, ], }, }, - { setTopic: topic }, + { + setTopic: topic, + }, ], }) } @@ -275,7 +114,7 @@ export function buildTransferXcmFromParachain( topic: string, asset: Asset, tokenAmount: bigint, - fee: DeliveryFee, + fee: DeliveryFeeV2, claimerLocation?: any, callHex?: string, ) { @@ -283,10 +122,10 @@ export function buildTransferXcmFromParachain( let sourceLocation = accountToLocation(sourceAccount) let tokenLocation = asset.location - let localDOTFeeAmount: bigint = - fee.localExecutionFeeDOT! + fee.localDeliveryFeeDOT! + fee.returnToSenderExecutionFeeDOT + let localDOTFeeAmount: bigint = fee.localExecutionFeeDOT! + fee.localDeliveryFeeDOT! let totalDOTFeeAmount: bigint = fee.totalFeeInDot! let remoteEtherFeeAmount: bigint = fee.ethereumExecutionFee! + let remoteEtherFeeInDOTAmount: bigint = fee.ethereumExecutionFeeInNative! let assets = [] assets.push({ @@ -301,12 +140,14 @@ export function buildTransferXcmFromParachain( Fungible: totalDOTFeeAmount, }, }) - assets.push({ - id: bridgeLocation(ethChainId), - fun: { - Fungible: remoteEtherFeeAmount, - }, - }) + if (!fee.feeLocation) { + assets.push({ + id: bridgeLocation(ethChainId), + fun: { + Fungible: remoteEtherFeeAmount, + }, + }) + } let appendixInstructions = buildAppendixInstructions( envName, @@ -317,10 +158,35 @@ export function buildTransferXcmFromParachain( let remoteXcm = buildEthereumInstructions(beneficiaryLocation, topic, callHex) + let exchangeInstruction = fee.feeLocation + ? { + exchangeAsset: { + give: { + Wild: { + AllOf: { + id: fee.feeLocation, + fun: "Fungible", + }, + }, + }, + want: [ + { + id: bridgeLocation(ethChainId), + fun: { + Fungible: remoteEtherFeeAmount, + }, + }, + ], + maximal: false, + }, + } + : undefined + let remoteInstructionsOnAH: any[] = [ { setAppendix: appendixInstructions, }, + ...(exchangeInstruction ? [exchangeInstruction] : []), { initiateTransfer: { destination: bridgeLocation(ethChainId), @@ -402,7 +268,10 @@ export function buildTransferXcmFromParachain( { id: DOT_LOCATION, fun: { - Fungible: totalDOTFeeAmount - localDOTFeeAmount, + Fungible: + totalDOTFeeAmount - + localDOTFeeAmount - + remoteEtherFeeInDOTAmount, }, }, ], @@ -413,12 +282,19 @@ export function buildTransferXcmFromParachain( { reserveWithdraw: { definite: [ - { - id: bridgeLocation(ethChainId), - fun: { - Fungible: remoteEtherFeeAmount, - }, - }, + remoteEtherFeeInDOTAmount > 0 + ? { + id: DOT_LOCATION, + fun: { + Fungible: remoteEtherFeeInDOTAmount, + }, + } + : { + id: bridgeLocation(ethChainId), + fun: { + Fungible: remoteEtherFeeAmount, + }, + }, ], }, }, diff --git a/web/packages/api/src/xcmbuilders/toEthereum/pnaFromParachainWithDotAsFee.ts b/web/packages/api/src/xcmbuilders/toEthereum/pnaFromParachainWithDotAsFee.ts deleted file mode 100644 index d97ab90e8..000000000 --- a/web/packages/api/src/xcmbuilders/toEthereum/pnaFromParachainWithDotAsFee.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { Registry } from "@polkadot/types/types" -import { - bridgeLocation, - DOT_LOCATION, - parachainLocation, - accountToLocation, - buildAppendixInstructions, - buildEthereumInstructions, -} from "../../xcmBuilder" -import { Asset } from "@snowbridge/base-types" -import { DeliveryFee } from "../../toEthereum_v2" - -export function buildTransferXcmFromParachainWithDOTAsFee( - registry: Registry, - envName: string, - ethChainId: number, - assetHubParaId: number, - sourceParachainId: number, - sourceAccount: string, - beneficiary: string, - topic: string, - asset: Asset, - tokenAmount: bigint, - fee: DeliveryFee, - claimerLocation?: any, - callHex?: string, -) { - let beneficiaryLocation = accountToLocation(beneficiary) - let sourceLocation = accountToLocation(sourceAccount) - let tokenLocation = asset.location - - let localDOTFeeAmount: bigint = - fee.localExecutionFeeDOT! + fee.localDeliveryFeeDOT! + fee.returnToSenderExecutionFeeDOT - let totalDOTFeeAmount: bigint = fee.totalFeeInDot - let remoteEtherFeeAmount: bigint = fee.ethereumExecutionFee! - let remoteEtherFeeInDOTAmount: bigint = fee.ethereumExecutionFeeInNative! - - let assets = [ - { - id: tokenLocation, - fun: { - Fungible: tokenAmount, - }, - }, - { - id: DOT_LOCATION, - fun: { - Fungible: totalDOTFeeAmount, - }, - }, - ] - - let appendixInstructions = buildAppendixInstructions( - envName, - sourceParachainId, - sourceAccount, - claimerLocation, - ) - - let remoteXcm = buildEthereumInstructions(beneficiaryLocation, topic, callHex) - - let remoteInstructionsOnAH: any[] = [ - { - setAppendix: appendixInstructions, - }, - { - exchangeAsset: { - give: { - Wild: { - AllOf: { - id: DOT_LOCATION, - fun: "Fungible", - }, - }, - }, - want: [ - { - id: bridgeLocation(ethChainId), - fun: { - Fungible: remoteEtherFeeAmount, - }, - }, - ], - maximal: false, - }, - }, - { - initiateTransfer: { - destination: bridgeLocation(ethChainId), - remote_fees: { - reserveWithdraw: { - definite: [ - { - id: bridgeLocation(ethChainId), - fun: { - Fungible: remoteEtherFeeAmount, - }, - }, - ], - }, - }, - preserveOrigin: true, - assets: [ - { - reserveDeposit: { - definite: [ - { - id: asset.locationOnAH, - fun: { - Fungible: tokenAmount, - }, - }, - ], - }, - }, - ], - remoteXcm: remoteXcm, - }, - }, - { - setTopic: topic, - }, - ] - return registry.createType("XcmVersionedXcm", { - v5: [ - { - withdrawAsset: assets, - }, - { - payfees: { - asset: { - id: DOT_LOCATION, - fun: { - Fungible: localDOTFeeAmount, - }, - }, - }, - }, - { - setAppendix: [ - { - refundSurplus: null, - }, - { - depositAsset: { - assets: { - wild: { - allCounted: 3, - }, - }, - beneficiary: { - parents: 0, - interior: { x1: [sourceLocation] }, - }, - }, - }, - ], - }, - { - initiateTransfer: { - destination: parachainLocation(assetHubParaId), - remote_fees: { - reserveWithdraw: { - definite: [ - { - id: DOT_LOCATION, - fun: { - Fungible: - totalDOTFeeAmount - - localDOTFeeAmount - - remoteEtherFeeInDOTAmount, - }, - }, - ], - }, - }, - preserveOrigin: true, - assets: [ - { - reserveWithdraw: { - definite: [ - { - id: DOT_LOCATION, - fun: { - Fungible: remoteEtherFeeInDOTAmount, - }, - }, - ], - }, - }, - { - teleport: { - definite: [ - { - id: tokenLocation, - fun: { - Fungible: tokenAmount, - }, - }, - ], - }, - }, - ], - remoteXcm: remoteInstructionsOnAH, - }, - }, - { - setTopic: topic, - }, - ], - }) -} diff --git a/web/packages/api/src/xcmbuilders/toEthereum/pnaFromParachainWithNativeAsFee.ts b/web/packages/api/src/xcmbuilders/toEthereum/pnaFromParachainWithNativeAsFee.ts index 678e82fb7..3625c5744 100644 --- a/web/packages/api/src/xcmbuilders/toEthereum/pnaFromParachainWithNativeAsFee.ts +++ b/web/packages/api/src/xcmbuilders/toEthereum/pnaFromParachainWithNativeAsFee.ts @@ -9,7 +9,7 @@ import { buildEthereumInstructions, } from "../../xcmBuilder" import { Asset } from "@snowbridge/base-types" -import { DeliveryFee } from "../../toEthereum_v2" +import { DeliveryFeeV2 } from "../../toEthereumSnowbridgeV2" export function buildTransferXcmFromParachainWithNativeAssetFee( registry: Registry, @@ -22,7 +22,7 @@ export function buildTransferXcmFromParachainWithNativeAssetFee( topic: string, asset: Asset, tokenAmount: bigint, - fee: DeliveryFee, + fee: DeliveryFeeV2, claimerLocation?: any, callHex?: string, ) { @@ -30,10 +30,7 @@ export function buildTransferXcmFromParachainWithNativeAssetFee( let sourceLocation = accountToLocation(sourceAccount) let tokenLocation = asset.location - let localNativeFeeAmount = - fee.localExecutionFeeInNative! + - fee.localDeliveryFeeInNative! + - fee.returnToSenderExecutionFeeNative! + let localNativeFeeAmount = fee.localExecutionFeeInNative! + fee.localDeliveryFeeInNative! let totalNativeFeeAmount = fee.totalFeeInNative! let remoteEtherFeeAmount = fee.ethereumExecutionFee! let remoteEtherFeeNativeAmount = fee.ethereumExecutionFeeInNative! diff --git a/web/packages/operations/src/transfer_to_ethereum_v2.ts b/web/packages/operations/src/transfer_to_ethereum_v2.ts index 74fcdeff3..2c923c281 100644 --- a/web/packages/operations/src/transfer_to_ethereum_v2.ts +++ b/web/packages/operations/src/transfer_to_ethereum_v2.ts @@ -54,13 +54,14 @@ export const transferToEthereum = async ( const transferImpl = await toEthereumSnowbridgeV2.createTransferImplementation( sourceParaId, registry, - TOKEN_CONTRACT, + [TOKEN_CONTRACT], ) // Step 1. Get the delivery fee for the transaction - let fee: toEthereumV2.DeliveryFee = await transferImpl.getDeliveryFee( - { sourceParaId, context }, + let fee: toEthereumSnowbridgeV2.DeliveryFeeV2 = await transferImpl.getDeliveryFee( + context, + sourceParaId, registry, - TOKEN_CONTRACT, + [TOKEN_CONTRACT], { feeTokenLocation: options?.feeTokenLocation, slippagePadPercentage: 20n, @@ -70,12 +71,12 @@ export const transferToEthereum = async ( // Step 2. Create a transfer tx const transfer = await transferImpl.createTransfer( - { sourceParaId, context }, + context, + sourceParaId, registry, POLKADOT_ACCOUNT_PUBLIC, ETHEREUM_ACCOUNT_PUBLIC, - TOKEN_CONTRACT, - amount, + [{ address: TOKEN_CONTRACT, amount }], fee, options, ) From b92aa8bd390c1dd9a50616f9bf3fafd0027d6081 Mon Sep 17 00:00:00 2001 From: ron Date: Sun, 16 Nov 2025 21:03:29 +0800 Subject: [PATCH 03/13] Add reserves for Ethereum assets --- web/packages/test/scripts/configure-others.sh | 6 ++++++ web/packages/test/scripts/configure-substrate.sh | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/web/packages/test/scripts/configure-others.sh b/web/packages/test/scripts/configure-others.sh index fc91f2d6d..36c25cbd4 100755 --- a/web/packages/test/scripts/configure-others.sh +++ b/web/packages/test/scripts/configure-others.sh @@ -156,11 +156,17 @@ add_liquidity_on_penpal() { send_transact_through_user_origin_from_relaychain $PENPAL_PARAID "$sudo_pubkey" "$call" } +set_reserve_on_ah() { + local call="0x3521020109079edaa80204020109079edaa80200" + send_transact_through_bridge_from_relaychain $ASSET_HUB_PARAID "$call" +} + configure_on_ah() { register_weth_on_ah register_wnd_on_ethereum register_roc_on_ah add_liquidity_on_ah + set_reserve_on_ah } configure_on_penpal() { diff --git a/web/packages/test/scripts/configure-substrate.sh b/web/packages/test/scripts/configure-substrate.sh index 6edf9a652..f0e8efd35 100755 --- a/web/packages/test/scripts/configure-substrate.sh +++ b/web/packages/test/scripts/configure-substrate.sh @@ -54,7 +54,7 @@ configure_bh() { configure_ah() { # Create Ether - local call="0x28020c1f04020109079edaa802040000003501020109079edaa80200ce796ae65569a670d0c1cc1ac12515a3ce21b5fbf729d63d7b289baad070139d01043513020109079edaa8021445746865721445746865721200" + local call="0x28020c1f04020109079edaa802050000003501020109079edaa80200ce796ae65569a670d0c1cc1ac12515a3ce21b5fbf729d63d7b289baad070139d01043513020109079edaa8021445746865721445746865721200" send_governance_transact_from_relaychain $ASSET_HUB_PARAID "$call" # Mint Ether to Alice local call="0x3506020109079edaa80200d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d1300002cf61a24a229" From be2a3f92fbd383b9526b1902f022b42e30d4aa4b Mon Sep 17 00:00:00 2001 From: ron Date: Sun, 16 Nov 2025 21:04:00 +0800 Subject: [PATCH 04/13] Fix import --- web/packages/api/src/transfers/toEthereum/erc20FromParachain.ts | 2 +- web/packages/api/src/transfers/toEthereum/pnaFromAH.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/packages/api/src/transfers/toEthereum/erc20FromParachain.ts b/web/packages/api/src/transfers/toEthereum/erc20FromParachain.ts index 1e21b100d..6b0d4d010 100644 --- a/web/packages/api/src/transfers/toEthereum/erc20FromParachain.ts +++ b/web/packages/api/src/transfers/toEthereum/erc20FromParachain.ts @@ -24,7 +24,7 @@ import { TransferV2, ValidationResultV2, } from "../../toEthereumSnowbridgeV2" -import { AggregatedAsset, ConcreteAsset, ConcreteToken } from "src/assets_v2" +import { AggregatedAsset, ConcreteAsset, ConcreteToken } from "../../assets_v2" export class ERC20FromParachain implements TransferInterface { async getDeliveryFee( diff --git a/web/packages/api/src/transfers/toEthereum/pnaFromAH.ts b/web/packages/api/src/transfers/toEthereum/pnaFromAH.ts index 4fe8db9dd..886376db7 100644 --- a/web/packages/api/src/transfers/toEthereum/pnaFromAH.ts +++ b/web/packages/api/src/transfers/toEthereum/pnaFromAH.ts @@ -23,7 +23,7 @@ import { TransferV2, ValidationResultV2, } from "../../toEthereumSnowbridgeV2" -import { ConcreteToken } from "src/assets_v2" +import { ConcreteToken } from "../../assets_v2" export class PNAFromAH implements TransferInterface { async getDeliveryFee( From 00c847290e7b6823e47a49234acb037223b6dc13 Mon Sep 17 00:00:00 2001 From: ron Date: Sun, 16 Nov 2025 14:35:30 +0000 Subject: [PATCH 05/13] Revamp operation scripts --- web/packages/operations/package.json | 3 +- .../operations/src/transfer_from_p2e_v2.ts | 18 +++++--- .../src/transfer_from_p2e_v2_with_fee.ts | 22 +++++----- .../src/transfer_from_p2e_v2_with_transact.ts | 26 ++++++------ .../operations/src/transfer_to_ethereum_v2.ts | 42 +++++++++++++------ 5 files changed, 67 insertions(+), 44 deletions(-) diff --git a/web/packages/operations/package.json b/web/packages/operations/package.json index 486e29b82..78b8f3091 100644 --- a/web/packages/operations/package.json +++ b/web/packages/operations/package.json @@ -64,7 +64,8 @@ "transferWndToAHV2": "npx ts-node src/transfer_from_e2p_v2.ts 1000 WND 1000000000", "transferEtherToPenpalV2": "npx ts-node src/transfer_from_e2p_v2.ts 2000 Eth 100000000000000", "transferWndToPenpalV2": "npx ts-node src/transfer_from_e2p_v2.ts 2000 WND 1000000000", - "transactFromAHToEthereum": "npx ts-node src/transfer_from_p2e_v2_with_transact.ts 1000 Eth 100000000000000 0x31A6Dd971306bb72f2ffF771bF30b1B98dB8B2c5 0xf410fd0e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001e48656c6c6f2066726f6d20656e642075736572206f6e2057657374656e640000 0 1000000" + "transactFromAHToEthereum": "npx ts-node src/transfer_from_p2e_v2_with_transact.ts 1000 Eth 100000000000000 0x31A6Dd971306bb72f2ffF771bF30b1B98dB8B2c5 0xf410fd0e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001e48656c6c6f2066726f6d20656e642075736572206f6e2057657374656e640000 0 1000000", + "transferWethAndEtherFromAHV2": "npx ts-node src/transfer_from_p2e_v2.ts 1000 WETH 100000000000000 Eth 100000000000000" }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^8.45.0", diff --git a/web/packages/operations/src/transfer_from_p2e_v2.ts b/web/packages/operations/src/transfer_from_p2e_v2.ts index f442a7bad..dd46276d3 100644 --- a/web/packages/operations/src/transfer_from_p2e_v2.ts +++ b/web/packages/operations/src/transfer_from_p2e_v2.ts @@ -1,16 +1,24 @@ import "dotenv/config" import { transferToEthereum } from "./transfer_to_ethereum_v2" +import { ConcreteToken } from "@snowbridge/api/dist/assets_v2" -const transfer = async (sourceParaId: number, symbol: string, amount: bigint) => { - await transferToEthereum(sourceParaId, symbol, amount) +const transfer = async (sourceParaId: number, tokens: ConcreteToken[]) => { + await transferToEthereum(sourceParaId, tokens) } -if (process.argv.length != 5) { - console.error("Expected arguments: `destinationChainId, symbol, amount`") +if (process.argv.length > 13 || process.argv.length < 5) { + console.error("Expected arguments: `sourceParaId, (address, amount)...`") process.exit(1) } -transfer(parseInt(process.argv[2]), process.argv[3], BigInt(process.argv[4])) +let tokenPairs = (process.argv.length - 3) / 2 +let tokens: ConcreteToken[] = [] +for (let i = 0; i < tokenPairs; i++) { + const token = process.argv[3 + i * 2] + const amount = BigInt(process.argv[4 + i * 2]) + tokens.push({ address: token, amount: amount }) +} +transfer(parseInt(process.argv[2]), tokens) .then(() => process.exit(0)) .catch((error) => { console.error("Error:", error) diff --git a/web/packages/operations/src/transfer_from_p2e_v2_with_fee.ts b/web/packages/operations/src/transfer_from_p2e_v2_with_fee.ts index 393ebf617..cb9d5f165 100644 --- a/web/packages/operations/src/transfer_from_p2e_v2_with_fee.ts +++ b/web/packages/operations/src/transfer_from_p2e_v2_with_fee.ts @@ -1,8 +1,9 @@ import "dotenv/config" import { transferToEthereum } from "./transfer_to_ethereum_v2" import { xcmBuilder } from "@snowbridge/api" +import { ConcreteToken } from "@snowbridge/api/dist/assets_v2" -const transfer = async (sourceParaId: number, symbol: string, feeType: number, amount: bigint) => { +const transfer = async (sourceParaId: number, feeType: number, tokens: ConcreteToken[]) => { let feeTokenLocation: any // Currently, we only support two types of fees to maintain V1 behavior if (feeType == 0) { @@ -10,20 +11,17 @@ const transfer = async (sourceParaId: number, symbol: string, feeType: number, a } else { feeTokenLocation = xcmBuilder.parachainLocation(sourceParaId) } - await transferToEthereum(sourceParaId, symbol, amount, feeTokenLocation) + await transferToEthereum(sourceParaId, tokens, feeTokenLocation) } -if (process.argv.length != 6) { - console.error("Expected arguments: `destinationChainId, symbol, feeType, amount`") - process.exit(1) +let tokenPairs = (process.argv.length - 4) / 2 +let tokens: ConcreteToken[] = [] +for (let i = 0; i < tokenPairs; i++) { + const token = process.argv[4 + i * 2] + const amount = BigInt(process.argv[5 + i * 2]) + tokens.push({ address: token, amount: amount }) } - -transfer( - parseInt(process.argv[2]), - process.argv[3], - parseInt(process.argv[4]), - BigInt(process.argv[5]), -) +transfer(parseInt(process.argv[2]), parseInt(process.argv[3]), tokens) .then(() => process.exit(0)) .catch((error) => { console.error("Error:", error) diff --git a/web/packages/operations/src/transfer_from_p2e_v2_with_transact.ts b/web/packages/operations/src/transfer_from_p2e_v2_with_transact.ts index ecfe54177..d00f5c20c 100644 --- a/web/packages/operations/src/transfer_from_p2e_v2_with_transact.ts +++ b/web/packages/operations/src/transfer_from_p2e_v2_with_transact.ts @@ -1,16 +1,16 @@ import "dotenv/config" import { transferToEthereum } from "./transfer_to_ethereum_v2" +import { ConcreteToken } from "@snowbridge/api/dist/assets_v2" const transact = async ( sourceParaId: number, - symbol: string, - amount: bigint, target: string, calldata: string, value: bigint, gas: bigint, + tokens: ConcreteToken[], ) => { - await transferToEthereum(sourceParaId, symbol, amount, { + await transferToEthereum(sourceParaId, tokens, { feeTokenLocation: undefined, contractCall: { target, @@ -21,21 +21,21 @@ const transact = async ( }) } -if (process.argv.length != 9) { - console.error( - "Expected arguments: `destinationChainId, symbol, amount, target, calldata, value, gas`", - ) - process.exit(1) +let tokenPairs = (process.argv.length - 7) / 2 +let tokens: ConcreteToken[] = [] +for (let i = 0; i < tokenPairs; i++) { + const token = process.argv[7 + i * 2] + const amount = BigInt(process.argv[8 + i * 2]) + tokens.push({ address: token, amount: amount }) } transact( parseInt(process.argv[2]), process.argv[3], - BigInt(process.argv[4]), - process.argv[5], - process.argv[6], - BigInt(process.argv[7]), - BigInt(process.argv[8]), + process.argv[4], + BigInt(process.argv[5]), + BigInt(process.argv[6]), + tokens, ) .then(() => process.exit(0)) .catch((error) => { diff --git a/web/packages/operations/src/transfer_to_ethereum_v2.ts b/web/packages/operations/src/transfer_to_ethereum_v2.ts index 2c923c281..31de9c36f 100644 --- a/web/packages/operations/src/transfer_to_ethereum_v2.ts +++ b/web/packages/operations/src/transfer_to_ethereum_v2.ts @@ -1,14 +1,17 @@ import { Keyring } from "@polkadot/keyring" -import { Context, toEthereumSnowbridgeV2, contextConfigFor, toEthereumV2 } from "@snowbridge/api" +import { Context, toEthereumSnowbridgeV2, contextConfigFor } from "@snowbridge/api" import { cryptoWaitReady } from "@polkadot/util-crypto" import { formatUnits, Wallet } from "ethers" import { assetRegistryFor } from "@snowbridge/registry" import { ContractCall } from "../../base-types" +import { ConcreteToken } from "@snowbridge/api/dist/assets_v2" export const transferToEthereum = async ( sourceParaId: number, - symbol: string, - amount: bigint, + tokens: { + address: string + amount: bigint + }[], options?: { feeTokenLocation?: any contractCall?: ContractCall @@ -38,14 +41,27 @@ export const transferToEthereum = async ( console.log("eth", ETHEREUM_ACCOUNT_PUBLIC, "sub", POLKADOT_ACCOUNT_PUBLIC) const registry = assetRegistryFor(env) - const assets = registry.ethereumChains[registry.ethChainId].assets - const TOKEN_CONTRACT = Object.keys(assets) - .map((t) => assets[t]) - .find((asset) => asset.symbol.toLowerCase().startsWith(symbol.toLowerCase()))?.token - if (!TOKEN_CONTRACT) { - console.error("no token contract exists, check it and rebuild asset registry.") - throw Error(`No token found for ${symbol}`) + + let tokenAddresses = [], + concreteTokens: ConcreteToken[] = [] + for (const t of tokens) { + if (t.address.startsWith("0x") == false) { + const tokenAddress = Object.keys(assets) + .map((t) => assets[t]) + .find((asset) => + asset.symbol.toLowerCase().startsWith(t.address.toLowerCase()), + )?.token + if (!tokenAddress) { + console.error("no token contract exists, check it and rebuild asset registry.") + throw Error(`No token found for ${t.address.toLowerCase()}`) + } + tokenAddresses.push(tokenAddress) + concreteTokens.push({ address: tokenAddress, amount: t.amount }) + } else { + tokenAddresses.push(t.address) + concreteTokens.push({ address: t.address, amount: t.amount }) + } } console.log("Asset Hub to Ethereum") @@ -54,14 +70,14 @@ export const transferToEthereum = async ( const transferImpl = await toEthereumSnowbridgeV2.createTransferImplementation( sourceParaId, registry, - [TOKEN_CONTRACT], + tokenAddresses, ) // Step 1. Get the delivery fee for the transaction let fee: toEthereumSnowbridgeV2.DeliveryFeeV2 = await transferImpl.getDeliveryFee( context, sourceParaId, registry, - [TOKEN_CONTRACT], + tokenAddresses, { feeTokenLocation: options?.feeTokenLocation, slippagePadPercentage: 20n, @@ -76,7 +92,7 @@ export const transferToEthereum = async ( registry, POLKADOT_ACCOUNT_PUBLIC, ETHEREUM_ACCOUNT_PUBLIC, - [{ address: TOKEN_CONTRACT, amount }], + concreteTokens, fee, options, ) From ea1f2dd8e77a20158b343976d3ac722649bd4c28 Mon Sep 17 00:00:00 2001 From: ron Date: Sun, 16 Nov 2025 15:50:25 +0000 Subject: [PATCH 06/13] Fix building XCM --- .../src/xcmbuilders/toEthereum/erc20FromAH.ts | 24 +++++++-------- .../toEthereum/erc20FromParachain.ts | 26 ++++++++-------- .../erc20FromParachainWithNativeAsFee.ts | 30 +++++++++++-------- 3 files changed, 42 insertions(+), 38 deletions(-) diff --git a/web/packages/api/src/xcmbuilders/toEthereum/erc20FromAH.ts b/web/packages/api/src/xcmbuilders/toEthereum/erc20FromAH.ts index 7409e5ee1..b25f9c0e8 100644 --- a/web/packages/api/src/xcmbuilders/toEthereum/erc20FromAH.ts +++ b/web/packages/api/src/xcmbuilders/toEthereum/erc20FromAH.ts @@ -123,7 +123,7 @@ export function buildTransferXcmFromAssetHub( let remoteEtherFeeAmount = fee.ethereumExecutionFee! let assets = [], - assetInstructions = [] + reserveWithdrawAssets: any[] = [] assets.push({ id: DOT_LOCATION, @@ -172,16 +172,10 @@ export function buildTransferXcmFromAssetHub( for (const asset of concreteAssets) { const tokenLocation = erc20Location(ethChainId, asset.id.token) const tokenAmount = asset.amount - assetInstructions.push({ - reserveWithdraw: { - definite: [ - { - id: tokenLocation, - fun: { - Fungible: tokenAmount, - }, - }, - ], + reserveWithdrawAssets.push({ + id: tokenLocation, + fun: { + Fungible: tokenAmount, }, }) } @@ -263,7 +257,13 @@ export function buildTransferXcmFromAssetHub( }, }, preserveOrigin: true, - assets: assetInstructions, + assets: [ + { + reserveWithdraw: { + definite: reserveWithdrawAssets, + }, + }, + ], remoteXcm: remoteXcm, }, }, diff --git a/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachain.ts b/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachain.ts index 0c168e4f8..0914f44c8 100644 --- a/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachain.ts +++ b/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachain.ts @@ -203,20 +203,14 @@ export function buildTransferXcmFromParachain( } : undefined - let assetInstructions = [] + let reserveWithdrawAssets: any[] = [] for (const asset of concreteAssets) { const tokenLocation = erc20Location(ethChainId, asset.id.token) const tokenAmount = asset.amount - assetInstructions.push({ - reserveWithdraw: { - definite: [ - { - id: tokenLocation, - fun: { - Fungible: tokenAmount, - }, - }, - ], + reserveWithdrawAssets.push({ + id: tokenLocation, + fun: { + Fungible: tokenAmount, }, }) } @@ -242,7 +236,13 @@ export function buildTransferXcmFromParachain( }, }, preserveOrigin: true, - assets: assetInstructions, + assets: [ + { + reserveWithdraw: { + definite: reserveWithdrawAssets, + }, + }, + ], remoteXcm: remoteXcm, }, }, @@ -321,10 +321,10 @@ export function buildTransferXcmFromParachain( Fungible: remoteEtherFeeAmount, }, }, + ...reserveWithdrawAssets, ], }, }, - ...assetInstructions, ], remoteXcm: remoteInstructionsOnAH, }, diff --git a/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachainWithNativeAsFee.ts b/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachainWithNativeAsFee.ts index 22aa1f022..9cff11f0c 100644 --- a/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachainWithNativeAsFee.ts +++ b/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachainWithNativeAsFee.ts @@ -35,7 +35,7 @@ export function buildTransferXcmFromParachainWithNativeAssetFee( let remoteEtherFeeNativeAmount = fee.ethereumExecutionFeeInNative! let assets = [], - assetInstructions = [] + reserveWithdrawAssets = [] assets.push({ id: HERE_LOCATION, fun: { @@ -51,16 +51,10 @@ export function buildTransferXcmFromParachainWithNativeAssetFee( Fungible: tokenAmount, }, }) - assetInstructions.push({ - reserveWithdraw: { - definite: [ - { - id: tokenLocation, - fun: { - Fungible: tokenAmount, - }, - }, - ], + reserveWithdrawAssets.push({ + id: tokenLocation, + fun: { + Fungible: tokenAmount, }, }) } @@ -141,7 +135,13 @@ export function buildTransferXcmFromParachainWithNativeAssetFee( }, }, preserveOrigin: true, - assets: assetInstructions, + assets: [ + { + reserveWithdraw: { + definite: reserveWithdrawAssets, + }, + }, + ], remoteXcm: remoteXcm, }, }, @@ -217,7 +217,11 @@ export function buildTransferXcmFromParachainWithNativeAssetFee( ], }, }, - ...assetInstructions, + { + reserveWithdraw: { + definite: reserveWithdrawAssets, + }, + }, ], remoteXcm: remoteInstructionsOnAH, }, From 1af53a2019545a70b6e09d00cc741bb38d0e80a6 Mon Sep 17 00:00:00 2001 From: ron Date: Mon, 17 Nov 2025 12:08:24 +0800 Subject: [PATCH 07/13] Set reserve location --- web/packages/test/scripts/configure-others.sh | 9 +++------ web/packages/test/scripts/configure-substrate.sh | 3 +++ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/web/packages/test/scripts/configure-others.sh b/web/packages/test/scripts/configure-others.sh index 36c25cbd4..6b10410db 100755 --- a/web/packages/test/scripts/configure-others.sh +++ b/web/packages/test/scripts/configure-others.sh @@ -11,6 +11,9 @@ register_weth_on_ah() { # set metadata local call='0x3513020209079edaa8020300b8ea8cb425d85536b158d661da1ef0895bb92f1d105745544810574554481200' send_governance_transact_from_relaychain $ASSET_HUB_PARAID "$call" + # set reserve + local call="0x3521020209079edaa8020300b8ea8cb425d85536b158d661da1ef0895bb92f1d04020109079edaa80200" + send_transact_through_bridge_from_relaychain $ASSET_HUB_PARAID "$call" # Mint weth to Penpal Sovereign on AH local call="0x3506020209079edaa8020300b8ea8cb425d85536b158d661da1ef0895bb92f1d007369626cd00700000000000000000000000000000000000000000000000000001300002cf61a24a229" send_transact_through_bridge_from_relaychain $ASSET_HUB_PARAID "$call" @@ -156,17 +159,11 @@ add_liquidity_on_penpal() { send_transact_through_user_origin_from_relaychain $PENPAL_PARAID "$sudo_pubkey" "$call" } -set_reserve_on_ah() { - local call="0x3521020109079edaa80204020109079edaa80200" - send_transact_through_bridge_from_relaychain $ASSET_HUB_PARAID "$call" -} - configure_on_ah() { register_weth_on_ah register_wnd_on_ethereum register_roc_on_ah add_liquidity_on_ah - set_reserve_on_ah } configure_on_penpal() { diff --git a/web/packages/test/scripts/configure-substrate.sh b/web/packages/test/scripts/configure-substrate.sh index f0e8efd35..de261bbdb 100755 --- a/web/packages/test/scripts/configure-substrate.sh +++ b/web/packages/test/scripts/configure-substrate.sh @@ -56,6 +56,9 @@ configure_ah() { # Create Ether local call="0x28020c1f04020109079edaa802050000003501020109079edaa80200ce796ae65569a670d0c1cc1ac12515a3ce21b5fbf729d63d7b289baad070139d01043513020109079edaa8021445746865721445746865721200" send_governance_transact_from_relaychain $ASSET_HUB_PARAID "$call" + # Set reserve + local call="0x3521020109079edaa80204020109079edaa80200" + send_transact_through_bridge_from_relaychain $ASSET_HUB_PARAID "$call" # Mint Ether to Alice local call="0x3506020109079edaa80200d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d1300002cf61a24a229" send_transact_through_bridge_from_relaychain $ASSET_HUB_PARAID "$call" From f5ab8ea99bb13be328ff904ee63ad47715c4ba5e Mon Sep 17 00:00:00 2001 From: ron Date: Fri, 21 Nov 2025 03:56:29 +0000 Subject: [PATCH 08/13] Fix build xcm --- .../api/src/toEthereumSnowbridgeV2.ts | 1 - .../src/transfers/toEthereum/erc20FromAH.ts | 2 ++ .../src/xcmbuilders/toEthereum/erc20FromAH.ts | 27 +++++++++++++++--- .../toEthereum/erc20FromParachain.ts | 28 ++++++++++++++++--- .../erc20FromParachainWithNativeAsFee.ts | 18 +++++++++++- 5 files changed, 66 insertions(+), 10 deletions(-) diff --git a/web/packages/api/src/toEthereumSnowbridgeV2.ts b/web/packages/api/src/toEthereumSnowbridgeV2.ts index 0769239af..990f3ab73 100644 --- a/web/packages/api/src/toEthereumSnowbridgeV2.ts +++ b/web/packages/api/src/toEthereumSnowbridgeV2.ts @@ -314,7 +314,6 @@ export const estimateFeesFromAssetHub = async ( let localExecutionFeeDOT = 0n let assetHubExecutionFeeDOT = 0n - let returnToSenderExecutionFeeDOT = 0n let bridgeHubDeliveryFeeDOT = 0n let snowbridgeDeliveryFeeDOT = 0n diff --git a/web/packages/api/src/transfers/toEthereum/erc20FromAH.ts b/web/packages/api/src/transfers/toEthereum/erc20FromAH.ts index a84d4b719..be715ea57 100644 --- a/web/packages/api/src/transfers/toEthereum/erc20FromAH.ts +++ b/web/packages/api/src/transfers/toEthereum/erc20FromAH.ts @@ -58,6 +58,7 @@ export class ERC20FromAH implements TransferInterface { concreteAssets, mockDeliveryFee, ) + console.log("localXcm:", localXcm.toHuman()) let forwardedXcmToBH = buildExportXcm( assetHub.registry, @@ -68,6 +69,7 @@ export class ERC20FromAH implements TransferInterface { concreteAssets, mockDeliveryFee, ) + console.log("forwardedXcmToBH:", forwardedXcmToBH.toHuman()) const fees = await estimateFeesFromAssetHub( context, diff --git a/web/packages/api/src/xcmbuilders/toEthereum/erc20FromAH.ts b/web/packages/api/src/xcmbuilders/toEthereum/erc20FromAH.ts index b25f9c0e8..d714d44ab 100644 --- a/web/packages/api/src/xcmbuilders/toEthereum/erc20FromAH.ts +++ b/web/packages/api/src/xcmbuilders/toEthereum/erc20FromAH.ts @@ -25,12 +25,22 @@ export function buildExportXcm( let senderLocation = accountToLocation(sender) let beneficiaryLocation = accountToLocation(beneficiary) let assets = [] - for (const asset of concreteAssets) { - const tokenLocation = erc20LocationReanchored(asset.id.token) + const { etherAsset, otherAssets } = splitEtherAsset(ethChainId, concreteAssets) + if (etherAsset) { + assets.push({ + id: bridgeLocation(ethChainId), + fun: { + Fungible: etherAsset.amount, + }, + }) + } + for (const asset of otherAssets) { + const tokenLocation = erc20Location(ethChainId, asset.id.token) + const tokenAmount = asset.amount assets.push({ id: tokenLocation, fun: { - Fungible: asset.amount, + Fungible: tokenAmount, }, }) } @@ -169,7 +179,16 @@ export function buildTransferXcmFromAssetHub( }) } } - for (const asset of concreteAssets) { + const { etherAsset, otherAssets } = splitEtherAsset(ethChainId, concreteAssets) + if (etherAsset) { + reserveWithdrawAssets.push({ + id: bridgeLocation(ethChainId), + fun: { + Fungible: etherAsset.amount, + }, + }) + } + for (const asset of otherAssets) { const tokenLocation = erc20Location(ethChainId, asset.id.token) const tokenAmount = asset.amount reserveWithdrawAssets.push({ diff --git a/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachain.ts b/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachain.ts index 0914f44c8..8235682d0 100644 --- a/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachain.ts +++ b/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachain.ts @@ -31,11 +31,22 @@ export function buildResultXcmAssetHubERC20TransferFromParachain( }, }, ] - for (const asset of concreteAssets) { + const { etherAsset, otherAssets } = splitEtherAsset(ethChainId, concreteAssets) + if (etherAsset) { assets.push({ - id: erc20Location(ethChainId, asset.id.token), + id: bridgeLocation(ethChainId), + fun: { + Fungible: etherAsset.amount, + }, + }) + } + for (const asset of otherAssets) { + const tokenLocation = erc20Location(ethChainId, asset.id.token) + const tokenAmount = asset.amount + assets.push({ + id: tokenLocation, fun: { - Fungible: asset.amount, + Fungible: tokenAmount, }, }) } @@ -204,7 +215,16 @@ export function buildTransferXcmFromParachain( : undefined let reserveWithdrawAssets: any[] = [] - for (const asset of concreteAssets) { + const { etherAsset, otherAssets } = splitEtherAsset(ethChainId, concreteAssets) + if (etherAsset) { + reserveWithdrawAssets.push({ + id: bridgeLocation(ethChainId), + fun: { + Fungible: etherAsset.amount, + }, + }) + } + for (const asset of otherAssets) { const tokenLocation = erc20Location(ethChainId, asset.id.token) const tokenAmount = asset.amount reserveWithdrawAssets.push({ diff --git a/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachainWithNativeAsFee.ts b/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachainWithNativeAsFee.ts index 9cff11f0c..f4d679d6d 100644 --- a/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachainWithNativeAsFee.ts +++ b/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachainWithNativeAsFee.ts @@ -8,6 +8,7 @@ import { HERE_LOCATION, buildAppendixInstructions, buildEthereumInstructions, + splitEtherAsset, } from "../../xcmBuilder" import { DeliveryFeeV2 } from "../../toEthereumSnowbridgeV2" import { ConcreteAsset } from "../../assets_v2" @@ -42,7 +43,22 @@ export function buildTransferXcmFromParachainWithNativeAssetFee( Fungible: totalNativeFeeAmount, }, }) - for (const asset of concreteAssets) { + const { etherAsset, otherAssets } = splitEtherAsset(ethChainId, concreteAssets) + if (etherAsset) { + assets.push({ + id: bridgeLocation(ethChainId), + fun: { + Fungible: etherAsset.amount, + }, + }) + reserveWithdrawAssets.push({ + id: bridgeLocation(ethChainId), + fun: { + Fungible: etherAsset.amount, + }, + }) + } + for (const asset of otherAssets) { const tokenLocation = erc20Location(ethChainId, asset.id.token) const tokenAmount = asset.amount assets.push({ From db32a8f7a8f27a891c47afdbedd87ec1bb5a3d8e Mon Sep 17 00:00:00 2001 From: ron Date: Fri, 21 Nov 2025 14:37:49 +0800 Subject: [PATCH 09/13] Add PNA flag --- web/packages/api/src/parachains/assethub.ts | 3 +++ web/packages/api/src/parachains/frequency.ts | 1 + web/packages/api/src/parachains/penpal.ts | 1 + web/packages/api/src/toEthereumSnowbridgeV2.ts | 4 ---- web/packages/base-types/src/index.ts | 1 + 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/web/packages/api/src/parachains/assethub.ts b/web/packages/api/src/parachains/assethub.ts index f2c18fc39..0668a5cb4 100644 --- a/web/packages/api/src/parachains/assethub.ts +++ b/web/packages/api/src/parachains/assethub.ts @@ -115,6 +115,7 @@ export class AssetHubParachain extends ParachainBase { minimumBalance: BigInt(assetInfo?.minBalance), isSufficient: Boolean(assetInfo?.isSufficient), assetId: String(assetId), + isPNA: true, } } else if ( locationOnAH?.parents == DOT_LOCATION.parents && @@ -135,6 +136,7 @@ export class AssetHubParachain extends ParachainBase { foreignId: foreignId, minimumBalance: BigInt(existentialDeposit as any), isSufficient: true, + isPNA: true, } } else { let assetType = this.provider.registry.createType( @@ -181,6 +183,7 @@ export class AssetHubParachain extends ParachainBase { foreignId: foreignId, minimumBalance: BigInt(assetInfo?.minBalance), isSufficient: Boolean(assetInfo?.isSufficient), + isPNA: true, } } } diff --git a/web/packages/api/src/parachains/frequency.ts b/web/packages/api/src/parachains/frequency.ts index ff3885ee0..7dbfbf7f5 100644 --- a/web/packages/api/src/parachains/frequency.ts +++ b/web/packages/api/src/parachains/frequency.ts @@ -53,6 +53,7 @@ export class FrequencyParachain extends ParachainBase { location: HERE_LOCATION, locationOnAH: FREQUENCY_TOKEN_LOCATION_ON_WESTEND_AH, locationOnEthereum: FREQUENCY_WESTEND_TOKEN_LOCATION_ON_ETHEREUM, + isPNA: true, } } else { throw Error( diff --git a/web/packages/api/src/parachains/penpal.ts b/web/packages/api/src/parachains/penpal.ts index 38a139c67..4a8c925d5 100644 --- a/web/packages/api/src/parachains/penpal.ts +++ b/web/packages/api/src/parachains/penpal.ts @@ -108,6 +108,7 @@ export class PenpalParachain extends ParachainBase { minimumBalance: BigInt(assetInfo?.minBalance), isSufficient: Boolean(assetInfo?.isSufficient), assetId: String(assetId), + isPNA: true, } } } diff --git a/web/packages/api/src/toEthereumSnowbridgeV2.ts b/web/packages/api/src/toEthereumSnowbridgeV2.ts index 0769239af..efb637e5f 100644 --- a/web/packages/api/src/toEthereumSnowbridgeV2.ts +++ b/web/packages/api/src/toEthereumSnowbridgeV2.ts @@ -250,10 +250,6 @@ export async function dryRunAssetHub( export const MaxWeight = { refTime: 15_000_000_000n, proofSize: 800_000 } -export const isFeeAllowed = (feeLocation: any, sourceParaId: number) => { - return isRelaychainLocation(feeLocation) || isParachainNative(feeLocation, sourceParaId) -} - export const getSnowbridgeDeliveryFee = async (assetHub: ApiPromise, defaultFee?: bigint) => { const feeStorageKey = xxhashAsHex(":BridgeHubEthereumBaseFeeV2:", 128, true) const feeStorageItem = await assetHub.rpc.state.getStorage(feeStorageKey) diff --git a/web/packages/base-types/src/index.ts b/web/packages/base-types/src/index.ts index 92041f82b..4e78c3251 100644 --- a/web/packages/base-types/src/index.ts +++ b/web/packages/base-types/src/index.ts @@ -50,6 +50,7 @@ export type Asset = { decimals: number; isSufficient: boolean; xc20?: string; + isPNA?: boolean; // Location on source Parachain location?: any; // Location reanchored on AH From e65d9d984c8faa0bbcf77a01be153f69c1ac8489 Mon Sep 17 00:00:00 2001 From: ron Date: Fri, 21 Nov 2025 06:44:55 +0000 Subject: [PATCH 10/13] Add isPNA to metadata --- web/packages/api/src/parachains/assethub.ts | 3 ++ web/packages/api/src/parachains/frequency.ts | 1 + web/packages/api/src/parachains/penpal.ts | 1 + .../api/src/toEthereumSnowbridgeV2.ts | 4 -- web/packages/base-types/src/index.ts | 1 + .../src/polkadot_mainnet.registry.json | 48 +++++++++++-------- 6 files changed, 35 insertions(+), 23 deletions(-) diff --git a/web/packages/api/src/parachains/assethub.ts b/web/packages/api/src/parachains/assethub.ts index f2c18fc39..0668a5cb4 100644 --- a/web/packages/api/src/parachains/assethub.ts +++ b/web/packages/api/src/parachains/assethub.ts @@ -115,6 +115,7 @@ export class AssetHubParachain extends ParachainBase { minimumBalance: BigInt(assetInfo?.minBalance), isSufficient: Boolean(assetInfo?.isSufficient), assetId: String(assetId), + isPNA: true, } } else if ( locationOnAH?.parents == DOT_LOCATION.parents && @@ -135,6 +136,7 @@ export class AssetHubParachain extends ParachainBase { foreignId: foreignId, minimumBalance: BigInt(existentialDeposit as any), isSufficient: true, + isPNA: true, } } else { let assetType = this.provider.registry.createType( @@ -181,6 +183,7 @@ export class AssetHubParachain extends ParachainBase { foreignId: foreignId, minimumBalance: BigInt(assetInfo?.minBalance), isSufficient: Boolean(assetInfo?.isSufficient), + isPNA: true, } } } diff --git a/web/packages/api/src/parachains/frequency.ts b/web/packages/api/src/parachains/frequency.ts index ff3885ee0..7dbfbf7f5 100644 --- a/web/packages/api/src/parachains/frequency.ts +++ b/web/packages/api/src/parachains/frequency.ts @@ -53,6 +53,7 @@ export class FrequencyParachain extends ParachainBase { location: HERE_LOCATION, locationOnAH: FREQUENCY_TOKEN_LOCATION_ON_WESTEND_AH, locationOnEthereum: FREQUENCY_WESTEND_TOKEN_LOCATION_ON_ETHEREUM, + isPNA: true, } } else { throw Error( diff --git a/web/packages/api/src/parachains/penpal.ts b/web/packages/api/src/parachains/penpal.ts index 38a139c67..4a8c925d5 100644 --- a/web/packages/api/src/parachains/penpal.ts +++ b/web/packages/api/src/parachains/penpal.ts @@ -108,6 +108,7 @@ export class PenpalParachain extends ParachainBase { minimumBalance: BigInt(assetInfo?.minBalance), isSufficient: Boolean(assetInfo?.isSufficient), assetId: String(assetId), + isPNA: true, } } } diff --git a/web/packages/api/src/toEthereumSnowbridgeV2.ts b/web/packages/api/src/toEthereumSnowbridgeV2.ts index 0769239af..efb637e5f 100644 --- a/web/packages/api/src/toEthereumSnowbridgeV2.ts +++ b/web/packages/api/src/toEthereumSnowbridgeV2.ts @@ -250,10 +250,6 @@ export async function dryRunAssetHub( export const MaxWeight = { refTime: 15_000_000_000n, proofSize: 800_000 } -export const isFeeAllowed = (feeLocation: any, sourceParaId: number) => { - return isRelaychainLocation(feeLocation) || isParachainNative(feeLocation, sourceParaId) -} - export const getSnowbridgeDeliveryFee = async (assetHub: ApiPromise, defaultFee?: bigint) => { const feeStorageKey = xxhashAsHex(":BridgeHubEthereumBaseFeeV2:", 128, true) const feeStorageItem = await assetHub.rpc.state.getStorage(feeStorageKey) diff --git a/web/packages/base-types/src/index.ts b/web/packages/base-types/src/index.ts index 92041f82b..4e78c3251 100644 --- a/web/packages/base-types/src/index.ts +++ b/web/packages/base-types/src/index.ts @@ -50,6 +50,7 @@ export type Asset = { decimals: number; isSufficient: boolean; xc20?: string; + isPNA?: boolean; // Location on source Parachain location?: any; // Location reanchored on AH diff --git a/web/packages/registry/src/polkadot_mainnet.registry.json b/web/packages/registry/src/polkadot_mainnet.registry.json index 984f498d3..8a3363c89 100644 --- a/web/packages/registry/src/polkadot_mainnet.registry.json +++ b/web/packages/registry/src/polkadot_mainnet.registry.json @@ -1,5 +1,5 @@ { - "timestamp": "2025-11-06T11:20:37.352Z", + "timestamp": "2025-11-21T06:41:33.326Z", "environment": "polkadot_mainnet", "ethChainId": 1, "gatewayAddress": "0x27ca963c279c93801941e1eb8799c23f407d68e7", @@ -13,7 +13,7 @@ "accountType": "AccountId32", "name": "Polkadot", "specName": "polkadot", - "specVersion": 2000000 + "specVersion": 2000001 }, "bridgeHub": { "tokenSymbols": "DOT", @@ -383,7 +383,7 @@ "accountType": "AccountId32", "name": "Polkadot Asset Hub", "specName": "statemint", - "specVersion": 2000000 + "specVersion": 2000002 }, "assets": { "0x9d39a5de30e57443bff2a8307a4256c8797a3497": { @@ -653,7 +653,8 @@ }, "foreignId": "0x4e241583d94b5d48a27a22064cd49b2ed6f5231d2d950e432f9b7c2e0ade52b2", "minimumBalance": "bigint:100000000", - "isSufficient": true + "isSufficient": true, + "isPNA": true }, "0x21fab0ea070f162180447881d5873cf3d57200d6": { "token": "0x21fab0ea070f162180447881d5873cf3d57200d6", @@ -710,7 +711,8 @@ "foreignId": "0xad050334b66c8d3abaac7ef6667e97e3e6f4a25d9b7b4765133290f0dc19aa6e", "minimumBalance": "bigint:1000000000000", "isSufficient": false, - "assetId": "86" + "assetId": "86", + "isPNA": true }, "0x12bbfdc9e813614eef8dc8a2560b0efbeaf7c2ab": { "token": "0x12bbfdc9e813614eef8dc8a2560b0efbeaf7c2ab", @@ -755,7 +757,8 @@ }, "foreignId": "0x03b6054d0c576dd8391e34e1609cf398f68050c23009d19ce93c000922bcd852", "minimumBalance": "bigint:1000000000", - "isSufficient": true + "isSufficient": true, + "isPNA": true }, "0x5fdcd48f09fb67de3d202cd854b372aec1100ed5": { "token": "0x5fdcd48f09fb67de3d202cd854b372aec1100ed5", @@ -812,7 +815,8 @@ "foreignId": "0x7ca757304cac2ff0881de18dc6a1dfa7f10e51b0cba0297e0e762f8072049c98", "minimumBalance": "bigint:10000000", "isSufficient": false, - "assetId": "31337" + "assetId": "31337", + "isPNA": true }, "0xa37b046782518a80e2e69056009fbd0431d36e50": { "token": "0xa37b046782518a80e2e69056009fbd0431d36e50", @@ -869,7 +873,8 @@ "foreignId": "0xbc8785969587ef3d22739d3385cb519a9e0133dd5da8d320c376772468c19be6", "minimumBalance": "bigint:1", "isSufficient": false, - "assetId": "23" + "assetId": "23", + "isPNA": true }, "0x769916a66fdac0e3d57363129caac59386ea622b": { "token": "0x769916a66fdac0e3d57363129caac59386ea622b", @@ -913,7 +918,8 @@ }, "foreignId": "0x3b7f577715347bdcde4739a1bf1a7f1dec71e8ff4dbe23a6a49348ebf920c658", "minimumBalance": "bigint:1000000000", - "isSufficient": false + "isSufficient": false, + "isPNA": true }, "0x92262680a8d6636bba9bffdf484c274ca2de6400": { "token": "0x92262680a8d6636bba9bffdf484c274ca2de6400", @@ -970,7 +976,8 @@ "foreignId": "0x536917d1276896038c09bb6499bd0d7197e609983ec22e9ca4e75b394b23752b", "minimumBalance": "bigint:1", "isSufficient": false, - "assetId": "30" + "assetId": "30", + "isPNA": true } }, "estimatedExecutionFeeDOT": "bigint:0", @@ -993,7 +1000,7 @@ "accountType": "AccountId32", "name": "Acala", "specName": "acala", - "specVersion": 2310 + "specVersion": 2320 }, "assets": { "0x0000000000000000000000000000000000000000": { @@ -1115,7 +1122,7 @@ "evmChainId": 996, "name": "Bifrost Polkadot", "specName": "bifrost_polkadot", - "specVersion": 22001 + "specVersion": 22002 }, "assets": { "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2": { @@ -1135,7 +1142,7 @@ "isSufficient": false } }, - "estimatedExecutionFeeDOT": "bigint:67408398", + "estimatedExecutionFeeDOT": "bigint:82916020", "estimatedDeliveryFeeDOT": "bigint:307100000" }, "2034": { @@ -1156,7 +1163,7 @@ "evmChainId": 222222, "name": "Hydration", "specName": "hydradx", - "specVersion": 347 + "specVersion": 359 }, "assets": { "0x45804880de22913dafe09f4980848ece6ecbaf78": { @@ -1312,7 +1319,7 @@ "isSufficient": true } }, - "estimatedExecutionFeeDOT": "bigint:1784885", + "estimatedExecutionFeeDOT": "bigint:1654487", "estimatedDeliveryFeeDOT": "bigint:307100000" }, "2043": { @@ -1399,7 +1406,7 @@ "accountType": "AccountId32", "name": "Kusama Asset Hub", "specName": "statemine", - "specVersion": 2000001 + "specVersion": 2000002 }, "assets": { "0x9d39a5de30e57443bff2a8307a4256c8797a3497": { @@ -1621,7 +1628,8 @@ }, "foreignId": "0x4e241583d94b5d48a27a22064cd49b2ed6f5231d2d950e432f9b7c2e0ade52b2", "minimumBalance": "bigint:10000000", - "isSufficient": true + "isSufficient": true, + "isPNA": true }, "0x12bbfdc9e813614eef8dc8a2560b0efbeaf7c2ab": { "token": "0x12bbfdc9e813614eef8dc8a2560b0efbeaf7c2ab", @@ -1650,7 +1658,8 @@ }, "foreignId": "0x03b6054d0c576dd8391e34e1609cf398f68050c23009d19ce93c000922bcd852", "minimumBalance": "bigint:3333333", - "isSufficient": true + "isSufficient": true, + "isPNA": true }, "0x5fdcd48f09fb67de3d202cd854b372aec1100ed5": { "token": "0x5fdcd48f09fb67de3d202cd854b372aec1100ed5", @@ -1722,7 +1731,8 @@ }, "foreignId": "0x7ca757304cac2ff0881de18dc6a1dfa7f10e51b0cba0297e0e762f8072049c98", "minimumBalance": "bigint:10000000", - "isSufficient": false + "isSufficient": false, + "isPNA": true } }, "estimatedExecutionFeeDOT": "bigint:0", From e87284eb6ed860dc9100cfcd3d27f524cb866a67 Mon Sep 17 00:00:00 2001 From: ron Date: Fri, 21 Nov 2025 14:53:16 +0800 Subject: [PATCH 11/13] Add token length check --- web/packages/api/src/toEthereumSnowbridgeV2.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/web/packages/api/src/toEthereumSnowbridgeV2.ts b/web/packages/api/src/toEthereumSnowbridgeV2.ts index 79719812e..ba3c2b992 100644 --- a/web/packages/api/src/toEthereumSnowbridgeV2.ts +++ b/web/packages/api/src/toEthereumSnowbridgeV2.ts @@ -106,15 +106,27 @@ export function createTransferImplementation( let transferImpl if (sourceParaId == registry.assetHubParaId) { - if (sourceAssetMetadata.location) { + if (sourceAssetMetadata.isPNA) { + if (tokenAddresses.length !== 1) { + throw Error("PNA transfer from Asset Hub only supports single asset transfer.") + } transferImpl = new PNAFromAH() } else { + if (tokenAddresses.length > 5) { + throw Error("ERC20 transfer from Asset Hub only supports up to 5 assets.") + } transferImpl = new ERC20FromAH() } } else { - if (sourceAssetMetadata.location) { + if (sourceAssetMetadata.isPNA) { + if (tokenAddresses.length !== 1) { + throw Error("PNA transfer from Parachain only supports single asset transfer.") + } transferImpl = new PNAFromParachain() } else { + if (tokenAddresses.length > 5) { + throw Error("ERC20 transfer from Parachain only supports up to 5 assets.") + } transferImpl = new ERC20FromParachain() } } From 8706c102f9d992be409c35078f1231eec5f0cfad Mon Sep 17 00:00:00 2001 From: ron Date: Wed, 26 Nov 2025 03:19:58 +0000 Subject: [PATCH 12/13] Wrap InsufficientTokenBalance --- .../api/src/toEthereumSnowbridgeV2.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/web/packages/api/src/toEthereumSnowbridgeV2.ts b/web/packages/api/src/toEthereumSnowbridgeV2.ts index ba3c2b992..e35cff70f 100644 --- a/web/packages/api/src/toEthereumSnowbridgeV2.ts +++ b/web/packages/api/src/toEthereumSnowbridgeV2.ts @@ -639,6 +639,17 @@ export const validateTransfer = async ( bridgeHubDryRunError = dryRunResultBridgeHub.errorMessage } } else { + if ( + dryRunResultAssetHub.error && + (dryRunResultAssetHub.error as any).error?.module?.index == 31 && + (dryRunResultAssetHub.error as any).error?.module?.error == "0x1c000900" + ) { + logs.push({ + kind: ValidationKind.Error, + reason: ValidationReason.InsufficientTokenBalance, + message: "Insufficient token balance to submit transaction.", + }) + } logs.push({ kind: ValidationKind.Error, reason: ValidationReason.DryRunFailed, @@ -657,6 +668,17 @@ export const validateTransfer = async ( sourceAccountHex, ) if (!dryRunSource.success) { + if ( + dryRunSource.error && + (dryRunSource.error as any).error?.module?.index == 31 && + (dryRunSource.error as any).error?.module?.error == "0x1c000900" + ) { + logs.push({ + kind: ValidationKind.Error, + reason: ValidationReason.InsufficientTokenBalance, + message: "Insufficient token balance to submit transaction.", + }) + } logs.push({ kind: ValidationKind.Error, reason: ValidationReason.DryRunFailed, From 74ec9f139c241bb7d44321b4ef5662589a3857e7 Mon Sep 17 00:00:00 2001 From: ron Date: Wed, 26 Nov 2025 14:07:31 +0800 Subject: [PATCH 13/13] Add the fee check back --- .../api/src/toEthereumSnowbridgeV2.ts | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/web/packages/api/src/toEthereumSnowbridgeV2.ts b/web/packages/api/src/toEthereumSnowbridgeV2.ts index e35cff70f..ea8ed73ae 100644 --- a/web/packages/api/src/toEthereumSnowbridgeV2.ts +++ b/web/packages/api/src/toEthereumSnowbridgeV2.ts @@ -554,7 +554,7 @@ export const validateTransfer = async ( context: Context, transfer: TransferV2, ): Promise => { - const { registry } = transfer.input + const { registry, fee } = transfer.input const { sourceAccountHex, sourceParaId, @@ -595,6 +595,22 @@ export const validateTransfer = async ( }) } } + // No fee specified means that the fee.ethereumExecutionFee is paid in Ether on source chain. + if (!fee.feeLocation) { + let etherBalance = await sourceParachainImpl.getTokenBalance( + sourceAccountHex, + registry.ethChainId, + ETHER_TOKEN_ADDRESS, + ) + + if (fee.ethereumExecutionFee! > etherBalance) { + logs.push({ + kind: ValidationKind.Error, + reason: ValidationReason.InsufficientEtherBalance, + message: "Insufficient ether balance to submit transaction.", + }) + } + } let contractCall = transfer.input.contractCall if (contractCall) { try { @@ -730,6 +746,16 @@ export const validateTransfer = async ( const paymentInfo = await tx.paymentInfo(sourceAccountHex) const sourceExecutionFee = paymentInfo["partialFee"].toBigInt() + if ( + sourceParaId == registry.assetHubParaId && + sourceExecutionFee + fee.totalFeeInDot > dotBalance + ) { + logs.push({ + kind: ValidationKind.Error, + reason: ValidationReason.InsufficientDotFee, + message: "Insufficient DOT balance to submit transaction on the source parachain.", + }) + } const bridgeStatus = await getOperatingStatus({ gateway, bridgeHub }) if (bridgeStatus.toEthereum.outbound !== "Normal") {