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: {