Skip to content
This repository has been archived by the owner on Jul 22, 2024. It is now read-only.

Commit

Permalink
feat: transaction notifications (#218)
Browse files Browse the repository at this point in the history
  • Loading branch information
statictype authored Jun 19, 2023
1 parent 22cf3d1 commit 4b7fc35
Show file tree
Hide file tree
Showing 10 changed files with 165 additions and 38 deletions.
15 changes: 15 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions www/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@
"react-router-dom": "^6.12.1",
"react-tooltip": "^5.13.2",
"tailwind-merge": "^1.13.1",
"uuid": "^9.0.0",
"vite-tsconfig-paths": "^4.2.0"
},
"devDependencies": {
"@babel/core": ">=7.12.10 <8.0.0",
"@preact/preset-vite": "^2.5.0",
"@tailwindcss/forms": "^0.5.3",
"@types/uuid": "^9.0.2",
"autoprefixer": "^10.4.14",
"clsx": "^1.2.1",
"postcss": "^8.4.24",
Expand Down
49 changes: 27 additions & 22 deletions www/src/components/Notifications.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Signal, signal } from "@preact/signals"
import { clsx } from "clsx"
import { useCallback, useEffect } from "preact/hooks"
import { useCallback, useId } from "preact/hooks"
import { IconClose } from "./icons/IconClose.js"
import { Spinner } from "./icons/Spinner.js"

export type Notification = {
id: string
type: "success" | "error"
message: string
type: "success" | "error" | "info" | "loading"
message: string | string[]
}

export type NotificationsState = {
Expand All @@ -20,8 +21,13 @@ export const notificationsState: NotificationsState = {
export function useNotifications() {
const { notifications } = notificationsState

const addNotification = (newNotification: Notification) => {
const addNotification = (newNotification: Notification, delayMs = 6000) => {
notifications.value = [...notifications.value, newNotification]
if (newNotification.type !== "loading") {
setTimeout(() => {
closeNotification(newNotification.id)
}, delayMs)
}
}

const closeNotification = (id: string) => {
Expand All @@ -40,29 +46,12 @@ export function useNotifications() {
}
}

function useBeforeUnload(callback: () => void) {
useEffect(() => {
const handleBeforeUnload = () => {
callback()
}
window.addEventListener("beforeunload", handleBeforeUnload)
return () => {
window.removeEventListener("beforeunload", handleBeforeUnload)
}
}, [callback])
}

export function Notifications() {
const {
notifications,
clearNotifications,
closeNotification,
} = useNotifications()

useBeforeUnload(() => {
clearNotifications()
})

return (
<div className="pointer-events-none fixed inset-0 mx-4 my-1 z-50">
<div className="flex w-full flex-col space-y-2 items-end">
Expand Down Expand Up @@ -99,10 +88,26 @@ function NotificationItem({ notification, onClose }: PropsNotificationItem) {
{
"bg-notification-error": notification.type === "error",
},
{
"bg-notification-info": notification.type === "info",
},
{
"bg-notification-loading": notification.type === "loading",
},
)}
>
<div className="flex p-4 justify-between">
<span>{notification.message}</span>
<div className="flex justify-start">
{notification.type === "loading" && <Spinner />}
<div>
{Array.isArray(notification.message)
? notification.message.map((message) => (
<p id={useId()}>{message}</p>
))
: <p>{notification.message}</p>}
</div>
</div>

<button
type="button"
className="focus:outline-none hover:opacity-60"
Expand Down
13 changes: 10 additions & 3 deletions www/src/components/Setup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useProposals } from "../hooks/useProposals.js"
import { accounts, defaultAccount, defaultSender } from "../signals/accounts.js"
import { formatBalance } from "../util/balance.js"
import { toMultiAddressIdRune, toMultisigRune } from "../util/capi-helpers.js"
import { filterEvents, handleException } from "../util/events.js"
import { AccountId } from "./AccountId.js"
import { Button } from "./Button.js"
import { CenteredCard } from "./CenteredCard.js"
Expand Down Expand Up @@ -42,7 +43,9 @@ export function Setup({ setup }: Props) {
.signed(signature({ sender }))
.sent()
.dbgStatus("Ratify")
.finalized()
.inBlockEvents()
.unhandleFailed()
.pipe(filterEvents)

return ratifyCall.run()
},
Expand All @@ -67,15 +70,19 @@ export function Setup({ setup }: Props) {
.signed(signature({ sender }))
.sent()
.dbgStatus("Cancel")
.finalized()
.inBlockEvents()
.unhandleFailed()
.pipe(filterEvents)

return cancelCall.run()
},
onSuccess: (result) => {
console.log({ result })
refetchProposals()
},
onError: (error) => console.error(error),
onError: (error: any) => {
handleException(error)
},
})

return (
Expand Down
24 changes: 24 additions & 0 deletions www/src/components/icons/Spinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export const Spinner = () => (
<svg
class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
>
</circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
>
</path>
</svg>
)
12 changes: 8 additions & 4 deletions www/src/components/wizards/multisig/Fund.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { zodResolver } from "@hookform/resolvers/zod/dist/zod.js"
import { signature } from "capi/patterns/signature/polkadot"
import { Controller, useForm } from "react-hook-form"
import { defaultSender } from "../../../signals/accounts.js"
import { toBalance } from "../../../util/balance.js"
import { toMultiAddressIdRune } from "../../../util/capi-helpers.js"
import { filterEvents, handleException } from "../../../util/events.js"
import { BalanceInput } from "../../BalanceInput.js"
import { Button } from "../../Button.js"
import { goNext } from "../Wizard.js"
Expand Down Expand Up @@ -34,19 +36,21 @@ export function MultisigFund() {

const fundStashCall = westend.Balances
.transfer({
value: BigInt(fundingAmount), // TODO properly scale the amount
value: toBalance(fundingAmount),
dest: toMultiAddressIdRune(stash),
})
.signed(signature({ sender }))
.sent()
.dbgStatus("Transfer:")
.finalized()
.inBlockEvents()
.unhandleFailed()
.pipe(filterEvents)

await fundStashCall.run()
updateWizardData({ ...formDataNew })
goNext()
} catch (exception) {
console.error("Something went wrong:", exception)
} catch (exception: any) {
handleException(exception)
}
}

Expand Down
18 changes: 11 additions & 7 deletions www/src/components/wizards/multisig/Members.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ import { MultiAddress, Westend, westend } from "@capi/westend"
import { zodResolver } from "@hookform/resolvers/zod/dist/zod.js"
import { Rune, ss58 } from "capi"
import { MultisigRune } from "capi/patterns/multisig"
import {
filterPureCreatedEvents,
replaceDelegateCalls,
} from "capi/patterns/proxy"
import { replaceDelegateCalls } from "capi/patterns/proxy"
import { signature } from "capi/patterns/signature/polkadot"
import { Controller, useForm } from "react-hook-form"
import {
Expand All @@ -19,6 +16,11 @@ import {
PROXY_DEPOSIT_BASE,
PROXY_DEPOSIT_FACTOR,
} from "../../../util/chain-constants.js"
import {
filterEvents,
filterPureCreatedEvents,
handleException,
} from "../../../util/events.js"
import { storeSetup } from "../../../util/local-storage.js"
import { AccountSelect } from "../../AccountSelect.js"
import { Button } from "../../Button.js"
Expand Down Expand Up @@ -95,7 +97,7 @@ export function MultisigMembers() {
.signed(signature({ sender: defaultSender.value }))
.sent()
.dbgStatus("Creating Pure Proxy:")
.finalizedEvents()
.inBlockEvents()
.unhandleFailed()
.pipe(filterPureCreatedEvents)
// TODO typing is broken in capi
Expand Down Expand Up @@ -124,7 +126,9 @@ export function MultisigMembers() {
.signed(signature({ sender: defaultSender.value }))
.sent()
.dbgStatus("Replacing Proxy Delegates:")
.finalized()
.inBlockEvents()
.unhandleFailed()
.pipe(filterEvents)

await replaceDelegates.run()

Expand All @@ -149,7 +153,7 @@ export function MultisigMembers() {

goNext()
} catch (exception) {
console.error("Something went wrong:", exception)
handleException(exception)
}
}

Expand Down
7 changes: 5 additions & 2 deletions www/src/components/wizards/transaction/Sign.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import { useState } from "preact/hooks"
import { useNavigate } from "react-router-dom"
import { toBalance } from "../../../util/balance.js"
import { filterEvents, handleException } from "../../../util/events.js"
import { storeCall } from "../../../util/local-storage.js"
import { AccountId } from "../../AccountId.js"
import { Button } from "../../Button.js"
Expand Down Expand Up @@ -47,7 +48,9 @@ export function TransactionSign() {
.signed(signature({ sender }))
.sent()
.dbgStatus("Ratify")
.finalized()
.inBlockEvents()
.unhandleFailed()
.pipe(filterEvents)

ratifyCall
.run()
Expand All @@ -57,7 +60,7 @@ export function TransactionSign() {
navigate("/")
})
.catch((exception) => {
console.error(exception)
handleException(exception)
setSubmitting(false)
})
}
Expand Down
61 changes: 61 additions & 0 deletions www/src/util/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { PalletProxyEvent, RuntimeEvent } from "@capi/westend"
import { Rune, RunicArgs } from "capi"
import { v4 as uuid } from "uuid"
import { useNotifications } from "../components/Notifications.js"

const { addNotification, closeNotification } = useNotifications()

const initialId = uuid()

function filterPureCreatedEvents<X>(...[events]: RunicArgs<X, [any[]]>) {
addNotification({
id: initialId,
message: "Processing...",
type: "loading",
})
return Rune.resolve(events).map((events) => {
closeNotification(initialId)
addNotification({ id: uuid(), message: "InBlock", type: "success" })
const eventNames = events.map((e) =>
`${e.event.type}:${e.event.value.type}`
)
addNotification({ id: uuid(), message: eventNames, type: "info" })

return events
.map((e) => e.event).filter((event): event is RuntimeEvent.Proxy =>
event.type === "Proxy"
)
.map((e) => e.value)
.filter((event): event is PalletProxyEvent.PureCreated =>
event.type === "PureCreated"
)
})
}

function filterEvents<X>(...[events]: RunicArgs<X, [any[]]>) {
addNotification({
id: initialId,
message: "Processing...",
type: "loading",
})
return Rune.resolve(events).map((events) => {
closeNotification(initialId)
addNotification({ id: uuid(), message: "InBlock", type: "success" })
const eventNames = events.map((e) =>
`${e.event.type}:${e.event.value.type}`
)
addNotification({ id: uuid(), message: eventNames, type: "info" })
})
}

function handleException(exception: any) {
closeNotification(initialId)
console.error("Something went wrong:", exception)
addNotification({
id: uuid(),
message: `${exception.value.name}:${exception.value.message}`,
type: "error",
})
}

export { filterEvents, filterPureCreatedEvents, handleException }
Loading

0 comments on commit 4b7fc35

Please sign in to comment.