restyle transactions
This commit is contained in:
@@ -28,8 +28,8 @@ export const TabsTrigger: React.FC<DivProps & { active?: boolean }> = ({
|
||||
{...props}
|
||||
className={cn(
|
||||
"cursor-pointer inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
"border border-polkadot-500",
|
||||
active ? "bg-polkadot-500 text-foreground shadow-sm" : "",
|
||||
"border border-primary/50",
|
||||
active ? "bg-primary/50 text-foreground shadow-sm" : "",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
||||
+2
-1
@@ -8,9 +8,10 @@ import { dynamicBuilder$ } from "./chain.state.ts"
|
||||
import { TooltipProvider } from "./components/Tooltip.tsx"
|
||||
import "./index.css"
|
||||
import { explorer$ } from "./pages/Explorer"
|
||||
import { transactions$ } from "./pages/Transactions"
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<Subscribe source$={merge(dynamicBuilder$, explorer$)}>
|
||||
<Subscribe source$={merge(dynamicBuilder$, explorer$, transactions$)}>
|
||||
<RemoveSubscribe>
|
||||
<StrictMode>
|
||||
<BrowserRouter>
|
||||
|
||||
@@ -102,19 +102,36 @@ export const selectedAccount$ = state(
|
||||
null,
|
||||
)
|
||||
|
||||
const allAccounts$ = state(
|
||||
combineLatest([walletConnectAccounts$, accountsByExtension$]).pipe(
|
||||
map(([wcAccounts, accounts]) => [
|
||||
...Object.keys(wcAccounts).map((account) => `${account}-wallet_connect`),
|
||||
...[...accounts.entries()].flatMap(([extension, accounts]) =>
|
||||
accounts.map((account) => `${account.address}-${extension}`),
|
||||
),
|
||||
]),
|
||||
),
|
||||
[],
|
||||
)
|
||||
|
||||
export const AccountProvider: React.FC = () => {
|
||||
const value = useStateObservable(selectedValue$)
|
||||
const extensions = useStateObservable(selectedExtensions$)
|
||||
const walletConnect = useStateObservable(walletConnectStatus$)
|
||||
const allAccounts = useStateObservable(allAccounts$)
|
||||
|
||||
const activeExtensions = [...extensions.values()].filter((v) => !!v)
|
||||
const valueExists = value && allAccounts.includes(value)
|
||||
|
||||
if (!activeExtensions.length && walletConnect.type !== "connected")
|
||||
return null
|
||||
|
||||
return (
|
||||
<Select value={value ?? ""} onValueChange={selectValue}>
|
||||
<SelectTrigger className="h-auto">
|
||||
<Select
|
||||
value={valueExists ? (value ?? "") : ""}
|
||||
onValueChange={selectValue}
|
||||
>
|
||||
<SelectTrigger className="h-auto border-foreground/30">
|
||||
<SelectValue placeholder="Select an account" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -17,9 +17,6 @@ export const ExtensionProvider: React.FC = () => {
|
||||
const selectedExtensions = useStateObservable(selectedExtensions$)
|
||||
const walletConnectStatus = useStateObservable(walletConnectStatus$)
|
||||
|
||||
if (availableExtensions.length === 0)
|
||||
return <div>No extension provider detected</div>
|
||||
|
||||
return (
|
||||
<>
|
||||
<Label>Click on the provider name to toggle it:</Label>
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
import * as React from "react"
|
||||
import * as Dialog from "@radix-ui/react-dialog"
|
||||
import { X, Loader2 } from "lucide-react"
|
||||
import { combineKeys, createSignal, partitionByKey } from "@react-rxjs/utils"
|
||||
import { HexString, InvalidTxError, TxBroadcastEvent } from "polkadot-api"
|
||||
import { state, useStateObservable, withDefault } from "@react-rxjs/core"
|
||||
import { chainClient$ } from "@/chain.state"
|
||||
import {
|
||||
catchError,
|
||||
map,
|
||||
mergeMap,
|
||||
Observable,
|
||||
of,
|
||||
skip,
|
||||
takeUntil,
|
||||
tap,
|
||||
withLatestFrom,
|
||||
} from "rxjs"
|
||||
import { shortStr } from "@/utils"
|
||||
import { CopyText } from "@/components/Copy"
|
||||
import { Link } from "react-router-dom"
|
||||
|
||||
const [signedTx$, onNexTx] = createSignal<HexString>()
|
||||
export { onNexTx }
|
||||
|
||||
const transactions$ = signedTx$.pipe(
|
||||
withLatestFrom(chainClient$),
|
||||
mergeMap(([tx, { client }]) => {
|
||||
const submitAndWatch = client.submitAndWatch as (
|
||||
tx: HexString,
|
||||
at: string,
|
||||
withSigned: boolean,
|
||||
) => Observable<TxBroadcastEvent>
|
||||
|
||||
let txHash: string = ""
|
||||
return submitAndWatch(tx, "finalized", true).pipe(
|
||||
tap((e) => {
|
||||
txHash = e.txHash
|
||||
}),
|
||||
catchError((err) =>
|
||||
of({
|
||||
type:
|
||||
err instanceof InvalidTxError
|
||||
? ("invalid" as "invalid")
|
||||
: ("error" as "error"),
|
||||
txHash,
|
||||
value: err,
|
||||
}),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
const [tx$, txKeys$] = partitionByKey(
|
||||
transactions$,
|
||||
(x) => x.txHash,
|
||||
(x) => x.pipe(takeUntil(chainClient$.pipe(skip(1)))),
|
||||
)
|
||||
|
||||
const grouppedTransactions$ = state(combineKeys(txKeys$, tx$))
|
||||
const onGoingEvents = new Set(["signed", "broadcasted", "txBestBlocksState"])
|
||||
|
||||
const txsSummary$ = grouppedTransactions$.pipeState(
|
||||
map((x) => {
|
||||
const all = [...x.values()]
|
||||
const nOngoing = all.filter((x) => onGoingEvents.has(x.type)).length
|
||||
return { nTxs: all.length, nOngoing }
|
||||
}),
|
||||
withDefault({ nTxs: 0, nOngoing: 0 }),
|
||||
)
|
||||
txsSummary$.subscribe()
|
||||
|
||||
const transactionList$ = grouppedTransactions$.pipeState(
|
||||
map((x) =>
|
||||
[...x.values()].map((props) => (
|
||||
<Transaction key={props.txHash} {...props} />
|
||||
)),
|
||||
),
|
||||
withDefault([]),
|
||||
)
|
||||
transactionList$.subscribe()
|
||||
|
||||
const Transaction: React.FC<
|
||||
TxBroadcastEvent | { type: "invalid" | "error"; value: any; txHash: string }
|
||||
> = (event) => {
|
||||
const getStatus = () => {
|
||||
const { type } = event
|
||||
switch (event.type) {
|
||||
case "error":
|
||||
return <span>There was an unexpected error.</span>
|
||||
case "invalid":
|
||||
return (
|
||||
<span>
|
||||
Invalid transaction. {JSON.stringify(event.value, null, 2)}
|
||||
</span>
|
||||
)
|
||||
case "txBestBlocksState": {
|
||||
return event.found ? (
|
||||
<span>
|
||||
Transaction in{" "}
|
||||
<Link
|
||||
className="underline font-bold"
|
||||
to={`/explorer/${event.block.hash}#tx=${event.block.index}`}
|
||||
>
|
||||
best block
|
||||
</Link>
|
||||
.
|
||||
</span>
|
||||
) : (
|
||||
<span>Transaction no longer in a best block.</span>
|
||||
)
|
||||
}
|
||||
case "signed":
|
||||
case "broadcasted":
|
||||
return <span>Transaction {type}.</span>
|
||||
default:
|
||||
return (
|
||||
<span>
|
||||
Transaction in{" "}
|
||||
<Link
|
||||
className="underline font-bold"
|
||||
to={`/explorer/${event.block.hash}#tx=${event.block.index}`}
|
||||
>
|
||||
{" "}
|
||||
finalized block
|
||||
</Link>
|
||||
.
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
||||
const { txHash, type } = event
|
||||
return (
|
||||
<div className="mb-4 p-3 bg-polkadot-800 rounded-lg">
|
||||
<p className="font-medium">
|
||||
{shortStr(txHash, 14)} <CopyText text={txHash} />
|
||||
</p>
|
||||
<p
|
||||
className={`text-sm ${
|
||||
onGoingEvents.has(type)
|
||||
? "text-yellow-500"
|
||||
: type === "finalized"
|
||||
? "text-green-500"
|
||||
: "text-red-500"
|
||||
}`}
|
||||
>
|
||||
{getStatus()}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function Transactions() {
|
||||
const [isOpen, setIsOpen] = React.useState(false)
|
||||
const { nTxs, nOngoing } = useStateObservable(txsSummary$)
|
||||
return !nTxs ? null : (
|
||||
<Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
|
||||
<Dialog.Trigger asChild>
|
||||
<button className="fixed bottom-4 right-4 bg-polkadot-500 text-white px-4 py-2 rounded-full shadow-lg hover:bg-white hover:font-bold hover:text-polkadot-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 flex items-center space-x-2">
|
||||
{nOngoing > 0 && (
|
||||
<>
|
||||
<Loader2 className="animate-spin" size={20} />
|
||||
<span className="font-bold">{nOngoing}</span>
|
||||
</>
|
||||
)}
|
||||
<span>Transactions</span>
|
||||
</button>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="fixed inset-0 bg-black/50" />
|
||||
<Dialog.Content className="fixed bottom-0 border-t border-x border-polkadot-800 right-0 w-full sm:w-96 h-96 bg-polkadot-900 rounded-t-xl shadow-xl focus:outline-none">
|
||||
<div className="flex justify-between items-center p-4 border-b border-polkadot-800">
|
||||
<Dialog.Title className="text-lg font-semibold">
|
||||
Transactions
|
||||
</Dialog.Title>
|
||||
<Dialog.Close asChild>
|
||||
<button className="text-gray-400 hover:text-gray-600">
|
||||
<X size={24} />
|
||||
</button>
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
<div className="p-4 h-[calc(100%-4rem)] overflow-y-auto">
|
||||
{transactionList$}
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { CopyText } from "@/components/Copy"
|
||||
import { shortStr } from "@/utils"
|
||||
import { TxBroadcastEvent } from "polkadot-api"
|
||||
import * as React from "react"
|
||||
import { Link } from "react-router-dom"
|
||||
import { onGoingEvents } from "./transactions.state"
|
||||
|
||||
export const Transaction: React.FC<
|
||||
TxBroadcastEvent | { type: "invalid" | "error"; value: any; txHash: string }
|
||||
> = (event) => {
|
||||
const getStatus = () => {
|
||||
const { type } = event
|
||||
switch (event.type) {
|
||||
case "error":
|
||||
return <span>There was an unexpected error.</span>
|
||||
case "invalid":
|
||||
return (
|
||||
<span>
|
||||
Invalid transaction. {JSON.stringify(event.value, null, 2)}
|
||||
</span>
|
||||
)
|
||||
case "txBestBlocksState": {
|
||||
return event.found ? (
|
||||
<span>
|
||||
Transaction in{" "}
|
||||
<Link
|
||||
className="underline font-bold"
|
||||
to={`/explorer/${event.block.hash}#tx=${event.block.index}`}
|
||||
>
|
||||
best block
|
||||
</Link>
|
||||
.
|
||||
</span>
|
||||
) : (
|
||||
<span>Transaction no longer in a best block.</span>
|
||||
)
|
||||
}
|
||||
case "signed":
|
||||
case "broadcasted":
|
||||
return <span>Transaction {type}.</span>
|
||||
default:
|
||||
return (
|
||||
<span>
|
||||
Transaction in{" "}
|
||||
<Link
|
||||
className="underline font-bold"
|
||||
to={`/explorer/${event.block.hash}#tx=${event.block.index}`}
|
||||
>
|
||||
{" "}
|
||||
finalized block
|
||||
</Link>
|
||||
.
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
||||
const { txHash, type } = event
|
||||
return (
|
||||
<div className="mb-4 p-3 bg-secondary/60 text-secondary-foreground/80 border border-border rounded-lg">
|
||||
<p className="font-medium">
|
||||
{shortStr(txHash, 14)} <CopyText text={txHash} />
|
||||
</p>
|
||||
<p
|
||||
className={`text-sm ${
|
||||
onGoingEvents.has(type)
|
||||
? "text-yellow-500"
|
||||
: type === "finalized"
|
||||
? "text-green-500"
|
||||
: "text-red-500"
|
||||
}`}
|
||||
>
|
||||
{getStatus()}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import * as Dialog from "@radix-ui/react-dialog"
|
||||
import { useStateObservable, withDefault } from "@react-rxjs/core"
|
||||
import { Loader2, X } from "lucide-react"
|
||||
import * as React from "react"
|
||||
import { map } from "rxjs"
|
||||
import { Transaction } from "./Transaction"
|
||||
import { grouppedTransactions$, onGoingEvents } from "./transactions.state"
|
||||
|
||||
const txsSummary$ = grouppedTransactions$.pipeState(
|
||||
map((x) => {
|
||||
const all = [...x.values()]
|
||||
const nOngoing = all.filter((x) => onGoingEvents.has(x.type)).length
|
||||
return { nTxs: all.length, nOngoing }
|
||||
}),
|
||||
withDefault({ nTxs: 0, nOngoing: 0 }),
|
||||
)
|
||||
|
||||
const transactionList$ = grouppedTransactions$.pipeState(
|
||||
map((x) =>
|
||||
[...x.values()].map((props) => (
|
||||
<Transaction key={props.txHash} {...props} />
|
||||
)),
|
||||
),
|
||||
withDefault([]),
|
||||
)
|
||||
|
||||
export function Transactions() {
|
||||
const [isOpen, setIsOpen] = React.useState(false)
|
||||
const { nTxs, nOngoing } = useStateObservable(txsSummary$)
|
||||
return !nTxs ? null : (
|
||||
<Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
|
||||
<Dialog.Trigger asChild>
|
||||
<button className="fixed bottom-4 right-4 bg-primary/90 text-primary-foreground px-4 py-2 rounded-full shadow-lg hover:bg-primary hover:font-bold focus:outline-none focus:ring-2 focus:ring-ring focus:ring-opacity-50 flex items-center space-x-2">
|
||||
{nOngoing > 0 && (
|
||||
<>
|
||||
<Loader2 className="animate-spin" size={20} />
|
||||
<span className="font-bold">{nOngoing}</span>
|
||||
</>
|
||||
)}
|
||||
<span>Transactions</span>
|
||||
</button>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="fixed inset-0 bg-black/50" />
|
||||
<Dialog.Content className="fixed bottom-0 border-t border-x border-border right-0 w-full sm:w-96 h-96 bg-card text-card-foreground rounded-t-xl shadow-xl focus:outline-none">
|
||||
<div className="flex justify-between items-center p-4 border-b border-border">
|
||||
<Dialog.Title className="text-lg font-semibold">
|
||||
Transactions
|
||||
</Dialog.Title>
|
||||
<Dialog.Close asChild>
|
||||
<button className="text-gray-400 hover:text-gray-600">
|
||||
<X size={24} />
|
||||
</button>
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
<div className="p-4 h-[calc(100%-4rem)] overflow-y-auto">
|
||||
{transactionList$}
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { grouppedTransactions$ } from "./transactions.state"
|
||||
|
||||
export * from "./Transactions"
|
||||
export { onNexTx } from "./transactions.state"
|
||||
export const transactions$ = grouppedTransactions$
|
||||
@@ -0,0 +1,58 @@
|
||||
import { chainClient$ } from "@/chain.state"
|
||||
import { state } from "@react-rxjs/core"
|
||||
import { combineKeys, createSignal, partitionByKey } from "@react-rxjs/utils"
|
||||
import { HexString, InvalidTxError, TxBroadcastEvent } from "polkadot-api"
|
||||
import {
|
||||
catchError,
|
||||
mergeMap,
|
||||
Observable,
|
||||
of,
|
||||
skip,
|
||||
takeUntil,
|
||||
tap,
|
||||
withLatestFrom,
|
||||
} from "rxjs"
|
||||
|
||||
const [signedTx$, onNexTx] = createSignal<HexString>()
|
||||
export { onNexTx }
|
||||
|
||||
const transactions$ = signedTx$.pipe(
|
||||
withLatestFrom(chainClient$),
|
||||
mergeMap(([tx, { client }]) => {
|
||||
const submitAndWatch = client.submitAndWatch as (
|
||||
tx: HexString,
|
||||
at: string,
|
||||
withSigned: boolean,
|
||||
) => Observable<TxBroadcastEvent>
|
||||
|
||||
let txHash: string = ""
|
||||
return submitAndWatch(tx, "finalized", true).pipe(
|
||||
tap((e) => {
|
||||
txHash = e.txHash
|
||||
}),
|
||||
catchError((err) =>
|
||||
of({
|
||||
type:
|
||||
err instanceof InvalidTxError
|
||||
? ("invalid" as const)
|
||||
: ("error" as const),
|
||||
txHash,
|
||||
value: err,
|
||||
}),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
const [tx$, txKeys$] = partitionByKey(
|
||||
transactions$,
|
||||
(x) => x.txHash,
|
||||
(x) => x.pipe(takeUntil(chainClient$.pipe(skip(1)))),
|
||||
)
|
||||
|
||||
export const grouppedTransactions$ = state(combineKeys(txKeys$, tx$))
|
||||
export const onGoingEvents = new Set([
|
||||
"signed",
|
||||
"broadcasted",
|
||||
"txBestBlocksState",
|
||||
])
|
||||
Reference in New Issue
Block a user