Skip to content

Commit d6cfe32

Browse files
committed
show transaction notifications
1 parent 376f944 commit d6cfe32

File tree

6 files changed

+163
-69
lines changed

6 files changed

+163
-69
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"react-dom": "^18.3.1",
2424
"react-portal": "^4.2.2",
2525
"react-router-dom": "^6.26.1",
26+
"react-toastify": "^10.0.5",
2627
"rxjs": "^7.8.1",
2728
"tailwind-merge": "^2.5.2"
2829
},

pnpm-lock.yaml

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/actions/send/Submit.tsx

Lines changed: 81 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,97 @@
11
import * as Progress from "@radix-ui/react-progress"
2-
import { useStateObservable } from "@react-rxjs/core"
3-
import { useEffect, useState } from "react"
4-
import { senderChainId$, submitTransfer$, transferStatus$ } from "./send"
5-
import { state } from "@react-rxjs/core"
6-
import { allChains, ChainId } from "@/api"
7-
import { of, merge } from "rxjs"
2+
import { state, useStateObservable, withDefault } from "@react-rxjs/core"
3+
import { map, of, switchMap, withLatestFrom } from "rxjs"
4+
import {
5+
senderChainId$,
6+
submitTransfer$,
7+
TransactionStatus,
8+
transferStatus$,
9+
} from "./send"
10+
import { allChains } from "@/api"
11+
import { twMerge } from "tailwind-merge"
12+
13+
transferStatus$.subscribe()
814

915
const finalizedBlock$ = state(
10-
(chainId: ChainId | "") =>
11-
chainId === "" ? of(null) : allChains[chainId].client.finalizedBlock$,
16+
senderChainId$.pipe(
17+
switchMap((chain) =>
18+
chain ? allChains[chain].client.finalizedBlock$ : of(null),
19+
),
20+
),
1221
null,
1322
)
1423

15-
const subscriptions = state(merge(transferStatus$)).subscribe()
24+
const progress$ = transferStatus$.pipeState(
25+
withLatestFrom(finalizedBlock$),
26+
switchMap(([v, finalized]) => {
27+
if (!v) return [null]
1628

17-
export default function Submit() {
18-
const txStatus = useStateObservable(transferStatus$)
19-
const selectedChain = useStateObservable(senderChainId$)!
20-
const finalizedBlock = useStateObservable(finalizedBlock$(selectedChain))
29+
if (
30+
v.status === TransactionStatus.BestBlock &&
31+
"number" in v &&
32+
v.number &&
33+
finalized
34+
) {
35+
const start = finalized.number
36+
const end = v.number
37+
return finalizedBlock$.pipe(
38+
map((finalized) => ({
39+
ok: v.ok,
40+
value: v.status,
41+
subProgress: {
42+
value: finalized!.number - start,
43+
total: end - start,
44+
},
45+
})),
46+
)
47+
}
2148

22-
const [isSubmitting, setSubmitting] = useState(false)
23-
const [isTransacting, setIsTransacting] = useState(false)
24-
const [statusLabel, setStatusLabel] = useState("")
49+
return [
50+
{
51+
ok: v.ok,
52+
value: v.status,
53+
},
54+
]
55+
}),
56+
withDefault(null),
57+
)
2558

26-
const [progress, setProgress] = useState(2)
59+
const transactionStatusLabel: Record<TransactionStatus, string> = {
60+
[TransactionStatus.Signing]: "Signing",
61+
[TransactionStatus.Broadcasted]:
62+
"Broadcasting complete. Sending to best blocks",
63+
[TransactionStatus.BestBlock]: "In best block state: ",
64+
[TransactionStatus.Finalized]: "Transaction completed successfully!",
65+
}
2766

28-
useEffect(() => {
29-
setProgress(5)
30-
}, [isSubmitting])
67+
export default function Submit() {
68+
const selectedChain = useStateObservable(senderChainId$)
3169

32-
useEffect(() => {
33-
switch (txStatus?.type) {
34-
case "signed": {
35-
setProgress(25)
36-
setIsTransacting(true)
37-
setStatusLabel("Transaction Signed successfully. Broadcasting...")
38-
break
39-
}
40-
case "broadcasted":
41-
setProgress(50)
42-
setStatusLabel("Broadcasting complete. Sending to best blocks")
43-
break
44-
case "txBestBlocksState":
45-
setProgress(75)
46-
setStatusLabel("In best blocks state: ")
47-
// set micro progress per block
48-
break
49-
case "finalized":
50-
setProgress(100)
51-
setStatusLabel("Transaction completed successfully!")
70+
const txProgress = useStateObservable(progress$)
71+
const isTransacting =
72+
txProgress && txProgress.value > TransactionStatus.Signing
73+
const isSigning = !!txProgress && !isTransacting
74+
const progress =
75+
(txProgress?.value ?? 0) +
76+
(txProgress && "subProgress" in txProgress
77+
? (50 * txProgress.subProgress.value) / txProgress.subProgress.total
78+
: 0)
5279

53-
// setTimeout(() => )
54-
break
55-
}
56-
}, [txStatus])
80+
if (!selectedChain) return null
5781

5882
return (
5983
<div className="mb-5">
6084
{isTransacting ? (
6185
<>
62-
<div className="mb-4 text-pink font-semibold flex flex-col">
63-
<span>{statusLabel}</span>
64-
{txStatus?.type === "txBestBlocksState" && txStatus.found === true
65-
? `${finalizedBlock?.number}/${txStatus.block.number}`
86+
<div
87+
className={twMerge(
88+
"mb-4 text-pink font-semibold flex flex-col",
89+
txProgress.ok ? "" : "text-orange-500",
90+
)}
91+
>
92+
<span>{transactionStatusLabel[txProgress.value]}</span>
93+
{"subProgress" in txProgress
94+
? `${txProgress.subProgress.value}/${txProgress.subProgress.total}`
6695
: null}
6796
</div>
6897
<Progress.Root
@@ -81,14 +110,11 @@ export default function Submit() {
81110
) : (
82111
<>
83112
<button
84-
className={`rounded mb-10 bg-pink p-2 text-white w-40 ${!selectedChain || isSubmitting ? "opacity-80" : ""}`}
85-
disabled={!selectedChain || isSubmitting}
86-
onClick={() => {
87-
submitTransfer$()
88-
setSubmitting(true)
89-
}}
113+
className={`rounded mb-10 bg-pink p-2 text-white w-40 ${isSigning ? "opacity-80" : ""}`}
114+
disabled={isSigning}
115+
onClick={submitTransfer$}
90116
>
91-
{isSubmitting ? "Sign transaction" : "Send Transaction"}
117+
{isSigning ? "Sign transaction" : "Send Transaction"}
92118
</button>
93119
</>
94120
)}

src/actions/send/send.ts

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,20 @@ import { parseCurrency } from "@/utils/currency"
1313
import { state } from "@react-rxjs/core"
1414
import { createSignal } from "@react-rxjs/utils"
1515
import {
16+
catchError,
1617
combineLatest,
1718
defer,
1819
filter,
1920
map,
2021
materialize,
2122
of,
2223
scan,
24+
startWith,
2325
switchMap,
24-
tap,
2526
withLatestFrom,
2627
} from "rxjs"
2728
import { findRoute, predefinedTransfers } from "./transfers"
29+
import { toast } from "react-toastify"
2830

2931
const PATTERN = "/send/:chain/:account"
3032

@@ -213,10 +215,7 @@ export const feeEstimation$ = state(
213215
.tx(recipient, transferAmount)
214216
.getEstimatedFees(selectedAccount.address),
215217
),
216-
).pipe(
217-
tap((v) => console.log(v)),
218-
map((v) => v.reduce((a, b) => a + b)),
219-
)
218+
).pipe(map((v) => v.reduce((a, b) => a + b, 0n)))
220219
: [null]
221220
},
222221
),
@@ -252,15 +251,72 @@ export const tx$ = state(
252251
),
253252
)
254253

254+
export enum TransactionStatus {
255+
Signing = 5,
256+
Broadcasted = 25,
257+
BestBlock = 50,
258+
Finalized = 100,
259+
}
260+
261+
const errorToast = (error: string) =>
262+
toast(error, {
263+
type: "error",
264+
})
265+
266+
const successToast = (message: string) =>
267+
toast(message, {
268+
type: "success",
269+
})
270+
255271
export const transferStatus$ = state(
256272
onSubmitted$.pipe(
257273
withLatestFrom(tx$, selectedAccount$),
258274
switchMap(([, tx, selectedAccount]) => {
259275
if (!tx || !selectedAccount) return []
260276

261-
return tx.signSubmitAndWatch(selectedAccount.polkadotSigner)
277+
return tx.signSubmitAndWatch(selectedAccount.polkadotSigner).pipe(
278+
map((v) => {
279+
switch (v.type) {
280+
case "signed":
281+
case "broadcasted":
282+
return {
283+
ok: true,
284+
status: TransactionStatus.Broadcasted,
285+
}
286+
case "txBestBlocksState":
287+
return {
288+
ok: v.found && v.ok,
289+
status: TransactionStatus.BestBlock,
290+
number: v.found ? v.block.number : null,
291+
}
292+
case "finalized":
293+
if (!v.ok) {
294+
console.log("dispatchError", v.dispatchError)
295+
const error =
296+
v.dispatchError.type === "Module"
297+
? JSON.stringify(v.dispatchError.value)
298+
: v.dispatchError.type
299+
errorToast("Transaction didn't succeed: " + error)
300+
return null
301+
}
302+
successToast("Transaction succeeded 🎉")
303+
return {
304+
ok: true,
305+
status: TransactionStatus.Finalized,
306+
}
307+
}
308+
}),
309+
catchError((err) => {
310+
errorToast("Transaction failed: " + (err.message ?? "Unknown"))
311+
312+
return of(null)
313+
}),
314+
startWith({
315+
ok: true,
316+
status: TransactionStatus.Signing,
317+
}),
318+
)
262319
}),
263-
tap((status) => console.log("Tx status: ", status)),
264320
),
265321
null,
266322
)

src/actions/void.tsx

Lines changed: 0 additions & 7 deletions
This file was deleted.

src/main.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@ import { createRoot } from "react-dom/client"
33
import App from "./App.tsx"
44
import "./index.css"
55
import { Router } from "./router"
6+
import { ToastContainer } from "react-toastify"
7+
import "react-toastify/dist/ReactToastify.css"
68

79
createRoot(document.getElementById("root")!).render(
810
<StrictMode>
911
<Router>
1012
<App />
1113
</Router>
14+
<ToastContainer />
1215
</StrictMode>,
1316
)

0 commit comments

Comments
 (0)