Skip to content
Open
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
45 changes: 45 additions & 0 deletions web/packages/api/src/feeSchedule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
type FeeBand = {
lowerUsd: bigint
upperUsd: bigint
numerator: bigint
denominator: bigint
}

const MAX_USD = 999_999_999_999n

const FEE_SCHEDULE: FeeBand[] = [
{ lowerUsd: 0n, upperUsd: 100n, numerator: 16n, denominator: 10_000n },
{ lowerUsd: 100n, upperUsd: 1_000n, numerator: 14n, denominator: 10_000n },
{ lowerUsd: 1_000n, upperUsd: 10_000n, numerator: 12n, denominator: 10_000n },
{ lowerUsd: 10_000n, upperUsd: 100_000n, numerator: 10n, denominator: 10_000n },
{ lowerUsd: 100_000n, upperUsd: 1_000_000n, numerator: 8n, denominator: 10_000n },
{ lowerUsd: 1_000_000n, upperUsd: MAX_USD, numerator: 6n, denominator: 10_000n },
]

export type VolumeFeeParams = {
txValueUsd: bigint
ethToUsdNumerator: bigint
ethToUsdDenominator: bigint
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we planning to leave this to the UI to provide it, or should it be calculated internally in the SDK, using a price oracle?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Planing to leave this to the UI to do. Because I dont want to rely on apis/keys here.


export function lookupFeeRatio(txValueUsd: bigint): { numerator: bigint; denominator: bigint } {
for (const band of FEE_SCHEDULE) {
if (txValueUsd >= band.lowerUsd && txValueUsd < band.upperUsd) {
return { numerator: band.numerator, denominator: band.denominator }
}
}
return { numerator: 6n, denominator: 10_000n }
}

export function calculateVolumeTipInWei(params: VolumeFeeParams): bigint {
if (params.txValueUsd < 0n) throw new Error("txValueUsd must be >= 0")
if (params.ethToUsdNumerator <= 0n || params.ethToUsdDenominator <= 0n) {
throw new Error("ethToUsdNumerator and ethToUsdDenominator must be > 0")
}
const { numerator: feeNum, denominator: feeDen } = lookupFeeRatio(params.txValueUsd)
const WEI = 1_000_000_000_000_000_000n
return (
(params.txValueUsd * feeNum * WEI * params.ethToUsdDenominator) /
(feeDen * params.ethToUsdNumerator)
)
}
34 changes: 34 additions & 0 deletions web/packages/api/src/fees.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { DeliveryFee, FeeAsset, FeeItem } from "./types/fee"

export function addBreakdown<K extends string>(
breakdown: DeliveryFee<K>["breakdown"],
key: K,
asset: FeeAsset,
): void {
if (asset.amount === 0n) return
const bucket = (breakdown as Record<string, FeeAsset[]>)[key] ?? []
;(breakdown as Record<string, FeeAsset[]>)[key] = bucket
bucket.push(asset)
}

export function computeTotals(summary: FeeItem[]): FeeAsset[] {
const totals = new Map<string, bigint>()
for (const item of summary) {
totals.set(item.symbol, (totals.get(item.symbol) ?? 0n) + item.amount)
}
return [...totals.entries()].map(([symbol, amount]) => ({ symbol, amount }))
}

export function findInBreakdown<K extends string>(
breakdown: DeliveryFee<K>["breakdown"],
key: K,
symbol: string,
): bigint {
return ((breakdown as Record<string, FeeAsset[]>)[key] ?? [])
.filter((a) => a.symbol === symbol)
.reduce((acc, a) => acc + a.amount, 0n)
}

export function findTotal<K extends string>(fee: DeliveryFee<K>, symbol: string): bigint {
return fee.totals.find((t) => t.symbol === symbol)?.amount ?? 0n
}
14 changes: 13 additions & 1 deletion web/packages/api/src/forInterParachain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
TransferRoute,
} from "@snowbridge/base-types"
import { ensureValidationSuccess, padFeeByPercentage } from "./utils"
import { addBreakdown, computeTotals } from "./fees"
import { resolveBeneficiary } from "./crypto"
import { Context } from "."
import { buildMessageId } from "./toEthereum_v2"
Expand Down Expand Up @@ -152,11 +153,22 @@ export class InterParachainTransfer<T extends EthereumProviderTypes>
options?.padFeeByPercentage ?? 33n,
)

const totalFeeInDot = deliveryFee + executionFee

const breakdown: DeliveryFee["breakdown"] = {}
addBreakdown(breakdown, "assetHubDelivery", { amount: deliveryFee, symbol: "DOT" })
addBreakdown(breakdown, "destinationExecution", { amount: executionFee, symbol: "DOT" })

const summary = [{ description: "Transfer fee", amount: totalFeeInDot, symbol: "DOT" }]

return {
kind: "polkadot->polkadot",
deliveryFee,
executionFee,
totalFeeInDot: deliveryFee + executionFee,
totalFeeInDot,
breakdown,
summary,
totals: computeTotals(summary),
}
}

Expand Down
18 changes: 18 additions & 0 deletions web/packages/api/src/forKusama.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
import { CallDryRunEffects, XcmDryRunApiError, XcmDryRunEffects } from "@polkadot/types/interfaces"
import { Result } from "@polkadot/types"
import { ensureValidationSuccess, padFeeByPercentage, u32ToLeBytes } from "./utils"
import { addBreakdown, computeTotals } from "./fees"
import { resolveBeneficiary } from "./crypto"
import { TransferInterface as KusamaTransferInterface } from "./transfers/forKusama/transferInterface"
import { Context } from "."
Expand Down Expand Up @@ -264,13 +265,30 @@ export class KusamaTransfer<T extends EthereumProviderTypes> implements KusamaTr
totalXcmBridgeFee = padFeeByPercentage(totalXcmBridgeFee, 33n)

let totalFee = totalXcmBridgeFee + bridgeHubDeliveryFee + destinationFee
const sourceSymbol = this.#direction() === Direction.ToPolkadot ? "KSM" : "DOT"

const breakdown: DeliveryFee["breakdown"] = {}
addBreakdown(breakdown, "xcmBridge", { amount: totalXcmBridgeFee, symbol: sourceSymbol })
addBreakdown(breakdown, "bridgeHubDelivery", {
amount: bridgeHubDeliveryFee,
symbol: sourceSymbol,
})
addBreakdown(breakdown, "destinationExecution", {
amount: destinationFee,
symbol: sourceSymbol,
})

const summary = [{ description: "Bridge fee", amount: totalFee, symbol: sourceSymbol }]

return {
kind: this.from.kind === "kusama" ? "kusama->polkadot" : "polkadot->kusama",
xcmBridgeFee: totalXcmBridgeFee,
destinationFee,
bridgeHubDeliveryFee,
totalFeeInNative: totalFee,
breakdown,
summary,
totals: computeTotals(summary),
}
}

Expand Down
13 changes: 13 additions & 0 deletions web/packages/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,19 @@ 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 feeSchedule from "./feeSchedule"
export type { VolumeFeeParams } from "./feeSchedule"
export type {
FeeAsset,
FeeItem,
ToPolkadotFeeKey,
L2ToPolkadotFeeKey,
ToEthereumFeeKey,
InterParachainFeeKey,
KusamaFeeKey,
V1ToPolkadotFeeKey,
} from "./types/fee"
export { addBreakdown, computeTotals, findInBreakdown, findTotal } from "./fees"
export * as utils from "./utils"
export * as status from "./status"
export * as assetsV2 from "./assets_v2"
Expand Down
103 changes: 103 additions & 0 deletions web/packages/api/src/toEthereumSnowbridgeV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import { ensureValidationSuccess, padFeeByPercentage } from "./utils"
import { Context } from "./index"
import { DOT_LOCATION, ETHER_TOKEN_ADDRESS, findL2TokenAddress } from "./assets_v2"
import { getOperatingStatus } from "./status"
import { calculateVolumeTipInWei } from "./feeSchedule"
import { addBreakdown, computeTotals } from "./fees"
import { estimateFees } from "./across/api"
import type { MessageReceipt, Transfer, ValidatedTransfer, ValidationLog } from "./types/toEthereum"
import { ValidationKind, ValidationReason } from "./types/toEthereum"
Expand Down Expand Up @@ -114,6 +116,7 @@ export class TransferToEthereum<T extends EthereumProviderTypes> implements Tran
feeTokenLocation?: any
claimerLocation?: any
contractCall?: ContractCall
volumeFee?: import("./feeSchedule").VolumeFeeParams
},
): Promise<DeliveryFee> {
return this.#resolveByTokenAddress(tokenAddress).fee(tokenAddress, options)
Expand Down Expand Up @@ -153,6 +156,7 @@ export class TransferToEthereum<T extends EthereumProviderTypes> implements Tran
feeTokenLocation?: any
claimerLocation?: any
contractCall?: ContractCall
volumeFee?: import("./feeSchedule").VolumeFeeParams
}
tx?: {
claimerLocation?: any
Expand Down Expand Up @@ -212,6 +216,7 @@ export async function dryRunOnSourceParachain(
let assetHubForwarded
let bridgeHubForwarded
const success = result.isOk && result.asOk.executionResult.isOk
console.log('HEREEEREREREEE')
if (!success) {
console.error(
"Error during dry run on source parachain:",
Expand Down Expand Up @@ -386,6 +391,7 @@ export const estimateFeesFromAssetHub = async <T extends EthereumProviderTypes>(
l2PadFeeByPercentage?: bigint
l2TransferGasLimit?: bigint
fillDeadlineBuffer?: bigint
volumeFee?: import("./feeSchedule").VolumeFeeParams
},
l2ChainId?: number,
tokenAmount?: bigint,
Expand Down Expand Up @@ -454,6 +460,12 @@ export const estimateFeesFromAssetHub = async <T extends EthereumProviderTypes>(
feePadPercentage,
)

let volumeTip: bigint | undefined
if (options?.volumeFee) {
volumeTip = calculateVolumeTipInWei(options.volumeFee)
ethereumExecutionFee += volumeTip
}

// calculate the cost of swapping in native asset
let totalFeeInNative: bigint | undefined = undefined
let assetHubExecutionFeeNative: bigint | undefined = undefined
Expand All @@ -476,6 +488,43 @@ export const estimateFeesFromAssetHub = async <T extends EthereumProviderTypes>(
}
}

const breakdown: DeliveryFee["breakdown"] = {}
addBreakdown(breakdown, "localExecution", { amount: localExecutionFeeDOT, symbol: "DOT" })
addBreakdown(breakdown, "snowbridgeDelivery", {
amount: snowbridgeDeliveryFeeDOT,
symbol: "DOT",
})
addBreakdown(breakdown, "assetHubExecution", { amount: assetHubExecutionFeeDOT, symbol: "DOT" })
addBreakdown(breakdown, "bridgeHubDelivery", { amount: bridgeHubDeliveryFeeDOT, symbol: "DOT" })
addBreakdown(breakdown, "returnToSenderDelivery", {
amount: returnToSenderDeliveryFeeDOT,
symbol: "DOT",
})
addBreakdown(breakdown, "returnToSenderExecution", {
amount: returnToSenderExecutionFeeDOT,
symbol: "DOT",
})
addBreakdown(breakdown, "ethereumExecution", { amount: ethereumExecutionFee ?? 0n, symbol: "ETH" })
if (volumeTip !== undefined) {
addBreakdown(breakdown, "volumeTip", { amount: volumeTip, symbol: "ETH" })
}
if (ethereumExecutionFeeInNative !== undefined && feeLocation) {
addBreakdown(breakdown, "ethereumExecution", {
amount: ethereumExecutionFeeInNative,
symbol: "NATIVE",
})
}
if (l2BridgeFeeInL1Token > 0n) {
addBreakdown(breakdown, "l2Bridge", { amount: l2BridgeFeeInL1Token, symbol: "ETH" })
}

const summary: DeliveryFee["summary"] = feeLocation
? [{ description: "Bridge fee", amount: totalFeeInNative!, symbol: "NATIVE" }]
: [{ description: "Bridge fee", amount: totalFeeInDot, symbol: "DOT" }]
if (l2BridgeFeeInL1Token > 0n) {
summary.push({ description: "L2 bridge fee", amount: l2BridgeFeeInL1Token, symbol: "ETH" })
}

return {
kind: l2ChainId ? "polkadot->ethereum_l2" : "polkadot->ethereum",
localExecutionFeeDOT,
Expand All @@ -493,6 +542,10 @@ export const estimateFeesFromAssetHub = async <T extends EthereumProviderTypes>(
localExecutionFeeInNative,
totalFeeInNative,
l2BridgeFeeInL1Token,
volumeTip,
breakdown,
summary,
totals: computeTotals(summary),
}
}

Expand All @@ -508,6 +561,7 @@ export const estimateFeesFromParachains = async <T extends EthereumProviderTypes
defaultFee?: bigint
feeTokenLocation?: any
contractCall?: ContractCall
volumeFee?: import("./feeSchedule").VolumeFeeParams
},
): Promise<DeliveryFee> => {
const sourceParachain = registry.parachains[`polkadot_${sourceParaId}`]
Expand Down Expand Up @@ -585,6 +639,12 @@ export const estimateFeesFromParachains = async <T extends EthereumProviderTypes
options,
)

let volumeTip: bigint | undefined
if (options?.volumeFee) {
volumeTip = calculateVolumeTipInWei(options.volumeFee)
ethereumExecutionFee += volumeTip
}

// calculate the cost of swapping in native asset
let totalFeeInNative: bigint | undefined = undefined
let assetHubExecutionFeeNative: bigint | undefined = undefined
Expand Down Expand Up @@ -631,6 +691,42 @@ export const estimateFeesFromParachains = async <T extends EthereumProviderTypes
}
}

const breakdown: DeliveryFee["breakdown"] = {}
addBreakdown(breakdown, "localExecution", { amount: localExecutionFeeDOT, symbol: "DOT" })
addBreakdown(breakdown, "localDelivery", { amount: localDeliveryFeeDOT, symbol: "DOT" })
addBreakdown(breakdown, "snowbridgeDelivery", {
amount: snowbridgeDeliveryFeeDOT,
symbol: "DOT",
})
addBreakdown(breakdown, "assetHubExecution", { amount: assetHubExecutionFeeDOT, symbol: "DOT" })
addBreakdown(breakdown, "bridgeHubDelivery", { amount: bridgeHubDeliveryFeeDOT, symbol: "DOT" })
addBreakdown(breakdown, "ethereumExecution", { amount: ethereumExecutionFee, symbol: "ETH" })
if (volumeTip !== undefined) {
addBreakdown(breakdown, "volumeTip", { amount: volumeTip, symbol: "ETH" })
}
if (localExecutionFeeInNative !== undefined) {
addBreakdown(breakdown, "localExecution", {
amount: localExecutionFeeInNative,
symbol: "NATIVE",
})
}
if (localDeliveryFeeInNative !== undefined) {
addBreakdown(breakdown, "localDelivery", {
amount: localDeliveryFeeInNative,
symbol: "NATIVE",
})
}
if (ethereumExecutionFeeInNative !== undefined) {
addBreakdown(breakdown, "ethereumExecution", {
amount: ethereumExecutionFeeInNative,
symbol: "NATIVE",
})
}

const summary: DeliveryFee["summary"] = feeLocation
? [{ description: "Bridge fee", amount: totalFeeInNative!, symbol: "NATIVE" }]
: [{ description: "Bridge fee", amount: totalFeeInDot, symbol: "DOT" }]

return {
kind: "polkadot->ethereum",
localExecutionFeeDOT,
Expand All @@ -649,6 +745,10 @@ export const estimateFeesFromParachains = async <T extends EthereumProviderTypes
localExecutionFeeInNative,
localDeliveryFeeInNative,
totalFeeInNative,
volumeTip,
breakdown,
summary,
totals: computeTotals(summary),
}
}

Expand Down Expand Up @@ -1058,6 +1158,9 @@ export const mockDeliveryFee: DeliveryFee = {
returnToSenderExecutionFeeDOT: 0n,
totalFeeInDot: 10n,
ethereumExecutionFee: 1n,
breakdown: {},
summary: [{ description: "Bridge fee", amount: 10n, symbol: "DOT" }],
totals: [{ amount: 10n, symbol: "DOT" }],
}

// Agent creation exports
Expand Down
Loading
Loading