diff --git a/web/packages/api/src/index.ts b/web/packages/api/src/index.ts index d5cac36b10..32a140e033 100644 --- a/web/packages/api/src/index.ts +++ b/web/packages/api/src/index.ts @@ -24,7 +24,7 @@ import type { AddTipInterface } from "./addTip/addTipInterface" import type { AgentCreationInterface } from "./types/registration/agent" import type { RegistrationInterface } from "./types/registration/toPolkadot" import type { TransferInterface as ForInterParachainTransferInterface } from "./transfers/forInterParachain/transferInterface" -import type { TransferInterface as ForKusamaTransferInterface } from "./transfers/forKusama/transferInterface" +import type { TransferInterface as PolkadotKusamaTransferInterface } from "./transfers/polkadotKusama/transferInterface" import type { TransferInterface as ToPolkadotTransferInterface } from "./transfers/toPolkadot/transferInterface" import type { TransferInterface as ToPolkadotL2TransferInterface } from "./transfers/l2ToPolkadot/transferInterface" import type { TransferInterface as ToEthereumTransferInterface } from "./transfers/toEthereum/transferInterface" @@ -60,7 +60,7 @@ export * as toPolkadotSnowbridgeV2 from "./types/toPolkadotSnowbridgeV2" export * as toEthereumV2 from "./types/toEthereum" export * as toEthereumFromEVMV2 from "./types/toEthereumEvm" export * as forInterParachain from "./types/forInterParachain" -export * as forKusama from "./types/forKusama" +export * as polkadotKusama from "./types/polkadotKusama" export type { VolumeFeeParams } from "./feeSchedule" export type { FeeAsset, @@ -419,8 +419,8 @@ export type ApiOptions

> = { export type TransferImplementation = | ({ kind: "polkadot->polkadot" } & ForInterParachainTransferInterface) - | ({ kind: "kusama->polkadot" } & ForKusamaTransferInterface) - | ({ kind: "polkadot->kusama" } & ForKusamaTransferInterface) + | ({ kind: "kusama->polkadot" } & PolkadotKusamaTransferInterface) + | ({ kind: "polkadot->kusama" } & PolkadotKusamaTransferInterface) | ({ kind: "polkadot->ethereum" } & ToEthereumTransferInterface) | ({ kind: "ethereum->polkadot" } & ToPolkadotTransferInterface) | ({ kind: "ethereum->ethereum" } & ToEthereumEvmTransferInterface) @@ -564,9 +564,9 @@ export class SnowbridgeApi

> { case "polkadot->kusama": { const sourceParachain = sourceChain as Parachain const destinationParachain = destinationChain as Parachain - const { KusamaTransfer } = require("./forKusama") + const { PolkadotKusamaTransfer } = require("./polkadotKusama") return withKind( - new KusamaTransfer( + new PolkadotKusamaTransfer( this.info, this.context, route, diff --git a/web/packages/api/src/forKusama.ts b/web/packages/api/src/polkadotKusama.ts similarity index 87% rename from web/packages/api/src/forKusama.ts rename to web/packages/api/src/polkadotKusama.ts index 52772b0a14..17ba0d57e1 100644 --- a/web/packages/api/src/forKusama.ts +++ b/web/packages/api/src/polkadotKusama.ts @@ -35,23 +35,30 @@ import { import { CallDryRunEffects, XcmDryRunApiError, XcmDryRunEffects } from "@polkadot/types/interfaces" import { Result } from "@polkadot/types" import { ensureValidationSuccess, padFeeByPercentage, u32ToLeBytes } from "./utils" -import { addBreakdown, computeTotals, findInBreakdown, findInBreakdownOrZero, findTotal } from "./fees" +import { + addBreakdown, + computeTotals, + findInBreakdown, + findInBreakdownOrZero, + findTotal, +} from "./fees" import { checkDotKsmPoolLiquidityForKusamaToPolkadot, checkKsmDotPoolLiquidityForPolkadotToKusama, } from "./poolReserves" import { resolveBeneficiary } from "./crypto" -import { TransferInterface as KusamaTransferInterface } from "./transfers/forKusama/transferInterface" +import { TransferInterface as PolkadotKusamaTransferInterface } from "./transfers/polkadotKusama/transferInterface" import { Context } from "." import type { DeliveryFee, MessageReceipt, Transfer, + TransferOptions, ValidatedTransfer, ValidationLog, -} from "./types/forKusama" -import { ValidationKind, ValidationReason } from "./types/forKusama" -export { ValidationKind, ValidationReason } from "./types/forKusama" +} from "./types/polkadotKusama" +import { ValidationKind, ValidationReason } from "./types/polkadotKusama" +export { ValidationKind, ValidationReason } from "./types/polkadotKusama" export enum Direction { ToKusama, @@ -90,7 +97,7 @@ function resolveInputs( return { sourceAssetMetadata, destAssetMetadata, sourceParachain } } -export class KusamaTransfer implements KusamaTransferInterface { +export class PolkadotKusamaTransfer implements PolkadotKusamaTransferInterface { readonly info: BridgeInfo readonly context: Context readonly route: TransferRoute @@ -134,7 +141,7 @@ export class KusamaTransfer implements KusamaTr return { sourceAssetHub: polkadotAssetHub, destAssetHub: kusamaAssetHub } } - async fee(tokenAddress: string): Promise { + async fee(tokenAddress: string, options?: TransferOptions): Promise { const { sourceAssetHub, destAssetHub } = await this.#connections() let baseFeeInStorage = await getStorageItem(sourceAssetHub, ":XcmBridgeHubRouterBaseFee:") let xcmBridgeBaseFee: bigint @@ -268,7 +275,16 @@ export class KusamaTransfer implements KusamaTr destinationFee = destinationFee + BigInt(minBalanceFeeDest) totalXcmBridgeFee = padFeeByPercentage(totalXcmBridgeFee, 33n) - let totalFee = totalXcmBridgeFee + bridgeHubDeliveryFee + destinationFee + // Optional service fee, normalising the recipient to a hex account id (accepts SS58 or hex). + const serviceFee = + options?.serviceFee && options.serviceFee.amount > 0n + ? { + amount: options.serviceFee.amount, + recipient: resolveBeneficiary(options.serviceFee.recipient).hexAddress, + } + : undefined + + const totalFee = totalXcmBridgeFee + bridgeHubDeliveryFee + destinationFee const sourceSymbol = this.#direction() === Direction.ToPolkadot ? "KSM" : "DOT" // destNativeSymbol is the asset coming OUT of the destination AH swap // (DOT for kusama→polkadot, KSM for polkadot→kusama). @@ -288,14 +304,18 @@ export class KusamaTransfer implements KusamaTr amount: destinationFeeInDestNative, symbol: destNativeSymbol, }) - const summary = [{ description: "Bridge fee", amount: totalFee, symbol: sourceSymbol }] + if (serviceFee) { + addBreakdown(breakdown, "serviceFee", { amount: serviceFee.amount, symbol: sourceSymbol }) + summary.push({ description: "Service fee", amount: serviceFee.amount, symbol: sourceSymbol }) + } return { kind: this.from.kind === "kusama" ? "kusama->polkadot" : "polkadot->kusama", breakdown, summary, totals: computeTotals(summary), + serviceFee, } } @@ -344,6 +364,8 @@ export class KusamaTransfer implements KusamaTr this.#direction(), tokenAddress, ) + const serviceFeeAmount = fee.serviceFee?.amount ?? 0n + const serviceFeeRecipient = fee.serviceFee?.recipient let tx if (this.#direction() == Direction.ToPolkadot) { tx = createERC20ToPolkadotTx( @@ -357,6 +379,8 @@ export class KusamaTransfer implements KusamaTr "destinationExecution", fee.kind === "kusama->polkadot" ? "KSM" : "DOT", ), + serviceFeeAmount, + serviceFeeRecipient, messageId, ) } else { @@ -371,6 +395,8 @@ export class KusamaTransfer implements KusamaTr "destinationExecution", fee.kind === "kusama->polkadot" ? "KSM" : "DOT", ), + serviceFeeAmount, + serviceFeeRecipient, messageId, ) } @@ -403,8 +429,9 @@ export class KusamaTransfer implements KusamaTr beneficiaryAccount: string, tokenAddress: string, amount: bigint, + options?: TransferOptions, ): Promise { - const fee = await this.fee(tokenAddress) + const fee = await this.fee(tokenAddress, options) const transfer = await this.tx(sourceAccount, beneficiaryAccount, tokenAddress, amount, fee) return ensureValidationSuccess(await this.validate(transfer)) } @@ -419,6 +446,7 @@ export class KusamaTransfer implements KusamaTr sourceAccountHex, sourceParachain: _source, beneficiaryAddressHex, + sourceAssetMetadata, destAssetMetadata, } = transfer.computed const { tx } = transfer @@ -452,6 +480,22 @@ export class KusamaTransfer implements KusamaTr }) } + // The transferred amount must exceed the destination token's minimum balance, else the + // deposit fails and the funds trap. resolveInputs labels metadata by chain (source = + // Polkadot AH, dest = Kusama AH), not by transfer direction, so pick the true destination. + const destTokenMinimumBalance = + this.#direction() === Direction.ToPolkadot + ? sourceAssetMetadata.minimumBalance + : destAssetMetadata.minimumBalance + if (amount <= destTokenMinimumBalance) { + logs.push({ + kind: ValidationKind.Error, + reason: ValidationReason.MinimumAmountValidation, + message: + "The amount transferred is at or below the minimum balance of the token on the destination chain.", + }) + } + let assetHubDryRunError const dryRunSource = await dryRunSourceAssetHub( @@ -501,6 +545,8 @@ export class KusamaTransfer implements KusamaTr transfer.input.amount, transfer.computed.beneficiaryAddressHex, "0x0000000000000000000000000000000000000000000000000000000000000000", + fee.serviceFee?.amount ?? 0n, + fee.serviceFee?.recipient, ) } else { destAssetHubXCM = buildPolkadotToKusamaDestAssetHubXCM( @@ -629,9 +675,24 @@ export class KusamaTransfer implements KusamaTr transfer.tx.signAndSend(account, options, (c) => { if (c.isError) { console.error(c) - reject(c.internalError || c.dispatchError || c) + if (c.internalError instanceof Error) { + reject(c.internalError) + } else if (c.dispatchError?.isModule) { + const decoded = sourceAssetHub.registry.findMetaError( + c.dispatchError.asModule, + ) + reject(new Error(`Transaction failed: ${decoded.section}.${decoded.name}`)) + } else if (c.dispatchError) { + reject(new Error(`Transaction failed: ${c.dispatchError.toString()}`)) + } else { + reject(new Error(`Transaction failed with status: ${c.status.type}`)) + } + return } - if (c.isFinalized) { + // messageId is computed off-chain (SetTopic we control), so re-orgs can't + // change it, resolve on isInBlock to avoid the finalization wait. + const resolveOnInBlock = transfer.computed.messageId !== undefined + if ((resolveOnInBlock && c.isInBlock) || c.isFinalized) { const result = { txHash: u8aToHex(c.txHash), txIndex: c.txIndex || 0, @@ -680,6 +741,8 @@ function createERC20ToKusamaTx( beneficiaryAccount: string, amount: bigint, destFeeInSourceNative: bigint, + serviceFee: bigint, + serviceFeeRecipient: string | undefined, topic: string, ): SubmittableExtrinsic<"promise", ISubmittableResult> { let assets: any @@ -722,7 +785,7 @@ function createERC20ToKusamaTx( beneficiaryAccount, topic, ) - return parachain.tx.polkadotXcm.transferAssetsUsingTypeAndThen( + const transfer = parachain.tx.polkadotXcm.transferAssetsUsingTypeAndThen( destination, assets, reserveTypeAsset, @@ -731,6 +794,15 @@ function createERC20ToKusamaTx( customXcm, "Unlimited", ) + if (serviceFee <= 0n || !serviceFeeRecipient) { + return transfer + } + // Pay the service fee as a local DOT transfer on Polkadot AH, atomic with the bridge transfer. + const serviceFeeTransfer = parachain.tx.balances.transferKeepAlive( + serviceFeeRecipient, + serviceFee, + ) + return parachain.tx.utility.batchAll([serviceFeeTransfer, transfer]) } function createERC20ToPolkadotTx( @@ -740,6 +812,8 @@ function createERC20ToPolkadotTx( beneficiaryAccount: string, amount: bigint, destFeeInSourceNative: bigint, + serviceFee: bigint, + serviceFeeRecipient: string | undefined, topic: string, ): SubmittableExtrinsic<"promise", ISubmittableResult> { let assets: any @@ -750,7 +824,7 @@ function createERC20ToPolkadotTx( v4: [ { id: NATIVE_TOKEN_LOCATION, - fun: { Fungible: destFeeInSourceNative + amount }, + fun: { Fungible: destFeeInSourceNative + amount + serviceFee }, }, ], } @@ -760,7 +834,7 @@ function createERC20ToPolkadotTx( v4: [ { id: NATIVE_TOKEN_LOCATION, - fun: { Fungible: destFeeInSourceNative }, + fun: { Fungible: destFeeInSourceNative + serviceFee }, }, { id: tokenLocation, @@ -779,6 +853,8 @@ function createERC20ToPolkadotTx( parachain.registry, beneficiaryAccount, topic, + serviceFee, + serviceFeeRecipient, ) return parachain.tx.polkadotXcm.transferAssetsUsingTypeAndThen( destination, @@ -852,7 +928,7 @@ async function dryRunSourceAssetHub( } } -async function dryRunDestAssetHub(assetHub: ApiPromise, parachainId: number, xcm: any) { +export async function dryRunDestAssetHub(assetHub: ApiPromise, parachainId: number, xcm: any) { const sourceParachain = { v4: { parents: 1, interior: { x1: [{ parachain: parachainId }] } } } const result = await assetHub.call.dryRunApi.dryRunXcm< Result diff --git a/web/packages/api/src/transfers/forKusama/transferInterface.ts b/web/packages/api/src/transfers/polkadotKusama/transferInterface.ts similarity index 84% rename from web/packages/api/src/transfers/forKusama/transferInterface.ts rename to web/packages/api/src/transfers/polkadotKusama/transferInterface.ts index 82ceeabd34..dd02d95098 100644 --- a/web/packages/api/src/transfers/forKusama/transferInterface.ts +++ b/web/packages/api/src/transfers/polkadotKusama/transferInterface.ts @@ -5,13 +5,14 @@ import type { DeliveryFee, MessageReceipt, Transfer, + TransferOptions, ValidatedTransfer, -} from "../../types/forKusama" +} from "../../types/polkadotKusama" export interface TransferInterface { readonly context: Context - fee(tokenAddress: string): Promise + fee(tokenAddress: string, options?: TransferOptions): Promise tx( sourceAccount: string, @@ -28,6 +29,7 @@ export interface TransferInterface { beneficiaryAccount: string, tokenAddress: string, amount: bigint, + options?: TransferOptions, ): Promise signAndSend( diff --git a/web/packages/api/src/types/fee.ts b/web/packages/api/src/types/fee.ts index a1c4fc35b3..53c74a9ecd 100644 --- a/web/packages/api/src/types/fee.ts +++ b/web/packages/api/src/types/fee.ts @@ -35,7 +35,11 @@ export type ToEthereumFeeKey = export type InterParachainFeeKey = "assetHubDelivery" | "destinationExecution" // kusama ↔ polkadot -export type KusamaFeeKey = "xcmBridge" | "bridgeHubDelivery" | "destinationExecution" +export type KusamaFeeKey = + | "xcmBridge" + | "bridgeHubDelivery" + | "destinationExecution" + | "serviceFee" // v1 ethereum → polkadot (legacy adapter) export type V1ToPolkadotFeeKey = "destinationDelivery" | "destinationExecution" diff --git a/web/packages/api/src/types/forKusama.ts b/web/packages/api/src/types/polkadotKusama.ts similarity index 83% rename from web/packages/api/src/types/forKusama.ts rename to web/packages/api/src/types/polkadotKusama.ts index 32ee375108..d837c9ba07 100644 --- a/web/packages/api/src/types/forKusama.ts +++ b/web/packages/api/src/types/polkadotKusama.ts @@ -26,11 +26,23 @@ export type Transfer = { tx: SubmittableExtrinsic<"promise", ISubmittableResult> } +// Optional service fee deposited on Polkadot AH. `amount` is in the source chain's native asset +// (DOT for polkadot->kusama, KSM for kusama->polkadot). +export type ServiceFeeConfig = { + recipient: string + amount: bigint +} + +export type TransferOptions = { + serviceFee?: ServiceFeeConfig +} + export type DeliveryFee = { kind: "kusama->polkadot" | "polkadot->kusama" breakdown: { [P in KusamaFeeKey]?: FeeAsset[] } summary: FeeItem[] totals: FeeAsset[] + serviceFee?: ServiceFeeConfig } export enum ValidationKind { @@ -45,6 +57,7 @@ export enum ValidationReason { MaxConsumersReached, AccountDoesNotExist, InsufficientPoolReserves, + MinimumAmountValidation, } export type ValidationLog = { diff --git a/web/packages/api/src/xcmBuilder.ts b/web/packages/api/src/xcmBuilder.ts index 8836cf582c..6d55f58b0a 100644 --- a/web/packages/api/src/xcmBuilder.ts +++ b/web/packages/api/src/xcmBuilder.ts @@ -397,25 +397,47 @@ export function buildParachainERC20ReceivedXcmOnAssetHub( }) } -function buildAssetHubXcmFromParachainKusama(beneficiary: string, topic: string) { - return [ - { +function buildAssetHubXcmFromParachainKusama( + beneficiary: string, + topic: string, + // When set, skim this KSM amount to feeRecipient before depositing the remainder to the beneficiary. + serviceFeeKsm?: bigint, + feeRecipient?: string, +) { + const instructions: any[] = [] + if (serviceFeeKsm && serviceFeeKsm > 0n && feeRecipient) { + instructions.push({ depositAsset: { assets: { - Wild: { - AllCounted: 2, - }, + Definite: [ + { + id: ksmLocationOnPolkadotAssetHub, + fun: { Fungible: serviceFeeKsm }, + }, + ], }, beneficiary: { parents: 0, - interior: { x1: [{ AccountId32: { id: beneficiary } }] }, + interior: { x1: [{ AccountId32: { id: feeRecipient } }] }, }, }, + }) + } + instructions.push({ + depositAsset: { + assets: { + Wild: { + AllCounted: 2, + }, + }, + beneficiary: { + parents: 0, + interior: { x1: [{ AccountId32: { id: beneficiary } }] }, + }, }, - { - setTopic: topic, - }, - ] + }) + instructions.push({ setTopic: topic }) + return instructions } function buildAssetHubXcmFromParachain( @@ -531,9 +553,16 @@ export function buildAssetHubERC20TransferToKusama( registry: Registry, beneficiary: string, topic: string, + serviceFeeKsm?: bigint, + feeRecipient?: string, ) { return registry.createType("XcmVersionedXcm", { - v4: buildAssetHubXcmFromParachainKusama(beneficiary, topic), + v4: buildAssetHubXcmFromParachainKusama( + beneficiary, + topic, + serviceFeeKsm, + feeRecipient, + ), }) } diff --git a/web/packages/api/src/xcmBuilderKusama.ts b/web/packages/api/src/xcmBuilderKusama.ts index acb2acd0f7..216c3b186e 100644 --- a/web/packages/api/src/xcmBuilderKusama.ts +++ b/web/packages/api/src/xcmBuilderKusama.ts @@ -241,13 +241,16 @@ export function buildKusamaToPolkadotDestAssetHubXCM( transferAmount: bigint, beneficiary: string, topic: string, + // When set, skim this KSM amount to feeRecipient before the beneficiary deposit. + serviceFeeKsm: bigint = 0n, + feeRecipient?: string, ) { let withdrawAssets: any[] = [] let reserveAssetsDeposited = [ { id: ksmLocationOnPolkadotAssetHub, fun: { - Fungible: totalFeeInNative, + Fungible: totalFeeInNative + serviceFeeKsm, }, }, ] @@ -306,6 +309,23 @@ export function buildKusamaToPolkadotDestAssetHubXCM( withdrawAsset: withdrawAssets, }, { clearOrigin: null }, + ...(serviceFeeKsm > 0n && feeRecipient + ? [ + { + depositAsset: { + assets: { + Definite: [ + { + id: ksmLocationOnPolkadotAssetHub, + fun: { Fungible: serviceFeeKsm }, + }, + ], + }, + beneficiary: accountId32Location(feeRecipient), + }, + }, + ] + : []), { depositAsset: { assets: { diff --git a/web/packages/operations/package.json b/web/packages/operations/package.json index 87957f7ae4..def3f13268 100644 --- a/web/packages/operations/package.json +++ b/web/packages/operations/package.json @@ -19,16 +19,16 @@ "historyV2": "npx ts-node src/global_transfer_history_v2.ts", "buildTokenRegistry": "npx ts-node src/build_asset_registry.ts", "generateReferencePreimages": "npx ts-node src/generate_reference_preimages.ts", - "transferWethFromPolkadotToKusama": "npx ts-node src/transfer_for_kusama_entry.ts WEthPToK 0 WETH 200000000000000", - "transferEthFromPolkadotToKusama": "npx ts-node src/transfer_for_kusama_entry.ts EthPToK 0 ETH 200000000000000", - "transferDotFromPolkadotToKusama": "npx ts-node src/transfer_for_kusama_entry.ts DotPToK 0 DOT 200000000000000", - "transferKsmFromPolkadotToKusama": "npx ts-node src/transfer_for_kusama_entry.ts KsmPToK 0 KSM 200000000000000", - "transferWethFromKusamaToPolkadot": "npx ts-node src/transfer_for_kusama_entry.ts WEthKToP 1 WETH 200000000000000", - "transferEthFromKusamaToPolkadot": "npx ts-node src/transfer_for_kusama_entry.ts EthKToP 1 ETH 200000000000000", - "transferDotFromKusamaToPolkadot": "npx ts-node src/transfer_for_kusama_entry.ts DotKToP 1 DOT 200000000000000", - "transferKsmFromKusamaToPolkadot": "npx ts-node src/transfer_for_kusama_entry.ts KsmKToP 1 KSM 200000000000000", - "transferWudFromPolkadotToKusama": "npx ts-node src/transfer_for_kusama_entry.ts WudPToK 0 WUD 200000000000000", - "transferWudFromKusamaToPolkadot": "npx ts-node src/transfer_for_kusama_entry.ts WudKToP 1 WUD 200000000000000", + "transferWethFromPolkadotToKusama": "npx ts-node src/transfer_polkadot_kusama_entry.ts WEthPToK 0 WETH 200000000000000", + "transferEthFromPolkadotToKusama": "npx ts-node src/transfer_polkadot_kusama_entry.ts EthPToK 0 ETH 200000000000000", + "transferDotFromPolkadotToKusama": "npx ts-node src/transfer_polkadot_kusama_entry.ts DotPToK 0 DOT 200000000000000", + "transferKsmFromPolkadotToKusama": "npx ts-node src/transfer_polkadot_kusama_entry.ts KsmPToK 0 KSM 200000000000000", + "transferWethFromKusamaToPolkadot": "npx ts-node src/transfer_polkadot_kusama_entry.ts WEthKToP 1 WETH 200000000000000", + "transferEthFromKusamaToPolkadot": "npx ts-node src/transfer_polkadot_kusama_entry.ts EthKToP 1 ETH 200000000000000", + "transferDotFromKusamaToPolkadot": "npx ts-node src/transfer_polkadot_kusama_entry.ts DotKToP 1 DOT 200000000000000", + "transferKsmFromKusamaToPolkadot": "npx ts-node src/transfer_polkadot_kusama_entry.ts KsmKToP 1 KSM 200000000000000", + "transferWudFromPolkadotToKusama": "npx ts-node src/transfer_polkadot_kusama_entry.ts WudPToK 0 WUD 200000000000000", + "transferWudFromKusamaToPolkadot": "npx ts-node src/transfer_polkadot_kusama_entry.ts WudKToP 1 WUD 200000000000000", "transferEtherToAH": "npx ts-node src/transfer_from_e2p.ts 1000 Eth 200000000000000", "transferEtherFromAH": "npx ts-node src/transfer_from_p2e.ts 1000 Eth 10000000000", "transferEtherFromHydration": "npx ts-node src/transfer_from_p2e.ts 2034 Eth 10000000000", diff --git a/web/packages/operations/src/transfer_for_kusama.ts b/web/packages/operations/src/transfer_polkadot_kusama.ts similarity index 69% rename from web/packages/operations/src/transfer_for_kusama.ts rename to web/packages/operations/src/transfer_polkadot_kusama.ts index 7ad39084ac..21fdc1250b 100644 --- a/web/packages/operations/src/transfer_for_kusama.ts +++ b/web/packages/operations/src/transfer_polkadot_kusama.ts @@ -2,10 +2,10 @@ import "dotenv/config" import { Keyring } from "@polkadot/keyring" import { createApi } from "@snowbridge/api" import { EthersEthereumProvider } from "@snowbridge/provider-ethers" -import { Direction } from "@snowbridge/api/dist/forKusama" +import { Direction } from "@snowbridge/api/dist/polkadotKusama" import { bridgeInfoFor } from "@snowbridge/registry" -export const transferForKusama = async ( +export const transferPolkadotKusama = async ( transferName: string, direction: Direction, amount: bigint, @@ -26,12 +26,11 @@ export const transferForKusama = async ( const polkadot_keyring = new Keyring({ type: "sr25519" }) - const SOURCE_ACCOUNT = process.env["SOURCE_SUBSTRATE_KEY"] - ? polkadot_keyring.addFromUri(process.env["SOURCE_SUBSTRATE_KEY"]) - : polkadot_keyring.addFromUri("//Ferdie") - const DEST_ACCOUNT = process.env["DEST_SUBSTRATE_KEY"] - ? polkadot_keyring.addFromUri(process.env["DEST_SUBSTRATE_KEY"]) - : polkadot_keyring.addFromUri("//Ferdie") + const sourceUri = + process.env["SOURCE_SUBSTRATE_KEY"] ?? process.env["KUSAMA_SUBSTRATE_KEY"] ?? "//Ferdie" + const destUri = process.env["DEST_SUBSTRATE_KEY"] ?? sourceUri + const SOURCE_ACCOUNT = polkadot_keyring.addFromUri(sourceUri) + const DEST_ACCOUNT = polkadot_keyring.addFromUri(destUri) const SOURCE_ACCOUNT_PUBLIC = SOURCE_ACCOUNT.address const DEST_ACCOUNT_PUBLIC = DEST_ACCOUNT.address @@ -76,8 +75,20 @@ export const transferForKusama = async ( { kind: "kusama", id: registry.kusama!.assetHubParaId }, ) - // Step 1. Get the delivery fee for the transaction - const fee = await transferImpl.fee(tokenAddress) + // Step 1. Get the delivery fee. Optional service fee via SERVICE_FEE_RECIPIENT + + // SERVICE_FEE_AMOUNT (source native base units: DOT for p->k, KSM for k->p). + const serviceFeeRecipient = process.env["SERVICE_FEE_RECIPIENT"] + const serviceFeeAmount = process.env["SERVICE_FEE_AMOUNT"] + const options = + serviceFeeRecipient && serviceFeeAmount + ? { + serviceFee: { + recipient: serviceFeeRecipient, + amount: BigInt(serviceFeeAmount), + }, + } + : undefined + const fee = await transferImpl.fee(tokenAddress, options) // Step 2. Create a transfer tx const transfer = await transferImpl.tx( @@ -97,6 +108,15 @@ export const transferForKusama = async ( throw Error(`validation has one of more errors.`) } + console.log("fee:", fee) + console.log("validation:", validation) + + if (process.env["DRY_RUN"] == "true") { + console.log("DRY_RUN=true: validation passed, skipping signAndSend") + await context.destroyContext() + return + } + // Step 5. Submit transaction and get receipt for tracking const response = await transferImpl.signAndSend(transfer, SOURCE_ACCOUNT, { withSignedTransaction: true, diff --git a/web/packages/operations/src/transfer_for_kusama_entry.ts b/web/packages/operations/src/transfer_polkadot_kusama_entry.ts similarity index 73% rename from web/packages/operations/src/transfer_for_kusama_entry.ts rename to web/packages/operations/src/transfer_polkadot_kusama_entry.ts index 8ea541ee08..3cc8f2d989 100644 --- a/web/packages/operations/src/transfer_for_kusama_entry.ts +++ b/web/packages/operations/src/transfer_polkadot_kusama_entry.ts @@ -1,6 +1,6 @@ import "dotenv/config" -import { Direction } from "@snowbridge/api/dist/forKusama" -import { transferForKusama } from "./transfer_for_kusama" +import { Direction } from "@snowbridge/api/dist/polkadotKusama" +import { transferPolkadotKusama } from "./transfer_polkadot_kusama" const transfer = async ( transferName: string, @@ -9,7 +9,7 @@ const transfer = async ( amount: bigint, ) => { const directionEnum = direction === 1 ? Direction.ToPolkadot : Direction.ToKusama - await transferForKusama(transferName, directionEnum, amount, symbol) + await transferPolkadotKusama(transferName, directionEnum, amount, symbol) } if (process.argv.length != 6) { diff --git a/web/packages/registry/scripts/buildRegistry.ts b/web/packages/registry/scripts/buildRegistry.ts index af90ba0394..915378c974 100644 --- a/web/packages/registry/scripts/buildRegistry.ts +++ b/web/packages/registry/scripts/buildRegistry.ts @@ -460,6 +460,36 @@ function buildTransferLocations( } } + // Polkadot AH ↔ Kusama AH paths (assets common to both). Only the Kusama Asset Hub is a + // supported destination; the transfer impl always resolves to kusama.assetHubParaId. + const kusamaAssetHub = + registry.kusama?.parachains[`kusama_${registry.kusama.assetHubParaId}`] + if (kusamaAssetHub) { + const assetHubAssets = Object.keys(assetHub.assets) + const kusamaAssets = Object.keys(kusamaAssetHub.assets) + const pahKahCommon = new Set( + assetHubAssets.filter((pa) => kusamaAssets.find((ka) => ka === pa)), + ) + for (const asset of pahKahCommon) { + const p1: Path = { + source: { kind: assetHub.kind, id: assetHub.id }, + destination: { kind: kusamaAssetHub.kind, id: kusamaAssetHub.id }, + asset, + } + if (pathFilter(p1)) { + locations.push(p1) + } + const p2: Path = { + source: p1.destination, + destination: p1.source, + asset, + } + if (pathFilter(p2)) { + locations.push(p2) + } + } + } + // L2 paths if (environment.l2Bridge) { const assetHubAssets = Object.keys(assetHub.assets) diff --git a/web/packages/registry/src/polkadot_mainnet_bridge_info.g.ts b/web/packages/registry/src/polkadot_mainnet_bridge_info.g.ts index 5ab53e1732..d3a93d0b05 100644 --- a/web/packages/registry/src/polkadot_mainnet_bridge_info.g.ts +++ b/web/packages/registry/src/polkadot_mainnet_bridge_info.g.ts @@ -460,6 +460,80 @@ const registry = { "0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9", ], }, + { + from: { + kind: "polkadot", + id: 1000, + }, + to: { + kind: "kusama", + id: 1000, + }, + assets: [ + "0x9d39a5de30e57443bff2a8307a4256c8797a3497", + "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "0x6982508145454ce325ddbe47a25d4ec3d2311933", + "0x5a98fcbea516cf06857215779fd812ca3bef1b32", + "0xa3931d71877c0e7a3148cb7eb4463524fec27fbd", + "0x8236a87084f8b84306f72007f36f2618a5634494", + "0x1abaea1f7c830bd89acc67ec4af516284b1bc33c", + "0x56072c95faa701256059aa122697b133aded9279", + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "0x0e186357c323c806c1efdad36d217f7a54b63d18", + "0x18084fba666a33d37592fa2633fd49a74dd93a88", + "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0", + "0x582d872a1b094fc48f5de31d3b73f2d9be47def1", + "0x6b175474e89094c44da98b954eedeac495271d0f", + "0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce", + "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", + "0x8daebade922df735c38c80c7ebd708af50815faa", + "0x0000000000000000000000000000000000000000", + "0x5d3d01fd6d2ad1169b17918eb4f153c6616288eb", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0x514910771af9ca656af840dff83e8264ecf986ca", + "0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9", + "0x196c20da81fbc324ecdf55501e95ce9f0bd84d14", + "0x12bbfdc9e813614eef8dc8a2560b0efbeaf7c2ab", + "0x5fdcd48f09fb67de3d202cd854b372aec1100ed5", + ], + }, + { + from: { + kind: "kusama", + id: 1000, + }, + to: { + kind: "polkadot", + id: 1000, + }, + assets: [ + "0x9d39a5de30e57443bff2a8307a4256c8797a3497", + "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "0x6982508145454ce325ddbe47a25d4ec3d2311933", + "0x5a98fcbea516cf06857215779fd812ca3bef1b32", + "0xa3931d71877c0e7a3148cb7eb4463524fec27fbd", + "0x8236a87084f8b84306f72007f36f2618a5634494", + "0x1abaea1f7c830bd89acc67ec4af516284b1bc33c", + "0x56072c95faa701256059aa122697b133aded9279", + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "0x0e186357c323c806c1efdad36d217f7a54b63d18", + "0x18084fba666a33d37592fa2633fd49a74dd93a88", + "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0", + "0x582d872a1b094fc48f5de31d3b73f2d9be47def1", + "0x6b175474e89094c44da98b954eedeac495271d0f", + "0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce", + "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", + "0x8daebade922df735c38c80c7ebd708af50815faa", + "0x0000000000000000000000000000000000000000", + "0x5d3d01fd6d2ad1169b17918eb4f153c6616288eb", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0x514910771af9ca656af840dff83e8264ecf986ca", + "0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9", + "0x196c20da81fbc324ecdf55501e95ce9f0bd84d14", + "0x12bbfdc9e813614eef8dc8a2560b0efbeaf7c2ab", + "0x5fdcd48f09fb67de3d202cd854b372aec1100ed5", + ], + }, { from: { kind: "polkadot", @@ -636,7 +710,7 @@ const registry = { }, ], registry: { - timestamp: "2026-06-28T06:07:00.628Z", + timestamp: "2026-07-24T10:32:42.264Z", environment: "polkadot_mainnet", ethChainId: 1, gatewayAddress: "0x27ca963c279c93801941e1eb8799c23f407d68e7", @@ -1906,7 +1980,7 @@ const registry = { evmChainId: 1284, name: "Moonbeam", specName: "moonbeam", - specVersion: 4303, + specVersion: 4401, }, xcDOT: "0xffffffff1fcacbd218edc0eba20fc2308c778080", assets: { @@ -1974,7 +2048,7 @@ const registry = { xc20: "0xffffffff7bc304425217b49e9598415c514ae81b", }, }, - estimatedExecutionFeeDOT: 49192809n, + estimatedExecutionFeeDOT: 49271805n, estimatedDeliveryFeeDOT: 306500000n, }, polkadot_2030: { @@ -2002,7 +2076,7 @@ const registry = { evmChainId: 996, name: "Bifrost Polkadot", specName: "bifrost_polkadot", - specVersion: 25000, + specVersion: 25002, }, assets: { "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2": { @@ -2174,7 +2248,7 @@ const registry = { isSufficient: true, }, }, - estimatedExecutionFeeDOT: 3618870n, + estimatedExecutionFeeDOT: 5606528n, estimatedDeliveryFeeDOT: 307100000n, }, polkadot_2043: {