diff --git a/package.json b/package.json index 1d8ed6a..aad832c 100644 --- a/package.json +++ b/package.json @@ -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 ", "maintainers": [ diff --git a/src/containers/Nominations.tsx b/src/containers/Nominations.tsx index 26203df..631c238 100644 --- a/src/containers/Nominations.tsx +++ b/src/containers/Nominations.tsx @@ -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 (
@@ -509,18 +562,18 @@ export const Nominations = () => { className="sm:w-[300px] w-full" placeholder={applyDecimals(readyToWithdraw, tokenDecimals, tokenSymbol)} />} /> - 1 ? "s" : ""}`} element={ 0 && ( 1 ? "s" : ""}`} element={} /> + />} />)}
+ {eraRewardPoints?.individual.some((indivial: RewardPoints) => indivial?.at(0) === account?.address) && ( +

+ 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. +

+ )} {!error && transactionStatus && (

diff --git a/src/hooks/index.ts b/src/hooks/index.ts index fbbba05..bcf144a 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -16,3 +16,4 @@ export * from "./useBondedAddress" export * from "./useNominations" export * from "./useLedger" export * from "./usePayee" +export * from "./useSlasingSpans" diff --git a/src/hooks/useCalldata.tsx b/src/hooks/useCalldata.tsx index d0633fb..6d94e00 100644 --- a/src/hooks/useCalldata.tsx +++ b/src/hooks/useCalldata.tsx @@ -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 +} diff --git a/src/hooks/useSlasingSpans.tsx b/src/hooks/useSlasingSpans.tsx new file mode 100644 index 0000000..4c82107 --- /dev/null +++ b/src/hooks/useSlasingSpans.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 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 +}