ability to unbond with according text fields added

Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
This commit is contained in:
Uncle Fatso 2025-08-20 20:10:33 +03:00
parent 5b32bd5500
commit c24edbfe0d
Signed by: f4ts0
GPG Key ID: 565F4F2860226EBB
8 changed files with 342 additions and 57 deletions

View File

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

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState, useMemo, useCallback } from "react" import React, { useEffect, useState, useMemo, useCallback } from "react"
import { Nut, NutOff, Users } from "lucide-react" import { Nut, NutOff, Users, PiggyBank } from "lucide-react"
import { ss58Decode } from "@polkadot-labs/hdkd-helpers" import { ss58Decode } from "@polkadot-labs/hdkd-helpers"
import { toHex } from "@polkadot-api/utils" import { toHex } from "@polkadot-api/utils"
@ -22,6 +22,8 @@ import {
useMetadata, useMetadata,
useEraIndex, useEraIndex,
useNominations, useNominations,
useLedger,
usePayee,
useEraRewardPoints, useEraRewardPoints,
useCurrentValidators, useCurrentValidators,
useValidatorsOverview, useValidatorsOverview,
@ -29,6 +31,7 @@ import {
useNominateCalldata, useNominateCalldata,
useSystemAccount, useSystemAccount,
useBondCalldata, useBondCalldata,
useUnbondCalldata,
useUnstableProvider, useUnstableProvider,
RewardPoints, RewardPoints,
} from "../hooks" } from "../hooks"
@ -110,7 +113,9 @@ const Item: React.FC<ItemProps> = (props) => {
<div> <div>
<AccordionTrigger> <AccordionTrigger>
<div className="w-100 flex flex-row items-center justify-start gap-4 space-x-2 cursor-pointer"> <div className="w-100 flex flex-row items-center justify-start gap-4 space-x-2 cursor-pointer">
<div className={`w-[300px] overflow-hidden whitespace-nowrap text-ellipsis text-left ${nominated ? "text-foreground" : ""}`}>{name ?? address}</div> <div className={`w-[300px] overflow-hidden whitespace-nowrap text-ellipsis text-left ${nominated ? "text-foreground" : ""}`}>
{name ?? (address.slice(0, 15) + "..." + address.slice(-15))}
</div>
<div>{points}</div> <div>{points}</div>
</div> </div>
</AccordionTrigger> </AccordionTrigger>
@ -198,10 +203,12 @@ export const Nominations = () => {
const chainSpecV1 = useChainSpecV1() const chainSpecV1 = useChainSpecV1()
const eraIndex = useEraIndex() const eraIndex = useEraIndex()
const nominations = useNominations({ address: account?.address }) const nominations = useNominations({ address: account?.address })
const ledger = useLedger({ address: account?.address })
const payee = usePayee({ address: account?.address })
const eraRewardPoints = useEraRewardPoints({ eraIndex: eraIndex?.index }) const eraRewardPoints = useEraRewardPoints({ eraIndex: eraIndex?.index })
const currentValidators = useCurrentValidators({ address: interestingValidator }) const currentValidators = useCurrentValidators({ address: interestingValidator })
const validatorOverview = useValidatorsOverview({ eraIndex: eraIndex?.index, address: interestingValidator }) const validatorOverview = useValidatorsOverview({ eraIndex: eraIndex?.index, address: interestingValidator })
console.log(ledger)
const tokenDecimals: number = chainSpecV1?.properties?.tokenDecimals ?? 0 const tokenDecimals: number = chainSpecV1?.properties?.tokenDecimals ?? 0
const tokenSymbol: string = chainSpecV1?.properties?.tokenSymbol ?? "" const tokenSymbol: string = chainSpecV1?.properties?.tokenSymbol ?? ""
@ -225,6 +232,54 @@ export const Nominations = () => {
const bondCalldata = useBondCalldata( const bondCalldata = useBondCalldata(
convertedAmount > 0n ? convertedAmount : undefined convertedAmount > 0n ? convertedAmount : undefined
) )
const unbondCalldata = useUnbondCalldata(
convertedAmount > 0n ? convertedAmount : undefined
)
const payeeDescription = useMemo(() => {
let description = "Unknown reward destination"
switch (payee?.type) {
case "Staked":
description = "Re-stake rewards"
break;
case "Stash":
description = "Withdraw rewards to free"
break;
case "Account":
description = `Rewards to => ${payee?.value}`
break;
case "None":
description = "Refuse to receive rewards"
break;
}
return description
}, [payee])
const readyToWithdraw = useMemo(() => {
return ledger?.unlocking.reduce((acc, item) => {
if ((eraIndex?.index ?? 0) >= item.era) {
return item.value
}
return 0n
}, 0n)
}, [ledger, eraIndex])
const waitingForWithdraw = useMemo(() => {
return ledger?.unlocking.reduce((acc, item) => {
if ((eraIndex?.index ?? 0) < item.era) {
return item.value
}
return 0n
}, 0n)
}, [ledger, eraIndex])
const latestWithdrawEra = useMemo(() => {
return Math.max(ledger?.unlocking.map(el => el.era - (eraIndex?.index ?? 0)) ?? [])
}, [eraIndex, ledger])
useEffect(() => {
setIsCheckedAddresses([])
}, [account])
useEffect(() => { useEffect(() => {
if (checkedAddresses.length === 0 && nominations) { if (checkedAddresses.length === 0 && nominations) {
@ -247,6 +302,55 @@ export const Nominations = () => {
return `${formatter.format(numberValue)} ${tokenSymbol}` 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 () => { const handleOnBond = useCallback(async () => {
setIsSubmittingTransaction(true) setIsSubmittingTransaction(true)
setTransactionStatus(undefined) setTransactionStatus(undefined)
@ -366,23 +470,76 @@ export const Nominations = () => {
connectAccount={connectAccount} connectAccount={connectAccount}
applyDecimals={applyDecimals} applyDecimals={applyDecimals}
/> />
<Row title="Bonded Amount" element={<Input {ledger && ledger.total > 0n && (<Accordion type="multiple" className="w-full flex flex-col gap-4 mb-4">
<AccordionItem className="bg-muted rounded text-sm" value="Bonding Details">
<AccordionTrigger>
<div className="flex items-center gap-2 space-x-2 cursor-pointer">
Bonding Details
</div>
</AccordionTrigger>
<AccordionContent className="flex flex-col gap-2 pl-8">
<Row title="Destination" element={<Input
readOnly readOnly
aria-label="Bonded Amount" aria-label="Destination"
type="text" type="text"
className="sm:w-[300px] w-full" className="sm:w-[300px] w-full"
placeholder={applyDecimals(senderAccount?.data.frozen, tokenDecimals, tokenSymbol)} placeholder={payeeDescription}
/>} /> />} />
<hr /> <Row title="Total Bond" element={<Input
<Row title="Amount" element={<Input readOnly
aria-label="Total Bond"
type="text"
className="sm:w-[300px] w-full"
placeholder={applyDecimals(ledger?.total, tokenDecimals, tokenSymbol)}
/>} />
<Row title="Active Bond" element={<Input
readOnly
aria-label="Active Bond"
type="text"
className="sm:w-[300px] w-full"
placeholder={applyDecimals(ledger?.active, tokenDecimals, tokenSymbol)}
/>} />
{ledger && ledger.unlocking.length > 0 && (
<>
<hr className="my-2" />
<Row title="Ready" element={<Input
readOnly
aria-label="Ready"
type="text"
className="sm:w-[300px] w-full"
placeholder={applyDecimals(readyToWithdraw, tokenDecimals, tokenSymbol)}
/>} />
<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")}
disabled={isSubmittingTransaction || readyToWithdraw === 0n}
>
<PiggyBank className="w-4 h-4 inline-block mr-2" />
Withdraw
</Button>
</>
)}
</AccordionContent>
</AccordionItem>
</Accordion>)}
<Input
value={amount} value={amount}
onChange={e => setAmount(e.target.value)} onChange={e => setAmount(e.target.value)}
disabled={isSubmittingTransaction} disabled={isSubmittingTransaction}
aria-label="Transfer Amount" aria-label="Transfer Amount"
type="text" type="text"
className="sm:w-[300px] w-full" className="w-full"
placeholder="Input amount to bond or unbond" placeholder="Input amount to bond or unbond"
/>} /> />
<div className="flex justify-between gap-2"> <div className="flex justify-between gap-2">
{bondedAddress && ( {bondedAddress && (
<Button <Button
@ -390,6 +547,7 @@ export const Nominations = () => {
variant="secondary" variant="secondary"
className="text-sm p-4 w-full" className="text-sm p-4 w-full"
onClick={handleOnNominate} onClick={handleOnNominate}
disabled={isSubmittingTransaction || convertedAmount === 0n}
> >
<Users className="w-4 h-4 inline-block mr-2" /> <Users className="w-4 h-4 inline-block mr-2" />
Nominate Nominate
@ -401,6 +559,7 @@ export const Nominations = () => {
variant="secondary" variant="secondary"
className="text-sm p-4 w-full" className="text-sm p-4 w-full"
onClick={handleOnBond} onClick={handleOnBond}
disabled={isSubmittingTransaction || convertedAmount === 0n}
> >
<Nut className="w-4 h-4 inline-block mr-2" /> <Nut className="w-4 h-4 inline-block mr-2" />
Bond Bond
@ -409,8 +568,9 @@ export const Nominations = () => {
<Button <Button
type="button" type="button"
variant="secondary" variant="secondary"
disabled={!bondedAddress}
className="text-sm p-4 w-full" className="text-sm p-4 w-full"
onClick={handleOnUnbond}
disabled={isSubmittingTransaction || convertedAmount === 0n}
> >
<NutOff className="w-4 h-4 inline-block mr-2" /> <NutOff className="w-4 h-4 inline-block mr-2" />
Unbond Unbond
@ -439,7 +599,7 @@ export const Nominations = () => {
)} )}
</div> </div>
{eraRewardPoints && ( {eraRewardPoints && (
<Accordion 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"> <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">
{eraRewardPoints?.individual.map((indivial: RewardPoints, idx: number) => ( {eraRewardPoints?.individual.map((indivial: RewardPoints, idx: number) => (
<Item <Item
key={idx} key={idx}

View File

@ -14,3 +14,5 @@ export * from "./useCurrentValidators"
export * from "./useValidatorsOverview" export * from "./useValidatorsOverview"
export * from "./useBondedAddress" export * from "./useBondedAddress"
export * from "./useNominations" export * from "./useNominations"
export * from "./useLedger"
export * from "./usePayee"

View File

@ -48,12 +48,34 @@ export const useTransferCalldata = (destination: SS58String | undefined, amount:
return calldata return calldata
} }
export const useUnbondCalldata = (amount: bigint | undefined) => {
const { client, chainId } = useUnstableProvider()
const metadata = useMetadata()
const { data: calldata } = useSWR(
client && chainId && amount && metadata
? ["unbond", client, chainId, metadata, amount]
: null,
([_, client, _chainId, metadata, amount]) => {
const builder = getDynamicBuilder(getLookupFn(metadata))
const { codec, location } = builder.buildCall("Staking", "unbond")
return toHex(
mergeUint8(
new Uint8Array(location),
codec.enc({ value: amount }),
),
)
}
)
return calldata
}
export const useBondCalldata = (amount: bigint | undefined) => { export const useBondCalldata = (amount: bigint | undefined) => {
const { client, chainId } = useUnstableProvider() const { client, chainId } = useUnstableProvider()
const metadata = useMetadata() const metadata = useMetadata()
const { data: calldata } = useSWR( const { data: calldata } = useSWR(
client && chainId && amount && metadata client && chainId && amount && metadata
? ["metadata", client, chainId, metadata, amount] ? ["bond", client, chainId, metadata, amount]
: null, : null,
([_, client, _chainId, metadata, amount]) => { ([_, client, _chainId, metadata, amount]) => {
const builder = getDynamicBuilder(getLookupFn(metadata)) const builder = getDynamicBuilder(getLookupFn(metadata))
@ -78,7 +100,7 @@ export const useNominateCalldata = (addresses: string[]) => {
const metadata = useMetadata() const metadata = useMetadata()
const { data: calldata } = useSWR( const { data: calldata } = useSWR(
client && chainId && addresses && metadata client && chainId && addresses && metadata
? ["metadata", client, chainId, metadata, addresses] ? ["nominate", client, chainId, metadata, addresses]
: null, : null,
([_, client, _chainId, metadata, addresses]) => { ([_, client, _chainId, metadata, addresses]) => {
const builder = getDynamicBuilder(getLookupFn(metadata)) const builder = getDynamicBuilder(getLookupFn(metadata))

54
src/hooks/useLedger.tsx Normal file
View File

@ -0,0 +1,54 @@
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"
type Unlocking = {
value: bigint
era: number
}
export type AddressLedger = {
stash: string
total: bigint
active: bigint
unlocking: Unlocking[]
}
export const useLedger = ({ address }: { address: string | undefined }) => {
const { chainHead$, chainId } = useUnstableProvider()
const metadata = useMetadata()
const { data: ledger } = useSWRSubscription(
chainHead$ && address && chainId && metadata
? ["ledger", 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 ledger = builder.buildStorage("Staking", "Ledger")
return storage$(blockInfo?.hash, "value", () =>
ledger?.keys.enc(address)
).pipe(
filter(Boolean),
distinct(),
map((value: string) => ledger?.value.dec(value) as AddressLedger)
)
}),
)
.subscribe({
next(ledger: AddressLedger) {
next(null, ledger)
},
error: next,
})
return () => subscription.unsubscribe()
}
)
return ledger
}

47
src/hooks/usePayee.tsx Normal file
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 usePayee = ({ address }: { address: string | undefined }) => {
const { chainHead$, chainId } = useUnstableProvider()
const metadata = useMetadata()
const { data: payee } = useSWRSubscription(
chainHead$ && address && chainId && metadata
? ["payee", 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 payee = builder.buildStorage("Staking", "Payee")
return storage$(blockInfo?.hash, "value", () =>
payee?.keys.enc(address)
).pipe(
filter(Boolean),
distinct(),
map((value: string) => payee?.value.dec(value) as Payee)
)
}),
)
.subscribe({
next(payee: Payee) {
next(null, payee)
},
error: next,
})
return () => subscription.unsubscribe()
}
)
return payee
}