Skip to content

Commit

Permalink
feat: 0-amount chain swaps
Browse files Browse the repository at this point in the history
  • Loading branch information
michael1011 committed Nov 26, 2024
1 parent 93464ac commit aad5e7a
Show file tree
Hide file tree
Showing 18 changed files with 310 additions and 76 deletions.
51 changes: 51 additions & 0 deletions e2e/chainSwaps/zeroAmount.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { expect, test } from "@playwright/test";

import { getBitcoinAddress } from "../utils";

test.describe("Chain Swap 0-amount", () => {
test("should allow 0-amount chain swaps", async ({ page }) => {
await page.goto("/");

const assetSelector = page.locator("div[class='asset asset-LN'] div");
await assetSelector.click();

await page.locator("div[data-testid='select-L-BTC']").click();

const inputOnchainAddress = page.locator(
"input[data-testid='onchainAddress']",
);
await inputOnchainAddress.fill(await getBitcoinAddress());

const buttonCreateSwap = page.locator(
"button[data-testid='create-swap-button']",
);
await buttonCreateSwap.click();

const skipDownload = page.getByText("Skip download");
await skipDownload.click();
});

test("should not allow 0-amount chain swaps when sending RBTC", async ({
page,
}) => {
await page.goto("/");

const assetSelector = page.locator("div[class='asset asset-LN'] div");
await assetSelector.click();

await page.locator("div[data-testid='select-RBTC']").click();

const inputOnchainAddress = page.locator(
"input[data-testid='onchainAddress']",
);
await inputOnchainAddress.fill(await getBitcoinAddress());

const buttonCreateSwap = page.locator(
"button[data-testid='create-swap-button']",
);
const createButton = await buttonCreateSwap.elementHandle();
await expect(createButton.getAttribute("disabled")).resolves.toEqual(
"",
);
});
});
5 changes: 3 additions & 2 deletions src/components/AddressInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,11 @@ const AddressInput = () => {
break;
}
} catch (e) {
log.debug(`Invalid address input: ${formatError(e)}`);

setAddressValid(false);

if (inputValue.length !== 0) {
log.debug(`Invalid address input: ${formatError(e)}`);

const msg = t("invalid_address", { asset: assetReceive() });
input.classList.add("invalid");
input.setCustomValidity(msg);
Expand Down
11 changes: 10 additions & 1 deletion src/components/CreateButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,16 @@ export const CreateButton = () => {
setButtonLabel({ key: "invalid_pair" });
return;
}
if (!amountValid()) {
if (
!amountValid() &&
// Chain swaps with 0-amount that do not have RBTC as sending asset
// can skip this check
!(
swapType() === SwapType.Chain &&
assetSend() !== RBTC &&
sendAmount().isZero()
)
) {
const lessThanMin = Number(sendAmount()) < minimum();
setButtonLabel({
key: lessThanMin ? "minimum_amount" : "maximum_amount",
Expand Down
6 changes: 2 additions & 4 deletions src/components/PayInvoice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ import { Show } from "solid-js";
import CopyButton from "../components/CopyButton";
import QrCode from "../components/QrCode";
import { BTC } from "../consts/Assets";
import { Denomination } from "../consts/Enums";
import { useGlobalContext } from "../context/Global";
import { formatAmount } from "../utils/denomination";
import { formatAmount, formatDenomination } from "../utils/denomination";
import { clipboard, cropString, isMobile } from "../utils/helper";
import { invoicePrefix } from "../utils/invoice";
import { enableWebln } from "../utils/webln";
Expand All @@ -30,8 +29,7 @@ const PayInvoice = (props: { sendAmount: number; invoice: string }) => {
denomination(),
separator(),
),
denomination:
denomination() === Denomination.Sat ? "sats" : BTC,
denomination: formatDenomination(denomination(), BTC),
})}
</h2>
<hr />
Expand Down
154 changes: 101 additions & 53 deletions src/components/PayOnchain.tsx
Original file line number Diff line number Diff line change
@@ -1,73 +1,121 @@
import { BigNumber } from "bignumber.js";
import { Show } from "solid-js";
import { Show, createMemo, createResource } from "solid-js";

import CopyButton from "../components/CopyButton";
import QrCode from "../components/QrCode";
import { BTC } from "../consts/Assets";
import { Denomination } from "../consts/Enums";
import { SwapType } from "../consts/Enums";
import { useGlobalContext } from "../context/Global";
import { formatAmount } from "../utils/denomination";
import { clipboard, cropString, isMobile } from "../utils/helper";
import { getPairs } from "../utils/boltzClient";
import { formatAmount, formatDenomination } from "../utils/denomination";
import { clipboard, cropString, getPair, isMobile } from "../utils/helper";
import LoadingSpinner from "./LoadingSpinner";

const PayOnchain = (props: {
asset: string;
type: SwapType;
assetSend: string;
assetReceive: string;
expectedAmount: number;
address: string;
bip21: string;
}) => {
const { t, denomination, separator } = useGlobalContext();
const { t, denomination, separator, setPairs, pairs } = useGlobalContext();

const [pairsFetch] = createResource(async () => {
if (pairs() !== undefined) {
return pairs();
}

const p = await getPairs();
setPairs(p);
return p;
});

const headerText = createMemo(() => {
const denom = formatDenomination(denomination(), props.assetSend);

if (props.expectedAmount > 0) {
return t("send_to", {
denomination: denom,
amount: formatAmount(
BigNumber(props.expectedAmount),
denomination(),
separator(),
),
});
}

if (pairs() === undefined) {
return "";
}

const pair = getPair(
pairs(),
props.type,
props.assetSend,
props.assetReceive,
);
return t("send_between", {
denomination: denom,
min: formatAmount(
BigNumber(pair.limits.minimal),
denomination(),
separator(),
),
max: formatAmount(
BigNumber(pair.limits.maximal),
denomination(),
separator(),
),
});
});

return (
<div>
<h2>
{t("send_to", {
amount: formatAmount(
BigNumber(props.expectedAmount),
denomination(),
separator(),
),
denomination:
denomination() === Denomination.Sat
? "sats"
: props.asset,
})}
</h2>
<hr />
<a href={props.bip21}>
<QrCode asset={props.asset} data={props.bip21} />
</a>
<hr />
<p
onClick={() => clipboard(props.address)}
class="address-box break-word">
{cropString(props.address)}
</p>
<Show when={props.asset === BTC}>
<hr class="spacer" />
<h3>{t("warning_expiry")}</h3>
</Show>
<Show when={isMobile()}>
<Show
when={!pairsFetch.loading || headerText() === ""}
fallback={<LoadingSpinner />}>
<div>
<h2>{headerText()}</h2>
<hr />
<a href={props.bip21} class="btn btn-light">
{t("open_in_wallet")}
<a href={props.bip21}>
<QrCode asset={props.assetSend} data={props.bip21} />
</a>
</Show>
<hr />
<div class="btns" data-testid="pay-onchain-buttons">
<CopyButton
label="copy_amount"
data={() =>
formatAmount(
BigNumber(props.expectedAmount),
denomination(),
separator(),
)
}
/>
<CopyButton label="copy_address" data={props.address} />
<CopyButton label="copy_bip21" data={props.bip21} />
<hr />
<p
onClick={() => clipboard(props.address)}
class="address-box break-word">
{cropString(props.address)}
</p>
<Show when={props.assetSend === BTC}>
<hr class="spacer" />
<h3>{t("warning_expiry")}</h3>
</Show>
<Show when={isMobile()}>
<hr />
<a href={props.bip21} class="btn btn-light">
{t("open_in_wallet")}
</a>
</Show>
<hr />
<div class="btns" data-testid="pay-onchain-buttons">
<Show when={props.expectedAmount > 0}>
<CopyButton
label="copy_amount"
data={() =>
formatAmount(
BigNumber(props.expectedAmount),
denomination(),
separator(),
)
}
/>
</Show>

<CopyButton label="copy_address" data={props.address} />
<CopyButton label="copy_bip21" data={props.bip21} />
</div>
</div>
</div>
</Show>
);
};

Expand Down
8 changes: 8 additions & 0 deletions src/i18n/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ const dict = {
pay_timeout_blockheight: "Timeout block height",
pay_expected_amount: "Expected amount",
send_to: "Send {{ amount }} {{ denomination }} to",
send_between:
"Send between {{ min }} and {{ max }} {{ denomination }} to",
pay_invoice_to:
"Pay this invoice about {{ amount }} {{ denomination }}",
pay_address: "Address",
Expand Down Expand Up @@ -284,6 +286,8 @@ const dict = {
pay_timeout_blockheight: "Timeout Blockhöhe",
pay_expected_amount: "Erwarteter Betrag",
send_to: "Sende {{ amount }} {{ denomination }} an",
send_between:
"Sende zwischen {{ min }} und {{ max }} {{ denomination }} an",
pay_invoice_to:
"Zahle diese Rechnung über {{ amount }} {{ denomination }}",
pay_address: "Adresse",
Expand Down Expand Up @@ -517,6 +521,7 @@ const dict = {
pay_timeout_blockheight: "Altura del bloque de tiempo de espera",
pay_expected_amount: "Importe esperado",
send_to: "Enviar {{ amount }} {{ denomination }} a",
send_between: "Enviar entre {{ min }} y {{ max }} {{ denomination }} a",
pay_invoice_to: "Pague esta factura de {{ amount }} {{ denomination }}",
pay_address: "Dirección",
no_wallet: "Monedero no está instalado",
Expand Down Expand Up @@ -746,6 +751,7 @@ const dict = {
pay_expected_amount: "预期金额",
send_to:
"请确保将准确的金额{{ amount }}{{ denomination }}发送到下面显示的地址。否则,交换将失败。最好使用“复制BIP21”按钮,并将其粘贴到您钱包的发送屏幕中。",
send_between: "在 {{ min }} 和 {{ max }} {{ denomination }} 之间发送至",
pay_invoice_to: "支付金额为{{ amount }}{{ denomination }}的发票",
pay_address: "地址",
no_wallet: "未安装钱包",
Expand Down Expand Up @@ -954,6 +960,8 @@ const dict = {
pay_expected_amount: "予想金額",
send_to:
"{{ amount }} {{ denomination }} を以下のアドレスへ送金して下さい",
send_between:
"{{ min }} から {{ max }} {{ denomination }} を送信してください",
pay_invoice_to:
"このインボイスを支払う {{ amount }} {{ denomination }}",
pay_address: "アドレス",
Expand Down
9 changes: 9 additions & 0 deletions src/pages/Create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,15 @@ const Create = () => {
setCustomValidity("", false);

const amount = Number(sendAmount());
if (
swapType() === SwapType.Chain &&
assetSend() !== RBTC &&
amount === 0
) {
setAmountValid(true);
return;
}

const lessThanMin = amount < minimum();

if (lessThanMin || amount > maximum()) {
Expand Down
14 changes: 9 additions & 5 deletions src/pages/Hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@ import bitcoin from "../assets/bitcoin-icon.svg";
import lightning from "../assets/lightning-icon.svg";
import liquid from "../assets/liquid-icon.svg";
import rbtc from "../assets/rootstock-icon.svg";
import { BTC } from "../consts/Assets";
import { Denomination } from "../consts/Enums";
import { useGlobalContext } from "../context/Global";
import Create from "../pages/Create";
import "../style/hero.scss";
import { getNodeStats } from "../utils/boltzClient";
import { formatAmountDenomination } from "../utils/denomination";
import {
formatAmountDenomination,
formatDenomination,
} from "../utils/denomination";

export const Hero = () => {
const navigate = useNavigate();
Expand Down Expand Up @@ -102,10 +106,10 @@ export const Hero = () => {
{formatStatsAmount(capacity(), denomination())}{" "}
<small>
{t("capacity", {
denomination:
denomination() === Denomination.Sat
? "sats"
: "BTC",
denomination: formatDenomination(
denomination(),
BTC,
),
})}
</small>
</div>
Expand Down
6 changes: 4 additions & 2 deletions src/status/InvoiceSet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import PayOnchain from "../components/PayOnchain";
import { RBTC } from "../consts/Assets";
import { usePayContext } from "../context/Pay";
import { decodeInvoice } from "../utils/invoice";
import { SubmarineSwap, getRelevantAssetForSwap } from "../utils/swapCreator";
import { SubmarineSwap } from "../utils/swapCreator";

const InvoiceSet = () => {
const { swap } = usePayContext();
Expand All @@ -21,7 +21,9 @@ const InvoiceSet = () => {
when={submarine.assetSend === RBTC}
fallback={
<PayOnchain
asset={getRelevantAssetForSwap(submarine)}
type={submarine.type}
assetSend={submarine.assetSend}
assetReceive={submarine.assetReceive}
expectedAmount={submarine.expectedAmount}
address={submarine.address}
bip21={submarine.bip21}
Expand Down
Loading

0 comments on commit aad5e7a

Please sign in to comment.