diff --git a/e2e/refund/refundFile.spec.ts b/e2e/refund/refundFile.spec.ts index a16006de..61e2a4ef 100644 --- a/e2e/refund/refundFile.spec.ts +++ b/e2e/refund/refundFile.spec.ts @@ -10,6 +10,9 @@ test.describe("Refund files", () => { await page.goto("/"); await page.getByRole("link", { name: "Refund" }).click(); + await page + .getByRole("button", { name: "Refund external swap" }) + .click(); await page.getByTestId("refundUpload").click(); await page diff --git a/src/components/RefundButton.tsx b/src/components/RefundButton.tsx index d6499802..90d2bbf4 100644 --- a/src/components/RefundButton.tsx +++ b/src/components/RefundButton.tsx @@ -178,10 +178,9 @@ const RefundButton = (props: { currentSwap.refundTx = res.refundTx; await setSwapStorage(currentSwap); setSwap(currentSwap); - } else { - if (props.setRefundTxId) { - props.setRefundTxId(res.refundTx); - } + } + if (props.setRefundTxId) { + props.setRefundTxId(res.refundTx); } } catch (error) { log.warn("refund failed", error); diff --git a/src/components/SwapList.tsx b/src/components/SwapList.tsx index 66999e19..7534f6d1 100644 --- a/src/components/SwapList.tsx +++ b/src/components/SwapList.tsx @@ -9,6 +9,7 @@ import { SwapIcons } from "./SwapIcons"; const SwapList = (props: { swapsSignal: Accessor; + action: string; onDelete?: () => Promise; }) => { const navigate = useNavigate(); @@ -51,7 +52,7 @@ const SwapList = (props: { class="swaplist-item" onClick={() => navigate(`/swap/${swap.id}`)}> - {t("view")} + {props.action} diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts index b9bf78f9..4dec34c1 100644 --- a/src/i18n/i18n.ts +++ b/src/i18n/i18n.ts @@ -103,6 +103,17 @@ const dict = { refund_past_swaps: "Past swaps", refund_past_swaps_subline: "Swaps that got saved into your browsers storage", + no_refundable_swaps: + "No refundable swaps found in your browser history", + cant_find_swap: "Can't find your swap?", + refund_external_explainer: + "Try refunding an external swap via refund file and other emergency methods", + refund_external_explainer_rsk: + "Connect your Rootstock Wallet to scan for refundable swaps that are not saved in this browser’s swap history.", + connected_wallet_no_swaps: + "The connected Rootstock Wallet does NOT contain any refundable swaps.", + rsk_log_endpoint_not_available: "Log endpoint not available", + refund_external_swap: "Refund External Swap", history_no_swaps: "Looks like you didn't do any swaps yet.", refund_address_header: "Enter address of your {{ asset }} wallet to refund", diff --git a/src/index.tsx b/src/index.tsx index 2adb8d15..688fc0d0 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -25,6 +25,7 @@ import NotFound from "./pages/NotFound"; 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"; @@ -105,6 +106,8 @@ const cleanup = render( } /> + + diff --git a/src/pages/History.tsx b/src/pages/History.tsx index 1535f442..cd7d431c 100644 --- a/src/pages/History.tsx +++ b/src/pages/History.tsx @@ -153,6 +153,7 @@ const History = () => { onDelete={async () => { setSwaps(await getSwaps()); }} + action={t("view")} /> 0}> diff --git a/src/pages/Refund.tsx b/src/pages/Refund.tsx index c7c3d5bb..c8c2cdcc 100644 --- a/src/pages/Refund.tsx +++ b/src/pages/Refund.tsx @@ -1,158 +1,26 @@ import { useNavigate } from "@solidjs/router"; import log from "loglevel"; -import QrScanner from "qr-scanner"; -import { Show, createEffect, createSignal, onCleanup, onMount } from "solid-js"; +import { Show, createSignal, onMount } from "solid-js"; -import BlockExplorer from "../components/BlockExplorer"; -import ConnectWallet from "../components/ConnectWallet"; -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 { RBTC } from "../consts/Assets"; import { SwapType } from "../consts/Enums"; import { swapStatusFailed, swapStatusSuccess } from "../consts/SwapStatus"; import { useGlobalContext } from "../context/Global"; -import { useWeb3Signer } from "../context/Web3"; +import "../style/tabs.scss"; import { getLockupTransaction, getSwapStatus } from "../utils/boltzClient"; -import { - LogRefundData, - scanLogsForPossibleRefunds, -} from "../utils/contractLogs"; -import { validateRefundFile } from "../utils/refundFile"; import { SomeSwap } from "../utils/swapCreator"; import ErrorWasm from "./ErrorWasm"; -enum RefundError { - InvalidData, -} - const Refund = () => { const navigate = useNavigate(); - const { getSwap, getSwaps, updateSwapStatus, wasmSupported, t } = - useGlobalContext(); - const { signer, providers, getEtherSwap } = useWeb3Signer(); - - const [swapFound, setSwapFound] = createSignal(null); - const [refundInvalid, setRefundInvalid] = createSignal< - RefundError | undefined - >(undefined); - const [refundJson, setRefundJson] = createSignal(null); - const [refundTxId, setRefundTxId] = createSignal(""); - - const checkRefundJsonKeys = async ( - input: HTMLInputElement, - json: Record, - ) => { - log.debug("checking refund json", json); - - try { - const data = validateRefundFile(json); - - // When the swap id is found in the local storage, show a redirect to it - const localStorageSwap = await getSwap(data.id); - if (localStorageSwap !== null) { - setSwapFound(data.id); - return; - } - - setRefundJson(data); - setRefundInvalid(undefined); - } catch (e) { - log.warn("Refund json validation failed", e); - setRefundInvalid(RefundError.InvalidData); - input.setCustomValidity(t("invalid_refund_file")); - } - }; - - const uploadChange = async (e: Event) => { - const input = e.currentTarget as HTMLInputElement; - const inputFile = input.files[0]; - input.setCustomValidity(""); - setRefundJson(null); - setSwapFound(null); - setRefundInvalid(undefined); - - if (["image/png", "image/jpg", "image/jpeg"].includes(inputFile.type)) { - try { - const res = await QrScanner.scanImage(inputFile, { - returnDetailedScanResult: true, - }); - await checkRefundJsonKeys(input, JSON.parse(res.data)); - } catch (e) { - log.error("invalid QR code upload", e); - setRefundInvalid(RefundError.InvalidData); - input.setCustomValidity(t("invalid_refund_file")); - } - } else { - try { - const data = await inputFile.text(); - await checkRefundJsonKeys(input, JSON.parse(data)); - } catch (e) { - log.error("invalid file upload", e); - setRefundInvalid(RefundError.InvalidData); - input.setCustomValidity(t("invalid_refund_file")); - } - } - }; + const { getSwaps, updateSwapStatus, wasmSupported, t } = useGlobalContext(); const refundSwapsSanityFilter = (swap: SomeSwap) => swap.type !== SwapType.Reverse && swap.refundTx === undefined; const [refundableSwaps, setRefundableSwaps] = createSignal([]); - const [logRefundableSwaps, setLogRefundableSwaps] = createSignal< - LogRefundData[] - >([]); - const [refundScanProgress, setRefundScanProgress] = createSignal< - string | undefined - >(undefined); - - let refundScanAbort: AbortController | undefined = undefined; - - onCleanup(() => { - if (refundScanAbort) { - refundScanAbort.abort(); - } - }); - - // eslint-disable-next-line solid/reactivity - createEffect(async () => { - setLogRefundableSwaps([]); - - if (refundScanAbort !== undefined) { - refundScanAbort.abort("signer changed"); - } - - if (signer() === undefined) { - return; - } - - setRefundScanProgress( - t("logs_scan_progress", { - value: Number(0).toFixed(2), - }), - ); - - refundScanAbort = new AbortController(); - - const generator = scanLogsForPossibleRefunds( - refundScanAbort.signal, - signer(), - getEtherSwap(), - ); - - for await (const value of generator) { - setRefundScanProgress( - t("logs_scan_progress", { - value: (value.progress * 100).toFixed(2), - }), - ); - setLogRefundableSwaps(logRefundableSwaps().concat(value.events)); - } - - setRefundScanProgress(undefined); - }); onMount(async () => { const addToRefundableSwaps = (swap: SomeSwap) => { @@ -204,80 +72,30 @@ const Refund = () => { }>
- +
+ +

{t("refund_a_swap")}

+
0} fallback={ <> -

{t("refund_a_swap")}

-

{t("refund_a_swap_subline")}

+

{t("no_refundable_swaps")}

+
}> -

{t("refund_swap", { id: refundJson().id })}

-
- - 0}> - - - 0}> - - - uploadChange(e)} - /> - 0 && - (refundJson() === null || - refundJson().assetSend === RBTC) - }> -
- -
- -
-

{t("swap_in_history")}

- -
- -
- -
- -
- -
- -
-

{t("refunded")}

-
-
+

{t("cant_find_swap")}

+

{t("refund_external_explainer")}

+
diff --git a/src/pages/RefundExternal.tsx b/src/pages/RefundExternal.tsx new file mode 100644 index 00000000..390c1198 --- /dev/null +++ b/src/pages/RefundExternal.tsx @@ -0,0 +1,257 @@ +import { useNavigate, useParams } from "@solidjs/router"; +import log from "loglevel"; +import QrScanner from "qr-scanner"; +import { + For, + Match, + Show, + Switch, + createEffect, + createSignal, + onCleanup, +} from "solid-js"; + +import BlockExplorer from "../components/BlockExplorer"; +import ConnectWallet from "../components/ConnectWallet"; +import RefundButton from "../components/RefundButton"; +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 { + LogRefundData, + scanLogsForPossibleRefunds, +} from "../utils/contractLogs"; +import { validateRefundFile } from "../utils/refundFile"; +import ErrorWasm from "./ErrorWasm"; + +enum RefundError { + InvalidData, +} + +const RefundBtcLike = () => { + const { t } = useGlobalContext(); + + const [refundInvalid, setRefundInvalid] = createSignal< + RefundError | undefined + >(undefined); + const [refundJson, setRefundJson] = createSignal(null); + const [refundTxId, setRefundTxId] = createSignal(""); + + const checkRefundJsonKeys = ( + input: HTMLInputElement, + json: Record, + ) => { + log.debug("checking refund json", json); + + try { + const data = validateRefundFile(json); + + setRefundJson(data); + setRefundInvalid(undefined); + } catch (e) { + log.warn("Refund json validation failed", e); + setRefundInvalid(RefundError.InvalidData); + input.setCustomValidity(t("invalid_refund_file")); + } + }; + + const uploadChange = async (e: Event) => { + const input = e.currentTarget as HTMLInputElement; + const inputFile = input.files[0]; + input.setCustomValidity(""); + setRefundJson(null); + setRefundInvalid(undefined); + + if (["image/png", "image/jpg", "image/jpeg"].includes(inputFile.type)) { + try { + const res = await QrScanner.scanImage(inputFile, { + returnDetailedScanResult: true, + }); + checkRefundJsonKeys(input, JSON.parse(res.data)); + } catch (e) { + log.error("invalid QR code upload", e); + setRefundInvalid(RefundError.InvalidData); + input.setCustomValidity(t("invalid_refund_file")); + } + } else { + try { + const data = await inputFile.text(); + checkRefundJsonKeys(input, JSON.parse(data)); + } catch (e) { + log.error("invalid file upload", e); + setRefundInvalid(RefundError.InvalidData); + input.setCustomValidity(t("invalid_refund_file")); + } + } + }; + + return ( + <> +

{t("refund_a_swap_subline")}

+ uploadChange(e)} + /> + +
+ +
+ +
+ +
+ +
+

{t("refunded")}

+
+ +
+ + + ); +}; + +const RefundRsk = () => { + const { t } = useGlobalContext(); + const { signer, getEtherSwap } = useWeb3Signer(); + + const [logRefundableSwaps, setLogRefundableSwaps] = createSignal< + LogRefundData[] + >([]); + const [refundScanProgress, setRefundScanProgress] = createSignal< + string | undefined + >(undefined); + + let refundScanAbort: AbortController | undefined = undefined; + + onCleanup(() => { + if (refundScanAbort) { + refundScanAbort.abort(); + } + }); + + // eslint-disable-next-line solid/reactivity + createEffect(async () => { + setLogRefundableSwaps([]); + + if (refundScanAbort !== undefined) { + refundScanAbort.abort("signer changed"); + } + + if (signer() === undefined) { + return; + } + + setRefundScanProgress( + t("logs_scan_progress", { + value: Number(0).toFixed(2), + }), + ); + + refundScanAbort = new AbortController(); + + const generator = scanLogsForPossibleRefunds( + refundScanAbort.signal, + signer(), + getEtherSwap(), + ); + + for await (const value of generator) { + setRefundScanProgress( + t("logs_scan_progress", { + value: (value.progress * 100).toFixed(2), + }), + ); + setLogRefundableSwaps(logRefundableSwaps().concat(value.events)); + } + + setRefundScanProgress(undefined); + }); + + return ( + {t("rsk_log_endpoint_not_available")}

}> + {t("refund_external_explainer_rsk")}

}> + 0}> + + + +

{refundScanProgress()}

+
+ +

{t("connected_wallet_no_swaps")}

+
+
+
+ +
+ ); +}; + +const RefundExternal = () => { + const { wasmSupported, t } = useGlobalContext(); + + const params = useParams(); + const navigate = useNavigate(); + + const tabBtc = { name: "Bitcoin / Liquid", value: "btc" }; + const tabRsk = { name: "Rootstock", value: "rsk" }; + + const selected = () => params.type ?? tabBtc.value; + + return ( + }> +
+
+
+ +

{t("refund_external_swap")}

+
+
+ + {(tab) => ( +
+ navigate( + `/refund/external/${tab.value}`, + ) + }> + {tab.name} +
+ )} +
+
+ + + + + + + +
+
+
+ ); +}; + +export default RefundExternal; diff --git a/src/style/index.scss b/src/style/index.scss index 23360141..27f68940 100644 --- a/src/style/index.scss +++ b/src/style/index.scss @@ -651,3 +651,7 @@ textarea { font-weight: 600; text-align: center; } + +header { + margin-bottom: 1rem; +} diff --git a/src/style/tabs.scss b/src/style/tabs.scss new file mode 100644 index 00000000..4834b094 --- /dev/null +++ b/src/style/tabs.scss @@ -0,0 +1,28 @@ +@use "sass:color"; +@use "vars"; + +.tabs { + display: flex; + gap: 0.5rem; + align-items: center; + justify-content: center; + + .tab { + padding: 0.5rem 1rem; + background-color: vars.$background; + color: vars.$color; + border-radius: 20px; + text-align: center; + cursor: pointer; + transition: all 0.3s ease; + + &:hover { + background-color: color.adjust(vars.$background, $lightness: -10%); + } + + &.active { + background-color: vars.$primary; + color: vars.$background; + } + } +} diff --git a/tests/components/SwapList.spec.tsx b/tests/components/SwapList.spec.tsx index 819afd0d..0cd65858 100644 --- a/tests/components/SwapList.spec.tsx +++ b/tests/components/SwapList.spec.tsx @@ -19,7 +19,7 @@ describe("SwapList", () => { const { container: { firstChild: firstChild }, - } = render(() => , { + } = render(() => , { wrapper: contextWrapper, }); diff --git a/tests/pages/Refund.spec.tsx b/tests/pages/Refund.spec.tsx index 2ba5c23e..0f3ed6ed 100644 --- a/tests/pages/Refund.spec.tsx +++ b/tests/pages/Refund.spec.tsx @@ -54,7 +54,7 @@ describe("Refund", () => { const refundFrame = (await screen.findByTestId( "refundFrame", )) as HTMLDivElement; - expect(refundFrame.children.length).toEqual(5); + expect(refundFrame.children.length).toEqual(7); const uploadInput = await screen.findByTestId("refundUpload"); const swapFile = new File(["{}"], "swap.json", {