withdraw_unbonded added and warning message for validators

Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
This commit is contained in:
Uncle Fatso 2025-08-25 18:50:41 +03:00
parent 9ab91c801d
commit 0d2c700e1a
Signed by: f4ts0
GPG Key ID: 565F4F2860226EBB
5 changed files with 134 additions and 6 deletions

View File

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

View File

@ -24,11 +24,13 @@ import {
useNominations,
useLedger,
usePayee,
useSlasingSpans,
useEraRewardPoints,
useCurrentValidators,
useValidatorsOverview,
useBondedAddress,
useNominateCalldata,
useWithdrawCalldata,
useSystemAccount,
useBondCalldata,
useUnbondCalldata,
@ -205,6 +207,7 @@ export const Nominations = () => {
const nominations = useNominations({ address: account?.address })
const ledger = useLedger({ address: account?.address })
const payee = usePayee({ address: account?.address })
const slashingSpans = useSlasingSpans({ address: account?.address })
const eraRewardPoints = useEraRewardPoints({ eraIndex: eraIndex?.index })
const currentValidators = useCurrentValidators({ address: interestingValidator })
const validatorOverview = useValidatorsOverview({ eraIndex: eraIndex?.index, address: interestingValidator })
@ -228,6 +231,7 @@ export const Nominations = () => {
const bondedAddress = useBondedAddress({ address: account?.address })
const withdrawCalldata = useWithdrawCalldata({ numberOfSpans: slashingSpans?.length ?? 0 })
const nominateCalldata = useNominateCalldata(checkedAddresses)
const bondCalldata = useBondCalldata(
convertedAmount > 0n ? convertedAmount : undefined
@ -274,7 +278,7 @@ export const Nominations = () => {
}, [ledger, eraIndex])
const latestWithdrawEra = useMemo(() => {
return Math.max(ledger?.unlocking.map((el: Unlocking) => el.era - (eraIndex?.index ?? 0)) ?? [])
return Math.max(ledger?.unlocking.map((el: Unlocking) => el.era - (eraIndex?.index ?? 0)), [])
}, [eraIndex, ledger])
useEffect(() => {
@ -449,6 +453,55 @@ export const Nominations = () => {
}
}, [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])
return (
<div className="sm:w-[500px] w-[85%] h-fit flex flex-col flex-1 gap-8 justify-center self-center rounded py-2">
<div className="bg-muted p-4 rounded flex flex-col gap-4">
@ -509,18 +562,18 @@ export const Nominations = () => {
className="sm:w-[300px] w-full"
placeholder={applyDecimals(readyToWithdraw, tokenDecimals, tokenSymbol)}
/>} />
<Row title={`After ${latestWithdrawEra} era${latestWithdrawEra > 1 ? "s" : ""}`} element={<Input
{latestWithdrawEra > 0 && (<Row title={`After ${latestWithdrawEra} era${latestWithdrawEra > 1 ? "s" : ""}`} element={<Input
readOnly
aria-label="Pending"
type="text"
className="sm:w-[300px] w-full"
placeholder={applyDecimals(waitingForWithdraw, tokenDecimals, tokenSymbol)}
/>} />
/>} />)}
<Button
type="button"
variant="secondary"
className="text-sm my-4 w-full"
onClick={() => alert("Not implemented yet")}
onClick={handleOnWithdraw}
disabled={isSubmittingTransaction || readyToWithdraw === 0n}
>
<PiggyBank className="w-4 h-4 inline-block mr-2" />
@ -547,7 +600,7 @@ export const Nominations = () => {
variant="secondary"
className="text-sm p-4 w-full"
onClick={handleOnNominate}
disabled={isSubmittingTransaction || convertedAmount === 0n}
disabled={isSubmittingTransaction || checkedAddresses.length === 0}
>
<Users className="w-4 h-4 inline-block mr-2" />
Nominate
@ -576,6 +629,11 @@ export const Nominations = () => {
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">

View File

@ -16,3 +16,4 @@ export * from "./useBondedAddress"
export * from "./useNominations"
export * from "./useLedger"
export * from "./usePayee"
export * from "./useSlasingSpans"

View File

@ -117,3 +117,25 @@ export const useNominateCalldata = (addresses: string[]) => {
)
return calldata
}
export const useWithdrawCalldata = (numberOfSpans: number) => {
const { client, chainId } = useUnstableProvider()
const metadata = useMetadata()
const { data: calldata } = useSWR(
client && chainId && numberOfSpans && metadata
? ["withdraw_unbonded", client, chainId, metadata, numberOfSpans]
: null,
([_, client, _chainId, metadata, numberOfSpans]) => {
const builder = getDynamicBuilder(getLookupFn(metadata))
const { codec, location } = builder.buildCall("Staking", "withdraw_unbonded")
return toHex(
mergeUint8(
new Uint8Array(location),
codec.enc({ num_slashing_spans: numberOfSpans }),
),
)
}
)
return calldata
}

View File

@ -0,0 +1,47 @@
import useSWRSubscription from "swr/subscription"
import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders"
import type { BlockInfo } from "@polkadot-api/observable-client"
import { distinct, filter, map, mergeMap } from "rxjs"
import { useUnstableProvider } from "./useUnstableProvider"
import { useMetadata } from "./useMetadata"
export type Payee = {
type: string
value: string
}
export const useSlasingSpans = ({ address }: { address: string | undefined }) => {
const { chainHead$, chainId } = useUnstableProvider()
const metadata = useMetadata()
const { data: slasingSpans } = useSWRSubscription(
chainHead$ && address && chainId && metadata
? ["slasingSpans", chainHead$, address, chainId, metadata]
: null,
([_, chainHead$, address, chainId, metadata], { next }) => {
const { finalized$, storage$ } = chainHead$
const subscription = finalized$.pipe(
filter(Boolean),
mergeMap((blockInfo: BlockInfo) => {
const builder = getDynamicBuilder(getLookupFn(metadata))
const slasingSpans = builder.buildStorage("Staking", "SlashingSpans")
return storage$(blockInfo?.hash, "value", () =>
slasingSpans?.keys.enc(address)
).pipe(
filter(Boolean),
distinct(),
map((value: string) => slasingSpans?.value.dec(value))
)
}),
)
.subscribe({
next(slasingSpans) {
next(null, slasingSpans)
},
error: next,
})
return () => subscription.unsubscribe()
}
)
return slasingSpans
}