Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show swap progress #4936

Merged
merged 15 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions frontend/app/src/components/home/BalanceWithRefresh.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
export let bold = false;
export let toppingUp = false;
export let showTopUp = false;
export let showRefresh = true;
export let refreshing = false;

$: cryptoLookup = client.cryptoLookup;
Expand Down Expand Up @@ -58,9 +59,11 @@
<div class="amount" class:bold>
{client.formatTokens(value, minDecimals, tokenDetails.decimals)}
</div>
<div class="refresh" class:refreshing on:click={refresh}>
<Refresh size={"1em"} color={"var(--icon-txt)"} />
</div>
{#if showRefresh}
<div class="refresh" class:refreshing on:click={refresh}>
<Refresh size={"1em"} color={"var(--icon-txt)"} />
</div>
{/if}
{#if showTopUp}
<div class="top-up" on:click={topUp} title={$_("cryptoAccount.topUp")}>
<Plus size={"1em"} color={toppingUp ? "var(--icon-selected)" : "var(--icon-txt)"} />
Expand Down
14 changes: 7 additions & 7 deletions frontend/app/src/components/home/profile/Accounts.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import ChevronDown from "svelte-material-icons/ChevronDown.svelte";
import ArrowRightBoldCircle from "svelte-material-icons/ArrowRightBoldCircle.svelte";
import ArrowLeftBoldCircle from "svelte-material-icons/ArrowLeftBoldCircle.svelte";
//import SwapIcon from "svelte-material-icons/SwapHorizontal.svelte";
import SwapIcon from "svelte-material-icons/SwapHorizontal.svelte";
import ViewList from "svelte-material-icons/ViewList.svelte";
import { _ } from "svelte-i18n";
import ErrorMessage from "../../ErrorMessage.svelte";
Expand Down Expand Up @@ -65,10 +65,10 @@
manageMode = "send";
}

// function showSwap(ledger: string) {
// selectedLedger = ledger;
// manageMode = "swap";
// }
function showSwap(ledger: string) {
selectedLedger = ledger;
manageMode = "swap";
}
</script>

{#if manageMode !== "none" && selectedLedger !== undefined}
Expand Down Expand Up @@ -126,7 +126,7 @@
slot="icon" />
<div slot="text">{$_("cryptoAccount.receive")}</div>
</MenuItem>
<!-- {#await client.getTokenSwaps(token.ledger) then swaps}
{#await client.getTokenSwaps(token.ledger) then swaps}
{#if Object.keys(swaps).length > 0}
<MenuItem on:click={() => showSwap(token.ledger)}>
<SwapIcon
Expand All @@ -136,7 +136,7 @@
<div slot="text">{$_("cryptoAccount.swap")}</div>
</MenuItem>
{/if}
{/await} -->
{/await}
{#if snsLedgers.has(token.ledger)}
<MenuItem on:click={() => (transactionsFor = token)}>
<ViewList
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
$: howToBuyUrl = tokenDetails.howToBuyUrl;
$: title = $_(`cryptoAccount.${mode}Token`, { values: { symbol } });
$: cryptoBalance = client.cryptoBalance;
$: swapping = mode === "swap" && swapStep === "swap" && busy;
$: secondaryButtonText = $_(
capturingAccount
? "noThanks"
Expand All @@ -55,7 +56,7 @@
);

$: remainingBalance =
amountToSend > BigInt(0)
amountToSend > BigInt(0) && swapStep !== "swapped"
? $cryptoBalance[ledger] - amountToSend - transferFees
: $cryptoBalance[ledger];

Expand Down Expand Up @@ -97,7 +98,7 @@
}
</script>

<Overlay on:close dismissible>
<Overlay on:close>
<ModalContent>
<span class="header" slot="header">
<div class="main-title">{title}</div>
Expand All @@ -107,6 +108,7 @@
value={remainingBalance}
label={$_("cryptoAccount.shortBalanceLabel")}
minDecimals={2}
showRefresh={!swapping}
bold
on:refreshed={onBalanceRefreshed}
on:error={onBalanceRefreshError} />
Expand Down Expand Up @@ -145,10 +147,12 @@
</form>
<span slot="footer">
<ButtonGroup>
<Button
secondary={mode !== "receive"}
tiny={$mobileWidth}
on:click={onSecondaryClick}>{secondaryButtonText}</Button>
{#if !swapping}
<Button
secondary={mode !== "receive"}
tiny={$mobileWidth}
on:click={onSecondaryClick}>{secondaryButtonText}</Button>
{/if}
{#if mode !== "receive" && swapStep !== "swapped"}
<Button
disabled={busy || !valid}
Expand Down
90 changes: 48 additions & 42 deletions frontend/app/src/components/home/profile/SwapCrypto.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { Record } from "@dfinity/candid/lib/cjs/idl";
import CryptoSelector from "../CryptoSelector.svelte";
import Legend from "../../Legend.svelte";
import SwapProgress from "./SwapProgress.svelte";

export let ledgerIn: string;
export let amountIn: bigint;
Expand All @@ -29,7 +30,12 @@
$: detailsIn = $cryptoLookup[ledgerIn];
$: detailsOut = ledgerOut !== undefined ? $cryptoLookup[ledgerOut] : undefined;
$: anySwapsAvailable = Object.keys(swaps).length > 0 && detailsOut !== undefined;
//$: swapping = swapStep === "swap" && busy;
$: swapping = swapStep === "swap" && busy;
$: amountInText = client.formatTokens(amountIn, 0, detailsIn.decimals);
$: minAmountOut =
bestQuote !== undefined
? (bestQuote[1] * BigInt(detailsIn.symbol === "CHAT" ? 102 : 98)) / BigInt(100)
megrogan marked this conversation as resolved.
Show resolved Hide resolved
: BigInt(0);

$: {
valid =
Expand Down Expand Up @@ -60,7 +66,6 @@

const [dexId, quote] = bestQuote!;
const amountOutText = client.formatTokens(quote, 0, detailsOut!.decimals);
const amountInText = client.formatTokens(amountIn, 0, detailsIn.decimals);
const rate = (Number(amountOutText) / Number(amountInText)).toPrecision(3);
const dex = dexName(dexId);
const swapText = $_("tokenSwap.title");
Expand Down Expand Up @@ -91,49 +96,11 @@
if (!valid) return;

busy = true;
message = "";
dispatch("error", undefined);

swapId = random128();

const [dex, quote] = bestQuote!;
const minAmountOut = (quote * BigInt(98)) / BigInt(100);
const amountInText = client.formatTokens(amountIn, 0, detailsIn.decimals);
const minAmountOutText = client.formatTokens(minAmountOut, 0, detailsOut!.decimals);
const values = {
tokenIn: detailsIn.symbol,
tokenOut: detailsOut!.symbol,
amountIn: amountInText,
minAmountOut: minAmountOutText,
dex: dexName(dex),
};

client
.swapTokens(swapId, ledgerIn, ledgerOut!, amountIn, minAmountOut, dex)
.then((response) => {
if (response.kind === "success") {
swapStep = "swapped";
const amountOutText = client.formatTokens(
response.amountOut,
0,
detailsOut!.decimals,
);
message = $_("tokenSwap.swapSucceeded", {
values: { ...values, amountOut: amountOutText },
});
} else {
dispatch("error", { error: "tokenSwap.swapFailed", values });
}
})
.catch((err) => {
client.logError(
`Failed to swap ${detailsIn.symbol} to ${detailsOut!.symbol} on ${dexName(
dex,
)}`,
err,
);
dispatch("error", { error: "tokenSwap.swapFailed", values });
})
.finally(() => (busy = false));
client.swapTokens(swapId, ledgerIn, ledgerOut!, amountIn, minAmountOut, bestQuote![0]);
}

function dexName(dex: DexId): string {
Expand All @@ -153,6 +120,34 @@
function onLedgerInSelected(ev: CustomEvent<{ ledger: string; urlFormat: string }>): void {
loadSwaps(ev.detail.ledger);
}

function onSwapComplete(
ev: CustomEvent<{ status: "success" | "failure" | "error"; amountOut: string }>,
): void {
busy = false;
swapStep = "swapped";

const minAmountOutText = client.formatTokens(minAmountOut, 0, detailsOut!.decimals);
const values = {
tokenIn: detailsIn.symbol,
tokenOut: detailsOut!.symbol,
amountIn: amountInText,
amountOut: ev.detail.amountOut,
minAmountOut: minAmountOutText,
dex: dexName(bestQuote![0]),
};

if (ev.detail.status === "success") {
message = $_("tokenSwap.swapSucceeded", { values });
} else if (ev.detail.status === "failure") {
message = $_("tokenSwap.swapFailed", { values });
} else {
message = $_("tokenSwap.swapError");
}

client.refreshAccountBalance(ledgerIn);
client.refreshAccountBalance(ledgerOut!);
}
</script>

{#if swapStep === "quote"}
Expand Down Expand Up @@ -187,6 +182,17 @@
{/await}
{/if}

{#if swapping && swapId !== undefined && detailsOut !== undefined && bestQuote !== undefined}
<SwapProgress
{swapId}
tokenIn={detailsIn.symbol}
tokenOut={detailsOut.symbol}
amountIn={amountInText}
decimalsOut={detailsOut.decimals}
dex={dexName(bestQuote[0])}
on:complete={onSwapComplete} />
{/if}

{#if message !== undefined}
<Markdown inline={false} text={message} />
{/if}
Expand Down
109 changes: 109 additions & 0 deletions frontend/app/src/components/home/profile/SwapProgress.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<script lang="ts">
import type { OpenChat } from "openchat-client";
import { Poller } from "openchat-client";
import { createEventDispatcher, getContext, onMount } from "svelte";
import { _ } from "svelte-i18n";

export let swapId: bigint;
export let tokenIn: string;
export let tokenOut: string;
export let amountIn: string;
export let decimalsOut: number;
export let dex: string;

const client = getContext<OpenChat>("client");
const dispatch = createEventDispatcher();
const POLL_INTERVAL = 1000;

let step = 0;
let amountOut = "";
let swapProgressSteps = ["get", "deposit", "notify", "swap", "withdraw"];
let swapProgressResults: (boolean | undefined)[] = [
undefined,
undefined,
undefined,
undefined,
undefined,
];

$: values = {
tokenIn,
tokenOut,
amountIn,
amountOut,
dex,
};

onMount(() => {
const poller = new Poller(querySwapProgress, POLL_INTERVAL, POLL_INTERVAL, true);

return () => poller.stop();
});

async function querySwapProgress() {
let response = await client.tokenSwapStatus(swapId);

if (response.kind === "success") {
if (response.withdrawnFromDex?.kind === "ok" && step <= 4) {
const status =
response.amountSwapped?.kind === "ok" &&
response.amountSwapped.value.kind === "ok"
? "success"
: "failure";
swapProgressResults = [true, true, true, status === "success", true];
step = 5;
onComplete(status);
} else if (response.amountSwapped?.kind === "ok" && step <= 3) {
if (response.amountSwapped.value.kind === "ok") {
amountOut = client.formatTokens(
response.amountSwapped.value.value,
0,
decimalsOut,
);
swapProgressResults = [true, true, true, true, undefined];
} else {
swapProgressSteps[4] = "refund";
swapProgressResults = [true, true, true, false, undefined];
}
step = 4;
} else if (response.notifyDex?.kind == "ok" && step <= 2) {
swapProgressResults = [true, true, true, undefined, undefined];
step = 3;
} else if (response.transfer?.kind == "ok" && step <= 1) {
swapProgressResults = [true, true, undefined, undefined, undefined];
step = 2;
} else if (response.transfer?.kind == "error" && step <= 1) {
swapProgressResults = [true, false, undefined, undefined, undefined];
onComplete("error");
} else if (response.depositAccount?.kind == "ok" && step === 0) {
swapProgressResults = [true, undefined, undefined, undefined, undefined];
step = 1;
} else if (response.depositAccount?.kind == "error" && step === 0) {
swapProgressResults = [false, undefined, undefined, undefined, undefined];
onComplete("error");
}
}
}

function onComplete(status: "success" | "failure" | "error") {
dispatch("complete", { status, amountOut });
}
</script>

<ol>
{#each swapProgressSteps as label, i}
{#if step >= i}
<li>
{$_(`tokenSwap.progress.${label}`, {
values,
})}... {#if swapProgressResults[i] === true}✅️{:else if swapProgressResults[i] === false}❌️{/if}
</li>
{/if}
{/each}
</ol>

<style lang="scss">
ol {
margin-left: $sp4;
}
</style>
13 changes: 11 additions & 2 deletions frontend/app/src/i18n/cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -1188,8 +1188,17 @@
"getTokenSwapsError": "获取 {tokenIn} 的交换时出错",
"quoteError": "获取交换 {tokenIn} 的报价时出错",
"findingAvailableTokens": "正在寻找可用的代币进行交换...",
"swapFailed": "无法使用 {amountIn} 将 **{tokenIn}** {minAmountOut} 至少交换为 **{tokenOut}** {dex}",
"swapSucceeded": "已使用 {amountIn} 成功将 **{tokenIn}** {amountOut} 交换为 **{tokenOut}** {dex}",
"swapInfo": "最优惠的报价是来自 {rate} 的 **{dex}**。这为 **{amountOut}** {tokenOut} 提供了 **{amountIn}** {tokenIn}。仅当收到的 {tokenOut} 金额至少为报价金额的 98% 时,交换才会继续进行。您想继续尝试“{swap}”吗?"
"swapFailed": "{dex} 无法将 **{amountIn}** {tokenIn} 交换为至少 **{minAmountOut}** {tokenOut}。",
"progress": {
"get": "获取存款账户",
"deposit": "将 {amountIn} {tokenIn} 转移到 {dex}",
"notify": "通知{dex}存款",
"swap": "指示 {dex} 执行交换",
"withdraw": "从 {amountOut} 中撤回 {tokenOut} {dex}",
"refund": "从 {amountIn} 退款 {tokenIn} {dex}"
},
"swapInfo": "所报的最佳费率是 {rate} 每 {tokenOut} 的 **{tokenIn}** {dex}。这为 **{amountOut}** {tokenOut} 提供了 **{amountIn}** {tokenIn}。仅当收到的 {tokenOut} 金额至少为报价金额的 98% 时,交换才会进行。您想继续尝试“{swap}”吗?",
"swapError": "无法处理交换。没有资金损失。"
}
}
Loading
Loading