From 8b9c1cbf62ab562529d0029149b1398b81cc0034 Mon Sep 17 00:00:00 2001 From: michael1011 Date: Thu, 12 Sep 2024 21:36:00 +0200 Subject: [PATCH 1/2] feat: bolt12 support for submarine swaps --- e2e/bolt12.spec.ts | 32 ++++ e2e/utils.ts | 5 + jest.config.js | 5 +- package-lock.json | 294 +++++++++++++++++++++++++++++++- package.json | 5 +- regtest | 2 +- src/components/CreateButton.tsx | 85 +++++++-- src/components/InvoiceInput.tsx | 6 + src/config.ts | 1 + src/context/Create.tsx | 7 + src/utils/boltzClient.ts | 9 + src/utils/invoice.ts | 59 ++++++- tests/mocks/bolt12.ts | 0 vite.config.mjs | 10 +- 14 files changed, 498 insertions(+), 22 deletions(-) create mode 100644 e2e/bolt12.spec.ts create mode 100644 tests/mocks/bolt12.ts diff --git a/e2e/bolt12.spec.ts b/e2e/bolt12.spec.ts new file mode 100644 index 00000000..9362c25e --- /dev/null +++ b/e2e/bolt12.spec.ts @@ -0,0 +1,32 @@ +import { expect, test } from "@playwright/test"; + +import { generateBitcoinBlock, getBolt12Offer } from "./utils"; + +test.describe("BOLT12", () => { + test.beforeEach(async () => { + await generateBitcoinBlock(); + }); + + test("Resolve bolt12 offer", async ({ page }) => { + await page.goto("/"); + + const divFlipAssets = page.locator("#flip-assets"); + await divFlipAssets.click(); + + const receiveAmount = "0.01"; + const inputReceiveAmount = page.locator( + "input[data-testid='receiveAmount']", + ); + await inputReceiveAmount.fill(receiveAmount); + + const invoiceInput = page.locator("textarea[data-testid='invoice']"); + await invoiceInput.fill(await getBolt12Offer()); + const buttonCreateSwap = page.locator( + "button[data-testid='create-swap-button']", + ); + await buttonCreateSwap.click(); + + const skipDownload = page.getByText("Skip download"); + await expect(skipDownload).toBeVisible(); + }); +}); diff --git a/e2e/utils.ts b/e2e/utils.ts index 0f434756..327aef18 100644 --- a/e2e/utils.ts +++ b/e2e/utils.ts @@ -71,3 +71,8 @@ export const generateInvoiceLnd = async (amount: number): Promise => { await execCommand(`lncli-sim 1 addinvoice --amt ${amount}`), ).payment_request; }; + +export const getBolt12Offer = async (): Promise => { + return JSON.parse(await execCommand("lightning-cli-sim 1 offer any ''")) + .bolt12; +}; diff --git a/jest.config.js b/jest.config.js index 57ccbad1..061f3e77 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,11 +1,14 @@ module.exports = { preset: "solid-jest/preset/browser", setupFilesAfterEnv: ["/node_modules/@testing-library/jest-dom"], - transformIgnorePatterns: ["node_modules/(?!@solidjs|solid-icons)"], + transformIgnorePatterns: [ + "node_modules/(?!@solidjs|solid-icons|boltz-bolt12)", + ], moduleNameMapper: { "^.+\\.svg": "/tests/mocks/SvgMock.tsx", "^.+\\.css": "/tests/mocks/StylesMock.tsx", "^.+\\.scss": "/tests/mocks/StylesMock.tsx", + "boltz-bolt12": "/tests/mocks/bolt12.ts", }, globals: { Buffer: Buffer, diff --git a/package-lock.json b/package-lock.json index fa4a5821..2462d913 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "bignumber.js": "^9.1.2 ", "bitcoinjs-lib": "^6.1.6", "bolt11": "^1.4.1", + "boltz-bolt12": "^0.1.2", "boltz-core": "^2.1.2", "buffer": "^6.0.3", "create-hmac": "^1.1.7", @@ -61,7 +62,9 @@ "vite": "^5.4.8", "vite-plugin-mkcert": "^1.17.6", "vite-plugin-node-polyfills": "^0.22.0", - "vite-plugin-solid": "^2.10.2" + "vite-plugin-solid": "^2.10.2", + "vite-plugin-top-level-await": "^1.4.4", + "vite-plugin-wasm": "^3.3.0" } }, "node_modules/@adobe/css-tools": { @@ -3836,6 +3839,24 @@ } } }, + "node_modules/@rollup/plugin-virtual": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-virtual/-/plugin-virtual-3.0.2.tgz", + "integrity": "sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, "node_modules/@rollup/pluginutils": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.5.tgz", @@ -4183,6 +4204,232 @@ } } }, + "node_modules/@swc/core": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.26.tgz", + "integrity": "sha512-f5uYFf+TmMQyYIoxkn/evWhNGuUzC730dFwAKGwBVHHVoPyak1/GvJUm6i1SKl+2Hrj9oN0i3WSoWWZ4pgI8lw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.12" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.7.26", + "@swc/core-darwin-x64": "1.7.26", + "@swc/core-linux-arm-gnueabihf": "1.7.26", + "@swc/core-linux-arm64-gnu": "1.7.26", + "@swc/core-linux-arm64-musl": "1.7.26", + "@swc/core-linux-x64-gnu": "1.7.26", + "@swc/core-linux-x64-musl": "1.7.26", + "@swc/core-win32-arm64-msvc": "1.7.26", + "@swc/core-win32-ia32-msvc": "1.7.26", + "@swc/core-win32-x64-msvc": "1.7.26" + }, + "peerDependencies": { + "@swc/helpers": "*" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.26.tgz", + "integrity": "sha512-FF3CRYTg6a7ZVW4yT9mesxoVVZTrcSWtmZhxKCYJX9brH4CS/7PRPjAKNk6kzWgWuRoglP7hkjQcd6EpMcZEAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.26.tgz", + "integrity": "sha512-az3cibZdsay2HNKmc4bjf62QVukuiMRh5sfM5kHR/JMTrLyS6vSw7Ihs3UTkZjUxkLTT8ro54LI6sV6sUQUbLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.26.tgz", + "integrity": "sha512-VYPFVJDO5zT5U3RpCdHE5v1gz4mmR8BfHecUZTmD2v1JeFY6fv9KArJUpjrHEEsjK/ucXkQFmJ0jaiWXmpOV9Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.26.tgz", + "integrity": "sha512-YKevOV7abpjcAzXrhsl+W48Z9mZvgoVs2eP5nY+uoMAdP2b3GxC0Df1Co0I90o2lkzO4jYBpTMcZlmUXLdXn+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.26.tgz", + "integrity": "sha512-3w8iZICMkQQON0uIcvz7+Q1MPOW6hJ4O5ETjA0LSP/tuKqx30hIniCGOgPDnv3UTMruLUnQbtBwVCZTBKR3Rkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.26.tgz", + "integrity": "sha512-c+pp9Zkk2lqb06bNGkR2Looxrs7FtGDMA4/aHjZcCqATgp348hOKH5WPvNLBl+yPrISuWjbKDVn3NgAvfvpH4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.26.tgz", + "integrity": "sha512-PgtyfHBF6xG87dUSSdTJHwZ3/8vWZfNIXQV2GlwEpslrOkGqy+WaiiyE7Of7z9AvDILfBBBcJvJ/r8u980wAfQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.26.tgz", + "integrity": "sha512-9TNXPIJqFynlAOrRD6tUQjMq7KApSklK3R/tXgIxc7Qx+lWu8hlDQ/kVPLpU7PWvMMwC/3hKBW+p5f+Tms1hmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.26.tgz", + "integrity": "sha512-9YngxNcG3177GYdsTum4V98Re+TlCeJEP4kEwEg9EagT5s3YejYdKwVAkAsJszzkXuyRDdnHUpYbTrPG6FiXrQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.26.tgz", + "integrity": "sha512-VR+hzg9XqucgLjXxA13MtV5O3C0bK0ywtLIBw/+a+O+Oc6mxFWHtdUeXDbIi5AiPbn0fjgVJMqYnyjGyyX8u0w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.12.tgz", + "integrity": "sha512-wBJA+SdtkbFhHjTMYH+dEH1y4VpfGdAc2Kw/LK09i9bXd/K6j6PkDcFCEzb6iVfZMkPRrl/q0e3toqTAJdkIVA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", @@ -5296,6 +5543,12 @@ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" }, + "node_modules/boltz-bolt12": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/boltz-bolt12/-/boltz-bolt12-0.1.2.tgz", + "integrity": "sha512-bZ5CL5Hr6HYYRt0c7h7AZp/eeGot6G/6F7S4PrdWPm51EGHGxxo8FxtaaNQV8E1JALfHPWdkbGjhIFbmxHM9/w==", + "license": "MIT" + }, "node_modules/boltz-core": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/boltz-core/-/boltz-core-2.1.2.tgz", @@ -12305,6 +12558,20 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-to-istanbul": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", @@ -12454,6 +12721,31 @@ } } }, + "node_modules/vite-plugin-top-level-await": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/vite-plugin-top-level-await/-/vite-plugin-top-level-await-1.4.4.tgz", + "integrity": "sha512-QyxQbvcMkgt+kDb12m2P8Ed35Sp6nXP+l8ptGrnHV9zgYDUpraO0CPdlqLSeBqvY2DToR52nutDG7mIHuysdiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/plugin-virtual": "^3.0.2", + "@swc/core": "^1.7.0", + "uuid": "^10.0.0" + }, + "peerDependencies": { + "vite": ">=2.8" + } + }, + "node_modules/vite-plugin-wasm": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/vite-plugin-wasm/-/vite-plugin-wasm-3.3.0.tgz", + "integrity": "sha512-tVhz6w+W9MVsOCHzxo6SSMSswCeIw4HTrXEi6qL3IRzATl83jl09JVO1djBqPSwfjgnpVHNLYcaMbaDX5WB/pg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "vite": "^2 || ^3 || ^4 || ^5" + } + }, "node_modules/vitefu": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz", diff --git a/package.json b/package.json index 56ff2b16..7d8795c9 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,9 @@ "vite": "^5.4.8", "vite-plugin-mkcert": "^1.17.6", "vite-plugin-node-polyfills": "^0.22.0", - "vite-plugin-solid": "^2.10.2" + "vite-plugin-solid": "^2.10.2", + "vite-plugin-top-level-await": "^1.4.4", + "vite-plugin-wasm": "^3.3.0" }, "dependencies": { "@bitcoinerlab/secp256k1": "^1.1.1", @@ -62,6 +64,7 @@ "bignumber.js": "^9.1.2 ", "bitcoinjs-lib": "^6.1.6", "bolt11": "^1.4.1", + "boltz-bolt12": "^0.1.2", "boltz-core": "^2.1.2", "buffer": "^6.0.3", "create-hmac": "^1.1.7", diff --git a/regtest b/regtest index aa720429..a52774c2 160000 --- a/regtest +++ b/regtest @@ -1 +1 @@ -Subproject commit aa720429278435fdf226a16933a7fdb8cf1157fe +Subproject commit a52774c25af49bf652062c19968a60e32f6ad63b diff --git a/src/components/CreateButton.tsx b/src/components/CreateButton.tsx index 50897c1c..00dfc3dd 100644 --- a/src/components/CreateButton.tsx +++ b/src/components/CreateButton.tsx @@ -10,10 +10,11 @@ import { useCreateContext } from "../context/Create"; import { useGlobalContext } from "../context/Global"; import { useWeb3Signer } from "../context/Web3"; import { GasNeededToClaim, getSmartWalletAddress } from "../rif/Signer"; -import { getPairs } from "../utils/boltzClient"; +import { fetchBolt12Invoice, getPairs } from "../utils/boltzClient"; import { formatAmount } from "../utils/denomination"; +import { formatError } from "../utils/errors"; import { coalesceLn } from "../utils/helper"; -import { fetchLnurl } from "../utils/invoice"; +import { fetchBip353, fetchLnurl } from "../utils/invoice"; import { SomeSwap, createChain, @@ -58,6 +59,8 @@ export const CreateButton = () => { maximum, invoiceValid, invoiceError, + bolt12Offer, + setBolt12Offer, } = useCreateContext(); const { getEtherSwap, signer } = useWeb3Signer(); @@ -67,10 +70,10 @@ export const CreateButton = () => { key: "create_swap", }); - const validLnurl = () => { + const validWayToFetchInvoice = () => { return ( swapType() === SwapType.Submarine && - lnurl() !== "" && + (lnurl() !== "" || bolt12Offer() !== undefined) && amountValid() && sendAmount().isGreaterThan(0) ); @@ -93,6 +96,7 @@ export const CreateButton = () => { online, minimum, assetReceive, + bolt12Offer, ], () => { if (!online()) { @@ -131,7 +135,7 @@ export const CreateButton = () => { return; } } else { - if (validLnurl()) { + if (validWayToFetchInvoice()) { setButtonLabel({ key: "create_swap" }); return; } @@ -148,15 +152,64 @@ export const CreateButton = () => { ); const create = async () => { - if (validLnurl()) { - try { - const inv = await fetchLnurl(lnurl(), Number(receiveAmount())); - setInvoice(inv); - setLnurl(""); - } catch (e) { - notify("error", e.message); - log.warn("fetch lnurl failed", e); - return; + if (validWayToFetchInvoice()) { + if (lnurl() !== undefined && lnurl() !== "") { + log.info("Fetching invoice from LNURL or BIP-353", lnurl()); + + const fetchResults = await Promise.allSettled([ + (() => { + try { + return fetchLnurl(lnurl(), Number(receiveAmount())); + } catch (e) { + log.warn("Fetching invoice from LNURL failed", e); + throw e; + } + })(), + (() => { + try { + return fetchBip353( + lnurl(), + Number(receiveAmount()), + ); + } catch (e) { + log.warn("Fetching invoice from BIP-353 failed", e); + throw e; + } + })(), + ]); + + const fetched = fetchResults.find( + (res) => res.status === "fulfilled", + ); + if (fetched !== undefined) { + setInvoice(fetched.value); + setLnurl(""); + } else { + // All failed, so we can safely cast the first one + notify( + "error", + (fetchResults[0] as PromiseRejectedResult).reason, + ); + return; + } + } else { + log.info("Fetching invoice from bolt12 offer", bolt12Offer()); + try { + const res = await fetchBolt12Invoice( + bolt12Offer(), + Number(receiveAmount()), + ); + setInvoice(res.invoice); + setBolt12Offer(undefined); + } catch (e) { + if (typeof e.json === "function") { + e = (await e.json()).error; + } + + notify("error", formatError(e)); + log.warn("Fetching invoice from bol12 failed", e); + return; + } } } @@ -286,7 +339,9 @@ export const CreateButton = () => { data-testid="create-swap-button" class={buttonClass()} disabled={ - !online() || !(valid() || validLnurl()) || buttonDisable() + !online() || + !(valid() || validWayToFetchInvoice()) || + buttonDisable() } onClick={buttonClick}> {getButtonLabel(buttonLabel())} diff --git a/src/components/InvoiceInput.tsx b/src/components/InvoiceInput.tsx index c829aca0..9dd9e943 100644 --- a/src/components/InvoiceInput.tsx +++ b/src/components/InvoiceInput.tsx @@ -11,6 +11,7 @@ import { decodeInvoice, extractAddress, extractInvoice, + isBolt12Offer, isLnurl, } from "../utils/invoice"; import { validateInvoice } from "../utils/validation"; @@ -37,6 +38,7 @@ const InvoiceInput = () => { assetSend, setAssetReceive, setOnchainAddress, + setBolt12Offer, } = useCreateContext(); const validate = (input: HTMLTextAreaElement) => { @@ -62,6 +64,8 @@ const InvoiceInput = () => { input.classList.remove("invalid"); if (isLnurl(inputValue)) { setLnurl(inputValue); + } else if (isBolt12Offer(inputValue)) { + setBolt12Offer(inputValue); } else { const sats = validateInvoice(inputValue); setReceiveAmount(BigNumber(sats)); @@ -74,11 +78,13 @@ const InvoiceInput = () => { ), ); setInvoice(inputValue); + setBolt12Offer(undefined); setLnurl(""); setInvoiceValid(true); } } catch (e) { setInvoiceValid(false); + setBolt12Offer(undefined); setLnurl(""); setInvoiceError(e.message); if (inputValue.length !== 0) { diff --git a/src/config.ts b/src/config.ts index 1ce8410e..b88832cc 100644 --- a/src/config.ts +++ b/src/config.ts @@ -23,6 +23,7 @@ const defaults = { testnetUrl: "https://testnet.boltz.exchange", telegramUrl: "https://t.me/boltzhq", email: "hi@bol.tz", + dnsOverHttps: "https://1.1.1.1/dns-query", }; type Asset = { diff --git a/src/context/Create.tsx b/src/context/Create.tsx index df4bffc1..cb9824b6 100644 --- a/src/context/Create.tsx +++ b/src/context/Create.tsx @@ -125,6 +125,8 @@ export type CreateContextType = { setInvoice: Setter; lnurl: Accessor; setLnurl: Setter; + bolt12Offer: Accessor; + setBolt12Offer: Setter; onchainAddress: Accessor; setOnchainAddress: Setter; assetSend: Accessor; @@ -175,6 +177,9 @@ const CreateProvider = (props: { children: any }) => { const [swapType, setSwapType] = createSignal(SwapType.Submarine); const [invoice, setInvoice] = createSignal(""); const [lnurl, setLnurl] = createSignal(""); + const [bolt12Offer, setBolt12Offer] = createSignal( + undefined, + ); const [onchainAddress, setOnchainAddress] = createSignal(""); const [assetReceive, setAssetReceive] = makePersisted( @@ -258,6 +263,8 @@ const CreateProvider = (props: { children: any }) => { setInvoice, lnurl, setLnurl, + bolt12Offer, + setBolt12Offer, onchainAddress, setOnchainAddress, assetSend, diff --git a/src/utils/boltzClient.ts b/src/utils/boltzClient.ts index 5da0592a..2b683c6d 100644 --- a/src/utils/boltzClient.ts +++ b/src/utils/boltzClient.ts @@ -178,6 +178,15 @@ export const getPairs = async (): Promise => { }; }; +export const fetchBolt12Invoice = async ( + offer: string, + amountSat: number, +): Promise<{ invoice: string }> => + fetcher("/v2/lightning/BTC/bolt12/fetch", { + offer, + amount: amountSat, + }); + export const createSubmarineSwap = ( from: string, to: string, diff --git a/src/utils/invoice.ts b/src/utils/invoice.ts index 5db793ac..0613f617 100644 --- a/src/utils/invoice.ts +++ b/src/utils/invoice.ts @@ -1,9 +1,11 @@ import { bech32, utf8 } from "@scure/base"; import { BigNumber } from "bignumber.js"; import bolt11 from "bolt11"; +import { Invoice, Offer } from "boltz-bolt12"; import log from "loglevel"; import { config } from "../config"; +import { fetchBolt12Invoice } from "./boltzClient"; import { checkResponse } from "./http"; type LnurlResponse = { @@ -29,6 +31,8 @@ const bolt11Prefixes = { regtest: "lnbcrt", }; +const bip353Prefix = "₿"; + export const getExpiryEtaHours = (invoice: string): number => { const decoded = decodeInvoice(invoice); const now = Date.now() / 1000; @@ -60,7 +64,18 @@ export const decodeInvoice = ( ).data as string, }; } catch (e) { - throw new Error("invalid_invoice"); + try { + const decoded = new Invoice(invoice); + const res = { + satoshis: Number(decoded.amount_msat / 1_000n), + preimageHash: Buffer.from(decoded.payment_hash).toString("hex"), + }; + + decoded.free(); + return res; + } catch (e) { + throw new Error("invalid_invoice"); + } } }; @@ -92,6 +107,37 @@ export const fetchLnurl = ( }); }; +export const fetchBip353 = async ( + bip353: string, + amountSat: number, +): Promise => { + const split = bip353.split("@"); + if (split.length !== 2) { + throw "invalid BIP-353"; + } + + if (split[0].startsWith(bip353Prefix)) { + split[0] = split[0].substring(bip353Prefix.length); + } + + log.debug(`Fetching BIP-353: ${bip353}`); + + const params = new URLSearchParams({ + type: "TXT", + name: `${split[0]}.user._bitcoin-payment.${split[1]}`, + }); + const res = await fetch(`${config.dnsOverHttps}?${params.toString()}`, { + headers: { + Accept: "application/dns-json", + }, + }); + const resBody = await res.json(); + const paymentRequest = resBody.Answer[0].data; + const offer = new URLSearchParams(paymentRequest.split("?")[1]).get("lno"); + return (await fetchBolt12Invoice(offer.replaceAll('"', ""), amountSat)) + .invoice; +}; + const checkLnurlResponse = (amount: number, data: LnurlResponse) => { log.debug( "amount check: (x, min, max)", @@ -150,7 +196,7 @@ export const isInvoice = (data: string) => { if (prefix === bolt11Prefixes.mainnet && startsWithPrefix) { return !data.toLowerCase().startsWith(bolt11Prefixes.regtest); } - return startsWithPrefix; + return startsWithPrefix || data.toLowerCase().startsWith("lni"); }; const isValidBech32 = (data: string) => { @@ -172,3 +218,12 @@ export const isLnurl = (data: string) => { (data.startsWith("lnurl") && isValidBech32(data)) ); }; + +export const isBolt12Offer = (offer: string) => { + try { + new Offer(offer); + return true; + } catch (e) { + return false; + } +}; diff --git a/tests/mocks/bolt12.ts b/tests/mocks/bolt12.ts new file mode 100644 index 00000000..e69de29b diff --git a/vite.config.mjs b/vite.config.mjs index 3a7cd37f..ad8d0137 100644 --- a/vite.config.mjs +++ b/vite.config.mjs @@ -3,6 +3,8 @@ import { defineConfig } from "vite"; import mkcert from "vite-plugin-mkcert"; import { nodePolyfills } from "vite-plugin-node-polyfills"; import solidPlugin from "vite-plugin-solid"; +import topLevelAwait from "vite-plugin-top-level-await"; +import wasm from "vite-plugin-wasm"; const commitHash = child .execSync("git rev-parse --short HEAD") @@ -10,7 +12,13 @@ const commitHash = child .trim(); export default defineConfig({ - plugins: [solidPlugin(), mkcert(), nodePolyfills()], + plugins: [ + solidPlugin(), + wasm(), + topLevelAwait(), + mkcert(), + nodePolyfills(), + ], server: { https: true, cors: { origin: "*" }, From 8e32075485ddcea9d02a11ddb548b227ff8158cb Mon Sep 17 00:00:00 2001 From: Kilian <19181985+kilrau@users.noreply.github.com> Date: Fri, 4 Oct 2024 21:32:54 +0200 Subject: [PATCH 2/2] chore: reword for bolt12 --- src/i18n/i18n.ts | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts index c2bf8fa5..593311f0 100644 --- a/src/i18n/i18n.ts +++ b/src/i18n/i18n.ts @@ -50,8 +50,7 @@ const dict = { success_swap: "Swap Success", feecheck: "Network fee was updated based on network situation, please confirm new amounts and continue with swap.", - create_and_paste: - "Paste a bolt11 lightning invoice\n or a Lightning address\nor a LNURL Paylink", + create_and_paste: "Paste a Lightning invoice, offer or LNURL", congrats: "Congratulations!", successfully_swapped: "You successfully received {{ amount }} {{ denomination }}!", @@ -79,8 +78,7 @@ const dict = { download_refund_file: "Download refund file", invalid_refund_file: "Invalid refund file", invalid_backup_file: "Invalid backup file", - invalid_invoice: - "Please provide a valid LN invoice, LNAddress or LNURL", + invalid_invoice: "Please provide a valid invoice, offer or LNURL", invalid_0_amount: "Invoices without amount are not supported", copy_invoice: "lightning invoice", copy_address: "address", @@ -266,8 +264,7 @@ const dict = { success_swap: "Swap erfolgreich!", feecheck: "Die Netzwerkgebühr wurde aufgrund der Netzwerksituation aktualisiert. Bitte bestätige die neuen Beträge und fahren mit dem Swap fort.", - create_and_paste: - "Füge eine bolt11 Lightning-Rechnung\n eine Lightning-Adresse\n oder einen LNURL Paylink hier ein", + create_and_paste: "Füge Lightning Invoice, Offer oder LNURL hier ein", congrats: "Herzlichen Glückwunsch!", successfully_swapped: "Du hast erfolgreich {{ amount }} {{ denomination }} empfangen!", @@ -296,7 +293,7 @@ const dict = { download_refund_file: "Rückerstattungsdatei herunterladen", invalid_refund_file: "Ungültige Rückerstattungsdatei", invalid_backup_file: "Ungültige Backupdatei", - invalid_invoice: "Bitte gültige Rechnung, LNAdresse/LNURL eingeben", + invalid_invoice: "Bitte gültige Invoice, Offer oder LNURL eingeben", invalid_0_amount: "Lightning Rechnungen ohne Betrag nicht unterstützt", copy_invoice: "Lightning-Rechnung", copy_address: "Adresse", @@ -489,8 +486,7 @@ const dict = { success_swap: "Intercambio realizado con éxito!", feecheck: "La comisión de red se actualizó según la situación de la red. Por favor, confirma los nuevos importes y continúa con el intercambio.", - create_and_paste: - "Pegar una factura Lightning bolt11\n o una dirección Lightning\n o un enlace LNURL Pay", + create_and_paste: "Pegar Lightning Invoice, Offer o LNURL", congrats: "¡Felicitaciones!", successfully_swapped: "Has recibido con éxito {{ amount }} {{ denomination }}!", @@ -517,7 +513,7 @@ const dict = { download_refund_file: "Descargar archivo de reembolso", invalid_refund_file: "Archivo de reembolso no válido", invalid_backup_file: "Archivo de backup no válido", - invalid_invoice: "Por favor, pegue una factura, LNAddress/LNURL válida", + invalid_invoice: "Por favor, pegue Invoice, Offer o LNURL válida", invalid_0_amount: "No se admiten facturas sin importe", copy_invoice: "factura Lightning", copy_address: "dirección", @@ -709,8 +705,7 @@ const dict = { new_swap: "新的交换", success_swap: "交换成功", feecheck: "根据网络情况更新了网络费用,请确认新的金额并继续进行交换。", - create_and_paste: - "粘贴一个bolt11闪电发票\n或闪电网络地址\n或LNURL支付链接", + create_and_paste: "粘贴闪电发票,offer或LNURL", congrats: "恭喜!", successfully_swapped: "您成功收到{{ amount }}{{ denomination }}!", timeout_eta: "超过预期时间", @@ -736,7 +731,7 @@ const dict = { download_refund_file: "下载退款文件", invalid_refund_file: "无效的退款文件", invalid_backup_file: "无效的备份文件", - invalid_invoice: "请提供有效的闪电网络发票、LNAddress或LNURL", + invalid_invoice: "请提供有效的发票,offer或LNURL", invalid_0_amount: "不支持没有金额的发票", copy_invoice: "闪电网络发票", copy_address: "地址",