Compare commits
3 Commits
9ab91c801d
...
b5ab8a6b81
Author | SHA1 | Date | |
---|---|---|---|
b5ab8a6b81 | |||
7822556988 | |||
0d2c700e1a |
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ghost-lite",
|
"name": "ghost-lite",
|
||||||
"version": "0.1.4",
|
"version": "0.1.7",
|
||||||
"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": [
|
||||||
|
@ -1 +0,0 @@
|
|||||||
export * from "./submitTransaction$"
|
|
@ -1,7 +0,0 @@
|
|||||||
import { map } from "rxjs"
|
|
||||||
|
|
||||||
export const submitTransaction$ = (clientFull: any, tx: string) => {
|
|
||||||
return clientFull?.submitAndWatch(tx).pipe(
|
|
||||||
map((txEvent) => ({ tx, txEvent }))
|
|
||||||
)
|
|
||||||
}
|
|
@ -2,7 +2,11 @@ import { lazy, Suspense } from "react"
|
|||||||
import { HashRouter, Routes, Route, Navigate } from "react-router-dom"
|
import { HashRouter, Routes, Route, Navigate } from "react-router-dom"
|
||||||
|
|
||||||
import { Layout, Sidebar, Header } from "../components"
|
import { Layout, Sidebar, Header } from "../components"
|
||||||
import { UnstableProviderProvider, MetadataProviderProvider } from "../hooks"
|
import {
|
||||||
|
UnstableProviderProvider,
|
||||||
|
MetadataProviderProvider,
|
||||||
|
TransactionStatusProviderProvider,
|
||||||
|
} from "../hooks"
|
||||||
import { DEFAULT_CHAIN_ID } from "../settings"
|
import { DEFAULT_CHAIN_ID } from "../settings"
|
||||||
|
|
||||||
const HealthCheck = lazy(() => import("./HealthCheck").then(module => ({ default: module.HealthCheck })))
|
const HealthCheck = lazy(() => import("./HealthCheck").then(module => ({ default: module.HealthCheck })))
|
||||||
@ -16,19 +20,21 @@ export const App = () => {
|
|||||||
<Suspense fallback={<div></div>}>
|
<Suspense fallback={<div></div>}>
|
||||||
<UnstableProviderProvider defaultChainId={DEFAULT_CHAIN_ID}>
|
<UnstableProviderProvider defaultChainId={DEFAULT_CHAIN_ID}>
|
||||||
<MetadataProviderProvider>
|
<MetadataProviderProvider>
|
||||||
<Layout>
|
<TransactionStatusProviderProvider>
|
||||||
<Sidebar />
|
<Layout>
|
||||||
<div style={{ maxWidth: "calc(100% - 100px)"}} className="w-full h-full flex flex-col sm:px-6 px-2 sm:py-8 py-2">
|
<Sidebar />
|
||||||
<Header />
|
<div style={{ maxWidth: "calc(100% - 100px)"}} className="w-full h-full flex flex-col sm:px-6 px-2 sm:py-8 py-2">
|
||||||
<Routes>
|
<Header />
|
||||||
<Route path="/health" element={<HealthCheck />} />
|
<Routes>
|
||||||
<Route path="/transactions" element={<Transactions />} />
|
<Route path="/health" element={<HealthCheck />} />
|
||||||
<Route path="/book" element={<AddressBook />} />
|
<Route path="/transactions" element={<Transactions />} />
|
||||||
<Route path="/nominations" element={<Nominations />} />
|
<Route path="/book" element={<AddressBook />} />
|
||||||
<Route path="*" element={<Navigate to="/health" replace />} />
|
<Route path="/nominations" element={<Nominations />} />
|
||||||
</Routes>
|
<Route path="*" element={<Navigate to="/health" replace />} />
|
||||||
</div>
|
</Routes>
|
||||||
</Layout>
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</TransactionStatusProviderProvider>
|
||||||
</MetadataProviderProvider>
|
</MetadataProviderProvider>
|
||||||
</UnstableProviderProvider>
|
</UnstableProviderProvider>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
import React, { useEffect, useState, useMemo, useCallback } from "react"
|
import React, { useEffect, useState, useMemo } 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 { lastValueFrom, tap } from "rxjs"
|
|
||||||
import { submitTransaction$ } from "../api"
|
|
||||||
|
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectValue,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
} from "../components/ui/select"
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionContent,
|
AccordionContent,
|
||||||
@ -19,30 +22,30 @@ import { Button } from "../components/ui/button"
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
useChainSpecV1,
|
useChainSpecV1,
|
||||||
useMetadata,
|
|
||||||
useEraIndex,
|
useEraIndex,
|
||||||
useNominations,
|
useNominations,
|
||||||
useLedger,
|
useLedger,
|
||||||
usePayee,
|
usePayee,
|
||||||
|
useSlasingSpans,
|
||||||
useEraRewardPoints,
|
useEraRewardPoints,
|
||||||
useCurrentValidators,
|
useCurrentValidators,
|
||||||
useValidatorsOverview,
|
useValidatorsOverview,
|
||||||
useBondedAddress,
|
useBondedAddress,
|
||||||
useNominateCalldata,
|
useNominateCalldata,
|
||||||
|
useWithdrawCalldata,
|
||||||
|
usePayeeCalldata,
|
||||||
useSystemAccount,
|
useSystemAccount,
|
||||||
useBondCalldata,
|
useBondCalldata,
|
||||||
useUnbondCalldata,
|
useUnbondCalldata,
|
||||||
|
useBondExtraCalldata,
|
||||||
useUnstableProvider,
|
useUnstableProvider,
|
||||||
|
useTransactionStatusProvider,
|
||||||
RewardPoints,
|
RewardPoints,
|
||||||
Unlocking,
|
Unlocking,
|
||||||
|
RewardDestination,
|
||||||
} from "../hooks"
|
} from "../hooks"
|
||||||
|
|
||||||
import {
|
import { AddressBookRecord } from "../types"
|
||||||
AddressBookRecord,
|
|
||||||
FollowTransaction,
|
|
||||||
TransactionError,
|
|
||||||
} from "../types"
|
|
||||||
|
|
||||||
import { Sender } from "./Accounts"
|
import { Sender } from "./Accounts"
|
||||||
import { Row } from "./Row"
|
import { Row } from "./Row"
|
||||||
|
|
||||||
@ -60,7 +63,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) => {
|
||||||
@ -80,7 +82,7 @@ const Item: React.FC<ItemProps> = (props) => {
|
|||||||
setIsCheckedAddresses,
|
setIsCheckedAddresses,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const [copied, setCopied] = useState<boolean>(false);
|
const [copied, setCopied] = useState<boolean>(false)
|
||||||
const convertToFixed = (value: bigint, decimals: number) => {
|
const convertToFixed = (value: bigint, decimals: number) => {
|
||||||
if (!value || !decimals) {
|
if (!value || !decimals) {
|
||||||
return parseFloat("0").toFixed(5)
|
return parseFloat("0").toFixed(5)
|
||||||
@ -92,7 +94,7 @@ const Item: React.FC<ItemProps> = (props) => {
|
|||||||
const handleCopy = (textToCopy: string) => {
|
const handleCopy = (textToCopy: string) => {
|
||||||
if (!textToCopy) return
|
if (!textToCopy) return
|
||||||
navigator.clipboard.writeText(textToCopy).then(() => {
|
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||||
setCopied(true);
|
setCopied(true)
|
||||||
setTimeout(() => setCopied(false), 2000)
|
setTimeout(() => setCopied(false), 2000)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -181,36 +183,35 @@ const HeaderInfo = ({ text, value }: { text: string, value: string }) => {
|
|||||||
|
|
||||||
export const Nominations = () => {
|
export const Nominations = () => {
|
||||||
const addressBook: AddressBookRecord[] = JSON.parse(localStorage.getItem('addressBook') ?? '[]')
|
const addressBook: AddressBookRecord[] = JSON.parse(localStorage.getItem('addressBook') ?? '[]')
|
||||||
|
|
||||||
const [checkedAddresses, setIsCheckedAddresses] = useState<string[]>([])
|
const [checkedAddresses, setIsCheckedAddresses] = useState<string[]>([])
|
||||||
const [isSubmittingTransaction, setIsSubmittingTransaction] = useState<boolean>(false)
|
|
||||||
const [transactionStatus, setTransactionStatus] = useState<FollowTransaction | undefined>()
|
|
||||||
const [error, setError] = useState<TransactionError | undefined>()
|
|
||||||
|
|
||||||
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>("")
|
||||||
|
|
||||||
const {
|
const {
|
||||||
provider,
|
setTransactionStatus,
|
||||||
clientFull,
|
setError,
|
||||||
chainId,
|
isSubmittingTransaction,
|
||||||
account,
|
handleTransaction,
|
||||||
accounts,
|
renderStatus,
|
||||||
connectAccount
|
} = useTransactionStatusProvider()
|
||||||
} = useUnstableProvider()
|
const { account, accounts, connectAccount } = useUnstableProvider()
|
||||||
|
|
||||||
const metadata = useMetadata()
|
|
||||||
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 ledger = useLedger({ address: account?.address })
|
||||||
const payee = usePayee({ address: account?.address })
|
const payee = usePayee({ address: account?.address })
|
||||||
|
const slashingSpans = useSlasingSpans({ 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 })
|
||||||
|
|
||||||
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 ?? ""
|
||||||
|
const ss58Format: number = chainSpecV1?.properties?.ss58Format ?? 1995
|
||||||
|
|
||||||
const senderAccount = useSystemAccount({
|
const senderAccount = useSystemAccount({
|
||||||
account: account
|
account: account
|
||||||
@ -218,6 +219,18 @@ export const Nominations = () => {
|
|||||||
: undefined
|
: undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const destinationReceiverIsValid = useMemo(() => {
|
||||||
|
try {
|
||||||
|
const [, prefix] = ss58Decode(destinationReceiver)
|
||||||
|
if (prefix !== ss58Format) {
|
||||||
|
throw new Error("bad prefix")
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}, [destinationReceiver, ss58Format])
|
||||||
|
|
||||||
const convertedAmount = useMemo(() => {
|
const convertedAmount = useMemo(() => {
|
||||||
try {
|
try {
|
||||||
return BigInt(Number(amount) * Math.pow(10, tokenDecimals))
|
return BigInt(Number(amount) * Math.pow(10, tokenDecimals))
|
||||||
@ -228,7 +241,12 @@ export const Nominations = () => {
|
|||||||
|
|
||||||
const bondedAddress = useBondedAddress({ address: account?.address })
|
const bondedAddress = useBondedAddress({ address: account?.address })
|
||||||
|
|
||||||
|
const payeeCalldata = usePayeeCalldata(expectedPayee, destinationReceiver)
|
||||||
|
const withdrawCalldata = useWithdrawCalldata(slashingSpans?.prior?.length ?? 0)
|
||||||
const nominateCalldata = useNominateCalldata(checkedAddresses)
|
const nominateCalldata = useNominateCalldata(checkedAddresses)
|
||||||
|
const bondExtraCalldata = useBondExtraCalldata(
|
||||||
|
convertedAmount > 0n ? convertedAmount : undefined
|
||||||
|
)
|
||||||
const bondCalldata = useBondCalldata(
|
const bondCalldata = useBondCalldata(
|
||||||
convertedAmount > 0n ? convertedAmount : undefined
|
convertedAmount > 0n ? convertedAmount : undefined
|
||||||
)
|
)
|
||||||
@ -236,24 +254,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) => {
|
||||||
@ -274,13 +292,21 @@ export const Nominations = () => {
|
|||||||
}, [ledger, eraIndex])
|
}, [ledger, eraIndex])
|
||||||
|
|
||||||
const latestWithdrawEra = useMemo(() => {
|
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])
|
}, [eraIndex, ledger])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
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 ?? [])
|
||||||
@ -288,9 +314,21 @@ export const Nominations = () => {
|
|||||||
}, [checkedAddresses, nominations])
|
}, [checkedAddresses, nominations])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setError(undefined)
|
if (amount !== "") {
|
||||||
setTransactionStatus(undefined)
|
setError(undefined)
|
||||||
}, [amount])
|
setTransactionStatus(undefined)
|
||||||
|
}
|
||||||
|
}, [amount, setError, setTransactionStatus])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isSubmittingTransaction) {
|
||||||
|
setAmount("")
|
||||||
|
}
|
||||||
|
}, [isSubmittingTransaction])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDestinationReceiver("")
|
||||||
|
}, [expectedPayee])
|
||||||
|
|
||||||
const applyDecimals = (value = 0n, decimals = 0, tokenSymbol = "CSPR") => {
|
const applyDecimals = (value = 0n, decimals = 0, tokenSymbol = "CSPR") => {
|
||||||
if (!value) return `0 ${tokenSymbol}`
|
if (!value) return `0 ${tokenSymbol}`
|
||||||
@ -302,152 +340,12 @@ export const Nominations = () => {
|
|||||||
return `${formatter.format(numberValue)} ${tokenSymbol}`
|
return `${formatter.format(numberValue)} ${tokenSymbol}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOnUnbond = useCallback(async () => {
|
const handleOnBond = () => handleTransaction({ calldata: bondCalldata, txName: "bond" })
|
||||||
setIsSubmittingTransaction(true)
|
const handleOnExtraBond = () => handleTransaction({ calldata: bondExtraCalldata, txName: "bond_extra" })
|
||||||
setTransactionStatus(undefined)
|
const handleOnUnbond = () => handleTransaction({ calldata: unbondCalldata, txName: "unbond" })
|
||||||
setError(undefined)
|
const handleOnNominate = () => handleTransaction({ calldata: nominateCalldata, txName: "nominate" })
|
||||||
|
const handleOnWithdraw = () => handleTransaction({ calldata: withdrawCalldata, txName: "withdraw" })
|
||||||
try {
|
const handleOnSetPayee = () => handleTransaction({ calldata: payeeCalldata, txName: "set_payee" })
|
||||||
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])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sm:w-[500px] w-[85%] h-fit flex flex-col flex-1 gap-8 justify-center self-center rounded py-2">
|
<div className="sm:w-[500px] w-[85%] h-fit flex flex-col flex-1 gap-8 justify-center self-center rounded py-2">
|
||||||
@ -478,13 +376,66 @@ 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>
|
||||||
|
} />)}
|
||||||
|
{payee?.type === "Account" && (
|
||||||
|
<Row title="" element={<Input
|
||||||
|
readOnly
|
||||||
|
aria-label="Destination Account Address"
|
||||||
|
type="text"
|
||||||
|
className="sm:w-[300px] w-full"
|
||||||
|
placeholder={payee?.value}
|
||||||
|
/>} />
|
||||||
|
)}
|
||||||
|
{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={handleOnSetPayee}
|
||||||
|
disabled={
|
||||||
|
isSubmittingTransaction
|
||||||
|
|| !payeeCalldata
|
||||||
|
|| (expectedPayee === "Account" && !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"
|
||||||
@ -509,18 +460,18 @@ export const Nominations = () => {
|
|||||||
className="sm:w-[300px] w-full"
|
className="sm:w-[300px] w-full"
|
||||||
placeholder={applyDecimals(readyToWithdraw, tokenDecimals, tokenSymbol)}
|
placeholder={applyDecimals(readyToWithdraw, tokenDecimals, tokenSymbol)}
|
||||||
/>} />
|
/>} />
|
||||||
<Row title={`After ${latestWithdrawEra} era${latestWithdrawEra > 1 ? "s" : ""}`} element={<Input
|
{latestWithdrawEra > 0 && (<Row title={`After ${latestWithdrawEra} era${latestWithdrawEra > 1 ? "s" : ""}`} element={<Input
|
||||||
readOnly
|
readOnly
|
||||||
aria-label="Pending"
|
aria-label="Pending"
|
||||||
type="text"
|
type="text"
|
||||||
className="sm:w-[300px] w-full"
|
className="sm:w-[300px] w-full"
|
||||||
placeholder={applyDecimals(waitingForWithdraw, tokenDecimals, tokenSymbol)}
|
placeholder={applyDecimals(waitingForWithdraw, tokenDecimals, tokenSymbol)}
|
||||||
/>} />
|
/>} />)}
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="text-sm my-4 w-full"
|
className="text-sm my-4 w-full"
|
||||||
onClick={() => alert("Not implemented yet")}
|
onClick={handleOnWithdraw}
|
||||||
disabled={isSubmittingTransaction || readyToWithdraw === 0n}
|
disabled={isSubmittingTransaction || readyToWithdraw === 0n}
|
||||||
>
|
>
|
||||||
<PiggyBank className="w-4 h-4 inline-block mr-2" />
|
<PiggyBank className="w-4 h-4 inline-block mr-2" />
|
||||||
@ -541,30 +492,16 @@ export const Nominations = () => {
|
|||||||
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 && (
|
<Button
|
||||||
<Button
|
type="button"
|
||||||
type="button"
|
variant="secondary"
|
||||||
variant="secondary"
|
className="text-sm p-4 w-full"
|
||||||
className="text-sm p-4 w-full"
|
onClick={bondedAddress ? handleOnExtraBond : handleOnBond}
|
||||||
onClick={handleOnNominate}
|
disabled={isSubmittingTransaction || convertedAmount === 0n}
|
||||||
disabled={isSubmittingTransaction || convertedAmount === 0n}
|
>
|
||||||
>
|
<Nut className="w-4 h-4 inline-block mr-2" />
|
||||||
<Users className="w-4 h-4 inline-block mr-2" />
|
Bond
|
||||||
Nominate
|
</Button>
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{!bondedAddress && (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="secondary"
|
|
||||||
className="text-sm p-4 w-full"
|
|
||||||
onClick={handleOnBond}
|
|
||||||
disabled={isSubmittingTransaction || convertedAmount === 0n}
|
|
||||||
>
|
|
||||||
<Nut className="w-4 h-4 inline-block mr-2" />
|
|
||||||
Bond
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
@ -576,27 +513,26 @@ export const Nominations = () => {
|
|||||||
Unbond
|
Unbond
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{!error && transactionStatus && (
|
<div>
|
||||||
<div className="flex flex-col">
|
{bondedAddress && (
|
||||||
<p className="text-xs text-accent overflow-hidden whitespace-nowrap text-ellipsis">
|
<Button
|
||||||
Transaction status: {`${transactionStatus.status}`}
|
type="button"
|
||||||
</p>
|
variant="secondary"
|
||||||
{transactionStatus.hash && (<p className="text-xs text-accent overflow-hidden whitespace-nowrap text-ellipsis">
|
className="text-sm p-4 w-full"
|
||||||
{transactionStatus.hash}
|
onClick={handleOnNominate}
|
||||||
</p>)}
|
disabled={isSubmittingTransaction || checkedAddresses.length === 0}
|
||||||
</div>
|
>
|
||||||
|
<Users className="w-4 h-4 inline-block mr-2" />
|
||||||
)}
|
Nominate
|
||||||
{!error && !metadata && (
|
</Button>
|
||||||
<p className="text-xs text-accent overflow-hidden whitespace-nowrap text-ellipsis">
|
)}
|
||||||
Downloading chain metadata...
|
</div>
|
||||||
</p>
|
{eraRewardPoints?.individual.some((indivial: RewardPoints) => indivial?.at(0) === account?.address) && (
|
||||||
)}
|
<p className="text-xs text-destructive">
|
||||||
{error && (
|
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.
|
||||||
<p className="text-xs text-destructive overflow-hidden whitespace-nowrap text-ellipsis">
|
|
||||||
Error: {error.error}
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{renderStatus()}
|
||||||
</div>
|
</div>
|
||||||
{eraRewardPoints && (
|
{eraRewardPoints && (
|
||||||
<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">
|
<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">
|
||||||
|
@ -6,29 +6,20 @@ import {
|
|||||||
ArrowBigRightDash,
|
ArrowBigRightDash,
|
||||||
ArrowBigDownDash
|
ArrowBigDownDash
|
||||||
} from "lucide-react"
|
} 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 { useLocation } from "react-router-dom"
|
||||||
import { lastValueFrom, tap } from "rxjs"
|
|
||||||
|
|
||||||
import { ss58Decode } from "@polkadot-labs/hdkd-helpers"
|
import { ss58Decode } from "@polkadot-labs/hdkd-helpers"
|
||||||
import { toHex } from "@polkadot-api/utils"
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useChainSpecV1,
|
useChainSpecV1,
|
||||||
useSystemAccount,
|
useSystemAccount,
|
||||||
useUnstableProvider,
|
useUnstableProvider,
|
||||||
|
useTransactionStatusProvider,
|
||||||
useMetadata,
|
useMetadata,
|
||||||
useTransferCalldata,
|
useTransferCalldata,
|
||||||
useExistentialDeposit
|
useExistentialDeposit
|
||||||
} from "../hooks"
|
} from "../hooks"
|
||||||
|
|
||||||
import { submitTransaction$ } from "../api"
|
|
||||||
import {
|
|
||||||
FollowTransaction,
|
|
||||||
TransactionError,
|
|
||||||
TransactionHistory,
|
|
||||||
} from "../types"
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionContent,
|
AccordionContent,
|
||||||
@ -39,6 +30,7 @@ import { Input } from "../components/ui/input"
|
|||||||
import { Button } from "../components/ui/button"
|
import { Button } from "../components/ui/button"
|
||||||
|
|
||||||
import { Sender, Receiver } from "./Accounts"
|
import { Sender, Receiver } from "./Accounts"
|
||||||
|
import { TransactionHistory } from "../types"
|
||||||
|
|
||||||
export const Transactions = () => {
|
export const Transactions = () => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
@ -58,19 +50,24 @@ export const Transactions = () => {
|
|||||||
localStorage.getItem("defaultTransactAmount") ?? ""
|
localStorage.getItem("defaultTransactAmount") ?? ""
|
||||||
)
|
)
|
||||||
|
|
||||||
const [transactionStatus, setTransactionStatus] = useState<FollowTransaction | undefined>()
|
|
||||||
const [error, setError] = useState<TransactionError | undefined>()
|
|
||||||
const [isSubmittingTransaction, setIsSubmittingTransaction] = useState<boolean>(false)
|
|
||||||
|
|
||||||
const [activeTab, onActiveTabChanged] = useState<string>("transact")
|
const [activeTab, onActiveTabChanged] = useState<string>("transact")
|
||||||
const [receiver, setReceiver] = useState<string>(initialReceiver)
|
const [receiver, setReceiver] = useState<string>(initialReceiver)
|
||||||
const [amount, setAmount] = useState<string>(defaultTransactAmount)
|
const [amount, setAmount] = useState<string>(defaultTransactAmount)
|
||||||
|
|
||||||
|
const {
|
||||||
|
setTransactionStatus,
|
||||||
|
setError,
|
||||||
|
isSubmittingTransaction,
|
||||||
|
handleTransaction,
|
||||||
|
renderStatus,
|
||||||
|
} = useTransactionStatusProvider()
|
||||||
|
|
||||||
const metadata = useMetadata()
|
const metadata = useMetadata()
|
||||||
const existentialDeposit = useExistentialDeposit()
|
const existentialDeposit = useExistentialDeposit()
|
||||||
const chainSpecV1 = useChainSpecV1()
|
const chainSpecV1 = useChainSpecV1()
|
||||||
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 ?? ""
|
||||||
|
const ss58Format: number = chainSpecV1?.properties?.ss58Format ?? 1995
|
||||||
|
|
||||||
const convertedAmount = useMemo(() => {
|
const convertedAmount = useMemo(() => {
|
||||||
try {
|
try {
|
||||||
@ -81,22 +78,19 @@ export const Transactions = () => {
|
|||||||
}, [amount, tokenDecimals])
|
}, [amount, tokenDecimals])
|
||||||
|
|
||||||
const convertedTimestamp = ({ timestamp }: { timestamp: number }) => {
|
const convertedTimestamp = ({ timestamp }: { timestamp: number }) => {
|
||||||
const secondsInMinute = 60;
|
const secondsInMinute = 60
|
||||||
const secondsInHour = secondsInMinute * 60;
|
const secondsInHour = secondsInMinute * 60
|
||||||
const secondsInDay = secondsInHour * 24;
|
const secondsInDay = secondsInHour * 24
|
||||||
|
|
||||||
const days = Math.floor(timestamp / secondsInDay);
|
const days = Math.floor(timestamp / secondsInDay)
|
||||||
const hours = Math.floor((timestamp % secondsInDay) / secondsInHour);
|
const hours = Math.floor((timestamp % secondsInDay) / secondsInHour)
|
||||||
const minutes = Math.floor((timestamp % secondsInHour) / secondsInMinute);
|
const minutes = Math.floor((timestamp % secondsInHour) / secondsInMinute)
|
||||||
const seconds = timestamp % secondsInMinute;
|
const seconds = timestamp % secondsInMinute
|
||||||
|
|
||||||
return `${days} days, ${hours} hours, ${minutes} minutes, ${seconds} seconds`;
|
return `${days} days, ${hours} hours, ${minutes} minutes, ${seconds} seconds`
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
provider,
|
|
||||||
clientFull,
|
|
||||||
chainId,
|
|
||||||
account,
|
account,
|
||||||
accounts,
|
accounts,
|
||||||
connectAccount
|
connectAccount
|
||||||
@ -105,14 +99,14 @@ export const Transactions = () => {
|
|||||||
const receiverObject = useMemo(() => {
|
const receiverObject = useMemo(() => {
|
||||||
try {
|
try {
|
||||||
const [, prefix] = ss58Decode(receiver)
|
const [, prefix] = ss58Decode(receiver)
|
||||||
if (prefix !== 1995 && prefix !== 1996) {
|
if (prefix !== ss58Format) {
|
||||||
throw new Error("bad prefix")
|
throw new Error("bad prefix")
|
||||||
}
|
}
|
||||||
return { isValid: true, address: receiver }
|
return { isValid: true, address: receiver }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return { isValid: false, address: receiver }
|
return { isValid: false, address: receiver }
|
||||||
}
|
}
|
||||||
}, [receiver])
|
}, [receiver, ss58Format])
|
||||||
|
|
||||||
const calldata = useTransferCalldata(
|
const calldata = useTransferCalldata(
|
||||||
receiverObject.isValid ? receiverObject.address : undefined,
|
receiverObject.isValid ? receiverObject.address : undefined,
|
||||||
@ -160,85 +154,51 @@ export const Transactions = () => {
|
|||||||
localStorage.setItem("defaultTransactAmount", defaultTransactAmount)
|
localStorage.setItem("defaultTransactAmount", defaultTransactAmount)
|
||||||
}, [defaultTransactAmount])
|
}, [defaultTransactAmount])
|
||||||
|
|
||||||
const handleOnTransfer = useCallback(async () => {
|
useEffect(() => {
|
||||||
setIsSubmittingTransaction(true)
|
if (!isSubmittingTransaction) {
|
||||||
setTransactionStatus(undefined)
|
setAmount("")
|
||||||
setError(undefined)
|
}
|
||||||
|
}, [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 ?? "",
|
sender: account?.address ?? "",
|
||||||
receiver: receiverObject?.address ?? "",
|
receiver: receiverObject?.address ?? "",
|
||||||
amount,
|
amount,
|
||||||
timestamp: Math.floor(Date.now() / 1000),
|
timestamp: Math.floor(Date.now() / 1000),
|
||||||
calldata: calldata ?? "",
|
calldata: calldata ?? "",
|
||||||
tokenSymbol: tokenSymbol ?? "",
|
tokenSymbol: tokenSymbol ?? "",
|
||||||
status: "Initiated"
|
status,
|
||||||
|
blockHash: txEvent?.block?.hash ?? "",
|
||||||
|
txHash: txEvent?.txHash ?? "",
|
||||||
|
blockNumber: txEvent?.block?.number
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
setTransactionHistory((prevHistory) => {
|
||||||
const tx = await provider!.createTx(
|
const existingIndex = prevHistory.findIndex(tx => tx.txHash === newTransaction.txHash)
|
||||||
chainId ?? "",
|
if (existingIndex > -1) {
|
||||||
account ? toHex(ss58Decode(account.address)[0]) : "",
|
const updatedHistory = [...prevHistory]
|
||||||
calldata ?? ""
|
updatedHistory[existingIndex] = {
|
||||||
)
|
...updatedHistory[existingIndex],
|
||||||
await lastValueFrom(
|
...newTransaction,
|
||||||
submitTransaction$(clientFull, tx)
|
}
|
||||||
.pipe(
|
return updatedHistory
|
||||||
tap(({ txEvent }) => {
|
} else {
|
||||||
let status: string = ""
|
return [...prevHistory, newTransaction]
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
console.error(err)
|
})
|
||||||
} finally {
|
}
|
||||||
setAmount(defaultTransactAmount ?? "")
|
const handleOnTransfer = () => handleTransaction({ calldata, txName: "transfer", extraLogic })
|
||||||
setIsSubmittingTransaction(false)
|
|
||||||
setTransactionHistory([...transactionHistory, transactStory])
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
provider,
|
|
||||||
chainId,
|
|
||||||
account,
|
|
||||||
amount,
|
|
||||||
calldata,
|
|
||||||
clientFull,
|
|
||||||
defaultTransactAmount,
|
|
||||||
receiverObject?.address,
|
|
||||||
tokenSymbol,
|
|
||||||
transactionHistory
|
|
||||||
])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sm:w-[500px] w-[85%] h-fit flex flex-col flex-1 gap-2 justify-center self-center rounded py-2">
|
<div className="sm:w-[500px] w-[85%] h-fit flex flex-col flex-1 gap-2 justify-center self-center rounded py-2">
|
||||||
@ -304,8 +264,8 @@ export const Transactions = () => {
|
|||||||
<Button
|
<Button
|
||||||
onClick={handleOnTransfer}
|
onClick={handleOnTransfer}
|
||||||
disabled={
|
disabled={
|
||||||
isSubmittingTransaction || !provider || !senderAccount ||
|
isSubmittingTransaction || !senderAccount || !calldata ||
|
||||||
!receiverObject.isValid || !tokenDecimals || !calldata ||
|
!receiverObject.isValid || !tokenDecimals ||
|
||||||
!existentialDeposit || convertedAmount < existentialDeposit ||
|
!existentialDeposit || convertedAmount < existentialDeposit ||
|
||||||
!metadata || senderAccount.data.free < convertedAmount
|
!metadata || senderAccount.data.free < convertedAmount
|
||||||
}
|
}
|
||||||
@ -315,27 +275,7 @@ export const Transactions = () => {
|
|||||||
>
|
>
|
||||||
{isSubmittingTransaction ? "Submitting" : "Transfer"}
|
{isSubmittingTransaction ? "Submitting" : "Transfer"}
|
||||||
</Button>
|
</Button>
|
||||||
{!error && transactionStatus && (
|
{renderStatus()}
|
||||||
<div className="flex flex-col">
|
|
||||||
<p className="text-xs text-accent overflow-hidden whitespace-nowrap text-ellipsis">
|
|
||||||
Transaction status: {`${transactionStatus.status}`}
|
|
||||||
</p>
|
|
||||||
{transactionStatus.hash && (<p className="text-xs text-accent overflow-hidden whitespace-nowrap text-ellipsis">
|
|
||||||
{transactionStatus.hash}
|
|
||||||
</p>)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
)}
|
|
||||||
{!error && !metadata && (
|
|
||||||
<p className="text-xs text-accent overflow-hidden whitespace-nowrap text-ellipsis">
|
|
||||||
Downloading chain metadata...
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{error && (
|
|
||||||
<p className="text-xs text-destructive overflow-hidden whitespace-nowrap text-ellipsis">
|
|
||||||
Error: {error.error}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{activeTab === "history" && (
|
{activeTab === "history" && (
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
export * from "./useSystemAccount"
|
export * from "./useSystemAccount"
|
||||||
export * from "./useUnstableProvider"
|
export * from "./useUnstableProvider"
|
||||||
export * from "./useMetadata"
|
export * from "./useMetadata"
|
||||||
|
export * from "./useTransactionStatusProvider"
|
||||||
export * from "./useChains"
|
export * from "./useChains"
|
||||||
export * from "./useIsMounted"
|
export * from "./useIsMounted"
|
||||||
export * from "./useChainSpecV1"
|
export * from "./useChainSpecV1"
|
||||||
@ -16,3 +17,4 @@ export * from "./useBondedAddress"
|
|||||||
export * from "./useNominations"
|
export * from "./useNominations"
|
||||||
export * from "./useLedger"
|
export * from "./useLedger"
|
||||||
export * from "./usePayee"
|
export * from "./usePayee"
|
||||||
|
export * from "./useSlasingSpans"
|
||||||
|
@ -15,13 +15,41 @@ const AccountId = (value: SS58String) => Enum<
|
|||||||
"Id"
|
"Id"
|
||||||
>("Id", value)
|
>("Id", value)
|
||||||
|
|
||||||
const RewardDestination = () => Enum<
|
export const RewardDestination = () => {
|
||||||
{
|
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: SS58String
|
||||||
|
},
|
||||||
|
"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(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -95,6 +123,28 @@ export const useBondCalldata = (amount: bigint | undefined) => {
|
|||||||
return calldata
|
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[]) => {
|
export const useNominateCalldata = (addresses: string[]) => {
|
||||||
const { client, chainId } = useUnstableProvider()
|
const { client, chainId } = useUnstableProvider()
|
||||||
const metadata = useMetadata()
|
const metadata = useMetadata()
|
||||||
@ -117,3 +167,63 @@ export const useNominateCalldata = (addresses: string[]) => {
|
|||||||
)
|
)
|
||||||
return calldata
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
49
src/hooks/useSlasingSpans.tsx
Normal file
49
src/hooks/useSlasingSpans.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
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 SlashingSpans = {
|
||||||
|
span_index: number,
|
||||||
|
last_start: number,
|
||||||
|
last_nonzero_slash: number,
|
||||||
|
prior: number[],
|
||||||
|
}
|
||||||
|
|
||||||
|
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) as SlashingSpans)
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.subscribe({
|
||||||
|
next(slasingSpans: SlashingSpans) {
|
||||||
|
next(null, slasingSpans)
|
||||||
|
},
|
||||||
|
error: next,
|
||||||
|
})
|
||||||
|
return () => subscription.unsubscribe()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return slasingSpans
|
||||||
|
}
|
145
src/hooks/useTransactionStatusProvider.tsx
Normal file
145
src/hooks/useTransactionStatusProvider.tsx
Normal file
@ -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<void>;
|
||||||
|
renderStatus: () => JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TransactionStatusProvider = createContext<Context>(null!)
|
||||||
|
export const useTransactionStatusProvider = () => useContext(TransactionStatusProvider)
|
||||||
|
|
||||||
|
export const TransactionStatusProviderProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
const [isSubmittingTransaction, setIsSubmittingTransaction] = useState<boolean>(false)
|
||||||
|
const [transactionStatus, setTransactionStatus] = useState<FollowTransaction | undefined>()
|
||||||
|
const [error, setError] = useState<TransactionError | undefined>()
|
||||||
|
|
||||||
|
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 && (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<p className="text-xs text-accent overflow-hidden whitespace-nowrap text-ellipsis">
|
||||||
|
Transaction status: {`${transactionStatus.status}`}
|
||||||
|
</p>
|
||||||
|
{transactionStatus.hash && (<p className="text-xs text-accent overflow-hidden whitespace-nowrap text-ellipsis">
|
||||||
|
{transactionStatus.hash}
|
||||||
|
</p>)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
)}
|
||||||
|
{!error && !metadata && (
|
||||||
|
<p className="text-xs text-accent overflow-hidden whitespace-nowrap text-ellipsis">
|
||||||
|
Downloading chain metadata...
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{error && (
|
||||||
|
<p className="text-xs text-destructive overflow-hidden whitespace-nowrap text-ellipsis">
|
||||||
|
Error: {error.error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TransactionStatusProvider.Provider
|
||||||
|
value={{
|
||||||
|
isSubmittingTransaction,
|
||||||
|
transactionStatus,
|
||||||
|
error,
|
||||||
|
setIsSubmittingTransaction,
|
||||||
|
setTransactionStatus,
|
||||||
|
setError,
|
||||||
|
handleTransaction,
|
||||||
|
renderStatus
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</TransactionStatusProvider.Provider>
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user