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/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 8fb01306e..ea8ed73ae 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,31 +34,99 @@ 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 + totalFeeInDot: bigint + localExecutionFeeDOT?: bigint + localDeliveryFeeDOT?: bigint + ethereumExecutionFee?: bigint + feeLocation?: any + totalFeeInNative?: bigint + assetHubExecutionFeeNative?: 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, - tokenAddress: string, + tokenAddresses: string[], ): TransferInterface { - const { sourceAssetMetadata } = resolveInputs(registry, tokenAddress, sourceParaId) + const { sourceAssetMetadata } = resolveInputs(registry, tokenAddresses[0], sourceParaId) 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() } } @@ -191,10 +262,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) @@ -212,34 +279,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 +313,7 @@ export const estimateFeesFromAssetHub = async ( feeTokenLocation?: any contractCall?: ContractCall }, -): Promise => { +): Promise => { const assetHub = await context.parachain(registry.assetHubParaId) const assetHubImpl = await paraImplementation(assetHub) @@ -257,8 +322,6 @@ export const estimateFeesFromAssetHub = async ( let localExecutionFeeDOT = 0n let assetHubExecutionFeeDOT = 0n - let returnToSenderExecutionFeeDOT = 0n - let returnToSenderDeliveryFeeDOT = 0n let bridgeHubDeliveryFeeDOT = 0n let snowbridgeDeliveryFeeDOT = 0n @@ -281,15 +344,13 @@ export const estimateFeesFromAssetHub = async ( localExecutionFeeDOT + snowbridgeDeliveryFeeDOT + assetHubExecutionFeeDOT + - returnToSenderExecutionFeeDOT + - returnToSenderDeliveryFeeDOT + bridgeHubDeliveryFeeDOT let ethereumExecutionFee = await estimateEthereumExecutionFee( context, registry, registry.assetHubParaId, - tokenAddress, + tokenAddresses, options?.contractCall, ) @@ -321,13 +382,10 @@ export const estimateFeesFromAssetHub = async ( snowbridgeDeliveryFeeDOT, assetHubExecutionFeeDOT, bridgeHubDeliveryFeeDOT, - returnToSenderDeliveryFeeDOT, - returnToSenderExecutionFeeDOT, totalFeeInDot, ethereumExecutionFee, feeLocation, assetHubExecutionFeeNative, - returnToSenderExecutionFeeNative, ethereumExecutionFeeInNative, localExecutionFeeInNative, totalFeeInNative, @@ -338,7 +396,7 @@ export const estimateFeesFromParachains = async ( context: Context, sourceParaId: number, registry: AssetRegistry, - tokenAddress: string, + tokenAddresses: string[], deliveryXcm: DeliveryXcm, options?: { padPercentage?: bigint @@ -347,7 +405,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)) @@ -360,8 +418,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 @@ -380,10 +436,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 +448,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, @@ -426,15 +470,13 @@ export const estimateFeesFromParachains = async ( localDeliveryFeeDOT + snowbridgeDeliveryFeeDOT + assetHubExecutionFeeDOT + - returnToSenderExecutionFeeDOT + - returnToSenderDeliveryFeeDOT + bridgeHubDeliveryFeeDOT let ethereumExecutionFee = await estimateEthereumExecutionFee( context, registry, sourceParaId, - tokenAddress, + tokenAddresses, options?.contractCall, ) @@ -497,13 +539,10 @@ export const estimateFeesFromParachains = async ( snowbridgeDeliveryFeeDOT, assetHubExecutionFeeDOT, bridgeHubDeliveryFeeDOT, - returnToSenderDeliveryFeeDOT, - returnToSenderExecutionFeeDOT, totalFeeInDot, ethereumExecutionFee, feeLocation, assetHubExecutionFeeNative, - returnToSenderExecutionFeeNative, ethereumExecutionFeeInNative, localExecutionFeeInNative, localDeliveryFeeInNative, @@ -511,188 +550,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, fee } = transfer.input const { sourceAccountHex, sourceParaId, + aggregatedAssets, sourceParachain: source, - sourceAssetMetadata, } = transfer.computed const { tx } = transfer @@ -710,45 +577,25 @@ 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.", + 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, }) } } - + // No fee specified means that the fee.ethereumExecutionFee is paid in Ether on source chain. if (!fee.feeLocation) { let etherBalance = await sourceParachainImpl.getTokenBalance( sourceAccountHex, @@ -756,7 +603,6 @@ export const validateTransferFromParachain = async ( ETHER_TOKEN_ADDRESS, ) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion if (fee.ethereumExecutionFee! > etherBalance) { logs.push({ kind: ValidationKind.Error, @@ -765,7 +611,6 @@ export const validateTransferFromParachain = async ( }) } } - let contractCall = transfer.input.contractCall if (contractCall) { try { @@ -786,59 +631,114 @@ 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 { + 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, - 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) { + 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, - 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 } } } @@ -846,6 +746,16 @@ export const validateTransferFromParachain = 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") { @@ -866,7 +776,7 @@ export const validateTransferFromParachain = async ( nativeBalance, dotBalance, sourceExecutionFee, - tokenBalance, + tokenBalances, sourceDryRunError, assetHubDryRunError, bridgeHubDryRunError, @@ -886,13 +796,11 @@ 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, 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/erc20FromAH.ts b/web/packages/api/src/transfers/toEthereum/erc20FromAH.ts index 11ccdf504..be715ea57 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,26 @@ export class ERC20FromAH implements TransferInterface { "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", - sourceAssetMetadata, - 1n, + concreteAssets, mockDeliveryFee, ) + console.log("localXcm:", localXcm.toHuman()) let forwardedXcmToBH = buildExportXcm( assetHub.registry, registry.ethChainId, - sourceAssetMetadata, "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", - 1n, - 1n, + concreteAssets, + mockDeliveryFee, ) + console.log("forwardedXcmToBH:", forwardedXcmToBH.toHuman()) const fees = await estimateFeesFromAssetHub( - source.context, + context, registry, - tokenAddress, + tokenAddresses, { localXcm, forwardedXcmToBH, @@ -86,50 +85,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 +152,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 +177,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 +189,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..6b0d4d010 100644 --- a/web/packages/api/src/transfers/toEthereum/erc20FromParachain.ts +++ b/web/packages/api/src/transfers/toEthereum/erc20FromParachain.ts @@ -3,24 +3,15 @@ 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, - 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 { AssetRegistry, ContractCall } from "@snowbridge/base-types" +import { buildMessageId, resolveInputs } from "../../toEthereum_v2" import { Context } from "../.." import { TransferInterface } from "./transferInterface" import { @@ -28,14 +19,19 @@ import { estimateFeesFromParachains, MaxWeight, mockDeliveryFee, - validateTransferFromParachain, + validateTransfer, + DeliveryFeeV2, + TransferV2, + ValidationResultV2, } from "../../toEthereumSnowbridgeV2" +import { AggregatedAsset, ConcreteAsset, ConcreteToken } from "../../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 +40,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 +70,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 +104,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 +174,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 +202,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 +227,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 +243,7 @@ export class ERC20FromParachain implements TransferInterface { sourceAccount, beneficiaryAccount, messageId, - asset, - amount, + concreteAssets, fee, claimerLocation, callHex, @@ -274,8 +260,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..886376db7 100644 --- a/web/packages/api/src/transfers/toEthereum/pnaFromAH.ts +++ b/web/packages/api/src/transfers/toEthereum/pnaFromAH.ts @@ -8,16 +8,9 @@ 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, - DeliveryFee, - resolveInputs, - Transfer, - ValidationResult, -} from "../../toEthereum_v2" +import { buildMessageId, resolveInputs } from "../../toEthereum_v2" import { Context } from "../.." import { TransferInterface } from "./transferInterface" import { @@ -25,14 +18,19 @@ import { estimateFeesFromAssetHub, MaxWeight, mockDeliveryFee, - validateTransferFromAssetHub, + validateTransfer, + DeliveryFeeV2, + TransferV2, + ValidationResultV2, } from "../../toEthereumSnowbridgeV2" +import { ConcreteToken } from "../../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 +38,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 +68,9 @@ export class PNAFromAH implements TransferInterface { ) const fees = await estimateFeesFromAssetHub( - source.context, + context, registry, - tokenAddress, + tokenAddresses, { localXcm, forwardedXcmToBH, @@ -94,49 +81,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 +131,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 +166,7 @@ export class PNAFromAH implements TransferInterface { asset: Asset, amount: bigint, messageId: string, - fee: DeliveryFee, + fee: DeliveryFeeV2, options?: { claimerLocation?: any contractCall?: ContractCall @@ -188,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 57d834847..53b1fa63f 100644 --- a/web/packages/api/src/transfers/toEthereum/pnaFromParachain.ts +++ b/web/packages/api/src/transfers/toEthereum/pnaFromParachain.ts @@ -7,20 +7,12 @@ 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 +20,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 +41,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 +67,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 +93,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 +108,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 +150,7 @@ export class PNAFromParachain implements TransferInterface { sourceAccountHex, beneficiaryAccount, sourceAssetMetadata, - amount, + tokens[0].amount, messageId, fee, options, @@ -182,26 +161,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 +199,7 @@ export class PNAFromParachain implements TransferInterface { asset: Asset, amount: bigint, messageId: string, - fee: DeliveryFee, + fee: DeliveryFeeV2, options?: { claimerLocation?: any contractCall?: ContractCall @@ -247,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/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..d714d44ab 100644 --- a/web/packages/api/src/xcmbuilders/toEthereum/erc20FromAH.ts +++ b/web/packages/api/src/xcmbuilders/toEthereum/erc20FromAH.ts @@ -6,31 +6,51 @@ 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 = [] + 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: tokenAmount, + }, + }) + } let exportXcm: any[] = [ { withdrawAsset: [ { id: HERE_LOCATION, fun: { - Fungible: feeInEther, + Fungible: deliveryFee.ethereumExecutionFee!, }, }, ], @@ -40,20 +60,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,40 +122,76 @@ 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 = [], + reserveWithdrawAssets: any[] = [] + assets.push({ id: DOT_LOCATION, fun: { Fungible: totalDOTFeeAmount, }, }) - if (isEthereumNative(tokenLocation, ethChainId)) { + if (!containsEther(ethChainId, concreteAssets)) { + 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 + assets.push({ + id: tokenLocation, + fun: { + Fungible: tokenAmount, + }, + }) + } + } else { + const { etherAsset, otherAssets } = splitEtherAsset(ethChainId, concreteAssets) assets.push({ id: bridgeLocation(ethChainId), fun: { - Fungible: tokenAmount + remoteEtherFeeAmount, + Fungible: etherAsset!.amount + remoteEtherFeeAmount, }, }) - } else { - assets.push({ + for (const asset of otherAssets) { + const tokenLocation = erc20Location(ethChainId, asset.id.token) + const tokenAmount = asset.amount + assets.push({ + id: tokenLocation, + fun: { + Fungible: tokenAmount, + }, + }) + } + } + const { etherAsset, otherAssets } = splitEtherAsset(ethChainId, concreteAssets) + if (etherAsset) { + reserveWithdrawAssets.push({ id: bridgeLocation(ethChainId), fun: { - Fungible: remoteEtherFeeAmount, + Fungible: etherAsset.amount, }, }) - assets.push({ + } + for (const asset of otherAssets) { + const tokenLocation = erc20Location(ethChainId, asset.id.token) + const tokenAmount = asset.amount + reserveWithdrawAssets.push({ id: tokenLocation, fun: { Fungible: tokenAmount, @@ -152,6 +201,30 @@ export function buildTransferXcmFromAssetHub( 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 +248,7 @@ export function buildTransferXcmFromAssetHub( depositAsset: { assets: { wild: { - allCounted: 2, + allCounted: 8, }, }, beneficiary: { @@ -186,6 +259,7 @@ export function buildTransferXcmFromAssetHub( }, ], }, + ...(exchangeInstruction ? [exchangeInstruction] : []), { initiateTransfer: { destination: bridgeLocation(ethChainId), @@ -205,14 +279,7 @@ export function buildTransferXcmFromAssetHub( assets: [ { reserveWithdraw: { - definite: [ - { - id: tokenLocation, - fun: { - Fungible: tokenAmount, - }, - }, - ], + definite: reserveWithdrawAssets, }, }, ], 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 0d9d62d53..8235682d0 100644 --- a/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachain.ts +++ b/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachain.ts @@ -1,46 +1,59 @@ import { Registry } from "@polkadot/types/types" -import { beneficiaryMultiAddress } from "../../utils" import { bridgeLocation, DOT_LOCATION, erc20Location, - erc20LocationReanchored, parachainLocation, accountToLocation, - 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!, + }, + }, + ] + 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: tokenAmount, + }, + }) + } 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 +61,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. + beneficiary: { + parents: 0, + interior: { x1: [{ AccountKey20: { key: beneficiary } }] }, }, }, - weight_limit: "Unlimited", - }, - }, - { - depositAsset: { - assets: { - Wild: { - AllCounted: 1, - }, - }, - 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,49 +122,63 @@ 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 + 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({ id: DOT_LOCATION, fun: { Fungible: totalDOTFeeAmount, }, }) - if (isEthereumNative(tokenLocation, ethChainId)) { - assets.push({ - id: bridgeLocation(ethChainId), - fun: { - Fungible: remoteEtherFeeAmount + tokenAmount, - }, - }) + if (!containsEther(ethChainId, concreteAssets)) { + 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 + assets.push({ + id: tokenLocation, + fun: { + Fungible: tokenAmount, + }, + }) + } } else { + const { etherAsset, otherAssets } = splitEtherAsset(ethChainId, concreteAssets) assets.push({ id: bridgeLocation(ethChainId), fun: { - Fungible: remoteEtherFeeAmount, - }, - }) - assets.push({ - id: tokenLocation, - 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 appendixInstructions = buildAppendixInstructions( @@ -331,10 +190,56 @@ 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 reserveWithdrawAssets: any[] = [] + 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({ + id: tokenLocation, + fun: { + Fungible: tokenAmount, + }, + }) + } + let remoteInstructionsOnAH: any[] = [ { setAppendix: appendixInstructions, }, + ...(exchangeInstruction ? [exchangeInstruction] : []), { initiateTransfer: { destination: bridgeLocation(ethChainId), @@ -354,14 +259,7 @@ export function buildTransferXcmFromParachain( assets: [ { reserveWithdraw: { - definite: [ - { - id: tokenLocation, - fun: { - Fungible: tokenAmount, - }, - }, - ], + definite: reserveWithdrawAssets, }, }, ], @@ -396,7 +294,7 @@ export function buildTransferXcmFromParachain( depositAsset: { assets: { wild: { - allCounted: 3, + allCounted: 8, }, }, beneficiary: { @@ -416,7 +314,10 @@ export function buildTransferXcmFromParachain( { id: DOT_LOCATION, fun: { - Fungible: totalDOTFeeAmount - localDOTFeeAmount, + Fungible: + totalDOTFeeAmount - + localDOTFeeAmount - + remoteEtherFeeInDOTAmount, }, }, ], @@ -427,24 +328,20 @@ export function buildTransferXcmFromParachain( { reserveWithdraw: { definite: [ - { - id: bridgeLocation(ethChainId), - fun: { - Fungible: remoteEtherFeeAmount, - }, - }, - ], - }, - }, - { - reserveWithdraw: { - definite: [ - { - id: tokenLocation, - fun: { - Fungible: tokenAmount, - }, - }, + remoteEtherFeeInDOTAmount > 0 + ? { + id: DOT_LOCATION, + fun: { + Fungible: remoteEtherFeeInDOTAmount, + }, + } + : { + id: bridgeLocation(ethChainId), + fun: { + Fungible: remoteEtherFeeAmount, + }, + }, + ...reserveWithdrawAssets, ], }, }, 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 bc4e32298..f4d679d6d 100644 --- a/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachainWithNativeAsFee.ts +++ b/web/packages/api/src/xcmbuilders/toEthereum/erc20FromParachainWithNativeAsFee.ts @@ -8,9 +8,10 @@ import { HERE_LOCATION, buildAppendixInstructions, buildEthereumInstructions, + splitEtherAsset, } from "../../xcmBuilder" -import { Asset } from "@snowbridge/base-types" -import { DeliveryFee } from "../../toEthereum_v2" +import { DeliveryFeeV2 } from "../../toEthereumSnowbridgeV2" +import { ConcreteAsset } from "../../assets_v2" export function buildTransferXcmFromParachainWithNativeAssetFee( registry: Registry, @@ -21,38 +22,58 @@ export function buildTransferXcmFromParachainWithNativeAssetFee( 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 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 = [ - { - id: HERE_LOCATION, + let assets = [], + reserveWithdrawAssets = [] + assets.push({ + id: HERE_LOCATION, + fun: { + Fungible: totalNativeFeeAmount, + }, + }) + const { etherAsset, otherAssets } = splitEtherAsset(ethChainId, concreteAssets) + if (etherAsset) { + assets.push({ + id: bridgeLocation(ethChainId), fun: { - Fungible: totalNativeFeeAmount, + 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({ id: tokenLocation, fun: { Fungible: tokenAmount, }, - }, - ] + }) + reserveWithdrawAssets.push({ + id: tokenLocation, + fun: { + Fungible: tokenAmount, + }, + }) + } let appendixInstructions = buildAppendixInstructions( envName, @@ -133,14 +154,7 @@ export function buildTransferXcmFromParachainWithNativeAssetFee( assets: [ { reserveWithdraw: { - definite: [ - { - id: tokenLocation, - fun: { - Fungible: tokenAmount, - }, - }, - ], + definite: reserveWithdrawAssets, }, }, ], @@ -176,7 +190,7 @@ export function buildTransferXcmFromParachainWithNativeAssetFee( depositAsset: { assets: { wild: { - allCounted: 3, + allCounted: 8, }, }, beneficiary: { @@ -221,14 +235,7 @@ export function buildTransferXcmFromParachainWithNativeAssetFee( }, { reserveWithdraw: { - definite: [ - { - id: tokenLocation, - fun: { - Fungible: tokenAmount, - }, - }, - ], + definite: reserveWithdrawAssets, }, }, ], 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 626d504b4..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,270 +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 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: { + depositReserveAsset: { assets: { - wild: { - allCounted: 2, - }, - }, - beneficiary: { - parents: 0, - interior: { x1: [beneficiaryLocation] }, - }, - }, - }, - { 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, + Wild: { + AllOf: { id: assetLocationOnAH, fun: "Fungible" }, }, }, - ], - }, - { - initiateTeleport: { - assets: { - definite: [ - { - id: assetLocationOnAH, - fun: { - Fungible: transferAmount, - }, - }, - ], - }, - 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, + }, ], }) } @@ -336,7 +114,7 @@ export function buildTransferXcmFromParachain( topic: string, asset: Asset, tokenAmount: bigint, - fee: DeliveryFee, + fee: DeliveryFeeV2, claimerLocation?: any, callHex?: string, ) { @@ -344,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({ @@ -362,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, @@ -378,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), @@ -463,7 +268,10 @@ export function buildTransferXcmFromParachain( { id: DOT_LOCATION, fun: { - Fungible: totalDOTFeeAmount - localDOTFeeAmount, + Fungible: + totalDOTFeeAmount - + localDOTFeeAmount - + remoteEtherFeeInDOTAmount, }, }, ], @@ -474,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/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/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 74fcdeff3..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,13 +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: toEthereumV2.DeliveryFee = await transferImpl.getDeliveryFee( - { sourceParaId, context }, + let fee: toEthereumSnowbridgeV2.DeliveryFeeV2 = await transferImpl.getDeliveryFee( + context, + sourceParaId, registry, - TOKEN_CONTRACT, + tokenAddresses, { feeTokenLocation: options?.feeTokenLocation, slippagePadPercentage: 20n, @@ -70,12 +87,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, + concreteTokens, fee, options, ) 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", diff --git a/web/packages/test/scripts/configure-others.sh b/web/packages/test/scripts/configure-others.sh index fc91f2d6d..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" diff --git a/web/packages/test/scripts/configure-substrate.sh b/web/packages/test/scripts/configure-substrate.sh index 6edf9a652..de261bbdb 100755 --- a/web/packages/test/scripts/configure-substrate.sh +++ b/web/packages/test/scripts/configure-substrate.sh @@ -54,8 +54,11 @@ configure_bh() { configure_ah() { # Create Ether - local call="0x28020c1f04020109079edaa802040000003501020109079edaa80200ce796ae65569a670d0c1cc1ac12515a3ce21b5fbf729d63d7b289baad070139d01043513020109079edaa8021445746865721445746865721200" + 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"