diff --git a/package-lock.json b/package-lock.json index df5d4b2d..75ae7195 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@reown/appkit": "^1.6.5", "@reown/appkit-adapter-ethers": "^1.6.5", "@scure/base": "^1.2.4", + "@scure/bip32": "^1.6.2", "@solid-primitives/i18n": "^2.1.1", "@solid-primitives/storage": "^4.2.1", "@solidjs/router": "^0.15.2", @@ -3826,12 +3827,12 @@ } }, "node_modules/@noble/curves": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.7.0.tgz", - "integrity": "sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz", + "integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==", "license": "MIT", "dependencies": { - "@noble/hashes": "1.6.0" + "@noble/hashes": "1.7.1" }, "engines": { "node": "^14.21.3 || >=16" @@ -3841,9 +3842,9 @@ } }, "node_modules/@noble/curves/node_modules/@noble/hashes": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.0.tgz", - "integrity": "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", + "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", "license": "MIT", "engines": { "node": "^14.21.3 || >=16" @@ -4728,52 +4729,31 @@ } }, "node_modules/@scure/bip32": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz", - "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.6.2.tgz", + "integrity": "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==", "license": "MIT", "dependencies": { - "@noble/curves": "~1.4.0", - "@noble/hashes": "~1.4.0", - "@scure/base": "~1.1.6" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32/node_modules/@noble/curves": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", - "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.4.0" + "@noble/curves": "~1.8.1", + "@noble/hashes": "~1.7.1", + "@scure/base": "~1.2.2" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@scure/bip32/node_modules/@noble/hashes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", - "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", + "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", "license": "MIT", "engines": { - "node": ">= 16" + "node": "^14.21.3 || >=16" }, "funding": { "url": "https://paulmillr.com/funding/" } }, - "node_modules/@scure/bip32/node_modules/@scure/base": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", - "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@scure/bip39": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz", @@ -9729,6 +9709,29 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/ethereum-cryptography/node_modules/@scure/base": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethereum-cryptography/node_modules/@scure/bip32": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz", + "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.4.0", + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/ethers": { "version": "6.13.5", "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.13.5.tgz", @@ -13540,20 +13543,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/ox/node_modules/@scure/bip32": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.6.0.tgz", - "integrity": "sha512-82q1QfklrUUdXJzjuRU7iG7D7XiFx5PHYVS0+oeNKhyDLT7WPqs6pBcM2W5ZdwOwKCwoE1Vy1se+DHjcXwCYnA==", - "license": "MIT", - "dependencies": { - "@noble/curves": "~1.7.0", - "@noble/hashes": "~1.6.0", - "@scure/base": "~1.2.1" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/ox/node_modules/@scure/bip39": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.5.0.tgz", diff --git a/package.json b/package.json index 8333741f..ed45e1c3 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "@reown/appkit": "^1.6.5", "@reown/appkit-adapter-ethers": "^1.6.5", "@scure/base": "^1.2.4", + "@scure/bip32": "^1.6.2", "@solid-primitives/i18n": "^2.1.1", "@solid-primitives/storage": "^4.2.1", "@solidjs/router": "^0.15.2", diff --git a/src/components/CreateButton.tsx b/src/components/CreateButton.tsx index 235b9d56..66fba1e0 100644 --- a/src/components/CreateButton.tsx +++ b/src/components/CreateButton.tsx @@ -14,7 +14,7 @@ import { fetchBolt12Invoice, getPairs } from "../utils/boltzClient"; import { formatAmount } from "../utils/denomination"; import { formatError } from "../utils/errors"; import { HardwareSigner } from "../utils/hardware/HadwareSigner"; -import { coalesceLn, isMobile } from "../utils/helper"; +import { coalesceLn } from "../utils/helper"; import { fetchBip353, fetchLnurl } from "../utils/invoice"; import { SomeSwap, @@ -36,7 +36,8 @@ export const CreateButton = () => { notify, ref, t, - isRecklessMode, + newKey, + deriveKey, } = useGlobalContext(); const { invoice, @@ -63,8 +64,7 @@ export const CreateButton = () => { bolt12Offer, setBolt12Offer, } = useCreateContext(); - const { getEtherSwap, signer, providers, hasBrowserWallet } = - useWeb3Signer(); + const { getEtherSwap, signer, providers } = useWeb3Signer(); const [buttonDisable, setButtonDisable] = createSignal(false); const [buttonClass, setButtonClass] = createSignal("btn"); @@ -291,6 +291,7 @@ export const CreateButton = () => { invoice(), ref(), useRif, + newKey, ); break; @@ -304,6 +305,7 @@ export const CreateButton = () => { claimAddress, ref(), useRif, + newKey, ); break; @@ -317,11 +319,12 @@ export const CreateButton = () => { claimAddress, ref(), useRif, + newKey, ); break; } - if (!(await validateResponse(data, getEtherSwap))) { + if (!(await validateResponse(data, deriveKey, getEtherSwap))) { navigate("/error"); return; } @@ -349,21 +352,7 @@ export const CreateButton = () => { setOnchainAddress(""); setAddressValid(false); - // Mobile EVM browsers struggle with downloading files - const isMobileEvmBrowser = () => isMobile() && hasBrowserWallet(); - - // No backups needed for Reverse Swaps or when we send RBTC - if ( - isRecklessMode() || - swapType() === SwapType.Reverse || - assetSend() === RBTC || - // Only disable refund files on mobile EVM browsers when one side is RSK - (assetReceive() === RBTC && isMobileEvmBrowser()) - ) { - navigate("/swap/" + data.id); - } else { - navigate("/swap/refund/" + data.id); - } + navigate("/swap/" + data.id); } catch (err) { if (err === "invalid pair hash") { setPairs(await getPairs()); diff --git a/src/components/DownloadRefund.tsx b/src/components/DownloadRefund.tsx deleted file mode 100644 index 15b0b108..00000000 --- a/src/components/DownloadRefund.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import log from "loglevel"; -import QRCode from "qrcode/lib/server"; - -import { useGlobalContext } from "../context/Global"; -import { usePayContext } from "../context/Pay"; -import { download, downloadJson, getRefundFileName } from "../utils/download"; -import { isIos, isMobile } from "../utils/helper"; - -const downloadRefundJson = (swap: { id: string } & Record) => { - downloadJson(getRefundFileName(swap.id), swap); -}; - -const DownloadRefund = () => { - const { swap } = usePayContext(); - const { t } = useGlobalContext(); - - const downloadRefundQr = ( - swap: { id: string } & Record, - ) => { - QRCode.toDataURL(JSON.stringify(swap), { - width: 2500, - errorCorrectionLevel: "L", - }) - .then((url: string) => { - if (isIos()) { - // Compatibility with third party iOS browsers - const newTab = window.open(); - newTab.document.body.innerHTML = ` - - -

${t("ios_image_download_do_not_share")}

-

${t("ios_image_download")}

- - `; - } else { - download(`${getRefundFileName(swap.id)}.png`, url); - } - }) - .catch((err: Error) => { - log.error("qr code generation error", err); - }); - }; - - return ( - - ); -}; - -export default DownloadRefund; diff --git a/src/components/RefundButton.tsx b/src/components/RefundButton.tsx index 48ee6423..2ee84200 100644 --- a/src/components/RefundButton.tsx +++ b/src/components/RefundButton.tsx @@ -121,6 +121,7 @@ export const RefundBtc = (props: { notify, externalBroadcast, t, + deriveKey, } = useGlobalContext(); const { setSwap } = usePayContext(); @@ -163,6 +164,7 @@ export const RefundBtc = (props: { try { const res = await refund( + deriveKey, props.swap(), refundAddress(), lockupTransaction(), diff --git a/src/components/SwapChecker.tsx b/src/components/SwapChecker.tsx index a533f0a6..4fc1882e 100644 --- a/src/components/SwapChecker.tsx +++ b/src/components/SwapChecker.tsx @@ -2,7 +2,6 @@ import { OutputType } from "boltz-core"; import log from "loglevel"; import { createEffect, onCleanup, onMount } from "solid-js"; -import { config } from "../config"; import { BTC, LBTC, RBTC } from "../consts/Assets"; import { SwapType } from "../consts/Enums"; import { @@ -53,7 +52,6 @@ class BoltzWebSocket { constructor( private readonly url: string, - private readonly wsFallback: string | undefined, private readonly relevantIds: Set, private readonly prepareSwap: (id: string, status: SwapStatus) => void, private readonly claimSwap: ( @@ -64,12 +62,7 @@ class BoltzWebSocket { public connect = () => { log.debug("Opening WebSocket"); - this.openWebSocket(`${this.url}/v2/ws`).catch(() => { - if (this.wsFallback !== undefined) { - log.debug("Opening fallback WebSocket"); - void this.openWebSocket(this.wsFallback).then().catch(); - } - }); + void this.openWebSocket(`${this.url}/v2/ws`); }; public close = () => { @@ -179,6 +172,7 @@ export const SwapChecker = () => { setSwapStorage, externalBroadcast, t, + deriveKey, } = useGlobalContext(); let ws: BoltzWebSocket | undefined = undefined; @@ -266,6 +260,7 @@ export const SwapChecker = () => { ) { try { const res = await claim( + deriveKey, currentSwap as ReverseSwap | ChainSwap, data.transaction as { hex: string }, true, @@ -294,7 +289,10 @@ export const SwapChecker = () => { data.status === swapStatusPending.TransactionClaimPending ) { try { - await createSubmarineSignature(currentSwap as SubmarineSwap); + await createSubmarineSignature( + deriveKey, + currentSwap as SubmarineSwap, + ); notify( "success", t("swap_completed", { id: currentSwap.id }), @@ -329,7 +327,10 @@ export const SwapChecker = () => { log.debug( `Helping server claim ${swap.assetSend} of Chain Swap ${swap.id}`, ); - const sig = await createTheirPartialChainSwapSignature(swap); + const sig = await createTheirPartialChainSwapSignature( + deriveKey, + swap, + ); await postChainSwapDetails(swap.id, undefined, sig); } catch (e) { log.warn( @@ -350,7 +351,6 @@ export const SwapChecker = () => { ws = new BoltzWebSocket( getApiUrl(), - config.apiUrl.wsFallback, new Set(swapsToCheck.map((s) => s.id)), prepareSwap, claimSwap, diff --git a/src/components/SwapIcons.tsx b/src/components/SwapIcons.tsx index a732cff3..6e2aaa70 100644 --- a/src/components/SwapIcons.tsx +++ b/src/components/SwapIcons.tsx @@ -1,27 +1,37 @@ import { VsArrowSmallRight } from "solid-icons/vs"; +import { Show } from "solid-js"; import { LN } from "../consts/Assets"; import { SwapType } from "../consts/Enums"; +import { RecoverableSwap } from "../utils/boltzClient"; import { SomeSwap } from "../utils/swapCreator"; -export const SwapIcons = (props: { swap: SomeSwap }) => { +export const SwapIcons = (props: { swap: SomeSwap | RecoverableSwap }) => { return ( - - - - - + + + + }> + + + + + + ); }; diff --git a/src/components/SwapList.tsx b/src/components/SwapList.tsx index 7534f6d1..af9d23e9 100644 --- a/src/components/SwapList.tsx +++ b/src/components/SwapList.tsx @@ -4,25 +4,37 @@ import { Accessor, For, Show, createEffect, createSignal } from "solid-js"; import { useGlobalContext } from "../context/Global"; import "../style/swaplist.scss"; +import type { RecoverableSwap } from "../utils/boltzClient"; import { SomeSwap } from "../utils/swapCreator"; import { SwapIcons } from "./SwapIcons"; +type Swap = SomeSwap | RecoverableSwap; + +const getSwapDate = (swap: Swap) => { + if ("date" in swap) { + return swap.date; + } + + return swap.createdAt; +}; + const SwapList = (props: { - swapsSignal: Accessor; + swapsSignal: Accessor; action: string; onDelete?: () => Promise; }) => { const navigate = useNavigate(); const { deleteSwap, t } = useGlobalContext(); - const [sortedSwaps, setSortedSwaps] = createSignal([]); + const [sortedSwaps, setSortedSwaps] = createSignal([]); const [lastSwap, setLastSwap] = createSignal(); createEffect(() => { - const sorted = props - .swapsSignal() - .sort((a: SomeSwap, b: SomeSwap) => - a.date > b.date ? -1 : a.date === b.date ? 0 : 1, - ); + const sorted = props.swapsSignal().sort((a: Swap, b: Swap) => { + const aDate = getSwapDate(a); + const bDate = getSwapDate(b); + + return aDate > bDate ? -1 : aDate === bDate ? 0 : 1; + }); setSortedSwaps(sorted); setLastSwap(sorted[sorted.length - 1]); }); @@ -62,7 +74,7 @@ const SwapList = (props: { {t("created")}:  - {formatDate(swap.date)} + {formatDate(getSwapDate(swap))} diff --git a/src/components/settings/RecklessModeSetting.tsx b/src/components/settings/RecklessModeSetting.tsx deleted file mode 100644 index 68223ba2..00000000 --- a/src/components/settings/RecklessModeSetting.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { useGlobalContext } from "../../context/Global"; - -const RecklessModeSetting = () => { - const { t, isRecklessMode, setRecklessMode } = useGlobalContext(); - - const toggle = (evt: MouseEvent) => { - setRecklessMode(!isRecklessMode()); - evt.stopPropagation(); - }; - - return ( - <> -
- {t("on")} - - {t("off")} - -
- - ); -}; - -export default RecklessModeSetting; diff --git a/src/components/settings/SettingsMenu.tsx b/src/components/settings/SettingsMenu.tsx index c380014b..7fd49379 100644 --- a/src/components/settings/SettingsMenu.tsx +++ b/src/components/settings/SettingsMenu.tsx @@ -1,8 +1,6 @@ import { IoClose } from "solid-icons/io"; import type { JSXElement } from "solid-js"; -import { Show } from "solid-js"; -import { config } from "../../config"; import { useGlobalContext } from "../../context/Global"; import type { DictKey } from "../../i18n/i18n"; import "../../style/settings.scss"; @@ -11,7 +9,6 @@ import BroadcastSetting from "./BroadcastSetting"; import BrowserNotification from "./BrowserNotification"; import Denomination from "./Denomination"; import Logs from "./Logs"; -import RecklessModeSetting from "./RecklessModeSetting"; import Separator from "./Separator"; import Tooltip from "./Tooltip"; @@ -71,13 +68,6 @@ const SettingsMenu = () => { tooltipLabel={"broadcast_setting_tooltip"} settingElement={} /> - - } - /> - ECPairInterface; +export type newKeyFn = () => { index: number; key: ECPairInterface }; + export type GlobalContextType = { online: Accessor; setOnline: Setter; @@ -83,9 +94,6 @@ export type GlobalContextType = { getLogs: () => Promise>; clearLogs: () => Promise; - isRecklessMode: Accessor; - setRecklessMode: Setter; - setSwapStorage: (swap: SomeSwap) => Promise; getSwap: (id: string) => Promise; getSwaps: () => Promise; @@ -101,6 +109,10 @@ export type GlobalContextType = { externalBroadcast: Accessor; setExternalBroadcast: Setter; + + newKey: newKeyFn; + deriveKey: deriveKeyFn; + getXpub: () => string; }; const defaultReferral = () => { @@ -195,6 +207,45 @@ const GlobalProvider = (props: { children: JSX.Element }) => { }, ); + const [recoveryFile, setRecoveryFile] = makePersisted( + // eslint-disable-next-line solid/reactivity + createSignal(null), + { + name: "recoveryFile", + }, + ); + + const [lastUsedKey, setLastUsedKey] = makePersisted( + // eslint-disable-next-line solid/reactivity + createSignal(0), + { + name: "lastUsedKey", + }, + ); + + createEffect(() => { + if (recoveryFile() === null) { + log.debug("Generating recovery file"); + setRecoveryFile(generateRecoveryFile()); + } + }); + + const deriveKeyWrapper = (index: number) => { + return ECPair.fromPrivateKey( + Buffer.from(deriveKey(recoveryFile(), index).privateKey), + ); + }; + + const newKey = () => { + const index = lastUsedKey(); + setLastUsedKey(index + 1); + return { index, key: deriveKeyWrapper(index) }; + }; + + const getXpubWrapper = () => { + return getXpub(recoveryFile()); + }; + const notify = ( type: string, message: unknown, @@ -339,14 +390,6 @@ const GlobalProvider = (props: { children: JSX.Element }) => { }, ); - const [isRecklessMode, setRecklessMode] = makePersisted( - // eslint-disable-next-line solid/reactivity - createSignal(false), - { - name: "recklessMode", - }, - ); - const [hardwareDerivationPath, setHardwareDerivationPath] = makePersisted( // eslint-disable-next-line solid/reactivity createSignal(""), @@ -429,8 +472,6 @@ const GlobalProvider = (props: { children: JSX.Element }) => { deleteSwap, getSwaps, clearSwaps, - isRecklessMode, - setRecklessMode, setRdns, getRdnsForAddress, @@ -439,6 +480,10 @@ const GlobalProvider = (props: { children: JSX.Element }) => { externalBroadcast, setExternalBroadcast, + + newKey, + deriveKey: deriveKeyWrapper, + getXpub: getXpubWrapper, }}> {props.children} diff --git a/src/index.tsx b/src/index.tsx index 688fc0d0..7b66d5b6 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -26,10 +26,11 @@ import Pay from "./pages/Pay"; import Refund from "./pages/Refund"; import RefundEvm from "./pages/RefundEvm"; import RefundExternal from "./pages/RefundExternal"; -import RefundStep from "./pages/RefundStep"; import "./style/index.scss"; import "./utils/patches"; +// TODO: cleanup i18n + if ("serviceWorker" in navigator) { void navigator.serviceWorker .register("/service-worker.js", { scope: "/" }) @@ -103,7 +104,6 @@ const cleanup = render( path="/swap/refund/evm/:asset/:txHash" component={RefundEvm} /> - } /> diff --git a/src/pages/RefundExternal.tsx b/src/pages/RefundExternal.tsx index c069b020..cce80d79 100644 --- a/src/pages/RefundExternal.tsx +++ b/src/pages/RefundExternal.tsx @@ -2,35 +2,88 @@ import { useNavigate, useParams } from "@solidjs/router"; import log from "loglevel"; import QrScanner from "qr-scanner"; import { + Accessor, For, Match, Show, Switch, createEffect, + createResource, createSignal, onCleanup, } from "solid-js"; import BlockExplorer from "../components/BlockExplorer"; import ConnectWallet from "../components/ConnectWallet"; +import LoadingSpinner from "../components/LoadingSpinner"; import RefundButton from "../components/RefundButton"; +import SwapList from "../components/SwapList"; import SwapListLogs from "../components/SwapListLogs"; import SettingsCog from "../components/settings/SettingsCog"; import SettingsMenu from "../components/settings/SettingsMenu"; import { useGlobalContext } from "../context/Global"; import { useWeb3Signer } from "../context/Web3"; import "../style/tabs.scss"; +import { getRecoverableSwaps } from "../utils/boltzClient"; import { LogRefundData, scanLogsForPossibleRefunds, } from "../utils/contractLogs"; -import { validateRefundFile } from "../utils/refundFile"; +import { formatError } from "../utils/errors"; +import { getXpub } from "../utils/recoveryFile"; +import { validateRecoveryFile, validateRefundFile } from "../utils/refundFile"; +import { ChainSwap, SubmarineSwap } from "../utils/swapCreator"; import ErrorWasm from "./ErrorWasm"; enum RefundError { InvalidData, } +enum RefundType { + Recovery, + Legacy, +} + +const BtcLikeLegacy = (props: { + refundJson: Accessor; + refundInvalid: Accessor; +}) => { + const { t } = useGlobalContext(); + + const [refundTxId, setRefundTxId] = createSignal(""); + + return ( + <> + +
+ +
+ +
+

{t("refunded")}

+
+ ) + .asset || props.refundJson().assetSend + } + txId={refundTxId()} + /> +
+ + + ); +}; + export const RefundBtcLike = () => { const { t } = useGlobalContext(); @@ -38,21 +91,45 @@ export const RefundBtcLike = () => { RefundError | undefined >(undefined); const [refundJson, setRefundJson] = createSignal(null); - const [refundTxId, setRefundTxId] = createSignal(""); + const [refundType, setRefundType] = createSignal(); + + const [recoverySwaps] = createResource( + () => ({ refundJson: refundJson(), type: refundType() }), + async (source) => { + if ( + source.type !== RefundType.Recovery || + source.refundJson === null + ) { + return undefined; + } + + return await getRecoverableSwaps(getXpub(source.refundJson)); + }, + ); const checkRefundJsonKeys = ( - input: HTMLInputElement, json: Record, ) => { log.debug("checking refund json", json); try { - const data = validateRefundFile(json); + if ("xpriv" in json) { + log.info("Found recovery file"); + setRefundType(RefundType.Recovery); + setRefundJson(validateRecoveryFile(json)); + setRefundInvalid(undefined); + } else { + log.info("Found legacy refund file"); - setRefundJson(data); - setRefundInvalid(undefined); + const data = validateRefundFile(json); + + setRefundType(RefundType.Legacy); + setRefundJson(data); + setRefundInvalid(undefined); + } } catch (e) { log.warn("Refund json validation failed", e); + setRefundType(undefined); setRefundInvalid(RefundError.InvalidData); } }; @@ -68,7 +145,7 @@ export const RefundBtcLike = () => { const res = await QrScanner.scanImage(inputFile, { returnDetailedScanResult: true, }); - checkRefundJsonKeys(input, JSON.parse(res.data)); + checkRefundJsonKeys(JSON.parse(res.data)); } catch (e) { log.error("invalid QR code upload", e); setRefundInvalid(RefundError.InvalidData); @@ -76,7 +153,7 @@ export const RefundBtcLike = () => { } else { try { const data = await inputFile.text(); - checkRefundJsonKeys(input, JSON.parse(data)); + checkRefundJsonKeys(JSON.parse(data)); } catch (e) { log.error("invalid file upload", e); setRefundInvalid(RefundError.InvalidData); @@ -95,29 +172,30 @@ export const RefundBtcLike = () => { accept="application/json,image/png,imagine/jpg,image/jpeg" onChange={(e) => uploadChange(e)} /> - -
- + + + + + + + + + +

+ Error: {formatError(recoverySwaps.error)} +

+
+
- -
-

{t("refunded")}

-
- +
- ); }; diff --git a/src/pages/RefundStep.tsx b/src/pages/RefundStep.tsx deleted file mode 100644 index b3b88a0f..00000000 --- a/src/pages/RefundStep.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { useNavigate, useParams } from "@solidjs/router"; -import log from "loglevel"; -import { For, createResource } from "solid-js"; - -import DownloadRefund from "../components/DownloadRefund"; -import Warning from "../components/Warning"; -import { useGlobalContext } from "../context/Global"; -import { usePayContext } from "../context/Pay"; -import type { DictKey } from "../i18n/i18n"; - -const RefundStep = () => { - const params = useParams(); - const navigate = useNavigate(); - const { setSwap } = usePayContext(); - const { getSwap, t } = useGlobalContext(); - - createResource(async () => { - const currentSwap = await getSwap(params.id); - if (currentSwap) { - log.debug("selecting swap", currentSwap); - setSwap(currentSwap); - } - }); - - return ( -
-
-

{t("backup_refund")}

-

{t("backup_refund_subline")}

-
    - - {(path) =>
  • {t(path)}
  • } -
    -
-
- -

{t("backup_refund_skip")}

-
-
- -
navigate("/swap/" + params.id)}> - -
-
-
-
- ); -}; - -export default RefundStep; diff --git a/src/status/InvoiceFailedToPay.tsx b/src/status/InvoiceFailedToPay.tsx index 4a2e46b9..72face7c 100644 --- a/src/status/InvoiceFailedToPay.tsx +++ b/src/status/InvoiceFailedToPay.tsx @@ -1,9 +1,6 @@ -import { OutputType } from "boltz-core"; -import { Accessor, Show } from "solid-js"; +import { Accessor } from "solid-js"; -import DownloadRefund from "../components/DownloadRefund"; import RefundButton from "../components/RefundButton"; -import { RBTC } from "../consts/Assets"; import { useGlobalContext } from "../context/Global"; import { usePayContext } from "../context/Pay"; import { ChainSwap, SubmarineSwap } from "../utils/swapCreator"; @@ -11,7 +8,6 @@ import { ChainSwap, SubmarineSwap } from "../utils/swapCreator"; const InvoiceFailedToPay = () => { const { failureReason, swap } = usePayContext(); const { t } = useGlobalContext(); - const isTaproot = swap().version === OutputType.Taproot; return (
@@ -21,9 +17,6 @@ const InvoiceFailedToPay = () => {


} /> - - -
); diff --git a/src/utils/boltzClient.ts b/src/utils/boltzClient.ts index 91b51285..0076667b 100644 --- a/src/utils/boltzClient.ts +++ b/src/utils/boltzClient.ts @@ -166,6 +166,10 @@ type ChainSwapTransaction = { type TransactionInterface = Transaction | LiquidTransaction; +export type RecoverableSwap = Awaited< + ReturnType +>[number]; + export const getPairs = async (): Promise => { const [submarine, reverse, chain] = await Promise.all([ fetcher("/v2/swap/submarine"), @@ -473,6 +477,11 @@ export const acceptChainSwapNewQuote = (id: string, amount: number) => export const getSubmarinePreimage = (id: string) => fetcher<{ preimage: string }>(`/v2/swap/submarine/${id}/preimage`); +export const getRecoverableSwaps = (xpub: string) => + fetcher< + { id: string; type: SwapType; symbol: string; createdAt: number }[] + >(`/v2/swap/recovery`, { xpub }); + export { Pairs, Contracts, diff --git a/src/utils/claim.ts b/src/utils/claim.ts index dea1656b..7b6306d9 100644 --- a/src/utils/claim.ts +++ b/src/utils/claim.ts @@ -11,6 +11,7 @@ import log from "loglevel"; import { LBTC, RBTC } from "../consts/Assets"; import { SwapType } from "../consts/Enums"; +import { deriveKeyFn } from "../context/Global"; import { TransactionInterface, broadcastTransaction, @@ -72,6 +73,7 @@ const createAdjustedClaim = async < }; const claimReverseSwap = async ( + deriveKey: deriveKeyFn, swap: ReverseSwap, lockupTx: TransactionInterface, cooperative: boolean = true, @@ -79,11 +81,12 @@ const claimReverseSwap = async ( log.info(`Claiming Taproot swap cooperatively: ${cooperative}`); const asset = getRelevantAssetForSwap(swap); - const privateKey = parsePrivateKey(swap.claimPrivateKey); - log.debug("privateKey: ", swap.claimPrivateKey); - + const privateKey = parsePrivateKey( + deriveKey, + swap.claimPrivateKeyIndex, + swap.claimPrivateKey, + ); const preimage = Buffer.from(swap.preimage, "hex"); - log.debug("preimage: ", swap.preimage); const decodedAddress = decodeAddress(asset, swap.claimAddress); const boltzPublicKey = Buffer.from(swap.refundPublicKey, "hex"); @@ -137,11 +140,12 @@ const claimReverseSwap = async ( return claimTx; } catch (e) { log.warn("Uncooperative Taproot claim because", e); - return claimReverseSwap(swap, lockupTx, false); + return claimReverseSwap(deriveKey, swap, lockupTx, false); } }; export const createTheirPartialChainSwapSignature = async ( + deriveKey: deriveKeyFn, swap: ChainSwap, ): Promise> | undefined> => { // RSK claim transactions can't be signed cooperatively @@ -158,7 +162,11 @@ export const createTheirPartialChainSwapSignature = async ( "hex", ); const theirClaimMusig = await createMusig( - parsePrivateKey(swap.refundPrivateKey), + parsePrivateKey( + deriveKey, + swap.refundPrivateKeyIndex, + swap.refundPrivateKey, + ), boltzClaimPublicKey, ); tweakMusig( @@ -195,11 +203,10 @@ export const createTheirPartialChainSwapSignature = async ( throw err; } - - return undefined; }; const claimChainSwap = async ( + deriveKey: deriveKeyFn, swap: ChainSwap, lockupTx: TransactionInterface, cooperative = true, @@ -209,7 +216,11 @@ const claimChainSwap = async ( swap.claimDetails.serverPublicKey, "hex", ); - const claimPrivateKey = parsePrivateKey(swap.claimPrivateKey); + const claimPrivateKey = parsePrivateKey( + deriveKey, + swap.claimPrivateKeyIndex, + swap.claimPrivateKey, + ); const ourClaimMusig = await createMusig( claimPrivateKey, boltzRefundPublicKey, @@ -258,7 +269,7 @@ const claimChainSwap = async ( const theirPartial = await postChainSwapDetails( swap.id, swap.preimage, - await createTheirPartialChainSwapSignature(swap), + await createTheirPartialChainSwapSignature(deriveKey, swap), { index: 0, transaction: claimTx.toHex(), @@ -291,11 +302,12 @@ const claimChainSwap = async ( return claimTx; } catch (e) { log.warn("Uncooperative Taproot claim because", e); - return claimChainSwap(swap, lockupTx, false); + return claimChainSwap(deriveKey, swap, lockupTx, false); } }; export const claim = async ( + deriveKey: deriveKeyFn, swap: T, swapStatusTransaction: { hex: string }, cooperative: boolean, @@ -313,12 +325,14 @@ export const claim = async ( let claimTransaction: TransactionInterface; if (swap.type === SwapType.Reverse) { claimTransaction = await claimReverseSwap( + deriveKey, swap as ReverseSwap, lockupTx, cooperative, ); } else { claimTransaction = await claimChainSwap( + deriveKey, swap as ChainSwap, lockupTx, cooperative, @@ -340,7 +354,10 @@ export const claim = async ( return swap; }; -export const createSubmarineSignature = async (swap: SubmarineSwap) => { +export const createSubmarineSignature = async ( + deriveKey: deriveKeyFn, + swap: SubmarineSwap, +) => { const swapAsset = getRelevantAssetForSwap(swap); if (swapAsset === RBTC) { return; @@ -358,7 +375,11 @@ export const createSubmarineSignature = async (swap: SubmarineSwap) => { const boltzPublicKey = Buffer.from(swap.claimPublicKey, "hex"); const musig = await createMusig( - parsePrivateKey(swap.refundPrivateKey), + parsePrivateKey( + deriveKey, + swap.refundPrivateKeyIndex, + swap.refundPrivateKey, + ), boltzPublicKey, ); const tree = SwapTreeSerializer.deserializeSwapTree(swap.swapTree); diff --git a/src/utils/helper.ts b/src/utils/helper.ts index b5cdb9bb..b0a1bd34 100644 --- a/src/utils/helper.ts +++ b/src/utils/helper.ts @@ -5,7 +5,7 @@ import { chooseUrl, config } from "../config"; import { BTC, LN } from "../consts/Assets"; import { SwapType } from "../consts/Enums"; import { referralIdKey } from "../consts/LocalStorage"; -import { defaultReferral } from "../context/Global"; +import { defaultReferral, deriveKeyFn } from "../context/Global"; import { ChainPairTypeTaproot, Pairs, @@ -121,14 +121,22 @@ export const fetcher = async ( return (await response.json()) as T; }; -export const parsePrivateKey = (privateKey: string): ECPairInterface => { +export const parsePrivateKey = ( + deriveKey: deriveKeyFn, + keyIndex?: number, + privateKeyHex?: string, +): ECPairInterface => { + if (keyIndex !== undefined) { + return deriveKey(keyIndex); + } + try { - return ECPair.fromPrivateKey(Buffer.from(privateKey, "hex")); + return ECPair.fromPrivateKey(Buffer.from(privateKeyHex, "hex")); // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { // When the private key is not HEX, we try to decode it as WIF - return ECPair.fromWIF(privateKey); + return ECPair.fromWIF(privateKeyHex); } }; diff --git a/src/utils/recoveryFile.ts b/src/utils/recoveryFile.ts new file mode 100644 index 00000000..037b0c8a --- /dev/null +++ b/src/utils/recoveryFile.ts @@ -0,0 +1,29 @@ +import { HDKey } from "@scure/bip32"; + +export type RecoveryFile = { + xpriv: string; + birthday: number; +}; + +const getPath = (index: number) => `m/44/0/0/0/${index}`; + +export const getXpub = (recoveryFile: RecoveryFile) => { + const key = HDKey.fromExtendedKey(recoveryFile.xpriv); + return key.publicExtendedKey; +}; + +export const generateRecoveryFile = (): RecoveryFile => { + const entropy = new Uint8Array(32); + crypto.getRandomValues(entropy); + const key = HDKey.fromMasterSeed(entropy); + + return { + xpriv: key.privateExtendedKey, + birthday: Math.floor(Date.now() / 1000), + }; +}; + +export const deriveKey = (recoveryFile: RecoveryFile, index: number) => { + const key = HDKey.fromExtendedKey(recoveryFile.xpriv); + return key.derive(getPath(index)); +}; diff --git a/src/utils/refund.ts b/src/utils/refund.ts index 6d35bd70..fd5c3358 100644 --- a/src/utils/refund.ts +++ b/src/utils/refund.ts @@ -12,6 +12,7 @@ import log from "loglevel"; import { LBTC } from "../consts/Assets"; import { SwapType } from "../consts/Enums"; +import { deriveKeyFn } from "../context/Global"; import { TransactionInterface, broadcastTransaction, @@ -81,7 +82,7 @@ const refundTaproot = async ( swap.assetSend, swap.assetSend === LBTC && decodedAddress.blindingKey === undefined, ); - const claimTx = constructRefundTransaction( + const refundTx = constructRefundTransaction( details, decodedAddress.script, cooperative ? 0 : timeoutBlockHeight, @@ -94,7 +95,7 @@ const refundTaproot = async ( if (!cooperative) { return { cooperativeError, - transaction: claimTx as T, + transaction: refundTx as T, }; } @@ -103,7 +104,7 @@ const refundTaproot = async ( swap.id, swap.type, Buffer.from(musig.getPublicNonce()), - claimTx, + refundTx, 0, ); musig.aggregateNonces([[boltzPublicKey, boltzSig.pubNonce]]); @@ -112,17 +113,17 @@ const refundTaproot = async ( swap.assetSend, getNetwork(swap.assetSend), details, - claimTx, + refundTx, 0, ), ); musig.signPartial(); musig.addPartial(boltzPublicKey, boltzSig.signature); - claimTx.ins[0].witness = [musig.aggregatePartials()]; + refundTx.ins[0].witness = [musig.aggregatePartials()]; return { - transaction: claimTx as T, + transaction: refundTx as T, }; } catch (e) { if (!cooperative) { @@ -185,6 +186,7 @@ const broadcastRefund = async ( }; export const refund = async ( + deriveKey: deriveKeyFn, swap: T, refundAddress: string, transactionToRefund: { hex: string; timeoutBlockHeight: number }, @@ -200,7 +202,11 @@ export const refund = async ( const lockupTransaction = getTransaction(swap.assetSend).fromHex( transactionToRefund.hex, ); - const privateKey = parsePrivateKey(swap.refundPrivateKey); + const privateKey = parsePrivateKey( + deriveKey, + swap.refundPrivateKeyIndex, + swap.refundPrivateKey, + ); let refundTransaction: Awaited>; diff --git a/src/utils/refundFile.ts b/src/utils/refundFile.ts index d783e31e..98e075a8 100644 --- a/src/utils/refundFile.ts +++ b/src/utils/refundFile.ts @@ -1,6 +1,7 @@ import { LBTC } from "../consts/Assets"; import { SwapType } from "../consts/Enums"; import { migrateSwapToChainSwapFormat } from "./migration"; +import { RecoveryFile } from "./recoveryFile"; const getRequiredKeys = ( isLegacy: boolean, @@ -64,3 +65,13 @@ export const validateRefundFile = ( return data as { id: string } & Record; }; + +export const validateRecoveryFile = ( + data: Record, +): RecoveryFile => { + if (!("xpriv" in data)) { + throw "invalid recovery file"; + } + + return data as RecoveryFile; +}; diff --git a/src/utils/swapCreator.ts b/src/utils/swapCreator.ts index 6a097a46..aaef4268 100644 --- a/src/utils/swapCreator.ts +++ b/src/utils/swapCreator.ts @@ -5,6 +5,7 @@ import { randomBytes } from "crypto"; import { RBTC } from "../consts/Assets"; import { SwapType } from "../consts/Enums"; +import { newKeyFn } from "../context/Global"; import { ChainSwapCreatedResponse, Pairs, @@ -14,7 +15,6 @@ import { createReverseSwap, createSubmarineSwap, } from "./boltzClient"; -import { ECPair } from "./ecpair"; import { getPair } from "./helper"; export type SwapBase = { @@ -43,6 +43,9 @@ export type SubmarineSwap = SwapBase & SubmarineCreatedResponse & { invoice: string; preimage?: string; + refundPrivateKeyIndex?: number; + + // Deprecated; used for backwards compatibility refundPrivateKey?: string; }; @@ -50,6 +53,9 @@ export type ReverseSwap = SwapBase & ReverseCreatedResponse & { preimage: string; claimAddress: string; + claimPrivateKeyIndex?: number; + + // Deprecated; used for backwards compatibility claimPrivateKey?: string; }; @@ -57,8 +63,12 @@ export type ChainSwap = SwapBase & ChainSwapCreatedResponse & { preimage: string; claimAddress: string; - claimPrivateKey: string; - refundPrivateKey: string; + claimPrivateKeyIndex?: number; + refundPrivateKeyIndex?: number; + + // Deprecated; used for backwards compatibility + claimPrivateKey?: string; + refundPrivateKey?: string; }; export type SomeSwap = SubmarineSwap | ReverseSwap | ChainSwap; @@ -82,16 +92,17 @@ export const createSubmarine = async ( invoice: string, referralId: string, useRif: boolean, + newKey: newKeyFn, ): Promise => { const isRsk = assetReceive === RBTC; - const refundKeys = !isRsk ? ECPair.makeRandom() : undefined; + const key = !isRsk ? newKey() : undefined; const res = await createSubmarineSwap( assetSend, assetReceive, invoice, getPair(pairs, SwapType.Submarine, assetSend, assetReceive).hash, referralId, - refundKeys?.publicKey.toString("hex"), + key?.key.publicKey.toString("hex"), ); return { @@ -105,7 +116,7 @@ export const createSubmarine = async ( useRif, ), invoice, - refundPrivateKey: refundKeys?.privateKey.toString("hex"), + refundPrivateKeyIndex: key?.index, }; }; @@ -118,11 +129,12 @@ export const createReverse = async ( claimAddress: string, referralId: string, useRif: boolean, + newKey: newKeyFn, ): Promise => { const isRsk = assetReceive === RBTC; const preimage = randomBytes(32); - const claimKeys = !isRsk ? ECPair.makeRandom() : undefined; + const key = !isRsk ? newKey() : undefined; const res = await createReverseSwap( assetSend, @@ -131,7 +143,7 @@ export const createReverse = async ( crypto.sha256(preimage).toString("hex"), getPair(pairs, SwapType.Reverse, assetSend, assetReceive).hash, referralId, - claimKeys?.publicKey.toString("hex"), + key?.key.publicKey.toString("hex"), claimAddress, ); @@ -147,7 +159,7 @@ export const createReverse = async ( ), claimAddress, preimage: preimage.toString("hex"), - claimPrivateKey: claimKeys?.privateKey.toString("hex"), + claimPrivateKeyIndex: key?.index, }; }; @@ -160,10 +172,11 @@ export const createChain = async ( claimAddress: string, referralId: string, useRif: boolean, + newKey: newKeyFn, ): Promise => { const preimage = randomBytes(32); - const claimKeys = assetReceive !== RBTC ? ECPair.makeRandom() : undefined; - const refundKeys = assetSend !== RBTC ? ECPair.makeRandom() : undefined; + const claimKey = assetReceive !== RBTC ? newKey() : undefined; + const refundKey = assetSend !== RBTC ? newKey() : undefined; const res = await createChainSwap( assetSend, @@ -172,8 +185,8 @@ export const createChain = async ( ? undefined : Number(sendAmount), crypto.sha256(preimage).toString("hex"), - claimKeys?.publicKey.toString("hex"), - refundKeys?.publicKey.toString("hex"), + claimKey?.key.publicKey.toString("hex"), + refundKey?.key.publicKey.toString("hex"), claimAddress, getPair(pairs, SwapType.Chain, assetSend, assetReceive).hash, referralId, @@ -191,8 +204,8 @@ export const createChain = async ( ), claimAddress, preimage: preimage.toString("hex"), - claimPrivateKey: claimKeys?.privateKey.toString("hex"), - refundPrivateKey: refundKeys?.privateKey.toString("hex"), + claimPrivateKeyIndex: claimKey?.index, + refundPrivateKeyIndex: refundKey?.index, }; }; diff --git a/src/utils/validation.ts b/src/utils/validation.ts index 51d744df..87ae0142 100644 --- a/src/utils/validation.ts +++ b/src/utils/validation.ts @@ -15,11 +15,12 @@ import log from "loglevel"; import { LBTC, RBTC } from "../consts/Assets"; import { Denomination, Side, SwapType } from "../consts/Enums"; +import { deriveKeyFn } from "../context/Global"; import { etherSwapCodeHashes } from "../context/Web3"; import { ChainSwapDetails } from "./boltzClient"; import { decodeAddress } from "./compat"; import { formatAmountDenomination } from "./denomination"; -import { ECPair, ecc } from "./ecpair"; +import { ecc } from "./ecpair"; import { decodeInvoice, isInvoice, isLnurl } from "./invoice"; import { ChainSwap, ReverseSwap, SomeSwap, SubmarineSwap } from "./swapCreator"; import { createMusig, tweakMusig } from "./taproot/musig"; @@ -103,6 +104,7 @@ const validateBip21 = ( const validateReverse = async ( swap: ReverseSwap, + deriveKey: deriveKeyFn, getEtherSwap: ContractGetter, buffer: BufferConstructor, ) => { @@ -129,9 +131,7 @@ const validateReverse = async ( // SwapTree const tree = SwapTreeSerializer.deserializeSwapTree(swap.swapTree); - const ourKeys = ECPair.fromPrivateKey( - buffer.from(swap.claimPrivateKey, "hex"), - ); + const ourKeys = deriveKey(swap.claimPrivateKeyIndex); const theirPublicKey = buffer.from(swap.refundPublicKey, "hex"); const compareTree = reverseSwapTree( @@ -159,6 +159,7 @@ const validateReverse = async ( const validateSubmarine = async ( swap: SubmarineSwap, + deriveKey: deriveKeyFn, getEtherSwap: ContractGetter, buffer: typeof BufferBrowser.Buffer, ) => { @@ -176,9 +177,7 @@ const validateSubmarine = async ( const tree = SwapTreeSerializer.deserializeSwapTree(swap.swapTree); - const ourKeys = ECPair.fromPrivateKey( - buffer.from(swap.refundPrivateKey, "hex"), - ); + const ourKeys = deriveKey(swap.refundPrivateKeyIndex); const theirPublicKey = buffer.from(swap.claimPublicKey, "hex"); const compareTree = swapTree( @@ -213,6 +212,7 @@ const validateSubmarine = async ( const validateChainSwap = async ( swap: ChainSwap, + deriveKey: deriveKeyFn, getEtherSwap: ContractGetter, buffer: BufferConstructor, ) => { @@ -240,13 +240,10 @@ const validateChainSwap = async ( return validateContract(getEtherSwap); } - const ourKeys = ECPair.fromPrivateKey( - buffer.from( - side === Side.Send - ? swap.refundPrivateKey - : swap.claimPrivateKey, - "hex", - ), + const ourKeys = deriveKey( + side === Side.Send + ? swap.refundPrivateKeyIndex + : swap.claimPrivateKeyIndex, ); const theirPublicKey = buffer.from(details.serverPublicKey, "hex"); const tree = SwapTreeSerializer.deserializeSwapTree(details.swapTree); @@ -293,6 +290,7 @@ const validateChainSwap = async ( // To be able to use the Buffer from Node.js export const validateResponse = async ( swap: SomeSwap, + deriveKey: deriveKeyFn, getEtherSwap: ContractGetter, buffer: typeof BufferBrowser.Buffer = BufferBrowser as never, ): Promise => { @@ -301,6 +299,7 @@ export const validateResponse = async ( case SwapType.Submarine: return await validateSubmarine( swap as SubmarineSwap, + deriveKey, getEtherSwap, buffer, ); @@ -308,6 +307,7 @@ export const validateResponse = async ( case SwapType.Reverse: return await validateReverse( swap as ReverseSwap, + deriveKey, getEtherSwap, buffer, ); @@ -315,6 +315,7 @@ export const validateResponse = async ( case SwapType.Chain: return await validateChainSwap( swap as ChainSwap, + deriveKey, getEtherSwap, buffer, ); diff --git a/tests/utils/refundFile.spec.ts b/tests/utils/refundFile.spec.ts deleted file mode 100644 index 91842310..00000000 --- a/tests/utils/refundFile.spec.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { migrateSwapToChainSwapFormat } from "../../src/utils/migration"; -import { validateRefundFile } from "../../src/utils/refundFile"; - -describe("refundFile", () => { - test.each` - data - ${{}} - ${{ not: "valid" }} - `("should throw for invalid refund files", ({ data }) => { - expect(() => validateRefundFile(data)).toThrow("invalid refund file"); - }); - - test("should accept chain swap format of refund files", () => { - const data = { - id: "vEtptZZKPHLS", - bip21: "bitcoin:bcrt1pf2vm5s99zyvljzj94u8vfkezt5dhl5upmu8gktyaw4hmqesg5a6qukqnx5?amount=0.00054047&label=Send%20to%20BTC%20lightning", - address: - "bcrt1pf2vm5s99zyvljzj94u8vfkezt5dhl5upmu8gktyaw4hmqesg5a6qukqnx5", - swapTree: { - claimLeaf: { - version: 192, - output: "a9145b7b19647829c7f35a3dae9341b0748df9deb5518820482a2db89ce575fb8e6cae372abdbba22e3a4d84c4dea7f923486dcb085318eeac", - }, - refundLeaf: { - version: 192, - output: "20ea45594a484aa75fad517b89cbdfa80e805c3e703e01a3b67dbedc96b896faf9ad028104b1", - }, - }, - acceptZeroConf: false, - expectedAmount: 54047, - claimPublicKey: - "02482a2db89ce575fb8e6cae372abdbba22e3a4d84c4dea7f923486dcb085318ee", - timeoutBlockHeight: 1153, - type: "submarine", - assetSend: "BTC", - assetReceive: "BTC", - date: 1716899886587, - version: 3, - sendAmount: 54047, - receiveAmount: 52432, - invoice: - "lnbcrt524320n1pn9t5pdpp5g0ncs2naeuwlcv67lpjh47apapx04rw5cq2grasz00wn90pdgasqdq5g9kxy7fqd9h8vmmfvdjscqzzsxqyz5vqsp5jgv5gtcscec294dthygen846neew82rvgalzz4xhzyq3qh3nkg6s9qyyssqtsvzq6cnvyfzlfcunc7k3ks09jmz7l8ehel2v9hryng3al8dpkljs7zh8yc88n6xq0t9u477l563j35fyznwxrk2vmhj7lffecu3ungpn4yhy4", - refundPrivateKey: - "096a66d99d5f12776040ccd3ac6150069275677a0af4fc358054b9084bc162f2", - }; - - expect(validateRefundFile(data)).toEqual(data); - }); - - test("should accept and migrate legacy refund files", () => { - const data = { - id: "uYZcNe", - asset: "BTC", - privateKey: - "def0a13214538650fb84a7545c9b81128a639f55147cdd61c46d5ea0f70045a3", - redeemScript: - "a914dd28cc8e29bc4bcad9a23dec8002b11b7e5a99e687632103884ff511cc5061a90f07e553de127095df5d438b2bda23db4159c5f32df5e1f967022101b175210275c22382a4fb52536034e6630434b01fd94eee6b462df5cdc89901b5ef45ac2768ac", - timeoutBlockHeight: 289, - }; - - expect(validateRefundFile(data)).toEqual({ - ...migrateSwapToChainSwapFormat(data), - reverse: false, - }); - }); -});