move transaction handler to context and add ability to change payee

Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
This commit is contained in:
Uncle Fatso 2025-08-26 16:48:59 +03:00
parent 7822556988
commit b5ab8a6b81
Signed by: f4ts0
GPG Key ID: 565F4F2860226EBB
10 changed files with 389 additions and 432 deletions

View File

@ -1,6 +1,6 @@
{
"name": "ghost-lite",
"version": "0.1.6",
"version": "0.1.7",
"description": "Web application for Ghost and Casper chain.",
"author": "Uncle f4ts0 <f4ts0@ghostchain.io>",
"maintainers": [

View File

@ -1 +0,0 @@
export * from "./submitTransaction$"

View File

@ -1,7 +0,0 @@
import { map } from "rxjs"
export const submitTransaction$ = (clientFull: any, tx: string) => {
return clientFull?.submitAndWatch(tx).pipe(
map((txEvent) => ({ tx, txEvent }))
)
}

View File

@ -2,7 +2,11 @@ import { lazy, Suspense } from "react"
import { HashRouter, Routes, Route, Navigate } from "react-router-dom"
import { Layout, Sidebar, Header } from "../components"
import { UnstableProviderProvider, MetadataProviderProvider } from "../hooks"
import {
UnstableProviderProvider,
MetadataProviderProvider,
TransactionStatusProviderProvider,
} from "../hooks"
import { DEFAULT_CHAIN_ID } from "../settings"
const HealthCheck = lazy(() => import("./HealthCheck").then(module => ({ default: module.HealthCheck })))
@ -16,6 +20,7 @@ export const App = () => {
<Suspense fallback={<div></div>}>
<UnstableProviderProvider defaultChainId={DEFAULT_CHAIN_ID}>
<MetadataProviderProvider>
<TransactionStatusProviderProvider>
<Layout>
<Sidebar />
<div style={{ maxWidth: "calc(100% - 100px)"}} className="w-full h-full flex flex-col sm:px-6 px-2 sm:py-8 py-2">
@ -29,6 +34,7 @@ export const App = () => {
</Routes>
</div>
</Layout>
</TransactionStatusProviderProvider>
</MetadataProviderProvider>
</UnstableProviderProvider>
</Suspense>

View File

@ -1,11 +1,6 @@
import React, { useEffect, useState, useMemo, useCallback } from "react"
import React, { useEffect, useState, useMemo } from "react"
import { Nut, NutOff, Users, PiggyBank, RefreshCcw } from "lucide-react"
import { ss58Decode } from "@polkadot-labs/hdkd-helpers"
import { toHex } from "@polkadot-api/utils"
import { lastValueFrom, tap } from "rxjs"
import { submitTransaction$ } from "../api"
import {
Select,
@ -27,7 +22,6 @@ import { Button } from "../components/ui/button"
import {
useChainSpecV1,
useMetadata,
useEraIndex,
useNominations,
useLedger,
@ -39,21 +33,19 @@ import {
useBondedAddress,
useNominateCalldata,
useWithdrawCalldata,
usePayeeCalldata,
useSystemAccount,
useBondCalldata,
useUnbondCalldata,
useBondExtraCalldata,
useUnstableProvider,
useTransactionStatusProvider,
RewardPoints,
Unlocking,
RewardDestination,
} from "../hooks"
import {
AddressBookRecord,
FollowTransaction,
TransactionError,
} from "../types"
import { AddressBookRecord } from "../types"
import { Sender } from "./Accounts"
import { Row } from "./Row"
@ -90,7 +82,7 @@ const Item: React.FC<ItemProps> = (props) => {
setIsCheckedAddresses,
} = props
const [copied, setCopied] = useState<boolean>(false);
const [copied, setCopied] = useState<boolean>(false)
const convertToFixed = (value: bigint, decimals: number) => {
if (!value || !decimals) {
return parseFloat("0").toFixed(5)
@ -102,7 +94,7 @@ const Item: React.FC<ItemProps> = (props) => {
const handleCopy = (textToCopy: string) => {
if (!textToCopy) return
navigator.clipboard.writeText(textToCopy).then(() => {
setCopied(true);
setCopied(true)
setTimeout(() => setCopied(false), 2000)
})
}
@ -191,27 +183,22 @@ const HeaderInfo = ({ text, value }: { text: string, value: string }) => {
export const Nominations = () => {
const addressBook: AddressBookRecord[] = JSON.parse(localStorage.getItem('addressBook') ?? '[]')
const [checkedAddresses, setIsCheckedAddresses] = useState<string[]>([])
const [isSubmittingTransaction, setIsSubmittingTransaction] = useState<boolean>(false)
const [transactionStatus, setTransactionStatus] = useState<FollowTransaction | undefined>()
const [error, setError] = useState<TransactionError | undefined>()
const [interestingValidator, setInterestingValidator] = useState<string | undefined>(undefined)
const [amount, setAmount] = useState<string>("")
const [destinationReceiver, setDestinationReceiver] = useState<string>("")
const [expectedPayee, setExpectedPayee] = useState<string | undefined>(undefined)
const [expectedPayee, setExpectedPayee] = useState<string>("")
const {
provider,
clientFull,
chainId,
account,
accounts,
connectAccount
} = useUnstableProvider()
setTransactionStatus,
setError,
isSubmittingTransaction,
handleTransaction,
renderStatus,
} = useTransactionStatusProvider()
const { account, accounts, connectAccount } = useUnstableProvider()
const metadata = useMetadata()
const chainSpecV1 = useChainSpecV1()
const eraIndex = useEraIndex()
const nominations = useNominations({ address: account?.address })
@ -224,6 +211,7 @@ export const Nominations = () => {
const tokenDecimals: number = chainSpecV1?.properties?.tokenDecimals ?? 0
const tokenSymbol: string = chainSpecV1?.properties?.tokenSymbol ?? ""
const ss58Format: number = chainSpecV1?.properties?.ss58Format ?? 1995
const senderAccount = useSystemAccount({
account: account
@ -234,15 +222,14 @@ export const Nominations = () => {
const destinationReceiverIsValid = useMemo(() => {
try {
const [, prefix] = ss58Decode(destinationReceiver)
if (prefix !== 1995 && prefix !== 1996) {
if (prefix !== ss58Format) {
throw new Error("bad prefix")
}
return true
} catch {
return false
}
}, [destinationReceiver])
}, [destinationReceiver, ss58Format])
const convertedAmount = useMemo(() => {
try {
@ -254,8 +241,12 @@ export const Nominations = () => {
const bondedAddress = useBondedAddress({ address: account?.address })
const withdrawCalldata = useWithdrawCalldata({ numberOfSpans: slashingSpans?.length ?? 0 })
const payeeCalldata = usePayeeCalldata(expectedPayee, destinationReceiver)
const withdrawCalldata = useWithdrawCalldata(slashingSpans?.prior?.length ?? 0)
const nominateCalldata = useNominateCalldata(checkedAddresses)
const bondExtraCalldata = useBondExtraCalldata(
convertedAmount > 0n ? convertedAmount : undefined
)
const bondCalldata = useBondCalldata(
convertedAmount > 0n ? convertedAmount : undefined
)
@ -268,16 +259,16 @@ export const Nominations = () => {
switch (destination) {
case "Staked":
description = "Re-stake upcoming rewards"
break;
break
case "Stash":
description = "Withdraw rewards to free"
break;
break
case "Account":
description = `Rewards to account`
break;
break
case "None":
description = "Refuse to receive rewards"
break;
break
}
return description
}
@ -301,7 +292,11 @@ export const Nominations = () => {
}, [ledger, eraIndex])
const latestWithdrawEra = useMemo(() => {
return Math.max(ledger?.unlocking.map((el: Unlocking) => el.era - (eraIndex?.index ?? 0)), [])
if (!ledger || ledger.unlocking.length === 0) {
return 0
}
const index = eraIndex?.index ?? 0
return Math.max(...ledger.unlocking.map((el: Unlocking) => el.era - index), 0)
}, [eraIndex, ledger])
useEffect(() => {
@ -309,7 +304,7 @@ export const Nominations = () => {
}, [account])
useEffect(() => {
if (!expectedPayee) setExpectedPayee(payee?.type)
if (expectedPayee === "") setExpectedPayee(payee?.type ?? "")
}, [expectedPayee, setExpectedPayee, payee])
useEffect(() => {
@ -319,9 +314,21 @@ export const Nominations = () => {
}, [checkedAddresses, nominations])
useEffect(() => {
if (amount !== "") {
setError(undefined)
setTransactionStatus(undefined)
}, [amount])
}
}, [amount, setError, setTransactionStatus])
useEffect(() => {
if (!isSubmittingTransaction) {
setAmount("")
}
}, [isSubmittingTransaction])
useEffect(() => {
setDestinationReceiver("")
}, [expectedPayee])
const applyDecimals = (value = 0n, decimals = 0, tokenSymbol = "CSPR") => {
if (!value) return `0 ${tokenSymbol}`
@ -333,201 +340,12 @@ export const Nominations = () => {
return `${formatter.format(numberValue)} ${tokenSymbol}`
}
const handleOnUnbond = useCallback(async () => {
setIsSubmittingTransaction(true)
setTransactionStatus(undefined)
setError(undefined)
try {
const tx = await provider!.createTx(
chainId ?? "",
account ? toHex(ss58Decode(account.address)[0]) : "",
unbondCalldata ?? ""
)
await lastValueFrom(
submitTransaction$(clientFull, tx)
.pipe(
tap(({ txEvent }) => {
let status: string = ""
switch (txEvent.type) {
case "broadcasted":
status = "broadcasted to available peers"
break
case "txBestBlocksState":
status = `included in block #${txEvent.block.number}`
break
case "finalized":
status = `finalized at block #${txEvent.block.number}`
break
case "throttled":
status = "throttling to detect chain head..."
break
}
setTransactionStatus({
status,
hash: txEvent.block?.hash,
})
}),
),
)
} catch (err) {
if (err instanceof Error) {
const currentError = { type: "error", error: err.message }
setError(currentError)
}
console.error(err)
} finally {
setAmount("")
setIsSubmittingTransaction(false)
}
}, [account, unbondCalldata, chainId, clientFull, provider])
const handleOnBond = useCallback(async () => {
setIsSubmittingTransaction(true)
setTransactionStatus(undefined)
setError(undefined)
try {
const tx = await provider!.createTx(
chainId ?? "",
account ? toHex(ss58Decode(account.address)[0]) : "",
bondCalldata ?? ""
)
await lastValueFrom(
submitTransaction$(clientFull, tx)
.pipe(
tap(({ txEvent }) => {
let status: string = ""
switch (txEvent.type) {
case "broadcasted":
status = "broadcasted to available peers"
break
case "txBestBlocksState":
status = `included in block #${txEvent.block.number}`
break
case "finalized":
status = `finalized at block #${txEvent.block.number}`
break
case "throttled":
status = "throttling to detect chain head..."
break
}
setTransactionStatus({
status,
hash: txEvent.block?.hash,
})
}),
),
)
} catch (err) {
if (err instanceof Error) {
const currentError = { type: "error", error: err.message }
setError(currentError)
}
console.error(err)
} finally {
setAmount("")
setIsSubmittingTransaction(false)
}
}, [account, bondCalldata, chainId, clientFull, provider])
const handleOnNominate = useCallback(async () => {
setIsSubmittingTransaction(true)
setTransactionStatus(undefined)
setError(undefined)
try {
const tx = await provider!.createTx(
chainId ?? "",
account ? toHex(ss58Decode(account.address)[0]) : "",
nominateCalldata ?? ""
)
await lastValueFrom(
submitTransaction$(clientFull, tx)
.pipe(
tap(({ txEvent }) => {
let status: string = ""
switch (txEvent.type) {
case "broadcasted":
status = "broadcasted to available peers"
break
case "txBestBlocksState":
status = `included in block #${txEvent.block.number}`
break
case "finalized":
status = `finalized at block #${txEvent.block.number}`
break
case "throttled":
status = "throttling to detect chain head..."
break
}
setTransactionStatus({
status,
hash: txEvent.block?.hash,
})
}),
),
)
} catch (err) {
if (err instanceof Error) {
const currentError = { type: "error", error: err.message }
setError(currentError)
}
console.error(err)
} finally {
setAmount("")
setIsSubmittingTransaction(false)
}
}, [account, chainId, clientFull, nominateCalldata, provider])
const handleOnWithdraw = useCallback(async () => {
setIsSubmittingTransaction(true)
setTransactionStatus(undefined)
setError(undefined)
try {
const tx = await provider!.createTx(
chainId ?? "",
account ? toHex(ss58Decode(account.address)[0]) : "",
withdrawCalldata ?? ""
)
await lastValueFrom(
submitTransaction$(clientFull, tx)
.pipe(
tap(({ txEvent }) => {
let status: string = ""
switch (txEvent.type) {
case "broadcasted":
status = "broadcasted to available peers"
break
case "txBestBlocksState":
status = `included in block #${txEvent.block.number}`
break
case "finalized":
status = `finalized at block #${txEvent.block.number}`
break
case "throttled":
status = "throttling to detect chain head..."
break
}
setTransactionStatus({
status,
hash: txEvent.block?.hash,
})
}),
),
)
} catch (err) {
if (err instanceof Error) {
const currentError = { type: "error", error: err.message }
setError(currentError)
}
console.error(err)
} finally {
setAmount("")
setIsSubmittingTransaction(false)
}
}, [account, chainId, clientFull, withdrawCalldata, provider])
const handleOnBond = () => handleTransaction({ calldata: bondCalldata, txName: "bond" })
const handleOnExtraBond = () => handleTransaction({ calldata: bondExtraCalldata, txName: "bond_extra" })
const handleOnUnbond = () => handleTransaction({ calldata: unbondCalldata, txName: "unbond" })
const handleOnNominate = () => handleTransaction({ calldata: nominateCalldata, txName: "nominate" })
const handleOnWithdraw = () => handleTransaction({ calldata: withdrawCalldata, txName: "withdraw" })
const handleOnSetPayee = () => handleTransaction({ calldata: payeeCalldata, txName: "set_payee" })
return (
<div className="sm:w-[500px] w-[85%] h-fit flex flex-col flex-1 gap-8 justify-center self-center rounded py-2">
@ -583,6 +401,15 @@ export const Nominations = () => {
</SelectContent>
</Select>
} />)}
{payee?.type === "Account" && (
<Row title="" element={<Input
readOnly
aria-label="Destination Account Address"
type="text"
className="sm:w-[300px] w-full"
placeholder={payee?.value}
/>} />
)}
{expectedPayee !== payee?.type && expectedPayee === "Account" && (
<Row title="Receiver" element={<Input
aria-label="Destination Account"
@ -598,8 +425,12 @@ export const Nominations = () => {
type="button"
variant="secondary"
className="text-sm my-4 w-full"
onClick={() => alert("not ready yet")}
disabled={isSubmittingTransaction || !destinationReceiverIsValid}
onClick={handleOnSetPayee}
disabled={
isSubmittingTransaction
|| !payeeCalldata
|| (expectedPayee === "Account" && !destinationReceiverIsValid)
}
>
<RefreshCcw className="w-4 h-4 inline-block mr-2" />
Change Destination
@ -661,6 +492,28 @@ export const Nominations = () => {
placeholder="Input amount to bond or unbond"
/>
<div className="flex justify-between gap-2">
<Button
type="button"
variant="secondary"
className="text-sm p-4 w-full"
onClick={bondedAddress ? handleOnExtraBond : handleOnBond}
disabled={isSubmittingTransaction || convertedAmount === 0n}
>
<Nut className="w-4 h-4 inline-block mr-2" />
Bond
</Button>
<Button
type="button"
variant="secondary"
className="text-sm p-4 w-full"
onClick={handleOnUnbond}
disabled={isSubmittingTransaction || convertedAmount === 0n}
>
<NutOff className="w-4 h-4 inline-block mr-2" />
Unbond
</Button>
</div>
<div>
{bondedAddress && (
<Button
type="button"
@ -673,55 +526,13 @@ export const Nominations = () => {
Nominate
</Button>
)}
{!bondedAddress && (
<Button
type="button"
variant="secondary"
className="text-sm p-4 w-full"
onClick={handleOnBond}
disabled={isSubmittingTransaction || convertedAmount === 0n}
>
<Nut className="w-4 h-4 inline-block mr-2" />
Bond
</Button>
)}
<Button
type="button"
variant="secondary"
className="text-sm p-4 w-full"
onClick={handleOnUnbond}
disabled={isSubmittingTransaction || convertedAmount === 0n}
>
<NutOff className="w-4 h-4 inline-block mr-2" />
Unbond
</Button>
</div>
{eraRewardPoints?.individual.some((indivial: RewardPoints) => indivial?.at(0) === account?.address) && (
<p className="text-xs text-destructive">
You are attempting to use the nomination functionality from the current validator account, which is mutually exclusive. Please switch accounts or proceed at your own risk.
</p>
)}
{!error && transactionStatus && (
<div className="flex flex-col">
<p className="text-xs text-accent overflow-hidden whitespace-nowrap text-ellipsis">
Transaction status: {`${transactionStatus.status}`}
</p>
{transactionStatus.hash && (<p className="text-xs text-accent overflow-hidden whitespace-nowrap text-ellipsis">
{transactionStatus.hash}
</p>)}
</div>
)}
{!error && !metadata && (
<p className="text-xs text-accent overflow-hidden whitespace-nowrap text-ellipsis">
Downloading chain metadata...
</p>
)}
{error && (
<p className="text-xs text-destructive overflow-hidden whitespace-nowrap text-ellipsis">
Error: {error.error}
</p>
)}
{renderStatus()}
</div>
{eraRewardPoints && (
<Accordion collapsible onValueChange={setInterestingValidator} type="single" className="sm:w-[500px] w-[85%] h-fit flex flex-col flex-1 gap-4 justify-center self-center sm:text-base text-xs">

View File

@ -6,29 +6,20 @@ import {
ArrowBigRightDash,
ArrowBigDownDash
} from "lucide-react"
import React, { useState, useEffect, useCallback, useMemo } from "react"
import React, { useState, useEffect, useMemo } from "react"
import { useLocation } from "react-router-dom"
import { lastValueFrom, tap } from "rxjs"
import { ss58Decode } from "@polkadot-labs/hdkd-helpers"
import { toHex } from "@polkadot-api/utils"
import {
useChainSpecV1,
useSystemAccount,
useUnstableProvider,
useTransactionStatusProvider,
useMetadata,
useTransferCalldata,
useExistentialDeposit
} from "../hooks"
import { submitTransaction$ } from "../api"
import {
FollowTransaction,
TransactionError,
TransactionHistory,
} from "../types"
import {
Accordion,
AccordionContent,
@ -39,6 +30,7 @@ import { Input } from "../components/ui/input"
import { Button } from "../components/ui/button"
import { Sender, Receiver } from "./Accounts"
import { TransactionHistory } from "../types"
export const Transactions = () => {
const location = useLocation()
@ -58,19 +50,24 @@ export const Transactions = () => {
localStorage.getItem("defaultTransactAmount") ?? ""
)
const [transactionStatus, setTransactionStatus] = useState<FollowTransaction | undefined>()
const [error, setError] = useState<TransactionError | undefined>()
const [isSubmittingTransaction, setIsSubmittingTransaction] = useState<boolean>(false)
const [activeTab, onActiveTabChanged] = useState<string>("transact")
const [receiver, setReceiver] = useState<string>(initialReceiver)
const [amount, setAmount] = useState<string>(defaultTransactAmount)
const {
setTransactionStatus,
setError,
isSubmittingTransaction,
handleTransaction,
renderStatus,
} = useTransactionStatusProvider()
const metadata = useMetadata()
const existentialDeposit = useExistentialDeposit()
const chainSpecV1 = useChainSpecV1()
const tokenDecimals: number = chainSpecV1?.properties?.tokenDecimals ?? 0
const tokenSymbol: string = chainSpecV1?.properties?.tokenSymbol ?? ""
const ss58Format: number = chainSpecV1?.properties?.ss58Format ?? 1995
const convertedAmount = useMemo(() => {
try {
@ -81,22 +78,19 @@ export const Transactions = () => {
}, [amount, tokenDecimals])
const convertedTimestamp = ({ timestamp }: { timestamp: number }) => {
const secondsInMinute = 60;
const secondsInHour = secondsInMinute * 60;
const secondsInDay = secondsInHour * 24;
const secondsInMinute = 60
const secondsInHour = secondsInMinute * 60
const secondsInDay = secondsInHour * 24
const days = Math.floor(timestamp / secondsInDay);
const hours = Math.floor((timestamp % secondsInDay) / secondsInHour);
const minutes = Math.floor((timestamp % secondsInHour) / secondsInMinute);
const seconds = timestamp % secondsInMinute;
const days = Math.floor(timestamp / secondsInDay)
const hours = Math.floor((timestamp % secondsInDay) / secondsInHour)
const minutes = Math.floor((timestamp % secondsInHour) / secondsInMinute)
const seconds = timestamp % secondsInMinute
return `${days} days, ${hours} hours, ${minutes} minutes, ${seconds} seconds`;
return `${days} days, ${hours} hours, ${minutes} minutes, ${seconds} seconds`
}
const {
provider,
clientFull,
chainId,
account,
accounts,
connectAccount
@ -105,14 +99,14 @@ export const Transactions = () => {
const receiverObject = useMemo(() => {
try {
const [, prefix] = ss58Decode(receiver)
if (prefix !== 1995 && prefix !== 1996) {
if (prefix !== ss58Format) {
throw new Error("bad prefix")
}
return { isValid: true, address: receiver }
} catch (e) {
return { isValid: false, address: receiver }
}
}, [receiver])
}, [receiver, ss58Format])
const calldata = useTransferCalldata(
receiverObject.isValid ? receiverObject.address : undefined,
@ -160,85 +154,51 @@ export const Transactions = () => {
localStorage.setItem("defaultTransactAmount", defaultTransactAmount)
}, [defaultTransactAmount])
const handleOnTransfer = useCallback(async () => {
setIsSubmittingTransaction(true)
setTransactionStatus(undefined)
setError(undefined)
useEffect(() => {
if (!isSubmittingTransaction) {
setAmount("")
}
}, [isSubmittingTransaction])
const transactStory: TransactionHistory = {
useEffect(() => {
if (amount !== "") {
setError(undefined)
setTransactionStatus(undefined)
}
}, [amount, setError, setTransactionStatus])
const extraLogic = (txEvent: any) => {
const rawStatus = txEvent?.type.replace("txBestBlocksState", "mined") ?? "initiated"
const status = rawStatus.charAt(0).toUpperCase() + rawStatus.slice(1).toLowerCase()
const newTransaction: TransactionHistory = {
sender: account?.address ?? "",
receiver: receiverObject?.address ?? "",
amount,
timestamp: Math.floor(Date.now() / 1000),
calldata: calldata ?? "",
tokenSymbol: tokenSymbol ?? "",
status: "Initiated"
status,
blockHash: txEvent?.block?.hash ?? "",
txHash: txEvent?.txHash ?? "",
blockNumber: txEvent?.block?.number
}
try {
const tx = await provider!.createTx(
chainId ?? "",
account ? toHex(ss58Decode(account.address)[0]) : "",
calldata ?? ""
)
await lastValueFrom(
submitTransaction$(clientFull, tx)
.pipe(
tap(({ txEvent }) => {
let status: string = ""
switch (txEvent.type) {
case "broadcasted":
status = "broadcasted to available peers"
transactStory.status = "Broadcasted"
break
case "txBestBlocksState":
status = `included in block #${txEvent.block.number}`
transactStory.blockNumber = txEvent.block.number
transactStory.status = "Mined"
break
case "finalized":
status = `finalized at block #${txEvent.block.number}`
transactStory.blockNumber = txEvent.block.number
transactStory.status = "Finalized"
break
case "throttled":
status = "throttling to detect chain head..."
transactStory.status = "Throttled"
break
setTransactionHistory((prevHistory) => {
const existingIndex = prevHistory.findIndex(tx => tx.txHash === newTransaction.txHash)
if (existingIndex > -1) {
const updatedHistory = [...prevHistory]
updatedHistory[existingIndex] = {
...updatedHistory[existingIndex],
...newTransaction,
}
return updatedHistory
} else {
return [...prevHistory, newTransaction]
}
transactStory.txHash = txEvent.txHash
transactStory.blockHash = txEvent.block?.hash
setTransactionStatus({
status,
hash: txEvent.block?.hash,
})
}),
),
)
} catch (err) {
if (err instanceof Error) {
const currentError = { type: "error", error: err.message }
transactStory.error = currentError
setError(currentError)
}
console.error(err)
} finally {
setAmount(defaultTransactAmount ?? "")
setIsSubmittingTransaction(false)
setTransactionHistory([...transactionHistory, transactStory])
}
}, [
provider,
chainId,
account,
amount,
calldata,
clientFull,
defaultTransactAmount,
receiverObject?.address,
tokenSymbol,
transactionHistory
])
const handleOnTransfer = () => handleTransaction({ calldata, txName: "transfer", extraLogic })
return (
<div className="sm:w-[500px] w-[85%] h-fit flex flex-col flex-1 gap-2 justify-center self-center rounded py-2">
@ -304,8 +264,8 @@ export const Transactions = () => {
<Button
onClick={handleOnTransfer}
disabled={
isSubmittingTransaction || !provider || !senderAccount ||
!receiverObject.isValid || !tokenDecimals || !calldata ||
isSubmittingTransaction || !senderAccount || !calldata ||
!receiverObject.isValid || !tokenDecimals ||
!existentialDeposit || convertedAmount < existentialDeposit ||
!metadata || senderAccount.data.free < convertedAmount
}
@ -315,27 +275,7 @@ export const Transactions = () => {
>
{isSubmittingTransaction ? "Submitting" : "Transfer"}
</Button>
{!error && transactionStatus && (
<div className="flex flex-col">
<p className="text-xs text-accent overflow-hidden whitespace-nowrap text-ellipsis">
Transaction status: {`${transactionStatus.status}`}
</p>
{transactionStatus.hash && (<p className="text-xs text-accent overflow-hidden whitespace-nowrap text-ellipsis">
{transactionStatus.hash}
</p>)}
</div>
)}
{!error && !metadata && (
<p className="text-xs text-accent overflow-hidden whitespace-nowrap text-ellipsis">
Downloading chain metadata...
</p>
)}
{error && (
<p className="text-xs text-destructive overflow-hidden whitespace-nowrap text-ellipsis">
Error: {error.error}
</p>
)}
{renderStatus()}
</>
)}
{activeTab === "history" && (

View File

@ -1,6 +1,7 @@
export * from "./useSystemAccount"
export * from "./useUnstableProvider"
export * from "./useMetadata"
export * from "./useTransactionStatusProvider"
export * from "./useChains"
export * from "./useIsMounted"
export * from "./useChainSpecV1"

View File

@ -15,7 +15,7 @@ const AccountId = (value: SS58String) => Enum<
"Id"
>("Id", value)
export const RewardDestination = (account: SS58String | undefined) => {
export const RewardDestination = () => {
return {
Staked: () => Enum<
{
@ -36,7 +36,7 @@ export const RewardDestination = (account: SS58String | undefined) => {
Account: (account: SS58String) => Enum<
{
type: "Account"
value: account
value: SS58String
},
"Account"
>("Account", account),
@ -114,7 +114,7 @@ export const useBondCalldata = (amount: bigint | undefined) => {
new Uint8Array(location),
codec.enc({
value: amount,
payee: RewardDestination.Staked(),
payee: RewardDestination().Staked(),
}),
),
)
@ -123,6 +123,28 @@ export const useBondCalldata = (amount: bigint | undefined) => {
return calldata
}
export const useBondExtraCalldata = (amount: bigint | undefined) => {
const { client, chainId } = useUnstableProvider()
const metadata = useMetadata()
const { data: calldata } = useSWR(
client && chainId && amount && metadata
? ["bond_extra", client, chainId, metadata, amount]
: null,
([_, client, _chainId, metadata, amount]) => {
const builder = getDynamicBuilder(getLookupFn(metadata))
const { codec, location } = builder.buildCall("Staking", "bond_extra")
return toHex(
mergeUint8(
new Uint8Array(location),
codec.enc({ max_additional: amount }),
),
)
}
)
return calldata
}
export const useNominateCalldata = (addresses: string[]) => {
const { client, chainId } = useUnstableProvider()
const metadata = useMetadata()
@ -167,3 +189,41 @@ export const useWithdrawCalldata = (numberOfSpans: number) => {
)
return calldata
}
export const usePayeeCalldata = (expectedPayee: string | undefined, destinationReceiver: string) => {
const { client, chainId } = useUnstableProvider()
const metadata = useMetadata()
const { data: calldata } = useSWR(
client && chainId && expectedPayee && metadata
? ["withdraw_unbonded", client, chainId, metadata, expectedPayee]
: null,
([_, client, _chainId, metadata, expectedPayee]) => {
const builder = getDynamicBuilder(getLookupFn(metadata))
const { codec, location } = builder.buildCall("Staking", "set_payee")
let destination
switch (expectedPayee) {
case "Stash":
destination = RewardDestination().Stash()
break
case "Account":
destination = RewardDestination().Account(destinationReceiver)
break
case "None":
destination = RewardDestination().None()
break
default:
destination = RewardDestination().Staked()
break
}
return toHex(
mergeUint8(
new Uint8Array(location),
codec.enc({ payee: destination }),
),
)
}
)
return calldata
}

View File

@ -6,9 +6,11 @@ import { distinct, filter, map, mergeMap } from "rxjs"
import { useUnstableProvider } from "./useUnstableProvider"
import { useMetadata } from "./useMetadata"
export type Payee = {
type: string
value: string
export type SlashingSpans = {
span_index: number,
last_start: number,
last_nonzero_slash: number,
prior: number[],
}
export const useSlasingSpans = ({ address }: { address: string | undefined }) => {
@ -30,12 +32,12 @@ export const useSlasingSpans = ({ address }: { address: string | undefined }) =>
).pipe(
filter(Boolean),
distinct(),
map((value: string) => slasingSpans?.value.dec(value))
map((value: string) => slasingSpans?.value.dec(value) as SlashingSpans)
)
}),
)
.subscribe({
next(slasingSpans) {
next(slasingSpans: SlashingSpans) {
next(null, slasingSpans)
},
error: next,

View File

@ -0,0 +1,145 @@
import { type ReactNode, createContext, useContext, useState, useCallback } from "react"
import { toHex } from "@polkadot-api/utils"
import { ss58Decode } from "@polkadot-labs/hdkd-helpers"
import { lastValueFrom, tap, map } from "rxjs"
import { useUnstableProvider } from "./useUnstableProvider"
import { useMetadata } from "./useMetadata"
import { FollowTransaction, TransactionError } from "../types"
type HandleTransactionParams = {
calldata: string | undefined;
txName: string;
extraLogic?: (data: any) => void;
}
type Context = {
isSubmittingTransaction: boolean;
transactionStatus: FollowTransaction | undefined;
error: TransactionError | undefined;
setIsSubmittingTransaction: (isSubmitting: boolean) => void;
setTransactionStatus: (status: FollowTransaction | undefined) => void;
setError: (error: TransactionError | undefined) => void;
handleTransaction: ({ calldata, txName, extraLogic }: HandleTransactionParams) => Promise<void>;
renderStatus: () => JSX.Element;
}
const TransactionStatusProvider = createContext<Context>(null!)
export const useTransactionStatusProvider = () => useContext(TransactionStatusProvider)
export const TransactionStatusProviderProvider = ({ children }: { children: ReactNode }) => {
const [isSubmittingTransaction, setIsSubmittingTransaction] = useState<boolean>(false)
const [transactionStatus, setTransactionStatus] = useState<FollowTransaction | undefined>()
const [error, setError] = useState<TransactionError | undefined>()
const { provider, clientFull, chainId, account } = useUnstableProvider()
const metadata = useMetadata()
const handleTransaction = useCallback(async ({ calldata, txName, extraLogic }: HandleTransactionParams) => {
if (!calldata) {
throw new Error("Calldata is not recognizable");
}
if (!account) {
throw new Error("Account is not recognizable")
}
if (!clientFull) {
throw new Error("Connected client not found")
}
setIsSubmittingTransaction(true);
setTransactionStatus(undefined);
setError(undefined);
try {
const tx = await provider!.createTx(
chainId ?? "",
account ? toHex(ss58Decode(account?.address)[0]) : "",
calldata ?? ""
);
await lastValueFrom(
clientFull.submitAndWatch(tx)
.pipe(
map((txEvent) => ({ tx, txEvent })),
tap(({ txEvent }) => {
let status: string = "";
switch (txEvent.type) {
case "broadcasted":
status = `${txName} is broadcasted to available peers`;
break;
case "txBestBlocksState":
status = `${txName} included in block #${txEvent.block.number}`;
break;
case "finalized":
status = `${txName} finalized at block #${txEvent.block.number}`;
break;
case "throttled":
status = `${txName} throttling to detect chain head...`;
break;
}
setTransactionStatus({
status,
hash: txEvent.block?.hash,
});
extraLogic?.(txEvent)
}),
),
);
} catch (err) {
if (err instanceof Error) {
const currentError = { type: "error", error: err.message };
setError(currentError);
}
console.error(err);
} finally {
setIsSubmittingTransaction(false);
}
}, [account, chainId, clientFull, provider]);
const renderStatus = () => {
return (
<>
{!error && transactionStatus && (
<div className="flex flex-col">
<p className="text-xs text-accent overflow-hidden whitespace-nowrap text-ellipsis">
Transaction status: {`${transactionStatus.status}`}
</p>
{transactionStatus.hash && (<p className="text-xs text-accent overflow-hidden whitespace-nowrap text-ellipsis">
{transactionStatus.hash}
</p>)}
</div>
)}
{!error && !metadata && (
<p className="text-xs text-accent overflow-hidden whitespace-nowrap text-ellipsis">
Downloading chain metadata...
</p>
)}
{error && (
<p className="text-xs text-destructive overflow-hidden whitespace-nowrap text-ellipsis">
Error: {error.error}
</p>
)}
</>
)
}
return (
<TransactionStatusProvider.Provider
value={{
isSubmittingTransaction,
transactionStatus,
error,
setIsSubmittingTransaction,
setTransactionStatus,
setError,
handleTransaction,
renderStatus
}}
>
{children}
</TransactionStatusProvider.Provider>
)
}