From b0a772a1b7e39f9428df4d23936f6deb55ed92c3 Mon Sep 17 00:00:00 2001 From: michael1011 Date: Thu, 12 Sep 2024 21:36:00 +0200 Subject: [PATCH] feat: bolt12 support for submarine swaps --- package-lock.json | 294 +++++++++++++++++++++++++++++++- package.json | 5 +- src/components/CreateButton.tsx | 53 ++++-- src/components/InvoiceInput.tsx | 6 + src/context/Create.tsx | 7 + src/utils/boltzClient.ts | 9 + src/utils/invoice.ts | 25 ++- vite.config.mjs | 10 +- 8 files changed, 390 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5290b3c5..7a06b3ac 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.1", "boltz-core": "^2.1.2", "buffer": "^6.0.3", "create-hmac": "^1.1.7", @@ -61,7 +62,9 @@ "vite": "^5.4.1", "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", @@ -4128,6 +4149,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", @@ -5240,6 +5487,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.1", + "resolved": "https://registry.npmjs.org/boltz-bolt12/-/boltz-bolt12-0.1.1.tgz", + "integrity": "sha512-awpLmhzduNaX+pYIfsxhvv7E56wl6KY5gpjU9CUjyNuMjkfFRfB5xW1ljD+x5tXMtAaVQl9pSnAITomy0Tw+kw==", + "license": "MIT" + }, "node_modules/boltz-core": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/boltz-core/-/boltz-core-2.1.2.tgz", @@ -12243,6 +12496,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", @@ -12392,6 +12659,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 bb83c5cf..b5e8850f 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,9 @@ "vite": "^5.4.1", "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.1", "boltz-core": "^2.1.2", "buffer": "^6.0.3", "create-hmac": "^1.1.7", diff --git a/src/components/CreateButton.tsx b/src/components/CreateButton.tsx index a948c06e..ad784ca8 100644 --- a/src/components/CreateButton.tsx +++ b/src/components/CreateButton.tsx @@ -10,8 +10,9 @@ 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 { @@ -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) ); @@ -131,7 +134,7 @@ export const CreateButton = () => { return; } } else { - if (validLnurl()) { + if (validWayToFetchInvoice()) { setButtonLabel({ key: "create_swap" }); return; } @@ -148,15 +151,35 @@ 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", lnurl()); + try { + const inv = await fetchLnurl( + lnurl(), + Number(receiveAmount()), + ); + setInvoice(inv); + setLnurl(""); + } catch (e) { + notify("error", e.message); + log.warn("fetch lnurl failed", e); + 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) { + notify("error", formatError(e)); + log.warn("Fetching invoice from bol12 failed", e); + return; + } } } @@ -286,7 +309,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/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..c918a161 100644 --- a/src/utils/invoice.ts +++ b/src/utils/invoice.ts @@ -1,6 +1,7 @@ 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"; @@ -60,7 +61,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"); + } } }; @@ -150,7 +162,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 +184,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/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: "*" },