diff --git a/package.json b/package.json index 410d676..83592cd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghost-lite", - "version": "0.1.1", + "version": "0.1.2", "description": "Web application for Ghost and Casper chain.", "author": "Uncle f4ts0 ", "maintainers": [ diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx index d33c76d..465c5f3 100644 --- a/src/components/ui/accordion.tsx +++ b/src/components/ui/accordion.tsx @@ -11,9 +11,9 @@ const AccordionItem = React.forwardRef< React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) AccordionItem.displayName = "AccordionItem" diff --git a/src/containers/Accounts.tsx b/src/containers/Accounts.tsx index 26ee1f3..887afc9 100644 --- a/src/containers/Accounts.tsx +++ b/src/containers/Accounts.tsx @@ -88,41 +88,41 @@ export const Sender: React.FC = ({ Balance Details - - } /> - } /> - } /> - } /> - - + + } /> + } /> + } /> + } /> + + )} ) diff --git a/src/containers/Nominations.tsx b/src/containers/Nominations.tsx index d0a8057..eb8428d 100644 --- a/src/containers/Nominations.tsx +++ b/src/containers/Nominations.tsx @@ -1,5 +1,5 @@ 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 { toHex } from "@polkadot-api/utils" @@ -22,6 +22,8 @@ import { useMetadata, useEraIndex, useNominations, + useLedger, + usePayee, useEraRewardPoints, useCurrentValidators, useValidatorsOverview, @@ -29,6 +31,7 @@ import { useNominateCalldata, useSystemAccount, useBondCalldata, + useUnbondCalldata, useUnstableProvider, RewardPoints, } from "../hooks" @@ -110,7 +113,9 @@ const Item: React.FC = (props) => {
-
{name ?? address}
+
+ {name ?? (address.slice(0, 15) + "..." + address.slice(-15))} +
{points}
@@ -198,10 +203,12 @@ export const Nominations = () => { const chainSpecV1 = useChainSpecV1() const eraIndex = useEraIndex() 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 currentValidators = useCurrentValidators({ address: interestingValidator }) const validatorOverview = useValidatorsOverview({ eraIndex: eraIndex?.index, address: interestingValidator }) - +console.log(ledger) const tokenDecimals: number = chainSpecV1?.properties?.tokenDecimals ?? 0 const tokenSymbol: string = chainSpecV1?.properties?.tokenSymbol ?? "" @@ -225,6 +232,54 @@ export const Nominations = () => { const bondCalldata = useBondCalldata( 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(() => { if (checkedAddresses.length === 0 && nominations) { @@ -247,6 +302,55 @@ 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) @@ -366,23 +470,76 @@ export const Nominations = () => { connectAccount={connectAccount} applyDecimals={applyDecimals} /> - } /> -
- 0n && ( + + +
+ Bonding Details +
+
+ + } /> + } /> + } /> + {ledger && ledger.unlocking.length > 0 && ( + <> +
+ } /> + 1 ? "s" : ""}`} element={} /> + + + )} +
+
+
)} + setAmount(e.target.value)} disabled={isSubmittingTransaction} aria-label="Transfer Amount" type="text" - className="sm:w-[300px] w-full" + className="w-full" placeholder="Input amount to bond or unbond" - />} /> + />
{bondedAddress && (
{eraRewardPoints && ( - + {eraRewardPoints?.individual.map((indivial: RewardPoints, idx: number) => ( { + 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) => { const { client, chainId } = useUnstableProvider() const metadata = useMetadata() const { data: calldata } = useSWR( client && chainId && amount && metadata - ? ["metadata", client, chainId, metadata, amount] + ? ["bond", client, chainId, metadata, amount] : null, ([_, client, _chainId, metadata, amount]) => { const builder = getDynamicBuilder(getLookupFn(metadata)) @@ -78,7 +100,7 @@ export const useNominateCalldata = (addresses: string[]) => { const metadata = useMetadata() const { data: calldata } = useSWR( client && chainId && addresses && metadata - ? ["metadata", client, chainId, metadata, addresses] + ? ["nominate", client, chainId, metadata, addresses] : null, ([_, client, _chainId, metadata, addresses]) => { const builder = getDynamicBuilder(getLookupFn(metadata)) diff --git a/src/hooks/useLedger.tsx b/src/hooks/useLedger.tsx new file mode 100644 index 0000000..06e151b --- /dev/null +++ b/src/hooks/useLedger.tsx @@ -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 +} diff --git a/src/hooks/usePayee.tsx b/src/hooks/usePayee.tsx new file mode 100644 index 0000000..8c390fd --- /dev/null +++ b/src/hooks/usePayee.tsx @@ -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 +}