diff --git a/e2e/fees.spec.ts b/e2e/fees.spec.ts new file mode 100644 index 00000000..91b749d8 --- /dev/null +++ b/e2e/fees.spec.ts @@ -0,0 +1,32 @@ +import { expect, test } from "@playwright/test"; + +import { addReferral, getReferrals, setReferral } from "./utils"; + +const referral = "pro"; + +test.describe("Fees", () => { + test("should show routing fees", async ({ page }) => { + if (!(referral in (await getReferrals()))) { + await addReferral(referral); + } + + const config = { maxRoutingFee: 0.001 }; + await setReferral(referral, config); + + await page.goto(`/?ref=${referral}`); + + await page.locator(".arrow-down").first().click(); + await page.getByTestId("select-BTC").click(); + await page + .locator( + "div:nth-child(3) > .asset-wrap > .asset > .asset-selection > .arrow-down", + ) + .click(); + await page.getByTestId("select-LN").click(); + + const routingFees = page.getByTestId("routing-fee-limit"); + await expect(routingFees).toHaveText( + `${config.maxRoutingFee * 100 * 10_000} ppm`, + ); + }); +}); diff --git a/e2e/utils.ts b/e2e/utils.ts index 1a6816c7..17074181 100644 --- a/e2e/utils.ts +++ b/e2e/utils.ts @@ -25,6 +25,24 @@ const execCommand = async (command: string): Promise => { } }; +const boltzCli = async (command: string): Promise => { + try { + const { stdout, stderr } = await execAsync( + `docker exec boltz-backend boltz-cli ${command}`, + { shell: "/bin/bash" }, + ); + + if (stderr) { + throw new Error(`Error executing command: ${stderr}`); + } + + return stdout.trim(); + } catch (error) { + console.error(`Failed to execute command: ${command}`, error); + throw error; + } +}; + export const getBitcoinAddress = (): Promise => execCommand("bitcoin-cli-sim-client getnewaddress"); @@ -64,6 +82,18 @@ export const generateInvoiceLnd = async (amount: number): Promise => { ).payment_request as string; }; +export const addReferral = (name: string): Promise => + boltzCli(`addreferral ${name} 0`); + +export const getReferrals = async (): Promise> => + JSON.parse(await boltzCli(`getreferrals`)) as Record; + +export const setReferral = ( + name: string, + config: Record, +): Promise => + boltzCli(`setreferral ${name} '${JSON.stringify(config)}'`); + export const lookupInvoiceLnd = async ( invoice: string, ): Promise<{ state: string; r_preimage: string }> => { diff --git a/src/components/Fees.tsx b/src/components/Fees.tsx index a7fd584c..fcc967d4 100644 --- a/src/components/Fees.tsx +++ b/src/components/Fees.tsx @@ -1,5 +1,5 @@ import { BigNumber } from "bignumber.js"; -import { createEffect } from "solid-js"; +import { Show, createEffect, createSignal } from "solid-js"; import { LBTC } from "../consts/Assets"; import { SwapType } from "../consts/Enums"; @@ -19,6 +19,8 @@ import { formatAmount } from "../utils/denomination"; import { getPair } from "../utils/helper"; import Denomination from "./settings/Denomination"; +const ppmFactor = 10_000; + // When sending to an unconfidential address, we need to add an extra // confidential OP_RETURN output with 1 sat inside const unconfidentialExtra = 5; @@ -46,7 +48,15 @@ const Fees = () => { addressValid() && !isConfidentialAddress(onchainAddress()); + const [routingFee, setRoutingFee] = createSignal( + undefined, + ); + createEffect(() => { + // Reset routing fee when changing the pair + // (which might not be submarine and not set the signal) + setRoutingFee(undefined); + if (pairs()) { const cfg = getPair( pairs(), @@ -61,6 +71,10 @@ const Fees = () => { switch (swapType()) { case SwapType.Submarine: + setRoutingFee( + (cfg as SubmarinePairTypeTaproot).fees + .maximalRoutingFee, + ); setMinerFee( (cfg as SubmarinePairTypeTaproot).fees.minerFees, ); @@ -148,6 +162,13 @@ const Fees = () => { data-denominator={denomination()} /> + +
+ {t("routing_fee_limit")}:{" "} + + {routingFee() * ppmFactor} ppm + +
); diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts index b9bf78f9..7bbb8a5e 100644 --- a/src/i18n/i18n.ts +++ b/src/i18n/i18n.ts @@ -228,6 +228,7 @@ const dict = { refund_available_in: "Refund will be available in {{ blocks }} blocks", no_wallet_connected: "No wallet connected", no_lockup_transaction: "No lockup transaction found", + routing_fee_limit: "Routing fee limit", }, de: { language: "Deutsch", @@ -468,6 +469,7 @@ const dict = { refund_available_in: "Rückerstattung möglich in {{ blocks }} Blöcken", no_wallet_connected: "Kein Wallet verbunden", no_lockup_transaction: "Keine Lockup-Transaktion gefunden", + routing_fee_limit: "Routing Gebühr Limit", }, es: { language: "Español", @@ -704,6 +706,7 @@ const dict = { refund_available_in: "Reembolso disponible en {{ blocks }} bloques", no_wallet_connected: "No hay monedero conectado", no_lockup_transaction: "No se encontró ninguna transacción de lockup", + routing_fee_limit: "Límite de la tarifa de enrutamiento", }, zh: { language: "中文", @@ -914,6 +917,7 @@ const dict = { refund_available_in: "退款将分 {{ blocks }} 区块提供", no_wallet_connected: "未连接钱包", no_lockup_transaction: "未找到锁仓交易", + routing_fee_limit: "最大路由费用", }, ja: { language: "日本語", @@ -1149,6 +1153,7 @@ const dict = { refund_available_in: "返金は {{ blocks }} つのブロックに分かれる", no_wallet_connected: "財布はつながっていない!", no_lockup_transaction: "ロックアップトランザクションが見つかりません", + routing_fee_limit: "ルーティング料金の上限", }, }; diff --git a/src/utils/boltzClient.ts b/src/utils/boltzClient.ts index 95747920..e1b9d8b1 100644 --- a/src/utils/boltzClient.ts +++ b/src/utils/boltzClient.ts @@ -35,8 +35,9 @@ type SubmarinePairTypeTaproot = PairType & { maximalZeroConf: number; }; fees: { - percentage: number; minerFees: number; + percentage: number; + maximalRoutingFee?: number; }; }; diff --git a/src/utils/denomination.ts b/src/utils/denomination.ts index 5dc54f6d..9c34bc4e 100644 --- a/src/utils/denomination.ts +++ b/src/utils/denomination.ts @@ -53,7 +53,7 @@ export const formatAmountDenomination = ( default: { const chars = amount.toString().split("").reverse(); - return chars + const formatted = chars .reduce( (acc, char, i) => i % 3 === 0 ? acc + " " + char : acc + char, @@ -63,6 +63,12 @@ export const formatAmountDenomination = ( .split("") .reverse() .join(""); + + return ( + formatted.includes(".") || formatted.includes(",") + ? formatted.replaceAll(" .", ".").replaceAll(" ,", ",") + : formatted + ).replaceAll(".", separator); } } }; diff --git a/tests/utils/denomination.spec.ts b/tests/utils/denomination.spec.ts index 29cba9ab..2736169e 100644 --- a/tests/utils/denomination.spec.ts +++ b/tests/utils/denomination.spec.ts @@ -43,6 +43,8 @@ describe("denomination utils", () => { ${Denomination.Btc} | ${1000} | ${"."} | ${"0.00001"} ${Denomination.Btc} | ${10000} | ${"."} | ${"0.0001"} ${Denomination.Btc} | ${10000} | ${","} | ${"0,0001"} + ${Denomination.Sat} | ${0.12} | ${","} | ${"0,12"} + ${Denomination.Sat} | ${0.24} | ${","} | ${"0,24"} `( "format $amount in $denomination with `$separator` separator", ({ denomination, amount, formatted, separator }) => {