Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions web/packages/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -419,8 +419,8 @@ export type ApiOptions<P extends EthereumProvider<any>> = {

export type TransferImplementation<T extends EthereumProviderTypes = EthereumProviderTypes> =
| ({ kind: "polkadot->polkadot" } & ForInterParachainTransferInterface<T>)
| ({ kind: "kusama->polkadot" } & ForKusamaTransferInterface<T>)
| ({ kind: "polkadot->kusama" } & ForKusamaTransferInterface<T>)
| ({ kind: "kusama->polkadot" } & PolkadotKusamaTransferInterface<T>)
| ({ kind: "polkadot->kusama" } & PolkadotKusamaTransferInterface<T>)
| ({ kind: "polkadot->ethereum" } & ToEthereumTransferInterface<T>)
| ({ kind: "ethereum->polkadot" } & ToPolkadotTransferInterface<T>)
| ({ kind: "ethereum->ethereum" } & ToEthereumEvmTransferInterface<T>)
Expand Down Expand Up @@ -564,9 +564,9 @@ export class SnowbridgeApi<P extends EthereumProvider<any>> {
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -90,7 +97,7 @@ function resolveInputs(
return { sourceAssetMetadata, destAssetMetadata, sourceParachain }
}

export class KusamaTransfer<T extends EthereumProviderTypes> implements KusamaTransferInterface<T> {
export class PolkadotKusamaTransfer<T extends EthereumProviderTypes> implements PolkadotKusamaTransferInterface<T> {
readonly info: BridgeInfo
readonly context: Context<T>
readonly route: TransferRoute
Expand Down Expand Up @@ -134,7 +141,7 @@ export class KusamaTransfer<T extends EthereumProviderTypes> implements KusamaTr
return { sourceAssetHub: polkadotAssetHub, destAssetHub: kusamaAssetHub }
}

async fee(tokenAddress: string): Promise<DeliveryFee> {
async fee(tokenAddress: string, options?: TransferOptions): Promise<DeliveryFee> {
const { sourceAssetHub, destAssetHub } = await this.#connections()
let baseFeeInStorage = await getStorageItem(sourceAssetHub, ":XcmBridgeHubRouterBaseFee:")
let xcmBridgeBaseFee: bigint
Expand Down Expand Up @@ -268,7 +275,16 @@ export class KusamaTransfer<T extends EthereumProviderTypes> 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).
Expand All @@ -288,14 +304,18 @@ export class KusamaTransfer<T extends EthereumProviderTypes> 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,
}
}

Expand Down Expand Up @@ -344,6 +364,8 @@ export class KusamaTransfer<T extends EthereumProviderTypes> 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(
Expand All @@ -357,6 +379,8 @@ export class KusamaTransfer<T extends EthereumProviderTypes> implements KusamaTr
"destinationExecution",
fee.kind === "kusama->polkadot" ? "KSM" : "DOT",
),
serviceFeeAmount,
serviceFeeRecipient,
messageId,
)
} else {
Expand All @@ -371,6 +395,8 @@ export class KusamaTransfer<T extends EthereumProviderTypes> implements KusamaTr
"destinationExecution",
fee.kind === "kusama->polkadot" ? "KSM" : "DOT",
),
serviceFeeAmount,
serviceFeeRecipient,
messageId,
)
}
Expand Down Expand Up @@ -403,8 +429,9 @@ export class KusamaTransfer<T extends EthereumProviderTypes> implements KusamaTr
beneficiaryAccount: string,
tokenAddress: string,
amount: bigint,
options?: TransferOptions,
): Promise<ValidatedTransfer> {
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))
}
Expand All @@ -419,6 +446,7 @@ export class KusamaTransfer<T extends EthereumProviderTypes> implements KusamaTr
sourceAccountHex,
sourceParachain: _source,
beneficiaryAddressHex,
sourceAssetMetadata,
destAssetMetadata,
} = transfer.computed
const { tx } = transfer
Expand Down Expand Up @@ -452,6 +480,22 @@ export class KusamaTransfer<T extends EthereumProviderTypes> 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(
Expand Down Expand Up @@ -501,6 +545,8 @@ export class KusamaTransfer<T extends EthereumProviderTypes> implements KusamaTr
transfer.input.amount,
transfer.computed.beneficiaryAddressHex,
"0x0000000000000000000000000000000000000000000000000000000000000000",
fee.serviceFee?.amount ?? 0n,
fee.serviceFee?.recipient,
)
} else {
destAssetHubXCM = buildPolkadotToKusamaDestAssetHubXCM(
Expand Down Expand Up @@ -629,9 +675,24 @@ export class KusamaTransfer<T extends EthereumProviderTypes> 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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -722,7 +785,7 @@ function createERC20ToKusamaTx(
beneficiaryAccount,
topic,
)
return parachain.tx.polkadotXcm.transferAssetsUsingTypeAndThen(
const transfer = parachain.tx.polkadotXcm.transferAssetsUsingTypeAndThen(
destination,
assets,
reserveTypeAsset,
Expand All @@ -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(
Expand All @@ -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
Expand All @@ -750,7 +824,7 @@ function createERC20ToPolkadotTx(
v4: [
{
id: NATIVE_TOKEN_LOCATION,
fun: { Fungible: destFeeInSourceNative + amount },
fun: { Fungible: destFeeInSourceNative + amount + serviceFee },
},
],
}
Expand All @@ -760,7 +834,7 @@ function createERC20ToPolkadotTx(
v4: [
{
id: NATIVE_TOKEN_LOCATION,
fun: { Fungible: destFeeInSourceNative },
fun: { Fungible: destFeeInSourceNative + serviceFee },
},
{
id: tokenLocation,
Expand All @@ -779,6 +853,8 @@ function createERC20ToPolkadotTx(
parachain.registry,
beneficiaryAccount,
topic,
serviceFee,
serviceFeeRecipient,
)
return parachain.tx.polkadotXcm.transferAssetsUsingTypeAndThen(
destination,
Expand Down Expand Up @@ -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<XcmDryRunEffects, XcmDryRunApiError>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import type {
DeliveryFee,
MessageReceipt,
Transfer,
TransferOptions,
ValidatedTransfer,
} from "../../types/forKusama"
} from "../../types/polkadotKusama"

export interface TransferInterface<T extends EthereumProviderTypes> {
readonly context: Context<T>

fee(tokenAddress: string): Promise<DeliveryFee>
fee(tokenAddress: string, options?: TransferOptions): Promise<DeliveryFee>

tx(
sourceAccount: string,
Expand All @@ -28,6 +29,7 @@ export interface TransferInterface<T extends EthereumProviderTypes> {
beneficiaryAccount: string,
tokenAddress: string,
amount: bigint,
options?: TransferOptions,
): Promise<ValidatedTransfer>

signAndSend(
Expand Down
6 changes: 5 additions & 1 deletion web/packages/api/src/types/fee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -45,6 +57,7 @@ export enum ValidationReason {
MaxConsumersReached,
AccountDoesNotExist,
InsufficientPoolReserves,
MinimumAmountValidation,
}

export type ValidationLog = {
Expand Down
Loading
Loading