diff --git a/.github/assets/tangle-banner.png b/.github/assets/tangle-banner.png
new file mode 100644
index 0000000000..9347c3b386
Binary files /dev/null and b/.github/assets/tangle-banner.png differ
diff --git a/.github/workflows/check-build.yml b/.github/workflows/check-build.yml
index 110c863930..1839714e9b 100644
--- a/.github/workflows/check-build.yml
+++ b/.github/workflows/check-build.yml
@@ -20,7 +20,7 @@ jobs:
uses: styfle/cancel-workflow-action@0.12.1
with:
access_token: ${{ secrets.GITHUB_TOKEN }}
- - uses: actions/checkout@v4.1.4
+ - uses: actions/checkout@v4.1.6
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
diff --git a/.github/workflows/check-lint.yml b/.github/workflows/check-lint.yml
index de27b4b019..f48a6c43dd 100644
--- a/.github/workflows/check-lint.yml
+++ b/.github/workflows/check-lint.yml
@@ -18,7 +18,7 @@ jobs:
uses: styfle/cancel-workflow-action@0.12.1
with:
access_token: ${{ secrets.GITHUB_TOKEN }}
- - uses: actions/checkout@v4.1.4
+ - uses: actions/checkout@v4.1.6
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
@@ -40,7 +40,7 @@ jobs:
uses: styfle/cancel-workflow-action@0.12.1
with:
access_token: ${{ secrets.GITHUB_TOKEN }}
- - uses: actions/checkout@v4.1.4
+ - uses: actions/checkout@v4.1.6
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
@@ -58,7 +58,7 @@ jobs:
uses: styfle/cancel-workflow-action@0.12.1
with:
access_token: ${{ secrets.GITHUB_TOKEN }}
- - uses: actions/checkout@v4.1.4
+ - uses: actions/checkout@v4.1.6
- name: Link Checker
uses: lycheeverse/lychee-action@v1.10.0
with:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index eb6ad03534..92ee24823b 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -33,7 +33,7 @@ jobs:
with:
access_token: ${{ github.token }}
- name: Checkout repository
- uses: actions/checkout@v4.1.4
+ uses: actions/checkout@v4.1.6
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
diff --git a/.github/workflows/deploy-bridge-dapp-dev.yml b/.github/workflows/deploy-bridge-dapp-dev.yml
index 73599f92bd..51395bd014 100644
--- a/.github/workflows/deploy-bridge-dapp-dev.yml
+++ b/.github/workflows/deploy-bridge-dapp-dev.yml
@@ -24,7 +24,7 @@ jobs:
uses: styfle/cancel-workflow-action@0.12.1
with:
access_token: ${{ github.token }}
- - uses: actions/checkout@v4.1.4
+ - uses: actions/checkout@v4.1.6
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
diff --git a/.github/workflows/deploy-faucet.yml b/.github/workflows/deploy-faucet.yml
index 08817ef3e8..cf7167ff0e 100644
--- a/.github/workflows/deploy-faucet.yml
+++ b/.github/workflows/deploy-faucet.yml
@@ -29,7 +29,7 @@ jobs:
with:
access_token: ${{ secrets.GITHUB_TOKEN }}
- - uses: actions/checkout@v4.1.4
+ - uses: actions/checkout@v4.1.6
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
diff --git a/.github/workflows/deploy-stats-dapp-dev.yml b/.github/workflows/deploy-stats-dapp-dev.yml
index 6145132e50..85660b481c 100644
--- a/.github/workflows/deploy-stats-dapp-dev.yml
+++ b/.github/workflows/deploy-stats-dapp-dev.yml
@@ -25,7 +25,7 @@ jobs:
uses: styfle/cancel-workflow-action@0.12.1
with:
access_token: ${{ secrets.GITHUB_TOKEN }}
- - uses: actions/checkout@v4.1.4
+ - uses: actions/checkout@v4.1.6
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
diff --git a/.github/workflows/deploy-storybook-docs.yml b/.github/workflows/deploy-storybook-docs.yml
index 5053e5d73e..5c9a4fb75d 100644
--- a/.github/workflows/deploy-storybook-docs.yml
+++ b/.github/workflows/deploy-storybook-docs.yml
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
- uses: actions/checkout@v4.1.4
+ uses: actions/checkout@v4.1.6
with:
persist-credentials: false
- name: Install and Build 🔧
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 0222bd6b25..cdf9f378a0 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
steps:
#Check out
- - uses: actions/checkout@v4.1.4
+ - uses: actions/checkout@v4.1.6
with:
fetch-depth: 100
diff --git a/.github/workflows/ui-review.yml b/.github/workflows/ui-review.yml
index 1bbb11333e..7059edba32 100644
--- a/.github/workflows/ui-review.yml
+++ b/.github/workflows/ui-review.yml
@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
# Job steps
steps:
- - uses: actions/checkout@v4.1.4
+ - uses: actions/checkout@v4.1.6
with:
fetch-depth: 0
- name: Install dependencies
diff --git a/.husky/commit-msg b/.husky/commit-msg
old mode 100755
new mode 100644
diff --git a/.husky/pre-push b/.husky/pre-push
old mode 100755
new mode 100644
diff --git a/apps/bridge-dapp/src/components/Header/ActiveChainDropdown.tsx b/apps/bridge-dapp/src/components/Header/ActiveChainDropdown.tsx
index a7ffdbd41a..50344559e4 100644
--- a/apps/bridge-dapp/src/components/Header/ActiveChainDropdown.tsx
+++ b/apps/bridge-dapp/src/components/Header/ActiveChainDropdown.tsx
@@ -1,4 +1,4 @@
-import { DropdownMenuTrigger as DropdownButton } from '@radix-ui/react-dropdown-menu';
+import { DropdownMenuTrigger } from '@radix-ui/react-dropdown-menu';
import {
useWebContext,
useConnectWallet,
@@ -14,7 +14,7 @@ import {
} from '@webb-tools/webb-ui-components/components/Dropdown';
import { MenuItem } from '@webb-tools/webb-ui-components/components/MenuItem';
import { ScrollArea } from '@webb-tools/webb-ui-components/components/ScrollArea';
-import ChainButtonCmp from '@webb-tools/webb-ui-components/components/buttons/ChainButton';
+import ChainOrTokenButton from '@webb-tools/webb-ui-components/components/buttons/ChainOrTokenButton';
import { useWebbUI } from '@webb-tools/webb-ui-components/hooks/useWebbUI';
import { useCallback, useMemo } from 'react';
import useChainsFromRoute from '../../hooks/useChainsFromRoute';
@@ -64,14 +64,15 @@ const ActiveChainDropdown = () => {
return (
-
-
+
-
+
diff --git a/apps/bridge-dapp/src/pages/Account/AccountSummaryCard.tsx b/apps/bridge-dapp/src/pages/Account/AccountSummaryCard.tsx
index bb1b5cb1c7..c4bef04f1c 100644
--- a/apps/bridge-dapp/src/pages/Account/AccountSummaryCard.tsx
+++ b/apps/bridge-dapp/src/pages/Account/AccountSummaryCard.tsx
@@ -1,4 +1,4 @@
-import { DropdownMenuTrigger as DropdownButton } from '@radix-ui/react-dropdown-menu';
+import { DropdownMenuTrigger } from '@radix-ui/react-dropdown-menu';
import { useWebContext } from '@webb-tools/api-provider-environment';
import { ZERO_BIG_INT } from '@webb-tools/dapp-config/constants';
import ArrowLeftRightLineIcon from '@webb-tools/icons/ArrowLeftRightLineIcon';
@@ -174,7 +174,7 @@ function TotalShieldedBalance() {
-
-
+
diff --git a/apps/bridge-dapp/src/pages/Hubble/Bridge/SelectChain.tsx b/apps/bridge-dapp/src/pages/Hubble/Bridge/SelectChain.tsx
index e7d3c43887..62c3247e58 100644
--- a/apps/bridge-dapp/src/pages/Hubble/Bridge/SelectChain.tsx
+++ b/apps/bridge-dapp/src/pages/Hubble/Bridge/SelectChain.tsx
@@ -128,9 +128,11 @@ const useChains = (
return [];
}
- return Object.keys(anchorRec).map((typedChainId) => {
- return apiConfig.chains[parseInt(typedChainId)];
- });
+ return Object.keys(anchorRec)
+ .map((typedChainId) => {
+ return apiConfig.chains[parseInt(typedChainId)];
+ })
+ .filter(Boolean);
};
/**
diff --git a/apps/hubble-stats/README.md b/apps/hubble-stats/README.md
index a7694ca244..9ef4b0f42b 100644
--- a/apps/hubble-stats/README.md
+++ b/apps/hubble-stats/README.md
@@ -14,6 +14,36 @@
-## Run the Hubble Stats
+## Run Hubble Stats
-TBD
+Once the development environment is set up, you may proceed to install the required dependencies and run the dapp locally.
+
+1. Clone this repo
+
+ ```bash
+ git clone git@github.com:webb-tools/webb-dapp.git
+ ```
+
+2. Install dependencies by `yarn`
+
+ ```bash
+ yarn install
+ ```
+
+3. Start the dApp by the following command:
+
+ ```bash
+ yarn nx serve hubble-stats
+ ```
+
+Visit `http://localhost:4200/` to see the Hubble Stats app!
+
+Happy hacking!
+
+ Need help?
+
+If you need help or you want to additional information please:
+
+- Refer to the [Webb Official Documentation](https://docs.webb.tools/).
+- If you have feedback on how to improve the Webb Dapp interface or you have a specific question? Check out the [Webb Dapp Feedback Discussion](https://github.com/webb-tools/feedback/discussions/categories/webb-dapp-feedback).
+- If you found a bug please [open an issue](https://github.com/webb-tools/webb-dapp/issues/new/choose) or [join our Discord](https://discord.gg/jUDeFpggrR) server to report it.
diff --git a/apps/tangle-dapp/CHANGELOG.md b/apps/tangle-dapp/CHANGELOG.md
index e485370ca7..c5f4a6876d 100644
--- a/apps/tangle-dapp/CHANGELOG.md
+++ b/apps/tangle-dapp/CHANGELOG.md
@@ -211,3 +211,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fix cursor moves to the end when changing value in input - https://github.com/webb-tools/webb-dapp/pull/2234.
- UI improvements on Tangle Dapp (Key Stats Item + Fix Footer bottom bound + Static Tangle Icon) - https://github.com/webb-tools/webb-dapp/pull/2256.
- Fix account explorer link in wallet dropdown - https://github.com/webb-tools/webb-dapp/pull/2261.
+
+## [0.0.9] - 2024-05-27
+
+### Added
+
+- Add # of Active Services and Restake Amount to Validators Table in Nomination - https://github.com/webb-tools/webb-dapp/pull/2283
+- Backend Integration for Service Info Card and Participants table - https://github.com/webb-tools/webb-dapp/pull/2285
+- Added Loading Account Page - https://github.com/webb-tools/webb-dapp/pull/2300
+- Get Permitted Caller for Tangle Dapp - https://github.com/webb-tools/webb-dapp/pull/2303
+- Integrate backend on Active Service table on Overview page - https://github.com/webb-tools/webb-dapp/pull/2304
+- Add new Bridge page to the site with current functionalities of Bridge Container: select and switch Source & Destination Chain - https://github.com/webb-tools/webb-dapp/pull/2307
+- Switch chains in EVM wallets when switching networks - https://github.com/webb-tools/webb-dapp/pull/2311
+- Update Substrate wallet metadata when appropriate - https://github.com/webb-tools/webb-dapp/pull/2314
+- Added payouts loading state - https://github.com/webb-tools/webb-dapp/pull/2315
+- Show longest vesting schedule info - https://github.com/webb-tools/webb-dapp/pull/2324
+- Setup the code logic to handle different scenarios of bridging - https://github.com/webb-tools/webb-dapp/pull/2329
+
+### Changed
+
+- Update Chip for Restaking Service to normal case - https://github.com/webb-tools/webb-dapp/pull/2282
+- Font updated from Breeze Sans to Satoshi - https://github.com/webb-tools/webb-dapp/pull/2299
+- Transaction notification updates and Update HiddenValue to always display \*\*\*\* to increase privacy for users - https://github.com/webb-tools/webb-dapp/pull/2317
+- Update OpenGraph metadata and images - https://github.com/webb-tools/webb-dapp/pull/2323
+- README updated - https://github.com/webb-tools/webb-dapp/pull/2335
+
+### Fixed
+
+- Improve Balance Display - https://github.com/webb-tools/webb-dapp/pull/2289
+- Resolve Maximum Nomination Amount Error - https://github.com/webb-tools/webb-dapp/pull/2290
+- Slow Rendering of Checkboxes When Clicked - https://github.com/webb-tools/webb-dapp/pull/2291
+- Adjusted the dropdown body to be scrollable on webb-ui-kit, fixed the wrong theme icon and updated the background color of the theme switcher to improve the visual on the sidebar, show the correct number of active nominators on the nomination page, show cached stats value (if existed) instead of loading animation on the nomination page and show the correct staked amounts on the validators table - https://github.com/webb-tools/webb-dapp/pull/2294
+- Fixed total unclaimed payouts rewards bug and actual staked percentage bug - https://github.com/webb-tools/webb-dapp/pull/2335
diff --git a/apps/tangle-dapp/README.md b/apps/tangle-dapp/README.md
index f6a4682b4b..839faddf5b 100644
--- a/apps/tangle-dapp/README.md
+++ b/apps/tangle-dapp/README.md
@@ -1,19 +1,49 @@
-# Tangle dApp - EVM Staking UI
+# Tangle dApp
- EVM Staking UI
+ An interface for nominating validators on the Tangle network, managing account, bridging TNT, and more.
-## Run the Tangle dApp
+## Run Tangle dApp
-TBD
+Once the development environment is set up, you may proceed to install the required dependencies and run the dapp locally.
+
+1. Clone this repo
+
+ ```bash
+ git clone git@github.com:webb-tools/webb-dapp.git
+ ```
+
+2. Install dependencies by `yarn`
+
+ ```bash
+ yarn install
+ ```
+⚠️ **REQUIRED:** Node.js version >= v18.19.0
+
+3. Start the dApp by the following command:
+
+ ```bash
+ yarn nx serve tangle-dapp
+ ```
+
+Visit `http://localhost:4200/` to see the Tangle dApp!
+
+Happy hacking!
+
+ Need help?
+
+If you need help or you want to additional information please:
+
+- Refer to the [Tangle Network Official Documentation](https://docs.tangle.tools/).
+- If you have feedback on how to improve the Webb Dapp interface or you have a specific question? Check out the [Webb Dapp Feedback Discussion](https://github.com/webb-tools/feedback/discussions/categories/webb-dapp-feedback).
+- If you found a bug please [open an issue](https://github.com/webb-tools/webb-dapp/issues/new/choose) or [join our Discord](https://discord.gg/jUDeFpggrR) server to report it.
diff --git a/apps/tangle-dapp/app/bridge/AmountAndTokenInput.tsx b/apps/tangle-dapp/app/bridge/AmountAndTokenInput.tsx
new file mode 100644
index 0000000000..fd331842ce
--- /dev/null
+++ b/apps/tangle-dapp/app/bridge/AmountAndTokenInput.tsx
@@ -0,0 +1,74 @@
+'use client';
+
+import { DropdownMenuTrigger as DropdownTrigger } from '@radix-ui/react-dropdown-menu';
+import { TokenIcon } from '@webb-tools/icons/TokenIcon';
+import ChainOrTokenButton from '@webb-tools/webb-ui-components/components/buttons/ChainOrTokenButton';
+import {
+ Dropdown,
+ DropdownBody,
+} from '@webb-tools/webb-ui-components/components/Dropdown';
+import { MenuItem } from '@webb-tools/webb-ui-components/components/MenuItem';
+import { ScrollArea } from '@webb-tools/webb-ui-components/components/ScrollArea';
+import { FC } from 'react';
+
+import AmountInput from '../../components/AmountInput/AmountInput';
+import { BRIDGE_SUPPORTED_TOKENS } from '../../constants/bridge';
+import { useBridge } from '../../context/BridgeContext';
+
+const AmountAndTokenInput: FC = () => {
+ const {
+ amount,
+ setAmount,
+ selectedTokenId,
+ setSelectedTokenId,
+ tokenIdOptions,
+ } = useBridge();
+
+ return (
+
+
+
+
+
+
+
+
+
+ {tokenIdOptions.map((tokenId) => {
+ const token = BRIDGE_SUPPORTED_TOKENS[tokenId];
+ return (
+
+ }
+ onSelect={() => setSelectedTokenId(tokenId)}
+ className="px-3 normal-case"
+ >
+ {token.symbol}
+
+
+ );
+ })}
+
+
+
+
+
+ );
+};
+
+export default AmountAndTokenInput;
diff --git a/apps/tangle-dapp/app/bridge/BridgeContainer.tsx b/apps/tangle-dapp/app/bridge/BridgeContainer.tsx
new file mode 100644
index 0000000000..996bc3a9ad
--- /dev/null
+++ b/apps/tangle-dapp/app/bridge/BridgeContainer.tsx
@@ -0,0 +1,64 @@
+'use client';
+
+import Button from '@webb-tools/webb-ui-components/components/buttons/Button';
+import { FC } from 'react';
+import { twMerge } from 'tailwind-merge';
+
+import AddressInput, {
+ AddressType,
+} from '../../components/AddressInput/AddressInput';
+import { useBridge } from '../../context/BridgeContext';
+import AmountAndTokenInput from './AmountAndTokenInput';
+import ChainSelectors from './ChainSelectors';
+import useActionButton from './useActionButton';
+
+interface BridgeContainerProps {
+ className?: string;
+}
+
+const BridgeContainer: FC = ({ className }) => {
+ const { destinationAddress, setDestinationAddress } = useBridge();
+ const { buttonAction, buttonText, isLoading } = useActionButton();
+
+ return (
+
+
+
+
+
+
+
+
+
+ {/* TODO: Tx Info (Fees & Estimated Time) */}
+
+
+ {buttonText}
+
+
+
+ );
+};
+
+export default BridgeContainer;
diff --git a/apps/tangle-dapp/app/bridge/ChainSelectors.tsx b/apps/tangle-dapp/app/bridge/ChainSelectors.tsx
new file mode 100644
index 0000000000..0c78df4f75
--- /dev/null
+++ b/apps/tangle-dapp/app/bridge/ChainSelectors.tsx
@@ -0,0 +1,142 @@
+'use client';
+
+import { DropdownMenuTrigger as DropdownTrigger } from '@radix-ui/react-dropdown-menu';
+import { ChainConfig } from '@webb-tools/dapp-config/chains/chain-config.interface';
+import { ArrowRight } from '@webb-tools/icons/ArrowRight';
+import { ChainIcon } from '@webb-tools/icons/ChainIcon';
+import { calculateTypedChainId } from '@webb-tools/sdk-core/typed-chain-id';
+import ChainOrTokenButton from '@webb-tools/webb-ui-components/components/buttons/ChainOrTokenButton';
+import {
+ Dropdown,
+ DropdownBody,
+} from '@webb-tools/webb-ui-components/components/Dropdown';
+import { MenuItem } from '@webb-tools/webb-ui-components/components/MenuItem';
+import { ScrollArea } from '@webb-tools/webb-ui-components/components/ScrollArea';
+import assert from 'assert';
+import { FC, useCallback } from 'react';
+
+import { BRIDGE } from '../../constants/bridge';
+import { useBridge } from '../../context/BridgeContext';
+
+interface ChainSelectorProps {
+ selectedChain: ChainConfig;
+ chainOptions: ChainConfig[];
+ onSelectChain: (chain: ChainConfig) => void;
+ className?: string;
+}
+
+const ChainSelectors: FC = () => {
+ const {
+ selectedSourceChain,
+ setSelectedSourceChain,
+ selectedDestinationChain,
+ setSelectedDestinationChain,
+ sourceChainOptions,
+ destinationChainOptions,
+ } = useBridge();
+
+ const onSwitchChains = useCallback(() => {
+ const newSelectedDestinationChain = selectedSourceChain;
+ const newSelectedSourceChain = selectedDestinationChain;
+
+ assert(
+ sourceChainOptions.find(
+ (chain) =>
+ calculateTypedChainId(chain.chainType, chain.id) ===
+ calculateTypedChainId(
+ newSelectedSourceChain.chainType,
+ newSelectedSourceChain.id
+ )
+ ) !== undefined,
+ 'New source chain is not available in source chain options when switching chains'
+ );
+ setSelectedSourceChain(newSelectedSourceChain);
+
+ const newDestinationChainOptions =
+ BRIDGE[
+ calculateTypedChainId(
+ newSelectedSourceChain.chainType,
+ newSelectedSourceChain.id
+ )
+ ];
+ const newDestinationChainPresetTypedChainId = calculateTypedChainId(
+ newSelectedDestinationChain.chainType,
+ newSelectedDestinationChain.id
+ );
+ assert(
+ newDestinationChainPresetTypedChainId in newDestinationChainOptions,
+ 'New destination chain is not available in destination chain options when switching chains'
+ );
+ setSelectedDestinationChain(newSelectedDestinationChain);
+ }, [
+ setSelectedSourceChain,
+ setSelectedDestinationChain,
+ selectedDestinationChain,
+ selectedSourceChain,
+ sourceChainOptions,
+ ]);
+
+ return (
+
+ );
+};
+
+const ChainSelector: FC = ({
+ selectedChain,
+ chainOptions,
+ onSelectChain,
+ className,
+}) => {
+ return (
+
+
+
+
+
+
+
+ {chainOptions.map((chain) => {
+ return (
+
+ }
+ onSelect={() => onSelectChain(chain)}
+ >
+ {chain.name}
+
+
+ );
+ })}
+
+
+
+
+ );
+};
+
+export default ChainSelectors;
diff --git a/apps/tangle-dapp/app/bridge/hooks/useBridgeType.ts b/apps/tangle-dapp/app/bridge/hooks/useBridgeType.ts
new file mode 100644
index 0000000000..d7bb97ee5c
--- /dev/null
+++ b/apps/tangle-dapp/app/bridge/hooks/useBridgeType.ts
@@ -0,0 +1,59 @@
+'use client';
+
+import { ChainType } from '@webb-tools/sdk-core/typed-chain-id';
+
+import { useBridge } from '../../../context/BridgeContext';
+import { BridgeType } from '../../../types/bridge';
+
+export default function useBridgeType() {
+ const { selectedSourceChain, selectedDestinationChain } = useBridge();
+
+ // EVM to EVM
+ if (
+ isEVMChain(selectedSourceChain.chainType) &&
+ isEVMChain(selectedDestinationChain.chainType)
+ ) {
+ return BridgeType.SYGMA_EVM_TO_EVM;
+ }
+
+ // EVM to Substrate
+ if (
+ isEVMChain(selectedSourceChain.chainType) &&
+ isSubstrateChain(selectedDestinationChain.chainType)
+ ) {
+ return BridgeType.SYGMA_EVM_TO_SUBSTRATE;
+ }
+
+ // Substrate to EVM
+ if (
+ isSubstrateChain(selectedSourceChain.chainType) &&
+ isEVMChain(selectedDestinationChain.chainType)
+ ) {
+ return BridgeType.SYGMA_SUBSTRATE_TO_EVM;
+ }
+
+ // Substrate to Substrate
+ if (
+ isSubstrateChain(selectedSourceChain.chainType) &&
+ isSubstrateChain(selectedDestinationChain.chainType)
+ ) {
+ return BridgeType.SYGMA_SUBSTRATE_TO_SUBSTRATE;
+ }
+
+ throw new Error('Unsupported bridge type');
+}
+
+function isSubstrateChain(chainType: ChainType) {
+ return (
+ chainType === ChainType.Substrate ||
+ chainType === ChainType.SubstrateDevelopment ||
+ chainType === ChainType.PolkadotRelayChain ||
+ chainType === ChainType.KusamaRelayChain ||
+ chainType === ChainType.PolkadotParachain ||
+ chainType === ChainType.KusamaParachain
+ );
+}
+
+function isEVMChain(chainType: ChainType) {
+ return chainType === ChainType.EVM;
+}
diff --git a/apps/tangle-dapp/app/bridge/layout.tsx b/apps/tangle-dapp/app/bridge/layout.tsx
new file mode 100644
index 0000000000..be0bbdd8af
--- /dev/null
+++ b/apps/tangle-dapp/app/bridge/layout.tsx
@@ -0,0 +1,9 @@
+import { FC, PropsWithChildren } from 'react';
+
+import BridgeProvider from '../../context/BridgeContext';
+
+const BridgeLayout: FC = ({ children }) => {
+ return {children} ;
+};
+
+export default BridgeLayout;
diff --git a/apps/tangle-dapp/app/bridge/page.tsx b/apps/tangle-dapp/app/bridge/page.tsx
new file mode 100644
index 0000000000..aa2ef0e921
--- /dev/null
+++ b/apps/tangle-dapp/app/bridge/page.tsx
@@ -0,0 +1,19 @@
+import { Metadata } from 'next';
+import { FC } from 'react';
+
+import createPageMetadata from '../../utils/createPageMetadata';
+import BridgeContainer from './BridgeContainer';
+
+export const metadata: Metadata = createPageMetadata({
+ title: 'Bridge',
+});
+
+const Bridge: FC = () => {
+ return (
+
+
+
+ );
+};
+
+export default Bridge;
diff --git a/apps/tangle-dapp/app/bridge/useActionButton.tsx b/apps/tangle-dapp/app/bridge/useActionButton.tsx
new file mode 100644
index 0000000000..fa9f820664
--- /dev/null
+++ b/apps/tangle-dapp/app/bridge/useActionButton.tsx
@@ -0,0 +1,32 @@
+'use client';
+
+import {
+ useConnectWallet,
+ useWebContext,
+} from '@webb-tools/api-provider-environment';
+import { useCallback, useMemo } from 'react';
+
+export default function useActionButton() {
+ const { activeAccount, activeWallet, loading, isConnecting } =
+ useWebContext();
+
+ const { toggleModal } = useConnectWallet();
+
+ const noActiveAccountOrWallet = useMemo(() => {
+ return !activeAccount || !activeWallet;
+ }, [activeAccount, activeWallet]);
+
+ const openWalletModal = useCallback(() => {
+ toggleModal(true);
+ }, [toggleModal]);
+
+ const bridgeTx = useCallback(() => {
+ // TODO: handle bridge Tx for each case from the source and destination chain
+ }, []);
+
+ return {
+ isLoading: loading || isConnecting,
+ buttonAction: noActiveAccountOrWallet ? openWalletModal : bridgeTx,
+ buttonText: noActiveAccountOrWallet ? 'Connect' : 'Approve',
+ };
+}
diff --git a/apps/tangle-dapp/app/claim/EligibleSection.tsx b/apps/tangle-dapp/app/claim/EligibleSection.tsx
index f6fc4e067b..a37a750424 100644
--- a/apps/tangle-dapp/app/claim/EligibleSection.tsx
+++ b/apps/tangle-dapp/app/claim/EligibleSection.tsx
@@ -28,7 +28,7 @@ import ClaimRecipientInput from '../../components/claims/ClaimRecipientInput';
import useNetworkStore from '../../context/useNetworkStore';
import toAsciiHex from '../../utils/claims/toAsciiHex';
import getStatement, { Statement } from '../../utils/getStatement';
-import { getPolkadotApiPromise } from '../../utils/polkadot';
+import { getApiPromise } from '../../utils/polkadot';
import { formatTokenBalance } from '../../utils/polkadot/tokens';
import type { ClaimInfoType } from './types';
@@ -82,7 +82,7 @@ const EligibleSection: FC = ({
useEffect(() => {
const fetchStatement = async () => {
try {
- const api = await getPolkadotApiPromise(rpcEndpoint);
+ const api = await getApiPromise(rpcEndpoint);
const systemChain = await api.rpc.system.chain();
const statement = getStatement(
systemChain.toHuman(),
@@ -122,7 +122,7 @@ const EligibleSection: FC = ({
setIsClaiming(true);
setStep(Step.SIGN);
- const api = await getPolkadotApiPromise(rpcEndpoint);
+ const api = await getApiPromise(rpcEndpoint);
const accountId = activeAccount.address;
const isEvmRecipient = isEthereumAddress(recipient);
const isEvmSigner = isEthereumAddress(accountId);
@@ -143,6 +143,7 @@ const EligibleSection: FC = ({
setStep(Step.SENDING_TX);
+ // TODO: This needs to be changed to use the new hooks.
const tx = api.tx.claims.claimAttest(
isEvmRecipient ? { EVM: recipient } : { Native: recipient }, // destAccount
isEvmSigner ? { EVM: accountId } : { Native: accountId }, // signer
@@ -337,7 +338,8 @@ function preparePayload(
function sendTransaction(
tx: SubmittableExtrinsic<'promise', ISubmittableResult>
) {
- console.log(`Sending transaction with args ${tx.args.toString()}`);
+ console.debug(`Sending transaction with args ${tx.args.toString()}`);
+
return new Promise((resolve, reject) => {
tx.send(async (result) => {
const status = result.status;
diff --git a/apps/tangle-dapp/app/claim/page.tsx b/apps/tangle-dapp/app/claim/page.tsx
index 9d1e7d0972..635e80750c 100644
--- a/apps/tangle-dapp/app/claim/page.tsx
+++ b/apps/tangle-dapp/app/claim/page.tsx
@@ -14,7 +14,7 @@ import { useEffect, useMemo, useState } from 'react';
import { combineLatest, Subscription } from 'rxjs';
import useNetworkStore from '../../context/useNetworkStore';
-import { getPolkadotApiRx } from '../../utils/polkadot';
+import { getApiRx } from '../../utils/polkadot';
import EligibleSection from './EligibleSection';
import NotEligibleSection from './NotEligibleSection';
import type { ClaimInfoType } from './types';
@@ -63,7 +63,7 @@ export default function ClaimPage() {
const fetchClaimData = async () => {
try {
- const apiRx = await getPolkadotApiRx(rpcEndpoint);
+ const apiRx = await getApiRx(rpcEndpoint);
const params = isEthereumAddress(accountAddress)
? { EVM: accountAddress }
diff --git a/apps/tangle-dapp/app/claim/success/page.tsx b/apps/tangle-dapp/app/claim/success/page.tsx
index 100b500502..c1c0f21eec 100644
--- a/apps/tangle-dapp/app/claim/success/page.tsx
+++ b/apps/tangle-dapp/app/claim/success/page.tsx
@@ -2,11 +2,11 @@ import { isHex } from '@polkadot/util';
import { redirect } from 'next/navigation';
import { PagePath } from '../../../types';
-import { getPolkadotApiPromise } from '../../../utils/polkadot';
+import { getApiPromise } from '../../../utils/polkadot';
import SuccessClient from './SuccessClient';
const isBlockHashExistOnChain = async (
- api: NonNullable>>,
+ api: NonNullable>>,
blockHash: string
) => {
try {
@@ -30,7 +30,7 @@ const Page = async ({
return redirect(PagePath.CLAIM_AIRDROP);
}
- const api = await getPolkadotApiPromise(rpcEndpoint);
+ const api = await getApiPromise(rpcEndpoint);
const isValidBlockHash =
typeof blockHash === 'string' &&
diff --git a/apps/tangle-dapp/app/loading.tsx b/apps/tangle-dapp/app/loading.tsx
new file mode 100644
index 0000000000..375787dca0
--- /dev/null
+++ b/apps/tangle-dapp/app/loading.tsx
@@ -0,0 +1,23 @@
+import SkeletonLoader from '@webb-tools/webb-ui-components/components/SkeletonLoader';
+import { Typography } from '@webb-tools/webb-ui-components/typography/Typography/Typography';
+import type { FC } from 'react';
+
+const LoadingPage: FC = () => {
+ return (
+
+
+
+
+
+
+
+
+ Balances
+
+
+
+
+ );
+};
+
+export default LoadingPage;
diff --git a/apps/tangle-dapp/app/nomination/[validatorAddress]/ValidatorBasicInfoCard.tsx b/apps/tangle-dapp/app/nomination/[validatorAddress]/InfoCard.tsx
similarity index 52%
rename from apps/tangle-dapp/app/nomination/[validatorAddress]/ValidatorBasicInfoCard.tsx
rename to apps/tangle-dapp/app/nomination/[validatorAddress]/InfoCard.tsx
index 72a0845de1..a1ebdf6e9a 100644
--- a/apps/tangle-dapp/app/nomination/[validatorAddress]/ValidatorBasicInfoCard.tsx
+++ b/apps/tangle-dapp/app/nomination/[validatorAddress]/InfoCard.tsx
@@ -9,36 +9,27 @@ import {
Typography,
} from '@webb-tools/webb-ui-components';
import { shortenString } from '@webb-tools/webb-ui-components/utils/shortenString';
-import { FC, useEffect, useMemo, useState } from 'react';
+import { FC } from 'react';
import { twMerge } from 'tailwind-merge';
import { SocialChip, TangleCard } from '../../../components';
import useNetworkStore from '../../../context/useNetworkStore';
-import useRestakingRoleLedger from '../../../data/restaking/useRestakingRoleLedger';
-import useCurrentEra from '../../../data/staking/useCurrentEra';
+import useValidatorInfoCard from '../../../data/validatorDetails/useValidatorInfoCard';
+import useFormatNativeTokenAmount from '../../../hooks/useFormatNativeTokenAmount';
import { ExplorerType } from '../../../types';
-import {
- extractDataFromIdentityInfo,
- formatTokenBalance,
- IdentityDataType,
-} from '../../../utils/polkadot';
-import { getPolkadotApiPromise } from '../../../utils/polkadot/api';
-import {
- getProfileTypeFromRestakeRoleLedger,
- getTotalRestakedFromRestakeRoleLedger,
-} from '../../../utils/polkadot/restake';
import ValueSkeleton from './ValueSkeleton';
-interface ValidatorBasicInfoCardProps {
+interface InfoCardProps {
validatorAddress: string;
className?: string;
}
-const ValidatorBasicInfoCard: FC = ({
+const InfoCard: FC = ({
validatorAddress,
className,
-}: ValidatorBasicInfoCardProps) => {
- const { network, nativeTokenSymbol, rpcEndpoint } = useNetworkStore();
+}: InfoCardProps) => {
+ const { network, rpcEndpoint } = useNetworkStore();
+ const formatNativeTokenAmount = useFormatNativeTokenAmount();
const {
name,
@@ -50,7 +41,7 @@ const ValidatorBasicInfoCard: FC = ({
email,
web,
isLoading,
- } = useValidatorBasicInfo(rpcEndpoint, validatorAddress);
+ } = useValidatorInfoCard(rpcEndpoint, validatorAddress);
return (
@@ -123,7 +114,7 @@ const ValidatorBasicInfoCard: FC = ({
className="whitespace-nowrap"
>
{totalRestaked
- ? formatTokenBalance(totalRestaked, nativeTokenSymbol)
+ ? formatNativeTokenAmount(totalRestaked)
: '--'}
)}
@@ -162,99 +153,4 @@ const ValidatorBasicInfoCard: FC = ({
);
};
-export default ValidatorBasicInfoCard;
-
-function useValidatorBasicInfo(rpcEndpoint: string, validatorAddress: string) {
- const { data: currentEra } = useCurrentEra();
- const { data: ledgerOpt, isLoading: isLoadingLedgerOpt } =
- useRestakingRoleLedger(validatorAddress);
-
- const [name, setName] = useState(null);
- const [email, setEmail] = useState(null);
- const [web, setWeb] = useState(null);
- const [twitter, setTwitter] = useState(null);
- const [nominations, setNominations] = useState(null);
- const [isActive, setIsActive] = useState(null);
- const [isLoading, setIsLoading] = useState(true);
-
- const restakingMethod = useMemo(
- () => getProfileTypeFromRestakeRoleLedger(ledgerOpt),
- [ledgerOpt]
- );
-
- const totalRestaked = useMemo(
- () => getTotalRestakedFromRestakeRoleLedger(ledgerOpt),
- [ledgerOpt]
- );
-
- useEffect(() => {
- const fetchData = async () => {
- const api = await getPolkadotApiPromise(rpcEndpoint);
- const fetchNameAndSocials = async () => {
- const identityData = await api.query.identity.identityOf(
- validatorAddress
- );
-
- if (identityData.isSome) {
- const identity = identityData.unwrap();
- const info = identity[0]?.info;
- if (info) {
- setName(extractDataFromIdentityInfo(info, IdentityDataType.NAME));
- setEmail(extractDataFromIdentityInfo(info, IdentityDataType.EMAIL));
- setWeb(extractDataFromIdentityInfo(info, IdentityDataType.WEB));
- const twitterName = extractDataFromIdentityInfo(
- info,
- IdentityDataType.TWITTER
- );
- setTwitter(
- twitterName === null
- ? null
- : `https://twitter.com/${twitterName.substring(1)}`
- );
- }
- }
- };
-
- const fetchNominations = async () => {
- if (currentEra === null || !api.query.staking?.erasStakersOverview) {
- setNominations(null);
- setIsActive(null);
- return;
- }
-
- const erasStakersOverviewData =
- await api.query.staking.erasStakersOverview(
- currentEra,
- validatorAddress
- );
- if (erasStakersOverviewData.isSome) {
- const nominatorCount =
- erasStakersOverviewData.unwrap().nominatorCount;
- setNominations(nominatorCount.toNumber());
- setIsActive(true);
- return;
- }
-
- setNominations(null);
- setIsActive(false);
- };
-
- await Promise.all([fetchNameAndSocials(), fetchNominations()]);
- setIsLoading(false);
- };
-
- fetchData();
- }, [validatorAddress, rpcEndpoint, currentEra]);
-
- return {
- name,
- isActive,
- totalRestaked,
- restakingMethod,
- nominations,
- twitter,
- email,
- web,
- isLoading: isLoading || isLoadingLedgerOpt,
- };
-}
+export default InfoCard;
diff --git a/apps/tangle-dapp/app/nomination/[validatorAddress]/NodeSpecificationsTable.tsx b/apps/tangle-dapp/app/nomination/[validatorAddress]/NodeSpecificationsTable.tsx
index 4796ee4345..7c91a6a128 100644
--- a/apps/tangle-dapp/app/nomination/[validatorAddress]/NodeSpecificationsTable.tsx
+++ b/apps/tangle-dapp/app/nomination/[validatorAddress]/NodeSpecificationsTable.tsx
@@ -19,7 +19,7 @@ import { FC, useMemo } from 'react';
import ContainerSkeleton from '../../../components/skeleton/ContainerSkeleton';
import { HeaderCell } from '../../../components/tableCells';
-import useNodeSpecifications from '../../../data/useNodeSpecifications';
+import useNodeSpecifications from '../../../data/validatorDetails/useNodeSpecifications';
import { NodeSpecification } from '../../../types';
interface NodeSpecificationsTableProps {
diff --git a/apps/tangle-dapp/app/nomination/[validatorAddress]/RoleDistributionCard.tsx b/apps/tangle-dapp/app/nomination/[validatorAddress]/RoleDistributionCard.tsx
index 82184ec34c..628a29e99e 100644
--- a/apps/tangle-dapp/app/nomination/[validatorAddress]/RoleDistributionCard.tsx
+++ b/apps/tangle-dapp/app/nomination/[validatorAddress]/RoleDistributionCard.tsx
@@ -11,11 +11,11 @@ import GlassCard from '../../../components/GlassCard/GlassCard';
import useRestakingRoleLedger from '../../../data/restaking/useRestakingRoleLedger';
import { RestakingProfileType } from '../../../types';
import assertRestakingService from '../../../utils/assertRestakingService';
-import getChartDataAreaColorByServiceType from '../../../utils/getChartDataAreaColorByServiceType';
import {
getProfileTypeFromRestakeRoleLedger,
getRoleDistributionFromRestakeRoleLedger,
} from '../../../utils/polkadot/restake';
+import getChartDataAreaColorByServiceType from '../../../utils/restaking/getChartDataAreaColorByServiceType';
interface RoleDistributionCardProps {
validatorAddress: string;
@@ -26,7 +26,7 @@ const RoleDistributionCard: FC = ({
validatorAddress,
className,
}) => {
- const { data: ledgerOpt, isLoading } =
+ const { result: ledgerOpt, isLoading } =
useRestakingRoleLedger(validatorAddress);
const profileType = useMemo(
diff --git a/apps/tangle-dapp/app/nomination/[validatorAddress]/ServiceTableTabs.tsx b/apps/tangle-dapp/app/nomination/[validatorAddress]/ServiceTableTabs.tsx
index 618d84b20b..c139ba4de5 100644
--- a/apps/tangle-dapp/app/nomination/[validatorAddress]/ServiceTableTabs.tsx
+++ b/apps/tangle-dapp/app/nomination/[validatorAddress]/ServiceTableTabs.tsx
@@ -8,7 +8,7 @@ import {
ServiceTable,
TableStatus,
} from '../../../components';
-import useActiveServicesByValidator from '../../../data/ServiceTables/useActiveServicesByValidator';
+import useActiveServicesByValidator from '../../../data/validatorDetails/useActiveServicesByValidator';
interface ServiceTableTabsProps {
validatorAddress: string;
diff --git a/apps/tangle-dapp/app/nomination/[validatorAddress]/page.tsx b/apps/tangle-dapp/app/nomination/[validatorAddress]/page.tsx
index 2a55c31949..9660a64a5f 100644
--- a/apps/tangle-dapp/app/nomination/[validatorAddress]/page.tsx
+++ b/apps/tangle-dapp/app/nomination/[validatorAddress]/page.tsx
@@ -1,10 +1,11 @@
import { isAddress } from '@polkadot/util-crypto';
import { notFound } from 'next/navigation';
+import { IS_PRODUCTION_ENV } from '../../../constants/env';
+import InfoCard from './InfoCard';
import NodeSpecificationsTable from './NodeSpecificationsTable';
import RoleDistributionCard from './RoleDistributionCard';
import ServiceTableTabs from './ServiceTableTabs';
-import ValidatorBasicInfoCard from './ValidatorBasicInfoCard';
// TODO: might need to add metadata here
@@ -22,17 +23,17 @@ export default function ValidatorDetails({
return (
-
+
-
+ {/* TODO: Hide this for now */}
+ {!IS_PRODUCTION_ENV && (
+
+ )}
diff --git a/apps/tangle-dapp/app/nomination/layout.tsx b/apps/tangle-dapp/app/nomination/layout.tsx
new file mode 100644
index 0000000000..3b589a5925
--- /dev/null
+++ b/apps/tangle-dapp/app/nomination/layout.tsx
@@ -0,0 +1,7 @@
+import { PropsWithChildren } from 'react';
+
+import { BalancesProvider } from '../../context/BalancesContext';
+
+export default function Layout({ children }: PropsWithChildren) {
+ return {children} ;
+}
diff --git a/apps/tangle-dapp/app/page.tsx b/apps/tangle-dapp/app/page.tsx
index b64c0a48c6..280b925c70 100644
--- a/apps/tangle-dapp/app/page.tsx
+++ b/apps/tangle-dapp/app/page.tsx
@@ -1,4 +1,5 @@
-import { SkeletonLoader, Typography } from '@webb-tools/webb-ui-components';
+import SkeletonLoader from '@webb-tools/webb-ui-components/components/SkeletonLoader';
+import { Typography } from '@webb-tools/webb-ui-components/typography/Typography/Typography';
import { Metadata } from 'next';
import { FC, Suspense } from 'react';
@@ -15,7 +16,7 @@ export const metadata: Metadata = createPageMetadata({
const AccountPage: FC = () => {
return (
-
+
diff --git a/apps/tangle-dapp/app/providers.tsx b/apps/tangle-dapp/app/providers.tsx
index 9ea85f1be0..b50401e3fb 100644
--- a/apps/tangle-dapp/app/providers.tsx
+++ b/apps/tangle-dapp/app/providers.tsx
@@ -10,8 +10,6 @@ import { WebbUIProvider } from '@webb-tools/webb-ui-components';
import { type PropsWithChildren, type ReactNode } from 'react';
import z from 'zod';
-import { TxConfirmationProvider } from '../context/TxConfirmationContext';
-
const appEvent = new AppEvent();
const envSchema = z.object({
@@ -38,7 +36,7 @@ const Providers = ({ children }: PropsWithChildren): ReactNode => {
blockedRegions={blockedRegions}
blockedCountryCodes={blockedCountryCodes}
>
-
{children}
+ {children}
diff --git a/apps/tangle-dapp/app/restake/OverviewCard/ActionButton.tsx b/apps/tangle-dapp/app/restake/OverviewCard/ActionButton.tsx
index 140be105a9..eda37dc901 100644
--- a/apps/tangle-dapp/app/restake/OverviewCard/ActionButton.tsx
+++ b/apps/tangle-dapp/app/restake/OverviewCard/ActionButton.tsx
@@ -1,5 +1,6 @@
'use client';
+import type { BN } from '@polkadot/util';
import {
useConnectWallet,
useWebContext,
@@ -22,9 +23,11 @@ type Props = {
hasExistingProfile: boolean | null;
profileTypeOpt: Optional
| null;
isDataLoading?: boolean;
+ availableForRestake?: BN | null;
};
const ActionButton: FC = ({
+ availableForRestake = null,
hasExistingProfile,
profileTypeOpt,
isDataLoading,
@@ -56,7 +59,14 @@ const ActionButton: FC = ({
if (activeAccount && activeWallet) {
return (
<>
- setIsManageProfileModalOpen(true)}>
+ setIsManageProfileModalOpen(true)}
+ >
{hasExistingProfile ? 'Manage Profile' : 'Create Profile'}
diff --git a/apps/tangle-dapp/app/restake/OverviewCard/index.tsx b/apps/tangle-dapp/app/restake/OverviewCard/index.tsx
index b5f9e632c4..87e4c2fa03 100644
--- a/apps/tangle-dapp/app/restake/OverviewCard/index.tsx
+++ b/apps/tangle-dapp/app/restake/OverviewCard/index.tsx
@@ -13,8 +13,10 @@ import {
import { InfoIconWithTooltip } from '../../../components/InfoIconWithTooltip';
import TangleCard from '../../../components/TangleCard';
+import useRestakingAPY from '../../../data/restaking/useRestakingAPY';
import useRestakingLimits from '../../../data/restaking/useRestakingLimits';
import useRestakingProfile from '../../../data/restaking/useRestakingProfile';
+import useRestakingTotalRewards from '../../../data/restaking/useRestakingTotalRewards';
import useFormatNativeTokenAmount from '../../../hooks/useFormatNativeTokenAmount';
import { getTotalRestakedFromRestakeRoleLedger } from '../../../utils/polkadot/restake';
import ActionButton from './ActionButton';
@@ -22,13 +24,14 @@ import ActionButton from './ActionButton';
const OverviewCard = forwardRef, ComponentProps<'div'>>(
(props, ref) => {
const formatNativeTokenAmount = useFormatNativeTokenAmount();
- const {
- hasExistingProfile,
- profileTypeOpt,
- earningsRecord,
- ledgerOpt,
- isLoading,
- } = useRestakingProfile();
+
+ const { hasExistingProfile, profileTypeOpt, ledgerOpt, isLoading } =
+ useRestakingProfile();
+
+ const { result: totalRewards, isLoading: isTotalRewardLoading } =
+ useRestakingTotalRewards();
+
+ const apy = useRestakingAPY();
const { maxRestakingAmount } = useRestakingLimits();
const totalRestaked = useMemo(
@@ -36,15 +39,6 @@ const OverviewCard = forwardRef, ComponentProps<'div'>>(
[ledgerOpt]
);
- const earnings = useMemo(() => {
- if (isLoading || !earningsRecord) return null;
-
- return Object.values(earningsRecord).reduce(
- (total, curr) => total.add(curr),
- BN_ZERO
- );
- }, [earningsRecord, isLoading]);
-
const availableForRestake = useMemo(() => {
if (maxRestakingAmount !== null && totalRestaked !== null) {
return maxRestakingAmount.gt(totalRestaked)
@@ -79,18 +73,23 @@ const OverviewCard = forwardRef, ComponentProps<'div'>>(
/>
-
+
{
const { ledgerOpt, profileTypeOpt, isLoading } = useRestakingProfile();
diff --git a/apps/tangle-dapp/app/restake/RolesEarningsCard.tsx b/apps/tangle-dapp/app/restake/RolesEarningsCard.tsx
index 6c6489a171..f837b05b73 100644
--- a/apps/tangle-dapp/app/restake/RolesEarningsCard.tsx
+++ b/apps/tangle-dapp/app/restake/RolesEarningsCard.tsx
@@ -1,8 +1,10 @@
'use client';
+import { TANGLE_TOKEN_DECIMALS } from '@webb-tools/dapp-config/constants/tangle';
import { Spinner } from '@webb-tools/icons';
import { Typography } from '@webb-tools/webb-ui-components/typography/Typography';
-import { FC, useMemo } from 'react';
+import { ComponentProps, FC, useMemo } from 'react';
+import { formatUnits } from 'viem';
import { RoleEarningsChart } from '../../components/charts';
import GlassCard from '../../components/GlassCard/GlassCard';
@@ -11,13 +13,13 @@ import useRestakingProfile from '../../data/restaking/useRestakingProfile';
const RolesEarningsCard: FC = () => {
const { earningsRecord, isLoading } = useRestakingProfile();
- const data = useMemo(() => {
+ const data = useMemo['data']>(() => {
if (!earningsRecord) return [];
return Object.entries(earningsRecord).map(([era, reward]) => ({
era: +era,
- // Recharts can only handle number, temporarily convert to number
- reward: reward.toNumber(),
+ // Format to display already handled in the chart component
+ reward: +formatUnits(reward, TANGLE_TOKEN_DECIMALS),
}));
}, [earningsRecord]);
@@ -28,7 +30,7 @@ const RolesEarningsCard: FC = () => {
{isLoading ? (
-
+
) : (
diff --git a/apps/tangle-dapp/app/services/ActiveServicesTable.tsx b/apps/tangle-dapp/app/services/ActiveServicesTable.tsx
index 325b85cba4..704d4e2912 100644
--- a/apps/tangle-dapp/app/services/ActiveServicesTable.tsx
+++ b/apps/tangle-dapp/app/services/ActiveServicesTable.tsx
@@ -2,33 +2,30 @@
import { Typography } from '@webb-tools/webb-ui-components';
import { FC } from 'react';
-import useSWR from 'swr';
import { ContainerSkeleton, ServiceTable, TableStatus } from '../../components';
-import { getActiveServices } from '../../data/ServiceTables';
+import useServiceOverview from '../../data/serviceOverview/useServiceOverview';
const pageSize = 10;
const ActiveServicesTable: FC = () => {
- const { data: activeServicesData, isLoading: activeServicesDataLoading } =
- useSWR([getActiveServices.name], getActiveServices);
+ const { services, isLoading } = useServiceOverview();
return (
Active Services
- {activeServicesData && activeServicesData.length === 0 ? (
+ {services && services.length === 0 ? (
- ) : activeServicesDataLoading || !activeServicesData ? (
+ ) : isLoading || !services ? (
) : (
- // TODO: Handle lazy load when integrating with backend
-
+
)}
);
diff --git a/apps/tangle-dapp/app/services/[serviceId]/DetailTabs.tsx b/apps/tangle-dapp/app/services/[serviceId]/DetailTabs.tsx
deleted file mode 100644
index 0c9e808dbe..0000000000
--- a/apps/tangle-dapp/app/services/[serviceId]/DetailTabs.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-'use client';
-
-import { TabContent, TableAndChartTabs } from '@webb-tools/webb-ui-components';
-import { FC } from 'react';
-import { twMerge } from 'tailwind-merge';
-
-import JobsListTable from './JobsListTable';
-import SigningRules from './SigningRules';
-
-interface DetailTabsProps {
- serviceId: string;
- className?: string;
-}
-
-const JOBS_LIST_TAB = 'Jobs List';
-const SIGNING_RULES_TAB = 'Signing Rules';
-
-const DetailTabs: FC
= ({ serviceId, className }) => {
- return (
-
-
-
-
-
-
-
-
- );
-};
-
-export default DetailTabs;
diff --git a/apps/tangle-dapp/app/services/[serviceId]/InfoCard.tsx b/apps/tangle-dapp/app/services/[serviceId]/InfoCard.tsx
index f60daa1256..ec4e583a5f 100644
--- a/apps/tangle-dapp/app/services/[serviceId]/InfoCard.tsx
+++ b/apps/tangle-dapp/app/services/[serviceId]/InfoCard.tsx
@@ -1,13 +1,17 @@
+'use client';
+
import {
Chip,
- CopyWithTooltip,
- TimeProgress,
+ formatDateToUtc,
+ Tooltip,
+ TooltipBody,
+ TooltipTrigger,
Typography,
} from '@webb-tools/webb-ui-components';
-import { shortenString } from '@webb-tools/webb-ui-components/utils/shortenString';
+import { FC } from 'react';
import { twMerge } from 'tailwind-merge';
-import { getServiceDetailsInfo } from '../../../data/ServiceDetails';
+import useServiceInfoCard from '../../../data/serviceDetails/useServiceInfoCard';
import { getChipColorOfServiceType } from '../../../utils';
interface InfoCardProps {
@@ -15,9 +19,8 @@ interface InfoCardProps {
className?: string;
}
-async function InfoCard({ serviceId, className }: InfoCardProps) {
- const { serviceType, thresholds, key, startTimestamp, endTimestamp } =
- await getServiceDetailsInfo(serviceId);
+const InfoCard: FC = ({ serviceId, className }) => {
+ const { serviceType, threshold, endDate } = useServiceInfoCard();
return (
Phase 1 ID: {serviceId}
-
- {serviceType}
-
- {thresholds && (
-
- {thresholds}
-
- )}
-
- {key && (
-
-
- Key:
-
-
-
+ {serviceType && (
+
-
- {shortenString(key, 4)}
-
-
-
-
+ {serviceType}
+
+ )}
+ {threshold && (
+
+
+
+ {threshold}
+
+
+ Threshold
+
+ )}
+
+
+ {endDate && (
+
+ {/* Check if the service has ended or not */}
+ {endDate.getTime() > new Date().getTime()
+ ? 'Expect to end at'
+ : 'Ended at'}
+ : {formatDateToUtc(endDate)}
+
)}
-
+ /> */}
);
-}
+};
export default InfoCard;
diff --git a/apps/tangle-dapp/app/services/[serviceId]/JobsListTable.tsx b/apps/tangle-dapp/app/services/[serviceId]/JobsListTable.tsx
index bf1acd74cb..bc20248514 100644
--- a/apps/tangle-dapp/app/services/[serviceId]/JobsListTable.tsx
+++ b/apps/tangle-dapp/app/services/[serviceId]/JobsListTable.tsx
@@ -22,7 +22,7 @@ import { twMerge } from 'tailwind-merge';
import { SkeletonRow } from '../../../components/skeleton';
import { HeaderCell, StringCell } from '../../../components/tableCells';
import useNetworkStore from '../../../context/useNetworkStore';
-import { useServiceJobs } from '../../../data/ServiceDetails';
+import { useServiceJobs } from '../../../data/serviceDetails';
import { ExplorerType, ServiceJob } from '../../../types';
interface JobsListTableProps {
@@ -31,6 +31,8 @@ interface JobsListTableProps {
}
const PAGE_SIZE = 10;
+
+// TODO: This is hardcoded to the testnet.
const TANGLE_BLOCK_EXPLORER =
chainsConfig[PresetTypedChainId.TangleTestnetNative].blockExplorers?.default
.url;
diff --git a/apps/tangle-dapp/app/services/[serviceId]/ParticipantsTable.tsx b/apps/tangle-dapp/app/services/[serviceId]/ParticipantsTable.tsx
index 3860af0385..58be8defc4 100644
--- a/apps/tangle-dapp/app/services/[serviceId]/ParticipantsTable.tsx
+++ b/apps/tangle-dapp/app/services/[serviceId]/ParticipantsTable.tsx
@@ -5,9 +5,11 @@ import {
getCoreRowModel,
useReactTable,
} from '@tanstack/react-table';
+import { getExplorerURI } from '@webb-tools/api-provider-environment/transaction/utils';
import {
Avatar,
CopyWithTooltip,
+ ExternalLinkIcon,
fuzzyFilter,
shortenString,
Table,
@@ -19,59 +21,76 @@ import { twMerge } from 'tailwind-merge';
import { SkeletonRow } from '../../../components/skeleton';
import SocialChip from '../../../components/SocialChip';
import { HeaderCell } from '../../../components/tableCells';
-import { useServiceParticipants } from '../../../data/ServiceDetails';
-import type { ServiceParticipant } from '../../../types';
+import useNetworkStore from '../../../context/useNetworkStore';
+import { useServiceParticipants } from '../../../data/serviceDetails';
+import { ExplorerType, ServiceParticipant } from '../../../types';
interface ParticipantTableProps {
- serviceId: string;
className?: string;
}
const columnHelper = createColumnHelper();
-const columns = [
- columnHelper.accessor('identity', {
- header: () => ,
- cell: (props) => {
- const address = props.row.original.address;
- return (
-
-
+const addressColumn = columnHelper.accessor('address', {
+ header: () =>
,
+ cell: (props) => {
+ const { twitter, discord, email, web } = props.row.original;
+ return (
+
+ {twitter && }
+ {discord && }
+ {email && }
+ {web && }
+ {!twitter && !discord && !email && !web && (
- {props.getValue() ?? shortenString(address)}
+ {'--'}
-
-
- );
- },
- }),
- columnHelper.accessor('address', {
- header: () =>
,
- cell: (props) => {
- const { twitter, discord, email, web } = props.row.original;
- return (
-
- {twitter && }
- {discord && }
- {email && }
- {web && }
-
- );
- },
- }),
-];
+ )}
+
+ );
+ },
+});
+
+const ParticipantsTable: FC = ({ className }) => {
+ const { network } = useNetworkStore();
+ const { data, isLoading } = useServiceParticipants();
-const ParticipantsTable: FC = ({
- serviceId,
- className,
-}) => {
- const { data, isLoading, error } = useServiceParticipants(serviceId);
+ const columns = [
+ columnHelper.accessor('identity', {
+ header: () => ,
+ cell: (props) => {
+ const address = props.row.original.address;
+ const accountExplorerLink = getExplorerURI(
+ network.polkadotExplorerUrl,
+ address,
+ 'address',
+ ExplorerType.Substrate
+ ).toString();
+ return (
+
+
+
+ {props.getValue() ?? shortenString(address)}
+
+
+
+
+
+
+ );
+ },
+ }),
+ addressColumn,
+ ];
const table = useReactTable({
- data: data ?? [],
+ data: data,
columns,
filterFns: {
fuzzy: fuzzyFilter,
@@ -92,8 +111,7 @@ const ParticipantsTable: FC = ({
'border border-mono-0 dark:border-mono-160'
)}
>
- {/* Successfully get data */}
- {data && !isLoading && !error && (
+ {!isLoading ? (
= ({
className="h-full flex flex-col"
tableWrapperClassName="h-full overflow-y-auto"
/>
- )}
-
- {/* Loading */}
- {isLoading && }
-
- {/* Error */}
- {!isLoading && !data && error && (
- Error
+ ) : (
+
)}
diff --git a/apps/tangle-dapp/app/services/[serviceId]/PermittedCaller.tsx b/apps/tangle-dapp/app/services/[serviceId]/PermittedCaller.tsx
new file mode 100644
index 0000000000..37c8f9a210
--- /dev/null
+++ b/apps/tangle-dapp/app/services/[serviceId]/PermittedCaller.tsx
@@ -0,0 +1,112 @@
+'use client';
+
+import { getExplorerURI } from '@webb-tools/api-provider-environment/transaction/utils';
+import {
+ CodeFile,
+ CopyWithTooltip,
+ ExternalLinkIcon,
+ shortenHex,
+ Typography,
+} from '@webb-tools/webb-ui-components';
+import { FC } from 'react';
+import { twMerge } from 'tailwind-merge';
+
+import SkeletonRow from '../../../components/skeleton/SkeletonRow';
+import useNetworkStore from '../../../context/useNetworkStore';
+import { usePermittedCaller } from '../../../data/serviceDetails';
+
+interface PermittedCallerProps {
+ className?: string;
+}
+
+const PermittedCaller: FC = ({ className }) => {
+ const { network } = useNetworkStore();
+ const { permittedCaller, codeData, isLoading, error } = usePermittedCaller();
+
+ return (
+
+
+
+ Permitted Caller
+
+ {permittedCaller && (
+
+
+ {shortenHex(permittedCaller)}
+
+
+ {network.evmExplorerUrl && (
+
+ )}
+
+ )}
+
+
+ {/* Loading */}
+ {isLoading && }
+
+ {/* Error */}
+ {!isLoading && error !== null && (
+
+ Error
+
+ )}
+
+ {/* No Permitted Caller */}
+ {!isLoading &&
+ error === null &&
+ permittedCaller === null &&
+ codeData === null && (
+
+ No Permitted Caller was provided
+
+ )}
+
+ {/* Permitted Caller available but not a contract */}
+ {!isLoading &&
+ error === null &&
+ permittedCaller !== null &&
+ codeData === null && (
+
+ The code for Permitted Caller is not available since it is not a
+ smart contract.
+
+ )}
+
+ {/* Permitted Caller is a contract */}
+ {!isLoading &&
+ error === null &&
+ permittedCaller !== null &&
+ codeData !== null && (
+
+ )}
+
+
+ );
+};
+
+export default PermittedCaller;
diff --git a/apps/tangle-dapp/app/services/[serviceId]/SigningRules.tsx b/apps/tangle-dapp/app/services/[serviceId]/SigningRules.tsx
deleted file mode 100644
index 937580a2e2..0000000000
--- a/apps/tangle-dapp/app/services/[serviceId]/SigningRules.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-'use client';
-
-import { CodeFile, Typography } from '@webb-tools/webb-ui-components';
-import { FC, useEffect, useState } from 'react';
-import { twMerge } from 'tailwind-merge';
-
-import { getSigningRules } from '../../../data/ServiceDetails';
-
-interface SigningRulesProps {
- serviceId: string;
- className?: string;
-}
-
-const SigningRules: FC = ({ className }) => {
- const [signingRulesContractAddr, setSigningRulesContractAddr] = useState<
- string | null
- >(null);
- const [isLoading, setIsLoading] = useState(true);
- const [error, setError] = useState(null);
-
- useEffect(() => {
- // TODO: get permitted caller
- // TODO: check if permitted caller is a contract or not
- setSigningRulesContractAddr('');
- setIsLoading(false);
- setError(null);
- }, []);
-
- if (isLoading) {
- return Loading...
;
- }
-
- if (error !== null) {
- return (
-
- Error
-
- );
- }
-
- if (signingRulesContractAddr === null) {
- return (
-
- No Signing Rules associated with this service
-
- );
- }
-
- return (
-
- getSigningRules(signingRulesContractAddr)}
- isInNextProject
- className="bg-mono-20 dark:bg-mono-200 overflow-auto max-h-[740px]"
- // smart contract language: Solidity
- language="sol"
- />
-
- );
-};
-
-export default SigningRules;
diff --git a/apps/tangle-dapp/app/services/[serviceId]/layout.tsx b/apps/tangle-dapp/app/services/[serviceId]/layout.tsx
new file mode 100644
index 0000000000..1a58a8f854
--- /dev/null
+++ b/apps/tangle-dapp/app/services/[serviceId]/layout.tsx
@@ -0,0 +1,18 @@
+import { ReactNode } from 'react';
+
+import ServiceDetailsProvider from '../../../context/ServiceDetailsContext';
+
+export default function ServiceDetailsLayout({
+ children,
+ params,
+}: {
+ children: ReactNode;
+ params: { serviceId: string };
+}) {
+ const { serviceId } = params;
+ return (
+
+ {children}
+
+ );
+}
diff --git a/apps/tangle-dapp/app/services/[serviceId]/page.tsx b/apps/tangle-dapp/app/services/[serviceId]/page.tsx
index 0a61842ad5..f1f8e053b2 100644
--- a/apps/tangle-dapp/app/services/[serviceId]/page.tsx
+++ b/apps/tangle-dapp/app/services/[serviceId]/page.tsx
@@ -1,6 +1,6 @@
-import DetailTabs from './DetailTabs';
import InfoCard from './InfoCard';
import ParticipantsTable from './ParticipantsTable';
+import PermittedCaller from './PermittedCaller';
export default function ServiceDetails({
params,
@@ -8,20 +8,15 @@ export default function ServiceDetails({
params: { serviceId: string };
}) {
const { serviceId } = params;
+
return (
);
diff --git a/apps/tangle-dapp/app/services/hooks/useRoleDistribution.ts b/apps/tangle-dapp/app/services/hooks/useRoleDistribution.ts
index 2a0379afb1..0d2af8f598 100644
--- a/apps/tangle-dapp/app/services/hooks/useRoleDistribution.ts
+++ b/apps/tangle-dapp/app/services/hooks/useRoleDistribution.ts
@@ -4,7 +4,7 @@ import { BN } from '@polkadot/util';
import type { PieChartItem } from '../../../components/charts/types';
import { RestakingService } from '../../../types';
-import getChartDataAreaColorByServiceType from '../../../utils/getChartDataAreaColorByServiceType';
+import getChartDataAreaColorByServiceType from '../../../utils/restaking/getChartDataAreaColorByServiceType';
export default function useRoleDistribution(): PieChartItem[] {
const data = [
diff --git a/apps/tangle-dapp/app/services/layout.tsx b/apps/tangle-dapp/app/services/layout.tsx
new file mode 100644
index 0000000000..b0a0c6c169
--- /dev/null
+++ b/apps/tangle-dapp/app/services/layout.tsx
@@ -0,0 +1,11 @@
+import { ReactNode } from 'react';
+
+import ServiceOverviewContext from '../../context/ServiceOverviewContext';
+
+export default function ServiceDetailsLayout({
+ children,
+}: {
+ children: ReactNode;
+}) {
+ return {children} ;
+}
diff --git a/apps/tangle-dapp/components/AmountInput/AmountInput.tsx b/apps/tangle-dapp/components/AmountInput/AmountInput.tsx
index f297fb597b..923cc7c366 100644
--- a/apps/tangle-dapp/components/AmountInput/AmountInput.tsx
+++ b/apps/tangle-dapp/components/AmountInput/AmountInput.tsx
@@ -20,6 +20,10 @@ export type AmountInputProps = {
errorOnEmptyValue?: boolean;
setAmount: (newAmount: BN | null) => void;
setErrorMessage?: (error: string | null) => void;
+ placeholder?: string;
+ wrapperClassName?: string;
+ bodyClassName?: string;
+ dropdownBodyClassName?: string;
};
const AmountInput: FC = ({
@@ -36,6 +40,10 @@ const AmountInput: FC = ({
baseInputOverrides,
errorOnEmptyValue = false,
setErrorMessage,
+ placeholder,
+ wrapperClassName,
+ bodyClassName,
+ dropdownBodyClassName,
}) => {
const inputRef = useRef(null);
const { nativeTokenSymbol } = useNetworkStore();
@@ -93,13 +101,16 @@ const AmountInput: FC = ({
isDisabled={isDisabled}
{...baseInputOverrides}
actions={actions}
+ wrapperClassName={wrapperClassName}
+ bodyClassName={bodyClassName}
+ dropdownBodyClassName={dropdownBodyClassName}
>
= {
+ includeCommas: false,
+ fractionLength: undefined,
+};
+
const useInputAmount = (
amount: BN | null,
min: BN | null,
@@ -58,7 +65,7 @@ const useInputAmount = (
const [errorMessage, setErrorMessage] = useState(null);
const [displayAmount, setDisplayAmount] = useState(
- amount !== null ? formatBnToDisplayAmount(amount) : ''
+ amount !== null ? formatBnToDisplayAmount(amount, INPUT_AMOUNT_FORMAT) : ''
);
const handleChange = useCallback(
@@ -110,7 +117,9 @@ const useInputAmount = (
);
const refreshDisplayAmount = useCallback((newDisplayAmount: BN) => {
- setDisplayAmount(formatBnToDisplayAmount(newDisplayAmount));
+ setDisplayAmount(
+ formatBnToDisplayAmount(newDisplayAmount, INPUT_AMOUNT_FORMAT)
+ );
}, []);
return {
diff --git a/apps/tangle-dapp/components/Breadcrumbs/Breadcrumbs.tsx b/apps/tangle-dapp/components/Breadcrumbs/Breadcrumbs.tsx
index f9ceff2679..a143765887 100644
--- a/apps/tangle-dapp/components/Breadcrumbs/Breadcrumbs.tsx
+++ b/apps/tangle-dapp/components/Breadcrumbs/Breadcrumbs.tsx
@@ -2,6 +2,7 @@
import { isAddress } from '@polkadot/util-crypto';
import {
+ ArrowLeftRightLineIcon,
CheckboxBlankCircleLine,
CodeFill,
FundsLine,
@@ -30,6 +31,7 @@ const BREADCRUMB_ICONS: Record = {
services: ,
restake: ,
nomination: ,
+ bridge: ,
};
// TODO: Need to statically link the breadcrumb labels to the page path for better type safety and to enable fearless refactoring in the future.
diff --git a/apps/tangle-dapp/components/DelegatorTable/index.ts b/apps/tangle-dapp/components/DelegatorTable/index.ts
deleted file mode 100644
index 7d2aa0e533..0000000000
--- a/apps/tangle-dapp/components/DelegatorTable/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default as DelegatorTable } from './DelegatorTable';
diff --git a/apps/tangle-dapp/components/DelegatorTable/types.ts b/apps/tangle-dapp/components/DelegatorTable/types.ts
deleted file mode 100644
index 2cdb2a3629..0000000000
--- a/apps/tangle-dapp/components/DelegatorTable/types.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { Delegator } from '../../types';
-
-export interface DelegatorTableProps {
- data?: Delegator[];
- pageSize: number;
-}
diff --git a/apps/tangle-dapp/components/HeaderChip/ChipText.tsx b/apps/tangle-dapp/components/HeaderChip/ChipText.tsx
index dca35a1e50..74476457c6 100644
--- a/apps/tangle-dapp/components/HeaderChip/ChipText.tsx
+++ b/apps/tangle-dapp/components/HeaderChip/ChipText.tsx
@@ -3,7 +3,6 @@
import { notificationApi } from '@webb-tools/webb-ui-components';
import SkeletonLoader from '@webb-tools/webb-ui-components/components/SkeletonLoader';
-import getRoundedDownNumberWith2Decimals from '../../utils/getRoundedDownNumberWith2Decimals';
import dataHooks from './dataHooks';
import type { HeaderChipItemProps } from './types';
@@ -23,11 +22,11 @@ const ChipText = ({ label }: Props) => {
<>
{label}:{' '}
{isLoading ? (
-
+
) : error ? (
'Error'
) : data === null ? null : (
- getRoundedDownNumberWith2Decimals(data)
+ data
)}
>
);
diff --git a/apps/tangle-dapp/components/KeyStatsItem/KeyStatsItem.tsx b/apps/tangle-dapp/components/KeyStatsItem/KeyStatsItem.tsx
index 92014037d8..6ffa082b5c 100644
--- a/apps/tangle-dapp/components/KeyStatsItem/KeyStatsItem.tsx
+++ b/apps/tangle-dapp/components/KeyStatsItem/KeyStatsItem.tsx
@@ -22,6 +22,7 @@ export type KeyStatsItemProps = {
suffix?: string;
tooltip?: string;
className?: string;
+ showDataBeforeLoading?: boolean;
children?: ReactNode;
error: Error | null;
isLoading: boolean;
@@ -32,6 +33,7 @@ const KeyStatsItem: FC = ({
title,
tooltip,
className,
+ showDataBeforeLoading,
prefix,
suffix,
children,
@@ -69,7 +71,7 @@ const KeyStatsItem: FC = ({
- {isLoading ? (
+ {isLoading && !showDataBeforeLoading ? (
) : error !== null ? (
'Error'
diff --git a/apps/tangle-dapp/components/NetworkSelector/CustomRpcEndpointInput.tsx b/apps/tangle-dapp/components/NetworkSelector/CustomRpcEndpointInput.tsx
index d9770cd4dd..db7f37f3c6 100644
--- a/apps/tangle-dapp/components/NetworkSelector/CustomRpcEndpointInput.tsx
+++ b/apps/tangle-dapp/components/NetworkSelector/CustomRpcEndpointInput.tsx
@@ -1,6 +1,6 @@
import { Save, SaveWithBg } from '@webb-tools/icons';
import { Input } from '@webb-tools/webb-ui-components';
-import { FC, useCallback, useEffect, useState } from 'react';
+import { FC, useCallback, useEffect, useRef, useState } from 'react';
import useLocalStorage, { LocalStorageKey } from '../../hooks/useLocalStorage';
@@ -15,11 +15,12 @@ const CustomRpcEndpointInput: FC
= ({
placeholder,
setCustomNetwork,
}) => {
- const { get: getCachedCustomRpcEndpoint } = useLocalStorage(
+ const { refresh: getCachedCustomRpcEndpoint } = useLocalStorage(
LocalStorageKey.CUSTOM_RPC_ENDPOINT
);
const [value, setValue] = useState('');
+ const wasValueSetRef = useRef(false);
const handleSave = useCallback(
() => setCustomNetwork(value),
@@ -29,12 +30,18 @@ const CustomRpcEndpointInput: FC = ({
// On mount, load the cached custom RPC endpoint. If it
// exists, set it as the initial value of the input.
useEffect(() => {
+ // Don't set the value more than once.
+ if (wasValueSetRef.current) {
+ return;
+ }
+
const cachedCustomRpcEndpoint = getCachedCustomRpcEndpoint();
- if (cachedCustomRpcEndpoint !== null) {
- setValue(cachedCustomRpcEndpoint);
+ if (cachedCustomRpcEndpoint.value !== null && value === '') {
+ setValue(cachedCustomRpcEndpoint.value);
+ wasValueSetRef.current = true;
}
- }, [getCachedCustomRpcEndpoint]);
+ }, [getCachedCustomRpcEndpoint, value]);
return (
= ({
{/* Mainnet network */}
onNetworkChange(TANGLE_MAINNET_NETWORK)}
/>
{/* Testnet network */}
onNetworkChange(TANGLE_TESTNET_NATIVE_NETWORK)}
/>
@@ -49,7 +47,7 @@ export const NetworkSelectorDropdown: FC = ({
{/* Local dev network */}
onNetworkChange(TANGLE_LOCAL_DEV_NETWORK)}
tooltip={
diff --git a/apps/tangle-dapp/components/DelegatorTable/DelegatorTable.tsx b/apps/tangle-dapp/components/NominationsTable/NominationsTable.tsx
similarity index 71%
rename from apps/tangle-dapp/components/DelegatorTable/DelegatorTable.tsx
rename to apps/tangle-dapp/components/NominationsTable/NominationsTable.tsx
index 339053ef05..ebd5f1a5f5 100644
--- a/apps/tangle-dapp/components/DelegatorTable/DelegatorTable.tsx
+++ b/apps/tangle-dapp/components/NominationsTable/NominationsTable.tsx
@@ -19,18 +19,19 @@ import {
} from '@webb-tools/webb-ui-components';
import { type FC } from 'react';
-import { Delegator } from '../../types';
+import { Nominee } from '../../types';
+import calculateCommission from '../../utils/calculateCommission';
import { HeaderCell, StringCell } from '../tableCells';
-import { DelegatorTableProps } from './types';
+import TokenAmountCell from '../tableCells/TokenAmountCell';
-const columnHelper = createColumnHelper();
+const columnHelper = createColumnHelper();
const columns = [
columnHelper.accessor('address', {
header: () => ,
cell: (props) => {
const address = props.getValue();
- const identity = props.row.original.identity;
+ const identityName = props.row.original.identityName;
return (
@@ -39,7 +40,9 @@ const columns = [
- {identity === address ? shortenString(address, 6) : identity}
+ {identityName === address
+ ? shortenString(address, 6)
+ : identityName}
,
- cell: (props) => (
-
- ),
+ cell: (props) => ,
}),
- columnHelper.accessor('effectiveAmountStaked', {
+ columnHelper.accessor('totalStakeAmount', {
header: () => (
),
- cell: (props) => (
-
- ),
+ cell: (props) => ,
}),
- columnHelper.accessor('delegations', {
+ columnHelper.accessor('nominatorCount', {
header: () => ,
cell: (props) => (
-
+
),
}),
columnHelper.accessor('commission', {
header: () => ,
cell: (props) => (
),
}),
];
-const DelegatorTable: FC = ({ data = [], pageSize }) => {
+export type NominationsTableProps = {
+ nominees: Nominee[];
+ pageSize: number;
+};
+
+const NominationsTable: FC = ({
+ nominees,
+ pageSize,
+}) => {
const table = useReactTable({
- data,
+ data: nominees,
columns,
initialState: {
pagination: {
@@ -119,10 +126,10 @@ const DelegatorTable: FC = ({ data = [], pageSize }) => {
paginationClassName="bg-mono-0 dark:bg-mono-180 pl-6"
tableProps={table}
isPaginated
- totalRecords={data.length}
+ totalRecords={nominees.length}
/>
);
};
-export default DelegatorTable;
+export default NominationsTable;
diff --git a/apps/tangle-dapp/components/NominationsTable/index.ts b/apps/tangle-dapp/components/NominationsTable/index.ts
new file mode 100644
index 0000000000..c766fd6e33
--- /dev/null
+++ b/apps/tangle-dapp/components/NominationsTable/index.ts
@@ -0,0 +1 @@
+export { default as NominationsTable } from './NominationsTable';
diff --git a/apps/tangle-dapp/components/NominatorStatsItem/NominatorStatsItem.tsx b/apps/tangle-dapp/components/NominatorStatsItem/NominatorStatsItem.tsx
index f9fb2ec00c..44d846ebe1 100644
--- a/apps/tangle-dapp/components/NominatorStatsItem/NominatorStatsItem.tsx
+++ b/apps/tangle-dapp/components/NominatorStatsItem/NominatorStatsItem.tsx
@@ -1,25 +1,44 @@
-import { Typography } from '@webb-tools/webb-ui-components';
+import { SkeletonLoader, Typography } from '@webb-tools/webb-ui-components';
import type { FC } from 'react';
import { twMerge } from 'tailwind-merge';
import { InfoIconWithTooltip } from '..';
-import NominatorStatsItemText from './NominatorStatsItemText';
import { NominatorStatsItemProps } from './types';
export const NominatorStatsItem: FC = ({
title,
tooltip,
className,
- ...restProps
+ children,
+ isError,
}) => {
return (
-
+
+
+ {children === null ? (
+
+ ) : isError ? (
+ 'Error'
+ ) : (
+
+
+ {children}
+
+
+ )}
+
+
{title}
+
{tooltip && }
diff --git a/apps/tangle-dapp/components/NominatorStatsItem/NominatorStatsItemText.tsx b/apps/tangle-dapp/components/NominatorStatsItem/NominatorStatsItemText.tsx
deleted file mode 100644
index 35455e5969..0000000000
--- a/apps/tangle-dapp/components/NominatorStatsItem/NominatorStatsItemText.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-'use client';
-
-import { notificationApi } from '@webb-tools/webb-ui-components';
-import SkeletonLoader from '@webb-tools/webb-ui-components/components/SkeletonLoader';
-import { Typography } from '@webb-tools/webb-ui-components/typography/Typography';
-import { useEffect } from 'react';
-
-import useNetworkStore from '../../context/useNetworkStore';
-import { formatTokenBalance } from '../../utils/polkadot';
-import dataHooks from './dataHooks';
-import type { NominatorStatsItemProps } from './types';
-
-type Props = Pick;
-
-const NominatorStatsItemText = ({ address, type }: Props) => {
- const { nativeTokenSymbol } = useNetworkStore();
- const { isLoading, error, data } = dataHooks[type](address);
-
- useEffect(() => {
- if (error) {
- notificationApi({
- variant: 'error',
- message: error.message,
- key: error.message,
- });
- }
- }, [error]);
-
- return (
-
-
- {isLoading ? (
-
- ) : error ? (
- 'Error'
- ) : data === null ? null : (
-
-
- {data.value1
- ? formatTokenBalance(data.value1, nativeTokenSymbol)
- : '-'}
-
-
- )}
-
-
- );
-};
-
-export default NominatorStatsItemText;
diff --git a/apps/tangle-dapp/components/NominatorStatsItem/dataHooks.ts b/apps/tangle-dapp/components/NominatorStatsItem/dataHooks.ts
index 34f2cb979f..424c5fdbde 100644
--- a/apps/tangle-dapp/components/NominatorStatsItem/dataHooks.ts
+++ b/apps/tangle-dapp/components/NominatorStatsItem/dataHooks.ts
@@ -1,11 +1,13 @@
import useTokenWalletFreeBalance from '../../data/NominatorStats/useTokenWalletFreeBalance';
+import useTotalPayoutRewards from '../../data/NominatorStats/useTotalPayoutRewards';
import useTotalStakedAmountSubscription from '../../data/NominatorStats/useTotalStakedAmountSubscription';
-import useUnbondingAmountSubscription from '../../data/NominatorStats/useUnbondingAmountSubscription';
+import useUnbondingAmountSubscription from '../../data/NominatorStats/useUnbondingAmount';
const dataHooks = {
'Wallet Balance': useTokenWalletFreeBalance,
'Total Staked': useTotalStakedAmountSubscription,
'Unbonding Amount': useUnbondingAmountSubscription,
+ 'Total Payout Rewards': useTotalPayoutRewards,
} as const;
export default dataHooks;
diff --git a/apps/tangle-dapp/components/NominatorStatsItem/types.ts b/apps/tangle-dapp/components/NominatorStatsItem/types.ts
index dabd4fece3..98ee8fd4be 100644
--- a/apps/tangle-dapp/components/NominatorStatsItem/types.ts
+++ b/apps/tangle-dapp/components/NominatorStatsItem/types.ts
@@ -1,11 +1,9 @@
import React from 'react';
-export type StatsType = 'Wallet Balance' | 'Total Staked' | 'Unbonding Amount';
-
export interface NominatorStatsItemProps {
title: string;
- type: StatsType;
tooltip?: string | React.ReactNode;
- address: string;
className?: string;
+ children: React.ReactNode | null;
+ isError: boolean;
}
diff --git a/apps/tangle-dapp/components/PayoutTable/PayoutTable.tsx b/apps/tangle-dapp/components/PayoutTable/PayoutTable.tsx
index 75a83f7f01..6357c40364 100644
--- a/apps/tangle-dapp/components/PayoutTable/PayoutTable.tsx
+++ b/apps/tangle-dapp/components/PayoutTable/PayoutTable.tsx
@@ -12,7 +12,6 @@ import { WalletPayIcon } from '@webb-tools/icons';
import {
Avatar,
AvatarGroup,
- Chip,
CopyWithTooltip,
fuzzyFilter,
shortenString,
@@ -24,6 +23,7 @@ import { type FC, useState } from 'react';
import PayoutTxContainer from '../../containers/PayoutTxContainer/PayoutTxContainer';
import { AddressWithIdentity, Payout } from '../../types';
import { HeaderCell, StringCell } from '../tableCells';
+import TokenAmountCell from '../tableCells/TokenAmountCell';
import { PayoutTableProps } from './types';
const columnHelper = createColumnHelper();
@@ -32,14 +32,18 @@ const PayoutTable: FC = ({
data = [],
pageSize,
updateData,
+ sessionProgress,
+ historyDepth,
}) => {
const [isModalOpen, setIsModalOpen] = useState(false);
+
const [payoutTxProps, setPayoutTxProps] = useState<{
validatorAddress: string;
- era: string;
+ era: number;
}>({
+ // TODO: Should be using `null` for both values. Avoid using empty strings to circumvent TypeScript type checking.
validatorAddress: '',
- era: '',
+ era: 0,
});
const table = useReactTable({
@@ -86,7 +90,7 @@ const PayoutTable: FC = ({
),
cell: (props) => (
-
+
),
}),
columnHelper.accessor('nominators', {
@@ -115,22 +119,42 @@ const PayoutTable: FC = ({
),
cell: (props) => (
-
+
),
}),
columnHelper.accessor('nominatorTotalReward', {
header: () => (
),
- cell: (props) => (
-
- ),
+ cell: (props) => {
+ return (
+
+ );
+ },
}),
- columnHelper.accessor('status', {
+ columnHelper.display({
+ id: 'remaining',
header: () => (
-
+
),
- cell: (props) => {props.getValue()} ,
+ cell: (props) => {
+ const rowData = props.row.original;
+
+ const remainingErasToClaim = Math.abs(
+ sessionProgress && historyDepth
+ ? sessionProgress.currentEra.toNumber() -
+ historyDepth.toNumber() -
+ rowData.era
+ : 0
+ );
+
+ return (
+
+ );
+ },
}),
columnHelper.display({
id: 'claim',
@@ -143,7 +167,7 @@ const PayoutTable: FC = ({
onClick={() => {
setPayoutTxProps({
validatorAddress: rowData.validator.address,
- era: rowData.era.toString(),
+ era: rowData.era,
});
setIsModalOpen(true);
}}
diff --git a/apps/tangle-dapp/components/PayoutTable/types.ts b/apps/tangle-dapp/components/PayoutTable/types.ts
index e5e9ee4a9e..a1d56d1afd 100644
--- a/apps/tangle-dapp/components/PayoutTable/types.ts
+++ b/apps/tangle-dapp/components/PayoutTable/types.ts
@@ -1,7 +1,12 @@
-import { Payout } from '../../types';
+import { DeriveSessionProgress } from '@polkadot/api-derive/types';
+import { BN } from '@polkadot/util';
+import { Payout } from '../../types';
export interface PayoutTableProps {
data?: Payout[];
pageSize: number;
updateData: (data: Payout[]) => void;
+ sessionProgress: DeriveSessionProgress | null;
+ historyDepth: BN | null;
+ epochDuration: number | null;
}
diff --git a/apps/tangle-dapp/components/ServiceTable/ServiceTable.tsx b/apps/tangle-dapp/components/ServiceTable/ServiceTable.tsx
index 0758808f3a..1dc8beaf0f 100644
--- a/apps/tangle-dapp/components/ServiceTable/ServiceTable.tsx
+++ b/apps/tangle-dapp/components/ServiceTable/ServiceTable.tsx
@@ -30,7 +30,10 @@ const staticColumns = [
columnHelper.accessor('serviceType', {
header: () => ,
cell: (props) => (
-
+
{props.getValue()}
),
@@ -45,7 +48,7 @@ const staticColumns = [
header: () => (
),
- cell: (props) => {props.getValue()} ,
+ cell: (props) => {props.getValue().length} ,
}),
columnHelper.accessor('threshold', {
header: () => ,
@@ -71,7 +74,10 @@ const staticColumns = [
),
cell: (props) => (
-
+
),
}),
columnHelper.accessor('id', {
diff --git a/apps/tangle-dapp/components/Sidebar/sidebarProps.ts b/apps/tangle-dapp/components/Sidebar/sidebarProps.ts
index 9b183f032e..73f2cfde2b 100644
--- a/apps/tangle-dapp/components/Sidebar/sidebarProps.ts
+++ b/apps/tangle-dapp/components/Sidebar/sidebarProps.ts
@@ -1,6 +1,7 @@
import { isAppEnvironmentType } from '@webb-tools/dapp-config/types';
import {
AppsLine,
+ ArrowLeftRightLineIcon,
DocumentationIcon,
FundsLine,
GiftLineIcon,
@@ -33,6 +34,15 @@ const SIDEBAR_STATIC_ITEMS: SideBarItemProps[] = [
Icon: UserLineIcon,
subItems: [],
},
+ {
+ name: 'Bridge',
+ href: PagePath.BRIDGE,
+ isInternal: true,
+ isNext: true,
+ Icon: ArrowLeftRightLineIcon,
+ subItems: [],
+ environments: ['development', 'staging', 'test'],
+ },
{
name: 'Services',
href: '',
diff --git a/apps/tangle-dapp/components/TxConfirmationModal/TxConfirmationModal.tsx b/apps/tangle-dapp/components/TxConfirmationModal/TxConfirmationModal.tsx
deleted file mode 100644
index d972b4d7ed..0000000000
--- a/apps/tangle-dapp/components/TxConfirmationModal/TxConfirmationModal.tsx
+++ /dev/null
@@ -1,108 +0,0 @@
-'use client';
-
-import {
- ExternalLinkLine,
- ShieldedCheckLineIcon,
- SpamLineIcon,
-} from '@webb-tools/icons';
-import {
- Button,
- Modal,
- ModalContent,
- ModalFooter,
- ModalHeader,
- Typography,
-} from '@webb-tools/webb-ui-components';
-import { KeyValueWithButton } from '@webb-tools/webb-ui-components/components/KeyValueWithButton';
-import { FC, useCallback } from 'react';
-
-import useExplorerUrl from '../../hooks/useExplorerUrl';
-import { ExplorerType } from '../../types';
-import { TxConfirmationModalProps } from './types';
-
-export const TxConfirmationModal: FC = ({
- isModalOpen,
- setIsModalOpen,
- txStatus,
- txHash,
- txType,
-}) => {
- const getExplorerUrl = useExplorerUrl();
-
- const txExplorerUrl = getExplorerUrl(
- txHash,
- 'tx',
- txType === 'evm' ? ExplorerType.EVM : ExplorerType.Substrate
- );
-
- const closeModal = useCallback(() => {
- setIsModalOpen(false);
- }, [setIsModalOpen]);
-
- return (
-
-
-
- Transaction {txStatus === 'success' ? 'Successful' : 'Failed'}
-
-
-
- {txStatus === 'success' ? (
-
- ) : (
-
- )}
-
-
- {txStatus === 'success'
- ? 'Your transaction has been submitted to the network.'
- : 'Your transaction has failed to be submitted to the network.'}
-
-
-
-
- closeModal()}>
- Close
-
-
- {txExplorerUrl !== null ? (
- }
- >
- Open Explorer
-
- ) : txHash ? (
-
- ) : null}
-
-
-
- );
-};
diff --git a/apps/tangle-dapp/components/TxConfirmationModal/index.ts b/apps/tangle-dapp/components/TxConfirmationModal/index.ts
deleted file mode 100644
index e6ba38e56e..0000000000
--- a/apps/tangle-dapp/components/TxConfirmationModal/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from './TxConfirmationModal';
-export * from './types';
diff --git a/apps/tangle-dapp/components/TxConfirmationModal/types.ts b/apps/tangle-dapp/components/TxConfirmationModal/types.ts
deleted file mode 100644
index a2ee99132a..0000000000
--- a/apps/tangle-dapp/components/TxConfirmationModal/types.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export type TxConfirmationModalProps = {
- isModalOpen: boolean;
- setIsModalOpen: (isModalOpen: boolean) => void;
- txStatus: 'success' | 'error';
- txHash: string;
- txType: 'evm' | 'substrate';
-};
diff --git a/apps/tangle-dapp/components/UnbondingStatsItem/UnbondingStatsItem.tsx b/apps/tangle-dapp/components/UnbondingStatsItem/UnbondingStatsItem.tsx
index d6bdf54ea4..da3940741b 100644
--- a/apps/tangle-dapp/components/UnbondingStatsItem/UnbondingStatsItem.tsx
+++ b/apps/tangle-dapp/components/UnbondingStatsItem/UnbondingStatsItem.tsx
@@ -1,63 +1,75 @@
'use client';
-import { notificationApi } from '@webb-tools/webb-ui-components';
-import { type FC, Fragment, useMemo } from 'react';
+import { BN_ZERO } from '@polkadot/util';
+import { type FC, useMemo } from 'react';
+import useActiveAccountAddress from '../..//hooks/useActiveAccountAddress';
import useNetworkStore from '../../context/useNetworkStore';
-import useUnbondingRemainingErasSubscription from '../../data/NominatorStats/useUnbondingRemainingErasSubscription';
+import useUnbondingAmount from '../../data/NominatorStats/useUnbondingAmount';
+import useUnbonding from '../../data/staking/useUnbonding';
+import { formatBnWithCommas } from '../../utils/formatBnWithCommas';
import { formatTokenBalance } from '../../utils/polkadot';
import { NominatorStatsItem } from '../NominatorStatsItem';
-const UnbondingStatsItem: FC<{ address: string }> = ({ address }) => {
+const UnbondingStatsItem: FC = () => {
+ const activeAccountAddress = useActiveAccountAddress();
const { nativeTokenSymbol } = useNetworkStore();
- const {
- data: unbondingRemainingErasData,
- error: unbondingRemainingErasError,
- } = useUnbondingRemainingErasSubscription(address);
+ const { result: unbondingEntriesOpt, error: unbondingEntriesError } =
+ useUnbonding();
- const unbondingRemainingErasTooltip = useMemo(() => {
- if (unbondingRemainingErasError) {
- notificationApi({
- variant: 'error',
- message: unbondingRemainingErasError.message,
- });
- }
-
- if (!unbondingRemainingErasData?.value1) return null;
+ const { result: totalUnbondingAmount, error: unbondingAmountError } =
+ useUnbondingAmount();
- if (unbondingRemainingErasData.value1.length === 0) {
+ const unbondingRemainingErasTooltip = useMemo(() => {
+ if (unbondingEntriesOpt === null) {
+ return null;
+ } else if (
+ unbondingEntriesOpt.value === null ||
+ unbondingEntriesOpt.value.length === 0
+ ) {
return 'You have no unbonding tokens.';
}
- const elements = unbondingRemainingErasData.value1.map((era, index) => {
+ const elements = unbondingEntriesOpt.value.map((entry, index) => {
return (
-
-
-
- {era.remainingEras > 0 ? 'Unbonding' : 'Unbonded'}{' '}
- {formatTokenBalance(era.amount, nativeTokenSymbol)}
-
- {era.remainingEras > 0 &&
{era.remainingEras} eras remaining
}
-
-
+
+
+ {entry.remainingEras.gtn(0) ? 'Unbonding' : 'Unbonded'}{' '}
+ {formatTokenBalance(entry.amount, nativeTokenSymbol)}
+
+
+ {entry.remainingEras.gtn(0) && (
+
{formatBnWithCommas(entry.remainingEras)} eras remaining.
+ )}
+
);
});
return <>{elements}>;
- }, [
- unbondingRemainingErasError,
- unbondingRemainingErasData?.value1,
- nativeTokenSymbol,
- ]);
+ }, [unbondingEntriesOpt, nativeTokenSymbol]);
+
+ const balance =
+ // No account is active.
+ activeAccountAddress === null
+ ? '--'
+ : // Amount is still loading.
+ totalUnbondingAmount === null
+ ? null
+ : // Amount is loaded and there is an active account.
+ formatTokenBalance(
+ totalUnbondingAmount.value ?? BN_ZERO,
+ nativeTokenSymbol
+ );
return (
+ tooltip={unbondingRemainingErasTooltip ?? undefined}
+ isError={unbondingEntriesError !== null || unbondingAmountError !== null}
+ >
+ {balance}
+
);
};
diff --git a/apps/tangle-dapp/components/UpdateMetadataButton.tsx b/apps/tangle-dapp/components/UpdateMetadataButton.tsx
new file mode 100644
index 0000000000..46c68c5934
--- /dev/null
+++ b/apps/tangle-dapp/components/UpdateMetadataButton.tsx
@@ -0,0 +1,170 @@
+import {
+ InjectedExtension,
+ MetadataDef,
+} from '@polkadot/extension-inject/types';
+import { HexString } from '@polkadot/util/types';
+import { TANGLE_TOKEN_DECIMALS } from '@webb-tools/dapp-config';
+import { RefreshLineIcon } from '@webb-tools/icons';
+import {
+ IconButton,
+ Tooltip,
+ TooltipBody,
+ TooltipTrigger,
+} from '@webb-tools/webb-ui-components';
+import { NetworkId } from '@webb-tools/webb-ui-components/constants/networks';
+import _ from 'lodash';
+import { FC, useCallback, useMemo, useState } from 'react';
+
+import useNetworkStore from '../context/useNetworkStore';
+import useActiveAccountAddress from '../hooks/useActiveAccountAddress';
+import useLocalStorage, {
+ LocalStorageKey,
+ SubstrateWalletsMetadataEntry,
+} from '../hooks/useLocalStorage';
+import usePromise from '../hooks/usePromise';
+import { findInjectorForAddress, getApiPromise } from '../utils/polkadot';
+
+const UpdateMetadataButton: FC = () => {
+ const [isHidden, setIsHidden] = useState(false);
+
+ const activeAccountAddress = useActiveAccountAddress();
+ const { network } = useNetworkStore();
+
+ const { result: injector } = usePromise(
+ useCallback(() => {
+ if (activeAccountAddress === null) {
+ return Promise.resolve(null);
+ }
+
+ return findInjectorForAddress(activeAccountAddress);
+ }, [activeAccountAddress]),
+ null
+ );
+
+ const { result: apiPromise } = usePromise(
+ useCallback(
+ () => getApiPromise(network.wsRpcEndpoint),
+ [network.wsRpcEndpoint]
+ ),
+ null
+ );
+
+ const { setWithPreviousValue: setCache, valueOpt: cachedMetadata } =
+ useLocalStorage(LocalStorageKey.SUBSTRATE_WALLETS_METADATA, true);
+
+ const updateCache = useCallback(
+ (genesisHash: HexString, metadata: SubstrateWalletsMetadataEntry) => {
+ setCache((cache) => ({
+ ...cache,
+ [genesisHash]: metadata,
+ }));
+ },
+ [setCache]
+ );
+
+ const isMetadataUpToDate = useMemo(() => {
+ // Only update metadata for the mainnet. This is because
+ // the testnet and local networks have the same genesis hash,
+ // so they represent the same network. Only the mainnet's metadata
+ // is relevant.
+ if (apiPromise === null || network.id !== NetworkId.TANGLE_MAINNET) {
+ return null;
+ }
+
+ const genesisHash = apiPromise.genesisHash.toHex();
+ const cachedEntry = cachedMetadata?.value?.[genesisHash];
+
+ if (cachedEntry === undefined) {
+ return false;
+ }
+
+ return _.isEqual(cachedEntry, {
+ ss58Prefix: network.ss58Prefix,
+ tokenSymbol: network.tokenSymbol,
+ tokenDecimals: TANGLE_TOKEN_DECIMALS,
+ });
+ }, [
+ apiPromise,
+ cachedMetadata?.value,
+ network.id,
+ network.ss58Prefix,
+ network.tokenSymbol,
+ ]);
+
+ const handleClick = async () => {
+ if (
+ injector === null ||
+ activeAccountAddress === null ||
+ network.ss58Prefix === undefined
+ ) {
+ return;
+ }
+
+ const api = await getApiPromise(network.wsRpcEndpoint);
+ const genesisHash = api.genesisHash.toHex();
+
+ const metadata: MetadataDef = {
+ tokenDecimals: TANGLE_TOKEN_DECIMALS,
+ tokenSymbol: network.tokenSymbol,
+ genesisHash,
+ specVersion: api.runtimeVersion.specVersion.toNumber(),
+ icon: 'substrate',
+ ss58Format: network.ss58Prefix,
+ types: {},
+ chain: api.runtimeChain.toString(),
+ };
+
+ // The 'provide' request will throw an error if the user
+ // rejects the request. Leniently catch the error and log it.
+ const handleError = (error: unknown) => {
+ console.error(
+ `Failed to provide updated metadata to injected extension: ${error}`
+ );
+ };
+
+ await injector.metadata
+ ?.provide(metadata)
+ // Only update the cache if the metadata was successfully
+ // provided.
+ .then(() => {
+ if (network.ss58Prefix === undefined) {
+ return;
+ }
+
+ updateCache(genesisHash, {
+ ss58Prefix: network.ss58Prefix,
+ tokenSymbol: network.tokenSymbol,
+ tokenDecimals: TANGLE_TOKEN_DECIMALS,
+ });
+
+ // Hide the button after the metadata has been updated.
+ setIsHidden(true);
+ })
+ .catch(handleError);
+ };
+
+ // Hide the button if the metadata is up-to-date, if it's not yet known,
+ // and after metadata has been updated.
+ if (isMetadataUpToDate === null || isMetadataUpToDate || isHidden) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ Update metadata for Substrate wallets
+
+
+ );
+};
+
+export default UpdateMetadataButton;
diff --git a/apps/tangle-dapp/components/ValidatorList/ValidatorList.tsx b/apps/tangle-dapp/components/ValidatorList/ValidatorList.tsx
deleted file mode 100644
index 2261816af2..0000000000
--- a/apps/tangle-dapp/components/ValidatorList/ValidatorList.tsx
+++ /dev/null
@@ -1,295 +0,0 @@
-'use client';
-
-import {
- createColumnHelper,
- getCoreRowModel,
- getFilteredRowModel,
- getPaginationRowModel,
- useReactTable,
-} from '@tanstack/react-table';
-import { ArrowDropDownFill, ArrowDropUpFill, Search } from '@webb-tools/icons';
-import {
- Avatar,
- CheckBox,
- Chip,
- CopyWithTooltip,
- fuzzyFilter,
- Input,
- shortenString,
- Table,
- Typography,
-} from '@webb-tools/webb-ui-components';
-import React, { FC, useCallback, useMemo, useState } from 'react';
-
-import { ContainerSkeleton } from '../../components';
-import { Validator } from '../../types';
-import { HeaderCell } from '../tableCells';
-import { SortableKeys, SortBy, ValidatorListTableProps } from './types';
-
-const columnHelper = createColumnHelper();
-
-export const ValidatorListTable: FC = ({
- data,
- selectedValidators,
- setSelectedValidators,
-}) => {
- const [searchValue, setSearchValue] = useState('');
- const [totalStakeSortBy, setTotalStakeSortBy] = useState('dsc');
- const [nominationsSortBy, setNominationsSortBy] = useState('dsc');
- const [commissionSortBy, setCommissionSortBy] = useState('dsc');
- const [sortBy, setSortBy] = useState('effectiveAmountStaked');
-
- const sortedData = useMemo(() => {
- const selectedData = data.filter((validator) =>
- selectedValidators.includes(validator.address)
- );
- const unselectedData = data.filter(
- (validator) => !selectedValidators.includes(validator.address)
- );
-
- const sortData = (
- data: Validator[],
- sortBy: SortableKeys,
- sortOrder: SortBy
- ) => {
- return [...data].sort((a, b) => {
- const valueA = sortOrder === 'asc' ? a[sortBy] : b[sortBy];
- const valueB = sortOrder === 'asc' ? b[sortBy] : a[sortBy];
- return parseFloat(valueA) - parseFloat(valueB);
- });
- };
-
- const sortedSelectedData = sortData(
- selectedData,
- sortBy,
- sortBy === 'effectiveAmountStaked'
- ? totalStakeSortBy
- : sortBy === 'delegations'
- ? nominationsSortBy
- : commissionSortBy
- );
-
- const sortedUnselectedData = sortData(
- unselectedData,
- sortBy,
- sortBy === 'effectiveAmountStaked'
- ? totalStakeSortBy
- : sortBy === 'delegations'
- ? nominationsSortBy
- : commissionSortBy
- );
-
- return [...sortedSelectedData, ...sortedUnselectedData];
- }, [
- data,
- selectedValidators,
- sortBy,
- totalStakeSortBy,
- nominationsSortBy,
- commissionSortBy,
- ]);
-
- const filteredData = useMemo(
- () =>
- sortedData.filter(
- (validator) =>
- validator.identityName
- .toLowerCase()
- .includes(searchValue.toLowerCase()) ||
- validator.address.toLowerCase().includes(searchValue.toLowerCase())
- ),
- [searchValue, sortedData]
- );
-
- const handleValidatorToggle = useCallback(
- (address: string, isSelected: boolean) => {
- if (isSelected) {
- setSelectedValidators(
- selectedValidators.filter(
- (selectedValidator) => selectedValidator !== address
- )
- );
- } else {
- setSelectedValidators([...selectedValidators, address]);
- }
- },
- [selectedValidators, setSelectedValidators]
- );
-
- const columns = [
- columnHelper.accessor('address', {
- header: () => ,
- cell: (props) => {
- const address = props.getValue();
- const identity = props.row.original.identityName;
-
- return (
-
-
- handleValidatorToggle(
- address,
- selectedValidators.includes(address)
- )
- }
- />
-
-
-
- hello
-
-
-
- {identity === address ? shortenString(address, 6) : identity}
-
-
-
-
-
- );
- },
- }),
- columnHelper.accessor('effectiveAmountStaked', {
- header: () => (
-
-
-
- {totalStakeSortBy === 'asc' ? (
-
{
- setTotalStakeSortBy('dsc');
- setSortBy('effectiveAmountStaked');
- }}
- />
- ) : (
- {
- setTotalStakeSortBy('asc');
- setSortBy('effectiveAmountStaked');
- }}
- />
- )}
-
- ),
- cell: (props) => (
-
- {props.getValue()}
-
- ),
- }),
- columnHelper.accessor('delegations', {
- header: () => (
-
-
-
- {nominationsSortBy === 'asc' ? (
-
{
- setNominationsSortBy('dsc');
- setSortBy('delegations');
- }}
- />
- ) : (
- {
- setNominationsSortBy('asc');
- setSortBy('delegations');
- }}
- />
- )}
-
- ),
- cell: (props) => (
-
- {props.getValue()}
-
- ),
- }),
- columnHelper.accessor('commission', {
- header: () => (
-
-
-
- {commissionSortBy === 'asc' ? (
-
{
- setCommissionSortBy('dsc');
- setSortBy('commission');
- }}
- />
- ) : (
- {
- setCommissionSortBy('asc');
- setSortBy('commission');
- }}
- />
- )}
-
- ),
- cell: (props) => (
-
- {Number(props.getValue()).toFixed(2)}%
-
- ),
- }),
- ];
-
- const table = useReactTable({
- data: filteredData,
- columns,
- filterFns: {
- fuzzy: fuzzyFilter,
- },
- globalFilterFn: fuzzyFilter,
- getCoreRowModel: getCoreRowModel(),
- getFilteredRowModel: getFilteredRowModel(),
- getPaginationRowModel: getPaginationRowModel(),
- });
-
- return (
-
-
}
- placeholder="Search validators..."
- value={searchValue}
- onChange={(val) => setSearchValue(val)}
- className="mb-1"
- />
-
- {filteredData.length === 0 ? (
-
- ) : (
-
- )}
-
- );
-};
diff --git a/apps/tangle-dapp/components/ValidatorList/index.ts b/apps/tangle-dapp/components/ValidatorList/index.ts
deleted file mode 100644
index 977593fd7b..0000000000
--- a/apps/tangle-dapp/components/ValidatorList/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './ValidatorList';
diff --git a/apps/tangle-dapp/components/ValidatorList/types.ts b/apps/tangle-dapp/components/ValidatorList/types.ts
index 4ab42ada1b..aa1a9e7c96 100644
--- a/apps/tangle-dapp/components/ValidatorList/types.ts
+++ b/apps/tangle-dapp/components/ValidatorList/types.ts
@@ -1,14 +1,9 @@
-import { Validator } from '../../types';
+import type { Dispatch, SetStateAction } from 'react';
+
+import type { Validator } from '../../types';
export interface ValidatorListTableProps {
data: Validator[];
- selectedValidators: string[];
- setSelectedValidators: (selectedValidators: string[]) => void;
+ pageSize: number;
+ setSelectedValidators: Dispatch>>;
}
-
-export type SortBy = 'asc' | 'dsc';
-
-export type SortableKeys =
- | 'effectiveAmountStaked'
- | 'delegations'
- | 'commission';
diff --git a/apps/tangle-dapp/components/ValidatorSelectionTable/ValidatorSelectionTable.tsx b/apps/tangle-dapp/components/ValidatorSelectionTable/ValidatorSelectionTable.tsx
new file mode 100644
index 0000000000..8dc2085d32
--- /dev/null
+++ b/apps/tangle-dapp/components/ValidatorSelectionTable/ValidatorSelectionTable.tsx
@@ -0,0 +1,359 @@
+'use client';
+
+import { BN } from '@polkadot/util';
+import {
+ type Column,
+ createColumnHelper,
+ getCoreRowModel,
+ getFilteredRowModel,
+ getPaginationRowModel,
+ getSortedRowModel,
+ type PaginationState,
+ type Row,
+ type RowSelectionState,
+ type SortingColumn,
+ type SortingState,
+ type TableOptions,
+ useReactTable,
+} from '@tanstack/react-table';
+import { ArrowDropDownFill, ArrowDropUpFill, Search } from '@webb-tools/icons';
+import {
+ Avatar,
+ CheckBox,
+ Chip,
+ CopyWithTooltip,
+ fuzzyFilter,
+ Input,
+ shortenString,
+ Table,
+ Typography,
+} from '@webb-tools/webb-ui-components';
+import cx from 'classnames';
+import React, {
+ FC,
+ startTransition,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
+
+import { Validator } from '../../types';
+import calculateCommission from '../../utils/calculateCommission';
+import { ContainerSkeleton } from '..';
+import { HeaderCell } from '../tableCells';
+import TokenAmountCell from '../tableCells/TokenAmountCell';
+import { ValidatorSelectionTableProps } from './types';
+
+const DEFAULT_PAGINATION: PaginationState = {
+ pageIndex: 0,
+ pageSize: 20,
+};
+
+const columnHelper = createColumnHelper();
+
+const ValidatorSelectionTable: FC = ({
+ allValidators,
+ setSelectedValidators,
+}) => {
+ const [searchValue, setSearchValue] = useState('');
+ const [rowSelection, setRowSelection] = useState({});
+ const [sorting, setSorting] = useState([]);
+
+ const [pagination, setPagination] =
+ useState(DEFAULT_PAGINATION);
+
+ const toggleSortSelectionHandlerRef = useRef<
+ SortingColumn['toggleSorting'] | null
+ >(null);
+
+ // Sync the selected validators with the parent state
+ useEffect(() => {
+ startTransition(() => {
+ setSelectedValidators(new Set(Object.keys(rowSelection)));
+ });
+ }, [rowSelection, setSelectedValidators]);
+
+ const columns = useMemo(
+ () => [
+ columnHelper.accessor('address', {
+ header: ({ header }) => {
+ toggleSortSelectionHandlerRef.current = header.column.toggleSorting;
+ return ;
+ },
+ cell: (props) => {
+ const address = props.getValue();
+ const identity = props.row.original.identityName;
+
+ return (
+
+
+
+
+
+
+
+ {identity === address ? shortenString(address, 6) : identity}
+
+
+
+
+
+ );
+ },
+ // Sort the selected validators first
+ sortingFn: (rowA, rowB) => {
+ const rowASelected = rowA.getIsSelected();
+ const rowBSelected = rowB.getIsSelected();
+
+ if (rowASelected && !rowBSelected) {
+ return -1;
+ }
+
+ if (!rowASelected && rowBSelected) {
+ return 1;
+ }
+
+ return 0;
+ },
+ }),
+ columnHelper.accessor('totalStakeAmount', {
+ header: ({ header }) => (
+
+
+
+
+
+ ),
+ cell: (props) => (
+
+
+
+
+
+ ),
+ sortingFn,
+ }),
+ columnHelper.accessor('nominatorCount', {
+ header: ({ header }) => (
+
+
+
+
+
+ ),
+ cell: (props) => (
+
+ {props.getValue()}
+
+ ),
+ sortingFn,
+ }),
+ columnHelper.accessor('commission', {
+ header: ({ header }) => (
+
+
+
+
+
+ ),
+ cell: (props) => (
+
+
+ {calculateCommission(props.getValue()).toFixed(2)}%
+
+
+ ),
+ sortingFn,
+ }),
+ columnHelper.accessor('identityName', {
+ header: () => ,
+ cell: (props) => props.getValue(),
+ }),
+ ],
+ []
+ );
+
+ const tableProps = useMemo>(
+ () => ({
+ data: allValidators,
+ columns,
+ state: {
+ columnVisibility: {
+ identityName: false,
+ },
+ sorting,
+ rowSelection,
+ pagination,
+ globalFilter: searchValue,
+ },
+ enableRowSelection: true,
+ onPaginationChange: setPagination,
+ onGlobalFilterChange: (props) => {
+ setPagination(DEFAULT_PAGINATION);
+ setSearchValue(props);
+ },
+ onRowSelectionChange: (props) => {
+ toggleSortSelectionHandlerRef.current?.(false);
+ setRowSelection(props);
+ },
+ onSortingChange: (updaterOrValue) => {
+ if (typeof updaterOrValue === 'function') {
+ setSorting((prev) => {
+ const newSorting = updaterOrValue(prev);
+
+ // Modify the sorting state to always sort by the selected validators first
+ if (newSorting.length === 0) {
+ return [
+ {
+ id: 'address',
+ desc: false,
+ },
+ ];
+ } else if (newSorting[0].id === 'address') {
+ return newSorting;
+ } else {
+ return [
+ {
+ id: 'address',
+ desc: false,
+ },
+ ...newSorting,
+ ];
+ }
+ });
+ } else {
+ setSorting(updaterOrValue);
+ }
+ },
+ filterFns: {
+ fuzzy: fuzzyFilter,
+ },
+ globalFilterFn: fuzzyFilter,
+ getCoreRowModel: getCoreRowModel(),
+ getFilteredRowModel: getFilteredRowModel(),
+ getPaginationRowModel: getPaginationRowModel(),
+ getSortedRowModel: getSortedRowModel(),
+ getRowId: (row) => row.address,
+ autoResetPageIndex: false,
+ }),
+ [columns, allValidators, pagination, rowSelection, searchValue, sorting]
+ );
+
+ const table = useReactTable(tableProps);
+
+ return (
+ <>
+
+
}
+ placeholder="Search validators..."
+ value={searchValue}
+ onChange={(val) => setSearchValue(val)}
+ className="mb-1"
+ />
+
+ {allValidators.length === 0 ? (
+
+ ) : (
+
+ )}
+
+
+
+ Selected: {Object.keys(rowSelection).length}/
+ {table.getPreFilteredRowModel().rows.length}
+
+ >
+ );
+};
+
+type ColumnIdAssertFn = (
+ columnId: string
+) => asserts columnId is keyof Validator;
+
+const assertColumnId: ColumnIdAssertFn = (columnId) => {
+ if (
+ !['address', 'effectiveAmountStaked', 'delegations', 'commission'].includes(
+ columnId
+ )
+ ) {
+ throw new Error(`Invalid columnId: ${columnId}`);
+ }
+};
+
+const sortingFn = (
+ rowA: Row,
+ rowB: Row,
+ columnId: string
+) => {
+ assertColumnId(columnId);
+
+ if (columnId === 'totalStakeAmount') {
+ const totalStakedA = rowA.original.totalStakeAmount;
+ const totalStakedB = rowB.original.totalStakeAmount;
+ const result = totalStakedA.sub(totalStakedB);
+
+ return result.ltn(0) ? -1 : result.gtn(0) ? 1 : 0;
+ }
+
+ // TODO: Avoid using Number() here, if it is a BN value, it should be compared as such.
+ const rowAValue = Number(rowA.original[columnId]);
+ const rowBValue = Number(rowB.original[columnId]);
+
+ return rowAValue - rowBValue;
+};
+
+const SortArrow: FC<{ column: Column }> = ({
+ column,
+}) => {
+ const isSorted = column.getIsSorted();
+
+ if (!isSorted) {
+ return null;
+ }
+
+ return isSorted === 'asc' ? (
+
+ ) : (
+
+ );
+};
+
+export default React.memo(ValidatorSelectionTable);
diff --git a/apps/tangle-dapp/components/ValidatorSelectionTable/index.ts b/apps/tangle-dapp/components/ValidatorSelectionTable/index.ts
new file mode 100644
index 0000000000..bd17db84aa
--- /dev/null
+++ b/apps/tangle-dapp/components/ValidatorSelectionTable/index.ts
@@ -0,0 +1 @@
+export * from './ValidatorSelectionTable';
diff --git a/apps/tangle-dapp/components/ValidatorSelectionTable/types.ts b/apps/tangle-dapp/components/ValidatorSelectionTable/types.ts
new file mode 100644
index 0000000000..05661d7f96
--- /dev/null
+++ b/apps/tangle-dapp/components/ValidatorSelectionTable/types.ts
@@ -0,0 +1,15 @@
+import { Dispatch, SetStateAction } from 'react';
+
+import { Validator } from '../../types';
+
+export type ValidatorSelectionTableProps = {
+ allValidators: Validator[];
+ setSelectedValidators: Dispatch>>;
+};
+
+export type SortBy = 'asc' | 'dsc';
+
+export type SortableKeys = keyof Pick<
+ Validator,
+ 'commission' | 'nominatorCount' | 'totalStakeAmount'
+>;
diff --git a/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx b/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx
index 65242051f4..209af6c2f4 100644
--- a/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx
+++ b/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx
@@ -12,6 +12,7 @@ import { getExplorerURI } from '@webb-tools/api-provider-environment/transaction
import {
Avatar,
Button,
+ Chip,
CopyWithTooltip,
ExternalLinkIcon,
fuzzyFilter,
@@ -22,59 +23,91 @@ import {
import Link from 'next/link';
import { FC, useMemo } from 'react';
+import { IS_PRODUCTION_ENV } from '../../constants/env';
import useNetworkStore from '../../context/useNetworkStore';
import { ExplorerType, PagePath, Validator } from '../../types';
+import calculateCommission from '../../utils/calculateCommission';
import { HeaderCell, StringCell } from '../tableCells';
+import TokenAmountCell from '../tableCells/TokenAmountCell';
import { ValidatorTableProps } from './types';
const columnHelper = createColumnHelper();
-const staticColumns = [
- columnHelper.accessor('selfStaked', {
- header: () => ,
- cell: (props) => (
-
- ),
- }),
- columnHelper.accessor('effectiveAmountStaked', {
- header: () => (
-
- ),
- cell: (props) => (
-
- ),
- }),
- columnHelper.accessor('delegations', {
+const getStaticColumns = (isWaiting?: boolean) => [
+ // TODO: Hide this for live app for now
+ ...(IS_PRODUCTION_ENV
+ ? []
+ : [
+ columnHelper.accessor('activeServicesCount', {
+ header: () => ,
+ cell: (props) => (
+
+ {props.getValue()}
+
+ ),
+ }),
+ columnHelper.accessor('restakedAmount', {
+ header: () => ,
+ cell: (props) => ,
+ }),
+ ]),
+ // Hide the effective amount staked and self-staked columns on waiting validators tab
+ // as they don't have values for these columns
+ ...(isWaiting
+ ? []
+ : [
+ columnHelper.accessor('totalStakeAmount', {
+ header: () => (
+
+ ),
+ cell: (props) => ,
+ }),
+ columnHelper.accessor('selfStakeAmount', {
+ header: () => (
+
+ ),
+ cell: (props) => ,
+ }),
+ ]),
+ columnHelper.accessor('nominatorCount', {
header: () => ,
cell: (props) => (
-
+
),
}),
columnHelper.accessor('commission', {
- header: () => ,
+ header: () => ,
cell: (props) => (
),
}),
- columnHelper.accessor('address', {
- id: 'details',
- header: () => null,
- cell: (props) => (
-
-
-
- DETAILS
-
-
-
- ),
- }),
+ // TODO: Hide this for live app for now
+ ...(IS_PRODUCTION_ENV
+ ? []
+ : [
+ columnHelper.accessor('address', {
+ id: 'details',
+ header: () => null,
+ cell: (props) => (
+
+
+
+ DETAILS
+
+
+
+ ),
+ }),
+ ]),
];
-const ValidatorTable: FC = ({ data }) => {
+const ValidatorTable: FC = ({ data, isWaiting }) => {
const { network } = useNetworkStore();
const columns = useMemo(
@@ -120,9 +153,9 @@ const ValidatorTable: FC = ({ data }) => {
);
},
}),
- ...staticColumns,
+ ...getStaticColumns(isWaiting),
],
- [network.polkadotExplorerUrl]
+ [isWaiting, network.polkadotExplorerUrl]
);
const table = useReactTable({
@@ -142,9 +175,7 @@ const ValidatorTable: FC = ({ data }) => {
= ({
return isDisplayingEvmAddress
? activeAddress
- : evmToSubstrateAddress(activeAddress);
+ : toSubstrateAddress(activeAddress);
}, [activeAddress, isDisplayingEvmAddress]);
const possiblyHiddenAddress = useMemo(
diff --git a/apps/tangle-dapp/components/account/Actions.tsx b/apps/tangle-dapp/components/account/Actions.tsx
index 109d872006..70bc1fcf9d 100644
--- a/apps/tangle-dapp/components/account/Actions.tsx
+++ b/apps/tangle-dapp/components/account/Actions.tsx
@@ -1,6 +1,5 @@
'use client';
-import { ZERO_BIG_INT } from '@webb-tools/dapp-config/constants';
import {
ArrowLeftRightLineIcon,
CoinsLineIcon,
@@ -13,7 +12,6 @@ import { FC, useState } from 'react';
import TransferTxContainer from '../../containers/TransferTxContainer/TransferTxContainer';
import useNetworkStore from '../../context/useNetworkStore';
import useBalances from '../../data/balances/useBalances';
-import usePendingEVMBalance from '../../data/balances/usePendingEVMBalance';
import useAirdropEligibility from '../../data/claims/useAirdropEligibility';
import usePayoutsAvailability from '../../data/payouts/usePayoutsAvailability';
import useVestingInfo from '../../data/vesting/useVestingInfo';
@@ -23,7 +21,7 @@ import { TxStatus } from '../../hooks/useSubstrateTx';
import { PagePath, StaticSearchQueryPath } from '../../types';
import { formatTokenBalance } from '../../utils/polkadot';
import ActionItem from './ActionItem';
-import WithdrawEVMBalanceAction from './WithdrawEVMBalanceAction';
+import WithdrawEvmBalanceAction from './WithdrawEvmBalanceAction';
const Actions: FC = () => {
const { nativeTokenSymbol } = useNetworkStore();
@@ -37,8 +35,6 @@ const Actions: FC = () => {
const { transferable: transferableBalance } = useBalances();
- const { balance, ...restPendingEVMBalanceProps } = usePendingEVMBalance();
-
const {
isVesting,
hasClaimableTokens: hasClaimableVestingTokens,
@@ -119,7 +115,7 @@ const Actions: FC = () => {
>
) : (
<>
- There are vesting schedules in your account, but no tokens
+ There are vesting schedules in this account, but no tokens
have vested yet.
>
)
@@ -127,12 +123,7 @@ const Actions: FC = () => {
/>
)}
- {balance !== null && balance > ZERO_BIG_INT && (
-
- )}
+
{/* TODO: Might be better to use a hook instead of doing it this way. */}
diff --git a/apps/tangle-dapp/components/account/Balance.tsx b/apps/tangle-dapp/components/account/Balance.tsx
index 9faa9eccfa..ac66e5ae8a 100644
--- a/apps/tangle-dapp/components/account/Balance.tsx
+++ b/apps/tangle-dapp/components/account/Balance.tsx
@@ -3,13 +3,11 @@ import {
HiddenValueEye,
Typography,
} from '@webb-tools/webb-ui-components';
-import { FC, useMemo } from 'react';
+import { FC } from 'react';
import useNetworkStore from '../../context/useNetworkStore';
import useBalances from '../../data/balances/useBalances';
-import formatBnToDisplayAmount from '../../utils/formatBnToDisplayAmount';
import { formatTokenBalance } from '../../utils/polkadot';
-import { InfoIconWithTooltip } from '..';
const Balance: FC = () => {
const { transferable: balance } = useBalances();
@@ -22,21 +20,6 @@ const Balance: FC = () => {
const prefix = parts?.[0] ?? '--';
const suffix = parts?.[1] ?? nativeTokenSymbol;
- const formattedExtendedBalance = useMemo(() => {
- if (balance === null) {
- return null;
- }
-
- const amount = formatBnToDisplayAmount(balance, {
- // Show up to 4 decimal places.
- fractionLength: 4,
- includeCommas: true,
- padZerosInFraction: true,
- });
-
- return `${amount} ${nativeTokenSymbol}`;
- }, [balance, nativeTokenSymbol]);
-
return (
@@ -63,10 +46,6 @@ const Balance: FC = () => {
className="!leading-none pb-1 flex gap-2"
>
{suffix}
-
- {balance !== null && !balance.isZero() && (
-
- )}
diff --git a/apps/tangle-dapp/components/account/WithdrawEVMBalanceAction.tsx b/apps/tangle-dapp/components/account/WithdrawEVMBalanceAction.tsx
deleted file mode 100644
index 042aab6c19..0000000000
--- a/apps/tangle-dapp/components/account/WithdrawEVMBalanceAction.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-'use client';
-
-import RefundLineIcon from '@webb-tools/icons/RefundLineIcon';
-import Spinner from '@webb-tools/icons/Spinner';
-import { useWebbUI } from '@webb-tools/webb-ui-components/hooks/useWebbUI';
-import { FC, useCallback, useEffect } from 'react';
-
-import useNetworkStore from '../../context/useNetworkStore';
-import usePendingEVMBalance from '../../data/balances/usePendingEVMBalance';
-import { TxStatus } from '../../hooks/useSubstrateTx';
-import { formatTokenBalance } from '../../utils/polkadot/tokens';
-import ActionItem from './ActionItem';
-
-type WithdrawEVMBalanceActionProps = ReturnType & {
- balance: bigint;
-};
-
-const WithdrawEVMBalanceAction: FC = ({
- balance,
- execute,
- status,
- error,
-}) => {
- const { nativeTokenSymbol } = useNetworkStore();
- const { notificationApi } = useWebbUI();
-
- const handleWithdraw = useCallback(() => execute?.(), [execute]);
-
- const formattedBalance = formatTokenBalance(balance, nativeTokenSymbol);
-
- useEffect(() => {
- if (status === TxStatus.COMPLETE) {
- notificationApi({
- message: 'Withdraw successful',
- secondaryMessage: `You have successfully withdrawn ${formattedBalance}`,
- variant: 'success',
- });
- }
-
- if (status === TxStatus.ERROR) {
- notificationApi({
- message: `Withdraw failed`,
- secondaryMessage: error?.message ?? 'Failed to withdraw funds',
- variant: 'error',
- });
- }
- }, [error?.message, notificationApi, status, formattedBalance]);
-
- // If withdraw is successful, don't show the action item
- if (status === TxStatus.COMPLETE) {
- return null;
- }
-
- return (
-
- : RefundLineIcon
- }
- tooltip={
- status === TxStatus.ERROR ? (
- <>Oops, something went wrong. Please try again.>
- ) : (
- <>You have {formattedBalance} to withdraw, click to withdraw.>
- )
- }
- isDisabled={status === TxStatus.PROCESSING}
- onClick={handleWithdraw}
- />
- );
-};
-
-export default WithdrawEVMBalanceAction;
diff --git a/apps/tangle-dapp/components/account/WithdrawEvmBalanceAction.tsx b/apps/tangle-dapp/components/account/WithdrawEvmBalanceAction.tsx
new file mode 100644
index 0000000000..04146b54ef
--- /dev/null
+++ b/apps/tangle-dapp/components/account/WithdrawEvmBalanceAction.tsx
@@ -0,0 +1,87 @@
+'use client';
+
+import { ZERO_BIG_INT } from '@webb-tools/dapp-config';
+import RefundLineIcon from '@webb-tools/icons/RefundLineIcon';
+import Spinner from '@webb-tools/icons/Spinner';
+import { FC, useCallback, useMemo } from 'react';
+
+import useNetworkStore from '../../context/useNetworkStore';
+import useEvmBalanceWithdrawTx from '../../data/balances/useEvmBalanceWithdrawTx';
+import usePendingEvmBalance from '../../data/balances/usePendingEvmBalance';
+import useAgnosticAccountInfo from '../../hooks/useAgnosticAccountInfo';
+import { TxStatus } from '../../hooks/useSubstrateTx';
+import { formatTokenBalance } from '../../utils/polkadot';
+import { toEvmAddress20 } from '../../utils/toEvmAddress20';
+import ActionItem from './ActionItem';
+
+const WithdrawEvmBalanceAction: FC = () => {
+ const { nativeTokenSymbol } = useNetworkStore();
+ const { substrateAddress, isEvm } = useAgnosticAccountInfo();
+ const pendingEvmBalance = usePendingEvmBalance();
+
+ const evmAddress20 = useMemo(() => {
+ // Only Substrate accounts can withdraw EVM balances.
+ if (substrateAddress === null || isEvm) {
+ return null;
+ }
+
+ return toEvmAddress20(substrateAddress);
+ }, [isEvm, substrateAddress]);
+
+ const tokenAmountStr = useMemo(
+ () =>
+ pendingEvmBalance
+ ? formatTokenBalance(pendingEvmBalance, nativeTokenSymbol)
+ : null,
+ [pendingEvmBalance, nativeTokenSymbol]
+ );
+
+ const { execute, status } = useEvmBalanceWithdrawTx(tokenAmountStr);
+
+ const handleWithdraw = useCallback(async () => {
+ await execute({
+ pendingEvmBalance,
+ evmAddress20,
+ });
+ }, [execute, pendingEvmBalance, evmAddress20]);
+
+ // If withdraw was successful, don't show the action item.
+ if (status === TxStatus.COMPLETE) {
+ return null;
+ }
+ // Nothing to withdraw or not available.
+ else if (
+ pendingEvmBalance === null ||
+ pendingEvmBalance === ZERO_BIG_INT ||
+ tokenAmountStr === null
+ ) {
+ return null;
+ }
+
+ return (
+
+ : RefundLineIcon
+ }
+ tooltip={
+ status === TxStatus.ERROR ? (
+ <>Oops, something went wrong. Please try again.>
+ ) : (
+ <>
+ {tokenAmountStr} is available to withdraw. Use this
+ action to release the funds.
+ >
+ )
+ }
+ />
+ );
+};
+
+export default WithdrawEvmBalanceAction;
diff --git a/apps/tangle-dapp/components/index.ts b/apps/tangle-dapp/components/index.ts
index 18dc3ed271..014b99cdda 100644
--- a/apps/tangle-dapp/components/index.ts
+++ b/apps/tangle-dapp/components/index.ts
@@ -1,10 +1,11 @@
export * from './BondedTokensBalanceInfo';
export * from './Breadcrumbs';
-export * from './DelegatorTable';
export { default as GlassCard } from './GlassCard';
export * from './HeaderChip';
export * from './InfoIconWithTooltip';
export * from './KeyStatsItem';
+export * from './NominationsTable';
+export * from './NominationsTable';
export * from './NominatorStatsItem';
export * from './PayoutTable';
export * from './ServicesKeyMetricItem';
@@ -16,6 +17,6 @@ export * from './TableStatus';
export { default as TangleBigLogo } from './TangleBigLogo';
export { default as TangleCard } from './TangleCard';
export * from './UnbondingStatsItem';
-export * from './ValidatorList';
+export * from './ValidatorSelectionTable';
export * from './ValidatorTable';
export * from './WalletDropdown';
diff --git a/apps/tangle-dapp/components/tableCells/HeaderCell.tsx b/apps/tangle-dapp/components/tableCells/HeaderCell.tsx
index ba840d8868..dbb1876214 100644
--- a/apps/tangle-dapp/components/tableCells/HeaderCell.tsx
+++ b/apps/tangle-dapp/components/tableCells/HeaderCell.tsx
@@ -11,12 +11,13 @@ const HeaderCell: FC = ({ title, tooltip, className }) => {
variant="body1"
fw="bold"
className={twMerge(
- 'text-mono-140 dark:text-mono-40 flex-[1] whitespace-nowrap',
- 'flex items-center justify-center gap-0.5',
+ 'whitespace-nowrap text-mono-140 dark:text-mono-40 flex-[1]',
+ 'flex items-center gap-0.5',
className
)}
>
{title}
+
{tooltip && }
);
diff --git a/apps/tangle-dapp/components/tableCells/TokenAmountCell.tsx b/apps/tangle-dapp/components/tableCells/TokenAmountCell.tsx
new file mode 100644
index 0000000000..6f365a767a
--- /dev/null
+++ b/apps/tangle-dapp/components/tableCells/TokenAmountCell.tsx
@@ -0,0 +1,37 @@
+import { BN } from '@polkadot/util';
+import { FC } from 'react';
+import { twMerge } from 'tailwind-merge';
+
+import useNetworkStore from '../../context/useNetworkStore';
+import { formatTokenBalance } from '../../utils/polkadot';
+
+export type TokenAmountCellProps = {
+ amount: BN;
+ className?: string;
+};
+
+const TokenAmountCell: FC = ({ amount, className }) => {
+ const { nativeTokenSymbol } = useNetworkStore();
+ const formattedBalance = formatTokenBalance(amount);
+
+ const parts = formattedBalance.split('.');
+ const integerPart = parts[0];
+ const decimalPart = parts.at(1);
+
+ return (
+
+ {integerPart}
+
+
+ {decimalPart !== undefined && `.${decimalPart}`} {nativeTokenSymbol}
+
+
+ );
+};
+
+export default TokenAmountCell;
diff --git a/apps/tangle-dapp/constants/bridge.ts b/apps/tangle-dapp/constants/bridge.ts
new file mode 100644
index 0000000000..5fab56bd6b
--- /dev/null
+++ b/apps/tangle-dapp/constants/bridge.ts
@@ -0,0 +1,81 @@
+import { PresetTypedChainId } from '@webb-tools/dapp-types';
+
+import { BridgeTokenId, BridgeTokenType, ChainId } from '../types/bridge';
+
+export const BRIDGE_SUPPORTED_TOKENS: Record = {
+ tTNT: {
+ id: 'tTNT',
+ symbol: 'tTNT',
+ existentialDeposit: {},
+ destChainTransactionFee: {},
+ },
+ TNT: {
+ id: 'TNT',
+ symbol: 'TNT',
+ existentialDeposit: {},
+ destChainTransactionFee: {},
+ },
+};
+
+// A Map with key as source chain id and value as another map
+// with key as destination chain id and value as supported tokens
+type BridgeType = Record<
+ ChainId, // Source Chain Id
+ Record<
+ ChainId, // Destination Chain Id
+ {
+ supportedTokens: BridgeTokenId[];
+ }
+ >
+>;
+
+// TODO: This is a dummy data for now
+export const BRIDGE: BridgeType = {
+ [PresetTypedChainId.Sepolia]: {
+ [PresetTypedChainId.TangleMainnetEVM]: {
+ supportedTokens: ['TNT'],
+ },
+ [PresetTypedChainId.TangleTestnetEVM]: {
+ supportedTokens: ['tTNT'],
+ },
+ [PresetTypedChainId.Polkadot]: {
+ supportedTokens: ['tTNT', 'TNT'],
+ },
+ },
+
+ [PresetTypedChainId.Polkadot]: {
+ [PresetTypedChainId.TangleTestnetEVM]: {
+ supportedTokens: ['tTNT'],
+ },
+ [PresetTypedChainId.TangleMainnetEVM]: {
+ supportedTokens: ['TNT'],
+ },
+ [PresetTypedChainId.Sepolia]: {
+ supportedTokens: ['tTNT', 'TNT'],
+ },
+ },
+
+ [PresetTypedChainId.TangleMainnetEVM]: {
+ [PresetTypedChainId.TangleTestnetEVM]: {
+ supportedTokens: ['tTNT', 'TNT'],
+ },
+ [PresetTypedChainId.Sepolia]: {
+ supportedTokens: ['TNT'],
+ },
+ [PresetTypedChainId.Polkadot]: {
+ supportedTokens: ['TNT'],
+ },
+ },
+
+ [PresetTypedChainId.TangleTestnetEVM]: {
+ [PresetTypedChainId.TangleMainnetEVM]: {
+ supportedTokens: ['tTNT', 'TNT'],
+ },
+ [PresetTypedChainId.Sepolia]: {
+ supportedTokens: ['TNT'],
+ },
+ [PresetTypedChainId.Polkadot]: {
+ supportedTokens: ['tTNT', 'TNT'],
+ },
+ },
+};
diff --git a/apps/tangle-dapp/constants/env.ts b/apps/tangle-dapp/constants/env.ts
new file mode 100644
index 0000000000..15783da71f
--- /dev/null
+++ b/apps/tangle-dapp/constants/env.ts
@@ -0,0 +1 @@
+export const IS_PRODUCTION_ENV = process.env.NODE_ENV === 'production';
diff --git a/apps/tangle-dapp/constants/evmPrecompiles.ts b/apps/tangle-dapp/constants/evmPrecompiles.ts
index 3d8bf5d909..6dd41501ce 100644
--- a/apps/tangle-dapp/constants/evmPrecompiles.ts
+++ b/apps/tangle-dapp/constants/evmPrecompiles.ts
@@ -56,6 +56,7 @@ type InputOutputDef = {
type: AbiTypeSuper;
};
+// TODO: Use Viem ABI types instead, as they already handle this + providing argument type inference: https://viem.sh/docs/typescript#utilities
export type AbiFunction = {
inputs: InputOutputDef[];
name: AbiFunctionName;
@@ -521,7 +522,7 @@ export const BALANCES_ERC20_PRECOMPILE_ABI: AbiFunction[] {
switch (precompile) {
diff --git a/apps/tangle-dapp/constants/index.ts b/apps/tangle-dapp/constants/index.ts
index 1afd078381..80bad3b3fd 100644
--- a/apps/tangle-dapp/constants/index.ts
+++ b/apps/tangle-dapp/constants/index.ts
@@ -4,7 +4,11 @@ import {
TanglePrimitivesRolesZksaasZeroKnowledgeRoleType,
} from '@polkadot/types/lookup';
-import { RestakingService } from '../types';
+import {
+ RestakingService,
+ StakingRewardsDestination,
+ StakingRewardsDestinationDisplayText,
+} from '../types';
/**
* The lock ids are always 8 characters long, due to their representation
@@ -22,25 +26,6 @@ export enum SubstrateLockId {
OTHER = '?other',
}
-/**
- * Stale-while-revalidate (SWR) is a strategy for caching fetch requests.
- *
- * It helps automatically avoid redundant requests if made again before
- * a specified period has elapsed. This can be particularly useful for
- * Polkadot API requests that return Promise objects.
- *
- * Since these requests don't always require real-time data and their
- * responses might not change frequently, caching them for a particular
- * duration can be efficient.
- *
- * [Learn more about SWR](https://swr.vercel.app/)
- */
-export enum SwrBaseKey {
- ACTIVE_VALIDATORS = 'active-validators',
- WAITING_VALIDATORS = 'waiting-validators',
- ACTIVE_VALIDATORS_PAGINATED = 'active-validator-paginated',
-}
-
export enum StaticAssetPath {
RESTAKING_METHOD_INDEPENDENT_DARK = '/static/assets/restaking/method-independent-dark.svg',
RESTAKING_METHOD_SHARED_DARK = '/static/assets/restaking/method-shared-dark.svg',
@@ -71,10 +56,58 @@ export enum ChartColor {
LAVENDER = '#E7E2FF',
}
-export const PAYMENT_DESTINATION_OPTIONS = [
- 'Staked (increase the amount at stake)',
- 'Stash (do not increase the amount at stake)',
-];
+export enum TxName {
+ PAYOUT_ALL = 'payout all stakers',
+ PAYOUT_STAKERS = 'payout stakers',
+ VEST = 'vest',
+ BOND = 'bond',
+ REBOND = 'rebond',
+ UNBOND = 'unbond',
+ BOND_EXTRA = 'bond extra',
+ WITHDRAW_UNBONDED = 'withdraw unbonded',
+ SET_PAYEE = 'set payee',
+ TRANSFER = 'transfer',
+ CHILL = 'chill',
+ NOMINATE = 'nominate',
+ SETUP_NOMINATOR = 'setup nominator',
+ UPDATE_NOMINATOR = 'update nominator',
+ WITHDRAW_EVM_BALANCE = 'withdraw',
+ UPDATE_RESTAKE_PROFILE = 'update restake profile',
+}
+
+export const PAYMENT_DESTINATION_OPTIONS: StakingRewardsDestinationDisplayText[] =
+ [
+ StakingRewardsDestinationDisplayText.STAKED,
+ StakingRewardsDestinationDisplayText.STASH,
+ ];
+
+export const STAKING_PAYEE_TEXT_TO_VALUE_MAP: Record<
+ StakingRewardsDestinationDisplayText,
+ StakingRewardsDestination
+> = {
+ [StakingRewardsDestinationDisplayText.STAKED]:
+ StakingRewardsDestination.STAKED,
+ [StakingRewardsDestinationDisplayText.STASH]: StakingRewardsDestination.STASH,
+ [StakingRewardsDestinationDisplayText.CONTROLLER]:
+ StakingRewardsDestination.CONTROLLER,
+ [StakingRewardsDestinationDisplayText.ACCOUNT]:
+ StakingRewardsDestination.ACCOUNT,
+ [StakingRewardsDestinationDisplayText.NONE]: StakingRewardsDestination.NONE,
+};
+
+export const STAKING_PAYEE_VALUE_TO_TEXT_MAP: Record<
+ StakingRewardsDestination,
+ StakingRewardsDestinationDisplayText
+> = {
+ [StakingRewardsDestination.STAKED]:
+ StakingRewardsDestinationDisplayText.STAKED,
+ [StakingRewardsDestination.STASH]: StakingRewardsDestinationDisplayText.STASH,
+ [StakingRewardsDestination.CONTROLLER]:
+ StakingRewardsDestinationDisplayText.CONTROLLER,
+ [StakingRewardsDestination.ACCOUNT]:
+ StakingRewardsDestinationDisplayText.ACCOUNT,
+ [StakingRewardsDestination.NONE]: StakingRewardsDestinationDisplayText.NONE,
+};
/**
* The values are based off [Tangle's `RoleType` enum](https://github.com/webb-tools/tangle/blob/2a60f0382db2a1234c490766381872d2c7243f5e/primitives/src/roles/mod.rs#L40).
diff --git a/apps/tangle-dapp/constants/networks.ts b/apps/tangle-dapp/constants/networks.ts
index f994f48d74..9013b43b01 100644
--- a/apps/tangle-dapp/constants/networks.ts
+++ b/apps/tangle-dapp/constants/networks.ts
@@ -1,13 +1,17 @@
import {
+ NetworkId,
TANGLE_MAINNET_NETWORK,
- TANGLE_TESTNET_NATIVE_NETWORK,
} from '@webb-tools/webb-ui-components/constants/networks';
import { NetworkFeature } from '../types';
export const DEFAULT_NETWORK = TANGLE_MAINNET_NETWORK;
-export const NETWORK_FEATURE_MAP: Record =
- {
- [TANGLE_TESTNET_NATIVE_NETWORK.name]: [NetworkFeature.Faucet],
- };
+export const NETWORK_FEATURE_MAP: Record = {
+ [NetworkId.TANGLE_TESTNET]: [NetworkFeature.Faucet],
+ [NetworkId.TANGLE_MAINNET]: [NetworkFeature.EraStakersOverview],
+ // Assume that local and custom endpoints are using an updated runtime
+ // version which includes support for the era stakers overview query.
+ [NetworkId.TANGLE_LOCAL_DEV]: [NetworkFeature.EraStakersOverview],
+ [NetworkId.CUSTOM]: [NetworkFeature.EraStakersOverview],
+};
diff --git a/apps/tangle-dapp/constants/openGraph.ts b/apps/tangle-dapp/constants/openGraph.ts
index 3fe585b9fa..895d76a20f 100644
--- a/apps/tangle-dapp/constants/openGraph.ts
+++ b/apps/tangle-dapp/constants/openGraph.ts
@@ -13,7 +13,7 @@ export const APP_NAME = 'Tangle dApp';
export const APP_SUBTITLE = 'Kickstarting Blockchain Innovation with MPC';
export const APP_DESCRIPTION =
- 'Your portal to managing Tangle Network assets and multi-party computation (MPC) services, kickstarting advanced cryptographic developments and innovations in blockchain.';
+ "Your portal to managing Tangle Network assets and upcoming AVS Blueprints in Tangle's modular restaking infrastructure.";
export const DEFAULT_OPENGRAPH_METADATA = {
title: {
diff --git a/apps/tangle-dapp/containers/ApiDevStatsContainer/ApiDevStatsContainer.tsx b/apps/tangle-dapp/containers/ApiDevStatsContainer/ApiDevStatsContainer.tsx
index 2d8ab2b8b2..9d5b265512 100644
--- a/apps/tangle-dapp/containers/ApiDevStatsContainer/ApiDevStatsContainer.tsx
+++ b/apps/tangle-dapp/containers/ApiDevStatsContainer/ApiDevStatsContainer.tsx
@@ -6,7 +6,7 @@ import { FC, useCallback, useEffect, useState } from 'react';
import useNetworkStore from '../../context/useNetworkStore';
import usePromise from '../../hooks/usePromise';
-import { getPolkadotApiPromise, getPolkadotApiRx } from '../../utils/polkadot';
+import { getApiPromise, getApiRx } from '../../utils/polkadot';
/**
* Format bytes to megabytes, rounded to two decimal places
@@ -24,12 +24,12 @@ const ApiDevStats: FC = () => {
const { rpcEndpoint } = useNetworkStore();
const { result: api } = usePromise(
- useCallback(() => getPolkadotApiPromise(rpcEndpoint), [rpcEndpoint]),
+ useCallback(() => getApiPromise(rpcEndpoint), [rpcEndpoint]),
null
);
const { result: apiRx } = usePromise(
- useCallback(() => getPolkadotApiRx(rpcEndpoint), [rpcEndpoint]),
+ useCallback(() => getApiRx(rpcEndpoint), [rpcEndpoint]),
null
);
diff --git a/apps/tangle-dapp/containers/BalancesTableContainer/BalanceCell.tsx b/apps/tangle-dapp/containers/BalancesTableContainer/BalanceCell.tsx
index 17ee8b04a2..45379e8cc9 100644
--- a/apps/tangle-dapp/containers/BalancesTableContainer/BalanceCell.tsx
+++ b/apps/tangle-dapp/containers/BalancesTableContainer/BalanceCell.tsx
@@ -30,7 +30,7 @@ const BalanceCell: FC<{
{formattedBalance !== null ? (
// If the balance is not null, display it.
- {formattedBalance}
+ {formattedBalance}
) : isAccountActive ? (
// If there is an active account, but the balance is null,
diff --git a/apps/tangle-dapp/containers/BalancesTableContainer/BalancesTableContainer.tsx b/apps/tangle-dapp/containers/BalancesTableContainer/BalancesTableContainer.tsx
index 83f92bca0a..93b3113c69 100644
--- a/apps/tangle-dapp/containers/BalancesTableContainer/BalancesTableContainer.tsx
+++ b/apps/tangle-dapp/containers/BalancesTableContainer/BalancesTableContainer.tsx
@@ -15,8 +15,8 @@ import TangleTokenIcon from '../../components/TangleTokenIcon';
import useNetworkStore from '../../context/useNetworkStore';
import useBalances from '../../data/balances/useBalances';
import useVestingInfo from '../../data/vesting/useVestingInfo';
+import useApiRx from '../../hooks/useApiRx';
import useLocalStorage, { LocalStorageKey } from '../../hooks/useLocalStorage';
-import usePolkadotApiRx from '../../hooks/usePolkadotApiRx';
import useSubstrateAddress from '../../hooks/useSubstrateAddress';
import { StaticSearchQueryPath } from '../../types';
import TransferTxContainer from '../TransferTxContainer/TransferTxContainer';
@@ -24,21 +24,29 @@ import BalanceAction from './BalanceAction';
import BalanceCell from './BalanceCell';
import HeaderCell from './HeaderCell';
import LockedBalanceDetails from './LockedBalanceDetails/LockedBalanceDetails';
+import useLongestVestingScheduleTime from './useLongestVestingScheduleTime';
import VestBalanceAction from './VestBalanceAction';
const BalancesTableContainer: FC = () => {
- const { locked, transferable } = useBalances();
- const activeSubstrateAddress = useSubstrateAddress();
const [isTransferModalOpen, setIsTransferModalOpen] = useState(false);
const [isDetailsCollapsed, setIsDetailsCollapsed] = useState(false);
+ const { locked, transferable } = useBalances();
+ const activeSubstrateAddress = useSubstrateAddress();
+ const longestVestingScheduleTime = useLongestVestingScheduleTime();
+
const { hasClaimableTokens: hasVestedAmount, claimableAmount: vestedAmount } =
useVestingInfo();
- const { set: setCachedIsDetailsCollapsed, get: getCachedIsDetailsCollapsed } =
- useLocalStorage(LocalStorageKey.IS_BALANCES_TABLE_DETAILS_COLLAPSED, false);
+ const {
+ set: setCachedIsDetailsCollapsed,
+ valueOpt: cachedIsDetailsCollapsedOpt,
+ } = useLocalStorage(
+ LocalStorageKey.IS_BALANCES_TABLE_DETAILS_COLLAPSED,
+ false
+ );
- const { data: locks } = usePolkadotApiRx(
+ const { result: locks } = useApiRx(
useCallback(
(api) => {
if (!activeSubstrateAddress) return null;
@@ -50,12 +58,13 @@ const BalancesTableContainer: FC = () => {
// Load the cached collapsed state from local storage on mount.
useEffect(() => {
- const cachedIsDetailsCollapsed = getCachedIsDetailsCollapsed();
-
- if (cachedIsDetailsCollapsed !== null) {
- setIsDetailsCollapsed(cachedIsDetailsCollapsed);
+ if (
+ cachedIsDetailsCollapsedOpt !== null &&
+ cachedIsDetailsCollapsedOpt.value !== null
+ ) {
+ setIsDetailsCollapsed(cachedIsDetailsCollapsedOpt.value);
}
- }, [getCachedIsDetailsCollapsed]);
+ }, [cachedIsDetailsCollapsedOpt]);
const handleToggleDetails = useCallback(() => {
setIsDetailsCollapsed((prev) => {
@@ -85,7 +94,7 @@ const BalancesTableContainer: FC = () => {
{hasVestedAmount && (
)}
@@ -123,7 +132,10 @@ const BalancesTableContainer: FC = () => {
{/* Vested balance */}
{hasVestedAmount && (
-
+
diff --git a/apps/tangle-dapp/containers/BalancesTableContainer/LockedBalanceDetails/DemocracyUnlockingAt.tsx b/apps/tangle-dapp/containers/BalancesTableContainer/LockedBalanceDetails/DemocracyUnlockingAt.tsx
index 1f83d41fa3..b2e486ea5c 100644
--- a/apps/tangle-dapp/containers/BalancesTableContainer/LockedBalanceDetails/DemocracyUnlockingAt.tsx
+++ b/apps/tangle-dapp/containers/BalancesTableContainer/LockedBalanceDetails/DemocracyUnlockingAt.tsx
@@ -2,9 +2,10 @@ import { formatDecimal } from '@polkadot/util';
import { FC, useCallback } from 'react';
import useDemocracy from '../../../data/democracy/useDemocracy';
-import usePolkadotApi from '../../../hooks/usePolkadotApi';
-import usePolkadotApiRx from '../../../hooks/usePolkadotApiRx';
+import useApi from '../../../hooks/useApi';
+import useApiRx from '../../../hooks/useApiRx';
import calculateTimeRemaining from '../../../utils/calculateTimeRemaining';
+import getBlockDate from '../../../utils/getBlockDate';
import TextCell from './TextCell';
const DemocracyUnlockingAt: FC = () => {
@@ -13,12 +14,12 @@ const DemocracyUnlockingAt: FC = () => {
latestReferendum: latestDemocracyReferendum,
} = useDemocracy();
- const { data: currentBlockNumber } = usePolkadotApiRx(
+ const { result: currentBlockNumber } = useApiRx(
useCallback((api) => api.derive.chain.bestNumber(), [])
);
- const { value: babeExpectedBlockTime } = usePolkadotApi(
- useCallback((api) => Promise.resolve(api.consts.babe.expectedBlockTime), [])
+ const { result: babeExpectedBlockTime } = useApi(
+ useCallback((api) => api.consts.babe.expectedBlockTime, [])
);
const isInDemocracy =
@@ -33,12 +34,16 @@ const DemocracyUnlockingAt: FC = () => {
return;
}
- const timeRemaining = calculateTimeRemaining(
+ const democracyLockEndBlockDate = getBlockDate(
babeExpectedBlockTime,
currentBlockNumber,
democracyLockEndBlock
);
+ const timeRemaining = democracyLockEndBlockDate
+ ? calculateTimeRemaining(democracyLockEndBlockDate)
+ : null;
+
return (
{
const { schedulesOpt: vestingSchedulesOpt } = useVestingInfo();
const { isInDemocracy } = useDemocracy();
- const { data: currentEra } = useCurrentEra();
- const { data: unbondingEntries } = useUnbonding();
+ const { result: currentEra } = useCurrentEra();
+ const { result: unbondingEntriesOpt } = useUnbonding();
const { amount: stakingLockedBalance } = useBalancesLock(
SubstrateLockId.STAKING
);
+ const unbondingEntries = useMemo(() => {
+ if (unbondingEntriesOpt === null || unbondingEntriesOpt.value === null) {
+ return null;
+ }
+
+ return unbondingEntriesOpt.value;
+ }, [unbondingEntriesOpt]);
+
const showNomination =
stakingLockedBalance !== null && stakingLockedBalance.gt(BN_ZERO);
diff --git a/apps/tangle-dapp/containers/BalancesTableContainer/LockedBalanceDetails/VestingRemainingBalances.tsx b/apps/tangle-dapp/containers/BalancesTableContainer/LockedBalanceDetails/VestingRemainingBalances.tsx
index e9d08ab6cd..c683b23fe5 100644
--- a/apps/tangle-dapp/containers/BalancesTableContainer/LockedBalanceDetails/VestingRemainingBalances.tsx
+++ b/apps/tangle-dapp/containers/BalancesTableContainer/LockedBalanceDetails/VestingRemainingBalances.tsx
@@ -2,14 +2,14 @@ import { BN, BN_ZERO } from '@polkadot/util';
import { FC, useCallback } from 'react';
import useVestingInfo from '../../../data/vesting/useVestingInfo';
-import usePolkadotApiRx from '../../../hooks/usePolkadotApiRx';
+import useApiRx from '../../../hooks/useApiRx';
import BalanceCell from '../BalanceCell';
import { sortVestingSchedulesAscending } from './VestingScheduleBalances';
const VestingRemainingBalances: FC = () => {
const { schedulesOpt: vestingSchedulesOpt } = useVestingInfo();
- const { data: currentBlockNumber } = usePolkadotApiRx(
+ const { result: currentBlockNumber } = useApiRx(
useCallback((api) => api.derive.chain.bestNumber(), [])
);
diff --git a/apps/tangle-dapp/containers/BalancesTableContainer/LockedBalanceDetails/VestingScheduleBalances.tsx b/apps/tangle-dapp/containers/BalancesTableContainer/LockedBalanceDetails/VestingScheduleBalances.tsx
index 635f177b5d..a1e2bcda78 100644
--- a/apps/tangle-dapp/containers/BalancesTableContainer/LockedBalanceDetails/VestingScheduleBalances.tsx
+++ b/apps/tangle-dapp/containers/BalancesTableContainer/LockedBalanceDetails/VestingScheduleBalances.tsx
@@ -3,7 +3,7 @@ import { BN, BN_ZERO, formatDecimal } from '@polkadot/util';
import { FC, useCallback } from 'react';
import useVestingInfo from '../../../data/vesting/useVestingInfo';
-import usePolkadotApiRx from '../../../hooks/usePolkadotApiRx';
+import useApiRx from '../../../hooks/useApiRx';
import { formatTokenBalance } from '../../../utils/polkadot';
import BalanceCell from '../BalanceCell';
@@ -25,7 +25,7 @@ export const sortVestingSchedulesAscending = (
const VestingScheduleBalances: FC = () => {
const { schedulesOpt: vestingSchedulesOpt } = useVestingInfo();
- const { data: currentBlockNumber } = usePolkadotApiRx(
+ const { result: currentBlockNumber } = useApiRx(
useCallback((api) => api.derive.chain.bestNumber(), [])
);
diff --git a/apps/tangle-dapp/containers/BalancesTableContainer/LockedBalanceDetails/VestingSchedulesUnlockingAt.tsx b/apps/tangle-dapp/containers/BalancesTableContainer/LockedBalanceDetails/VestingSchedulesUnlockingAt.tsx
index 0065bb6017..163a8166b1 100644
--- a/apps/tangle-dapp/containers/BalancesTableContainer/LockedBalanceDetails/VestingSchedulesUnlockingAt.tsx
+++ b/apps/tangle-dapp/containers/BalancesTableContainer/LockedBalanceDetails/VestingSchedulesUnlockingAt.tsx
@@ -2,21 +2,22 @@ import { formatDecimal } from '@polkadot/util';
import { FC, useCallback } from 'react';
import useVestingInfo from '../../../data/vesting/useVestingInfo';
-import usePolkadotApi from '../../../hooks/usePolkadotApi';
-import usePolkadotApiRx from '../../../hooks/usePolkadotApiRx';
+import useApi from '../../../hooks/useApi';
+import useApiRx from '../../../hooks/useApiRx';
import calculateTimeRemaining from '../../../utils/calculateTimeRemaining';
+import getBlockDate from '../../../utils/getBlockDate';
import TextCell from './TextCell';
import { sortVestingSchedulesAscending } from './VestingScheduleBalances';
const VestingSchedulesUnlockingAt: FC = () => {
const { schedulesOpt: vestingSchedulesOpt } = useVestingInfo();
- const { data: currentBlockNumber } = usePolkadotApiRx(
+ const { result: currentBlockNumber } = useApiRx(
useCallback((api) => api.derive.chain.bestNumber(), [])
);
- const { value: babeExpectedBlockTime } = usePolkadotApi(
- useCallback((api) => Promise.resolve(api.consts.babe.expectedBlockTime), [])
+ const { result: babeExpectedBlockTime } = useApi(
+ useCallback((api) => api.consts.babe.expectedBlockTime, [])
);
if (
@@ -36,12 +37,16 @@ const VestingSchedulesUnlockingAt: FC = () => {
schedule.locked.div(schedule.perBlock)
);
- const timeRemaining = calculateTimeRemaining(
+ const endingBlockDate = getBlockDate(
babeExpectedBlockTime,
currentBlockNumber,
endingBlockNumber
);
+ const timeRemaining = endingBlockDate
+ ? calculateTimeRemaining(endingBlockDate)
+ : null;
+
const isComplete = currentBlockNumber.gte(endingBlockNumber);
const progressText = isComplete
diff --git a/apps/tangle-dapp/containers/BalancesTableContainer/useLongestVestingScheduleTime.ts b/apps/tangle-dapp/containers/BalancesTableContainer/useLongestVestingScheduleTime.ts
new file mode 100644
index 0000000000..9b076044ca
--- /dev/null
+++ b/apps/tangle-dapp/containers/BalancesTableContainer/useLongestVestingScheduleTime.ts
@@ -0,0 +1,69 @@
+import { formatDecimal } from '@polkadot/util';
+import { useCallback } from 'react';
+
+import useVestingInfo from '../../data/vesting/useVestingInfo';
+import useApi from '../../hooks/useApi';
+import useApiRx from '../../hooks/useApiRx';
+import calculateTimeRemaining from '../../utils/calculateTimeRemaining';
+import getBlockDate from '../../utils/getBlockDate';
+import { sortVestingSchedulesAscending } from './LockedBalanceDetails/VestingScheduleBalances';
+
+const useLongestVestingScheduleTime = () => {
+ const { result: babeExpectedBlockTime } = useApi(
+ useCallback((api) => api.consts.babe.expectedBlockTime, [])
+ );
+
+ const { result: currentBlockNumber } = useApiRx(
+ useCallback((api) => api.derive.chain.bestNumber(), [])
+ );
+
+ const { schedulesOpt: vestingSchedulesOpt } = useVestingInfo();
+
+ if (
+ babeExpectedBlockTime === null ||
+ currentBlockNumber === null ||
+ vestingSchedulesOpt === null ||
+ vestingSchedulesOpt.isNone
+ ) {
+ return null;
+ }
+
+ const vestingSchedules = vestingSchedulesOpt.unwrap();
+
+ if (vestingSchedules.length === 0) {
+ return null;
+ }
+
+ const sortedVestingSchedules = vestingSchedules.toSorted(
+ sortVestingSchedulesAscending
+ );
+
+ const longestVestingSchedule =
+ sortedVestingSchedules[sortedVestingSchedules.length - 1];
+
+ const endingBlockNumber = longestVestingSchedule.startingBlock.add(
+ longestVestingSchedule.locked.div(longestVestingSchedule.perBlock)
+ );
+
+ const endingBlockDate = getBlockDate(
+ babeExpectedBlockTime,
+ currentBlockNumber,
+ endingBlockNumber
+ );
+
+ const timeRemaining = endingBlockDate
+ ? calculateTimeRemaining(endingBlockDate)
+ : null;
+
+ const isComplete = currentBlockNumber.gte(endingBlockNumber);
+
+ return isComplete
+ ? null
+ : `${timeRemaining} remaining until the full amount across all vesting schedules is unlocked. Currently at block #${formatDecimal(
+ currentBlockNumber.toString()
+ )}, with ${formatDecimal(
+ endingBlockNumber.sub(currentBlockNumber).toString()
+ )} blocks left.`;
+};
+
+export default useLongestVestingScheduleTime;
diff --git a/apps/tangle-dapp/containers/BondMoreTxContainer/BondMoreTxContainer.tsx b/apps/tangle-dapp/containers/BondMoreTxContainer/BondMoreTxContainer.tsx
index 8135ebce99..7f888bd007 100644
--- a/apps/tangle-dapp/containers/BondMoreTxContainer/BondMoreTxContainer.tsx
+++ b/apps/tangle-dapp/containers/BondMoreTxContainer/BondMoreTxContainer.tsx
@@ -1,7 +1,6 @@
'use client';
import { BN, BN_ZERO } from '@polkadot/util';
-import { useWebContext } from '@webb-tools/api-provider-environment';
import {
Button,
Modal,
@@ -13,15 +12,11 @@ import {
} from '@webb-tools/webb-ui-components';
import { WEBB_TANGLE_DOCS_STAKING_URL } from '@webb-tools/webb-ui-components/constants';
import Link from 'next/link';
-import { type FC, useCallback, useEffect, useMemo, useState } from 'react';
+import { type FC, useCallback, useEffect, useState } from 'react';
import AmountInput from '../../components/AmountInput/AmountInput';
-import useNetworkStore from '../../context/useNetworkStore';
import useTokenWalletFreeBalance from '../../data/NominatorStats/useTokenWalletFreeBalance';
-import useExecuteTxWithNotification from '../../hooks/useExecuteTxWithNotification';
-import { bondExtraTokens as bondExtraTokensEvm } from '../../utils/evm';
-import formatBnToDisplayAmount from '../../utils/formatBnToDisplayAmount';
-import { bondExtraTokens as bondExtraTokensSubstrate } from '../../utils/polkadot';
+import useBondExtraTx from '../../data/staking/useBondExtraTx';
import { BondMoreTxContainerProps } from './types';
const BondMoreTxContainer: FC = ({
@@ -29,23 +24,12 @@ const BondMoreTxContainer: FC = ({
setIsModalOpen,
}) => {
const { notificationApi } = useWebbUI();
- const { activeAccount } = useWebContext();
- const executeTx = useExecuteTxWithNotification();
const [amountToBond, setAmountToBond] = useState(null);
- const { rpcEndpoint, nativeTokenSymbol } = useNetworkStore();
const [isBondMoreTxLoading, setIsBondMoreTxLoading] = useState(false);
const [hasErrors, setHasErrors] = useState(false);
- const walletAddress = useMemo(() => {
- if (!activeAccount?.address) {
- return '0x0';
- }
-
- return activeAccount.address;
- }, [activeAccount?.address]);
-
const { data: walletBalance, error: walletBalanceError } =
- useTokenWalletFreeBalance(walletAddress);
+ useTokenWalletFreeBalance();
useEffect(() => {
if (walletBalanceError) {
@@ -63,48 +47,35 @@ const BondMoreTxContainer: FC = ({
[setHasErrors]
);
- const continueToSignAndSubmitTx = useMemo(() => {
- return (
- amountToBond !== null &&
- amountToBond.gt(BN_ZERO) &&
- walletAddress !== '0x0' &&
- !hasErrors
- );
- }, [amountToBond, walletAddress, hasErrors]);
-
- const closeModal = useCallback(() => {
+ const closeModalAndReset = useCallback(() => {
setIsBondMoreTxLoading(false);
setIsModalOpen(false);
setAmountToBond(null);
setHasErrors(false);
}, [setIsModalOpen]);
+ const { execute: executeBondExtraTx } = useBondExtraTx();
+
const submitAndSignTx = useCallback(async () => {
+ if (executeBondExtraTx === null || amountToBond === null) {
+ return;
+ }
+
setIsBondMoreTxLoading(true);
try {
- if (amountToBond === null) return;
- const bondingAmount = +formatBnToDisplayAmount(amountToBond);
- await executeTx(
- () => bondExtraTokensEvm(walletAddress, bondingAmount),
- () =>
- bondExtraTokensSubstrate(rpcEndpoint, walletAddress, bondingAmount),
- `Successfully bonded ${bondingAmount} ${nativeTokenSymbol}.`,
- 'Failed to bond extra tokens!'
- );
-
- closeModal();
+ await executeBondExtraTx({
+ amount: amountToBond,
+ });
+
+ closeModalAndReset();
} catch {
setIsBondMoreTxLoading(false);
}
- }, [
- amountToBond,
- closeModal,
- executeTx,
- rpcEndpoint,
- walletAddress,
- nativeTokenSymbol,
- ]);
+ }, [executeBondExtraTx, amountToBond, closeModalAndReset]);
+
+ const canSubmitTx =
+ amountToBond !== null && amountToBond.gt(BN_ZERO) && !hasErrors;
return (
@@ -113,11 +84,11 @@ const BondMoreTxContainer: FC = ({
isOpen={isModalOpen}
className="w-full max-w-[416px] rounded-2xl bg-mono-0 dark:bg-mono-180"
>
-
+
Add Stake
-
+
= ({
diff --git a/apps/tangle-dapp/containers/DelegateTxContainer/BondTokens.tsx b/apps/tangle-dapp/containers/DelegateTxContainer/BondTokens.tsx
index 4a5be8137f..86e3a103ee 100644
--- a/apps/tangle-dapp/containers/DelegateTxContainer/BondTokens.tsx
+++ b/apps/tangle-dapp/containers/DelegateTxContainer/BondTokens.tsx
@@ -5,22 +5,42 @@ import {
InputField,
Typography,
} from '@webb-tools/webb-ui-components';
-import { type FC } from 'react';
+import _ from 'lodash';
+import { type FC, useCallback } from 'react';
+import z from 'zod';
import AmountInput from '../../components/AmountInput/AmountInput';
+import {
+ STAKING_PAYEE_TEXT_TO_VALUE_MAP,
+ STAKING_PAYEE_VALUE_TO_TEXT_MAP,
+} from '../../constants';
+import useBalances from '../../data/balances/useBalances';
+import { StakingRewardsDestinationDisplayText } from '../../types/index';
import { BondTokensProps } from './types';
const BondTokens: FC = ({
- isFirstTimeNominator,
+ isBondedOrNominating,
nominatorAddress,
amountToBond,
setAmountToBond,
- paymentDestinationOptions,
- paymentDestination,
- setPaymentDestination,
- walletBalance,
+ payeeOptions,
+ payee,
+ setPayee,
handleAmountToBondError,
}) => {
+ const { free: freeBalance } = useBalances();
+
+ const handleSetPayee = useCallback(
+ (newPayeeString: string) => {
+ const payeeDisplayText = z
+ .nativeEnum(StakingRewardsDestinationDisplayText)
+ .parse(newPayeeString);
+
+ setPayee(STAKING_PAYEE_TEXT_TO_VALUE_MAP[payeeDisplayText]);
+ },
+ [setPayee]
+ );
+
return (
@@ -53,8 +73,8 @@ const BondTokens: FC
= ({
= ({
<>
diff --git a/apps/tangle-dapp/containers/DelegateTxContainer/DelegateTxContainer.tsx b/apps/tangle-dapp/containers/DelegateTxContainer/DelegateTxContainer.tsx
index 9316dc61ef..8c7be15306 100644
--- a/apps/tangle-dapp/containers/DelegateTxContainer/DelegateTxContainer.tsx
+++ b/apps/tangle-dapp/containers/DelegateTxContainer/DelegateTxContainer.tsx
@@ -1,8 +1,6 @@
'use client';
import { BN, BN_ZERO } from '@polkadot/util';
-import { useWebContext } from '@webb-tools/api-provider-environment';
-import { isSubstrateAddress } from '@webb-tools/dapp-types';
import {
Alert,
Button,
@@ -10,35 +8,21 @@ import {
ModalContent,
ModalFooter,
ModalHeader,
- useWebbUI,
} from '@webb-tools/webb-ui-components';
import { WEBB_TANGLE_DOCS_STAKING_URL } from '@webb-tools/webb-ui-components/constants';
+import assert from 'assert';
import { type FC, useCallback, useState } from 'react';
-import { TxConfirmationModal } from '../../components/TxConfirmationModal';
-import { PAYMENT_DESTINATION_OPTIONS } from '../../constants';
+import { PAYMENT_DESTINATION_OPTIONS as PAYEE_OPTIONS } from '../../constants';
import useNetworkStore from '../../context/useNetworkStore';
-import usePaymentDestination from '../../data/NominatorStats/usePaymentDestinationSubscription';
-import useTokenWalletFreeBalance from '../../data/NominatorStats/useTokenWalletFreeBalance';
-import useErrorReporting from '../../hooks/useErrorReporting';
-import useExecuteTxWithNotification from '../../hooks/useExecuteTxWithNotification';
-import useIsFirstTimeNominator from '../../hooks/useIsFirstTimeNominator';
+import useStakingRewardsDestination from '../../data/NominatorStats/useStakingRewardsDestination';
+import useIsBondedOrNominating from '../../data/staking/useIsBondedOrNominating';
+import useSetupNominatorTx from '../../data/staking/useSetupNominatorTx';
+import useUpdateNominatorTx from '../../data/staking/useUpdateNominatorTx';
+import useActiveAccountAddress from '../../hooks/useActiveAccountAddress';
import useMaxNominationQuota from '../../hooks/useMaxNominationQuota';
-import useSubstrateAddress from '../../hooks/useSubstrateAddress';
-import { PaymentDestination } from '../../types';
-import {
- bondExtraTokens as bondExtraTokensEvm,
- bondTokens as bondTokensEvm,
- nominateValidators as nominateValidatorsEvm,
- updatePaymentDestination as updatePaymentDestinationEvm,
-} from '../../utils/evm';
-import formatBnToDisplayAmount from '../../utils/formatBnToDisplayAmount';
-import {
- bondExtraTokens as bondExtraTokensSubstrate,
- bondTokens as bondTokensSubstrate,
- nominateValidators as nominateValidatorsSubstrate,
- updatePaymentDestination as updatePaymentDestinationSubstrate,
-} from '../../utils/polkadot';
+import { TxStatus } from '../../hooks/useSubstrateTx';
+import { StakingRewardsDestination } from '../../types';
import SelectValidators from '../UpdateNominationsTxContainer/SelectValidators';
import BondTokens from './BondTokens';
import { DelegateTxContainerProps, DelegateTxSteps } from './types';
@@ -47,73 +31,34 @@ const DelegateTxContainer: FC = ({
isModalOpen,
setIsModalOpen,
}) => {
- const { notificationApi } = useWebbUI();
- const { activeAccount } = useWebContext();
const maxNominationQuota = useMaxNominationQuota();
const [amountToBond, setAmountToBond] = useState(null);
- const [selectedValidators, setSelectedValidators] = useState([]);
- const executeTx = useExecuteTxWithNotification();
- const activeSubstrateAddress = useSubstrateAddress();
- const { rpcEndpoint, nativeTokenSymbol } = useNetworkStore();
+ const [selectedValidators, setSelectedValidators] = useState>(
+ new Set()
+ );
+ const { nativeTokenSymbol } = useNetworkStore();
+ const [payee, setPayee] = useState(StakingRewardsDestination.STAKED);
const [hasAmountToBondError, setHasAmountToBondError] = useState(false);
-
- const [txConfirmationModalIsOpen, setTxnConfirmationModalIsOpen] =
- useState(false);
+ const activeAccountAddress = useActiveAccountAddress();
const [delegateTxStep, setDelegateTxStep] = useState(
DelegateTxSteps.BOND_TOKENS
);
- const [paymentDestination, setPaymentDestination] = useState(
- PaymentDestination.STAKED
- );
-
- const [isSubmitAndSignTxLoading, setIsSubmitAndSignTxLoading] =
- useState(false);
-
const isExceedingMaxNominationQuota =
- selectedValidators.length > maxNominationQuota;
-
- const [txStatus, setTxnStatus] = useState<{
- status: 'success' | 'error';
- hash: string;
- }>({
- status: 'error',
- hash: '',
- });
+ selectedValidators.size > maxNominationQuota;
const currentStep = (() => {
- if (delegateTxStep === DelegateTxSteps.BOND_TOKENS) {
- return '(1/2)';
- } else if (delegateTxStep === DelegateTxSteps.SELECT_DELEGATES) {
- return '(2/2)';
+ switch (delegateTxStep) {
+ case DelegateTxSteps.BOND_TOKENS:
+ return '(1/2)';
+ case DelegateTxSteps.SELECT_DELEGATES:
+ return '(2/2)';
}
})();
- const walletAddress = (() => {
- if (!activeAccount?.address) {
- return '0x0';
- }
-
- return activeAccount.address;
- })();
-
- const {
- isFirstTimeNominator,
- isLoading: isFirstTimeNominatorLoading,
- isError: isFirstTimeNominatorError,
- } = useIsFirstTimeNominator();
-
- const { data: walletBalance, error: walletBalanceError } =
- useTokenWalletFreeBalance(walletAddress);
-
- // TODO: Need to change defaulting to empty/dummy strings to instead adhere to the type system. Ex. should be using `| null` instead.
- const {
- data: currentPaymentDestination,
- error: currentPaymentDestinationError,
- } = usePaymentDestination(activeSubstrateAddress ?? '0x0');
-
- useErrorReporting(null, walletBalanceError);
+ const isBondedOrNominating = useIsBondedOrNominating();
+ const { result: currentPayeeOpt } = useStakingRewardsDestination();
const handleAmountToBondError = useCallback(
(error: string | null) => {
@@ -122,278 +67,194 @@ const DelegateTxContainer: FC = ({
[setHasAmountToBondError]
);
- const continueToSelectDelegatesStep = isFirstTimeNominator
- ? amountToBond !== null && amountToBond.gt(BN_ZERO)
- : true &&
- !hasAmountToBondError &&
- paymentDestination &&
- walletAddress !== '0x0'
- ? true
- : false;
-
- const continueToAuthorizeTxStep =
- selectedValidators.length > 0 && !isExceedingMaxNominationQuota
- ? true
- : false;
-
- const continueToSignAndSubmitTx =
- continueToSelectDelegatesStep && continueToAuthorizeTxStep;
-
- const closeModal = useCallback(() => {
- setIsSubmitAndSignTxLoading(false);
+ const closeModalAndReset = useCallback(() => {
setIsModalOpen(false);
+ setPayee(StakingRewardsDestination.STAKED);
setAmountToBond(null);
setHasAmountToBondError(false);
- setPaymentDestination(PaymentDestination.STAKED);
- setSelectedValidators([]);
+ setSelectedValidators(new Set());
setDelegateTxStep(DelegateTxSteps.BOND_TOKENS);
}, [setIsModalOpen]);
- const executeDelegate: () => Promise = useCallback(async () => {
- try {
- if (amountToBond === null) {
- throw new Error('Amount to bond is required.');
- }
- const bondingAmount = +formatBnToDisplayAmount(amountToBond);
- if (isFirstTimeNominator) {
- await executeTx(
- () =>
- bondTokensEvm(
- walletAddress,
- bondingAmount,
- PaymentDestination.STASH
- ),
- () =>
- bondTokensSubstrate(
- rpcEndpoint,
- walletAddress,
- bondingAmount,
- PaymentDestination.STASH
- ),
- `Successfully bonded ${bondingAmount} ${nativeTokenSymbol}.`,
- 'Failed to bond tokens!'
- );
- await executeTx(
- () => updatePaymentDestinationEvm(walletAddress, paymentDestination),
- () =>
- updatePaymentDestinationSubstrate(
- rpcEndpoint,
- walletAddress,
- paymentDestination
- ),
- `Successfully updated payment destination to ${paymentDestination}.`,
- 'Failed to update payment destination!'
- );
- const hash = await executeTx(
- () => nominateValidatorsEvm(walletAddress, selectedValidators),
- () =>
- nominateValidatorsSubstrate(
- rpcEndpoint,
- walletAddress,
- selectedValidators
- ),
- `Successfully nominated ${selectedValidators.length} validators.`,
- 'Failed to nominate validators!'
- );
- setTxnStatus({ status: 'success', hash });
- } else {
- if (bondingAmount > 0) {
- await executeTx(
- () => bondExtraTokensEvm(walletAddress, bondingAmount),
- () =>
- bondExtraTokensSubstrate(
- rpcEndpoint,
- walletAddress,
- bondingAmount
- ),
- `Successfully bonded ${bondingAmount} ${nativeTokenSymbol}.`,
- 'Failed to bond tokens!'
- );
- }
- if (currentPaymentDestinationError)
- notificationApi({
- variant: 'error',
- message: currentPaymentDestinationError.message,
- });
- const currPaymentDestination =
- currentPaymentDestination?.value1 === 'Staked'
- ? PaymentDestination.STAKED
- : PaymentDestination.STASH;
- if (currPaymentDestination !== paymentDestination) {
- await executeTx(
- () =>
- updatePaymentDestinationEvm(walletAddress, paymentDestination),
- () =>
- updatePaymentDestinationSubstrate(
- rpcEndpoint,
- walletAddress,
- paymentDestination
- ),
- `Successfully updated payment destination to ${paymentDestination}.`,
- 'Failed to update payment destination!'
- );
- }
- const hash = await executeTx(
- () => nominateValidatorsEvm(walletAddress, selectedValidators),
- () =>
- nominateValidatorsSubstrate(
- rpcEndpoint,
- walletAddress,
- selectedValidators
- ),
- `Successfully nominated ${selectedValidators.length} validator(s).`,
- 'Failed to nominate validator(s)!'
- );
- setTxnStatus({ status: 'success', hash });
- }
- setTxnConfirmationModalIsOpen(true);
- } catch {
- setTxnStatus({ status: 'error', hash: '' });
- setTxnConfirmationModalIsOpen(true);
+ const { execute: executeSetupNominatorTx, status: setupNominatorTxStatus } =
+ useSetupNominatorTx();
+
+ const { execute: executeUpdateNominatorTx, status: updateNominatorTxStatus } =
+ useUpdateNominatorTx();
+
+ const executeDelegateTx = useCallback(async () => {
+ // Not yet ready.
+ if (
+ isBondedOrNominating === null ||
+ executeSetupNominatorTx === null ||
+ executeUpdateNominatorTx === null
+ ) {
+ return;
+ }
+
+ // Setting up a new nominator.
+ if (!isBondedOrNominating) {
+ assert(
+ amountToBond !== null,
+ 'Amount to bond should be set if setting up a nominator'
+ );
+
+ await executeSetupNominatorTx({
+ bondAmount: amountToBond,
+ payee,
+ nominees: selectedValidators,
+ });
+ }
+ // Increasing bond, changing payee, nominating new validators,
+ // or a combination of the three.
+ else {
+ // Only bond extra if the amount is greater than zero, and
+ // if the user provided an amount to bond.
+ const extraBondingAmount =
+ amountToBond === null || amountToBond.isZero()
+ ? undefined
+ : amountToBond;
+
+ // Update the payee if it has changed.
+ const newPayee = currentPayeeOpt?.value === payee ? undefined : payee;
+
+ await executeUpdateNominatorTx({
+ bondAmount: extraBondingAmount,
+ payee: newPayee,
+ // TODO: Only update nominees if they have changed. Use `_.isEqual` or similar to compare arrays.
+ nominees: selectedValidators,
+ });
}
}, [
amountToBond,
- currentPaymentDestination?.value1,
- currentPaymentDestinationError,
- executeTx,
- isFirstTimeNominator,
- nativeTokenSymbol,
- notificationApi,
- paymentDestination,
- rpcEndpoint,
+ currentPayeeOpt?.value,
+ executeSetupNominatorTx,
+ executeUpdateNominatorTx,
+ isBondedOrNominating,
+ payee,
selectedValidators,
- walletAddress,
]);
- const submitAndSignTx = useCallback(async () => {
- setIsSubmitAndSignTxLoading(true);
- try {
- await executeDelegate();
- } catch {
- // notification is already handled in executeTx
- } finally {
- closeModal();
- }
- }, [closeModal, executeDelegate]);
+ const submitTx = useCallback(async () => {
+ await executeDelegateTx();
- if (
- isFirstTimeNominator == null ||
- isFirstTimeNominatorLoading ||
- isFirstTimeNominatorError
- ) {
+ // TODO: This will close the modal even if the transaction fails. Find a way to keep it open if it fails, and only close it if it succeeds.
+ closeModalAndReset();
+ }, [closeModalAndReset, executeDelegateTx]);
+
+ if (isBondedOrNominating == null) {
return null;
}
+ const canContinueToSelectDelegatesStep = isBondedOrNominating
+ ? !hasAmountToBondError && activeAccountAddress !== null
+ : amountToBond !== null && amountToBond.gt(BN_ZERO);
+
+ const canContinueToAuthorizeTxStep =
+ selectedValidators.size > 0 && !isExceedingMaxNominationQuota
+ ? true
+ : false;
+
+ const canContinueToSubmitTx =
+ canContinueToSelectDelegatesStep && canContinueToAuthorizeTxStep;
+
return (
- <>
-
-
-
- Setup Nomination {currentStep}
-
+
+
+
+ Setup Nomination {currentStep}
+
-
- {delegateTxStep === DelegateTxSteps.BOND_TOKENS ? (
-
- ) : delegateTxStep === DelegateTxSteps.SELECT_DELEGATES ? (
-
- ) : null}
+
+ {delegateTxStep === DelegateTxSteps.BOND_TOKENS ? (
+
+ ) : delegateTxStep === DelegateTxSteps.SELECT_DELEGATES ? (
+
+ ) : null}
- {isExceedingMaxNominationQuota && (
-
- )}
-
+ {isExceedingMaxNominationQuota && (
+
+ )}
+
-
- {delegateTxStep === DelegateTxSteps.BOND_TOKENS ? (
-
- Learn More
-
- ) : (
- setDelegateTxStep(DelegateTxSteps.BOND_TOKENS)}
- >
- Back
-
- )}
+
+ {delegateTxStep === DelegateTxSteps.BOND_TOKENS ? (
+
+ Learn More
+
+ ) : (
+ setDelegateTxStep(DelegateTxSteps.BOND_TOKENS)}
+ >
+ Back
+
+ )}
- {delegateTxStep === DelegateTxSteps.BOND_TOKENS ? (
- {
+ if (delegateTxStep === DelegateTxSteps.BOND_TOKENS) {
+ setDelegateTxStep(DelegateTxSteps.SELECT_DELEGATES);
}
- onClick={() => {
- if (delegateTxStep === DelegateTxSteps.BOND_TOKENS) {
- setDelegateTxStep(DelegateTxSteps.SELECT_DELEGATES);
- }
- }}
- >
- Next
-
- ) : (
-
- Confirm
-
- )}
-
-
-
-
-
- >
+ }}
+ >
+ Next
+
+ ) : (
+
+ Confirm
+
+ )}
+
+
+
);
};
diff --git a/apps/tangle-dapp/containers/DelegateTxContainer/types.ts b/apps/tangle-dapp/containers/DelegateTxContainer/types.ts
index 35299bc580..f6984e4066 100644
--- a/apps/tangle-dapp/containers/DelegateTxContainer/types.ts
+++ b/apps/tangle-dapp/containers/DelegateTxContainer/types.ts
@@ -1,6 +1,10 @@
import { BN } from '@polkadot/util';
-import { Validator } from '../../types';
+import {
+ StakingRewardsDestination,
+ StakingRewardsDestinationDisplayText,
+ Validator,
+} from '../../types';
export type DelegateTxContainerProps = {
isModalOpen: boolean;
@@ -8,15 +12,15 @@ export type DelegateTxContainerProps = {
};
export type BondTokensProps = {
- isFirstTimeNominator: boolean;
+ isBondedOrNominating: boolean;
nominatorAddress: string;
+ amountToBondError?: string;
+ payeeOptions: StakingRewardsDestinationDisplayText[];
+ payee: StakingRewardsDestination;
amountToBond: BN | null;
- setAmountToBond: (amount: BN | null) => void;
- paymentDestinationOptions: string[];
- paymentDestination: string;
- setPaymentDestination: (paymentDestination: string) => void;
tokenSymbol: string;
- walletBalance: BN | null;
+ setPayee: (payee: StakingRewardsDestination) => void;
+ setAmountToBond: (amount: BN | null) => void;
handleAmountToBondError: (error: string | null) => void;
};
diff --git a/apps/tangle-dapp/containers/KeyStatsContainer/ActiveValidatorsKeyStat.tsx b/apps/tangle-dapp/containers/KeyStatsContainer/ActiveValidatorsKeyStat.tsx
index 3a15e22776..9d5b9b3105 100644
--- a/apps/tangle-dapp/containers/KeyStatsContainer/ActiveValidatorsKeyStat.tsx
+++ b/apps/tangle-dapp/containers/KeyStatsContainer/ActiveValidatorsKeyStat.tsx
@@ -12,10 +12,11 @@ const ActiveValidatorsKeyStat: FC = () => {
- {data?.value1}/{data?.value2}
+ {data?.value1 ?? '--'}/{data?.value2 ?? '--'}
);
};
diff --git a/apps/tangle-dapp/containers/KeyStatsContainer/ActualStakedPercentageKeyStat.tsx b/apps/tangle-dapp/containers/KeyStatsContainer/ActualStakedPercentageKeyStat.tsx
index 9e517d1aff..a2697ed0c3 100644
--- a/apps/tangle-dapp/containers/KeyStatsContainer/ActualStakedPercentageKeyStat.tsx
+++ b/apps/tangle-dapp/containers/KeyStatsContainer/ActualStakedPercentageKeyStat.tsx
@@ -17,9 +17,7 @@ const ActualStakedPercentageKeyStat: FC = () => {
isLoading={actualStakedPercentage === null}
error={null}
>
- {actualStakedPercentage !== null
- ? actualStakedPercentage * 100
- : undefined}
+ {actualStakedPercentage !== null ? actualStakedPercentage : undefined}
);
};
diff --git a/apps/tangle-dapp/containers/KeyStatsContainer/IdealStakedPercentageKeyStat.tsx b/apps/tangle-dapp/containers/KeyStatsContainer/IdealStakedPercentageKeyStat.tsx
index a35694de8d..f473375786 100644
--- a/apps/tangle-dapp/containers/KeyStatsContainer/IdealStakedPercentageKeyStat.tsx
+++ b/apps/tangle-dapp/containers/KeyStatsContainer/IdealStakedPercentageKeyStat.tsx
@@ -14,10 +14,11 @@ const IdealStakedPercentageKeyStat: FC = () => {
tooltip="Ideal proportion of tokens staked to secure the network and sustain active token trade and usage."
className="!border-b-0"
suffix="%"
+ showDataBeforeLoading
isLoading={isLoading}
error={error}
>
- {data?.value1}
+ {data?.value1 ?? '--'}
);
};
diff --git a/apps/tangle-dapp/containers/KeyStatsContainer/ValidatorCountKeyStat.tsx b/apps/tangle-dapp/containers/KeyStatsContainer/ValidatorCountKeyStat.tsx
index 78e294fbaf..70dc152032 100644
--- a/apps/tangle-dapp/containers/KeyStatsContainer/ValidatorCountKeyStat.tsx
+++ b/apps/tangle-dapp/containers/KeyStatsContainer/ValidatorCountKeyStat.tsx
@@ -16,10 +16,11 @@ const ValidatorCountKeyStat: FC = () => {
- {validatorCount?.value1}/{validatorCount?.value2}
+ {validatorCount?.value1 ?? '--'}/{validatorCount?.value2 ?? '--'}
);
};
diff --git a/apps/tangle-dapp/containers/KeyStatsContainer/WaitingValidatorsKeyStat.tsx b/apps/tangle-dapp/containers/KeyStatsContainer/WaitingValidatorsKeyStat.tsx
index ceeedb1fcf..65f7f9ee5c 100644
--- a/apps/tangle-dapp/containers/KeyStatsContainer/WaitingValidatorsKeyStat.tsx
+++ b/apps/tangle-dapp/containers/KeyStatsContainer/WaitingValidatorsKeyStat.tsx
@@ -13,10 +13,11 @@ const WaitingValidatorsKeyStat: FC = () => {
title="Waiting"
tooltip="Nodes waiting in line to become active validators."
className="!border-r-0 lg:!border-r"
+ showDataBeforeLoading
error={error}
isLoading={isLoading}
>
- {data?.value1}
+ {data?.value1 ?? '--'}
);
};
diff --git a/apps/tangle-dapp/containers/Layout/FeedbackBanner.tsx b/apps/tangle-dapp/containers/Layout/FeedbackBanner.tsx
index ef665024d9..34db568dbe 100644
--- a/apps/tangle-dapp/containers/Layout/FeedbackBanner.tsx
+++ b/apps/tangle-dapp/containers/Layout/FeedbackBanner.tsx
@@ -15,8 +15,8 @@ const FeedbackBanner: FC = () => {
const {
isSet: isBannerDismissalCacheSet,
- get: getCachedWasBannerDismissed,
set: setCachedWasBannerDismissed,
+ valueOpt: wasBannerDismissedOpt,
} = useLocalStorage(LocalStorageKey.WAS_BANNER_DISMISSED);
// If there is no cache key, show the banner by default.
@@ -29,10 +29,10 @@ const FeedbackBanner: FC = () => {
// If the banner was dismissed, do not show it to prevent
// annoying the user.
useEffect(() => {
- if (getCachedWasBannerDismissed() === true) {
+ if (wasBannerDismissedOpt?.value === true) {
setShowBanner(false);
}
- }, [getCachedWasBannerDismissed]);
+ }, [wasBannerDismissedOpt?.value]);
const onCloseHandler = useCallback(() => {
setShowBanner(false);
diff --git a/apps/tangle-dapp/containers/Layout/Layout.tsx b/apps/tangle-dapp/containers/Layout/Layout.tsx
index 3c6ec82a3d..456bbae6e1 100644
--- a/apps/tangle-dapp/containers/Layout/Layout.tsx
+++ b/apps/tangle-dapp/containers/Layout/Layout.tsx
@@ -1,17 +1,16 @@
import { Footer } from '@webb-tools/webb-ui-components';
import {
bottomLinks,
- TANGLE_GITHUB_URL,
TANGLE_PRIVACY_POLICY_URL,
+ TANGLE_SOCIAL_URLS_RECORD,
TANGLE_TERMS_OF_SERVICE_URL,
- TANGLE_TWITTER_URL,
WEBB_AVAILABLE_SOCIALS,
} from '@webb-tools/webb-ui-components/constants';
import { getSidebarStateFromCookie } from '@webb-tools/webb-ui-components/next-utils';
-import React, { type FC, type PropsWithChildren } from 'react';
+import { type FC, type PropsWithChildren } from 'react';
import { Breadcrumbs, Sidebar, SidebarMenu } from '../../components';
-import { TxConfirmationModalContainer } from '../../containers';
+import { IS_PRODUCTION_ENV } from '../../constants/env';
import ApiDevStatsContainer from '../ApiDevStatsContainer';
import WalletAndChainContainer from '../WalletAndChainContainer/WalletAndChainContainer';
import { WalletModalContainer } from '../WalletModalContainer';
@@ -21,10 +20,7 @@ import FeedbackBanner from './FeedbackBanner';
// footer in Tangle dApp, since it defaults to the Webb socials.
const SOCIAL_LINK_OVERRIDES: Partial<
Record<(typeof WEBB_AVAILABLE_SOCIALS)[number], string>
-> = {
- twitter: TANGLE_TWITTER_URL,
- github: TANGLE_GITHUB_URL,
-};
+> = TANGLE_SOCIAL_URLS_RECORD;
const BOTTOM_LINK_OVERRIDES: Partial<
Record<(typeof bottomLinks)[number]['name'], string>
@@ -35,14 +31,14 @@ const BOTTOM_LINK_OVERRIDES: Partial<
const Layout: FC = ({ children }) => {
const isSidebarInitiallyExpanded = getSidebarStateFromCookie();
- const isDevelopment = process.env.NODE_ENV === 'development';
return (
-
+
+
@@ -71,9 +67,7 @@ const Layout: FC
= ({ children }) => {
-
-
- {isDevelopment &&
}
+ {!IS_PRODUCTION_ENV &&
}
);
};
diff --git a/apps/tangle-dapp/containers/ManageProfileModalContainer/ConfirmAllocationsStep.tsx b/apps/tangle-dapp/containers/ManageProfileModalContainer/ConfirmAllocationsStep.tsx
index 484eb6ed68..14dfb789a6 100644
--- a/apps/tangle-dapp/containers/ManageProfileModalContainer/ConfirmAllocationsStep.tsx
+++ b/apps/tangle-dapp/containers/ManageProfileModalContainer/ConfirmAllocationsStep.tsx
@@ -6,7 +6,11 @@ import { FC } from 'react';
import { twMerge } from 'tailwind-merge';
import useNetworkStore from '../../context/useNetworkStore';
-import { RestakingProfileType, RestakingService } from '../../types';
+import {
+ RestakingProfileType,
+ RestakingService,
+ TokenSymbol,
+} from '../../types';
import { getChipColorOfServiceType } from '../../utils';
import { formatTokenBalance } from '../../utils/polkadot';
import { filterAllocations } from './Independent/IndependentAllocationStep';
@@ -182,7 +186,7 @@ const ConfirmAllocationsStep: FC
= ({
type AllocationItemProps = {
services: RestakingService[];
amount?: BN;
- tokenSymbol: string;
+ tokenSymbol: TokenSymbol;
};
/** @internal */
diff --git a/apps/tangle-dapp/containers/ManageProfileModalContainer/Independent/IndependentAllocationStep.tsx b/apps/tangle-dapp/containers/ManageProfileModalContainer/Independent/IndependentAllocationStep.tsx
index 61bd942c87..8ba95a9c06 100644
--- a/apps/tangle-dapp/containers/ManageProfileModalContainer/Independent/IndependentAllocationStep.tsx
+++ b/apps/tangle-dapp/containers/ManageProfileModalContainer/Independent/IndependentAllocationStep.tsx
@@ -13,7 +13,7 @@ import { z } from 'zod';
import useNetworkStore from '../../../context/useNetworkStore';
import useRestakingLimits from '../../../data/restaking/useRestakingLimits';
-import usePolkadotApi from '../../../hooks/usePolkadotApi';
+import useApi from '../../../hooks/useApi';
import { RestakingService } from '../../../types';
import { formatTokenBalance } from '../../../utils/polkadot';
import { AllocationChartVariant } from '../AllocationChart';
@@ -56,11 +56,8 @@ const IndependentAllocationStep: FC = ({
null
);
- const { value: maxRolesPerAccount } = usePolkadotApi(
- useCallback(
- (api) => Promise.resolve(api.consts.roles.maxRolesPerAccount),
- []
- )
+ const { result: maxRolesPerAccount } = useApi(
+ useCallback((api) => api.consts.roles.maxRolesPerAccount, [])
);
const [newAllocationRole, setNewAllocationRole] =
diff --git a/apps/tangle-dapp/containers/ManageProfileModalContainer/ManageProfileModalContainer.tsx b/apps/tangle-dapp/containers/ManageProfileModalContainer/ManageProfileModalContainer.tsx
index eb84262e16..b6196322bc 100644
--- a/apps/tangle-dapp/containers/ManageProfileModalContainer/ManageProfileModalContainer.tsx
+++ b/apps/tangle-dapp/containers/ManageProfileModalContainer/ManageProfileModalContainer.tsx
@@ -143,7 +143,7 @@ const ManageProfileModalContainer: FC = ({
executeForIndependentProfile: executeUpdateIndependentProfileTx,
executeForSharedProfile: executeUpdateSharedProfileTx,
status: updateProfileTxStatus,
- } = useUpdateRestakingProfileTx(profileType, true, true);
+ } = useUpdateRestakingProfileTx(profileType, true);
const handlePreviousStep = useCallback(() => {
const diff = getStepDiff(step, false);
diff --git a/apps/tangle-dapp/containers/ManageProfileModalContainer/Shared/SharedRolesInput.tsx b/apps/tangle-dapp/containers/ManageProfileModalContainer/Shared/SharedRolesInput.tsx
index 1037a9c890..26cf24a712 100644
--- a/apps/tangle-dapp/containers/ManageProfileModalContainer/Shared/SharedRolesInput.tsx
+++ b/apps/tangle-dapp/containers/ManageProfileModalContainer/Shared/SharedRolesInput.tsx
@@ -5,7 +5,7 @@ import { twMerge } from 'tailwind-merge';
import BaseInput from '../../../components/AmountInput/BaseInput';
import useRestakingJobs from '../../../data/restaking/useRestakingJobs';
-import usePolkadotApi from '../../../hooks/usePolkadotApi';
+import useApi from '../../../hooks/useApi';
import { RestakingService } from '../../../types';
import {
getChartDataAreaColorByServiceType,
@@ -31,7 +31,7 @@ const SharedRolesInput: FC = ({
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
const { servicesWithJobs } = useRestakingJobs();
- const { value: maxRolesPerAccount } = usePolkadotApi(
+ const { result: maxRolesPerAccount } = useApi(
useCallback(
(api) => Promise.resolve(api.consts.roles.maxRolesPerAccount),
[]
diff --git a/apps/tangle-dapp/containers/NominationsPayoutsContainer/NominationsPayoutsContainer.tsx b/apps/tangle-dapp/containers/NominationsPayoutsContainer/NominationsPayoutsContainer.tsx
index 1b56c3be46..610f01ffc5 100644
--- a/apps/tangle-dapp/containers/NominationsPayoutsContainer/NominationsPayoutsContainer.tsx
+++ b/apps/tangle-dapp/containers/NominationsPayoutsContainer/NominationsPayoutsContainer.tsx
@@ -4,7 +4,6 @@ import {
useConnectWallet,
useWebContext,
} from '@webb-tools/api-provider-environment';
-import { isSubstrateAddress } from '@webb-tools/dapp-types';
import {
ActionsDropdown,
Button,
@@ -12,16 +11,32 @@ import {
TableAndChartTabs,
useCheckMobile,
} from '@webb-tools/webb-ui-components';
-import { type FC, useEffect, useMemo, useRef, useState } from 'react';
+import { TANGLE_DOCS_URL } from '@webb-tools/webb-ui-components/constants';
+import {
+ type FC,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
-import { DelegatorTable, TableStatus } from '../../components';
+import {
+ ContainerSkeleton,
+ NominationsTable,
+ PayoutTable,
+ TableStatus,
+} from '../../components';
import useNominations from '../../data/NominationsPayouts/useNominations';
import usePayouts from '../../data/NominationsPayouts/usePayouts';
-import useIsFirstTimeNominator from '../../hooks/useIsFirstTimeNominator';
-import useNetworkState from '../../hooks/useNetworkState';
+import useIsBondedOrNominating from '../../data/staking/useIsBondedOrNominating';
+import useApi from '../../hooks/useApi';
import useQueryParamKey from '../../hooks/useQueryParamKey';
-import { DelegationsAndPayoutsTab, Payout, QueryParamKey } from '../../types';
-import { evmToSubstrateAddress } from '../../utils';
+import {
+ DelegationsAndPayoutsTab as NominationsAndPayoutsTab,
+ Payout,
+ QueryParamKey,
+} from '../../types';
import { DelegateTxContainer } from '../DelegateTxContainer';
import { PayoutAllTxContainer } from '../PayoutAllTxContainer';
import { StopNominationTxContainer } from '../StopNominationTxContainer';
@@ -30,16 +45,16 @@ import { UpdatePayeeTxContainer } from '../UpdatePayeeTxContainer';
const PAGE_SIZE = 10;
-function assertTab(tab: string): DelegationsAndPayoutsTab {
+function assertTab(tab: string): NominationsAndPayoutsTab {
if (
- !Object.values(DelegationsAndPayoutsTab).includes(
- tab as DelegationsAndPayoutsTab
+ !Object.values(NominationsAndPayoutsTab).includes(
+ tab as NominationsAndPayoutsTab
)
) {
throw new Error(`Invalid tab: ${tab}`);
}
- return tab as DelegationsAndPayoutsTab;
+ return tab as NominationsAndPayoutsTab;
}
const DelegationsPayoutsContainer: FC = () => {
@@ -51,7 +66,17 @@ const DelegationsPayoutsContainer: FC = () => {
const [isPayoutAllModalOpen, setIsPayoutAllModalOpen] = useState(false);
const [isUpdatePayeeModalOpen, setIsUpdatePayeeModalOpen] = useState(false);
- const { network } = useNetworkState();
+ const { result: historyDepth } = useApi(
+ useCallback(async (api) => api.consts.staking.historyDepth.toBn(), [])
+ );
+
+ const { result: progress } = useApi(
+ useCallback((api) => api.derive.session.progress(), [])
+ );
+
+ const { result: epochDuration } = useApi(
+ useCallback(async (api) => api.consts.babe.epochDuration.toNumber(), [])
+ );
const { value: queryParamsTab } = useQueryParamKey(
QueryParamKey.DELEGATIONS_AND_PAYOUTS_TAB
@@ -61,7 +86,7 @@ const DelegationsPayoutsContainer: FC = () => {
// Default to the nominations tab if no matching browser URL
// hash is present.
const [activeTab, setActiveTab] = useState(
- queryParamsTab ?? DelegationsAndPayoutsTab.NOMINATIONS
+ queryParamsTab ?? NominationsAndPayoutsTab.NOMINATIONS
);
const [isUpdateNominationsModalOpen, setIsUpdateNominationsModalOpen] =
@@ -70,46 +95,32 @@ const DelegationsPayoutsContainer: FC = () => {
const [isStopNominationModalOpen, setIsStopNominationModalOpen] =
useState(false);
- const substrateAddress = useMemo(() => {
- if (!activeAccount?.address) {
- return '';
- } else if (isSubstrateAddress(activeAccount?.address)) {
- return activeAccount.address;
- }
+ const nomineesOpt = useNominations();
+ const isBondedOrNominating = useIsBondedOrNominating();
+ const { data: payoutsData } = usePayouts();
- return evmToSubstrateAddress(activeAccount.address);
- }, [activeAccount?.address]);
+ const currentNominationAddresses = useMemo(() => {
+ if (nomineesOpt === null) {
+ return null;
+ }
- const { data: delegatorsData } = useNominations(substrateAddress);
- const { isFirstTimeNominator } = useIsFirstTimeNominator();
- const { data: payoutsData } = usePayouts(substrateAddress);
+ return nomineesOpt.map((nominees) =>
+ nominees.map((nominee) => nominee.address)
+ );
+ }, [nomineesOpt]);
- const currentNominations = useMemo(() => {
- if (!delegatorsData?.delegators) {
- return [];
+ const fetchedPayouts = useMemo(() => {
+ if (payoutsData !== null) {
+ setPayouts(payoutsData);
+ return payoutsData;
}
+ }, [payoutsData]);
- return delegatorsData.delegators.map((delegator) => delegator.address);
- }, [delegatorsData?.delegators]);
-
- // const { valueAfterMount: cachedPayouts } = useLocalStorage(
- // LocalStorageKey.Payouts,
- // true
- // );
-
- // const fetchedPayouts = useMemo(() => {
- // if (payoutsData !== null) {
- // return payoutsData.payouts;
- // } else if (cachedPayouts) {
- // return cachedPayouts[substrateAddress] ?? [];
- // }
- // }, [cachedPayouts, payoutsData, substrateAddress]);
-
- const fetchedNominations = useMemo(() => {
- if (delegatorsData !== null) {
- return delegatorsData.delegators;
+ useEffect(() => {
+ if (updatedPayouts.length > 0) {
+ setPayouts(updatedPayouts);
}
- }, [delegatorsData]);
+ }, [updatedPayouts]);
// Scroll to the table when the tab changes, or when the page
// is first loaded with a tab query parameter present.
@@ -121,19 +132,11 @@ const DelegationsPayoutsContainer: FC = () => {
tableRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [queryParamsTab]);
- useEffect(() => {
- if (updatedPayouts.length > 0) {
- setPayouts(updatedPayouts);
- } else if (payoutsData && payoutsData.payouts) {
- setPayouts(payoutsData.payouts);
- }
- }, [payoutsData, updatedPayouts]);
-
const validatorAndEras = useMemo(
() =>
payouts.map((payout) => ({
- validatorAddress: payout.validator.address,
- era: payout.era.toString(),
+ validatorSubstrateAddress: payout.validator.address,
+ era: payout.era,
})),
[payouts]
);
@@ -154,11 +157,11 @@ const DelegationsPayoutsContainer: FC = () => {
setActiveTab(assertTab(tabString))}
- tabs={[...Object.values(DelegationsAndPayoutsTab)]}
+ tabs={[...Object.values(NominationsAndPayoutsTab)]}
headerClassName="w-full overflow-x-auto"
filterComponent={
- activeAccount?.address && !isFirstTimeNominator ? (
- activeTab === DelegationsAndPayoutsTab.NOMINATIONS ? (
+ activeAccount?.address && isBondedOrNominating ? (
+ activeTab === NominationsAndPayoutsTab.NOMINATIONS ? (
setIsUpdateNominationsModalOpen(true)
@@ -173,7 +176,7 @@ const DelegationsPayoutsContainer: FC = () => {
setIsPayoutAllModalOpen(true)}
>
Payout All
@@ -183,8 +186,8 @@ const DelegationsPayoutsContainer: FC = () => {
) : null
}
>
- {/* Delegations Table */}
-
+ {/* Nominations Table */}
+
{!activeAccount ? (
{
}}
icon="🔗"
/>
- ) : fetchedNominations && fetchedNominations.length === 0 ? (
+ ) : nomineesOpt === null ? (
+
+ ) : nomineesOpt.value === null || nomineesOpt.value.length === 0 ? (
setIsDelegateModalOpen(true),
}}
- icon="🔍"
/>
) : (
-
)}
{/* Payouts Table */}
-
- {/* {!activeAccount ? (
+
+ {!activeAccount ? (
{
/>
) : fetchedPayouts && fetchedPayouts.length === 0 ? (
setIsDelegateModalOpen(true),
+ onClick: () =>
+ !isBondedOrNominating
+ ? setIsDelegateModalOpen(true)
+ : window.open(TANGLE_DOCS_URL, '_blank'),
}}
- icon="🔍"
+ icon={!isBondedOrNominating ? '🔍' : '⏳'}
/>
) : (
- )} */}
-
-
+ )}
- {isDelegateModalOpen && (
-
- )}
-
- {isUpdateNominationsModalOpen && (
-
- )}
+
+
+ ` type here, instead of defaulting to `[]`, because that will lead to a situation where the lower component things the value is still loading and displays a loading state forever.
+ currentNominations={currentNominationAddresses?.value ?? []}
+ />
+
{
- const { activeAccount, loading: isActiveAccountLoading } = useWebContext();
- const { nativeTokenSymbol } = useNetworkStore();
const [isDelegateModalOpen, setIsDelegateModalOpen] = useState(false);
const [isBondMoreModalOpen, setIsBondMoreModalOpen] = useState(false);
const [isUnbondModalOpen, setIsUnbondModalOpen] = useState(false);
const [isRebondModalOpen, setIsRebondModalOpen] = useState(false);
+
+ const activeAccountAddress = useActiveAccountAddress();
+ const { activeAccount, loading: isActiveAccountLoading } = useWebContext();
+ const { nativeTokenSymbol } = useNetworkStore();
const networkFeatures = useNetworkFeatures();
+ const { free: freeBalance, error: balancesError } = useBalances();
+ const isBondedOrNominating = useIsBondedOrNominating();
+
+ const { error: totalPayoutRewardsError, data: totalPayoutRewards } =
+ useTotalPayoutRewards();
const [isWithdrawUnbondedModalOpen, setIsWithdrawUnbondedModalOpen] =
useState(false);
- const walletAddress = useMemo(() => {
- if (!activeAccount?.address) return '0x0';
-
- return activeAccount.address;
- }, [activeAccount?.address]);
-
- const substrateAddress = useMemo(() => {
- if (!activeAccount?.address) return '';
-
- if (isSubstrateAddress(activeAccount?.address))
- return activeAccount.address;
+ const { result: bondedAmountOpt } = useStakingLedger(
+ useCallback((ledger) => ledger.active.toBn(), [])
+ );
- return evmToSubstrateAddress(activeAccount.address);
- }, [activeAccount?.address]);
+ const bondedAmountBalance = useMemo(() => {
+ if (bondedAmountOpt === null) {
+ return null;
+ }
- const {
- isFirstTimeNominator,
- isLoading: isFirstTimeNominatorLoading,
- isError: isFirstTimeNominatorError,
- } = useIsFirstTimeNominator();
+ return formatTokenBalance(
+ bondedAmountOpt.value ?? BN_ZERO,
+ nativeTokenSymbol
+ );
+ }, [bondedAmountOpt, nativeTokenSymbol]);
return (
<>
-
+
{
'border-2 border-mono-0 dark:border-mono-160'
)}
>
-
+
+
+ {activeAccountAddress === null
+ ? '--'
+ : freeBalance === null
+ ? null
+ : formatTokenBalance(freeBalance, nativeTokenSymbol)}
+
+
+
+ {totalPayoutRewards === null
+ ? '--'
+ : totalPayoutRewards.value1 === null
+ ? '--'
+ : formatBnToDisplayAmount(totalPayoutRewards.value1) +
+ ` ${nativeTokenSymbol}`}
+
+
-
+
{networkFeatures.includes(NetworkFeature.Faucet) &&
!isActiveAccountLoading && (
@@ -96,7 +121,8 @@ const NominatorStatsContainer: FC = () => {
- {isFirstTimeNominator && (
+ {/* Only allow nominator setup if not already nominating or bonded */}
+ {isBondedOrNominating === false && (
{
-
-
+ isError={false}
+ >
+ {activeAccountAddress === null
+ ? '--'
+ : bondedAmountBalance === null
+ ? null
+ : bondedAmountBalance}
+
+
+
- {!isFirstTimeNominator ? (
+ {isBondedOrNominating === true ? (
<>
-
+
{
-
+
setIsRebondModalOpen(true)}
>
Rebond
@@ -169,11 +196,7 @@ const NominatorStatsContainer: FC = () => {
setIsWithdrawUnbondedModalOpen(true)}
>
Withdraw
@@ -195,12 +218,10 @@ const NominatorStatsContainer: FC = () => {
- {isDelegateModalOpen && (
-
- )}
+
= ({
@@ -27,9 +25,6 @@ const PayoutAllTxContainer: FC = ({
updatePayouts,
}) => {
const { activeAccount } = useWebContext();
- const executeTx = useExecuteTxWithNotification();
- const { rpcEndpoint } = useNetworkStore();
- const [isPayoutAllTxLoading, setIsPayoutAllTxLoading] = useState(false);
const walletAddress = useMemo(() => {
if (!activeAccount?.address) {
@@ -39,15 +34,17 @@ const PayoutAllTxContainer: FC = ({
return activeAccount.address;
}, [activeAccount?.address]);
- const continueToSignAndSubmitTx = validatorsAndEras.length > 0;
-
const payoutValidatorsAndEras = useMemo(
() => validatorsAndEras.slice(0, 10),
[validatorsAndEras]
);
const allValidators = useMemo(
- () => [...new Set(payoutValidatorsAndEras.map((v) => v.validatorAddress))],
+ () => [
+ ...new Set(
+ payoutValidatorsAndEras.map((v) => v.validatorSubstrateAddress)
+ ),
+ ],
[payoutValidatorsAndEras]
);
@@ -58,51 +55,43 @@ const PayoutAllTxContainer: FC = ({
}, [payoutValidatorsAndEras]);
const closeModal = useCallback(() => {
- setIsPayoutAllTxLoading(false);
setIsModalOpen(false);
}, [setIsModalOpen]);
- const submitAndSignTx = useCallback(async () => {
- setIsPayoutAllTxLoading(true);
-
- try {
- await executeTx(
- () => batchPayoutStakersEvm(walletAddress, payoutValidatorsAndEras),
- () =>
- batchPayoutStakersSubstrate(
- rpcEndpoint,
- walletAddress,
- payoutValidatorsAndEras
- ),
- `Successfully claimed rewards for all stakers!`,
- 'Failed to payout all stakers!'
- );
-
- const updatedPayouts = payouts.filter(
- (payout) =>
- !payoutValidatorsAndEras.find(
- (v) =>
- v.validatorAddress === payout.validator.address &&
- v.era === payout.era.toString()
- )
- );
-
- updatePayouts(updatedPayouts);
- } catch {
- setIsPayoutAllTxLoading(false);
- } finally {
- closeModal();
+ const { execute: executePayoutAllTx, status: payoutAllTxStatus } =
+ usePayoutAllTx();
+
+ const submitTx = useCallback(async () => {
+ if (executePayoutAllTx === null) {
+ return;
}
+
+ await executePayoutAllTx({
+ validatorEraPairs: payoutValidatorsAndEras,
+ });
+
+ const updatedPayouts = payouts.filter(
+ (payout) =>
+ !payoutValidatorsAndEras.find(
+ (v) =>
+ v.validatorSubstrateAddress === payout.validator.address &&
+ v.era === payout.era
+ )
+ );
+
+ updatePayouts(updatedPayouts);
+ closeModal();
}, [
- executeTx,
+ executePayoutAllTx,
+ payoutValidatorsAndEras,
payouts,
updatePayouts,
- walletAddress,
- payoutValidatorsAndEras,
- rpcEndpoint,
closeModal,
]);
+ const canSubmitTx =
+ validatorsAndEras.length > 0 && executePayoutAllTx !== null;
+
return (
= ({
Confirm
diff --git a/apps/tangle-dapp/containers/PayoutAllTxContainer/types.ts b/apps/tangle-dapp/containers/PayoutAllTxContainer/types.ts
index 14d534d39b..c96e08f4b2 100644
--- a/apps/tangle-dapp/containers/PayoutAllTxContainer/types.ts
+++ b/apps/tangle-dapp/containers/PayoutAllTxContainer/types.ts
@@ -1,8 +1,8 @@
import { Payout } from '../../types';
export type PayoutTxProps = {
- validatorAddress: string;
- era: string;
+ validatorSubstrateAddress: string;
+ era: number;
};
export type PayoutAllTxContainerProps = {
diff --git a/apps/tangle-dapp/containers/PayoutTxContainer/PayoutTxContainer.tsx b/apps/tangle-dapp/containers/PayoutTxContainer/PayoutTxContainer.tsx
index 433b68bef7..a31ec85102 100644
--- a/apps/tangle-dapp/containers/PayoutTxContainer/PayoutTxContainer.tsx
+++ b/apps/tangle-dapp/containers/PayoutTxContainer/PayoutTxContainer.tsx
@@ -11,26 +11,20 @@ import {
Typography,
} from '@webb-tools/webb-ui-components';
import { WEBB_TANGLE_DOCS_STAKING_URL } from '@webb-tools/webb-ui-components/constants';
-import { type FC, useCallback, useMemo, useState } from 'react';
+import { type FC, useCallback, useMemo } from 'react';
-import useNetworkStore from '../../context/useNetworkStore';
-import useExecuteTxWithNotification from '../../hooks/useExecuteTxWithNotification';
-import { payoutStakers as payoutStakersEvm } from '../../utils/evm';
-import { payoutStakers as payoutStakersSubstrate } from '../../utils/polkadot';
+import usePayoutStakersTx from '../../data/payouts/usePayoutStakersTx';
+import { TxStatus } from '../../hooks/useSubstrateTx';
import { PayoutTxContainerProps } from './types';
const PayoutTxContainer: FC = ({
isModalOpen,
setIsModalOpen,
- payoutTxProps,
+ payoutTxProps: { validatorAddress, era },
payouts,
updatePayouts,
}) => {
const { activeAccount } = useWebContext();
- const { validatorAddress, era } = payoutTxProps;
- const executeTx = useExecuteTxWithNotification();
- const { rpcEndpoint } = useNetworkStore();
- const [isPayoutTxLoading, setIsPayoutTxLoading] = useState(false);
const walletAddress = useMemo(() => {
if (!activeAccount?.address) {
@@ -40,57 +34,46 @@ const PayoutTxContainer: FC = ({
return activeAccount.address;
}, [activeAccount?.address]);
- const continueToSignAndSubmitTx = walletAddress && validatorAddress && era;
-
const closeModal = useCallback(() => {
- setIsPayoutTxLoading(false);
setIsModalOpen(false);
}, [setIsModalOpen]);
- const submitAndSignTx = useCallback(async () => {
- setIsPayoutTxLoading(true);
-
- try {
- await executeTx(
- () => payoutStakersEvm(walletAddress, validatorAddress, Number(era)),
- () =>
- payoutStakersSubstrate(
- rpcEndpoint,
- walletAddress,
- validatorAddress,
- Number(era)
- ),
- `Successfully claimed rewards for Era ${era}.`,
- 'Failed to payout stakers!'
- );
-
- const updatedPayouts = payouts.filter(
- (payout) =>
- !(
- payout.era === Number(era) &&
- payout.validator.address === validatorAddress
- )
- );
-
- updatePayouts(updatedPayouts);
-
- closeModal();
-
- closeModal();
- } catch {
- setIsPayoutTxLoading(false);
+ const { execute: executePayoutStakersTx, status: payoutStakersTxStatus } =
+ usePayoutStakersTx();
+
+ const submitTx = useCallback(async () => {
+ if (executePayoutStakersTx === null) {
+ return;
}
+
+ await executePayoutStakersTx({
+ era,
+ validatorAddress,
+ });
+
+ const updatedPayouts = payouts.filter(
+ (payout) =>
+ !(
+ payout.era === Number(era) &&
+ payout.validator.address === validatorAddress
+ )
+ );
+
+ updatePayouts(updatedPayouts);
+ closeModal();
}, [
closeModal,
era,
- executeTx,
+ executePayoutStakersTx,
payouts,
- rpcEndpoint,
updatePayouts,
validatorAddress,
- walletAddress,
]);
+ // TODO: This validation doesn't make much sense because the values are never null or undefined, so why are they being used as booleans? In fact, the variable's inferred type is not a boolean.
+ const canSubmitTx =
+ walletAddress && validatorAddress && era && executePayoutStakersTx !== null;
+
return (
= ({
Confirm
diff --git a/apps/tangle-dapp/containers/PayoutTxContainer/types.ts b/apps/tangle-dapp/containers/PayoutTxContainer/types.ts
index 235182af0a..afd8c9f88f 100644
--- a/apps/tangle-dapp/containers/PayoutTxContainer/types.ts
+++ b/apps/tangle-dapp/containers/PayoutTxContainer/types.ts
@@ -2,7 +2,7 @@ import { Payout } from '../../types';
export type PayoutTxProps = {
validatorAddress: string;
- era: string;
+ era: number;
};
export type PayoutTxContainerProps = {
diff --git a/apps/tangle-dapp/containers/RebondTxContainer/RebondTxContainer.tsx b/apps/tangle-dapp/containers/RebondTxContainer/RebondTxContainer.tsx
index 1e4bca0c61..032d2effa8 100644
--- a/apps/tangle-dapp/containers/RebondTxContainer/RebondTxContainer.tsx
+++ b/apps/tangle-dapp/containers/RebondTxContainer/RebondTxContainer.tsx
@@ -1,8 +1,6 @@
'use client';
import { BN, BN_ZERO } from '@polkadot/util';
-import { useWebContext } from '@webb-tools/api-provider-environment';
-import { isSubstrateAddress } from '@webb-tools/dapp-types';
import {
Button,
Modal,
@@ -10,84 +8,31 @@ import {
ModalFooter,
ModalHeader,
Typography,
- useWebbUI,
} from '@webb-tools/webb-ui-components';
import { WEBB_TANGLE_DOCS_STAKING_URL } from '@webb-tools/webb-ui-components/constants';
import Link from 'next/link';
-import { type FC, useCallback, useMemo, useState } from 'react';
+import { type FC, useCallback, useState } from 'react';
import { BondedTokensBalanceInfo } from '../../components';
import AmountInput from '../../components/AmountInput/AmountInput';
-import useNetworkStore from '../../context/useNetworkStore';
-import useTotalUnbondedAndUnbondingAmount from '../../data/NominatorStats/useTotalUnbondedAndUnbondingAmount';
-import useUnbondingAmountSubscription from '../../data/NominatorStats/useUnbondingAmountSubscription';
-import useExecuteTxWithNotification from '../../hooks/useExecuteTxWithNotification';
-import { evmToSubstrateAddress } from '../../utils';
-import { rebondTokens as rebondTokensEvm } from '../../utils/evm';
-import formatBnToDisplayAmount from '../../utils/formatBnToDisplayAmount';
-import { rebondTokens as rebondTokensSubstrate } from '../../utils/polkadot';
+import useUnbondedAmount from '../../data/NominatorStats/useUnbondedAmount';
+import useUnbondingAmount from '../../data/NominatorStats/useUnbondingAmount';
+import useRebondTx from '../../data/staking/useRebondTx';
+import { TxStatus } from '../../hooks/useSubstrateTx';
import { RebondTxContainerProps } from './types';
const RebondTxContainer: FC = ({
isModalOpen,
setIsModalOpen,
}) => {
- const { notificationApi } = useWebbUI();
- const { activeAccount } = useWebContext();
- const executeTx = useExecuteTxWithNotification();
- const { rpcEndpoint, nativeTokenSymbol } = useNetworkStore();
-
const [amountToRebond, setAmountToRebond] = useState(null);
- const [isRebondTxLoading, setIsRebondTxLoading] = useState(false);
const [hasErrors, setHasErrors] = useState(false);
- const walletAddress = useMemo(() => {
- if (!activeAccount?.address) {
- return '0x0';
- }
-
- return activeAccount.address;
- }, [activeAccount?.address]);
-
- const substrateAddress = useMemo(() => {
- if (!activeAccount?.address) {
- return '';
- }
-
- if (isSubstrateAddress(activeAccount?.address))
- return activeAccount.address;
-
- return evmToSubstrateAddress(activeAccount.address);
- }, [activeAccount?.address]);
-
- const { data: unbondingAmountData, error: unbondingAmountError } =
- useUnbondingAmountSubscription(substrateAddress);
+ const { result: totalUnbondingAmount } = useUnbondingAmount();
+ const { result: totalUnbondedAmount } = useUnbondedAmount();
+ const { execute: executeRebondTx, status: rebondTxStatus } = useRebondTx();
- const { data: totalUnbondedAndUnbondingAmountData } =
- useTotalUnbondedAndUnbondingAmount(substrateAddress);
-
- const remainingUnbondedTokensToRebond = useMemo(() => {
- if (unbondingAmountError) {
- notificationApi({
- variant: 'error',
- message: unbondingAmountError.message,
- });
- }
-
- return unbondingAmountData?.value1 ?? undefined;
- }, [notificationApi, unbondingAmountData?.value1, unbondingAmountError]);
-
- const continueToSignAndSubmitTx = useMemo(() => {
- return (
- amountToRebond !== null &&
- amountToRebond.gt(BN_ZERO) &&
- !hasErrors &&
- walletAddress !== '0x0'
- );
- }, [amountToRebond, hasErrors, walletAddress]);
-
- const closeModal = useCallback(() => {
- setIsRebondTxLoading(false);
+ const closeModalAndReset = useCallback(() => {
setIsModalOpen(false);
setAmountToRebond(null);
setHasErrors(false);
@@ -100,33 +45,27 @@ const RebondTxContainer: FC = ({
[setHasErrors]
);
- const submitAndSignTx = useCallback(async () => {
- setIsRebondTxLoading(true);
-
- try {
- if (amountToRebond === null || amountToRebond.eq(BN_ZERO)) {
- throw new Error('There is no amount to rebond.');
- }
- const rebondAmount = +formatBnToDisplayAmount(amountToRebond);
- await executeTx(
- () => rebondTokensEvm(walletAddress, rebondAmount),
- () => rebondTokensSubstrate(rpcEndpoint, walletAddress, rebondAmount),
- `Successfully rebonded ${rebondAmount} ${nativeTokenSymbol}.`,
- 'Failed to rebond tokens!'
- );
-
- closeModal();
- } catch {
- setIsRebondTxLoading(false);
+ const submitTx = useCallback(async () => {
+ if (
+ executeRebondTx === null ||
+ amountToRebond === null ||
+ amountToRebond.isZero()
+ ) {
+ return null;
}
- }, [
- amountToRebond,
- closeModal,
- executeTx,
- rpcEndpoint,
- walletAddress,
- nativeTokenSymbol,
- ]);
+
+ await executeRebondTx({
+ amount: amountToRebond,
+ });
+
+ closeModalAndReset();
+ }, [executeRebondTx, amountToRebond, closeModalAndReset]);
+
+ const canSubmitTx =
+ amountToRebond !== null &&
+ amountToRebond.gt(BN_ZERO) &&
+ !hasErrors &&
+ executeRebondTx !== null;
return (
@@ -135,11 +74,11 @@ const RebondTxContainer: FC = ({
isOpen={isModalOpen}
className="w-full max-w-[416px] rounded-2xl bg-mono-0 dark:bg-mono-180"
>
-
+
Rebond Funds
-
+
Rebond to return unbonding or unbonded tokens to staking without
withdrawing.
@@ -148,39 +87,34 @@ const RebondTxContainer: FC = ({
-
+
Confirm
diff --git a/apps/tangle-dapp/containers/StakingStatsContainer/StakingStatsContainer.tsx b/apps/tangle-dapp/containers/StakingStatsContainer/StakingStatsContainer.tsx
deleted file mode 100644
index 6735c0025c..0000000000
--- a/apps/tangle-dapp/containers/StakingStatsContainer/StakingStatsContainer.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-'use client';
-
-import { ListCheckIcon, TimerLine } from '@webb-tools/icons';
-import { FC, useCallback } from 'react';
-
-import PillCard from '../../components/account/PillCard';
-import useNetworkStore from '../../context/useNetworkStore';
-import useStakingPendingRewards from '../../data/staking/useStakingPendingRewards';
-import usePolkadotApi, { PolkadotApiSwrKey } from '../../hooks/usePolkadotApi';
-import { formatTokenBalance } from '../../utils/polkadot/tokens';
-
-const StakingStatsContainer: FC = () => {
- const { value: currentEra } = usePolkadotApi(
- useCallback(
- // TODO: Find out under what conditions can `eraOpt` be `None` and handle it. Will need to search in the Substrate codebase for this. Make sure to write a comment explaining the conditions under which `eraOpt` can be `None` here, once it's found.
- (api) =>
- api.query.staking.currentEra().then((eraOpt) => eraOpt.toString()),
- []
- ),
- PolkadotApiSwrKey.ERA
- );
-
- const pendingRewards = useStakingPendingRewards();
- const { nativeTokenSymbol } = useNetworkStore();
-
- const formattedPendingRewards =
- pendingRewards !== null
- ? formatTokenBalance(pendingRewards, nativeTokenSymbol)
- : null;
-
- return (
-
- );
-};
-
-export default StakingStatsContainer;
diff --git a/apps/tangle-dapp/containers/StakingStatsContainer/index.ts b/apps/tangle-dapp/containers/StakingStatsContainer/index.ts
deleted file mode 100644
index 5c7d10c2a6..0000000000
--- a/apps/tangle-dapp/containers/StakingStatsContainer/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default as StakingStatsContainer } from './StakingStatsContainer';
diff --git a/apps/tangle-dapp/containers/StopNominationTxContainer/StopNominationTxContainer.tsx b/apps/tangle-dapp/containers/StopNominationTxContainer/StopNominationTxContainer.tsx
index c3511c2fce..2ae882c2c3 100644
--- a/apps/tangle-dapp/containers/StopNominationTxContainer/StopNominationTxContainer.tsx
+++ b/apps/tangle-dapp/containers/StopNominationTxContainer/StopNominationTxContainer.tsx
@@ -1,7 +1,5 @@
'use client';
-import { useWebContext } from '@webb-tools/api-provider-environment';
-import { isSubstrateAddress } from '@webb-tools/dapp-types';
import { ProhibitedLineIcon } from '@webb-tools/icons';
import {
Button,
@@ -13,72 +11,32 @@ import {
} from '@webb-tools/webb-ui-components';
import { WEBB_TANGLE_DOCS_STAKING_URL } from '@webb-tools/webb-ui-components/constants';
import Link from 'next/link';
-import { type FC, useCallback, useMemo, useState } from 'react';
+import { type FC, useCallback } from 'react';
-import useNetworkStore from '../../context/useNetworkStore';
-import useNominations from '../../data/NominationsPayouts/useNominations';
-import useExecuteTxWithNotification from '../../hooks/useExecuteTxWithNotification';
-import { evmToSubstrateAddress } from '../../utils';
-import { stopNomination as stopNominationEvm } from '../../utils/evm';
-import { stopNomination as stopNominationSubstrate } from '../../utils/polkadot';
+import useChillTx from '../../data/staking/useChillTx';
+import useIsNominating from '../../hooks/useIsNominating';
+import { TxStatus } from '../../hooks/useSubstrateTx';
import { StopNominationTxContainerProps } from './types';
const StopNominationTxContainer: FC = ({
isModalOpen,
setIsModalOpen,
}) => {
- const { activeAccount } = useWebContext();
- const executeTx = useExecuteTxWithNotification();
- const { rpcEndpoint } = useNetworkStore();
-
- const [isStopNominationTxLoading, setIsStopNominationTxLoading] =
- useState(false);
-
- const walletAddress = useMemo(() => {
- if (!activeAccount?.address) {
- return '0x0';
- }
-
- return activeAccount.address;
- }, [activeAccount?.address]);
-
- const substrateAddress = useMemo(() => {
- if (!activeAccount?.address) {
- return '';
- }
-
- if (isSubstrateAddress(activeAccount?.address))
- return activeAccount.address;
-
- return evmToSubstrateAddress(activeAccount.address) ?? '';
- }, [activeAccount?.address]);
-
- const { data: delegatorsData } = useNominations(substrateAddress);
-
- const userHasActiveNominations = useMemo(() => {
- return delegatorsData?.delegators.length === 0 ? false : true;
- }, [delegatorsData?.delegators]);
+ const { execute: executeChillTx, status: chillTxStatus } = useChillTx();
+ const { isNominating } = useIsNominating();
const closeModal = useCallback(() => {
- setIsStopNominationTxLoading(false);
setIsModalOpen(false);
}, [setIsModalOpen]);
- const submitAndSignTx = useCallback(async () => {
- setIsStopNominationTxLoading(true);
-
- try {
- await executeTx(
- () => stopNominationEvm(walletAddress),
- () => stopNominationSubstrate(rpcEndpoint, walletAddress),
- `Successfully stopped nomination!`,
- 'Failed to stop nomination!'
- );
- closeModal();
- } catch {
- setIsStopNominationTxLoading(false);
+ const submitTx = useCallback(async () => {
+ if (executeChillTx === null) {
+ return null;
}
- }, [closeModal, executeTx, rpcEndpoint, walletAddress]);
+
+ await executeChillTx();
+ closeModal();
+ }, [closeModal, executeChillTx]);
return (
@@ -104,9 +62,9 @@ const StopNominationTxContainer: FC = ({
Confirm
diff --git a/apps/tangle-dapp/containers/TransferTxContainer/TransferTxContainer.tsx b/apps/tangle-dapp/containers/TransferTxContainer/TransferTxContainer.tsx
index ed9e91e576..824fb13688 100644
--- a/apps/tangle-dapp/containers/TransferTxContainer/TransferTxContainer.tsx
+++ b/apps/tangle-dapp/containers/TransferTxContainer/TransferTxContainer.tsx
@@ -91,8 +91,8 @@ const TransferTxContainer: FC = ({
const {
execute: executeTransferTx,
status,
- error: txError,
reset: resetTransferTx,
+ error: txError,
} = useTransferTx();
// TODO: Likely would ideally want to control this from the parent component.
diff --git a/apps/tangle-dapp/containers/TxConfirmationModalContainer/TxConfirmationModalContainer.tsx b/apps/tangle-dapp/containers/TxConfirmationModalContainer/TxConfirmationModalContainer.tsx
deleted file mode 100644
index df67f30bd5..0000000000
--- a/apps/tangle-dapp/containers/TxConfirmationModalContainer/TxConfirmationModalContainer.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-'use client';
-
-import { TxConfirmationModal } from '../../components/TxConfirmationModal';
-import { useTxConfirmationModal } from '../../context/TxConfirmationContext';
-
-export const TxConfirmationModalContainer = () => {
- const { txConfirmationState, setTxConfirmationState } =
- useTxConfirmationModal();
-
- return (
-
- setTxConfirmationState({ ...txConfirmationState, isOpen })
- }
- txStatus={txConfirmationState.status}
- txHash={txConfirmationState.hash}
- txType={txConfirmationState.txType}
- />
- );
-};
diff --git a/apps/tangle-dapp/containers/TxConfirmationModalContainer/index.ts b/apps/tangle-dapp/containers/TxConfirmationModalContainer/index.ts
deleted file mode 100644
index 884e9c8d22..0000000000
--- a/apps/tangle-dapp/containers/TxConfirmationModalContainer/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './TxConfirmationModalContainer';
diff --git a/apps/tangle-dapp/containers/UnbondTxContainer/UnbondTxContainer.tsx b/apps/tangle-dapp/containers/UnbondTxContainer/UnbondTxContainer.tsx
index f343a435f2..93bf3c6648 100644
--- a/apps/tangle-dapp/containers/UnbondTxContainer/UnbondTxContainer.tsx
+++ b/apps/tangle-dapp/containers/UnbondTxContainer/UnbondTxContainer.tsx
@@ -1,8 +1,6 @@
'use client';
import { BN, BN_ZERO } from '@polkadot/util';
-import { useWebContext } from '@webb-tools/api-provider-environment';
-import { isSubstrateAddress } from '@webb-tools/dapp-types';
import {
Button,
Modal,
@@ -19,50 +17,27 @@ import { type FC, useCallback, useMemo, useState } from 'react';
import AmountInput from '../../components/AmountInput/AmountInput';
import useNetworkStore from '../../context/useNetworkStore';
import useTotalStakedAmountSubscription from '../../data/NominatorStats/useTotalStakedAmountSubscription';
-import useUnbondingAmountSubscription from '../../data/NominatorStats/useUnbondingAmountSubscription';
-import useExecuteTxWithNotification from '../../hooks/useExecuteTxWithNotification';
-import { evmToSubstrateAddress } from '../../utils';
-import { unBondTokens as unbondTokensEvm } from '../../utils/evm';
-import formatBnToDisplayAmount from '../../utils/formatBnToDisplayAmount';
-import { unbondTokens as unbondTokensSubstrate } from '../../utils/polkadot';
+import useUnbondingAmount from '../../data/NominatorStats/useUnbondingAmount';
+import useUnbondTx from '../../data/staking/useUnbondTx';
+import { TxStatus } from '../../hooks/useSubstrateTx';
import { UnbondTxContainerProps } from './types';
const UnbondTxContainer: FC = ({
isModalOpen,
setIsModalOpen,
}) => {
- const { notificationApi } = useWebbUI();
- const { activeAccount } = useWebContext();
- const executeTx = useExecuteTxWithNotification();
- const { rpcEndpoint, nativeTokenSymbol } = useNetworkStore();
-
- const [amountToUnbond, setAmountToUnbond] = useState(null);
- const [isUnbondTxLoading, setIsUnbondTxLoading] = useState(false);
+ const [amount, setAmount] = useState(null);
const [hasErrors, setHasErrors] = useState(false);
- const walletAddress = useMemo(() => {
- if (!activeAccount?.address) {
- return '0x0';
- }
-
- return activeAccount.address;
- }, [activeAccount?.address]);
-
- const substrateAddress = useMemo(() => {
- if (!activeAccount?.address) {
- return '';
- } else if (isSubstrateAddress(activeAccount?.address)) {
- return activeAccount.address;
- }
-
- return evmToSubstrateAddress(activeAccount.address) ?? '';
- }, [activeAccount?.address]);
+ const { notificationApi } = useWebbUI();
+ const { nativeTokenSymbol } = useNetworkStore();
+ const { execute: executeUnbondTx, status: unbondTxStatus } = useUnbondTx();
const { data: totalStakedBalanceData, error: totalStakedBalanceError } =
- useTotalStakedAmountSubscription(substrateAddress);
+ useTotalStakedAmountSubscription();
- const { data: unbondingAmountData, error: unbondingAmountError } =
- useUnbondingAmountSubscription(substrateAddress);
+ const { result: unbondingAmount, error: unbondingAmountError } =
+ useUnbondingAmount();
const totalStakedBalance = useMemo(() => {
if (totalStakedBalanceError) {
@@ -91,31 +66,27 @@ const UnbondTxContainer: FC = ({
});
}
- if (!unbondingAmountData?.value1) {
+ if (unbondingAmount === null || unbondingAmount.value === null) {
return undefined;
}
- return totalStakedBalance.sub(unbondingAmountData.value1);
+ return totalStakedBalance.sub(unbondingAmount.value);
}, [
notificationApi,
totalStakedBalance,
- unbondingAmountData?.value1,
+ unbondingAmount,
unbondingAmountError,
]);
- const continueToSignAndSubmitTx = useMemo(() => {
- return (
- amountToUnbond !== null &&
- amountToUnbond.gt(BN_ZERO) &&
- walletAddress !== '0x0' &&
- !hasErrors
- );
- }, [amountToUnbond, hasErrors, walletAddress]);
-
- const closeModal = useCallback(() => {
- setIsUnbondTxLoading(false);
+ const canSubmitTx =
+ amount !== null &&
+ amount.gt(BN_ZERO) &&
+ executeUnbondTx !== null &&
+ !hasErrors;
+
+ const closeModalAndReset = useCallback(() => {
setIsModalOpen(false);
- setAmountToUnbond(null);
+ setAmount(null);
setHasErrors(false);
}, [setIsModalOpen]);
@@ -126,34 +97,17 @@ const UnbondTxContainer: FC = ({
[setHasErrors]
);
- const submitAndSignTx = useCallback(async () => {
- setIsUnbondTxLoading(true);
-
- try {
- if (amountToUnbond === null) {
- throw new Error('Amount to unbond is required.');
- }
- const unbondingAmount = +formatBnToDisplayAmount(amountToUnbond);
- await executeTx(
- () => unbondTokensEvm(walletAddress, unbondingAmount),
- () =>
- unbondTokensSubstrate(rpcEndpoint, walletAddress, unbondingAmount),
- `Successfully unbonded ${unbondingAmount} ${nativeTokenSymbol}.`,
- 'Failed to unbond tokens!'
- );
-
- closeModal();
- } catch {
- setIsUnbondTxLoading(false);
+ const submitTx = useCallback(async () => {
+ if (executeUnbondTx === null || amount === null) {
+ return;
}
- }, [
- amountToUnbond,
- closeModal,
- executeTx,
- rpcEndpoint,
- walletAddress,
- nativeTokenSymbol,
- ]);
+
+ await executeUnbondTx({
+ amount: amount,
+ });
+
+ closeModalAndReset();
+ }, [amount, closeModalAndReset, executeUnbondTx]);
return (
@@ -162,21 +116,21 @@ const UnbondTxContainer: FC = ({
isOpen={isModalOpen}
className="w-full max-w-[416px] rounded-2xl bg-mono-0 dark:bg-mono-180"
>
-
+
Unbond Stake
-
+
Once unbonding, you must wait certain number of eras for your funds
@@ -189,12 +143,12 @@ const UnbondTxContainer: FC = ({
-
+
Confirm
diff --git a/apps/tangle-dapp/containers/UpdateNominationsTxContainer/SelectValidators.tsx b/apps/tangle-dapp/containers/UpdateNominationsTxContainer/SelectValidators.tsx
index 6573512b10..505dfa4f18 100644
--- a/apps/tangle-dapp/containers/UpdateNominationsTxContainer/SelectValidators.tsx
+++ b/apps/tangle-dapp/containers/UpdateNominationsTxContainer/SelectValidators.tsx
@@ -1,32 +1,25 @@
-import { Alert, Typography } from '@webb-tools/webb-ui-components';
-import { type FC } from 'react';
+import { Alert } from '@webb-tools/webb-ui-components';
+import React, { Dispatch, type FC, SetStateAction } from 'react';
-import { ValidatorListTable } from '../../components';
+import ValidatorSelectionTable from '../../components/ValidatorSelectionTable/ValidatorSelectionTable';
import useAllValidators from '../../data/ValidatorTables/useAllValidators';
-import { SelectValidatorsProps } from './types';
+
+export type SelectValidatorsProps = {
+ setSelectedValidators: Dispatch>>;
+};
const SelectValidators: FC = ({
- selectedValidators,
setSelectedValidators,
}) => {
const validators = useAllValidators();
return (
-
-
+
-
- Selected: {selectedValidators.length}/{validators.length}
-
-
= ({
);
};
-export default SelectValidators;
+export default React.memo(SelectValidators);
diff --git a/apps/tangle-dapp/containers/UpdateNominationsTxContainer/UpdateNominationsTxContainer.tsx b/apps/tangle-dapp/containers/UpdateNominationsTxContainer/UpdateNominationsTxContainer.tsx
index 547d2adc26..3240420af4 100644
--- a/apps/tangle-dapp/containers/UpdateNominationsTxContainer/UpdateNominationsTxContainer.tsx
+++ b/apps/tangle-dapp/containers/UpdateNominationsTxContainer/UpdateNominationsTxContainer.tsx
@@ -1,6 +1,5 @@
'use client';
-import { useWebContext } from '@webb-tools/api-provider-environment';
import {
Alert,
Button,
@@ -9,60 +8,46 @@ import {
ModalFooter,
ModalHeader,
} from '@webb-tools/webb-ui-components';
-import { type FC, useCallback, useEffect, useMemo, useState } from 'react';
-
-import useNetworkStore from '../../context/useNetworkStore';
-import useExecuteTxWithNotification from '../../hooks/useExecuteTxWithNotification';
+import _ from 'lodash';
+import {
+ type Dispatch,
+ type FC,
+ type SetStateAction,
+ useCallback,
+ useEffect,
+ useMemo,
+ useState,
+} from 'react';
+
+import useNominateTx from '../../data/staking/useNominateTx';
import useMaxNominationQuota from '../../hooks/useMaxNominationQuota';
-import { nominateValidators as nominateValidatorsEvm } from '../../utils/evm';
-import { nominateValidators as nominateValidatorsSubstrate } from '../../utils/polkadot';
+import { TxStatus } from '../../hooks/useSubstrateTx';
import SelectValidators from './SelectValidators';
-import { UpdateNominationsTxContainerProps } from './types';
+
+export type UpdateNominationsTxContainerProps = {
+ isModalOpen: boolean;
+ setIsModalOpen: (isModalOpen: boolean) => void;
+ currentNominations: string[];
+};
const UpdateNominationsTxContainer: FC = ({
isModalOpen,
setIsModalOpen,
currentNominations,
}) => {
- const { activeAccount } = useWebContext();
- const executeTx = useExecuteTxWithNotification();
const maxNominationQuota = useMaxNominationQuota();
- const { rpcEndpoint } = useNetworkStore();
+
+ const { execute: executeNominateTx, status: nominateTxStatus } =
+ useNominateTx();
const [selectedValidators, setSelectedValidators] =
useState(currentNominations);
- const [isSubmitAndSignTxLoading, setIsSubmitAndSignTxLoading] =
- useState(false);
-
- const walletAddress = useMemo(() => {
- if (!activeAccount?.address) {
- return '0x0';
- }
-
- return activeAccount.address;
- }, [activeAccount?.address]);
-
+ // Cannot nominate more than a certain number of validators.
const isExceedingMaxNominationQuota =
+ selectedValidators !== null &&
selectedValidators.length > maxNominationQuota;
- const isReadyToSubmitAndSignTx = useMemo(() => {
- if (selectedValidators.length <= 0 || isExceedingMaxNominationQuota) {
- return false;
- }
-
- const sortedSelectedValidators = [...selectedValidators].sort();
- const sortedCurrentNominations = [...currentNominations].sort();
-
- const areArraysEqual =
- sortedSelectedValidators.length === sortedCurrentNominations.length &&
- sortedSelectedValidators.every(
- (val, index) => val === sortedCurrentNominations[index]
- );
-
- return !areArraysEqual;
- }, [currentNominations, isExceedingMaxNominationQuota, selectedValidators]);
-
// Update the selected validators when the current
// nominations prop changes.
useEffect(() => {
@@ -70,43 +55,59 @@ const UpdateNominationsTxContainer: FC = ({
}, [currentNominations]);
const closeModal = useCallback(() => {
- setIsSubmitAndSignTxLoading(false);
setIsModalOpen(false);
setSelectedValidators(currentNominations);
}, [currentNominations, setIsModalOpen]);
- const submitAndSignTx = useCallback(async () => {
- if (!isReadyToSubmitAndSignTx) {
+ const submitTx = useCallback(async () => {
+ if (executeNominateTx === null || selectedValidators === null) {
return;
}
- setIsSubmitAndSignTxLoading(true);
-
- try {
- await executeTx(
- () => nominateValidatorsEvm(walletAddress, selectedValidators),
- () =>
- nominateValidatorsSubstrate(
- rpcEndpoint,
- walletAddress,
- selectedValidators
- ),
- `Successfully updated nominations!`,
- 'Failed to update nominations!'
- );
- closeModal();
- } catch {
- setIsSubmitAndSignTxLoading(false);
+ await executeNominateTx({
+ validatorAddresses: selectedValidators,
+ });
+
+ closeModal();
+ }, [closeModal, executeNominateTx, selectedValidators]);
+
+ const canSubmitTx = useMemo(() => {
+ if (
+ selectedValidators === null ||
+ selectedValidators.length === 0 ||
+ isExceedingMaxNominationQuota
+ ) {
+ return false;
}
+
+ // Can only submit transaction if the selected validators differ
+ // from the current nominations.
+ return (
+ !_.isEqual(currentNominations, selectedValidators) &&
+ executeNominateTx !== null
+ );
}, [
- closeModal,
- executeTx,
- isReadyToSubmitAndSignTx,
- rpcEndpoint,
+ currentNominations,
+ executeNominateTx,
+ isExceedingMaxNominationQuota,
selectedValidators,
- walletAddress,
]);
+ // The outer selected validators state is array of string
+ // but the child select validators state is set of string
+ // so we need to handle the conversion between set <> array
+ const handleSelectedValidatorsChange = useCallback<
+ Dispatch>>
+ >((nextValueOrUpdater) => {
+ if (typeof nextValueOrUpdater === 'function') {
+ setSelectedValidators((prev) => {
+ return Array.from(nextValueOrUpdater(new Set(prev)));
+ });
+ } else {
+ setSelectedValidators(Array.from(nextValueOrUpdater));
+ }
+ }, []);
+
return (
= ({
{isExceedingMaxNominationQuota && (
@@ -133,16 +133,16 @@ const UpdateNominationsTxContainer: FC = ({
)}
-
+
Cancel
Confirm Nomination
diff --git a/apps/tangle-dapp/containers/UpdateNominationsTxContainer/types.ts b/apps/tangle-dapp/containers/UpdateNominationsTxContainer/types.ts
deleted file mode 100644
index e35e8c6e25..0000000000
--- a/apps/tangle-dapp/containers/UpdateNominationsTxContainer/types.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export type UpdateNominationsTxContainerProps = {
- isModalOpen: boolean;
- setIsModalOpen: (isModalOpen: boolean) => void;
- currentNominations: string[];
-};
-
-export type SelectValidatorsProps = {
- selectedValidators: string[];
- setSelectedValidators: (selectedValidators: string[]) => void;
-};
diff --git a/apps/tangle-dapp/containers/UpdatePayeeTxContainer/UpdatePayee.tsx b/apps/tangle-dapp/containers/UpdatePayeeTxContainer/UpdatePayee.tsx
index dfb10c8406..1e2f78b703 100644
--- a/apps/tangle-dapp/containers/UpdatePayeeTxContainer/UpdatePayee.tsx
+++ b/apps/tangle-dapp/containers/UpdatePayeeTxContainer/UpdatePayee.tsx
@@ -3,16 +3,43 @@ import {
InputField,
Typography,
} from '@webb-tools/webb-ui-components';
-import { type FC } from 'react';
+import { type FC, useCallback } from 'react';
+import { z } from 'zod';
+import {
+ STAKING_PAYEE_TEXT_TO_VALUE_MAP,
+ STAKING_PAYEE_VALUE_TO_TEXT_MAP,
+} from '../../constants';
+import { StakingRewardsDestinationDisplayText } from '../../types';
import { UpdatePayeeProps } from './types';
const UpdatePayee: FC = ({
currentPayee,
- paymentDestinationOptions,
- paymentDestination,
- setPaymentDestination,
+ payeeOptions,
+ selectedPayee: payee,
+ setSelectedPayee: setPayee,
}) => {
+ const handleSetPayee = useCallback(
+ (newPayeeString: string) => {
+ const payeeDisplayText = z
+ .nativeEnum(StakingRewardsDestinationDisplayText)
+ .parse(newPayeeString);
+
+ setPayee(STAKING_PAYEE_TEXT_TO_VALUE_MAP[payeeDisplayText]);
+ },
+ [setPayee]
+ );
+
+ const currentPayeeDisplayText: string = (() => {
+ if (currentPayee === null) {
+ return 'Loading...';
+ } else if (currentPayee.value !== null) {
+ return STAKING_PAYEE_VALUE_TO_TEXT_MAP[currentPayee.value];
+ } else {
+ return 'None';
+ }
+ })();
+
return (
@@ -21,7 +48,7 @@ const UpdatePayee: FC = ({
@@ -30,9 +57,9 @@ const UpdatePayee: FC = ({
{/* Payment Destination */}
diff --git a/apps/tangle-dapp/containers/UpdatePayeeTxContainer/UpdatePayeeTxContainer.tsx b/apps/tangle-dapp/containers/UpdatePayeeTxContainer/UpdatePayeeTxContainer.tsx
index 70dd01df9c..4142f85178 100644
--- a/apps/tangle-dapp/containers/UpdatePayeeTxContainer/UpdatePayeeTxContainer.tsx
+++ b/apps/tangle-dapp/containers/UpdatePayeeTxContainer/UpdatePayeeTxContainer.tsx
@@ -1,27 +1,21 @@
'use client';
-import { useWebContext } from '@webb-tools/api-provider-environment';
-import { isSubstrateAddress } from '@webb-tools/dapp-types';
import {
Button,
Modal,
ModalContent,
ModalFooter,
ModalHeader,
- useWebbUI,
} from '@webb-tools/webb-ui-components';
import { WEBB_TANGLE_DOCS_STAKING_URL } from '@webb-tools/webb-ui-components/constants';
import Link from 'next/link';
-import { type FC, useCallback, useMemo, useState } from 'react';
+import { type FC, useCallback, useState } from 'react';
import { PAYMENT_DESTINATION_OPTIONS } from '../../constants';
-import useNetworkStore from '../../context/useNetworkStore';
-import usePaymentDestinationSubscription from '../../data/NominatorStats/usePaymentDestinationSubscription';
-import useExecuteTxWithNotification from '../../hooks/useExecuteTxWithNotification';
-import { PaymentDestination } from '../../types';
-import { evmToSubstrateAddress } from '../../utils';
-import { updatePaymentDestination as updatePaymentDestinationEvm } from '../../utils/evm';
-import { updatePaymentDestination as updatePaymentDestinationSubstrate } from '../../utils/polkadot';
+import useStakingRewardsDestination from '../../data/NominatorStats/useStakingRewardsDestination';
+import useSetPayeeTx from '../../data/staking/useSetPayeeTx';
+import { TxStatus } from '../../hooks/useSubstrateTx';
+import { StakingRewardsDestination } from '../../types';
import { UpdatePayeeTxContainerProps } from './types';
import UpdatePayee from './UpdatePayee';
@@ -29,81 +23,36 @@ const UpdatePayeeTxContainer: FC
= ({
isModalOpen,
setIsModalOpen,
}) => {
- const { notificationApi } = useWebbUI();
- const { activeAccount } = useWebContext();
- const executeTx = useExecuteTxWithNotification();
- const { rpcEndpoint } = useNetworkStore();
+ const { result: currentPayee } = useStakingRewardsDestination();
- const [paymentDestination, setPaymentDestination] = useState(
- PaymentDestination.STAKED
+ const [selectedPayee, setSelectedPayee] = useState(
+ StakingRewardsDestination.STAKED
);
- const [
- isUpdatePaymentDestinationTxLoading,
- setIsUpdatePaymentDestinationTxLoading,
- ] = useState(false);
+ const { execute: executeSetPayeeTx, status: setPayeeTxStatus } =
+ useSetPayeeTx();
- const walletAddress = useMemo(() => {
- if (!activeAccount?.address) {
- return '0x0';
- }
-
- return activeAccount.address;
- }, [activeAccount?.address]);
-
- const substrateAddress = useMemo(() => {
- if (!activeAccount?.address) {
- return '';
- }
-
- if (isSubstrateAddress(activeAccount?.address)) {
- return activeAccount.address;
- }
-
- return evmToSubstrateAddress(activeAccount.address) ?? '';
- }, [activeAccount?.address]);
-
- const {
- data: currentPaymentDestination,
- error: currentPaymentDestinationError,
- } = usePaymentDestinationSubscription(substrateAddress);
-
- const continueToSignAndSubmitTx = paymentDestination;
-
- const closeModal = useCallback(() => {
- setIsUpdatePaymentDestinationTxLoading(false);
+ const closeModalAndReset = useCallback(() => {
setIsModalOpen(false);
- setPaymentDestination(PaymentDestination.STAKED);
+ setSelectedPayee(StakingRewardsDestination.STAKED);
}, [setIsModalOpen]);
- const submitAndSignTx = useCallback(async () => {
- setIsUpdatePaymentDestinationTxLoading(true);
-
- try {
- await executeTx(
- () => updatePaymentDestinationEvm(walletAddress, paymentDestination),
- () =>
- updatePaymentDestinationSubstrate(
- rpcEndpoint,
- walletAddress,
- paymentDestination
- ),
- `Successfully updated payment destination to ${paymentDestination}.`,
- 'Failed to update payment destination!'
- );
-
- closeModal();
- } catch {
- setIsUpdatePaymentDestinationTxLoading(false);
+ const submitTx = useCallback(async () => {
+ if (executeSetPayeeTx === null) {
+ return;
}
- }, [closeModal, executeTx, paymentDestination, rpcEndpoint, walletAddress]);
- if (currentPaymentDestinationError) {
- notificationApi({
- variant: 'error',
- message: currentPaymentDestinationError.message,
+ await executeSetPayeeTx({
+ payee: selectedPayee,
});
- }
+
+ closeModalAndReset();
+ }, [closeModalAndReset, executeSetPayeeTx, selectedPayee]);
+
+ const canSubmitTx =
+ currentPayee !== null &&
+ currentPayee.value !== selectedPayee &&
+ executeSetPayeeTx !== null;
return (
@@ -112,25 +61,25 @@ const UpdatePayeeTxContainer: FC = ({
isOpen={isModalOpen}
className="w-full max-w-[1000px] rounded-2xl bg-mono-0 dark:bg-mono-180"
>
-
+
Change Reward Destination
Confirm
diff --git a/apps/tangle-dapp/containers/UpdatePayeeTxContainer/types.ts b/apps/tangle-dapp/containers/UpdatePayeeTxContainer/types.ts
index 5be8ab1856..1c80079ab4 100644
--- a/apps/tangle-dapp/containers/UpdatePayeeTxContainer/types.ts
+++ b/apps/tangle-dapp/containers/UpdatePayeeTxContainer/types.ts
@@ -1,11 +1,17 @@
+import {
+ StakingRewardsDestination,
+ StakingRewardsDestinationDisplayText,
+} from '../../types';
+import Optional from '../../utils/Optional';
+
export type UpdatePayeeTxContainerProps = {
isModalOpen: boolean;
setIsModalOpen: (isModalOpen: boolean) => void;
};
export type UpdatePayeeProps = {
- currentPayee: string | number;
- paymentDestinationOptions: string[];
- paymentDestination: string;
- setPaymentDestination: (paymentDestination: string) => void;
+ currentPayee: Optional | null;
+ payeeOptions: StakingRewardsDestinationDisplayText[];
+ selectedPayee: StakingRewardsDestination;
+ setSelectedPayee: (newPayee: StakingRewardsDestination) => void;
};
diff --git a/apps/tangle-dapp/containers/ValidatorTablesContainer/ValidatorTablesContainer.tsx b/apps/tangle-dapp/containers/ValidatorTablesContainer/ValidatorTablesContainer.tsx
index 6cb6d29c6f..3d4077e62b 100644
--- a/apps/tangle-dapp/containers/ValidatorTablesContainer/ValidatorTablesContainer.tsx
+++ b/apps/tangle-dapp/containers/ValidatorTablesContainer/ValidatorTablesContainer.tsx
@@ -46,7 +46,7 @@ const ValidatorTablesContainer = () => {
{waitingValidatorsData !== null &&
waitingValidatorsData.length === 0 ? (
{
) : isWaitingValidatorsLoading ? (
) : (
-
+
)}
diff --git a/apps/tangle-dapp/containers/WalletAndChainContainer/WalletAndChainContainer.tsx b/apps/tangle-dapp/containers/WalletAndChainContainer/WalletAndChainContainer.tsx
index fdf2632786..a30463fe1f 100644
--- a/apps/tangle-dapp/containers/WalletAndChainContainer/WalletAndChainContainer.tsx
+++ b/apps/tangle-dapp/containers/WalletAndChainContainer/WalletAndChainContainer.tsx
@@ -15,6 +15,7 @@ import dynamic from 'next/dynamic';
import { type FC } from 'react';
import { WalletDropdown } from '../../components';
+import UpdateMetadataButton from '../../components/UpdateMetadataButton';
const NetworkSelectionButton = dynamic(
() => import('../../components/NetworkSelector/NetworkSelectionButton'),
@@ -58,11 +59,15 @@ const WalletAndChainContainer: FC = () => {
)
) : (
-
+
+
+
+
+
)}
diff --git a/apps/tangle-dapp/containers/WalletModalContainer/WalletModalContainer.tsx b/apps/tangle-dapp/containers/WalletModalContainer/WalletModalContainer.tsx
index 36bdf301d3..1f287a5168 100644
--- a/apps/tangle-dapp/containers/WalletModalContainer/WalletModalContainer.tsx
+++ b/apps/tangle-dapp/containers/WalletModalContainer/WalletModalContainer.tsx
@@ -77,26 +77,24 @@ const networkToTypedChainIds = (network: Network) => {
evm: PresetTypedChainId.TangleMainnetEVM,
substrate: PresetTypedChainId.TangleMainnetNative,
};
-
case NetworkId.TANGLE_TESTNET:
case NetworkId.TANGLE_LOCAL_DEV:
return {
evm: PresetTypedChainId.TangleTestnetEVM,
substrate: PresetTypedChainId.TangleTestnetNative,
};
-
case NetworkId.CUSTOM: {
- if (typeof network.chainId !== 'number') {
+ if (typeof network.evmChainId !== 'number') {
return;
}
return {
- evm: calculateTypedChainId(ChainType.EVM, network.chainId),
- substrate: calculateTypedChainId(ChainType.Substrate, network.chainId),
+ evm: calculateTypedChainId(ChainType.EVM, network.evmChainId),
+ substrate: calculateTypedChainId(
+ ChainType.Substrate,
+ network.evmChainId
+ ),
};
}
-
- default:
- return;
}
};
diff --git a/apps/tangle-dapp/containers/WithdrawUnbondedTxContainer/WithdrawUnbondedTxContainer.tsx b/apps/tangle-dapp/containers/WithdrawUnbondedTxContainer/WithdrawUnbondedTxContainer.tsx
index 79360fa8ed..2d86c18cf2 100644
--- a/apps/tangle-dapp/containers/WithdrawUnbondedTxContainer/WithdrawUnbondedTxContainer.tsx
+++ b/apps/tangle-dapp/containers/WithdrawUnbondedTxContainer/WithdrawUnbondedTxContainer.tsx
@@ -1,8 +1,6 @@
'use client';
import { BN_ZERO } from '@polkadot/util';
-import { useWebContext } from '@webb-tools/api-provider-environment';
-import { isSubstrateAddress } from '@webb-tools/dapp-types';
import {
Button,
Modal,
@@ -10,18 +8,14 @@ import {
ModalFooter,
ModalHeader,
Typography,
- useWebbUI,
} from '@webb-tools/webb-ui-components';
-import { type FC, useCallback, useMemo, useState } from 'react';
+import { type FC, useCallback, useState } from 'react';
import { BondedTokensBalanceInfo } from '../../components/BondedTokensBalanceInfo';
-import useNetworkStore from '../../context/useNetworkStore';
-import useTotalUnbondedAndUnbondingAmount from '../../data/NominatorStats/useTotalUnbondedAndUnbondingAmount';
-import useExecuteTxWithNotification from '../../hooks/useExecuteTxWithNotification';
-import { evmToSubstrateAddress } from '../../utils';
-import { withdrawUnbondedTokens as withdrawUnbondedTokensEvm } from '../../utils/evm';
-import { withdrawUnbondedTokens as withdrawUnbondedTokensSubstrate } from '../../utils/polkadot';
-import { getSlashingSpans } from '../../utils/polkadot';
+import useUnbondedAmount from '../../data/NominatorStats/useUnbondedAmount';
+import useUnbondingAmount from '../../data/NominatorStats/useUnbondingAmount';
+import useWithdrawUnbondedTx from '../../data/staking/useWithdrawUnbondedTx';
+import { TxStatus } from '../../hooks/useSubstrateTx';
import { RebondTxContainer } from '../RebondTxContainer';
import { WithdrawUnbondedTxContainerProps } from './types';
@@ -29,110 +23,37 @@ const WithdrawUnbondedTxContainer: FC = ({
isModalOpen,
setIsModalOpen,
}) => {
- const { notificationApi } = useWebbUI();
- const { activeAccount } = useWebContext();
- const executeTx = useExecuteTxWithNotification();
const [isRebondModalOpen, setIsRebondModalOpen] = useState(false);
- const { rpcEndpoint } = useNetworkStore();
- const [isWithdrawUnbondedTxLoading, setIsWithdrawUnbondedTxLoading] =
- useState(false);
-
- const walletAddress = useMemo(() => {
- if (!activeAccount?.address) {
- return '0x0';
- }
-
- return activeAccount.address;
- }, [activeAccount?.address]);
-
- const substrateAddress = useMemo(() => {
- if (!activeAccount?.address) return '';
-
- if (isSubstrateAddress(activeAccount?.address))
- return activeAccount.address;
-
- return evmToSubstrateAddress(activeAccount.address) ?? '';
- }, [activeAccount?.address]);
-
- const {
- data: totalUnbondedAndUnbondingAmountData,
- error: totalUnbondedAndUnbondingAmountError,
- } = useTotalUnbondedAndUnbondingAmount(substrateAddress);
-
- const totalUnbondedAmountAvailableToWithdraw = useMemo(() => {
- if (totalUnbondedAndUnbondingAmountError) {
- notificationApi({
- variant: 'error',
- message: totalUnbondedAndUnbondingAmountError.message,
- });
- }
-
- if (!totalUnbondedAndUnbondingAmountData?.value1?.unbonded)
- return undefined;
-
- return totalUnbondedAndUnbondingAmountData.value1.unbonded;
- }, [
- notificationApi,
- totalUnbondedAndUnbondingAmountData,
- totalUnbondedAndUnbondingAmountError,
- ]);
-
- const continueToSignAndSubmitTx = useMemo(() => {
- return totalUnbondedAmountAvailableToWithdraw &&
- totalUnbondedAmountAvailableToWithdraw.gt(BN_ZERO) &&
- walletAddress !== '0x0'
- ? true
- : false;
- }, [totalUnbondedAmountAvailableToWithdraw, walletAddress]);
+ const { result: totalUnbondedAmount } = useUnbondedAmount();
+ const { result: totalUnbondingAmount } = useUnbondingAmount();
const closeModal = useCallback(() => {
- setIsWithdrawUnbondedTxLoading(false);
setIsModalOpen(false);
}, [setIsModalOpen]);
- const submitAndSignTx = useCallback(async () => {
- setIsWithdrawUnbondedTxLoading(true);
-
- try {
- await executeTx(
- async () => {
- const slashingSpans = await getSlashingSpans(
- rpcEndpoint,
- substrateAddress
- );
-
- return withdrawUnbondedTokensEvm(
- walletAddress,
- Number(slashingSpans)
- );
- },
- async () => {
- const slashingSpans = await getSlashingSpans(
- rpcEndpoint,
- substrateAddress
- );
-
- return withdrawUnbondedTokensSubstrate(
- rpcEndpoint,
- walletAddress,
- Number(slashingSpans)
- );
- },
- `Successfully withdraw!`,
- 'Failed to withdraw tokens!'
- );
-
- closeModal();
- } catch {
- setIsWithdrawUnbondedTxLoading(false);
+ const {
+ execute: executeWithdrawUnbondedTx,
+ status: withdrawUnbondedTxStatus,
+ } = useWithdrawUnbondedTx(totalUnbondedAmount?.value ?? null);
+
+ const submitTx = useCallback(async () => {
+ if (executeWithdrawUnbondedTx === null) {
+ return;
}
- }, [closeModal, executeTx, rpcEndpoint, substrateAddress, walletAddress]);
- const onRebondClick = () => {
+ await executeWithdrawUnbondedTx();
+ closeModal();
+ }, [closeModal, executeWithdrawUnbondedTx]);
+
+ const onRebondClick = useCallback(() => {
closeModal();
setIsRebondModalOpen(true);
- };
+ }, [closeModal]);
+
+ const canSubmitTx =
+ totalUnbondedAmount?.value?.isZero() === false &&
+ executeWithdrawUnbondedTx !== null;
return (
<>
@@ -154,18 +75,12 @@ const WithdrawUnbondedTxContainer: FC = ({
@@ -173,9 +88,9 @@ const WithdrawUnbondedTxContainer: FC = ({
Confirm
diff --git a/apps/tangle-dapp/containers/index.ts b/apps/tangle-dapp/containers/index.ts
index a876d96cac..89512b977d 100644
--- a/apps/tangle-dapp/containers/index.ts
+++ b/apps/tangle-dapp/containers/index.ts
@@ -6,7 +6,6 @@ export { Layout } from './Layout';
export * from './NominationsPayoutsContainer';
export * from './NominatorStatsContainer';
export * from './RebondTxContainer';
-export * from './TxConfirmationModalContainer';
export * from './UnbondTxContainer';
export * from './UpdatePayeeTxContainer';
export * from './ValidatorTablesContainer';
diff --git a/apps/tangle-dapp/context/BalancesContext.tsx b/apps/tangle-dapp/context/BalancesContext.tsx
new file mode 100644
index 0000000000..3113bcf030
--- /dev/null
+++ b/apps/tangle-dapp/context/BalancesContext.tsx
@@ -0,0 +1,25 @@
+'use client';
+
+import { createContext, FC, PropsWithChildren, useContext } from 'react';
+
+import useBalances from '../data/balances/useBalances';
+
+const BalanceContext = createContext>({
+ free: null,
+ transferable: null,
+ locked: null,
+ isLoading: false,
+ error: null,
+});
+
+export const useBalancesContext = () => useContext(BalanceContext);
+
+export const BalancesProvider: FC = ({ children }) => {
+ const balances = useBalances();
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/apps/tangle-dapp/context/BridgeContext.tsx b/apps/tangle-dapp/context/BridgeContext.tsx
new file mode 100644
index 0000000000..f6d50dae81
--- /dev/null
+++ b/apps/tangle-dapp/context/BridgeContext.tsx
@@ -0,0 +1,194 @@
+'use client';
+
+import { BN } from '@polkadot/util';
+import { chainsConfig } from '@webb-tools/dapp-config/chains/chain-config';
+import { ChainConfig } from '@webb-tools/dapp-config/chains/chain-config.interface';
+import { calculateTypedChainId } from '@webb-tools/sdk-core/typed-chain-id';
+import assert from 'assert';
+import {
+ createContext,
+ FC,
+ PropsWithChildren,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+} from 'react';
+
+import { BRIDGE } from '../constants/bridge';
+import { BridgeTokenId } from '../types/bridge';
+
+const BRIDGE_SOURCE_CHAIN_OPTIONS = Object.keys(BRIDGE).map(
+ (presetTypedChainId) => chainsConfig[+presetTypedChainId]
+);
+
+const DEFAULT_DESTINATION_CHAIN_OPTIONS = Object.keys(
+ BRIDGE_SOURCE_CHAIN_OPTIONS[0]
+).map((presetTypedChainId) => chainsConfig[+presetTypedChainId]);
+
+const DEFAULT_TOKEN_OPTIONS = Object.values(Object.values(BRIDGE)[0])[0]
+ .supportedTokens;
+
+interface BridgeContextProps {
+ selectedSourceChain: ChainConfig;
+ setSelectedSourceChain: (chain: ChainConfig) => void;
+ sourceChainOptions: ChainConfig[];
+
+ selectedDestinationChain: ChainConfig;
+ setSelectedDestinationChain: (chain: ChainConfig) => void;
+ destinationChainOptions: ChainConfig[];
+
+ destinationAddress: string;
+ setDestinationAddress: (address: string) => void;
+
+ amount: BN | null;
+ setAmount: (amount: BN | null) => void;
+
+ selectedTokenId: BridgeTokenId;
+ setSelectedTokenId: (token: BridgeTokenId) => void;
+ tokenIdOptions: BridgeTokenId[];
+}
+
+const BridgeContext = createContext({
+ selectedSourceChain: BRIDGE_SOURCE_CHAIN_OPTIONS[0],
+ setSelectedSourceChain: () => {
+ return;
+ },
+ sourceChainOptions: BRIDGE_SOURCE_CHAIN_OPTIONS,
+
+ selectedDestinationChain: DEFAULT_DESTINATION_CHAIN_OPTIONS[0],
+ setSelectedDestinationChain: () => {
+ return;
+ },
+ destinationChainOptions: DEFAULT_DESTINATION_CHAIN_OPTIONS,
+
+ destinationAddress: '',
+ setDestinationAddress: () => {
+ return;
+ },
+
+ amount: null,
+ setAmount: () => {
+ return;
+ },
+
+ selectedTokenId: DEFAULT_TOKEN_OPTIONS[0],
+ setSelectedTokenId: () => {
+ return;
+ },
+ tokenIdOptions: DEFAULT_TOKEN_OPTIONS,
+});
+
+export const useBridge = () => {
+ return useContext(BridgeContext);
+};
+
+const BridgeProvider: FC = ({ children }) => {
+ const [selectedSourceChain, setSelectedSourceChain] = useState(
+ BRIDGE_SOURCE_CHAIN_OPTIONS[0]
+ );
+
+ const selectedSourcePresetTypedChainId = useMemo(
+ () =>
+ calculateTypedChainId(
+ selectedSourceChain.chainType,
+ selectedSourceChain.id
+ ),
+ [selectedSourceChain.chainType, selectedSourceChain.id]
+ );
+
+ const destinationChainOptions = useMemo(
+ () =>
+ Object.keys(BRIDGE[selectedSourcePresetTypedChainId]).map(
+ (presetTypedChainId) => chainsConfig[+presetTypedChainId]
+ ),
+ [selectedSourcePresetTypedChainId]
+ );
+
+ assert(destinationChainOptions.length > 0, 'No destination chain options');
+
+ const [selectedDestinationChain, setSelectedDestinationChain] =
+ useState(destinationChainOptions[0]);
+
+ const selectedDestinationPresetTypedChainId = useMemo(
+ () =>
+ calculateTypedChainId(
+ selectedDestinationChain.chainType,
+ selectedDestinationChain.id
+ ),
+ [selectedDestinationChain.chainType, selectedDestinationChain.id]
+ );
+
+ const [destinationAddress, setDestinationAddress] = useState('');
+ const [amount, setAmount] = useState(null);
+
+ const tokenIdOptions = useMemo(
+ () =>
+ BRIDGE[selectedSourcePresetTypedChainId][
+ selectedDestinationPresetTypedChainId
+ ]?.supportedTokens ??
+ Object.values(BRIDGE[selectedSourcePresetTypedChainId])[0]
+ .supportedTokens,
+ [selectedSourcePresetTypedChainId, selectedDestinationPresetTypedChainId]
+ );
+
+ const [selectedTokenId, setSelectedTokenId] = useState(
+ tokenIdOptions[0]
+ );
+
+ useEffect(() => {
+ // If current destination chain is not in the destination chain options,
+ // set the first option as the destination chain.
+ if (
+ !destinationChainOptions.find(
+ (chain) =>
+ calculateTypedChainId(chain.chainType, chain.id) ===
+ calculateTypedChainId(
+ selectedDestinationChain.chainType,
+ selectedDestinationChain.id
+ )
+ )
+ ) {
+ setSelectedDestinationChain(destinationChainOptions[0]);
+ }
+ }, [
+ destinationChainOptions,
+ selectedDestinationChain.id,
+ selectedDestinationChain.chainType,
+ ]);
+
+ useEffect(() => {
+ // If current token is not in the token options, set the first option as the token.
+ if (!tokenIdOptions.find((tokenId) => tokenId === selectedTokenId)) {
+ setSelectedTokenId(tokenIdOptions[0]);
+ }
+ }, [selectedTokenId, tokenIdOptions]);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default BridgeProvider;
diff --git a/apps/tangle-dapp/context/RestakeContext.tsx b/apps/tangle-dapp/context/RestakeContext.tsx
index 89145787a7..800b6a5961 100644
--- a/apps/tangle-dapp/context/RestakeContext.tsx
+++ b/apps/tangle-dapp/context/RestakeContext.tsx
@@ -4,22 +4,29 @@ import { Option } from '@polkadot/types';
import { PalletRolesRestakingLedger } from '@polkadot/types/lookup';
import { createContext, FC, PropsWithChildren } from 'react';
-import useRestakingEarnings, {
- EarningRecord,
-} from '../data/restaking/useRestakingEarnings';
+import type { EarningRecord } from '../data/restaking/types';
+import useRestakingEarnings from '../data/restaking/useRestakingEarnings';
import useRestakingRoleLedger from '../data/restaking/useRestakingRoleLedger';
import useSubstrateAddress from '../hooks/useSubstrateAddress';
-export const RestakeContext = createContext({
- ledger: null as Option | null,
- earningsRecord: null as EarningRecord | null,
+interface RestakeContextProps {
+ ledger: Option | null;
+ earningsRecord: EarningRecord | null;
+ isLoading: boolean;
+}
+
+export const RestakeContext = createContext({
+ ledger: null,
+ earningsRecord: null,
isLoading: true,
});
const RestakeProvider: FC = ({ children }) => {
const substrateAddress = useSubstrateAddress();
- const { data: ledger, isLoading: isLedgerLoading } =
+
+ const { result: ledger, isLoading: isLedgerLoading } =
useRestakingRoleLedger(substrateAddress);
+
const { data: earningsRecord, isLoading: isEarningsLoading } =
useRestakingEarnings(substrateAddress);
diff --git a/apps/tangle-dapp/context/ServiceDetailsContext.tsx b/apps/tangle-dapp/context/ServiceDetailsContext.tsx
new file mode 100644
index 0000000000..b3ec5ada34
--- /dev/null
+++ b/apps/tangle-dapp/context/ServiceDetailsContext.tsx
@@ -0,0 +1,46 @@
+'use client';
+
+import { Option, u64 } from '@polkadot/types';
+import { TanglePrimitivesJobsJobInfo } from '@polkadot/types/lookup';
+import { createContext, FC, PropsWithChildren, useCallback } from 'react';
+
+import useApi, { ApiFetcher } from '../hooks/useApi';
+import { Service } from '../types';
+import { extractServiceDetails } from '../utils/polkadot';
+
+export const ServiceDetailsContext = createContext<{
+ serviceDetails: Service | null;
+ isLoading: boolean;
+}>({
+ serviceDetails: null,
+ isLoading: true,
+});
+
+const ServiceDetailsProvider: FC> = ({
+ children,
+ serviceId,
+}) => {
+ const servicesFetcher = useCallback>(
+ async (api) => {
+ const jobInfoData = (await api.query.jobs.submittedJobs(
+ // no provided type here, only Id
+ null,
+ new u64(api.registry, BigInt(serviceId))
+ )) as Option; // Data is returned as Codec type here
+ return extractServiceDetails(serviceId, jobInfoData);
+ },
+ [serviceId]
+ );
+
+ const { result: serviceDetails } = useApi(servicesFetcher);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default ServiceDetailsProvider;
diff --git a/apps/tangle-dapp/context/ServiceOverviewContext.tsx b/apps/tangle-dapp/context/ServiceOverviewContext.tsx
new file mode 100644
index 0000000000..62ed6c745b
--- /dev/null
+++ b/apps/tangle-dapp/context/ServiceOverviewContext.tsx
@@ -0,0 +1,49 @@
+'use client';
+
+import { Option } from '@polkadot/types';
+import { TanglePrimitivesJobsJobInfo } from '@polkadot/types/lookup';
+import { createContext, FC, PropsWithChildren, useCallback } from 'react';
+import { map } from 'rxjs/operators';
+
+import useApiRx from '../hooks/useApiRx';
+import { Service } from '../types';
+import { extractServiceDetails } from '../utils/polkadot/services';
+
+export const ServiceOverviewContext = createContext<{
+ services: Service[];
+ isLoading: boolean;
+}>({
+ services: [],
+ isLoading: true,
+});
+
+const ServiceOverviewProvider: FC = ({ children }) => {
+ const { result: services, isLoading } = useApiRx(
+ useCallback((api) => {
+ return api.query.jobs.submittedJobs.entries().pipe(
+ map((jobsData) =>
+ jobsData
+ .map(([key, job]) => {
+ const id = key.args[1].toString();
+ const service = extractServiceDetails(
+ id,
+ job as Option
+ );
+ return service;
+ })
+ .filter((service): service is Service => service !== null)
+ )
+ );
+ }, [])
+ );
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default ServiceOverviewProvider;
diff --git a/apps/tangle-dapp/context/TxConfirmationContext.tsx b/apps/tangle-dapp/context/TxConfirmationContext.tsx
deleted file mode 100644
index 254b2cea68..0000000000
--- a/apps/tangle-dapp/context/TxConfirmationContext.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-'use client';
-
-import React, { createContext, useContext, useState } from 'react';
-
-interface TxConfirmationState {
- isOpen: boolean;
- status: 'success' | 'error';
- hash: string;
- txType: 'substrate' | 'evm';
-}
-
-interface TxConfirmationContextType {
- txConfirmationState: TxConfirmationState;
- setTxConfirmationState: (state: TxConfirmationState) => void;
-}
-
-const initialState: TxConfirmationState = {
- isOpen: false,
- status: 'error',
- hash: '',
- txType: 'evm',
-};
-
-const TxConfirmationContext = createContext<
- TxConfirmationContextType | undefined
->(undefined);
-
-export const TxConfirmationProvider: React.FC<{
- children: React.ReactNode;
-}> = ({ children }) => {
- const [txConfirmationState, setTxConfirmationState] =
- useState(initialState);
-
- const value = {
- txConfirmationState,
- setTxConfirmationState,
- };
-
- return (
-
- {children}
-
- );
-};
-
-export const useTxConfirmationModal = () => {
- const context = useContext(TxConfirmationContext);
- if (context === undefined) {
- throw new Error(
- 'useTxnConfirmationModal must be used within a TxConfirmationProvider'
- );
- }
- return context;
-};
diff --git a/apps/tangle-dapp/context/useNetworkStore.ts b/apps/tangle-dapp/context/useNetworkStore.ts
index 0b2cd55a30..1ee1bf0cc9 100644
--- a/apps/tangle-dapp/context/useNetworkStore.ts
+++ b/apps/tangle-dapp/context/useNetworkStore.ts
@@ -4,6 +4,7 @@ import { Network } from '@webb-tools/webb-ui-components/constants/networks';
import { create } from 'zustand';
import { DEFAULT_NETWORK } from '../constants/networks';
+import { TokenSymbol } from '../types';
/**
* A store for Network info to use when creating/using
@@ -13,16 +14,16 @@ const useNetworkStore = create<{
rpcEndpoint: string;
network: Network;
setNetwork: (network: Network) => void;
- nativeTokenSymbol: string;
+ nativeTokenSymbol: TokenSymbol;
}>((set) => ({
rpcEndpoint: DEFAULT_NETWORK.wsRpcEndpoint,
network: DEFAULT_NETWORK,
- nativeTokenSymbol: DEFAULT_NETWORK.nativeTokenSymbol,
+ nativeTokenSymbol: DEFAULT_NETWORK.tokenSymbol,
setNetwork: (network) =>
set({
network,
rpcEndpoint: network.wsRpcEndpoint,
- nativeTokenSymbol: network.nativeTokenSymbol,
+ nativeTokenSymbol: network.tokenSymbol,
}),
}));
diff --git a/apps/tangle-dapp/data/KeyStats/useActiveAndDelegationCountSubscription.ts b/apps/tangle-dapp/data/KeyStats/useActiveAndDelegationCountSubscription.ts
index 514837b5fd..4b8aec2653 100644
--- a/apps/tangle-dapp/data/KeyStats/useActiveAndDelegationCountSubscription.ts
+++ b/apps/tangle-dapp/data/KeyStats/useActiveAndDelegationCountSubscription.ts
@@ -1,16 +1,12 @@
'use client';
import { formatNumber } from '@polkadot/util';
-import { WebbError, WebbErrorCodes } from '@webb-tools/dapp-types/WebbError';
-import { useEffect, useState } from 'react';
-import { Subscription } from 'rxjs';
+import { DEFAULT_FLAGS_ELECTED } from '@webb-tools/dapp-config/constants/tangle';
+import { useCallback, useEffect, useState } from 'react';
+import { map } from 'rxjs';
-import useNetworkStore from '../../context/useNetworkStore';
-import useFormatReturnType from '../../hooks/useFormatReturnType';
-import useLocalStorage, { LocalStorageKey } from '../../hooks/useLocalStorage';
-import { getPolkadotApiPromise, getPolkadotApiRx } from '../../utils/polkadot';
+import useApiRx from '../../hooks/useApiRx';
-// TODO: This is causing performance issues. Needs to be optimized.
export default function useActiveAndDelegationCountSubscription(
defaultValue: { value1: number | null; value2: number | null } = {
value1: null,
@@ -21,90 +17,65 @@ export default function useActiveAndDelegationCountSubscription(
const [error, setError] = useState(null);
const [value1, setValue1] = useState(defaultValue.value1);
const [value2, setValue2] = useState(defaultValue.value2);
- const { rpcEndpoint } = useNetworkStore();
- const { get: getCachedValue, set: setCache } = useLocalStorage(
- LocalStorageKey.ACTIVE_AND_DELEGATION_COUNT,
- true
+ const {
+ isLoading: isLoadingCounterForNominators,
+ error: counterForNominatorsError,
+ } = useApiRx(
+ useCallback(
+ (apiRx) =>
+ apiRx.query.staking.counterForNominators().pipe(
+ map((nominatorsCount) => {
+ const nominatorsCountNum = Number(formatNumber(nominatorsCount));
+
+ setValue2(nominatorsCountNum);
+ })
+ ),
+ []
+ )
);
- // After mount, try to get the cached value and set it.
- useEffect(() => {
- const cachedValue = getCachedValue();
-
- if (cachedValue !== null) {
- setValue1(cachedValue.value1);
- setValue2(cachedValue.value2);
- setIsLoading(false);
- }
- }, [getCachedValue]);
-
- useEffect(() => {
- let isMounted = true;
- let sub: Subscription | null = null;
-
- const subscribeData = async () => {
- try {
- const api = await getPolkadotApiRx(rpcEndpoint);
- const apiPromise = await getPolkadotApiPromise(rpcEndpoint);
- const currentEra = await apiPromise.query.staking.currentEra();
- const eraIndex = currentEra.unwrap();
-
- sub = api.query.staking
- .counterForNominators()
- .subscribe(async (value) => {
- try {
- const counterForNominators = formatNumber(value);
- const exposures =
- await apiPromise.query.staking.erasStakers.entries(eraIndex);
-
- const nominatorsSet = new Set();
-
- exposures.forEach(([_, exposure]) => {
- exposure.others.forEach(({ who }) => {
- nominatorsSet.add(who.toString());
+ const { isLoading: isLoadingActiveNominators, error: activeNominatorsError } =
+ useApiRx(
+ useCallback(
+ (apiRx) =>
+ apiRx.derive.staking.electedInfo(DEFAULT_FLAGS_ELECTED).pipe(
+ map((electedInfo) => {
+ const nominators: Set = new Set();
+
+ for (let i = 0; i < electedInfo.info.length; i++) {
+ const { exposurePaged } = electedInfo.info[i];
+ const exposure = exposurePaged.isSome && exposurePaged.unwrap();
+ if (!exposure) {
+ continue;
+ }
+
+ exposure.others.map(({ who }) => {
+ nominators.add(who.toString());
});
- });
-
- const newValue1 = nominatorsSet.size;
- const newValue2 = Number(counterForNominators);
-
- if (isMounted && (newValue1 !== value1 || newValue2 !== value2)) {
- setValue1(newValue1);
- setValue2(newValue2);
- setCache({ value1: newValue1, value2: newValue2 });
- setIsLoading(false);
- }
- } catch (error) {
- if (isMounted) {
- setError(
- error instanceof Error
- ? error
- : WebbError.from(WebbErrorCodes.UnknownError)
- );
- setIsLoading(false);
}
- }
- });
- } catch (error) {
- if (isMounted) {
- setError(
- error instanceof Error
- ? error
- : WebbError.from(WebbErrorCodes.UnknownError)
- );
- setIsLoading(false);
- }
- }
- };
- subscribeData();
+ const activeNominatorsCount = nominators.size;
- return () => {
- isMounted = false;
- sub?.unsubscribe();
- };
- }, [rpcEndpoint, setCache, value1, value2]);
+ setValue1(activeNominatorsCount);
+ })
+ ),
+ []
+ )
+ );
- return useFormatReturnType({ isLoading, error, data: { value1, value2 } });
+ // Sync the loading & error states.
+ useEffect(() => {
+ setIsLoading(isLoadingCounterForNominators || isLoadingActiveNominators);
+ }, [isLoadingCounterForNominators, isLoadingActiveNominators]);
+
+ useEffect(() => {
+ setError(counterForNominatorsError || activeNominatorsError);
+ }, [counterForNominatorsError, activeNominatorsError]);
+
+ return {
+ data: { value1, value2 },
+ isLoading,
+ error,
+ };
}
diff --git a/apps/tangle-dapp/data/KeyStats/useIdealStakePercentage.ts b/apps/tangle-dapp/data/KeyStats/useIdealStakePercentage.ts
index 578b1cc85a..672fcc2590 100644
--- a/apps/tangle-dapp/data/KeyStats/useIdealStakePercentage.ts
+++ b/apps/tangle-dapp/data/KeyStats/useIdealStakePercentage.ts
@@ -4,45 +4,27 @@ import { BN_ZERO } from '@polkadot/util';
import { useEffect, useState } from 'react';
import useNetworkStore from '../../context/useNetworkStore';
-import useFormatReturnType from '../../hooks/useFormatReturnType';
-import useLocalStorage, { LocalStorageKey } from '../../hooks/useLocalStorage';
import { calculateInflation } from '../../utils';
import ensureError from '../../utils/ensureError';
-import { getPolkadotApiPromise } from '../../utils/polkadot';
+import { getApiPromise } from '../../utils/polkadot';
export default function useIdealStakedPercentage(
defaultValue: { value1: number | null } = { value1: null }
) {
- const { get: getCachedValue, set: setCache } = useLocalStorage(
- LocalStorageKey.IDEAL_STAKE_PERCENTAGE,
- true
- );
-
const [value1, setValue1] = useState(defaultValue.value1);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const { rpcEndpoint } = useNetworkStore();
- // After mount, try to get the cached value and set it.
- useEffect(() => {
- const cachedValue = getCachedValue();
-
- if (cachedValue !== null) {
- setValue1(cachedValue.value1);
- setIsLoading(false);
- }
- }, [getCachedValue]);
-
useEffect(() => {
const fetchData = async () => {
try {
- const api = await getPolkadotApiPromise(rpcEndpoint);
+ const api = await getApiPromise(rpcEndpoint);
const inflation = calculateInflation(api, BN_ZERO, BN_ZERO, BN_ZERO);
const idealStakePercentage = inflation.idealStake * 100;
if (idealStakePercentage !== value1) {
setValue1(idealStakePercentage);
- setCache({ value1: idealStakePercentage });
}
setIsLoading(false);
@@ -53,11 +35,11 @@ export default function useIdealStakedPercentage(
};
fetchData();
- }, [value1, setCache, rpcEndpoint]);
+ }, [value1, rpcEndpoint]);
- return useFormatReturnType({
+ return {
isLoading,
error,
data: { value1, value2: null },
- });
+ };
}
diff --git a/apps/tangle-dapp/data/KeyStats/useInflationPercentage.ts b/apps/tangle-dapp/data/KeyStats/useInflationPercentage.ts
index ec25409e33..361a5cb5df 100644
--- a/apps/tangle-dapp/data/KeyStats/useInflationPercentage.ts
+++ b/apps/tangle-dapp/data/KeyStats/useInflationPercentage.ts
@@ -8,7 +8,7 @@ import useNetworkStore from '../../context/useNetworkStore';
import useFormatReturnType from '../../hooks/useFormatReturnType';
import { calculateInflation } from '../../utils';
import ensureError from '../../utils/ensureError';
-import { getPolkadotApiPromise, getPolkadotApiRx } from '../../utils/polkadot';
+import { getApiPromise, getApiRx } from '../../utils/polkadot';
export default function useInflationPercentage(
defaultValue: { value1: number | null; value2: number | null } = {
@@ -27,8 +27,8 @@ export default function useInflationPercentage(
const fetchData = async () => {
try {
- const apiRx = await getPolkadotApiRx(rpcEndpoint);
- const apiPromise = await getPolkadotApiPromise(rpcEndpoint);
+ const apiRx = await getApiRx(rpcEndpoint);
+ const apiPromise = await getApiPromise(rpcEndpoint);
setIsLoading(true);
@@ -49,7 +49,7 @@ export default function useInflationPercentage(
const inflationPercentage = inflation.inflation;
if (isMounted) {
- setValue1(Number(inflationPercentage.toFixed(1)));
+ setValue1(Math.trunc(inflationPercentage * 10) / 10);
setIsLoading(false);
}
});
diff --git a/apps/tangle-dapp/data/KeyStats/useValidatorsCountSubscription.ts b/apps/tangle-dapp/data/KeyStats/useValidatorsCountSubscription.ts
index 3cdd50064d..abfbf9ec25 100644
--- a/apps/tangle-dapp/data/KeyStats/useValidatorsCountSubscription.ts
+++ b/apps/tangle-dapp/data/KeyStats/useValidatorsCountSubscription.ts
@@ -5,9 +5,7 @@ import { useEffect, useState } from 'react';
import { firstValueFrom, Subscription } from 'rxjs';
import useNetworkStore from '../../context/useNetworkStore';
-import useFormatReturnType from '../../hooks/useFormatReturnType';
-import useLocalStorage, { LocalStorageKey } from '../../hooks/useLocalStorage';
-import { getPolkadotApiRx } from '../../utils/polkadot';
+import { getApiRx } from '../../utils/polkadot';
export default function useValidatorCountSubscription(
defaultValue: { value1: number | null; value2: number | null } = {
@@ -15,35 +13,19 @@ export default function useValidatorCountSubscription(
value2: null,
}
) {
- const { get: getCachedValue, set: setCache } = useLocalStorage(
- LocalStorageKey.VALIDATOR_COUNTS,
- true
- );
-
const [value1, setValue1] = useState(defaultValue.value1);
const [value2, setValue2] = useState(defaultValue.value2);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const { rpcEndpoint } = useNetworkStore();
- // After mount, try to get the cached value and set it.
- useEffect(() => {
- const cachedValue = getCachedValue();
-
- if (cachedValue !== null) {
- setValue1(cachedValue.value1);
- setValue2(cachedValue.value2);
- setIsLoading(false);
- }
- }, [getCachedValue]);
-
useEffect(() => {
let isMounted = true;
let sub: Subscription | null = null;
const subscribeData = async () => {
try {
- const api = await getPolkadotApiRx(rpcEndpoint);
+ const api = await getApiRx(rpcEndpoint);
sub = api.query.session.validators().subscribe(async (validators) => {
try {
@@ -59,10 +41,6 @@ export default function useValidatorCountSubscription(
) {
setValue1(validators.length);
setValue2(totalValidatorsCount.toNumber());
- setCache({
- value1: validators.length,
- value2: totalValidatorsCount.toNumber(),
- });
setIsLoading(false);
}
} catch (error) {
@@ -94,7 +72,11 @@ export default function useValidatorCountSubscription(
isMounted = false;
sub?.unsubscribe();
};
- }, [value1, value2, setCache, rpcEndpoint]);
+ }, [value1, value2, rpcEndpoint]);
- return useFormatReturnType({ isLoading, error, data: { value1, value2 } });
+ return {
+ data: { value1, value2 },
+ isLoading,
+ error,
+ };
}
diff --git a/apps/tangle-dapp/data/KeyStats/useWaitingCountSubscription.ts b/apps/tangle-dapp/data/KeyStats/useWaitingCountSubscription.ts
index 74ec04e007..6fc1b5b7e1 100644
--- a/apps/tangle-dapp/data/KeyStats/useWaitingCountSubscription.ts
+++ b/apps/tangle-dapp/data/KeyStats/useWaitingCountSubscription.ts
@@ -5,9 +5,7 @@ import { useEffect, useState } from 'react';
import { Subscription } from 'rxjs';
import useNetworkStore from '../../context/useNetworkStore';
-import useFormatReturnType from '../../hooks/useFormatReturnType';
-import useLocalStorage, { LocalStorageKey } from '../../hooks/useLocalStorage';
-import { getPolkadotApiRx } from '../../utils/polkadot';
+import { getApiRx } from '../../utils/polkadot';
export default function useWaitingCountSubscription(
defaultValue: { value1: number | null; value2: number | null } = {
@@ -15,40 +13,24 @@ export default function useWaitingCountSubscription(
value2: null,
}
) {
- const { get: getCachedValue, set: setCache } = useLocalStorage(
- LocalStorageKey.WAITING_COUNT,
- true
- );
-
const [value1, setValue1] = useState(defaultValue.value1);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const { rpcEndpoint } = useNetworkStore();
- // After mount, try to get the cached value and set it.
- useEffect(() => {
- const cachedValue = getCachedValue();
-
- if (cachedValue !== null) {
- setValue1(cachedValue.value1);
- setIsLoading(false);
- }
- }, [getCachedValue]);
-
useEffect(() => {
let isMounted = true;
let sub: Subscription | null = null;
const subscribeData = async () => {
try {
- const api = await getPolkadotApiRx(rpcEndpoint);
+ const api = await getApiRx(rpcEndpoint);
sub = api.derive.staking.waitingInfo().subscribe((waitingInfo) => {
const newWaitingCount = waitingInfo.waiting.length;
if (isMounted && newWaitingCount !== value1) {
setValue1(newWaitingCount);
- setCache({ value1: newWaitingCount });
setIsLoading(false);
}
});
@@ -70,11 +52,11 @@ export default function useWaitingCountSubscription(
isMounted = false;
sub?.unsubscribe();
};
- }, [value1, setCache, rpcEndpoint]);
+ }, [value1, rpcEndpoint]);
- return useFormatReturnType({
+ return {
isLoading,
error,
data: { value1, value2: null },
- });
+ };
}
diff --git a/apps/tangle-dapp/data/NominationsPayouts/useNominations.ts b/apps/tangle-dapp/data/NominationsPayouts/useNominations.ts
index fedc212351..3ec48416b3 100644
--- a/apps/tangle-dapp/data/NominationsPayouts/useNominations.ts
+++ b/apps/tangle-dapp/data/NominationsPayouts/useNominations.ts
@@ -1,159 +1,89 @@
'use client';
-import { u128 } from '@polkadot/types';
-import { WebbError, WebbErrorCodes } from '@webb-tools/dapp-types/WebbError';
-import { useEffect, useState } from 'react';
-import { Subscription } from 'rxjs';
-
-import useNetworkStore from '../../context/useNetworkStore';
-import useFormatReturnType from '../../hooks/useFormatReturnType';
-import useLocalStorage, { LocalStorageKey } from '../../hooks/useLocalStorage';
-import { Delegator } from '../../types';
-import {
- formatTokenBalance,
- getPolkadotApiPromise,
- getPolkadotApiRx,
- getTotalNumberOfNominators,
- getValidatorCommission,
- getValidatorIdentityName,
-} from '../../utils/polkadot';
-
-export default function useNominations(
- address: string,
- defaultValue: { delegators: Delegator[] } = {
- delegators: [],
- }
-) {
- const [isLoading, setIsLoading] = useState(true);
- const [error, setError] = useState(null);
- const { rpcEndpoint, nativeTokenSymbol } = useNetworkStore();
-
- const {
- valueAfterMount: cachedNominations,
- setWithPreviousValue: setCachedNominations,
- } = useLocalStorage(LocalStorageKey.Nominations, true);
-
- const [delegators, setDelegators] = useState(
- (cachedNominations && cachedNominations[address]) ?? defaultValue.delegators
+import { useCallback, useMemo } from 'react';
+
+import useApiRx from '../../hooks/useApiRx';
+import useSubstrateAddress from '../../hooks/useSubstrateAddress';
+import { Nominee } from '../../types/index';
+import Optional from '../../utils/Optional';
+import createNominee from '../../utils/staking/createNominee';
+import useStakingExposures from '../staking/useStakingExposures';
+import useValidatorPrefs from '../staking/useValidatorPrefs';
+import useValidatorIdentityNames from '../ValidatorTables/useValidatorIdentityNames';
+
+const useNominations = () => {
+ const activeSubstrateAddress = useSubstrateAddress();
+ const { result: identities } = useValidatorIdentityNames();
+ const { result: prefs } = useValidatorPrefs();
+ const { result: exposures } = useStakingExposures();
+
+ const { result: sessionValidators } = useApiRx(
+ useCallback((api) => api.query.session.validators(), [])
);
- useEffect(() => {
- let isMounted = true;
- let sub: Subscription | null = null;
-
- const subscribeData = async () => {
- if (!address) {
- if (isMounted) {
- setDelegators([]);
- setIsLoading(false);
+ const { result: nominationInfoOpt } = useApiRx(
+ useCallback(
+ (api) => {
+ if (activeSubstrateAddress === null) {
+ return null;
}
- return;
- }
-
- try {
- const apiSub = await getPolkadotApiRx(rpcEndpoint);
- const apiPromise = await getPolkadotApiPromise(rpcEndpoint);
-
- setIsLoading(true);
-
- sub = apiSub.query.staking
- .nominators(address)
- .subscribe(async (nominatorData) => {
- const targets = nominatorData.unwrapOrDefault().targets;
-
- // TODO: This needs to be optimized. Make a single request to get all the data, then work off that data. Currently, this may make many requests, depending on how many targets there are PER nominator (O(nominators * targets)).
- const delegators: Delegator[] = await Promise.all(
- targets.map(async (target) => {
- const isActive = await apiPromise.query.session
- .validators()
- .then((activeValidators) =>
- activeValidators.some(
- (val) => val.toString() === target.toString()
- )
- );
-
- const identity = await getValidatorIdentityName(
- rpcEndpoint,
- target.toString()
- );
-
- const commission = await getValidatorCommission(
- rpcEndpoint,
- target.toString()
- );
-
- const delegationsValue = await getTotalNumberOfNominators(
- rpcEndpoint,
- target.toString()
- );
-
- const delegations = delegationsValue?.toString();
-
- const currentEra = await apiPromise.query.staking.currentEra();
- const exposure = await apiPromise.query.staking.erasStakers(
- currentEra.unwrap(),
- target.toString()
- );
-
- const selfStaked = new u128(
- apiPromise.registry,
- exposure.own.toString()
- );
-
- const selfStakedBalance = formatTokenBalance(
- selfStaked,
- nativeTokenSymbol
- );
-
- const totalStakeAmount = exposure.total.unwrap();
- const effectiveAmountStaked = formatTokenBalance(
- totalStakeAmount,
- nativeTokenSymbol
- );
-
- return {
- address: target.toString(),
- identity: identity ?? '',
- selfStaked: selfStakedBalance ?? '',
- isActive,
- commission: commission ?? '',
- delegations: delegations ?? '',
- effectiveAmountStaked: effectiveAmountStaked ?? '',
- };
- })
- );
-
- if (isMounted) {
- setDelegators(delegators);
- setCachedNominations((previous) => ({
- ...previous,
- [address]: delegators,
- }));
- setIsLoading(false);
- }
- });
- } catch (e) {
- if (isMounted) {
- setError(
- e instanceof Error ? e : WebbError.from(WebbErrorCodes.UnknownError)
- );
- setIsLoading(false);
- }
- }
- };
-
- subscribeData();
-
- return () => {
- isMounted = false;
- sub?.unsubscribe();
- };
- }, [address, rpcEndpoint, setCachedNominations, nativeTokenSymbol]);
-
- return useFormatReturnType({
- isLoading,
- error,
- data: { delegators },
- });
-}
+ return api.query.staking.nominators(activeSubstrateAddress);
+ },
+ [activeSubstrateAddress]
+ )
+ );
+
+ const nominees = useMemo | null>(() => {
+ if (
+ nominationInfoOpt === null ||
+ sessionValidators === null ||
+ identities === null ||
+ prefs === null ||
+ exposures === null
+ ) {
+ return null;
+ } else if (nominationInfoOpt.isNone) {
+ return new Optional();
+ }
+
+ const nomineeAccountIds = nominationInfoOpt.unwrap().targets;
+
+ const nominees = nomineeAccountIds.map((nomineeAccountId) => {
+ const nomineeAddress = nomineeAccountId.toString();
+
+ // TODO: Turn this into a set, and then use `has` instead of `some`.
+ const isActive = sessionValidators.some(
+ (validatorAddress) => validatorAddress.toString() === nomineeAddress
+ );
+
+ return createNominee({
+ address: nomineeAddress,
+ isActive,
+ identities,
+ prefs,
+ getExposure: (address) => {
+ const exposureOpt = exposures.get(address);
+
+ if (exposureOpt === undefined || exposureOpt.isNone) {
+ return undefined;
+ }
+
+ const exposure = exposureOpt.unwrap();
+
+ return {
+ own: exposure.own.toBn(),
+ total: exposure.total.toBn(),
+ nominatorCount: exposure.nominatorCount.toNumber(),
+ };
+ },
+ });
+ });
+
+ return new Optional(nominees);
+ }, [exposures, identities, nominationInfoOpt, prefs, sessionValidators]);
+
+ return nominees;
+};
+
+export default useNominations;
diff --git a/apps/tangle-dapp/data/NominationsPayouts/usePayouts.ts b/apps/tangle-dapp/data/NominationsPayouts/usePayouts.ts
index 720960695f..29a518cffe 100644
--- a/apps/tangle-dapp/data/NominationsPayouts/usePayouts.ts
+++ b/apps/tangle-dapp/data/NominationsPayouts/usePayouts.ts
@@ -1,318 +1,339 @@
'use client';
-import { u128 } from '@polkadot/types';
-import { WebbError, WebbErrorCodes } from '@webb-tools/dapp-types/WebbError';
-import { useEffect, useState } from 'react';
-import { Subscription } from 'rxjs';
+import { Option } from '@polkadot/types';
+import {
+ PalletStakingNominations,
+ PalletStakingValidatorPrefs,
+} from '@polkadot/types/lookup';
+import { BN_ZERO } from '@polkadot/util';
+import { decodeAddress, encodeAddress } from '@polkadot/util-crypto';
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import useNetworkStore from '../../context/useNetworkStore';
-import useFormatReturnType from '../../hooks/useFormatReturnType';
+import useApiRx from '../../hooks/useApiRx';
import useLocalStorage, { LocalStorageKey } from '../../hooks/useLocalStorage';
+import useSubstrateAddress from '../../hooks/useSubstrateAddress';
import { Payout } from '../../types';
import {
- formatTokenBalance,
- getPolkadotApiPromise,
- getPolkadotApiRx,
- getValidatorCommission,
+ getApiPromise as getPolkadotApiPromise,
getValidatorIdentityName,
} from '../../utils/polkadot';
+import useEraTotalRewards from '../payouts/useEraTotalRewards';
+
+type ValidatorReward = {
+ validatorAddress: string;
+ era: number;
+ eraTotalRewardPoints: number;
+ validatorRewardPoints: number;
+};
+
+type PayoutData = {
+ data: Payout[];
+ isLoading: boolean;
+};
+
+export default function usePayouts(): PayoutData {
+ const payoutsRef = useRef([]);
+ const isPayoutsFetched = useRef(false);
+ const fetchedPayoutPromises = useRef | null>(
+ null
+ );
+
+ const [isLoading, setIsLoading] = useState(false);
+
+ const { setWithPreviousValue: setCachedPayouts } = useLocalStorage(
+ LocalStorageKey.PAYOUTS,
+ true
+ );
+
+ const { rpcEndpoint, network } = useNetworkStore();
+
+ const activeSubstrateAddress = useSubstrateAddress();
+
+ const activeSubstrateAddressEncoded = useMemo(() => {
+ if (!activeSubstrateAddress) return;
+
+ const publicKey = decodeAddress(activeSubstrateAddress);
+
+ return encodeAddress(publicKey, network.ss58Prefix);
+ }, [activeSubstrateAddress, network.ss58Prefix]);
+
+ const { result: nominators } = useApiRx(
+ useCallback(
+ (api) => api.query.staking.nominators(activeSubstrateAddress),
+ [activeSubstrateAddress]
+ )
+ );
+
+ const { result: erasRewardsPoints } = useApiRx(
+ useCallback((api) => api.query.staking.erasRewardPoints.entries(), [])
+ );
+
+ const myNominations = useMemo(() => {
+ if (!nominators) return [];
+ const nominatorsData = nominators as Option;
+ return nominatorsData.isSome ? nominatorsData.unwrap().targets : [];
+ }, [nominators]);
-export default function usePayouts(
- address: string,
- defaultValue: { payouts: Payout[] } = {
- payouts: [],
- }
-) {
- const {
- valueAfterMount: cachedPayouts,
- setWithPreviousValue: setCachedPayouts,
- } = useLocalStorage(LocalStorageKey.Payouts, true);
-
- const [payouts, setPayouts] = useState(
- (cachedPayouts && cachedPayouts[address]) ?? defaultValue.payouts
+ const { data: eraTotalRewards } = useEraTotalRewards();
+
+ const { result: validators } = useApiRx(
+ useCallback((api) => api.query.staking.validators.entries(), [])
);
- const [isLoading, setIsLoading] = useState(true);
- const [error, setError] = useState(null);
- const { rpcEndpoint, nativeTokenSymbol } = useNetworkStore();
+ const mappedValidatorInfo = useMemo(() => {
+ const map = new Map();
+
+ validators?.forEach(([storageKey, validatorInfo]) => {
+ map.set(storageKey.args[0].toString(), validatorInfo);
+ });
+
+ return map;
+ }, [validators]);
useEffect(() => {
- let isMounted = true;
- let sub: Subscription | null = null;
-
- const subscribeData = async () => {
- if (!address) {
- if (isMounted) {
- setPayouts([]);
- setIsLoading(false);
+ isPayoutsFetched.current = false;
+ fetchedPayoutPromises.current = null;
+ setIsLoading(false);
+ }, [activeSubstrateAddress, rpcEndpoint]);
+
+ const payoutPromises = useMemo(() => {
+ if (isPayoutsFetched.current) return fetchedPayoutPromises.current;
+
+ if (
+ !erasRewardsPoints ||
+ myNominations.length === 0 ||
+ !eraTotalRewards ||
+ !mappedValidatorInfo ||
+ !activeSubstrateAddress ||
+ !activeSubstrateAddressEncoded
+ ) {
+ return null;
+ }
+
+ const allRewards: ValidatorReward[] = [];
+
+ for (const validatorAddress of myNominations) {
+ for (const point of erasRewardsPoints) {
+ const era = point[0].args[0].toNumber();
+ const rewards = point[1].toHuman();
+ let validatorRewardPoints = 0;
+ const totalRewardPoints = parseFloat(
+ rewards.total?.toString().replace(/,/g, '') ?? '0'
+ );
+ if (
+ typeof rewards.individual === 'object' &&
+ rewards.individual !== null
+ ) {
+ Object.entries(rewards.individual).forEach(([key, value]) => {
+ if (key === validatorAddress.toString()) {
+ validatorRewardPoints = value
+ ? parseFloat(value.toString().replace(/,/g, ''))
+ : 0;
+ }
+ });
}
- return;
+ allRewards.push({
+ era,
+ eraTotalRewardPoints: totalRewardPoints,
+ validatorAddress: validatorAddress.toString(),
+ validatorRewardPoints,
+ });
}
+ }
- try {
- const apiSub = await getPolkadotApiRx(rpcEndpoint);
+ const payoutPromises = Promise.all(
+ allRewards.map(async (reward) => {
const apiPromise = await getPolkadotApiPromise(rpcEndpoint);
- if (!apiSub || !apiPromise) {
- throw WebbError.from(WebbErrorCodes.ApiNotReady);
+ const claimedReward = await apiPromise.query.staking.claimedRewards(
+ reward.era,
+ reward.validatorAddress
+ );
+
+ if (claimedReward.length > 0) {
+ return undefined;
}
- setIsLoading(true);
-
- const nominations = await apiPromise.query.staking.nominators(address);
- const myNominations = nominations.isSome
- ? nominations.unwrap().targets
- : [];
-
- sub = apiSub.query.staking.erasRewardPoints
- .entries()
- .subscribe(async (points) => {
- const allRewards: {
- era: number;
- totalRewardPoints: number;
- validator: string;
- validatorRewardPoints: number;
- }[] = [];
-
- let validatorPayoutsPromises: Promise[] = [];
-
- myNominations.forEach((validator) => {
- points.forEach((point) => {
- // regex to remove commas from the era number
- const era = Number(
- point[0].toHuman()?.toString().replace(/,/g, '')
- );
-
- if (!era) {
- return;
- }
-
- const rewards = point[1].toHuman();
-
- if (!rewards) {
- return;
- }
-
- let validatorRewardPoints = 0;
-
- const totalRewardPoints = parseFloat(
- rewards.total?.toString().replace(/,/g, '') ?? '0'
- );
-
- if (
- typeof rewards.individual === 'object' &&
- rewards.individual !== null
- ) {
- Object.entries(rewards.individual).forEach(([key, value]) => {
- if (key === validator.toString()) {
- validatorRewardPoints = Number(value);
- }
- });
- }
-
- if (validatorRewardPoints > 0) {
- allRewards.push({
- era,
- totalRewardPoints,
- validator: validator.toString(),
- validatorRewardPoints,
- });
- }
- });
-
- validatorPayoutsPromises = allRewards
- .map(async (reward) => {
- const {
- era,
- totalRewardPoints,
- validator,
- validatorRewardPoints,
- } = reward;
-
- const validatorLedger = await apiPromise.query.staking.ledger(
- validator
- );
-
- // TODO: For some reason, this can be `undefined`. Might be caused to the Substrate types being out of date? For now, default to an empty array.
- const claimedRewards =
- validatorLedger.unwrap().claimedRewards ?? [];
-
- const claimedEras = claimedRewards.map((era) =>
- Number(era.toString().replace(/,/g, ''))
- );
-
- if (claimedEras.includes(era)) {
- return;
- }
-
- const erasTotalReward =
- await apiPromise.query.staking.erasValidatorReward(era);
-
- if (erasTotalReward.isNone) {
- return;
- }
-
- const validatorTotalReward =
- (validatorRewardPoints *
- Number(erasTotalReward.unwrap().toString())) /
- totalRewardPoints;
-
- if (validatorTotalReward > 0) {
- const validatorTotalRewardFormatted = formatTokenBalance(
- new u128(
- apiPromise.registry,
- BigInt(Math.floor(validatorTotalReward))
- ),
- nativeTokenSymbol
- );
-
- const eraStaker =
- await apiPromise.query.staking.erasStakers(
- era,
- validator
- );
-
- const validatorTotalStake = eraStaker.total.unwrap();
-
- const validatorTotalStakeFormatted = formatTokenBalance(
- validatorTotalStake,
- nativeTokenSymbol
- );
-
- if (
- Number(validatorTotalStake.toString()) > 0 &&
- eraStaker.others.length > 0
- ) {
- const nominatorStakeInfo = eraStaker.others.find(
- (nominator) => nominator.who.toString() === address
- );
-
- if (nominatorStakeInfo && !nominatorStakeInfo.isEmpty) {
- const nominatorTotalStake =
- nominatorStakeInfo.value.unwrap();
-
- if (Number(nominatorTotalStake.toString()) > 0) {
- const nominatorStakePercentage =
- (Number(nominatorTotalStake.toString()) /
- Number(validatorTotalStake.toString())) *
- 100;
-
- const validatorCommissionPercentage =
- await getValidatorCommission(
- rpcEndpoint,
- validator.toString()
- );
-
- const validatorCommission =
- validatorTotalReward *
- (Number(validatorCommissionPercentage) / 100);
-
- const distributableReward =
- validatorTotalReward - validatorCommission;
-
- const nominatorTotalReward =
- (nominatorStakePercentage / 100) *
- distributableReward;
-
- const nominatorTotalRewardFormatted =
- formatTokenBalance(
- new u128(
- apiPromise.registry,
- BigInt(Math.floor(nominatorTotalReward))
- ),
- nativeTokenSymbol
- );
-
- const validatorIdentity =
- await getValidatorIdentityName(
- rpcEndpoint,
- validator
- );
-
- const validatorNominators = await Promise.all(
- eraStaker.others.map(async (nominator) => {
- const nominatorIdentity =
- await getValidatorIdentityName(
- rpcEndpoint,
- nominator.who.toString()
- );
-
- return {
- address: nominator.who.toString(),
- identity: nominatorIdentity ?? '',
- };
- })
- );
-
- if (
- validatorTotalStakeFormatted &&
- validatorTotalRewardFormatted &&
- nominatorTotalRewardFormatted
- ) {
- return {
- era,
- validator: {
- address: validator,
- identity: validatorIdentity ?? '',
- },
- validatorTotalStake: validatorTotalStakeFormatted,
- nominators: validatorNominators,
- validatorTotalReward:
- validatorTotalRewardFormatted,
- nominatorTotalReward:
- nominatorTotalRewardFormatted,
- status: 'unclaimed',
- };
- }
- }
- }
- }
- }
- })
- .filter(
- (payout): payout is Promise => payout !== undefined
- );
- });
-
- if (myNominations.length > 0 && isMounted) {
- const validatorPayouts = await Promise.all(
- validatorPayoutsPromises
- );
-
- const payoutsData = validatorPayouts
- .filter((payout) => payout !== undefined)
- .sort((a, b) => Number(a.era) - Number(b.era));
-
- setPayouts(payoutsData);
- setCachedPayouts((previous) => ({
- ...previous,
- [address]: payoutsData,
- }));
- setIsLoading(false);
- }
- });
- } catch (e) {
- if (isMounted) {
- setPayouts([]);
- setError(
- e instanceof Error ? e : WebbError.from(WebbErrorCodes.UnknownError)
+ const eraTotalRewardOpt = eraTotalRewards.get(reward.era);
+ if (eraTotalRewardOpt === undefined || eraTotalRewardOpt.isNone) {
+ return undefined;
+ }
+
+ const eraTotalRewardOptValue = eraTotalRewardOpt.unwrap();
+
+ const validatorTotalReward = eraTotalRewardOptValue
+ .toBn()
+ .muln(reward.validatorRewardPoints)
+ .divn(reward.eraTotalRewardPoints);
+
+ if (validatorTotalReward.isZero()) {
+ return undefined;
+ }
+
+ const erasStakersOverview =
+ await apiPromise.query.staking.erasStakersOverview(
+ reward.era,
+ reward.validatorAddress
);
- setIsLoading(false);
+
+ const validatorTotalStake = !erasStakersOverview.isNone
+ ? erasStakersOverview.unwrap().total.toBn()
+ : BN_ZERO;
+ const validatorNominatorCount = !erasStakersOverview.isNone
+ ? erasStakersOverview.unwrap().nominatorCount.toNumber()
+ : 0;
+
+ if (
+ Number(validatorTotalStake) === 0 ||
+ validatorNominatorCount === 0
+ ) {
+ return undefined;
}
- }
- };
- subscribeData();
+ const eraStakerPaged = await apiPromise.query.staking.erasStakersPaged(
+ reward.era,
+ reward.validatorAddress,
+ 0
+ );
+
+ if (eraStakerPaged.isNone) {
+ return undefined;
+ }
+
+ const nominatorStakeInfo = eraStakerPaged
+ .unwrap()
+ .others.find(
+ (nominator) =>
+ nominator.who.toString() === activeSubstrateAddressEncoded
+ );
+
+ if (nominatorStakeInfo === undefined || nominatorStakeInfo.isEmpty) {
+ return undefined;
+ }
+
+ const nominatorTotalStake = nominatorStakeInfo.value.unwrap();
+
+ if (nominatorTotalStake.isZero()) {
+ return undefined;
+ }
+
+ const validatorInfo = mappedValidatorInfo.get(reward.validatorAddress);
+
+ if (!validatorInfo) {
+ return undefined;
+ }
- return () => {
- isMounted = false;
- sub?.unsubscribe();
+ const validatorIdentityName = await getValidatorIdentityName(
+ rpcEndpoint,
+ reward.validatorAddress
+ );
+
+ const validatorNominators = await Promise.all(
+ eraStakerPaged.unwrap().others.map(async (nominator) => {
+ const nominatorIdentity = await getValidatorIdentityName(
+ rpcEndpoint,
+ nominator.who.toString()
+ );
+
+ return {
+ address: nominator.who.toString(),
+ identity: nominatorIdentity ?? '',
+ };
+ })
+ );
+
+ const stakerEraReward = await apiPromise.derive.staking.stakerRewards(
+ activeSubstrateAddressEncoded
+ );
+
+ const stakerEraRewardsEra = stakerEraReward.find(
+ (_reward) => _reward.era.toNumber() === reward.era
+ );
+
+ let nominatorTotalReward = BN_ZERO;
+
+ if (stakerEraRewardsEra) {
+ if (stakerEraRewardsEra.validators[reward.validatorAddress]) {
+ nominatorTotalReward =
+ stakerEraRewardsEra.validators[reward.validatorAddress].value ??
+ BN_ZERO;
+ }
+ }
+
+ if (
+ validatorTotalStake &&
+ validatorTotalReward &&
+ nominatorTotalReward
+ ) {
+ const payout: Payout = {
+ era: reward.era,
+ validator: {
+ address: reward.validatorAddress,
+ identity: validatorIdentityName ?? '',
+ },
+ validatorTotalStake: validatorTotalStake,
+ nominators: validatorNominators,
+ validatorTotalReward: validatorTotalReward,
+ nominatorTotalReward: nominatorTotalReward,
+ nominatorTotalRewardRaw: nominatorTotalReward,
+ };
+
+ isPayoutsFetched.current = true;
+
+ return payout;
+ }
+
+ return undefined;
+ })
+ );
+
+ fetchedPayoutPromises.current = payoutPromises;
+
+ return payoutPromises;
+ }, [
+ activeSubstrateAddress,
+ activeSubstrateAddressEncoded,
+ eraTotalRewards,
+ erasRewardsPoints,
+ mappedValidatorInfo,
+ myNominations,
+ rpcEndpoint,
+ ]);
+
+ useEffect(() => {
+ if (!activeSubstrateAddress) return;
+
+ const computePayouts = async () => {
+ setIsLoading(true);
+
+ if (!payoutPromises) {
+ payoutsRef.current = [];
+ setIsLoading(false);
+ return;
+ }
+
+ const payouts = await payoutPromises;
+ const payoutsData = payouts
+ .filter((payout): payout is Payout => payout !== undefined)
+ .sort((a, b) => a.era - b.era);
+
+ payoutsRef.current = payoutsData;
+ setCachedPayouts((previous) => ({
+ ...previous?.value,
+ [rpcEndpoint]: {
+ ...previous?.value?.[rpcEndpoint],
+ [activeSubstrateAddress]: payoutsData,
+ },
+ }));
+ setIsLoading(false);
};
- }, [address, rpcEndpoint, setCachedPayouts, nativeTokenSymbol]);
- return useFormatReturnType({
+ computePayouts();
+ }, [activeSubstrateAddress, payoutPromises, rpcEndpoint, setCachedPayouts]);
+
+ return {
+ data: payoutsRef.current,
isLoading,
- error,
- data: { payouts },
- });
+ };
}
diff --git a/apps/tangle-dapp/data/NominatorStats/usePaymentDestinationSubscription.ts b/apps/tangle-dapp/data/NominatorStats/usePaymentDestinationSubscription.ts
deleted file mode 100644
index 74d537ee7a..0000000000
--- a/apps/tangle-dapp/data/NominatorStats/usePaymentDestinationSubscription.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-'use client';
-
-import { WebbError, WebbErrorCodes } from '@webb-tools/dapp-types/WebbError';
-import { useEffect, useState } from 'react';
-import { type Subscription } from 'rxjs';
-
-import useNetworkStore from '../../context/useNetworkStore';
-import useFormatReturnType from '../../hooks/useFormatReturnType';
-import { getPolkadotApiRx } from '../../utils/polkadot';
-
-export default function usePaymentDestinationSubscription(
- address: string,
- defaultValue: { value1: number | string | null } = { value1: null }
-) {
- const [value1, setValue1] = useState(defaultValue.value1);
- const [isLoading, setIsLoading] = useState(true);
- const [error, setError] = useState(null);
- const { rpcEndpoint } = useNetworkStore();
-
- useEffect(() => {
- let isMounted = true;
- let sub: Subscription | null = null;
-
- const subscribeData = async () => {
- try {
- const api = await getPolkadotApiRx(rpcEndpoint);
-
- if (!address) {
- setValue1(null);
- setIsLoading(false);
- return;
- }
-
- sub = api.query.staking
- .payee(address)
- .subscribe(async (stakingRewardDestinationData) => {
- if (isMounted) {
- const stakingRewardDestination =
- stakingRewardDestinationData.toString();
-
- setValue1(stakingRewardDestination ?? null);
- setIsLoading(false);
- }
- });
- } catch (error) {
- if (isMounted) {
- setError(
- error instanceof Error
- ? error
- : WebbError.from(WebbErrorCodes.UnknownError)
- );
- setIsLoading(false);
- }
- }
- };
-
- subscribeData();
-
- return () => {
- isMounted = false;
- sub?.unsubscribe();
- };
- }, [address, rpcEndpoint]);
-
- return useFormatReturnType({ isLoading, error, data: { value1 } });
-}
diff --git a/apps/tangle-dapp/data/NominatorStats/useStakingRewardsDestination.ts b/apps/tangle-dapp/data/NominatorStats/useStakingRewardsDestination.ts
new file mode 100644
index 0000000000..d7df3e6804
--- /dev/null
+++ b/apps/tangle-dapp/data/NominatorStats/useStakingRewardsDestination.ts
@@ -0,0 +1,55 @@
+'use client';
+
+import { PalletStakingRewardDestination } from '@polkadot/types/lookup';
+import { useCallback } from 'react';
+import { map } from 'rxjs';
+
+import useApiRx from '../../hooks/useApiRx';
+import useSubstrateAddress from '../../hooks/useSubstrateAddress';
+import { StakingRewardsDestination } from '../../types';
+import Optional from '../../utils/Optional';
+
+const STAKING_SUBSTRATE_PAYEE_TO_LOCAL_PAYEE_MAP: Record<
+ PalletStakingRewardDestination['type'],
+ StakingRewardsDestination
+> = {
+ Staked: StakingRewardsDestination.STAKED,
+ Controller: StakingRewardsDestination.CONTROLLER,
+ Stash: StakingRewardsDestination.STASH,
+ Account: StakingRewardsDestination.ACCOUNT,
+ None: StakingRewardsDestination.NONE,
+};
+
+const useStakingRewardsDestination = () => {
+ const activeSubstrateAddress = useSubstrateAddress();
+
+ return useApiRx>(
+ useCallback(
+ (api) => {
+ if (activeSubstrateAddress === null) {
+ return null;
+ }
+
+ return api.query.staking.payee(activeSubstrateAddress).pipe(
+ map((substrateRewardsDestinationOpt) => {
+ if (substrateRewardsDestinationOpt.isNone) {
+ return new Optional();
+ }
+
+ const substrateRewardsDestination =
+ substrateRewardsDestinationOpt.unwrap();
+
+ return new Optional(
+ STAKING_SUBSTRATE_PAYEE_TO_LOCAL_PAYEE_MAP[
+ substrateRewardsDestination.type
+ ]
+ );
+ })
+ );
+ },
+ [activeSubstrateAddress]
+ )
+ );
+};
+
+export default useStakingRewardsDestination;
diff --git a/apps/tangle-dapp/data/NominatorStats/useTokenWalletFreeBalance.ts b/apps/tangle-dapp/data/NominatorStats/useTokenWalletFreeBalance.ts
index 58aac75f9f..7e5f209f1b 100644
--- a/apps/tangle-dapp/data/NominatorStats/useTokenWalletFreeBalance.ts
+++ b/apps/tangle-dapp/data/NominatorStats/useTokenWalletFreeBalance.ts
@@ -1,71 +1,18 @@
'use client';
import { BN } from '@polkadot/util';
-import { WebbError, WebbErrorCodes } from '@webb-tools/dapp-types/WebbError';
-import { useEffect, useState } from 'react';
-import { type Subscription } from 'rxjs';
-import useNetworkStore from '../../context/useNetworkStore';
+import { useBalancesContext } from '../../context/BalancesContext';
import useFormatReturnType from '../../hooks/useFormatReturnType';
-import { evmToSubstrateAddress } from '../../utils/evmToSubstrateAddress';
-import { getPolkadotApiRx } from '../../utils/polkadot';
export default function useTokenWalletFreeBalance(
- address: string,
defaultValue: { value1: BN | null } = { value1: null }
) {
- const [value1, setValue1] = useState(defaultValue.value1);
- const [isLoading, setIsLoading] = useState(true);
- const [error, setError] = useState(null);
- const { rpcEndpoint } = useNetworkStore();
+ const { free, isLoading, error } = useBalancesContext();
- useEffect(() => {
- let isMounted = true;
- let sub: Subscription | null = null;
-
- const fetchData = async () => {
- if (!address || address === '0x0') {
- setValue1(null);
- setIsLoading(false);
- return;
- }
-
- try {
- const api = await getPolkadotApiRx(rpcEndpoint);
- if (!api) {
- throw WebbError.from(WebbErrorCodes.ApiNotReady);
- }
-
- const substrateAddress = evmToSubstrateAddress(address);
-
- sub = api.query.system
- .account(substrateAddress)
- .subscribe(async (accData) => {
- if (isMounted) {
- const freeBalance = accData.data.free;
- setValue1(freeBalance);
- setIsLoading(false);
- }
- });
- } catch (error) {
- if (isMounted) {
- setError(
- error instanceof Error
- ? error
- : WebbError.from(WebbErrorCodes.UnknownError)
- );
- setIsLoading(false);
- }
- }
- };
-
- fetchData();
-
- return () => {
- isMounted = false;
- sub?.unsubscribe();
- };
- }, [address, rpcEndpoint]);
-
- return useFormatReturnType({ isLoading, error, data: { value1 } });
+ return useFormatReturnType({
+ isLoading,
+ error,
+ data: { value1: free ?? defaultValue.value1 },
+ });
}
diff --git a/apps/tangle-dapp/data/NominatorStats/useTotalPayoutRewards.ts b/apps/tangle-dapp/data/NominatorStats/useTotalPayoutRewards.ts
new file mode 100644
index 0000000000..7bfe31c92e
--- /dev/null
+++ b/apps/tangle-dapp/data/NominatorStats/useTotalPayoutRewards.ts
@@ -0,0 +1,76 @@
+'use client';
+
+import { BN, hexToBn } from '@polkadot/util';
+import { useEffect, useMemo, useState } from 'react';
+
+import useNetworkStore from '../../context/useNetworkStore';
+import useFormatReturnType from '../../hooks/useFormatReturnType';
+import useLocalStorage, { LocalStorageKey } from '../../hooks/useLocalStorage';
+import useSubstrateAddress from '../../hooks/useSubstrateAddress';
+
+export default function useTotalPayoutRewards(
+ defaultValue: { value1: BN | null } = { value1: null }
+) {
+ const [value1, setValue1] = useState(defaultValue.value1);
+
+ const { rpcEndpoint } = useNetworkStore();
+
+ const { valueOpt: cachedPayouts } = useLocalStorage(
+ LocalStorageKey.PAYOUTS,
+ true
+ );
+
+ const address = useSubstrateAddress();
+
+ const payoutsData = useMemo(() => {
+ if (
+ cachedPayouts === null ||
+ cachedPayouts.value === null ||
+ address === null
+ ) {
+ return [];
+ }
+
+ const payouts = cachedPayouts.value[rpcEndpoint]?.[address];
+
+ return payouts ?? [];
+ }, [address, cachedPayouts, rpcEndpoint]);
+
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ try {
+ if (!address) {
+ setValue1(null);
+ setIsLoading(false);
+ return;
+ }
+
+ if (payoutsData.length === 0) {
+ setValue1(new BN(0));
+ setIsLoading(false);
+ return;
+ }
+
+ const totalPayoutRewards = payoutsData.reduce((acc, payout) => {
+ const currentReward = hexToBn(
+ payout.nominatorTotalRewardRaw.toString()
+ );
+ return acc.add(currentReward);
+ }, new BN(0));
+
+ setValue1(new BN(totalPayoutRewards.toString()));
+ setIsLoading(false);
+ } catch (e) {
+ setError(
+ e instanceof Error
+ ? e
+ : new Error('An error occurred while calculating total payouts.')
+ );
+ setIsLoading(false);
+ }
+ }, [address, payoutsData]);
+
+ return useFormatReturnType({ isLoading, error, data: { value1 } });
+}
diff --git a/apps/tangle-dapp/data/NominatorStats/useTotalStakedAmountSubscription.ts b/apps/tangle-dapp/data/NominatorStats/useTotalStakedAmountSubscription.ts
index 4a3250d4d1..110bdac1ec 100644
--- a/apps/tangle-dapp/data/NominatorStats/useTotalStakedAmountSubscription.ts
+++ b/apps/tangle-dapp/data/NominatorStats/useTotalStakedAmountSubscription.ts
@@ -8,16 +8,18 @@ import { type Subscription } from 'rxjs';
import useNetworkStore from '../../context/useNetworkStore';
import useFormatReturnType from '../../hooks/useFormatReturnType';
-import { getPolkadotApiRx } from '../../utils/polkadot';
+import useSubstrateAddress from '../../hooks/useSubstrateAddress';
+import { getApiRx } from '../../utils/polkadot';
export default function useTotalStakedAmountSubscription(
- address: string,
defaultValue: { value1: BN | null } = { value1: null }
) {
const [value1, setValue1] = useState(defaultValue.value1);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
+
const { rpcEndpoint, nativeTokenSymbol } = useNetworkStore();
+ const address = useSubstrateAddress();
useEffect(() => {
let isMounted = true;
@@ -25,7 +27,7 @@ export default function useTotalStakedAmountSubscription(
const subscribeData = async () => {
try {
- const api = await getPolkadotApiRx(rpcEndpoint);
+ const api = await getApiRx(rpcEndpoint);
if (!address) {
setValue1(null);
diff --git a/apps/tangle-dapp/data/NominatorStats/useTotalUnbondedAndUnbondingAmount.ts b/apps/tangle-dapp/data/NominatorStats/useTotalUnbondedAndUnbondingAmount.ts
deleted file mode 100644
index 17aaa9cc86..0000000000
--- a/apps/tangle-dapp/data/NominatorStats/useTotalUnbondedAndUnbondingAmount.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-'use client';
-
-import { BN, BN_ZERO } from '@polkadot/util';
-import { useEffect, useMemo, useState } from 'react';
-
-import useFormatReturnType from '../../hooks/useFormatReturnType';
-import useUnbondingRemainingErasSubscription from './useUnbondingRemainingErasSubscription';
-
-type UnbondedAndUnbondingAmount = {
- unbonded: BN;
- unbonding: BN;
-};
-
-export default function useTotalUnbondedAndUnbondingAmount(
- address: string,
- defaultValue: { value1: UnbondedAndUnbondingAmount | null } = {
- value1: null,
- }
-) {
- const [value1, setValue1] = useState(defaultValue.value1);
- const [isLoading, setIsLoading] = useState(true);
- const [error, setError] = useState(null);
-
- const {
- data: unbondingRemainingErasData,
- error: unbondingRemainingErasError,
- } = useUnbondingRemainingErasSubscription(address);
-
- const unbondingRemainingEras = useMemo(() => {
- if (!unbondingRemainingErasData?.value1) {
- return [];
- }
-
- return unbondingRemainingErasData.value1;
- }, [unbondingRemainingErasData?.value1]);
-
- useEffect(() => {
- if (unbondingRemainingErasError) {
- setError(unbondingRemainingErasError);
- setIsLoading(false);
-
- return;
- }
-
- if (unbondingRemainingEras.length === 0) {
- setValue1({ unbonded: BN_ZERO, unbonding: BN_ZERO });
- setIsLoading(false);
-
- return;
- }
-
- let unbondedAmount = BN_ZERO;
- let unbondingAmount = BN_ZERO;
-
- unbondingRemainingEras.forEach((era) => {
- if (era.remainingEras <= 0) {
- unbondedAmount = unbondedAmount.add(era.amount);
- } else if (era.remainingEras > 0) {
- unbondingAmount = unbondingAmount.add(era.amount);
- }
- });
-
- setValue1({ unbonded: unbondedAmount, unbonding: unbondingAmount });
- setIsLoading(false);
- }, [unbondingRemainingEras, unbondingRemainingErasError]);
-
- return useFormatReturnType({ isLoading, error, data: { value1 } });
-}
diff --git a/apps/tangle-dapp/data/NominatorStats/useUnbondedAmount.ts b/apps/tangle-dapp/data/NominatorStats/useUnbondedAmount.ts
new file mode 100644
index 0000000000..629ea1e73a
--- /dev/null
+++ b/apps/tangle-dapp/data/NominatorStats/useUnbondedAmount.ts
@@ -0,0 +1,32 @@
+import { BN } from '@polkadot/util';
+import { useMemo } from 'react';
+
+import useUnbonding from '../staking/useUnbonding';
+
+const useUnbondedAmount = () => {
+ const { result: unbondingEntriesOpt, ...other } = useUnbonding();
+
+ const unbondedTotalAmount = useMemo(() => {
+ if (unbondingEntriesOpt === null) {
+ return null;
+ }
+
+ return unbondingEntriesOpt.map((entries) =>
+ entries.reduce((acc, entry) => {
+ // Only consider those entries whose remaining eras
+ // are less than or equal to 0 (ie. already unbonded).
+ if (entry.remainingEras.lten(0)) {
+ return acc.add(entry.amount);
+ }
+
+ // Do not add to unbonded amount if the entry
+ // is still in the unbonding period.
+ return acc;
+ }, new BN(0))
+ );
+ }, [unbondingEntriesOpt]);
+
+ return { result: unbondedTotalAmount, ...other };
+};
+
+export default useUnbondedAmount;
diff --git a/apps/tangle-dapp/data/NominatorStats/useUnbondingAmount.ts b/apps/tangle-dapp/data/NominatorStats/useUnbondingAmount.ts
new file mode 100644
index 0000000000..b7c171356d
--- /dev/null
+++ b/apps/tangle-dapp/data/NominatorStats/useUnbondingAmount.ts
@@ -0,0 +1,28 @@
+'use client';
+
+import { BN } from '@polkadot/util';
+import { useMemo } from 'react';
+
+import useUnbonding from '../staking/useUnbonding';
+
+const useUnbondingAmount = () => {
+ const { result: unbondingEntriesOpt, ...other } = useUnbonding();
+
+ const unbondingTotalAmount = useMemo(() => {
+ if (unbondingEntriesOpt === null) {
+ return null;
+ }
+
+ return unbondingEntriesOpt.map((entries) =>
+ entries
+ // Only consider the entries that have remaining eras.
+ .filter((entry) => entry.remainingEras.gten(1))
+ // Sum their amounts.
+ .reduce((acc, entry) => acc.add(entry.amount), new BN(0))
+ );
+ }, [unbondingEntriesOpt]);
+
+ return { result: unbondingTotalAmount, ...other };
+};
+
+export default useUnbondingAmount;
diff --git a/apps/tangle-dapp/data/NominatorStats/useUnbondingAmountSubscription.ts b/apps/tangle-dapp/data/NominatorStats/useUnbondingAmountSubscription.ts
deleted file mode 100644
index 52c659eb72..0000000000
--- a/apps/tangle-dapp/data/NominatorStats/useUnbondingAmountSubscription.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-'use client';
-
-import { BN, BN_ZERO } from '@polkadot/util';
-import { WebbError, WebbErrorCodes } from '@webb-tools/dapp-types/WebbError';
-import { useEffect, useState } from 'react';
-import { type Subscription } from 'rxjs';
-
-import useNetworkStore from '../../context/useNetworkStore';
-import useFormatReturnType from '../../hooks/useFormatReturnType';
-import { getPolkadotApiRx } from '../../utils/polkadot';
-
-export default function useUnbondingAmountSubscription(
- address: string,
- defaultValue: { value1: BN | null } = {
- value1: null,
- }
-) {
- const [value1, setValue1] = useState(defaultValue.value1);
- const [isLoading, setIsLoading] = useState(true);
- const [error, setError] = useState(null);
- const { rpcEndpoint, nativeTokenSymbol } = useNetworkStore();
-
- useEffect(() => {
- let isMounted = true;
- let sub: Subscription | null = null;
-
- const subscribeData = async () => {
- try {
- const api = await getPolkadotApiRx(rpcEndpoint);
-
- if (!address) {
- setValue1(null);
- setIsLoading(false);
- return;
- }
-
- sub = api.query.staking
- .ledger(address)
- .subscribe(async (ledgerData) => {
- if (isMounted) {
- const ledger = ledgerData.unwrapOrDefault();
-
- const totalUnbondingAmout = ledger.unlocking.reduce(
- (acc, curr) =>
- acc.add(new BN(curr.value.toBigInt().toString())),
- BN_ZERO
- );
-
- setValue1(totalUnbondingAmout);
- setIsLoading(false);
- }
- });
- } catch (error) {
- if (isMounted) {
- setError(
- error instanceof Error
- ? error
- : WebbError.from(WebbErrorCodes.UnknownError)
- );
- setIsLoading(false);
- }
- }
- };
-
- subscribeData();
-
- return () => {
- isMounted = false;
- sub?.unsubscribe();
- };
- }, [address, rpcEndpoint, nativeTokenSymbol]);
-
- return useFormatReturnType({ isLoading, error, data: { value1 } });
-}
diff --git a/apps/tangle-dapp/data/NominatorStats/useUnbondingRemainingErasSubscription.ts b/apps/tangle-dapp/data/NominatorStats/useUnbondingRemainingErasSubscription.ts
deleted file mode 100644
index 6d8688d6cf..0000000000
--- a/apps/tangle-dapp/data/NominatorStats/useUnbondingRemainingErasSubscription.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-'use client';
-
-import { BN } from '@polkadot/util';
-import { WebbError, WebbErrorCodes } from '@webb-tools/dapp-types/WebbError';
-import { useEffect, useState } from 'react';
-import { firstValueFrom, type Subscription } from 'rxjs';
-
-import useNetworkStore from '../../context/useNetworkStore';
-import useFormatReturnType from '../../hooks/useFormatReturnType';
-import { getPolkadotApiRx } from '../../utils/polkadot';
-
-type UnbondingRemainingEras = {
- amount: BN;
- remainingEras: number;
-};
-
-export default function useUnbondingRemainingErasSubscription(
- address: string,
- defaultValue: { value1: UnbondingRemainingEras[] | null } = {
- value1: null,
- }
-) {
- const [value1, setValue1] = useState(defaultValue.value1);
- const [isLoading, setIsLoading] = useState(true);
- const [error, setError] = useState(null);
- const { rpcEndpoint, nativeTokenSymbol } = useNetworkStore();
-
- useEffect(() => {
- let isMounted = true;
- let sub: Subscription | null = null;
-
- const subscribeData = async () => {
- try {
- const api = await getPolkadotApiRx(rpcEndpoint);
-
- if (!address) {
- setValue1(null);
- setIsLoading(false);
- return;
- }
-
- const currentEraPromise = firstValueFrom(
- api.query.staking.currentEra()
- );
-
- sub = api.query.staking
- .ledger(address)
- .subscribe(async (ledgerData) => {
- if (isMounted) {
- const ledger = ledgerData.unwrapOrDefault();
-
- const currentEraOption = await currentEraPromise;
- const currentEra = currentEraOption.unwrapOrDefault().toNumber();
-
- const unbondingRemainingEras = ledger.unlocking.map(
- async (unlockChunk) => {
- const unbondedAmount = new BN(
- unlockChunk.value.toBigInt().toString()
- );
- const unlockingEra = unlockChunk.era.toNumber();
- const remainingEras = unlockingEra - currentEra;
- return {
- amount: unbondedAmount,
- remainingEras,
- };
- }
- );
-
- const unbondingRemainingErasFormatted = await Promise.all(
- unbondingRemainingEras
- );
-
- setValue1(unbondingRemainingErasFormatted ?? null);
- setIsLoading(false);
- }
- });
- } catch (error) {
- if (isMounted) {
- setError(
- error instanceof Error
- ? error
- : WebbError.from(WebbErrorCodes.UnknownError)
- );
- setIsLoading(false);
- }
- }
- };
-
- subscribeData();
-
- return () => {
- isMounted = false;
- sub?.unsubscribe();
- };
- }, [address, rpcEndpoint, nativeTokenSymbol]);
-
- return useFormatReturnType({ isLoading, error, data: { value1 } });
-}
diff --git a/apps/tangle-dapp/data/ServiceDetails/getServiceDetailsInfo.ts b/apps/tangle-dapp/data/ServiceDetails/getServiceDetailsInfo.ts
deleted file mode 100644
index 73d4ef3cd3..0000000000
--- a/apps/tangle-dapp/data/ServiceDetails/getServiceDetailsInfo.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import {
- randEthereumAddress,
- randRecentDate,
- randSoonDate,
-} from '@ngneat/falso';
-
-import { RestakingService } from '../../types';
-
-type ServiceDetailsInfo = {
- serviceType: RestakingService;
- thresholds?: number;
- key?: string;
- startTimestamp: Date;
- endTimestamp: Date;
-};
-
-export default async function getServiceDetailsInfo(
- _: string
-): Promise {
- return {
- serviceType: RestakingService.ZK_SAAS_GROTH16,
- thresholds: 3,
- key: randEthereumAddress(),
- startTimestamp: randRecentDate({ days: 10 }),
- endTimestamp: randSoonDate({ days: 10 }),
- };
-}
diff --git a/apps/tangle-dapp/data/ServiceDetails/getSigningRules.ts b/apps/tangle-dapp/data/ServiceDetails/getSigningRules.ts
deleted file mode 100644
index 4d4e64ccc9..0000000000
--- a/apps/tangle-dapp/data/ServiceDetails/getSigningRules.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-const MOCK_FILE = `pragma circom 2.0.0;
-
-include "../../node_modules/circomlib/circuits/poseidon.circom";
-include "../../node_modules/circomlib/circuits/bitify.circom";
-include "./merkleTreeUpdater.circom";
-include "./treeUpdateArgsHasher.circom";
-include "./merkleTree.circom";
-
-// Computes hashes of the next tree layer
-template TreeLayer(height) {
- var nItems = 1 << height;
- signal input ins[nItems * 2];
- signal output outs[nItems];
-
- component hash[nItems];
- for(var i = 0; i < nItems; i++) {
- hash[i] = HashLeftRight();
- hash[i].left <== ins[i * 2];
- hash[i].right <== ins[i * 2 + 1];
- hash[i].hash ==> outs[i];
- }
-}
-
-// Inserts a leaf batch into a tree
-// Checks that tree previously contained zero leaves in the same position
-// Hashes leaves with Poseidon hash
-// \`batchLevels\` should be less than \`levels\`
-template BatchTreeUpdate(levels, batchLevels, zeroBatchLeaf) {
- var height = levels - batchLevels;
- var nLeaves = 1 << batchLevels;
- signal input argsHash;
- signal input oldRoot;
- signal input newRoot;
- signal input pathIndices;
- signal input pathElements[height];
- signal input leaves[nLeaves];
- /* signal input blocks[nLeaves]; */
- // Check that hash of arguments is correct
- // We compress arguments into a single hash to considerably reduce gas usage on chain
- component argsHasher = TreeUpdateArgsHasher(nLeaves);
- argsHasher.oldRoot <== oldRoot;
- argsHasher.newRoot <== newRoot;
- argsHasher.pathIndices <== pathIndices;
- for(var i = 0; i < nLeaves; i++) {
- argsHasher.leaves[i] <== leaves[i];
- }
- argsHash === argsHasher.out;
- // Compute batch subtree merkle root
- component layers[batchLevels];
- for(var level = batchLevels - 1; level >= 0; level--) {
- layers[level] = TreeLayer(level);
- for(var i = 0; i < (1 << (level + 1)); i++) {
- layers[level].ins[i] <== level == batchLevels - 1 ? leaves[i] : layers[level + 1].outs[i];
- }
- }
- // Verify that batch subtree was inserted correctly
- component treeUpdater = MerkleTreeUpdater(height, zeroBatchLeaf);
- treeUpdater.oldRoot <== oldRoot;
- treeUpdater.newRoot <== newRoot;
- treeUpdater.leaf <== layers[0].outs[0];
- treeUpdater.pathIndices <== pathIndices;
- for(var i = 0; i < height; i++) {
- treeUpdater.pathElements[i] <== pathElements[i];
- }
-}
-
-// zeroLeaf = keccak256("tornado") % FIELD_SIZE
-// zeroBatchLeaf is poseidon(zeroLeaf, zeroLeaf) (batchLevels - 1) times
-function nthZero(n) {
- assert(n <= 15);
- if (n == 0) return 21663839004416932945382355908790599225266501822907911457504978515578255421292;
- if (n == 1) return 8995896153219992062710898675021891003404871425075198597897889079729967997688;
- if (n == 2) return 15126246733515326086631621937388047923581111613947275249184377560170833782629;
- if (n == 3) return 6404200169958188928270149728908101781856690902670925316782889389790091378414;
- if (n == 4) return 17903822129909817717122288064678017104411031693253675943446999432073303897479;
- if (n == 5) return 11423673436710698439362231088473903829893023095386581732682931796661338615804;
- if (n == 6) return 10494842461667482273766668782207799332467432901404302674544629280016211342367;
- if (n == 7) return 17400501067905286947724900644309270241576392716005448085614420258732805558809;
- if (n == 8) return 7924095784194248701091699324325620647610183513781643345297447650838438175245;
- if (n == 9) return 3170907381568164996048434627595073437765146540390351066869729445199396390350;
- if (n == 10) return 21224698076141654110749227566074000819685780865045032659353546489395159395031;
- if (n == 11) return 18113275293366123216771546175954550524914431153457717566389477633419482708807;
- if (n == 12) return 1952712013602708178570747052202251655221844679392349715649271315658568301659;
- if (n == 13) return 18071586466641072671725723167170872238457150900980957071031663421538421560166;
- if (n == 14) return 9993139859464142980356243228522899168680191731482953959604385644693217291503;
- if (n == 15) return 14825089209834329031146290681677780462512538924857394026404638992248153156554;
-}`;
-
-export default async function getSigningRules(_: string) {
- return MOCK_FILE;
-}
diff --git a/apps/tangle-dapp/data/ServiceDetails/useServiceParticipants.ts b/apps/tangle-dapp/data/ServiceDetails/useServiceParticipants.ts
deleted file mode 100644
index 800004c137..0000000000
--- a/apps/tangle-dapp/data/ServiceDetails/useServiceParticipants.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-'use client';
-
-import { useEffect, useState } from 'react';
-
-import useFormatReturnType from '../../hooks/useFormatReturnType';
-import type { ServiceParticipant } from '../../types';
-
-const participantsArr = new Array(5).fill({
- address: '5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy',
- identity: 'participant_1',
- twitter: 'https://twitter.com/tangle_network',
- discord: 'https://discord.com/invite/krp36ZSR8J',
- email: 'someone@example.com',
- web: 'https://tangle.tools/',
-} satisfies ServiceParticipant);
-
-export default function useServiceParticipants(serviceId: string) {
- console.log('serviceId :', serviceId);
- const [participants, setParticipants] = useState(
- null
- );
- const [isLoading, setIsLoading] = useState(true);
- const [error, setError] = useState(null);
-
- // TODO: integrate backend
- useEffect(() => {
- setIsLoading(false);
- setError(null);
- setParticipants(participantsArr);
- }, []);
-
- return useFormatReturnType({
- isLoading,
- error,
- data: participants,
- });
-}
diff --git a/apps/tangle-dapp/data/ServiceTables/getActiveServices.ts b/apps/tangle-dapp/data/ServiceTables/getActiveServices.ts
deleted file mode 100644
index 809d4b08e6..0000000000
--- a/apps/tangle-dapp/data/ServiceTables/getActiveServices.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { BN_TEN } from '@polkadot/util';
-
-import { RestakingService, type Service } from '../../types';
-
-export default async function getActiveServices(): Promise {
- return [
- {
- id: '123',
- serviceType: RestakingService.ZK_SAAS_GROTH16,
- participants: 2,
- threshold: 3,
- earnings: BN_TEN,
- expirationBlock: '456',
- },
- {
- id: '124',
- serviceType: RestakingService.TSS_SILENT_SHARD_DKLS23SECP256K1,
- participants: 1,
- earnings: BN_TEN,
- expirationBlock: '456',
- },
- {
- id: '125',
- serviceType: RestakingService.LIGHT_CLIENT_RELAYING,
- participants: 2,
- expirationBlock: '456',
- },
- {
- id: '126',
- serviceType: RestakingService.ZK_SAAS_MARLIN,
- participants: 12,
- threshold: 3,
- earnings: BN_TEN,
- expirationBlock: '456',
- },
- {
- id: '127',
- serviceType: RestakingService.LIGHT_CLIENT_RELAYING,
- participants: 9,
- expirationBlock: '456',
- },
- ];
-}
diff --git a/apps/tangle-dapp/data/ServiceTables/index.ts b/apps/tangle-dapp/data/ServiceTables/index.ts
deleted file mode 100644
index 9410dd5448..0000000000
--- a/apps/tangle-dapp/data/ServiceTables/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default as getActiveServices } from './getActiveServices';
-export { default as useActiveServicesByValidator } from './useActiveServicesByValidator';
diff --git a/apps/tangle-dapp/data/ServiceTables/useActiveServicesByValidator.ts b/apps/tangle-dapp/data/ServiceTables/useActiveServicesByValidator.ts
deleted file mode 100644
index 5c543a5da1..0000000000
--- a/apps/tangle-dapp/data/ServiceTables/useActiveServicesByValidator.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-'use client';
-
-import { BN } from '@polkadot/util';
-import { useWebbUI } from '@webb-tools/webb-ui-components/hooks/useWebbUI';
-import { useEffect, useState } from 'react';
-
-import {
- TANGLE_TO_SERVICE_TYPE_TSS_MAP,
- TANGLE_TO_SERVICE_TYPE_ZK_SAAS_MAP,
-} from '../../constants';
-import useNetworkStore from '../../context/useNetworkStore';
-import { Service } from '../../types';
-import ensureError from '../../utils/ensureError';
-import { getPolkadotApiPromise } from '../../utils/polkadot/api';
-import useJobIdAndTypeLookupByValidator from './useJobIdAndTypeLookupByValidator';
-
-export default function useActiveServicesByValidator(validatorAddress: string) {
- const { rpcEndpoint } = useNetworkStore();
- const { notificationApi } = useWebbUI();
- const {
- data: validatorIdAndTypeLookup,
- isLoading: isLoadingValidatorIdAndTypeLookup,
- error: validatorIdAndTypeLookupError,
- } = useJobIdAndTypeLookupByValidator(validatorAddress);
-
- const [services, setServices] = useState([]);
- const [isLoadingServices, setIsLoadingServices] = useState(true);
- const [errorLoadingServices, setErrorLoadingServices] =
- useState(null);
-
- useEffect(() => {
- const fetchData = async () => {
- if (validatorIdAndTypeLookup === null) {
- setServices([]);
- return;
- }
-
- try {
- const api = await getPolkadotApiPromise(rpcEndpoint);
- const fetchedServices = (
- await Promise.all(
- validatorIdAndTypeLookup.map(async (service) => {
- const jobInfoData = await api.query.jobs.submittedJobs(
- service.type,
- service.id
- );
- if (!jobInfoData.isSome) {
- throw new Error('Job info not found');
- } else {
- const jobInfo = jobInfoData.unwrap();
- const jobType = jobInfo.jobType;
- if (jobType.isNone) {
- throw new Error('Error fetching data for specific job');
- }
- const id = service.id.toString();
- const expirationBlock = jobInfo.expiry.toString();
- const fee = jobInfo.fee.toBn();
-
- if (jobType.isDkgtssPhaseOne) {
- const jobDetails = jobType.asDkgtssPhaseOne;
- const participantsNum = jobDetails.participants.length;
- return {
- id,
- serviceType:
- TANGLE_TO_SERVICE_TYPE_TSS_MAP[jobDetails.roleType.type],
- participants: participantsNum,
- threshold: jobDetails.threshold.toNumber(),
- expirationBlock,
- earnings: fee.div(new BN(participantsNum)),
- };
- } else if (jobType.isZkSaaSPhaseOne) {
- const jobDetails = jobType.asZkSaaSPhaseOne;
- const participantsNum = jobDetails.participants.length;
- return {
- id,
- serviceType:
- TANGLE_TO_SERVICE_TYPE_ZK_SAAS_MAP[
- jobDetails.roleType.type
- ],
- participants: participantsNum,
- expirationBlock,
- earnings: fee.div(new BN(participantsNum)),
- };
- } else {
- return null;
- }
- }
- })
- )
- ).filter((service) => service !== null) as Service[];
- setServices(fetchedServices);
- } catch (error) {
- setErrorLoadingServices(ensureError(error));
- } finally {
- setIsLoadingServices(false);
- }
- };
-
- fetchData();
- }, [notificationApi, validatorIdAndTypeLookup, rpcEndpoint]);
-
- useEffect(() => {
- if (validatorIdAndTypeLookupError || errorLoadingServices) {
- notificationApi({
- message: 'Failed to load services',
- variant: 'error',
- });
- }
- }, [notificationApi, validatorIdAndTypeLookupError, errorLoadingServices]);
-
- return {
- services,
- isLoading: isLoadingValidatorIdAndTypeLookup || isLoadingServices,
- };
-}
diff --git a/apps/tangle-dapp/data/ValidatorTables/useActiveValidators.ts b/apps/tangle-dapp/data/ValidatorTables/useActiveValidators.ts
index fe86d319ec..4181a9c5b5 100644
--- a/apps/tangle-dapp/data/ValidatorTables/useActiveValidators.ts
+++ b/apps/tangle-dapp/data/ValidatorTables/useActiveValidators.ts
@@ -1,14 +1,20 @@
+import { DEFAULT_FLAGS_ELECTED } from '@webb-tools/dapp-config';
import { useCallback } from 'react';
+import { map } from 'rxjs';
-import usePolkadotApiRx from '../../hooks/usePolkadotApiRx';
+import useApiRx from '../../hooks/useApiRx';
import { useValidators } from './useValidators';
const useActiveValidators = () => {
- const { data: activeValidatorAddresses } = usePolkadotApiRx(
- useCallback((api) => api.query.session.validators(), [])
+ const { result: activeValidatorAddresses } = useApiRx(
+ useCallback((api) => {
+ const electedInfo = api.derive.staking.electedInfo(DEFAULT_FLAGS_ELECTED);
+
+ return electedInfo.pipe(map((derive) => derive.nextElected));
+ }, [])
);
- return useValidators(activeValidatorAddresses, 'Active');
+ return useValidators(activeValidatorAddresses, true);
};
export default useActiveValidators;
diff --git a/apps/tangle-dapp/data/ValidatorTables/useAllValidators.ts b/apps/tangle-dapp/data/ValidatorTables/useAllValidators.ts
index 6765508eaa..b2d4844842 100644
--- a/apps/tangle-dapp/data/ValidatorTables/useAllValidators.ts
+++ b/apps/tangle-dapp/data/ValidatorTables/useAllValidators.ts
@@ -4,11 +4,10 @@ import useActiveValidators from './useActiveValidators';
import useWaitingValidators from './useWaitingValidators';
const useAllValidators = () => {
- console.debug('useAllValidators');
-
const activeValidators = useActiveValidators();
const waitingValidators = useWaitingValidators();
+ // TODO: Consider making this a map instead of an array.
const allValidators = useMemo(
() => [...(activeValidators ?? []), ...(waitingValidators ?? [])],
[activeValidators, waitingValidators]
diff --git a/apps/tangle-dapp/data/ValidatorTables/useValidatorIdentityNames.ts b/apps/tangle-dapp/data/ValidatorTables/useValidatorIdentityNames.ts
index c571f0c7ad..b32eff405c 100644
--- a/apps/tangle-dapp/data/ValidatorTables/useValidatorIdentityNames.ts
+++ b/apps/tangle-dapp/data/ValidatorTables/useValidatorIdentityNames.ts
@@ -5,8 +5,8 @@ import { ITuple } from '@polkadot/types/types';
import { useCallback } from 'react';
import { map } from 'rxjs';
+import useApiRx from '../../hooks/useApiRx';
import useEntryMap from '../../hooks/useEntryMap';
-import usePolkadotApiRx from '../../hooks/usePolkadotApiRx';
import {
extractDataFromIdentityInfo,
IdentityDataType,
@@ -30,7 +30,7 @@ const mapIdentitiesToNames = (
});
const useValidatorIdentityNames = () => {
- const { data: identityNames, ...other } = usePolkadotApiRx(
+ const { result: identityNames, ...other } = useApiRx(
useCallback(
(api) =>
api.query.identity.identityOf.entries().pipe(map(mapIdentitiesToNames)),
@@ -40,7 +40,7 @@ const useValidatorIdentityNames = () => {
const nameMap = useEntryMap(identityNames, (key) => key);
- return { data: nameMap, ...other };
+ return { result: nameMap, ...other };
};
export default useValidatorIdentityNames;
diff --git a/apps/tangle-dapp/data/ValidatorTables/useValidators.ts b/apps/tangle-dapp/data/ValidatorTables/useValidators.ts
old mode 100644
new mode 100755
index e692781110..b939b03365
--- a/apps/tangle-dapp/data/ValidatorTables/useValidators.ts
+++ b/apps/tangle-dapp/data/ValidatorTables/useValidators.ts
@@ -1,125 +1,80 @@
import { AccountId32 } from '@polkadot/types/interfaces';
-import {
- PalletStakingValidatorPrefs,
- SpStakingExposure,
-} from '@polkadot/types/lookup';
-import { BN_ZERO } from '@polkadot/util';
import { useCallback, useMemo } from 'react';
-import useNetworkStore from '../../context/useNetworkStore';
-import usePolkadotApiRx from '../../hooks/usePolkadotApiRx';
+import useApiRx from '../../hooks/useApiRx';
import { Validator } from '../../types';
-import { formatTokenBalance } from '../../utils/polkadot';
-import useCurrentEra from '../staking/useCurrentEra';
-import useValidatorsPrefs from '../staking/useValidatorsPrefs';
+import createValidator from '../../utils/staking/createValidator';
+import useAllRestakingLedgers from '../restaking/useAllRestakingLedgers';
+import useRestakingJobIdMap from '../restaking/useRestakingJobIdMap';
+import useStakingExposures2 from '../staking/useStakingExposures2';
+import useValidatorPrefs from '../staking/useValidatorPrefs';
import useValidatorIdentityNames from './useValidatorIdentityNames';
export const useValidators = (
addresses: AccountId32[] | null,
- status: 'Active' | 'Waiting'
+ isActive: boolean
): Validator[] | null => {
- const { nativeTokenSymbol } = useNetworkStore();
- const { data: currentEra } = useCurrentEra();
- const { data: identityNames } = useValidatorIdentityNames();
- const { data: validatorPrefs } = useValidatorsPrefs();
+ const { result: identityNames } = useValidatorIdentityNames();
+ const { result: validatorPrefs } = useValidatorPrefs();
+ const { result: exposures } = useStakingExposures2(isActive);
+ const { result: restakingLedgers } = useAllRestakingLedgers();
+ const { result: jobIdLookups } = useRestakingJobIdMap();
- const { data: nominations } = usePolkadotApiRx(
+ const { result: nominations } = useApiRx(
useCallback((api) => api.query.staking.nominators.entries(), [])
);
- const { data: exposures } = usePolkadotApiRx(
- useCallback(
- (api) =>
- currentEra === null
- ? null
- : api.query.staking.erasStakers.entries(currentEra),
- [currentEra]
- )
+ const { result: activeJobs } = useApiRx(
+ useCallback((api) => api.query.jobs.submittedJobs.entries(), [])
);
- const mappedExposures = useMemo(() => {
- const map = new Map();
- exposures?.forEach(([storageKey, exposure]) => {
- const accountId = storageKey.args[1].toString();
- map.set(accountId, exposure);
- });
- return map;
- }, [exposures]);
-
- // Mapping Validator Preferences
- const mappedValidatorPrefs = useMemo(() => {
- const map = new Map();
- validatorPrefs?.forEach(([storageKey, prefs]) => {
- const accountId = storageKey.args[0].toString();
- map.set(accountId, prefs);
- });
- return map;
- }, [validatorPrefs]);
-
return useMemo(() => {
if (
addresses === null ||
identityNames === null ||
exposures === null ||
+ restakingLedgers === null ||
nominations === null ||
- validatorPrefs === null
+ validatorPrefs === null ||
+ jobIdLookups === null ||
+ activeJobs === null
) {
return null;
}
- return addresses.map((address) => {
- const name = identityNames.get(address.toString()) ?? address.toString();
- const exposure = mappedExposures.get(address.toString());
- const totalStakeAmount = exposure?.total.unwrap() ?? BN_ZERO;
-
- const selfStakedAmount = exposure?.own.toBn() ?? BN_ZERO;
- const selfStakedBalance = formatTokenBalance(
- selfStakedAmount,
- nativeTokenSymbol
- );
-
- const nominators = nominations.filter(([, nominatorData]) => {
- if (nominatorData.isNone) {
- return false;
- }
+ return addresses.map((accountId) =>
+ createValidator({
+ address: accountId.toString(),
+ isActive,
+ identities: identityNames,
+ prefs: validatorPrefs,
+ restakingLedgers,
+ jobs: jobIdLookups,
+ activeJobIds: activeJobs,
+ getExposure: (address) => {
+ const exposure = exposures.get(address);
- const nominations = nominatorData.unwrap();
+ if (exposure === undefined || exposure.exposureMeta === null) {
+ return undefined;
+ }
- return (
- nominations.targets &&
- nominations.targets.some(
- (target) => target.toString() === address.toString()
- )
- );
- });
-
- const validatorPref = mappedValidatorPrefs.get(address.toString());
- const commissionRate = validatorPref?.commission.unwrap().toNumber() ?? 0;
- const commission = commissionRate / 10_000_000;
-
- return {
- address: address.toString(),
- identityName: name,
- selfStaked: selfStakedBalance,
- effectiveAmountStaked: formatTokenBalance(
- totalStakeAmount,
- nativeTokenSymbol
- ),
- effectiveAmountStakedRaw: totalStakeAmount.toString(),
- delegations: nominators.length.toString(),
- commission: commission.toString(),
- status,
- };
- });
+ return {
+ own: exposure.exposureMeta.own.toBn(),
+ total: exposure.exposureMeta.total.toBn(),
+ nominatorCount: exposure.exposureMeta.nominatorCount.toNumber(),
+ };
+ },
+ })
+ );
}, [
+ activeJobs,
addresses,
- identityNames,
exposures,
+ identityNames,
+ isActive,
+ jobIdLookups,
nominations,
+ restakingLedgers,
validatorPrefs,
- mappedExposures,
- mappedValidatorPrefs,
- nativeTokenSymbol,
- status,
]);
};
diff --git a/apps/tangle-dapp/data/ValidatorTables/useWaitingValidators.ts b/apps/tangle-dapp/data/ValidatorTables/useWaitingValidators.ts
index ef16f2e1cc..33a5febcd4 100644
--- a/apps/tangle-dapp/data/ValidatorTables/useWaitingValidators.ts
+++ b/apps/tangle-dapp/data/ValidatorTables/useWaitingValidators.ts
@@ -1,19 +1,22 @@
+import { DEFAULT_FLAGS_WAITING } from '@webb-tools/dapp-config';
import { useCallback } from 'react';
import { map } from 'rxjs';
-import usePolkadotApiRx from '../../hooks/usePolkadotApiRx';
+import useApiRx from '../../hooks/useApiRx';
import { useValidators } from './useValidators';
const useWaitingValidators = () => {
- const { data: waitingValidatorAddresses } = usePolkadotApiRx(
+ const { result: waitingValidatorAddresses } = useApiRx(
useCallback(
(api) =>
- api.derive.staking.waitingInfo().pipe(map((info) => info.waiting)),
+ api.derive.staking
+ .waitingInfo(DEFAULT_FLAGS_WAITING)
+ .pipe(map((derive) => derive.waiting)),
[]
)
);
- return useValidators(waitingValidatorAddresses, 'Waiting');
+ return useValidators(waitingValidatorAddresses, false);
};
export default useWaitingValidators;
diff --git a/apps/tangle-dapp/data/balances/useBalances.ts b/apps/tangle-dapp/data/balances/useBalances.ts
index 168623f54d..6d52a6451b 100644
--- a/apps/tangle-dapp/data/balances/useBalances.ts
+++ b/apps/tangle-dapp/data/balances/useBalances.ts
@@ -1,11 +1,8 @@
import { BN, BN_ZERO, bnMax } from '@polkadot/util';
-import { useWebContext } from '@webb-tools/api-provider-environment';
-import { useCallback, useEffect, useState } from 'react';
+import { useCallback } from 'react';
import { map } from 'rxjs/operators';
-import usePolkadotApiRx, {
- ObservableFactory,
-} from '../../hooks/usePolkadotApiRx';
+import useApiRx, { ObservableFactory } from '../../hooks/useApiRx';
import useSubstrateAddress from '../../hooks/useSubstrateAddress';
export type AccountBalances = {
@@ -32,14 +29,15 @@ export type AccountBalances = {
locked: BN | null;
};
-const useBalances = (): AccountBalances => {
- const { activeAccount } = useWebContext();
+const useBalances = () => {
const activeSubstrateAddress = useSubstrateAddress();
- const [balances, setBalances] = useState(null);
const balancesFetcher = useCallback>(
(api) => {
- if (!activeSubstrateAddress) return null;
+ if (activeSubstrateAddress === null) {
+ return null;
+ }
+
return api.query.system.account(activeSubstrateAddress).pipe(
map(({ data }) => {
// Note that without the null/undefined check, an error
@@ -69,26 +67,13 @@ const useBalances = (): AccountBalances => {
[activeSubstrateAddress]
);
- const { data, isLoading } = usePolkadotApiRx(balancesFetcher);
-
- useEffect(() => {
- // If there's data and it's not loading, set the balances.
- if (data && !isLoading) {
- setBalances(data);
- }
- }, [data, isLoading]);
-
- // Reset balances if there is no active account.
- useEffect(() => {
- if (activeAccount === null) {
- setBalances(null);
- }
- }, [activeAccount]);
+ const { result: balances, ...other } = useApiRx(balancesFetcher);
return {
free: balances?.free ?? null,
transferable: balances?.transferable ?? null,
locked: balances?.locked ?? null,
+ ...other,
};
};
diff --git a/apps/tangle-dapp/data/balances/useBalancesLock.ts b/apps/tangle-dapp/data/balances/useBalancesLock.ts
index 01d3f74c7a..b5bdbcaeca 100644
--- a/apps/tangle-dapp/data/balances/useBalancesLock.ts
+++ b/apps/tangle-dapp/data/balances/useBalancesLock.ts
@@ -1,18 +1,27 @@
-import { BN_ZERO } from '@polkadot/util';
+import { PalletBalancesReasons } from '@polkadot/types/lookup';
+import { BN, BN_ZERO } from '@polkadot/util';
import { useCallback, useMemo } from 'react';
import { SubstrateLockId } from '../../constants';
-import usePolkadotApiRx from '../../hooks/usePolkadotApiRx';
+import useApiRx from '../../hooks/useApiRx';
import useSubstrateAddress from '../../hooks/useSubstrateAddress';
import getSubstrateLockId from '../../utils/getSubstrateLockId';
-const useBalancesLock = (lockId: SubstrateLockId) => {
+export type BalancesLock = {
+ amount: BN | null;
+ reasons: PalletBalancesReasons | null;
+};
+
+const useBalancesLock = (lockId: SubstrateLockId): BalancesLock => {
const activeSubstrateAddress = useSubstrateAddress();
- const { data: locks } = usePolkadotApiRx(
+ const { result: locks } = useApiRx(
useCallback(
(api) => {
- if (!activeSubstrateAddress) return null;
+ if (activeSubstrateAddress === null) {
+ return null;
+ }
+
return api.query.balances.locks(activeSubstrateAddress);
},
[activeSubstrateAddress]
diff --git a/apps/tangle-dapp/data/balances/useEvmBalanceWithdrawTx.ts b/apps/tangle-dapp/data/balances/useEvmBalanceWithdrawTx.ts
new file mode 100644
index 0000000000..58a73b3b9d
--- /dev/null
+++ b/apps/tangle-dapp/data/balances/useEvmBalanceWithdrawTx.ts
@@ -0,0 +1,41 @@
+import type { HexString } from '@polkadot/util/types';
+import { ZERO_BIG_INT } from '@webb-tools/dapp-config';
+import { useCallback } from 'react';
+
+import { TxName } from '../../constants';
+import { useSubstrateTxWithNotification } from '../../hooks/useSubstrateTx';
+import { GetSuccessMessageFunctionType } from '../../types';
+
+type EvmBalanceWithdrawContext = {
+ pendingEvmBalance: bigint | null;
+ evmAddress20: HexString | null;
+};
+
+const useEvmBalanceWithdrawTx = (tokenAmountStr?: string | null) => {
+ const getSuccessMessageFnc: GetSuccessMessageFunctionType =
+ useCallback(
+ () =>
+ typeof tokenAmountStr === 'string'
+ ? `Successfully withdrew ${tokenAmountStr}.`
+ : '',
+ [tokenAmountStr]
+ );
+
+ return useSubstrateTxWithNotification(
+ TxName.WITHDRAW_EVM_BALANCE,
+ useCallback((api, _, { pendingEvmBalance, evmAddress20 }) => {
+ if (
+ evmAddress20 === null ||
+ pendingEvmBalance === null ||
+ pendingEvmBalance === ZERO_BIG_INT
+ ) {
+ return null;
+ }
+
+ return api.tx.evm.withdraw(evmAddress20, pendingEvmBalance);
+ }, []),
+ getSuccessMessageFnc
+ );
+};
+
+export default useEvmBalanceWithdrawTx;
diff --git a/apps/tangle-dapp/data/balances/useExistentialDeposit.ts b/apps/tangle-dapp/data/balances/useExistentialDeposit.ts
index 92c32c7bf6..b1f904828a 100644
--- a/apps/tangle-dapp/data/balances/useExistentialDeposit.ts
+++ b/apps/tangle-dapp/data/balances/useExistentialDeposit.ts
@@ -3,23 +3,23 @@
import { BN_ONE } from '@polkadot/util';
import { useCallback, useEffect, useState } from 'react';
-import usePolkadotApi from '../../hooks/usePolkadotApi';
+import useApi from '../../hooks/useApi';
function useExistentialDeposit() {
// Default existential deposit is 1 unit.
const [existentialDeposit, setExistentialDeposit] = useState(BN_ONE);
- const { isApiLoading, isValueLoading, error, value } = usePolkadotApi(
+ const { result } = useApi(
useCallback(async (api) => api.consts.balances.existentialDeposit, [])
);
useEffect(() => {
- if (isApiLoading || isValueLoading || error !== null || value === null) {
+ if (result === null) {
return;
}
- setExistentialDeposit((prev) => (value.eq(prev) ? prev : value));
- }, [error, isApiLoading, isValueLoading, value]);
+ setExistentialDeposit((prev) => (result.eq(prev) ? prev : result));
+ }, [result]);
return existentialDeposit;
}
diff --git a/apps/tangle-dapp/data/balances/usePendingEVMBalance.ts b/apps/tangle-dapp/data/balances/usePendingEVMBalance.ts
deleted file mode 100644
index c96a515eb8..0000000000
--- a/apps/tangle-dapp/data/balances/usePendingEVMBalance.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { ZERO_BIG_INT } from '@webb-tools/dapp-config/constants';
-import isSubstrateAddress from '@webb-tools/dapp-types/utils/isSubstrateAddress';
-import { useCallback, useMemo } from 'react';
-
-import useActiveAccountAddress from '../../hooks/useActiveAccountAddress';
-import usePromise from '../../hooks/usePromise';
-import useSubstrateTx from '../../hooks/useSubstrateTx';
-import useViemPublicClient from '../../hooks/useViemPublicClient';
-import { substrateToEvmAddress } from '../../utils/substrateToEvmAddress';
-
-export default function usePendingEVMBalance() {
- const activeAccountAddress = useActiveAccountAddress();
-
- // Only check the evm balance if the active account address is Substrate address.
- const address = useMemo(() => {
- if (!isValidAddress(activeAccountAddress)) {
- return null;
- }
-
- return substrateToEvmAddress(activeAccountAddress);
- }, [activeAccountAddress]);
-
- const evmClient = useViemPublicClient();
-
- const { result: balance } = usePromise(
- useCallback(async () => {
- if (!evmClient || address === null) {
- return null;
- }
-
- return evmClient.getBalance({
- address,
- });
- }, [address, evmClient]),
- null
- );
-
- const withdrawTx = useSubstrateTx(
- useCallback(
- (api) => {
- if (address === null || balance === null || balance === ZERO_BIG_INT) {
- return null;
- }
-
- return api.tx.evm.withdraw(address, balance);
- },
- [address, balance]
- )
- );
-
- return {
- balance,
- ...withdrawTx,
- };
-}
-
-/** @internal */
-const isValidAddress = (address: string | null): address is string =>
- address !== null && isSubstrateAddress(address);
diff --git a/apps/tangle-dapp/data/balances/usePendingEvmBalance.ts b/apps/tangle-dapp/data/balances/usePendingEvmBalance.ts
new file mode 100644
index 0000000000..e4352b25ed
--- /dev/null
+++ b/apps/tangle-dapp/data/balances/usePendingEvmBalance.ts
@@ -0,0 +1,40 @@
+import { useCallback, useMemo } from 'react';
+
+import useAgnosticAccountInfo from '../../hooks/useAgnosticAccountInfo';
+import usePromise from '../../hooks/usePromise';
+import useViemPublicClient from '../../hooks/useViemPublicClient';
+import { toEvmAddress20 } from '../../utils';
+
+/**
+ * See more here:
+ * https://docs.tangle.tools/docs/use/addresses/#case-2-sending-from-evm-to-substrate
+ */
+const usePendingEvmBalance = () => {
+ const viemPublicClient = useViemPublicClient();
+ const { substrateAddress, isEvm } = useAgnosticAccountInfo();
+
+ // Only check the EVM balance if the active account address
+ // is a Substrate address.
+ const evmAddress20 = useMemo(() => {
+ if (substrateAddress === null || isEvm) {
+ return null;
+ }
+
+ return toEvmAddress20(substrateAddress);
+ }, [isEvm, substrateAddress]);
+
+ const { result: balance } = usePromise(
+ useCallback(async () => {
+ if (viemPublicClient === null || evmAddress20 === null) {
+ return null;
+ }
+
+ return viemPublicClient.getBalance({ address: evmAddress20 });
+ }, [evmAddress20, viemPublicClient]),
+ null
+ );
+
+ return balance;
+};
+
+export default usePendingEvmBalance;
diff --git a/apps/tangle-dapp/data/balances/useTransferTx.ts b/apps/tangle-dapp/data/balances/useTransferTx.ts
index 675e69f524..2ad754dae0 100644
--- a/apps/tangle-dapp/data/balances/useTransferTx.ts
+++ b/apps/tangle-dapp/data/balances/useTransferTx.ts
@@ -1,12 +1,17 @@
import { BN } from '@polkadot/util';
+import { isAddress } from '@polkadot/util-crypto';
+import { shortenString } from '@webb-tools/webb-ui-components/utils/shortenString';
import { useCallback } from 'react';
+import { TxName } from '../../constants';
import { Precompile } from '../../constants/evmPrecompiles';
-import { EvmAbiCallData, EvmTxFactory } from '../../hooks/types';
import useAgnosticTx from '../../hooks/useAgnosticTx';
+import { AbiCall, EvmTxFactory } from '../../hooks/useEvmPrecompileAbiCall';
import useEvmPrecompileFeeFetcher from '../../hooks/useEvmPrecompileFee';
+import useFormatNativeTokenAmount from '../../hooks/useFormatNativeTokenAmount';
import { SubstrateTxFactory } from '../../hooks/useSubstrateTx';
-import { evmToSubstrateAddress, substrateToEvmAddress } from '../../utils';
+import { GetSuccessMessageFunctionType } from '../../types';
+import { toEvmAddress20, toSubstrateAddress } from '../../utils';
type TransferTxContext = {
receiverAddress: string;
@@ -16,6 +21,7 @@ type TransferTxContext = {
const useTransferTx = () => {
const { fetchEvmPrecompileFees } = useEvmPrecompileFeeFetcher();
+ const formatNativeTokenAmount = useFormatNativeTokenAmount();
const evmTxFactory: EvmTxFactory<
Precompile.BALANCES_ERC20,
@@ -23,12 +29,15 @@ const useTransferTx = () => {
> = useCallback(
async ({ receiverAddress, amount, maxAmount }) => {
const isMaxAmount = amount.eq(maxAmount);
- const receiverEvm = substrateToEvmAddress(receiverAddress);
- const sharedAbiCallData = {
+ const recipientEvmAddress20 = isAddress(receiverAddress)
+ ? toEvmAddress20(receiverAddress)
+ : receiverAddress;
+
+ const sharedAbiCallData: AbiCall = {
functionName: 'transfer',
- arguments: [receiverEvm, amount],
- } satisfies EvmAbiCallData;
+ arguments: [recipientEvmAddress20, amount],
+ };
// If the amount to transfer is not the maximum amount
// just return the abi call data for the transfer function.
@@ -58,7 +67,7 @@ const useTransferTx = () => {
return {
...sharedAbiCallData,
- arguments: [receiverEvm, amountToTransfer],
+ arguments: [recipientEvmAddress20, amountToTransfer],
};
},
[fetchEvmPrecompileFees]
@@ -69,12 +78,14 @@ const useTransferTx = () => {
api,
_activeSubstrateAddress,
{ receiverAddress, amount, maxAmount }
- ) =>
- amount.eq(maxAmount)
+ ) => {
+ // Convert the EVM address to a Substrate address, in case
+ // that it was provided as an EVM address.
+ const recipientSubstrateAddress = toSubstrateAddress(receiverAddress);
+
+ return amount.eq(maxAmount)
? api.tx.balances.transferAll(
- // Convert the EVM address to a Substrate address, in case
- // that it was provided as an EVM address.
- evmToSubstrateAddress(receiverAddress),
+ recipientSubstrateAddress,
// No need to keep the current account alive
false
)
@@ -83,19 +94,26 @@ const useTransferTx = () => {
// account to drop below the existential deposit, which
// would essentially cause the account to be 'reaped', or
// deleted from the chain.
- api.tx.balances.transferAllowDeath(
- // Convert the EVM address to a Substrate address, in case
- // that it was provided as an EVM address.
- evmToSubstrateAddress(receiverAddress),
- amount
- ),
+ api.tx.balances.transferAllowDeath(recipientSubstrateAddress, amount);
+ },
[]
);
+ const getSuccessMessageFnc: GetSuccessMessageFunctionType =
+ useCallback(
+ ({ receiverAddress, amount }) =>
+ `Successfully transferred ${formatNativeTokenAmount(
+ amount
+ )} to ${shortenString(receiverAddress)}.`,
+ [formatNativeTokenAmount]
+ );
+
return useAgnosticTx({
+ name: TxName.TRANSFER,
precompile: Precompile.BALANCES_ERC20,
evmTxFactory,
substrateTxFactory,
+ getSuccessMessageFnc,
});
};
diff --git a/apps/tangle-dapp/data/claims/useAirdropEligibility.ts b/apps/tangle-dapp/data/claims/useAirdropEligibility.ts
index 26a0a6050f..f630dce08a 100644
--- a/apps/tangle-dapp/data/claims/useAirdropEligibility.ts
+++ b/apps/tangle-dapp/data/claims/useAirdropEligibility.ts
@@ -4,17 +4,17 @@ import { isEthereumAddress } from '@polkadot/util-crypto';
import { useCallback, useEffect, useState } from 'react';
import useActiveAccountAddress from '../../hooks/useActiveAccountAddress';
-import usePolkadotApi from '../../hooks/usePolkadotApi';
+import useApiRx from '../../hooks/useApiRx';
const useAirdropEligibility = () => {
const [isEligible, setIsEligible] = useState(null);
const activeAccountAddress = useActiveAccountAddress();
- const { value: claimInfo } = usePolkadotApi(
+ const { result: claimInfo } = useApiRx(
useCallback(
(api) => {
if (activeAccountAddress === null) {
- return Promise.resolve(null);
+ return null;
}
const params = isEthereumAddress(activeAccountAddress)
diff --git a/apps/tangle-dapp/data/democracy/useDemocracy.ts b/apps/tangle-dapp/data/democracy/useDemocracy.ts
index ec12314844..ac7b5f6210 100644
--- a/apps/tangle-dapp/data/democracy/useDemocracy.ts
+++ b/apps/tangle-dapp/data/democracy/useDemocracy.ts
@@ -2,17 +2,20 @@ import { useCallback, useMemo } from 'react';
import { map } from 'rxjs';
import { SubstrateLockId } from '../../constants';
-import usePolkadotApiRx from '../../hooks/usePolkadotApiRx';
+import useApiRx from '../../hooks/useApiRx';
import useSubstrateAddress from '../../hooks/useSubstrateAddress';
import useBalancesLock from '../balances/useBalancesLock';
const useDemocracy = () => {
const activeSubstrateAddress = useSubstrateAddress();
- const { data: votes } = usePolkadotApiRx(
+ const { result: votes } = useApiRx(
useCallback(
(api) => {
- if (!activeSubstrateAddress) return null;
+ if (activeSubstrateAddress === null) {
+ return null;
+ }
+
return api.query.democracy.votingOf(activeSubstrateAddress);
},
[activeSubstrateAddress]
@@ -37,7 +40,7 @@ const useDemocracy = () => {
return latestDirectVote[0];
})();
- const { data: latestReferendum } = usePolkadotApiRx(
+ const { result: latestReferendum } = useApiRx(
useCallback(
(api) => {
if (latestReferendumIndex === null) {
diff --git a/apps/tangle-dapp/data/democracy/useDemocracyUnlockTx.ts b/apps/tangle-dapp/data/democracy/useDemocracyUnlockTx.ts
index f999f29be9..9715e6b65b 100644
--- a/apps/tangle-dapp/data/democracy/useDemocracyUnlockTx.ts
+++ b/apps/tangle-dapp/data/democracy/useDemocracyUnlockTx.ts
@@ -7,11 +7,10 @@ import useSubstrateTx from '../../hooks/useSubstrateTx';
* @remarks
* This is a Substrate-only transaction (at least for now).
*/
-const useDemocracyUnlockTx = (notifyTxStatusUpdates?: boolean) => {
- return useSubstrateTx(
- (api, activeSubstrateAddress) =>
- api.tx.democracy.unlock(activeSubstrateAddress),
- notifyTxStatusUpdates
+const useDemocracyUnlockTx = () => {
+ // TODO: Make this agnostic (add support for EVM).
+ return useSubstrateTx((api, activeSubstrateAddress) =>
+ api.tx.democracy.unlock(activeSubstrateAddress)
);
};
diff --git a/apps/tangle-dapp/data/payouts/useLedgers.ts b/apps/tangle-dapp/data/payouts/useAllLedgers.ts
similarity index 62%
rename from apps/tangle-dapp/data/payouts/useLedgers.ts
rename to apps/tangle-dapp/data/payouts/useAllLedgers.ts
index 06239973aa..de9e994a02 100644
--- a/apps/tangle-dapp/data/payouts/useLedgers.ts
+++ b/apps/tangle-dapp/data/payouts/useAllLedgers.ts
@@ -1,10 +1,10 @@
import { useCallback } from 'react';
+import useApiRx from '../../hooks/useApiRx';
import useEntryMap from '../../hooks/useEntryMap';
-import usePolkadotApiRx from '../../hooks/usePolkadotApiRx';
-const useLedgers = () => {
- const { data: ledgers, ...other } = usePolkadotApiRx(
+const useAllLedgers = () => {
+ const { result: ledgers, ...other } = useApiRx(
useCallback((api) => api.query.staking.ledger.entries(), [])
);
@@ -13,4 +13,4 @@ const useLedgers = () => {
return { data: ledgerMap, ...other };
};
-export default useLedgers;
+export default useAllLedgers;
diff --git a/apps/tangle-dapp/data/payouts/useEraTotalRewards.ts b/apps/tangle-dapp/data/payouts/useEraTotalRewards.ts
index 248ba1bc04..ae3fecc270 100644
--- a/apps/tangle-dapp/data/payouts/useEraTotalRewards.ts
+++ b/apps/tangle-dapp/data/payouts/useEraTotalRewards.ts
@@ -1,10 +1,10 @@
import { useCallback } from 'react';
+import useApiRx from '../../hooks/useApiRx';
import useEntryMap from '../../hooks/useEntryMap';
-import usePolkadotApiRx from '../../hooks/usePolkadotApiRx';
const useEraTotalRewards = () => {
- const { data: erasValidatorRewards, ...other } = usePolkadotApiRx(
+ const { result: erasValidatorRewards, ...other } = useApiRx(
useCallback((api) => api.query.staking.erasValidatorReward.entries(), [])
);
diff --git a/apps/tangle-dapp/data/payouts/usePayoutAllTx.ts b/apps/tangle-dapp/data/payouts/usePayoutAllTx.ts
new file mode 100644
index 0000000000..25a92fd26f
--- /dev/null
+++ b/apps/tangle-dapp/data/payouts/usePayoutAllTx.ts
@@ -0,0 +1,63 @@
+import { useCallback } from 'react';
+
+import { TxName } from '../../constants';
+import { Precompile } from '../../constants/evmPrecompiles';
+import useAgnosticTx from '../../hooks/useAgnosticTx';
+import { EvmTxFactory } from '../../hooks/useEvmPrecompileAbiCall';
+import { SubstrateTxFactory } from '../../hooks/useSubstrateTx';
+import { toSubstrateAddress } from '../../utils';
+import optimizeTxBatch from '../../utils/optimizeTxBatch';
+import createEvmBatchCallArgs from '../../utils/staking/createEvmBatchCallArgs';
+import createEvmBatchCallData from '../../utils/staking/createEvmBatchCallData';
+import toEvmAddress32 from '../../utils/toEvmAddress32';
+
+export type PayoutAllTxContext = {
+ validatorEraPairs: { validatorSubstrateAddress: string; era: number }[];
+};
+
+const usePayoutAllTx = () => {
+ const evmTxFactory: EvmTxFactory =
+ useCallback((context) => {
+ const batchCalls = context.validatorEraPairs.map(
+ ({ validatorSubstrateAddress, era }) => {
+ // The precompile function expects a 32-byte address.
+ const validatorEvmAddress32 = toEvmAddress32(
+ validatorSubstrateAddress
+ );
+
+ return createEvmBatchCallData(Precompile.STAKING, 'payoutStakers', [
+ validatorEvmAddress32,
+ era,
+ ]);
+ }
+ );
+
+ return {
+ functionName: 'batchAll',
+ arguments: createEvmBatchCallArgs(batchCalls),
+ };
+ }, []);
+
+ const substrateTxFactory: SubstrateTxFactory =
+ useCallback((api, _activeSubstrateAddress, context) => {
+ const txs = context.validatorEraPairs.map(
+ ({ validatorSubstrateAddress: validatorAddress, era }) => {
+ const validatorSubstrateAddress =
+ toSubstrateAddress(validatorAddress);
+
+ return api.tx.staking.payoutStakers(validatorSubstrateAddress, era);
+ }
+ );
+
+ return optimizeTxBatch(api, txs);
+ }, []);
+
+ return useAgnosticTx({
+ name: TxName.PAYOUT_ALL,
+ precompile: Precompile.BATCH,
+ evmTxFactory,
+ substrateTxFactory,
+ });
+};
+
+export default usePayoutAllTx;
diff --git a/apps/tangle-dapp/data/payouts/usePayoutStakersTx.ts b/apps/tangle-dapp/data/payouts/usePayoutStakersTx.ts
new file mode 100644
index 0000000000..6c4ece05ef
--- /dev/null
+++ b/apps/tangle-dapp/data/payouts/usePayoutStakersTx.ts
@@ -0,0 +1,48 @@
+import { useCallback } from 'react';
+
+import { TxName } from '../../constants';
+import { Precompile } from '../../constants/evmPrecompiles';
+import useAgnosticTx from '../../hooks/useAgnosticTx';
+import { EvmTxFactory } from '../../hooks/useEvmPrecompileAbiCall';
+import { SubstrateTxFactory } from '../../hooks/useSubstrateTx';
+import { toSubstrateAddress } from '../../utils';
+import toEvmAddress32 from '../../utils/toEvmAddress32';
+
+export type PayoutStakersTxContext = {
+ validatorAddress: string;
+ era: number;
+};
+
+const usePayoutStakersTx = () => {
+ const evmTxFactory: EvmTxFactory =
+ useCallback((context) => {
+ // The payout stakers precompile function expects a 32-byte address.
+ const validatorEvmAddress32 = toEvmAddress32(context.validatorAddress);
+
+ return {
+ functionName: 'payoutStakers',
+ arguments: [validatorEvmAddress32, context.era],
+ };
+ }, []);
+
+ const substrateTxFactory: SubstrateTxFactory =
+ useCallback((api, _activeSubstrateAddress, context) => {
+ const validatorSubstrateAddress = toSubstrateAddress(
+ context.validatorAddress
+ );
+
+ return api.tx.staking.payoutStakers(
+ validatorSubstrateAddress,
+ context.era
+ );
+ }, []);
+
+ return useAgnosticTx({
+ name: TxName.PAYOUT_STAKERS,
+ precompile: Precompile.STAKING,
+ evmTxFactory,
+ substrateTxFactory,
+ });
+};
+
+export default usePayoutStakersTx;
diff --git a/apps/tangle-dapp/data/payouts/usePayouts2.ts b/apps/tangle-dapp/data/payouts/usePayouts2.ts
deleted file mode 100644
index e63ff66a98..0000000000
--- a/apps/tangle-dapp/data/payouts/usePayouts2.ts
+++ /dev/null
@@ -1,242 +0,0 @@
-'use client';
-
-import { SpStakingExposure } from '@polkadot/types/lookup';
-import { useCallback, useEffect, useState } from 'react';
-
-import useNetworkStore from '../../context/useNetworkStore';
-import usePolkadotApiRx from '../../hooks/usePolkadotApiRx';
-import useSubstrateAddress from '../../hooks/useSubstrateAddress';
-import { Payout } from '../../types';
-import {
- formatTokenBalance,
- getPolkadotApiPromise,
- getValidatorCommission,
- getValidatorIdentityName,
-} from '../../utils/polkadot';
-import useValidatorIdentityNames from '../ValidatorTables/useValidatorIdentityNames';
-import useEraTotalRewards from './useEraTotalRewards';
-import useLedgers from './useLedgers';
-
-type ValidatorReward = {
- validatorAddress: string;
- era: number;
- eraTotalRewardPoints: number;
- validatorRewardPoints: number;
-};
-
-const usePayouts2 = () => {
- const { rpcEndpoint, nativeTokenSymbol } = useNetworkStore();
- const activeSubstrateAddress = useSubstrateAddress();
- const { data: ledgers } = useLedgers();
-
- const { data: erasRewardsPoints } = usePolkadotApiRx(
- useCallback((api) => api.query.staking.erasRewardPoints.entries(), [])
- );
-
- const { data: nominators } = usePolkadotApiRx(
- useCallback(
- (api) => {
- if (!activeSubstrateAddress) return null;
- return api.query.staking.nominators(activeSubstrateAddress);
- },
- [activeSubstrateAddress]
- )
- );
-
- const { data: identities } = useValidatorIdentityNames();
- const { data: eraTotalRewards } = useEraTotalRewards();
- const [payouts, setPayouts] = useState(null);
- const nominations = nominators?.isSome ? nominators.unwrap().targets : null;
-
- useEffect(() => {
- if (
- nominations === null ||
- erasRewardsPoints === null ||
- ledgers === null ||
- identities === null ||
- activeSubstrateAddress === null ||
- eraTotalRewards === null
- ) {
- return;
- }
-
- const rewards: ValidatorReward[] = [];
-
- for (const validatorAddress of nominations) {
- for (const point of erasRewardsPoints) {
- const era = point[0].args[0].toNumber();
- const rewardsForEra = point[1];
-
- const validatorRewardPoints =
- rewardsForEra.individual.get(validatorAddress);
-
- // No entry for this validator in this era.
- if (validatorRewardPoints === undefined) {
- continue;
- }
-
- rewards.push({
- era,
- eraTotalRewardPoints: rewardsForEra.total.toNumber(),
- validatorAddress: validatorAddress.toString(),
- validatorRewardPoints: validatorRewardPoints.toNumber(),
- });
- }
- }
-
- const payoutsPromise = Promise.all(
- rewards.map(async (reward) => {
- const apiPromise = await getPolkadotApiPromise(rpcEndpoint);
- const ledgerOpt = ledgers.get(reward.validatorAddress);
-
- // There might not be a ledger for this validator.
- if (ledgerOpt === undefined || ledgerOpt.isNone) {
- return;
- }
-
- const ledger = ledgerOpt.unwrap();
- const claimedEras = ledger.claimedRewards.map((era) => era.toNumber());
-
- // Validator has already claimed rewards for this era, so it
- // is not relevant for the payouts.
- if (claimedEras.includes(reward.era)) {
- return;
- }
-
- const eraTotalRewardOpt = eraTotalRewards.get(reward.era);
-
- // No rewards for this era.
- if (eraTotalRewardOpt === undefined || eraTotalRewardOpt.isNone) {
- return;
- }
-
- const eraTotalReward = eraTotalRewardOpt.unwrap();
-
- // validator's total reward = (era's total reward * validator's points) / era's total reward points
- const validatorTotalReward = eraTotalReward
- .muln(reward.validatorRewardPoints)
- .divn(reward.eraTotalRewardPoints);
-
- // Validator had no rewards for this era.
- if (validatorTotalReward.isZero()) {
- return;
- }
-
- const eraStakers: SpStakingExposure =
- await apiPromise.query.staking.erasStakers(
- reward.era,
- reward.validatorAddress
- );
-
- const validatorTotalStake = eraStakers.total.unwrap();
-
- if (
- Number(validatorTotalStake.toString()) === 0 ||
- eraStakers.others.length === 0
- ) {
- return;
- }
-
- const nominatorStakeInfo = eraStakers.others.find(
- (nominator) => nominator.who.toString() === activeSubstrateAddress
- );
-
- if (!nominatorStakeInfo || nominatorStakeInfo.isEmpty) {
- return;
- }
-
- const nominatorTotalStake = nominatorStakeInfo.value.unwrap();
-
- if (nominatorTotalStake.isZero()) {
- return;
- }
-
- const nominatorStakePercentage =
- (Number(nominatorTotalStake.toString()) /
- Number(validatorTotalStake.toString())) *
- 100;
-
- const validatorCommissionPercentage = await getValidatorCommission(
- rpcEndpoint,
- reward.validatorAddress
- );
-
- const validatorCommission = validatorTotalReward.muln(
- Number(validatorCommissionPercentage) / 100
- );
-
- const distributableReward =
- validatorTotalReward.sub(validatorCommission);
-
- const nominatorTotalReward = distributableReward.muln(
- nominatorStakePercentage / 100
- );
-
- const nominatorTotalRewardFormatted = formatTokenBalance(
- nominatorTotalReward,
- nativeTokenSymbol
- );
-
- const validatorIdentityName =
- identities.get(reward.validatorAddress) ?? null;
-
- const validatorNominators = await Promise.all(
- eraStakers.others.map(async (nominator) => {
- const nominatorIdentity = await getValidatorIdentityName(
- rpcEndpoint,
- nominator.who.toString()
- );
-
- return {
- address: nominator.who.toString(),
- identity: nominatorIdentity ?? '',
- };
- })
- );
-
- const validatorTotalRewardFormatted = formatTokenBalance(
- validatorTotalReward,
- nativeTokenSymbol
- );
-
- const validatorTotalStakeFormatted = formatTokenBalance(
- validatorTotalStake,
- nativeTokenSymbol
- );
-
- if (
- validatorTotalStakeFormatted &&
- validatorTotalRewardFormatted &&
- nominatorTotalRewardFormatted
- ) {
- const payout: Payout = {
- era: reward.era,
- validator: {
- address: reward.validatorAddress,
- identity: validatorIdentityName ?? '',
- },
- validatorTotalStake: validatorTotalStakeFormatted,
- nominators: validatorNominators,
- validatorTotalReward: validatorTotalRewardFormatted,
- nominatorTotalReward: nominatorTotalRewardFormatted,
- status: 'unclaimed',
- };
-
- return payout;
- }
- })
- );
-
- payoutsPromise.then((payouts) =>
- setPayouts(
- payouts
- .filter((payout): payout is Payout => payout !== undefined)
- .sort((a, b) => a.era - b.era)
- )
- );
- });
-
- return payouts;
-};
-
-export default usePayouts2;
diff --git a/apps/tangle-dapp/data/payouts/usePayoutsAvailability.ts b/apps/tangle-dapp/data/payouts/usePayoutsAvailability.ts
index cb91135a7a..a5028d77b5 100644
--- a/apps/tangle-dapp/data/payouts/usePayoutsAvailability.ts
+++ b/apps/tangle-dapp/data/payouts/usePayoutsAvailability.ts
@@ -1,18 +1,40 @@
-import { useEffect, useState } from 'react';
+import { useEffect, useMemo, useState } from 'react';
+import useNetworkStore from '../../context/useNetworkStore';
+import useLocalStorage, { LocalStorageKey } from '../../hooks/useLocalStorage';
import useSubstrateAddress from '../../hooks/useSubstrateAddress';
-import usePayouts from '../NominationsPayouts/usePayouts';
const usePayoutsAvailability = () => {
- const activeSubstrateAddress = useSubstrateAddress();
- const { data: payouts } = usePayouts(activeSubstrateAddress ?? '');
+ const { valueOpt: cachedPayouts } = useLocalStorage(
+ LocalStorageKey.PAYOUTS,
+ true
+ );
+
+ const { rpcEndpoint } = useNetworkStore();
+
+ const address = useSubstrateAddress();
+
+ const payoutsData = useMemo(() => {
+ if (
+ cachedPayouts === null ||
+ cachedPayouts.value === null ||
+ address === null
+ ) {
+ return [];
+ }
+
+ const payouts = cachedPayouts.value[rpcEndpoint]?.[address];
+
+ return payouts ?? [];
+ }, [address, cachedPayouts, rpcEndpoint]);
+
const [isPayoutsAvailable, setIsPayoutsAvailable] = useState(false);
useEffect(() => {
- if (payouts !== null) {
- setIsPayoutsAvailable(!!payouts.payouts.length);
+ if (payoutsData !== null) {
+ setIsPayoutsAvailable(!!payoutsData.length);
}
- }, [payouts]);
+ }, [payoutsData]);
return isPayoutsAvailable;
};
diff --git a/apps/tangle-dapp/data/restaking/types.ts b/apps/tangle-dapp/data/restaking/types.ts
new file mode 100644
index 0000000000..7708039cb1
--- /dev/null
+++ b/apps/tangle-dapp/data/restaking/types.ts
@@ -0,0 +1,19 @@
+/**
+ * Type for the restaking earnings record,
+ * key is the era number and value is the restaking earnings for that era
+ */
+export type EarningRecord = Record;
+
+/**
+ * Type for the restaking rewards record entries,
+ * first element is the era number and the second element is the restaking rewards for that era
+ */
+export type ErasRestakeRewardPointsEntry = [
+ number,
+ {
+ total: number;
+ individual: {
+ [accountId: string]: number;
+ };
+ }
+];
diff --git a/apps/tangle-dapp/data/restaking/useAllRestakingLedgers.ts b/apps/tangle-dapp/data/restaking/useAllRestakingLedgers.ts
new file mode 100644
index 0000000000..299fb54952
--- /dev/null
+++ b/apps/tangle-dapp/data/restaking/useAllRestakingLedgers.ts
@@ -0,0 +1,19 @@
+import { useCallback } from 'react';
+
+import useApiRx from '../../hooks/useApiRx';
+import useEntryMap from '../../hooks/useEntryMap';
+
+const useAllRestakingLedgers = () => {
+ const { result: restakingLedgers, ...other } = useApiRx(
+ useCallback((api) => api.query.roles.ledger.entries(), [])
+ );
+
+ const ledgerMap = useEntryMap(
+ restakingLedgers,
+ useCallback((key) => key.args[0].toString(), [])
+ );
+
+ return { result: ledgerMap, ...other };
+};
+
+export default useAllRestakingLedgers;
diff --git a/apps/tangle-dapp/data/restaking/useRestakingAPY.ts b/apps/tangle-dapp/data/restaking/useRestakingAPY.ts
new file mode 100755
index 0000000000..7697f460b4
--- /dev/null
+++ b/apps/tangle-dapp/data/restaking/useRestakingAPY.ts
@@ -0,0 +1,62 @@
+import type { ApiRx } from '@polkadot/api';
+import { type BN, BN_ZERO } from '@polkadot/util';
+import fraction from '@webb-tools/webb-ui-components/utils/fraction';
+import { useCallback } from 'react';
+import { map, type Observable } from 'rxjs';
+
+import useApiRx from '../../hooks/useApiRx';
+import useRestakingEraReward from './useRestakingEraReward';
+
+const useRestakingAPY = () => {
+ const { result: activeRestakerEra } = useApiRx(
+ useCallback(
+ (apiRx) =>
+ apiRx.query.roles.activeRestakerEra
+ ? apiRx.query.roles.activeRestakerEra()
+ : null,
+ []
+ )
+ );
+
+ const { data: currentEraRewards } = useRestakingEraReward(
+ activeRestakerEra?.unwrapOr(null)?.index.toNumber()
+ );
+
+ const { result: totalRestaked } = useApiRx(getTotalRestaked);
+
+ if (
+ currentEraRewards === null ||
+ totalRestaked === null ||
+ totalRestaked.isZero()
+ ) {
+ return null;
+ }
+
+ const rewardRate = fraction(currentEraRewards, totalRestaked.toString());
+
+ return (1 + rewardRate / 365) ** 365 - 1;
+};
+
+export default useRestakingAPY;
+
+const getTotalRestaked = (api: ApiRx): Observable => {
+ if (api.query.roles.totalRestake) {
+ return api.query.roles
+ .totalRestake()
+ .pipe(map((totalRestaked) => totalRestaked.toBn()));
+ }
+
+ const ledgerEntries = api.query.roles.ledger.entries();
+
+ return ledgerEntries.pipe(
+ map((entries) => {
+ return entries.reduce((acc, [, value]) => {
+ if (value.isNone) {
+ return acc;
+ }
+
+ return acc.add(value.unwrap().total.toBn());
+ }, BN_ZERO);
+ })
+ );
+};
diff --git a/apps/tangle-dapp/data/restaking/useRestakingEarnings.ts b/apps/tangle-dapp/data/restaking/useRestakingEarnings.ts
index 372fa9d793..2a226723ff 100644
--- a/apps/tangle-dapp/data/restaking/useRestakingEarnings.ts
+++ b/apps/tangle-dapp/data/restaking/useRestakingEarnings.ts
@@ -1,46 +1,89 @@
-import { BN } from '@polkadot/util';
+import { createWorkerFactory, useWorker } from '@shopify/react-web-worker';
import { useCallback } from 'react';
-import { map, of } from 'rxjs';
-
-import usePolkadotApiRx from '../../hooks/usePolkadotApiRx';
-
-/**
- * Type for the restaking earnings record,
- * key is the era number and value is the restaking earnings for that era
- */
-export type EarningRecord = Record;
-
-/**
- * Hook to get the restaking earnings for a given account
- * @param substrateAccount the account to get the restaking earnings for
- * @returns a record of era number to the restaking earnings for that era
- */
-const useRestakingEarnings = (substrateAccount: string | null) =>
- usePolkadotApiRx(
+import { map } from 'rxjs';
+
+import useNetworkStore from '../../context/useNetworkStore';
+import useApiRx from '../../hooks/useApiRx';
+import usePromise from '../../hooks/usePromise';
+import type { ErasRestakeRewardPointsEntry } from './types';
+
+const createWorker = createWorkerFactory(
+ () => import('./workers/calculateEarnings')
+);
+
+function useRestakingEarnings(accountAddress: string | null) {
+ const { rpcEndpoint } = useNetworkStore();
+
+ const worker = useWorker(createWorker);
+
+ const {
+ result: dataPromise,
+ isLoading: rxLoading,
+ error: rxError,
+ } = useApiRx(
useCallback(
(apiRx) => {
- if (!substrateAccount) return of(null);
+ if (!apiRx.query.roles.erasRestakeRewardPoints) {
+ return null;
+ }
- if (!('erasRestakeRewardPoints' in apiRx.query.roles))
- return of({});
+ if (accountAddress === null || accountAddress.length === 0) {
+ return null;
+ }
return apiRx.query.roles.erasRestakeRewardPoints.entries().pipe(
- map((entries) => {
- return entries.reduce((prev, [era, eraRewardPoints]) => {
- eraRewardPoints.individual.forEach((reward, accountId32) => {
- if (accountId32.toString() === substrateAccount) {
- // era is in type u32, which can be converted to number
- prev[era.args[0].toNumber()] = reward;
- }
- });
-
- return prev;
- }, {} as EarningRecord);
+ map((rewardPointsEntries) => {
+ const serializableEntries = rewardPointsEntries
+ .map(
+ ([era, rewardPoints]) =>
+ [
+ era.args[0].toNumber(),
+ {
+ total: rewardPoints.total.toNumber(),
+ individual: Object.fromEntries(
+ Array.from(rewardPoints.individual.entries()).map(
+ ([accountId, rewardPoints]) => [
+ accountId.toString(),
+ rewardPoints.toNumber(),
+ ]
+ )
+ ),
+ },
+ ] satisfies ErasRestakeRewardPointsEntry
+ )
+ // Copy and sort the entries by era number
+ .slice()
+ .sort(([a], [b]) => a - b);
+
+ return worker.calculateEarnings(
+ rpcEndpoint,
+ accountAddress,
+ serializableEntries
+ );
})
);
},
- [substrateAccount]
+ [accountAddress, rpcEndpoint, worker]
)
);
+ const {
+ result: data,
+ isLoading: promiseLoading,
+ error: promiseError,
+ } = usePromise(
+ useCallback(
+ () => (dataPromise === null ? Promise.resolve(null) : dataPromise),
+ [dataPromise]
+ ),
+ null
+ );
+
+ return {
+ data,
+ isLoading: rxLoading || promiseLoading,
+ error: rxError || promiseError,
+ };
+}
+
export default useRestakingEarnings;
diff --git a/apps/tangle-dapp/data/restaking/useRestakingEraReward.ts b/apps/tangle-dapp/data/restaking/useRestakingEraReward.ts
new file mode 100755
index 0000000000..7d5facbde6
--- /dev/null
+++ b/apps/tangle-dapp/data/restaking/useRestakingEraReward.ts
@@ -0,0 +1,74 @@
+import { createWorkerFactory, useWorker } from '@shopify/react-web-worker';
+import { useCallback } from 'react';
+import { map, of } from 'rxjs';
+
+import useNetworkStore from '../../context/useNetworkStore';
+import useApiRx from '../../hooks/useApiRx';
+import usePromise from '../../hooks/usePromise';
+
+const createWorker = createWorkerFactory(
+ () => import('./workers/calculateEraRewardPoint')
+);
+
+/**
+ * Calculate the total restaking reward for the given era
+ * @param era the era number
+ */
+function useRestakingEraReward(era: number | null = null) {
+ const { rpcEndpoint } = useNetworkStore();
+
+ const worker = useWorker(createWorker);
+
+ const {
+ result: dataPromise,
+ isLoading: rxLoading,
+ error: rxError,
+ } = useApiRx(
+ useCallback(
+ (apiRx) => {
+ if (
+ !apiRx.query.roles.erasRestakeRewardPoints ||
+ typeof era !== 'number'
+ ) {
+ return of(null);
+ }
+
+ return apiRx.query.roles.erasRestakeRewardPoints(era).pipe(
+ map((rewardPoints) => {
+ const individuals = rewardPoints.individual.entries();
+
+ return worker.calculateEraRewardPoints(
+ rpcEndpoint,
+ era,
+ Array.from(individuals).map(([accountId, rewardPoints]) => [
+ accountId.toString(),
+ rewardPoints.toNumber(),
+ ])
+ );
+ })
+ );
+ },
+ [era, rpcEndpoint, worker]
+ )
+ );
+
+ const {
+ result: data,
+ isLoading: promiseLoading,
+ error: promiseError,
+ } = usePromise(
+ useCallback(
+ () => (dataPromise === null ? Promise.resolve(null) : dataPromise),
+ [dataPromise]
+ ),
+ null
+ );
+
+ return {
+ data,
+ isLoading: rxLoading || promiseLoading,
+ error: rxError || promiseError,
+ };
+}
+
+export default useRestakingEraReward;
diff --git a/apps/tangle-dapp/data/restaking/useRestakingJobIdMap.ts b/apps/tangle-dapp/data/restaking/useRestakingJobIdMap.ts
new file mode 100644
index 0000000000..984be2f95b
--- /dev/null
+++ b/apps/tangle-dapp/data/restaking/useRestakingJobIdMap.ts
@@ -0,0 +1,19 @@
+import { useCallback } from 'react';
+
+import useApiRx from '../../hooks/useApiRx';
+import useEntryMap from '../../hooks/useEntryMap';
+
+const useRestakingJobIdMap = () => {
+ const { result: restakingLedgers, ...other } = useApiRx(
+ useCallback((api) => api.query.jobs.validatorJobIdLookup.entries(), [])
+ );
+
+ const ledgerMap = useEntryMap(
+ restakingLedgers,
+ useCallback((key) => key.args[0].toString(), [])
+ );
+
+ return { result: ledgerMap, ...other };
+};
+
+export default useRestakingJobIdMap;
diff --git a/apps/tangle-dapp/data/restaking/useRestakingJobs.ts b/apps/tangle-dapp/data/restaking/useRestakingJobs.ts
index 00a1bc6970..9de3b4a46c 100644
--- a/apps/tangle-dapp/data/restaking/useRestakingJobs.ts
+++ b/apps/tangle-dapp/data/restaking/useRestakingJobs.ts
@@ -1,13 +1,13 @@
import { useCallback, useMemo } from 'react';
-import usePolkadotApiRx from '../../hooks/usePolkadotApiRx';
+import useApiRx from '../../hooks/useApiRx';
import useSubstrateAddress from '../../hooks/useSubstrateAddress';
-import substrateRoleToServiceType from '../../utils/substrateRoleToServiceType';
+import substrateRoleToServiceType from '../../utils/restaking/substrateRoleToServiceType';
const useRestakingJobs = () => {
const activeSubstrateAddress = useSubstrateAddress();
- const { data: jobRoleIdPairsOpt } = usePolkadotApiRx(
+ const { result: jobRoleIdPairsOpt } = useApiRx(
useCallback(
(api) => {
if (activeSubstrateAddress === null) {
diff --git a/apps/tangle-dapp/data/restaking/useRestakingLimits.ts b/apps/tangle-dapp/data/restaking/useRestakingLimits.ts
index bc1d080bfb..f52357b82e 100644
--- a/apps/tangle-dapp/data/restaking/useRestakingLimits.ts
+++ b/apps/tangle-dapp/data/restaking/useRestakingLimits.ts
@@ -1,15 +1,15 @@
import { useCallback, useMemo } from 'react';
import { map } from 'rxjs';
-import usePolkadotApiRx from '../../hooks/usePolkadotApiRx';
-import useStakingLedgerRx from '../../hooks/useStakingLedgerRx';
+import useApiRx from '../../hooks/useApiRx';
+import useStakingLedger from '../staking/useStakingLedger';
const useRestakingLimits = () => {
- const { data: stakedBalance } = useStakingLedgerRx(
- useCallback((ledger) => ledger.total.toBn(), [])
+ const { result: stakedBalance } = useStakingLedger(
+ useCallback((ledger) => ledger?.total.toBn(), [])
);
- const { data: minRestakingBond } = usePolkadotApiRx(
+ const { result: minRestakingBond } = useApiRx(
useCallback(
(api) =>
api.query.roles
@@ -25,7 +25,7 @@ const useRestakingLimits = () => {
// from the Polkadot API.
// See: https://github.com/webb-tools/tangle/blob/8be20aa02a764422e1fd0ba30bc70b99d5f66887/runtime/mainnet/src/lib.rs#L1137
const maxRestakingAmount = useMemo(
- () => stakedBalance?.divn(2) ?? null,
+ () => stakedBalance?.value?.divn(2) ?? null,
[stakedBalance]
);
diff --git a/apps/tangle-dapp/data/restaking/useRestakingRoleLedger.ts b/apps/tangle-dapp/data/restaking/useRestakingRoleLedger.ts
index 00d0381497..3db586b2a8 100644
--- a/apps/tangle-dapp/data/restaking/useRestakingRoleLedger.ts
+++ b/apps/tangle-dapp/data/restaking/useRestakingRoleLedger.ts
@@ -1,9 +1,9 @@
import { useCallback } from 'react';
-import usePolkadotApiRx from '../../hooks/usePolkadotApiRx';
+import useApiRx from '../../hooks/useApiRx';
const useRestakingRoleLedger = (address: string | null) => {
- return usePolkadotApiRx(
+ return useApiRx(
useCallback(
(api) => {
if (address === null) {
diff --git a/apps/tangle-dapp/data/restaking/useRestakingTotalRewards.ts b/apps/tangle-dapp/data/restaking/useRestakingTotalRewards.ts
new file mode 100755
index 0000000000..fb28ba85d9
--- /dev/null
+++ b/apps/tangle-dapp/data/restaking/useRestakingTotalRewards.ts
@@ -0,0 +1,33 @@
+import BN from 'bn.js';
+import { useCallback } from 'react';
+import { map, of } from 'rxjs';
+
+import useApiRx from '../../hooks/useApiRx';
+import useSubstrateAddress from '../../hooks/useSubstrateAddress';
+
+const useRestakingTotalRewards = (): ReturnType> => {
+ const substrateAccount = useSubstrateAddress();
+
+ return useApiRx(
+ useCallback(
+ (apiRx) => {
+ if (!substrateAccount) return of(null);
+
+ if (!apiRx.query.jobs.validatorRewards) return of(null);
+
+ return apiRx.query.jobs.validatorRewards(substrateAccount).pipe(
+ map((reward) => {
+ if (reward.isNone) {
+ return null;
+ }
+
+ return reward.unwrap().toBn();
+ })
+ );
+ },
+ [substrateAccount]
+ )
+ );
+};
+
+export default useRestakingTotalRewards;
diff --git a/apps/tangle-dapp/data/restaking/useSharedRestakeAmount.ts b/apps/tangle-dapp/data/restaking/useSharedRestakeAmount.ts
index bd0970e046..fdb68c781d 100644
--- a/apps/tangle-dapp/data/restaking/useSharedRestakeAmount.ts
+++ b/apps/tangle-dapp/data/restaking/useSharedRestakeAmount.ts
@@ -8,7 +8,7 @@ import useRestakingRoleLedger from './useRestakingRoleLedger';
const useSharedRestakeAmount = () => {
const activeSubstrateAccount = useSubstrateAddress();
const ledgerResult = useRestakingRoleLedger(activeSubstrateAccount);
- const ledgerOpt = ledgerResult.data;
+ const ledgerOpt = ledgerResult.result;
const isLedgerAvailable = ledgerOpt !== null && ledgerOpt.isSome;
const sharedRestakeAmount = useMemo