diff --git a/.prettierrc b/.prettierrc
index 773c52b0..038fa7cb 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -4,5 +4,6 @@
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"tabWidth": 4,
- "bracketSameLine": true
+ "bracketSameLine": true,
+ "proseWrap": "always"
}
diff --git a/docs/README.md b/docs/README.md
index 48270857..cfa48cb5 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,7 +1,7 @@
---
description: >-
- We encourage our technical users to check the code and run the web app locally
- from source following the instructions below.
+ We encourage our technical users to check the code and run the web app
+ locally from source following the instructions below.
cover: .gitbook/assets/boltz-web_app_header.png
coverY: 0
---
@@ -10,13 +10,20 @@ coverY: 0
## Dependencies
-Make sure to have the latest [Node.js LTS and NPM](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) installed. We recommend using [nvm](https://github.com/nvm-sh/nvm#install--update-script) to manage npm installs: `nvm install --lts`
+Make sure to have the latest
+[Node.js LTS and NPM](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)
+installed. We recommend using
+[nvm](https://github.com/nvm-sh/nvm#install--update-script) to manage npm
+installs: `nvm install --lts`
### Run
-Clone the repository, change to the project folder and run `npm install` to install all dependencies. Then `npm run mainnet && npm run build` and `npx serve dist` to bring it up.
+Clone the repository, change to the project folder and run `npm install` to
+install all dependencies. Then `npm run mainnet && npm run build` and
+`npx serve dist` to bring it up.
-Open [http://localhost:3000](http://localhost:3000) in your browser and start swapping!
+Open [http://localhost:3000](http://localhost:3000) in your browser and start
+swapping!
## With Docker
diff --git a/docs/pwa.md b/docs/pwa.md
index bd73f780..0a56b4f1 100644
--- a/docs/pwa.md
+++ b/docs/pwa.md
@@ -1,8 +1,8 @@
---
description: >-
- For improved censorship resistance and privacy, Boltz is not available in app
- stores, but can be installed as Progressive Web App (PWA) on all Android and
- iOS devices, as well as desktop computers.
+ For improved censorship resistance and privacy, Boltz is not available in
+ app stores, but can be installed as Progressive Web App (PWA) on all Android
+ and iOS devices, as well as desktop computers.
---
# 📲 Install as App
@@ -11,7 +11,10 @@ description: >-
## Android
-1. Open [boltz.exchange](https://boltz.exchange) in a mobile browser like [Chrome](https://www.google.com/chrome/) or [Vanadium](https://github.com/GrapheneOS/Vanadium), open the browser menu and tap "Install app":
+1. Open [boltz.exchange](https://boltz.exchange) in a mobile browser like
+ [Chrome](https://www.google.com/chrome/) or
+ [Vanadium](https://github.com/GrapheneOS/Vanadium), open the browser menu and
+ tap "Install app":
@@ -19,11 +22,13 @@ description: >-
-3. Now you find Boltz as App Icon on your home screen which you can use just like any other app.
+3. Now you find Boltz as App Icon on your home screen which you can use just
+ like any other app.
## iOS
-1. Open [boltz.exchange](https://boltz.exchange) in your Safari mobile browser and tap the share button:
+1. Open [boltz.exchange](https://boltz.exchange) in your Safari mobile browser
+ and tap the share button:
@@ -35,13 +40,16 @@ description: >-
-4. Now you find Boltz as App Icon on your home screen which you can use just like any other app.
+4. Now you find Boltz as App Icon on your home screen which you can use just
+ like any other app.
## Desktop
-Here an example how to install Boltz as App on a Ubuntu Desktop Computer using [Chromium](https://www.chromium.org/Home/):
+Here an example how to install Boltz as App on a Ubuntu Desktop Computer using
+[Chromium](https://www.chromium.org/Home/):
-1. Open [boltz.exchange](https://boltz.exchange) and click the install icon that automatically appears:
+1. Open [boltz.exchange](https://boltz.exchange) and click the install icon that
+ automatically appears:
diff --git a/docs/urlParams.md b/docs/urlParams.md
new file mode 100644
index 00000000..9c491b57
--- /dev/null
+++ b/docs/urlParams.md
@@ -0,0 +1,33 @@
+---
+description: >-
+ To prefill certain inputs of the the site, URL query parameters can be used.
+ Those parameters are documented here.
+---
+
+# URL query parameters
+
+## Embedding
+
+When `embed` is set to `1`, only the swap box will be shown.
+
+## Destination
+
+`destination` prefills either the onchain address or invoice input field. The
+inferred asset takes precedence over `receiveAsset` and in case a lightning
+invoice is set, its amount takes precedence over all other inputs to set the
+amount.
+
+## Assets
+
+`sendAsset` and `receiveAsset` can be used to set the assets. Possible values
+are:
+
+- `LN`
+- `BTC`
+- `L-BTC`
+- `RBTC`
+
+## Amounts
+
+`sendAmount` or `receiveAmount` set the respective amounts. Value is denominated
+in satoshis and `sendAmount` takes precedence.
diff --git a/e2e/submarineSwap.spec.ts b/e2e/submarineSwap.spec.ts
index c237da1f..df5c72b2 100644
--- a/e2e/submarineSwap.spec.ts
+++ b/e2e/submarineSwap.spec.ts
@@ -28,9 +28,7 @@ test.describe("Submarine swap", () => {
await expect(inputSendAmount).toHaveValue(sendAmount);
const invoiceInput = page.locator("textarea[data-testid='invoice']");
- await invoiceInput.fill(
- JSON.parse(await generateInvoiceLnd(1000000)).payment_request,
- );
+ await invoiceInput.fill(await generateInvoiceLnd(1000000));
const buttonCreateSwap = page.locator(
"button[data-testid='create-swap-button']",
);
diff --git a/e2e/urlParams.spec.ts b/e2e/urlParams.spec.ts
new file mode 100644
index 00000000..e1eead80
--- /dev/null
+++ b/e2e/urlParams.spec.ts
@@ -0,0 +1,102 @@
+import { expect, test } from "@playwright/test";
+import BigNumber from "bignumber.js";
+
+import { Denomination } from "../src/consts/Enums";
+import { formatAmount } from "../src/utils/denomination";
+import {
+ generateInvoiceLnd,
+ getBitcoinAddress,
+ getLiquidAddress,
+} from "./utils";
+
+test.describe("URL params", () => {
+ test("BTC address destination", async ({ page }) => {
+ const address = await getBitcoinAddress();
+
+ await page.goto(`/?destination=${address}`);
+ const receiveAsset = page.locator(".asset-BTC");
+ expect(receiveAsset).toBeDefined();
+
+ const onchainAddress = page.getByTestId("onchainAddress");
+ await expect(onchainAddress).toHaveValue(address);
+ });
+
+ test("L-BTC address destination", async ({ page }) => {
+ const address = await getLiquidAddress();
+
+ await page.goto(`/?destination=${address}`);
+ const receiveAsset = page.locator(".asset-L-BTC");
+ expect(receiveAsset).toBeDefined();
+
+ const onchainAddress = page.getByTestId("onchainAddress");
+ await expect(onchainAddress).toHaveValue(address);
+ });
+
+ test("Lightning invoice destination", async ({ page }) => {
+ const amount = 100_000;
+ const invoice = await generateInvoiceLnd(amount);
+
+ await page.goto(`/?destination=${invoice}`);
+ const receiveAsset = page.locator(".asset-LN");
+ expect(receiveAsset).toBeDefined();
+
+ const invoiceInput = page.getByTestId("invoice");
+ await expect(invoiceInput).toHaveValue(invoice);
+
+ const receiveAmount = page.getByTestId("receiveAmount");
+ await expect(receiveAmount).toHaveValue(
+ formatAmount(BigNumber(amount), Denomination.Sat, "."),
+ );
+ });
+
+ test("should set send amount", async ({ page }) => {
+ const amount = 210_000;
+ await page.goto(`/?sendAmount=${amount}`);
+
+ const sendAmount = page.getByTestId("sendAmount");
+ await expect(sendAmount).toHaveValue(
+ formatAmount(BigNumber(amount), Denomination.Sat, "."),
+ );
+ });
+
+ test("should set receive amount", async ({ page }) => {
+ const amount = 210_000;
+ await page.goto(`/?receiveAmount=${amount}`);
+
+ const receiveAmount = page.getByTestId("receiveAmount");
+ await expect(receiveAmount).toHaveValue(
+ formatAmount(BigNumber(amount), Denomination.Sat, "."),
+ );
+ });
+
+ test("should not set receive amount when send amount is set", async ({
+ page,
+ }) => {
+ const sendAmount = 100_000;
+ const receiveAmount = 210_000;
+ await page.goto(
+ `/?sendAmount=${sendAmount}&receiveAmount=${receiveAmount}`,
+ );
+
+ const sendAmountInput = page.getByTestId("sendAmount");
+ await expect(sendAmountInput).toHaveValue(
+ formatAmount(BigNumber(sendAmount), Denomination.Sat, "."),
+ );
+ });
+
+ test("should not set amount when lightning invoice is set", async ({
+ page,
+ }) => {
+ const invoiceAmount = 200_000;
+ const invoice = await generateInvoiceLnd(invoiceAmount);
+
+ const sendAmount = 100_000;
+
+ await page.goto(`/?sendAmount=${sendAmount}&destination=${invoice}`);
+
+ const receiveAmount = page.getByTestId("receiveAmount");
+ await expect(receiveAmount).toHaveValue(
+ formatAmount(BigNumber(invoiceAmount), Denomination.Sat, "."),
+ );
+ });
+});
diff --git a/e2e/utils.ts b/e2e/utils.ts
index 15a5caa7..2762f8e0 100644
--- a/e2e/utils.ts
+++ b/e2e/utils.ts
@@ -28,6 +28,10 @@ export const getBitcoinAddress = async (): Promise => {
return execCommand("bitcoin-cli-sim-client getnewaddress");
};
+export const getLiquidAddress = async (): Promise => {
+ return execCommand("elements-cli-sim-client getnewaddress");
+};
+
export const bitcoinSendToAddress = async (
address: string,
amount: string,
@@ -63,5 +67,7 @@ export const payInvoiceLnd = async (invoice: string): Promise => {
};
export const generateInvoiceLnd = async (amount: number): Promise => {
- return execCommand(`lncli-sim 1 addinvoice --amt ${amount}`);
+ return JSON.parse(
+ await execCommand(`lncli-sim 1 addinvoice --amt ${amount}`),
+ ).payment_request;
};
diff --git a/src/consts/Assets.ts b/src/consts/Assets.ts
index 4528fc39..797d6bcc 100644
--- a/src/consts/Assets.ts
+++ b/src/consts/Assets.ts
@@ -4,3 +4,5 @@ export const LBTC = "L-BTC";
export const RBTC = "RBTC";
export type AssetType = typeof LN | typeof BTC | typeof LBTC | typeof RBTC;
+
+export const assets = [LN, BTC, LBTC, RBTC];
diff --git a/src/context/Create.tsx b/src/context/Create.tsx
index af34f75d..df4bffc1 100644
--- a/src/context/Create.tsx
+++ b/src/context/Create.tsx
@@ -1,5 +1,6 @@
import { makePersisted } from "@solid-primitives/storage";
import BigNumber from "bignumber.js";
+import type { Network as LiquidNetwork } from "liquidjs-lib/src/networks";
import {
Accessor,
Setter,
@@ -10,9 +11,113 @@ import {
} from "solid-js";
import { config } from "../config";
-import { LN, RBTC } from "../consts/Assets";
+import { BTC, LBTC, LN, RBTC, assets } from "../consts/Assets";
import { Side, SwapType } from "../consts/Enums";
import { DictKey } from "../i18n/i18n";
+import { getAddress, getNetwork } from "../utils/compat";
+import { getUrlParam, urlParamIsSet } from "../utils/urlParams";
+
+const setDestination = (
+ setAssetReceive: Setter,
+ setInvoice: Setter,
+ setOnchainAddress: Setter,
+) => {
+ const isValidForAsset = (
+ asset: typeof BTC | typeof LBTC,
+ address: string,
+ ) => {
+ try {
+ getAddress(asset).toOutputScript(
+ address,
+ getNetwork(asset) as LiquidNetwork,
+ );
+ return true;
+ } catch (e) {
+ return false;
+ }
+ };
+
+ const destination = getUrlParam("destination");
+ if (urlParamIsSet(destination)) {
+ if (isValidForAsset(BTC, destination)) {
+ setAssetReceive(BTC);
+ setOnchainAddress(destination);
+ return BTC;
+ } else if (isValidForAsset(LBTC, destination)) {
+ setAssetReceive(LBTC);
+ setOnchainAddress(destination);
+ return LBTC;
+ } else {
+ setAssetReceive(LN);
+ setInvoice(destination);
+ return LN;
+ }
+ }
+
+ return undefined;
+};
+
+const isValidAsset = (asset: string) =>
+ urlParamIsSet(asset) && assets.includes(asset);
+
+const parseAmount = (amount: string): BigNumber | undefined => {
+ if (!urlParamIsSet(amount)) {
+ return undefined;
+ }
+
+ const parsedAmount = new BigNumber(amount);
+ if (parsedAmount.isNaN()) {
+ return undefined;
+ }
+ return parsedAmount;
+};
+
+const handleUrlParams = (
+ setAssetSend: Setter,
+ setAssetReceive: Setter,
+ setInvoice: Setter,
+ setOnchainAddress: Setter,
+ setAmountChanged: Setter,
+ setSendAmount: Setter,
+ setReceiveAmount: Setter,
+) => {
+ const destinationAsset = setDestination(
+ setAssetReceive,
+ setInvoice,
+ setOnchainAddress,
+ );
+
+ const sendAsset = getUrlParam("sendAsset");
+ if (isValidAsset(sendAsset)) {
+ setAssetSend(sendAsset);
+ }
+
+ // The type of the destination takes precedence
+ if (destinationAsset === undefined) {
+ const receiveAsset = getUrlParam("receiveAsset");
+ if (isValidAsset(receiveAsset)) {
+ setAssetReceive(receiveAsset);
+ }
+ }
+
+ // Lightning invoice amounts take precedence
+ if (destinationAsset !== LN) {
+ const sendAmount = parseAmount(getUrlParam("sendAmount"));
+ if (sendAmount) {
+ setAmountChanged(Side.Send);
+ setSendAmount(sendAmount);
+ }
+
+ if (sendAmount === undefined) {
+ const receiveAmount = parseAmount(getUrlParam("receiveAmount"));
+
+ if (receiveAmount) {
+ setAmountChanged(Side.Receive);
+ setReceiveAmount(BigNumber(receiveAmount));
+ }
+ }
+ }
+};
export type CreateContextType = {
swapType: Accessor;
@@ -135,6 +240,16 @@ const CreateProvider = (props: { children: any }) => {
const [boltzFee, setBoltzFee] = createSignal(0);
const [minerFee, setMinerFee] = createSignal(0);
+ handleUrlParams(
+ setAssetSend,
+ setAssetReceive,
+ setInvoice,
+ setOnchainAddress,
+ setAmountChanged,
+ setSendAmount,
+ setReceiveAmount,
+ );
+
return (
{
setWasmSupported(checkWasmSupported());
// check referral
- const refParam = new URLSearchParams(window.location.search).get("ref");
+ const refParam = getUrlParam("ref");
if (refParam && refParam !== "") {
setRef(refParam);
- window.history.replaceState(
- {},
- document.title,
- window.location.pathname,
- );
}
- if (detectEmbedded()) {
+ if (isEmbed()) {
setEmbedded(true);
+ setHideHero(true);
}
const [browserNotification, setBrowserNotification] = makePersisted(
diff --git a/src/utils/urlParams.ts b/src/utils/urlParams.ts
new file mode 100644
index 00000000..6ef2924c
--- /dev/null
+++ b/src/utils/urlParams.ts
@@ -0,0 +1,27 @@
+const searchParams = () => new URLSearchParams(window.location.search);
+
+export const isEmbed = (): boolean => {
+ const param = searchParams().get("embed");
+ return param && param === "1";
+};
+
+export const getUrlParam = (name: string): string => {
+ const param = searchParams().get(name);
+ if (param) {
+ resetUrlParam(name);
+ }
+ return param;
+};
+
+export const urlParamIsSet = (param: string) => param && param !== "";
+
+export const resetUrlParam = (name: string) => {
+ const params = searchParams();
+ params.delete(name);
+
+ window.history.replaceState(
+ {},
+ document.title,
+ `${window.location.pathname}?${params.toString()}`,
+ );
+};