From b5ab8a6b814fcbfcf5157cfe825087d26f64fde5 Mon Sep 17 00:00:00 2001 From: Uncle Fatso Date: Tue, 26 Aug 2025 16:48:59 +0300 Subject: [PATCH] move transaction handler to context and add ability to change payee Signed-off-by: Uncle Fatso --- package.json | 2 +- src/api/index.ts | 1 - src/api/submitTransaction$.ts | 7 - src/containers/App.tsx | 34 +- src/containers/Nominations.tsx | 373 +++++---------------- src/containers/Transactions.tsx | 180 ++++------ src/hooks/index.ts | 1 + src/hooks/useCalldata.tsx | 66 +++- src/hooks/useSlasingSpans.tsx | 12 +- src/hooks/useTransactionStatusProvider.tsx | 145 ++++++++ 10 files changed, 389 insertions(+), 432 deletions(-) delete mode 100644 src/api/index.ts delete mode 100644 src/api/submitTransaction$.ts create mode 100644 src/hooks/useTransactionStatusProvider.tsx diff --git a/package.json b/package.json index b4238bb..3b6133e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghost-lite", - "version": "0.1.6", + "version": "0.1.7", "description": "Web application for Ghost and Casper chain.", "author": "Uncle f4ts0 ", "maintainers": [ diff --git a/src/api/index.ts b/src/api/index.ts deleted file mode 100644 index 24913e5..0000000 --- a/src/api/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./submitTransaction$" diff --git a/src/api/submitTransaction$.ts b/src/api/submitTransaction$.ts deleted file mode 100644 index cc19ed8..0000000 --- a/src/api/submitTransaction$.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { map } from "rxjs" - -export const submitTransaction$ = (clientFull: any, tx: string) => { - return clientFull?.submitAndWatch(tx).pipe( - map((txEvent) => ({ tx, txEvent })) - ) -} diff --git a/src/containers/App.tsx b/src/containers/App.tsx index 51d87bb..6205c0c 100644 --- a/src/containers/App.tsx +++ b/src/containers/App.tsx @@ -2,7 +2,11 @@ import { lazy, Suspense } from "react" import { HashRouter, Routes, Route, Navigate } from "react-router-dom" import { Layout, Sidebar, Header } from "../components" -import { UnstableProviderProvider, MetadataProviderProvider } from "../hooks" +import { + UnstableProviderProvider, + MetadataProviderProvider, + TransactionStatusProviderProvider, +} from "../hooks" import { DEFAULT_CHAIN_ID } from "../settings" const HealthCheck = lazy(() => import("./HealthCheck").then(module => ({ default: module.HealthCheck }))) @@ -16,19 +20,21 @@ export const App = () => { }> - - -
-
- - } /> - } /> - } /> - } /> - } /> - -
-
+ + + +
+
+ + } /> + } /> + } /> + } /> + } /> + +
+
+
diff --git a/src/containers/Nominations.tsx b/src/containers/Nominations.tsx index b88ed2b..6da8047 100644 --- a/src/containers/Nominations.tsx +++ b/src/containers/Nominations.tsx @@ -1,11 +1,6 @@ -import React, { useEffect, useState, useMemo, useCallback } from "react" +import React, { useEffect, useState, useMemo } from "react" import { Nut, NutOff, Users, PiggyBank, RefreshCcw } from "lucide-react" - import { ss58Decode } from "@polkadot-labs/hdkd-helpers" -import { toHex } from "@polkadot-api/utils" - -import { lastValueFrom, tap } from "rxjs" -import { submitTransaction$ } from "../api" import { Select, @@ -27,7 +22,6 @@ import { Button } from "../components/ui/button" import { useChainSpecV1, - useMetadata, useEraIndex, useNominations, useLedger, @@ -39,21 +33,19 @@ import { useBondedAddress, useNominateCalldata, useWithdrawCalldata, + usePayeeCalldata, useSystemAccount, useBondCalldata, useUnbondCalldata, + useBondExtraCalldata, useUnstableProvider, + useTransactionStatusProvider, RewardPoints, Unlocking, RewardDestination, } from "../hooks" -import { - AddressBookRecord, - FollowTransaction, - TransactionError, -} from "../types" - +import { AddressBookRecord } from "../types" import { Sender } from "./Accounts" import { Row } from "./Row" @@ -90,7 +82,7 @@ const Item: React.FC = (props) => { setIsCheckedAddresses, } = props - const [copied, setCopied] = useState(false); + const [copied, setCopied] = useState(false) const convertToFixed = (value: bigint, decimals: number) => { if (!value || !decimals) { return parseFloat("0").toFixed(5) @@ -102,7 +94,7 @@ const Item: React.FC = (props) => { const handleCopy = (textToCopy: string) => { if (!textToCopy) return navigator.clipboard.writeText(textToCopy).then(() => { - setCopied(true); + setCopied(true) setTimeout(() => setCopied(false), 2000) }) } @@ -191,27 +183,22 @@ const HeaderInfo = ({ text, value }: { text: string, value: string }) => { export const Nominations = () => { const addressBook: AddressBookRecord[] = JSON.parse(localStorage.getItem('addressBook') ?? '[]') - const [checkedAddresses, setIsCheckedAddresses] = useState([]) - const [isSubmittingTransaction, setIsSubmittingTransaction] = useState(false) - const [transactionStatus, setTransactionStatus] = useState() - const [error, setError] = useState() const [interestingValidator, setInterestingValidator] = useState(undefined) const [amount, setAmount] = useState("") const [destinationReceiver, setDestinationReceiver] = useState("") - const [expectedPayee, setExpectedPayee] = useState(undefined) + const [expectedPayee, setExpectedPayee] = useState("") const { - provider, - clientFull, - chainId, - account, - accounts, - connectAccount - } = useUnstableProvider() + setTransactionStatus, + setError, + isSubmittingTransaction, + handleTransaction, + renderStatus, + } = useTransactionStatusProvider() + const { account, accounts, connectAccount } = useUnstableProvider() - const metadata = useMetadata() const chainSpecV1 = useChainSpecV1() const eraIndex = useEraIndex() const nominations = useNominations({ address: account?.address }) @@ -224,6 +211,7 @@ export const Nominations = () => { const tokenDecimals: number = chainSpecV1?.properties?.tokenDecimals ?? 0 const tokenSymbol: string = chainSpecV1?.properties?.tokenSymbol ?? "" + const ss58Format: number = chainSpecV1?.properties?.ss58Format ?? 1995 const senderAccount = useSystemAccount({ account: account @@ -234,15 +222,14 @@ export const Nominations = () => { const destinationReceiverIsValid = useMemo(() => { try { const [, prefix] = ss58Decode(destinationReceiver) - if (prefix !== 1995 && prefix !== 1996) { + if (prefix !== ss58Format) { throw new Error("bad prefix") } return true } catch { return false } - - }, [destinationReceiver]) + }, [destinationReceiver, ss58Format]) const convertedAmount = useMemo(() => { try { @@ -254,8 +241,12 @@ export const Nominations = () => { const bondedAddress = useBondedAddress({ address: account?.address }) - const withdrawCalldata = useWithdrawCalldata({ numberOfSpans: slashingSpans?.length ?? 0 }) + const payeeCalldata = usePayeeCalldata(expectedPayee, destinationReceiver) + const withdrawCalldata = useWithdrawCalldata(slashingSpans?.prior?.length ?? 0) const nominateCalldata = useNominateCalldata(checkedAddresses) + const bondExtraCalldata = useBondExtraCalldata( + convertedAmount > 0n ? convertedAmount : undefined + ) const bondCalldata = useBondCalldata( convertedAmount > 0n ? convertedAmount : undefined ) @@ -268,16 +259,16 @@ export const Nominations = () => { switch (destination) { case "Staked": description = "Re-stake upcoming rewards" - break; + break case "Stash": description = "Withdraw rewards to free" - break; + break case "Account": description = `Rewards to account` - break; + break case "None": description = "Refuse to receive rewards" - break; + break } return description } @@ -301,7 +292,11 @@ export const Nominations = () => { }, [ledger, eraIndex]) const latestWithdrawEra = useMemo(() => { - return Math.max(ledger?.unlocking.map((el: Unlocking) => el.era - (eraIndex?.index ?? 0)), []) + if (!ledger || ledger.unlocking.length === 0) { + return 0 + } + const index = eraIndex?.index ?? 0 + return Math.max(...ledger.unlocking.map((el: Unlocking) => el.era - index), 0) }, [eraIndex, ledger]) useEffect(() => { @@ -309,7 +304,7 @@ export const Nominations = () => { }, [account]) useEffect(() => { - if (!expectedPayee) setExpectedPayee(payee?.type) + if (expectedPayee === "") setExpectedPayee(payee?.type ?? "") }, [expectedPayee, setExpectedPayee, payee]) useEffect(() => { @@ -319,9 +314,21 @@ export const Nominations = () => { }, [checkedAddresses, nominations]) useEffect(() => { - setError(undefined) - setTransactionStatus(undefined) - }, [amount]) + if (amount !== "") { + setError(undefined) + setTransactionStatus(undefined) + } + }, [amount, setError, setTransactionStatus]) + + useEffect(() => { + if (!isSubmittingTransaction) { + setAmount("") + } + }, [isSubmittingTransaction]) + + useEffect(() => { + setDestinationReceiver("") + }, [expectedPayee]) const applyDecimals = (value = 0n, decimals = 0, tokenSymbol = "CSPR") => { if (!value) return `0 ${tokenSymbol}` @@ -333,201 +340,12 @@ 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) - setError(undefined) - - try { - const tx = await provider!.createTx( - chainId ?? "", - account ? toHex(ss58Decode(account.address)[0]) : "", - bondCalldata ?? "" - ) - 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, bondCalldata, chainId, clientFull, provider]) - - const handleOnNominate = useCallback(async () => { - setIsSubmittingTransaction(true) - setTransactionStatus(undefined) - setError(undefined) - - try { - const tx = await provider!.createTx( - chainId ?? "", - account ? toHex(ss58Decode(account.address)[0]) : "", - nominateCalldata ?? "" - ) - 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, 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]) + const handleOnBond = () => handleTransaction({ calldata: bondCalldata, txName: "bond" }) + const handleOnExtraBond = () => handleTransaction({ calldata: bondExtraCalldata, txName: "bond_extra" }) + const handleOnUnbond = () => handleTransaction({ calldata: unbondCalldata, txName: "unbond" }) + const handleOnNominate = () => handleTransaction({ calldata: nominateCalldata, txName: "nominate" }) + const handleOnWithdraw = () => handleTransaction({ calldata: withdrawCalldata, txName: "withdraw" }) + const handleOnSetPayee = () => handleTransaction({ calldata: payeeCalldata, txName: "set_payee" }) return (
@@ -583,6 +401,15 @@ export const Nominations = () => { } />)} + {payee?.type === "Account" && ( + } /> + )} {expectedPayee !== payee?.type && expectedPayee === "Account" && ( { type="button" variant="secondary" className="text-sm my-4 w-full" - onClick={() => alert("not ready yet")} - disabled={isSubmittingTransaction || !destinationReceiverIsValid} + onClick={handleOnSetPayee} + disabled={ + isSubmittingTransaction + || !payeeCalldata + || (expectedPayee === "Account" && !destinationReceiverIsValid) + } > Change Destination @@ -661,6 +492,28 @@ export const Nominations = () => { placeholder="Input amount to bond or unbond" />
+ + +
+
{bondedAddress && ( )} - {!bondedAddress && ( - - )} -
{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 && ( -
-

- Transaction status: {`${transactionStatus.status}`} -

- {transactionStatus.hash && (

- {transactionStatus.hash} -

)} -
- - )} - {!error && !metadata && ( -

- Downloading chain metadata... -

- )} - {error && ( -

- Error: {error.error} -

- )} + {renderStatus()}
{eraRewardPoints && ( diff --git a/src/containers/Transactions.tsx b/src/containers/Transactions.tsx index 450a290..4667561 100644 --- a/src/containers/Transactions.tsx +++ b/src/containers/Transactions.tsx @@ -6,29 +6,20 @@ import { ArrowBigRightDash, ArrowBigDownDash } from "lucide-react" -import React, { useState, useEffect, useCallback, useMemo } from "react" +import React, { useState, useEffect, useMemo } from "react" import { useLocation } from "react-router-dom" -import { lastValueFrom, tap } from "rxjs" - import { ss58Decode } from "@polkadot-labs/hdkd-helpers" -import { toHex } from "@polkadot-api/utils" import { useChainSpecV1, useSystemAccount, useUnstableProvider, + useTransactionStatusProvider, useMetadata, useTransferCalldata, useExistentialDeposit } from "../hooks" -import { submitTransaction$ } from "../api" -import { - FollowTransaction, - TransactionError, - TransactionHistory, -} from "../types" - import { Accordion, AccordionContent, @@ -39,6 +30,7 @@ import { Input } from "../components/ui/input" import { Button } from "../components/ui/button" import { Sender, Receiver } from "./Accounts" +import { TransactionHistory } from "../types" export const Transactions = () => { const location = useLocation() @@ -58,19 +50,24 @@ export const Transactions = () => { localStorage.getItem("defaultTransactAmount") ?? "" ) - const [transactionStatus, setTransactionStatus] = useState() - const [error, setError] = useState() - const [isSubmittingTransaction, setIsSubmittingTransaction] = useState(false) - const [activeTab, onActiveTabChanged] = useState("transact") const [receiver, setReceiver] = useState(initialReceiver) const [amount, setAmount] = useState(defaultTransactAmount) + const { + setTransactionStatus, + setError, + isSubmittingTransaction, + handleTransaction, + renderStatus, + } = useTransactionStatusProvider() + const metadata = useMetadata() const existentialDeposit = useExistentialDeposit() const chainSpecV1 = useChainSpecV1() const tokenDecimals: number = chainSpecV1?.properties?.tokenDecimals ?? 0 const tokenSymbol: string = chainSpecV1?.properties?.tokenSymbol ?? "" + const ss58Format: number = chainSpecV1?.properties?.ss58Format ?? 1995 const convertedAmount = useMemo(() => { try { @@ -81,22 +78,19 @@ export const Transactions = () => { }, [amount, tokenDecimals]) const convertedTimestamp = ({ timestamp }: { timestamp: number }) => { - const secondsInMinute = 60; - const secondsInHour = secondsInMinute * 60; - const secondsInDay = secondsInHour * 24; + const secondsInMinute = 60 + const secondsInHour = secondsInMinute * 60 + const secondsInDay = secondsInHour * 24 - const days = Math.floor(timestamp / secondsInDay); - const hours = Math.floor((timestamp % secondsInDay) / secondsInHour); - const minutes = Math.floor((timestamp % secondsInHour) / secondsInMinute); - const seconds = timestamp % secondsInMinute; + const days = Math.floor(timestamp / secondsInDay) + const hours = Math.floor((timestamp % secondsInDay) / secondsInHour) + const minutes = Math.floor((timestamp % secondsInHour) / secondsInMinute) + const seconds = timestamp % secondsInMinute - return `${days} days, ${hours} hours, ${minutes} minutes, ${seconds} seconds`; + return `${days} days, ${hours} hours, ${minutes} minutes, ${seconds} seconds` } const { - provider, - clientFull, - chainId, account, accounts, connectAccount @@ -105,14 +99,14 @@ export const Transactions = () => { const receiverObject = useMemo(() => { try { const [, prefix] = ss58Decode(receiver) - if (prefix !== 1995 && prefix !== 1996) { + if (prefix !== ss58Format) { throw new Error("bad prefix") } return { isValid: true, address: receiver } } catch (e) { return { isValid: false, address: receiver } } - }, [receiver]) + }, [receiver, ss58Format]) const calldata = useTransferCalldata( receiverObject.isValid ? receiverObject.address : undefined, @@ -160,85 +154,51 @@ export const Transactions = () => { localStorage.setItem("defaultTransactAmount", defaultTransactAmount) }, [defaultTransactAmount]) - const handleOnTransfer = useCallback(async () => { - setIsSubmittingTransaction(true) - setTransactionStatus(undefined) - setError(undefined) + useEffect(() => { + if (!isSubmittingTransaction) { + setAmount("") + } + }, [isSubmittingTransaction]) - const transactStory: TransactionHistory = { + useEffect(() => { + if (amount !== "") { + setError(undefined) + setTransactionStatus(undefined) + } + }, [amount, setError, setTransactionStatus]) + + const extraLogic = (txEvent: any) => { + const rawStatus = txEvent?.type.replace("txBestBlocksState", "mined") ?? "initiated" + const status = rawStatus.charAt(0).toUpperCase() + rawStatus.slice(1).toLowerCase() + + const newTransaction: TransactionHistory = { sender: account?.address ?? "", receiver: receiverObject?.address ?? "", amount, timestamp: Math.floor(Date.now() / 1000), calldata: calldata ?? "", tokenSymbol: tokenSymbol ?? "", - status: "Initiated" + status, + blockHash: txEvent?.block?.hash ?? "", + txHash: txEvent?.txHash ?? "", + blockNumber: txEvent?.block?.number } - try { - const tx = await provider!.createTx( - chainId ?? "", - account ? toHex(ss58Decode(account.address)[0]) : "", - calldata ?? "" - ) - await lastValueFrom( - submitTransaction$(clientFull, tx) - .pipe( - tap(({ txEvent }) => { - let status: string = "" - switch (txEvent.type) { - case "broadcasted": - status = "broadcasted to available peers" - transactStory.status = "Broadcasted" - break - case "txBestBlocksState": - status = `included in block #${txEvent.block.number}` - transactStory.blockNumber = txEvent.block.number - transactStory.status = "Mined" - break - case "finalized": - status = `finalized at block #${txEvent.block.number}` - transactStory.blockNumber = txEvent.block.number - transactStory.status = "Finalized" - break - case "throttled": - status = "throttling to detect chain head..." - transactStory.status = "Throttled" - break - } - transactStory.txHash = txEvent.txHash - transactStory.blockHash = txEvent.block?.hash - setTransactionStatus({ - status, - hash: txEvent.block?.hash, - }) - }), - ), - ) - } catch (err) { - if (err instanceof Error) { - const currentError = { type: "error", error: err.message } - transactStory.error = currentError - setError(currentError) + setTransactionHistory((prevHistory) => { + const existingIndex = prevHistory.findIndex(tx => tx.txHash === newTransaction.txHash) + if (existingIndex > -1) { + const updatedHistory = [...prevHistory] + updatedHistory[existingIndex] = { + ...updatedHistory[existingIndex], + ...newTransaction, + } + return updatedHistory + } else { + return [...prevHistory, newTransaction] } - console.error(err) - } finally { - setAmount(defaultTransactAmount ?? "") - setIsSubmittingTransaction(false) - setTransactionHistory([...transactionHistory, transactStory]) - } - }, [ - provider, - chainId, - account, - amount, - calldata, - clientFull, - defaultTransactAmount, - receiverObject?.address, - tokenSymbol, - transactionHistory - ]) + }) + } + const handleOnTransfer = () => handleTransaction({ calldata, txName: "transfer", extraLogic }) return (
@@ -304,8 +264,8 @@ export const Transactions = () => { - {!error && transactionStatus && ( -
-

- Transaction status: {`${transactionStatus.status}`} -

- {transactionStatus.hash && (

- {transactionStatus.hash} -

)} -
- - )} - {!error && !metadata && ( -

- Downloading chain metadata... -

- )} - {error && ( -

- Error: {error.error} -

- )} + {renderStatus()} )} {activeTab === "history" && ( diff --git a/src/hooks/index.ts b/src/hooks/index.ts index bcf144a..a6b21a6 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,6 +1,7 @@ export * from "./useSystemAccount" export * from "./useUnstableProvider" export * from "./useMetadata" +export * from "./useTransactionStatusProvider" export * from "./useChains" export * from "./useIsMounted" export * from "./useChainSpecV1" diff --git a/src/hooks/useCalldata.tsx b/src/hooks/useCalldata.tsx index 3136466..aaf5c7c 100644 --- a/src/hooks/useCalldata.tsx +++ b/src/hooks/useCalldata.tsx @@ -15,7 +15,7 @@ const AccountId = (value: SS58String) => Enum< "Id" >("Id", value) -export const RewardDestination = (account: SS58String | undefined) => { +export const RewardDestination = () => { return { Staked: () => Enum< { @@ -36,7 +36,7 @@ export const RewardDestination = (account: SS58String | undefined) => { Account: (account: SS58String) => Enum< { type: "Account" - value: account + value: SS58String }, "Account" >("Account", account), @@ -114,7 +114,7 @@ export const useBondCalldata = (amount: bigint | undefined) => { new Uint8Array(location), codec.enc({ value: amount, - payee: RewardDestination.Staked(), + payee: RewardDestination().Staked(), }), ), ) @@ -123,6 +123,28 @@ export const useBondCalldata = (amount: bigint | undefined) => { return calldata } +export const useBondExtraCalldata = (amount: bigint | undefined) => { + const { client, chainId } = useUnstableProvider() + const metadata = useMetadata() + const { data: calldata } = useSWR( + client && chainId && amount && metadata + ? ["bond_extra", client, chainId, metadata, amount] + : null, + ([_, client, _chainId, metadata, amount]) => { + const builder = getDynamicBuilder(getLookupFn(metadata)) + const { codec, location } = builder.buildCall("Staking", "bond_extra") + + return toHex( + mergeUint8( + new Uint8Array(location), + codec.enc({ max_additional: amount }), + ), + ) + } + ) + return calldata +} + export const useNominateCalldata = (addresses: string[]) => { const { client, chainId } = useUnstableProvider() const metadata = useMetadata() @@ -167,3 +189,41 @@ export const useWithdrawCalldata = (numberOfSpans: number) => { ) return calldata } + +export const usePayeeCalldata = (expectedPayee: string | undefined, destinationReceiver: string) => { + const { client, chainId } = useUnstableProvider() + const metadata = useMetadata() + const { data: calldata } = useSWR( + client && chainId && expectedPayee && metadata + ? ["withdraw_unbonded", client, chainId, metadata, expectedPayee] + : null, + ([_, client, _chainId, metadata, expectedPayee]) => { + const builder = getDynamicBuilder(getLookupFn(metadata)) + const { codec, location } = builder.buildCall("Staking", "set_payee") + + let destination + switch (expectedPayee) { + case "Stash": + destination = RewardDestination().Stash() + break + case "Account": + destination = RewardDestination().Account(destinationReceiver) + break + case "None": + destination = RewardDestination().None() + break + default: + destination = RewardDestination().Staked() + break + } + + return toHex( + mergeUint8( + new Uint8Array(location), + codec.enc({ payee: destination }), + ), + ) + } + ) + return calldata +} diff --git a/src/hooks/useSlasingSpans.tsx b/src/hooks/useSlasingSpans.tsx index 4c82107..94a845c 100644 --- a/src/hooks/useSlasingSpans.tsx +++ b/src/hooks/useSlasingSpans.tsx @@ -6,9 +6,11 @@ import { distinct, filter, map, mergeMap } from "rxjs" import { useUnstableProvider } from "./useUnstableProvider" import { useMetadata } from "./useMetadata" -export type Payee = { - type: string - value: string +export type SlashingSpans = { + span_index: number, + last_start: number, + last_nonzero_slash: number, + prior: number[], } export const useSlasingSpans = ({ address }: { address: string | undefined }) => { @@ -30,12 +32,12 @@ export const useSlasingSpans = ({ address }: { address: string | undefined }) => ).pipe( filter(Boolean), distinct(), - map((value: string) => slasingSpans?.value.dec(value)) + map((value: string) => slasingSpans?.value.dec(value) as SlashingSpans) ) }), ) .subscribe({ - next(slasingSpans) { + next(slasingSpans: SlashingSpans) { next(null, slasingSpans) }, error: next, diff --git a/src/hooks/useTransactionStatusProvider.tsx b/src/hooks/useTransactionStatusProvider.tsx new file mode 100644 index 0000000..2611acc --- /dev/null +++ b/src/hooks/useTransactionStatusProvider.tsx @@ -0,0 +1,145 @@ +import { type ReactNode, createContext, useContext, useState, useCallback } from "react" +import { toHex } from "@polkadot-api/utils" +import { ss58Decode } from "@polkadot-labs/hdkd-helpers" +import { lastValueFrom, tap, map } from "rxjs" + +import { useUnstableProvider } from "./useUnstableProvider" +import { useMetadata } from "./useMetadata" +import { FollowTransaction, TransactionError } from "../types" + +type HandleTransactionParams = { + calldata: string | undefined; + txName: string; + extraLogic?: (data: any) => void; +} + +type Context = { + isSubmittingTransaction: boolean; + transactionStatus: FollowTransaction | undefined; + error: TransactionError | undefined; + setIsSubmittingTransaction: (isSubmitting: boolean) => void; + setTransactionStatus: (status: FollowTransaction | undefined) => void; + setError: (error: TransactionError | undefined) => void; + handleTransaction: ({ calldata, txName, extraLogic }: HandleTransactionParams) => Promise; + renderStatus: () => JSX.Element; +} + +const TransactionStatusProvider = createContext(null!) +export const useTransactionStatusProvider = () => useContext(TransactionStatusProvider) + +export const TransactionStatusProviderProvider = ({ children }: { children: ReactNode }) => { + const [isSubmittingTransaction, setIsSubmittingTransaction] = useState(false) + const [transactionStatus, setTransactionStatus] = useState() + const [error, setError] = useState() + + const { provider, clientFull, chainId, account } = useUnstableProvider() + const metadata = useMetadata() + + const handleTransaction = useCallback(async ({ calldata, txName, extraLogic }: HandleTransactionParams) => { + if (!calldata) { + throw new Error("Calldata is not recognizable"); + } + + if (!account) { + throw new Error("Account is not recognizable") + } + + if (!clientFull) { + throw new Error("Connected client not found") + } + + setIsSubmittingTransaction(true); + setTransactionStatus(undefined); + setError(undefined); + + try { + const tx = await provider!.createTx( + chainId ?? "", + account ? toHex(ss58Decode(account?.address)[0]) : "", + calldata ?? "" + ); + await lastValueFrom( + clientFull.submitAndWatch(tx) + .pipe( + map((txEvent) => ({ tx, txEvent })), + tap(({ txEvent }) => { + let status: string = ""; + switch (txEvent.type) { + case "broadcasted": + status = `${txName} is broadcasted to available peers`; + break; + case "txBestBlocksState": + status = `${txName} included in block #${txEvent.block.number}`; + break; + case "finalized": + status = `${txName} finalized at block #${txEvent.block.number}`; + break; + case "throttled": + status = `${txName} throttling to detect chain head...`; + break; + } + setTransactionStatus({ + status, + hash: txEvent.block?.hash, + }); + extraLogic?.(txEvent) + }), + ), + ); + } catch (err) { + if (err instanceof Error) { + const currentError = { type: "error", error: err.message }; + setError(currentError); + } + console.error(err); + } finally { + setIsSubmittingTransaction(false); + } + }, [account, chainId, clientFull, provider]); + + + const renderStatus = () => { + return ( + <> + {!error && transactionStatus && ( +
+

+ Transaction status: {`${transactionStatus.status}`} +

+ {transactionStatus.hash && (

+ {transactionStatus.hash} +

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

+ Downloading chain metadata... +

+ )} + {error && ( +

+ Error: {error.error} +

+ )} + + ) + } + + return ( + + {children} + + ) +}