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", {