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 15, 2024
1 parent 8f6e924 commit 5098b76
Show file tree
Hide file tree
Showing 17 changed files with 300 additions and 74 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
2 changes: 2 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
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
4 changes: 3 additions & 1 deletion src/status/SwapCreated.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ const SwapCreated = () => {
when={chain.assetSend === RBTC}
fallback={
<PayOnchain
asset={chain.assetSend}
type={chain.type}
assetSend={chain.assetSend}
assetReceive={chain.assetReceive}
expectedAmount={chain.lockupDetails.amount}
address={chain.lockupDetails.lockupAddress}
bip21={chain.lockupDetails.bip21}
Expand Down
2 changes: 1 addition & 1 deletion src/utils/boltzClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,13 +247,13 @@ export const createChainSwap = (
fetcher("/v2/swap/chain", {
from,
to,
userLockAmount,
preimageHash,
claimPublicKey,
refundPublicKey,
claimAddress,
pairHash,
referralId,
userLockAmount: userLockAmount === 0 ? undefined : userLockAmount,
});

export const getPartialRefundSignature = async (
Expand Down
3 changes: 3 additions & 0 deletions src/utils/denomination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ export const formatAmountDenomination = (
}
};

export const formatDenomination = (denom: Denomination, asset: string) =>
denom === Denomination.Sat ? "sats" : asset;

export const convertAmount = (amount: BigNumber, denom: string): BigNumber => {
switch (denom) {
case Denomination.Btc:
Expand Down
Loading

0 comments on commit 5098b76

Please sign in to comment.