make reward destinations more flexible for the user

Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
This commit is contained in:
Uncle Fatso 2025-08-25 19:43:11 +03:00
parent 0d2c700e1a
commit 7822556988
Signed by: f4ts0
GPG Key ID: 565F4F2860226EBB
3 changed files with 118 additions and 23 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "ghost-lite", "name": "ghost-lite",
"version": "0.1.5", "version": "0.1.6",
"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, PiggyBank } from "lucide-react" import { Nut, NutOff, Users, PiggyBank, RefreshCcw } 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"
@ -7,6 +7,14 @@ import { toHex } from "@polkadot-api/utils"
import { lastValueFrom, tap } from "rxjs" import { lastValueFrom, tap } from "rxjs"
import { submitTransaction$ } from "../api" import { submitTransaction$ } from "../api"
import {
Select,
SelectValue,
SelectTrigger,
SelectContent,
SelectGroup,
SelectItem,
} from "../components/ui/select"
import { import {
Accordion, Accordion,
AccordionContent, AccordionContent,
@ -37,6 +45,7 @@ import {
useUnstableProvider, useUnstableProvider,
RewardPoints, RewardPoints,
Unlocking, Unlocking,
RewardDestination,
} from "../hooks" } from "../hooks"
import { import {
@ -62,7 +71,6 @@ interface ItemProps {
nominated: boolean nominated: boolean
checkedAddresses: string[] checkedAddresses: string[]
setIsCheckedAddresses: React.Dispatch<React.SetStateAction<string[]>> setIsCheckedAddresses: React.Dispatch<React.SetStateAction<string[]>>
} }
const Item: React.FC<ItemProps> = (props) => { const Item: React.FC<ItemProps> = (props) => {
@ -191,6 +199,8 @@ export const Nominations = () => {
const [interestingValidator, setInterestingValidator] = useState<string | undefined>(undefined) const [interestingValidator, setInterestingValidator] = useState<string | undefined>(undefined)
const [amount, setAmount] = useState<string>("") const [amount, setAmount] = useState<string>("")
const [destinationReceiver, setDestinationReceiver] = useState<string>("")
const [expectedPayee, setExpectedPayee] = useState<string | undefined>(undefined)
const { const {
provider, provider,
@ -221,6 +231,19 @@ export const Nominations = () => {
: undefined : undefined
}) })
const destinationReceiverIsValid = useMemo(() => {
try {
const [, prefix] = ss58Decode(destinationReceiver)
if (prefix !== 1995 && prefix !== 1996) {
throw new Error("bad prefix")
}
return true
} catch {
return false
}
}, [destinationReceiver])
const convertedAmount = useMemo(() => { const convertedAmount = useMemo(() => {
try { try {
return BigInt(Number(amount) * Math.pow(10, tokenDecimals)) return BigInt(Number(amount) * Math.pow(10, tokenDecimals))
@ -240,24 +263,24 @@ export const Nominations = () => {
convertedAmount > 0n ? convertedAmount : undefined convertedAmount > 0n ? convertedAmount : undefined
) )
const payeeDescription = useMemo(() => { const payeeDescription = (destination: string | undefined) => {
let description = "Unknown reward destination" let description = "Unknown reward destination"
switch (payee?.type) { switch (destination) {
case "Staked": case "Staked":
description = "Re-stake rewards" description = "Re-stake upcoming rewards"
break; break;
case "Stash": case "Stash":
description = "Withdraw rewards to free" description = "Withdraw rewards to free"
break; break;
case "Account": case "Account":
description = `Rewards to => ${payee?.value}` description = `Rewards to account`
break; break;
case "None": case "None":
description = "Refuse to receive rewards" description = "Refuse to receive rewards"
break; break;
} }
return description return description
}, [payee]) }
const readyToWithdraw = useMemo(() => { const readyToWithdraw = useMemo(() => {
return ledger?.unlocking.reduce((acc: bigint, item: Unlocking) => { return ledger?.unlocking.reduce((acc: bigint, item: Unlocking) => {
@ -285,6 +308,10 @@ export const Nominations = () => {
setIsCheckedAddresses([]) setIsCheckedAddresses([])
}, [account]) }, [account])
useEffect(() => {
if (!expectedPayee) setExpectedPayee(payee?.type)
}, [expectedPayee, setExpectedPayee, payee])
useEffect(() => { useEffect(() => {
if (checkedAddresses.length === 0 && nominations) { if (checkedAddresses.length === 0 && nominations) {
setIsCheckedAddresses(nominations?.targets ?? []) setIsCheckedAddresses(nominations?.targets ?? [])
@ -531,13 +558,53 @@ export const Nominations = () => {
</div> </div>
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="flex flex-col gap-2 pl-8"> <AccordionContent className="flex flex-col gap-2 pl-8">
<Row title="Destination" element={<Input {payee && (<Row title="Destination" element={<Select
readOnly value={expectedPayee}
aria-label="Destination" onValueChange={(payeeType) => setExpectedPayee(payeeType)}
type="text" >
className="sm:w-[300px] w-full" <SelectTrigger
placeholder={payeeDescription} className={"text-muted-foreground sm:w-[300px] w-full"}
/>} /> data-testid="destination-select"
>
<SelectValue placeholder="Select Destination" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{Object.keys(RewardDestination()).map((destinationType, index) => (
<SelectItem
key={index}
data-testid={`destination-${destinationType}`}
value={destinationType}
>
{payeeDescription(destinationType)}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
} />)}
{expectedPayee !== payee?.type && expectedPayee === "Account" && (
<Row title="Receiver" element={<Input
aria-label="Destination Account"
type="text"
className="sm:w-[300px] w-full"
placeholder="Input destination account"
onChange={e => setDestinationReceiver(e.target.value)}
value={destinationReceiver}
/>} />
)}
{expectedPayee !== payee?.type && (
<Button
type="button"
variant="secondary"
className="text-sm my-4 w-full"
onClick={() => alert("not ready yet")}
disabled={isSubmittingTransaction || !destinationReceiverIsValid}
>
<RefreshCcw className="w-4 h-4 inline-block mr-2" />
Change Destination
</Button>
)}
<Row title="Total Bond" element={<Input <Row title="Total Bond" element={<Input
readOnly readOnly
aria-label="Total Bond" aria-label="Total Bond"

View File

@ -15,13 +15,41 @@ const AccountId = (value: SS58String) => Enum<
"Id" "Id"
>("Id", value) >("Id", value)
const RewardDestination = () => Enum< export const RewardDestination = (account: SS58String | undefined) => {
{ return {
type: "Staked" Staked: () => Enum<
value: [] {
}, type: "Staked"
"Staked" value: []
>("Staked", []) },
"Staked"
>("Staked", []),
Stash: () => Enum<
{
type: "Stash"
value: []
},
"Stash"
>("Stash", []),
Account: (account: SS58String) => Enum<
{
type: "Account"
value: account
},
"Account"
>("Account", account),
None: () => Enum<
{
type: "None"
value: []
},
"None"
>("None", []),
}
}
export const useTransferCalldata = (destination: SS58String | undefined, amount: bigint | undefined) => { export const useTransferCalldata = (destination: SS58String | undefined, amount: bigint | undefined) => {
const { client, chainId } = useUnstableProvider() const { client, chainId } = useUnstableProvider()
@ -86,7 +114,7 @@ export const useBondCalldata = (amount: bigint | undefined) => {
new Uint8Array(location), new Uint8Array(location),
codec.enc({ codec.enc({
value: amount, value: amount,
payee: RewardDestination(), payee: RewardDestination.Staked(),
}), }),
), ),
) )