From d6cfe32f44e90931fa8d35d2c494b2af7cf37cdf Mon Sep 17 00:00:00 2001 From: Victor Oliva Date: Tue, 20 Aug 2024 19:58:37 +0200 Subject: [PATCH] show transaction notifications --- package.json | 1 + pnpm-lock.yaml | 15 ++++ src/actions/send/Submit.tsx | 136 +++++++++++++++++++++--------------- src/actions/send/send.ts | 70 +++++++++++++++++-- src/actions/void.tsx | 7 -- src/main.tsx | 3 + 6 files changed, 163 insertions(+), 69 deletions(-) delete mode 100644 src/actions/void.tsx diff --git a/package.json b/package.json index 27b5929..b0562e4 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "react-dom": "^18.3.1", "react-portal": "^4.2.2", "react-router-dom": "^6.26.1", + "react-toastify": "^10.0.5", "rxjs": "^7.8.1", "tailwind-merge": "^2.5.2" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d754bae..d77e5fb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,9 @@ importers: react-router-dom: specifier: ^6.26.1 version: 6.26.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-toastify: + specifier: ^10.0.5 + version: 10.0.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) rxjs: specifier: ^7.8.1 version: 7.8.1 @@ -1810,6 +1813,12 @@ packages: peerDependencies: react: '>=16.8' + react-toastify@10.0.5: + resolution: {integrity: sha512-mNKt2jBXJg4O7pSdbNUfDdTsK9FIdikfsIE/yUCxbAEXl4HMyJaivrVFcn3Elvt5xvCQYhUZm+hqTIu1UXM3Pw==} + peerDependencies: + react: '>=18' + react-dom: '>=18' + react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -3845,6 +3854,12 @@ snapshots: '@remix-run/router': 1.19.1 react: 18.3.1 + react-toastify@10.0.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + clsx: 2.1.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react@18.3.1: dependencies: loose-envify: 1.4.0 diff --git a/src/actions/send/Submit.tsx b/src/actions/send/Submit.tsx index a2ebd0e..9580c28 100644 --- a/src/actions/send/Submit.tsx +++ b/src/actions/send/Submit.tsx @@ -1,68 +1,97 @@ import * as Progress from "@radix-ui/react-progress" -import { useStateObservable } from "@react-rxjs/core" -import { useEffect, useState } from "react" -import { senderChainId$, submitTransfer$, transferStatus$ } from "./send" -import { state } from "@react-rxjs/core" -import { allChains, ChainId } from "@/api" -import { of, merge } from "rxjs" +import { state, useStateObservable, withDefault } from "@react-rxjs/core" +import { map, of, switchMap, withLatestFrom } from "rxjs" +import { + senderChainId$, + submitTransfer$, + TransactionStatus, + transferStatus$, +} from "./send" +import { allChains } from "@/api" +import { twMerge } from "tailwind-merge" + +transferStatus$.subscribe() const finalizedBlock$ = state( - (chainId: ChainId | "") => - chainId === "" ? of(null) : allChains[chainId].client.finalizedBlock$, + senderChainId$.pipe( + switchMap((chain) => + chain ? allChains[chain].client.finalizedBlock$ : of(null), + ), + ), null, ) -const subscriptions = state(merge(transferStatus$)).subscribe() +const progress$ = transferStatus$.pipeState( + withLatestFrom(finalizedBlock$), + switchMap(([v, finalized]) => { + if (!v) return [null] -export default function Submit() { - const txStatus = useStateObservable(transferStatus$) - const selectedChain = useStateObservable(senderChainId$)! - const finalizedBlock = useStateObservable(finalizedBlock$(selectedChain)) + if ( + v.status === TransactionStatus.BestBlock && + "number" in v && + v.number && + finalized + ) { + const start = finalized.number + const end = v.number + return finalizedBlock$.pipe( + map((finalized) => ({ + ok: v.ok, + value: v.status, + subProgress: { + value: finalized!.number - start, + total: end - start, + }, + })), + ) + } - const [isSubmitting, setSubmitting] = useState(false) - const [isTransacting, setIsTransacting] = useState(false) - const [statusLabel, setStatusLabel] = useState("") + return [ + { + ok: v.ok, + value: v.status, + }, + ] + }), + withDefault(null), +) - const [progress, setProgress] = useState(2) +const transactionStatusLabel: Record = { + [TransactionStatus.Signing]: "Signing", + [TransactionStatus.Broadcasted]: + "Broadcasting complete. Sending to best blocks", + [TransactionStatus.BestBlock]: "In best block state: ", + [TransactionStatus.Finalized]: "Transaction completed successfully!", +} - useEffect(() => { - setProgress(5) - }, [isSubmitting]) +export default function Submit() { + const selectedChain = useStateObservable(senderChainId$) - useEffect(() => { - switch (txStatus?.type) { - case "signed": { - setProgress(25) - setIsTransacting(true) - setStatusLabel("Transaction Signed successfully. Broadcasting...") - break - } - case "broadcasted": - setProgress(50) - setStatusLabel("Broadcasting complete. Sending to best blocks") - break - case "txBestBlocksState": - setProgress(75) - setStatusLabel("In best blocks state: ") - // set micro progress per block - break - case "finalized": - setProgress(100) - setStatusLabel("Transaction completed successfully!") + const txProgress = useStateObservable(progress$) + const isTransacting = + txProgress && txProgress.value > TransactionStatus.Signing + const isSigning = !!txProgress && !isTransacting + const progress = + (txProgress?.value ?? 0) + + (txProgress && "subProgress" in txProgress + ? (50 * txProgress.subProgress.value) / txProgress.subProgress.total + : 0) - // setTimeout(() => ) - break - } - }, [txStatus]) + if (!selectedChain) return null return (
{isTransacting ? ( <> -
- {statusLabel} - {txStatus?.type === "txBestBlocksState" && txStatus.found === true - ? `${finalizedBlock?.number}/${txStatus.block.number}` +
+ {transactionStatusLabel[txProgress.value]} + {"subProgress" in txProgress + ? `${txProgress.subProgress.value}/${txProgress.subProgress.total}` : null}
)} diff --git a/src/actions/send/send.ts b/src/actions/send/send.ts index b8551b2..5aec6d8 100644 --- a/src/actions/send/send.ts +++ b/src/actions/send/send.ts @@ -13,6 +13,7 @@ import { parseCurrency } from "@/utils/currency" import { state } from "@react-rxjs/core" import { createSignal } from "@react-rxjs/utils" import { + catchError, combineLatest, defer, filter, @@ -20,11 +21,12 @@ import { materialize, of, scan, + startWith, switchMap, - tap, withLatestFrom, } from "rxjs" import { findRoute, predefinedTransfers } from "./transfers" +import { toast } from "react-toastify" const PATTERN = "/send/:chain/:account" @@ -213,10 +215,7 @@ export const feeEstimation$ = state( .tx(recipient, transferAmount) .getEstimatedFees(selectedAccount.address), ), - ).pipe( - tap((v) => console.log(v)), - map((v) => v.reduce((a, b) => a + b)), - ) + ).pipe(map((v) => v.reduce((a, b) => a + b, 0n))) : [null] }, ), @@ -252,15 +251,72 @@ export const tx$ = state( ), ) +export enum TransactionStatus { + Signing = 5, + Broadcasted = 25, + BestBlock = 50, + Finalized = 100, +} + +const errorToast = (error: string) => + toast(error, { + type: "error", + }) + +const successToast = (message: string) => + toast(message, { + type: "success", + }) + export const transferStatus$ = state( onSubmitted$.pipe( withLatestFrom(tx$, selectedAccount$), switchMap(([, tx, selectedAccount]) => { if (!tx || !selectedAccount) return [] - return tx.signSubmitAndWatch(selectedAccount.polkadotSigner) + return tx.signSubmitAndWatch(selectedAccount.polkadotSigner).pipe( + map((v) => { + switch (v.type) { + case "signed": + case "broadcasted": + return { + ok: true, + status: TransactionStatus.Broadcasted, + } + case "txBestBlocksState": + return { + ok: v.found && v.ok, + status: TransactionStatus.BestBlock, + number: v.found ? v.block.number : null, + } + case "finalized": + if (!v.ok) { + console.log("dispatchError", v.dispatchError) + const error = + v.dispatchError.type === "Module" + ? JSON.stringify(v.dispatchError.value) + : v.dispatchError.type + errorToast("Transaction didn't succeed: " + error) + return null + } + successToast("Transaction succeeded 🎉") + return { + ok: true, + status: TransactionStatus.Finalized, + } + } + }), + catchError((err) => { + errorToast("Transaction failed: " + (err.message ?? "Unknown")) + + return of(null) + }), + startWith({ + ok: true, + status: TransactionStatus.Signing, + }), + ) }), - tap((status) => console.log("Tx status: ", status)), ), null, ) diff --git a/src/actions/void.tsx b/src/actions/void.tsx deleted file mode 100644 index a777e5d..0000000 --- a/src/actions/void.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { polkadotApi } from "@/api" - -console.log(polkadotApi.apis) - -export default function Void() { - return
Void action
-} diff --git a/src/main.tsx b/src/main.tsx index 99e9f5f..14309eb 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,11 +3,14 @@ import { createRoot } from "react-dom/client" import App from "./App.tsx" import "./index.css" import { Router } from "./router" +import { ToastContainer } from "react-toastify" +import "react-toastify/dist/ReactToastify.css" createRoot(document.getElementById("root")!).render( + , )