From c8955204be3fff4424391791e4c1ee833280a95f Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Sun, 26 Jan 2025 17:28:29 -0500 Subject: [PATCH 01/28] refactor(tangle-dapp): Remove outdated parachain LS logic --- apps/tangle-dapp/src/abi/lst.ts | 357 ++++------- .../LiquidStaking/LsValidatorTable.tsx | 241 -------- .../ExchangeRateDetailItem.tsx | 12 +- .../stakeAndUnstake/FeeDetailItem.tsx | 76 --- .../stakeAndUnstake/LsStakeCard.tsx | 71 +-- .../stakeAndUnstake/LsUnstakeCard.tsx | 72 +-- .../UnstakePeriodDetailItem.tsx | 1 - .../stakeAndUnstake/useLsAgnosticBalance.ts | 20 +- .../stakeAndUnstake/useLsFeePercentage.ts | 34 - .../stakeAndUnstake/useLsSpendingLimits.ts | 83 --- .../src/constants/liquidStaking/constants.ts | 62 +- .../src/constants/liquidStaking/types.ts | 64 +- .../src/containers/VaultsAndBalancesTable.tsx | 31 +- .../src/data/liquidStaking/adapter.ts | 39 -- .../src/data/liquidStaking/adapters/astar.tsx | 202 ------ .../src/data/liquidStaking/adapters/manta.tsx | 230 ------- .../data/liquidStaking/adapters/moonbeam.tsx | 230 ------- .../src/data/liquidStaking/adapters/phala.tsx | 245 -------- .../data/liquidStaking/adapters/polkadot.tsx | 232 ------- .../liquidStaking/adapters/tangleLocal.tsx | 2 - .../liquidStaking/adapters/tangleMainnet.tsx | 2 - .../liquidStaking/adapters/tangleTestnet.tsx | 2 - .../src/data/liquidStaking/fetchHelpers.ts | 342 ---------- .../liquidStaking/getValueOfTangleCurrency.ts | 49 -- .../data/liquidStaking/parachain/useMintTx.ts | 39 -- .../parachain/useParachainBalances.ts | 76 --- .../parachain/useParachainLsFees.ts | 27 - .../liquidStaking/parachain/useRedeemTx.ts | 36 -- .../data/liquidStaking/useLsExchangeRate.ts | 82 +-- .../liquidStaking/useLsProtocolEntities.ts | 85 --- .../useLsValidatorSelectionTableColumns.tsx | 585 ------------------ .../src/data/liquidStaking/useLsValidators.ts | 298 --------- .../liquidStaking/useLstUnlockRequests.ts | 131 ---- .../data/liquidStaking/useOngoingTimeUnits.ts | 59 -- .../liquidStaking/useTokenUnlockDurations.ts | 56 -- .../liquidStaking/useTokenUnlockLedger.ts | 55 -- .../src/data/restake/useRestakeAsset.ts | 2 +- apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts | 3 + .../src/pages/liquid-staking/index.tsx | 10 +- apps/tangle-dapp/src/utils/CrossChainTime.ts | 60 -- .../tangle-dapp/src/utils/calculateBnRatio.ts | 28 +- .../liquidStaking/findLsNetworkByChainId.ts | 21 + .../src/utils/liquidStaking/getLsNetwork.ts | 3 - .../utils/liquidStaking/getLsProtocolDef.ts | 10 +- .../utils/liquidStaking/getLsTangleNetwork.ts | 7 - .../liquidStaking/getValueOfTangleTimeUnit.ts | 26 - .../liquidStaking/isLsParachainChainId.ts | 12 - .../utils/liquidStaking/isLsParachainToken.ts | 12 - .../utils/liquidStaking/stringifyTimeUnit.ts | 13 - .../tangleTimeUnitToSimpleInstance.ts | 15 - 50 files changed, 221 insertions(+), 4229 deletions(-) delete mode 100644 apps/tangle-dapp/src/components/LiquidStaking/LsValidatorTable.tsx delete mode 100644 apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/FeeDetailItem.tsx delete mode 100644 apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/useLsFeePercentage.ts delete mode 100644 apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/useLsSpendingLimits.ts delete mode 100644 apps/tangle-dapp/src/data/liquidStaking/adapter.ts delete mode 100644 apps/tangle-dapp/src/data/liquidStaking/adapters/astar.tsx delete mode 100644 apps/tangle-dapp/src/data/liquidStaking/adapters/manta.tsx delete mode 100644 apps/tangle-dapp/src/data/liquidStaking/adapters/moonbeam.tsx delete mode 100644 apps/tangle-dapp/src/data/liquidStaking/adapters/phala.tsx delete mode 100644 apps/tangle-dapp/src/data/liquidStaking/adapters/polkadot.tsx delete mode 100644 apps/tangle-dapp/src/data/liquidStaking/fetchHelpers.ts delete mode 100644 apps/tangle-dapp/src/data/liquidStaking/getValueOfTangleCurrency.ts delete mode 100644 apps/tangle-dapp/src/data/liquidStaking/parachain/useMintTx.ts delete mode 100644 apps/tangle-dapp/src/data/liquidStaking/parachain/useParachainBalances.ts delete mode 100644 apps/tangle-dapp/src/data/liquidStaking/parachain/useParachainLsFees.ts delete mode 100644 apps/tangle-dapp/src/data/liquidStaking/parachain/useRedeemTx.ts delete mode 100644 apps/tangle-dapp/src/data/liquidStaking/useLsProtocolEntities.ts delete mode 100644 apps/tangle-dapp/src/data/liquidStaking/useLsValidatorSelectionTableColumns.tsx delete mode 100644 apps/tangle-dapp/src/data/liquidStaking/useLsValidators.ts delete mode 100644 apps/tangle-dapp/src/data/liquidStaking/useLstUnlockRequests.ts delete mode 100644 apps/tangle-dapp/src/data/liquidStaking/useOngoingTimeUnits.ts delete mode 100644 apps/tangle-dapp/src/data/liquidStaking/useTokenUnlockDurations.ts delete mode 100644 apps/tangle-dapp/src/data/liquidStaking/useTokenUnlockLedger.ts create mode 100644 apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts delete mode 100644 apps/tangle-dapp/src/utils/CrossChainTime.ts create mode 100644 apps/tangle-dapp/src/utils/liquidStaking/findLsNetworkByChainId.ts delete mode 100644 apps/tangle-dapp/src/utils/liquidStaking/getValueOfTangleTimeUnit.ts delete mode 100644 apps/tangle-dapp/src/utils/liquidStaking/isLsParachainChainId.ts delete mode 100644 apps/tangle-dapp/src/utils/liquidStaking/isLsParachainToken.ts delete mode 100644 apps/tangle-dapp/src/utils/liquidStaking/stringifyTimeUnit.ts delete mode 100644 apps/tangle-dapp/src/utils/liquidStaking/tangleTimeUnitToSimpleInstance.ts diff --git a/apps/tangle-dapp/src/abi/lst.ts b/apps/tangle-dapp/src/abi/lst.ts index bb419c42d7..64d6c51f7a 100644 --- a/apps/tangle-dapp/src/abi/lst.ts +++ b/apps/tangle-dapp/src/abi/lst.ts @@ -3,320 +3,195 @@ import { AbiFunction } from 'viem'; const LST_PRECOMPILE_ABI = [ { inputs: [ - { - internalType: 'uint256', - name: 'amount', - type: 'uint256', - }, - { - internalType: 'uint256', - name: 'poolId', - type: 'uint256', - }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + { internalType: 'uint256', name: 'poolId', type: 'uint256' }, ], name: 'join', - outputs: [ - { - internalType: 'uint8', - name: '', - type: 'uint8', - }, - ], + outputs: [], stateMutability: 'nonpayable', type: 'function', }, { inputs: [ - { - internalType: 'uint256', - name: 'poolId', - type: 'uint256', - }, - { - internalType: 'uint8', - name: 'extraType', - type: 'uint8', - }, - { - internalType: 'uint256', - name: 'extra', - type: 'uint256', - }, + { internalType: 'uint256', name: 'poolId', type: 'uint256' }, + { internalType: 'uint8', name: 'extraType', type: 'uint8' }, + { internalType: 'uint256', name: 'extra', type: 'uint256' }, ], name: 'bondExtra', - outputs: [ - { - internalType: 'uint8', - name: '', - type: 'uint8', - }, - ], + outputs: [], stateMutability: 'nonpayable', type: 'function', }, { inputs: [ - { - internalType: 'bytes32', - name: 'memberAccount', - type: 'bytes32', - }, - { - internalType: 'uint256', - name: 'poolId', - type: 'uint256', - }, - { - internalType: 'uint256', - name: 'unbondingPoints', - type: 'uint256', - }, + { internalType: 'bytes32', name: 'memberAccount', type: 'bytes32' }, + { internalType: 'uint256', name: 'poolId', type: 'uint256' }, + { internalType: 'uint256', name: 'unbondingPoints', type: 'uint256' }, ], name: 'unbond', - outputs: [ - { - internalType: 'uint8', - name: '', - type: 'uint8', - }, - ], + outputs: [], stateMutability: 'nonpayable', type: 'function', }, { inputs: [ - { - internalType: 'uint256', - name: 'poolId', - type: 'uint256', - }, - { - internalType: 'uint32', - name: 'numSlashingSpans', - type: 'uint32', - }, + { internalType: 'uint256', name: 'poolId', type: 'uint256' }, + { internalType: 'uint32', name: 'numSlashingSpans', type: 'uint32' }, ], name: 'poolWithdrawUnbonded', - outputs: [ - { - internalType: 'uint8', - name: '', - type: 'uint8', - }, - ], + outputs: [], stateMutability: 'nonpayable', type: 'function', }, { inputs: [ - { - internalType: 'bytes32', - name: 'memberAccount', - type: 'bytes32', - }, - { - internalType: 'uint256', - name: 'poolId', - type: 'uint256', - }, - { - internalType: 'uint32', - name: 'numSlashingSpans', - type: 'uint32', - }, + { internalType: 'bytes32', name: 'memberAccount', type: 'bytes32' }, + { internalType: 'uint256', name: 'poolId', type: 'uint256' }, + { internalType: 'uint32', name: 'numSlashingSpans', type: 'uint32' }, ], name: 'withdrawUnbonded', - outputs: [ - { - internalType: 'uint8', - name: '', - type: 'uint8', - }, - ], + outputs: [], stateMutability: 'nonpayable', type: 'function', }, { inputs: [ - { - internalType: 'uint256', - name: 'amount', - type: 'uint256', - }, - { - internalType: 'bytes32', - name: 'root', - type: 'bytes32', - }, - { - internalType: 'bytes32', - name: 'nominator', - type: 'bytes32', - }, - { - internalType: 'bytes32', - name: 'bouncer', - type: 'bytes32', - }, - { - internalType: 'uint8[]', - name: 'name', - type: 'uint8[]', - }, - { - internalType: 'uint8[]', - name: 'icon', - type: 'uint8[]', - }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + { internalType: 'bytes32', name: 'root', type: 'bytes32' }, + { internalType: 'bytes32', name: 'nominator', type: 'bytes32' }, + { internalType: 'bytes32', name: 'bouncer', type: 'bytes32' }, + { internalType: 'uint8[]', name: 'name', type: 'uint8[]' }, + { internalType: 'uint8[]', name: 'icon', type: 'uint8[]' }, ], name: 'create', - outputs: [ - { - internalType: 'uint8', - name: '', - type: 'uint8', - }, - ], + outputs: [], stateMutability: 'nonpayable', type: 'function', }, { inputs: [ - { - internalType: 'uint256', - name: 'poolId', - type: 'uint256', - }, - { - internalType: 'bytes32[]', - name: 'validators', - type: 'bytes32[]', - }, + { internalType: 'uint256', name: 'poolId', type: 'uint256' }, + { internalType: 'bytes32[]', name: 'validators', type: 'bytes32[]' }, ], name: 'nominate', - outputs: [ - { - internalType: 'uint8', - name: '', - type: 'uint8', - }, - ], + outputs: [], stateMutability: 'nonpayable', type: 'function', }, { inputs: [ - { - internalType: 'uint256', - name: 'poolId', - type: 'uint256', - }, - { - internalType: 'uint8', - name: 'state', - type: 'uint8', - }, + { internalType: 'uint256', name: 'poolId', type: 'uint256' }, + { internalType: 'uint8', name: 'state', type: 'uint8' }, ], name: 'setState', - outputs: [ - { - internalType: 'uint8', - name: '', - type: 'uint8', - }, - ], + outputs: [], stateMutability: 'nonpayable', type: 'function', }, { inputs: [ - { - internalType: 'uint256', - name: 'poolId', - type: 'uint256', - }, - { - internalType: 'bytes', - name: 'metadata', - type: 'bytes', - }, + { internalType: 'uint256', name: 'poolId', type: 'uint256' }, + { internalType: 'bytes', name: 'metadata', type: 'bytes' }, ], name: 'setMetadata', - outputs: [ - { - internalType: 'uint8', - name: '', - type: 'uint8', - }, - ], + outputs: [], stateMutability: 'nonpayable', type: 'function', }, { inputs: [ - { - internalType: 'uint256', - name: 'minJoinBond', - type: 'uint256', - }, - { - internalType: 'uint256', - name: 'minCreateBond', - type: 'uint256', - }, - { - internalType: 'uint32', - name: 'maxPools', - type: 'uint32', - }, - { - internalType: 'uint32', - name: 'globalMaxCommission', - type: 'uint32', - }, + { internalType: 'uint256', name: 'minJoinBond', type: 'uint256' }, + { internalType: 'uint256', name: 'minCreateBond', type: 'uint256' }, + { internalType: 'uint32', name: 'maxPools', type: 'uint32' }, + { internalType: 'uint32', name: 'globalMaxCommission', type: 'uint32' }, ], name: 'setConfigs', - outputs: [ - { - internalType: 'uint8', - name: '', - type: 'uint8', - }, - ], + outputs: [], stateMutability: 'nonpayable', type: 'function', }, { inputs: [ - { - internalType: 'uint256', - name: 'poolId', - type: 'uint256', - }, - { - internalType: 'bytes32', - name: 'root', - type: 'bytes32', - }, - { - internalType: 'bytes32', - name: 'nominator', - type: 'bytes32', - }, - { - internalType: 'bytes32', - name: 'bouncer', - type: 'bytes32', - }, + { internalType: 'uint256', name: 'poolId', type: 'uint256' }, + { internalType: 'bytes32', name: 'root', type: 'bytes32' }, + { internalType: 'bytes32', name: 'nominator', type: 'bytes32' }, + { internalType: 'bytes32', name: 'bouncer', type: 'bytes32' }, ], name: 'updateRoles', - outputs: [ - { - internalType: 'uint8', - name: '', - type: 'uint8', - }, + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: 'poolId', type: 'uint256' }], + name: 'chill', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint256', name: 'poolId', type: 'uint256' }, + { internalType: 'bytes32', name: 'who', type: 'bytes32' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + ], + name: 'bondExtraOther', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint256', name: 'poolId', type: 'uint256' }, + { internalType: 'uint256', name: 'newCommission', type: 'uint256' }, + ], + name: 'setCommission', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint256', name: 'poolId', type: 'uint256' }, + { internalType: 'uint256', name: 'maxCommission', type: 'uint256' }, + ], + name: 'setCommissionMax', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint256', name: 'poolId', type: 'uint256' }, + { internalType: 'uint256', name: 'maxIncrease', type: 'uint256' }, + { internalType: 'uint256', name: 'minDelay', type: 'uint256' }, + ], + name: 'setCommissionChangeRate', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: 'poolId', type: 'uint256' }], + name: 'claimCommission', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: 'poolId', type: 'uint256' }], + name: 'adjustPoolDeposit', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint256', name: 'poolId', type: 'uint256' }, + { internalType: 'uint8', name: 'permission', type: 'uint8' }, ], + name: 'setCommissionClaimPermission', + outputs: [], stateMutability: 'nonpayable', type: 'function', }, diff --git a/apps/tangle-dapp/src/components/LiquidStaking/LsValidatorTable.tsx b/apps/tangle-dapp/src/components/LiquidStaking/LsValidatorTable.tsx deleted file mode 100644 index 04219f90ee..0000000000 --- a/apps/tangle-dapp/src/components/LiquidStaking/LsValidatorTable.tsx +++ /dev/null @@ -1,241 +0,0 @@ -import { - ColumnDef, - ColumnSort, - getCoreRowModel, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - PaginationState, - RowSelectionState, - SortingColumn, - SortingState, - TableOptions, - useReactTable, -} from '@tanstack/react-table'; -import { Search, Spinner } from '@webb-tools/icons'; -import { LiquidStakingItem } from '@webb-tools/tangle-shared-ui/types/liquidStaking'; -import { - fuzzyFilter, - Input, - Pagination, - Table, - Typography, -} from '@webb-tools/webb-ui-components'; -import pluralize from '@webb-tools/webb-ui-components/utils/pluralize'; -import { useEffect, useMemo, useRef, useState } from 'react'; - -import { useLsStore } from '../../data/liquidStaking/useLsStore'; -import useLsValidators from '../../data/liquidStaking/useLsValidators'; -import { useLsValidatorSelectionTableColumns } from '../../data/liquidStaking/useLsValidatorSelectionTableColumns'; -import { LiquidStakingItemType } from '../../types/liquidStaking'; - -const DEFAULT_PAGINATION: PaginationState = { - pageIndex: 0, - pageSize: 10, -}; - -const SELECTED_ITEMS_COLUMN_SORT = { - // TODO: Need to update the correct id. It seems that the `id` field no longer exists in the row's type. Need to statically-type-link-it instead of hard coding it, to avoid the same bug in the future. - id: 'id', - desc: false, -} as const satisfies ColumnSort; - -export const LsValidatorTable = () => { - const { lsProtocolId, setNetworkEntities } = useLsStore(); - - const { isLoading, data, dataType } = useLsValidators(lsProtocolId); - const [searchValue, setSearchValue] = useState(''); - const [rowSelection, setRowSelection] = useState({}); - - const [sorting, setSorting] = useState([ - SELECTED_ITEMS_COLUMN_SORT, - ]); - - const [pagination, setPagination] = - useState(DEFAULT_PAGINATION); - - const toggleSortSelectionHandlerRef = useRef< - SortingColumn['toggleSorting'] | null - >(null); - - useEffect(() => { - setNetworkEntities(new Set(Object.keys(rowSelection))); - }, [rowSelection, setNetworkEntities]); - - const columns = useLsValidatorSelectionTableColumns( - toggleSortSelectionHandlerRef, - dataType, - ) as ColumnDef[]; - - const tableTitle = useMemo(() => { - switch (dataType) { - case LiquidStakingItem.VALIDATOR: - return 'Validators'; - case LiquidStakingItem.DAPP: - return 'DApp'; - case LiquidStakingItem.VAULT_OR_STAKE_POOL: - return 'Vault or Stake Pool'; - case LiquidStakingItem.COLLATOR: - return 'Collators'; - } - }, [dataType]); - - const itemText = useMemo(() => { - switch (dataType) { - case LiquidStakingItem.VALIDATOR: - return 'Validator'; - case LiquidStakingItem.DAPP: - return 'dApp'; - case LiquidStakingItem.VAULT_OR_STAKE_POOL: - return 'Vault/Pool'; - case LiquidStakingItem.COLLATOR: - return 'Collator'; - } - }, [dataType]); - - const tableData = useMemo(() => (isLoading ? [] : data), [data, isLoading]); - - const tableColumns = useMemo( - () => (isLoading ? [] : columns), - [columns, isLoading], - ); - - const tableIsLoading = useMemo(() => { - if (isLoading) { - return true; - } - - return data.length > 0 && data[0].itemType !== dataType; - }, [data, dataType, isLoading]); - - const tableProps = useMemo>( - () => ({ - data: tableData, - columns: tableColumns, - state: { - sorting, - rowSelection, - pagination, - globalFilter: searchValue, - }, - enableRowSelection: true, - onPaginationChange: setPagination, - onGlobalFilterChange: (value) => { - setPagination(DEFAULT_PAGINATION); - setSearchValue(value); - }, - onRowSelectionChange: (value) => { - toggleSortSelectionHandlerRef.current?.(false); - setRowSelection(value); - }, - onSortingChange: (updaterOrValue) => { - setSorting((prev) => { - const newSorting = - typeof updaterOrValue === 'function' - ? updaterOrValue(prev) - : updaterOrValue; - - return newSorting.length === 0 - ? [SELECTED_ITEMS_COLUMN_SORT] - : newSorting[0].id === 'id' - ? newSorting - : [SELECTED_ITEMS_COLUMN_SORT, ...newSorting]; - }); - }, - filterFns: { fuzzy: fuzzyFilter }, - globalFilterFn: fuzzyFilter, - getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getRowId: (row) => row.id, - autoResetPageIndex: false, - enableMultiRowSelection: - dataType === LiquidStakingItem.VALIDATOR || - dataType === LiquidStakingItem.COLLATOR, - }), - [ - dataType, - pagination, - rowSelection, - searchValue, - sorting, - tableColumns, - tableData, - ], - ); - - const table = useReactTable(tableProps); - - return ( -
-
- {!tableIsLoading ? ( - <> -
- - Select {tableTitle} - - - } - placeholder={`Search ${tableTitle.toLowerCase()}...`} - value={searchValue} - onChange={(newSearchValue) => setSearchValue(newSearchValue)} - className="mb-1" - debounceTime={300} - /> - - - - - {data.length > DEFAULT_PAGINATION.pageSize && ( - 1)} - className="!px-0 !py-0 !pt-6" - /> - )} - - ) : ( -
-
- - - - Fetching {itemText}s... - -
-
- )} - - - {/* console.debug('Refresh')} - className="absolute top-[-12px] right-[-12px] border rounded-full shadow-md bg-mono-20 dark:bg-mono-160 border-mono-60 dark:border-mono-120 w-fit" - > - - */} - - ); -}; diff --git a/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/ExchangeRateDetailItem.tsx b/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/ExchangeRateDetailItem.tsx index b957ef24c2..c77d4d7e50 100644 --- a/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/ExchangeRateDetailItem.tsx +++ b/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/ExchangeRateDetailItem.tsx @@ -5,22 +5,16 @@ import { twMerge } from 'tailwind-merge'; import { LsToken } from '../../../constants/liquidStaking/types'; import useLsActivePoolDisplayName from '../../../data/liquidStaking/useLsActivePoolDisplayName'; -import useLsExchangeRate, { - ExchangeRateType, -} from '../../../data/liquidStaking/useLsExchangeRate'; +import useLsExchangeRate from '../../../data/liquidStaking/useLsExchangeRate'; import DetailItem from './DetailItem'; export type ExchangeRateDetailItemProps = { - type: ExchangeRateType; token: LsToken; }; -const ExchangeRateDetailItem: FC = ({ - type, - token, -}) => { +const ExchangeRateDetailItem: FC = ({ token }) => { const { displayName: lsActivePoolDisplayName } = useLsActivePoolDisplayName(); - const { exchangeRate, isRefreshing } = useLsExchangeRate(type); + const { exchangeRate, isRefreshing } = useLsExchangeRate(); const exchangeRateElement = exchangeRate instanceof Error ? ( diff --git a/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/FeeDetailItem.tsx b/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/FeeDetailItem.tsx deleted file mode 100644 index f6584d2d3c..0000000000 --- a/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/FeeDetailItem.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { BN, BN_ZERO } from '@polkadot/util'; -import { LsProtocolId } from '@webb-tools/tangle-shared-ui/types/liquidStaking'; -import { - AmountFormatStyle, - formatDisplayAmount, -} from '@webb-tools/webb-ui-components'; -import { EMPTY_VALUE_PLACEHOLDER } from '@webb-tools/webb-ui-components/constants'; -import formatPercentage from '@webb-tools/webb-ui-components/utils/formatPercentage'; -import { FC, useMemo } from 'react'; -import { twMerge } from 'tailwind-merge'; - -import getLsProtocolDef from '../../../utils/liquidStaking/getLsProtocolDef'; -import scaleAmountByPercentage from '../../../utils/scaleAmountByPercentage'; -import DetailItem from './DetailItem'; -import useLsFeePercentage from './useLsFeePercentage'; - -export type FeeDetailItemProps = { - isStaking: boolean; - inputAmount: BN | null; - protocolId: LsProtocolId; -}; - -const FeeDetailItem: FC = ({ - isStaking, - inputAmount, - protocolId, -}) => { - const feePercentage = useLsFeePercentage(protocolId, isStaking); - - const protocol = getLsProtocolDef(protocolId); - - const feeAmount = useMemo(() => { - // Propagate error or loading state. - if (typeof feePercentage !== 'number') { - return feePercentage; - } - - return scaleAmountByPercentage(inputAmount ?? BN_ZERO, feePercentage); - }, [feePercentage, inputAmount]); - - const formattedFeeAmount = useMemo(() => { - // Propagate error or loading state. - if (!(feeAmount instanceof BN)) { - return feeAmount; - } else if (feeAmount.isZero()) { - return EMPTY_VALUE_PLACEHOLDER; - } - - const formattedAmount = formatDisplayAmount( - feeAmount, - protocol.decimals, - AmountFormatStyle.EXACT, - ); - - return `${formattedAmount} ${protocol.token}`; - }, [feeAmount, protocol.decimals, protocol.token]); - - const feeTitle = - typeof feePercentage !== 'number' - ? 'Fee' - : `Fee (${formatPercentage(feePercentage * 100)})`; - - return ( - 0.1 && - 'text-yellow-500', - )} - /> - ); -}; - -export default FeeDetailItem; diff --git a/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx b/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx index f9635ba54a..fe7077ac8f 100644 --- a/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx +++ b/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx @@ -13,27 +13,20 @@ import { LsNetworkId, LsPoolDisplayName, } from '../../../constants/liquidStaking/types'; -import useMintTx from '../../../data/liquidStaking/parachain/useMintTx'; import useLsPoolJoinTx from '../../../data/liquidStaking/tangle/useLsPoolJoinTx'; -import useLsExchangeRate, { - ExchangeRateType, -} from '../../../data/liquidStaking/useLsExchangeRate'; +import useLsExchangeRate from '../../../data/liquidStaking/useLsExchangeRate'; import useAssetAccounts from '../../../data/liquidStaking/useAssetAccounts'; import useLsPools from '../../../data/liquidStaking/useLsPools'; import { useLsStore } from '../../../data/liquidStaking/useLsStore'; import useActiveAccountAddress from '../../../hooks/useActiveAccountAddress'; import { TxStatus } from '../../../hooks/useSubstrateTx'; import getLsProtocolDef from '../../../utils/liquidStaking/getLsProtocolDef'; -import scaleAmountByPercentage from '../../../utils/scaleAmountByPercentage'; import DetailsContainer from '../../DetailsContainer'; import ExchangeRateDetailItem from './ExchangeRateDetailItem'; -import FeeDetailItem from './FeeDetailItem'; import LsAgnosticBalance from './LsAgnosticBalance'; import LsInput from './LsInput'; import UnstakePeriodDetailItem from './UnstakePeriodDetailItem'; import useLsChangeNetwork from './useLsChangeNetwork'; -import useLsFeePercentage from './useLsFeePercentage'; -import useLsSpendingLimits from './useLsSpendingLimits'; import ListModal from '@webb-tools/tangle-shared-ui/components/ListModal'; import LstListItem from '../LstListItem'; import filterBy from '../../../utils/filterBy'; @@ -49,16 +42,8 @@ const LsStakeCard: FC = () => { const { execute: executeTanglePoolJoinTx, status: tanglePoolJoinTxStatus } = useLsPoolJoinTx(); - const { execute: executeParachainMintTx, status: parachainMintTxStatus } = - useMintTx(); - const activeAccountAddress = useActiveAccountAddress(); - const { maxSpendable, minSpendable } = useLsSpendingLimits( - true, - lsProtocolId, - ); - const selectedProtocol = getLsProtocolDef(lsProtocolId); const tryChangeNetwork = useLsChangeNetwork(); const lsPoolMembers = useAssetAccounts(); @@ -87,7 +72,7 @@ const LsStakeCard: FC = () => { const { exchangeRate: exchangeRateOrError, isRefreshing: isRefreshingExchangeRate, - } = useLsExchangeRate(ExchangeRateType.NativeToDerivative); + } = useLsExchangeRate(); // TODO: Properly handle the error state. const exchangeRate = @@ -100,14 +85,6 @@ const LsStakeCard: FC = () => { } if ( - selectedProtocol.networkId === LsNetworkId.TANGLE_RESTAKING_PARACHAIN && - executeParachainMintTx !== null - ) { - executeParachainMintTx({ - amount: fromAmount, - currency: selectedProtocol.currency, - }); - } else if ( isTangleNetwork && executeTanglePoolJoinTx !== null && lsPoolId !== null @@ -117,36 +94,18 @@ const LsStakeCard: FC = () => { poolId: lsPoolId, }); } - }, [ - executeParachainMintTx, - executeTanglePoolJoinTx, - fromAmount, - isTangleNetwork, - selectedProtocol, - lsPoolId, - ]); - - const feePercentage = useLsFeePercentage(lsProtocolId, true); + }, [executeTanglePoolJoinTx, fromAmount, isTangleNetwork, lsPoolId]); const toAmount = useMemo(() => { - if ( - fromAmount === null || - exchangeRate === null || - typeof feePercentage !== 'number' - ) { + if (fromAmount === null || exchangeRate === null) { return null; } - const feeAmount = scaleAmountByPercentage(fromAmount, feePercentage); - - return fromAmount.muln(exchangeRate).sub(feeAmount); - }, [fromAmount, exchangeRate, feePercentage]); + return fromAmount.muln(exchangeRate); + }, [fromAmount, exchangeRate]); const canCallStake = - (fromAmount !== null && - selectedProtocol.networkId === LsNetworkId.TANGLE_RESTAKING_PARACHAIN && - executeParachainMintTx !== null) || - (isTangleNetwork && executeTanglePoolJoinTx !== null && lsPoolId !== null); + isTangleNetwork && executeTanglePoolJoinTx !== null && lsPoolId !== null; const walletBalance = ( { - - - + ); diff --git a/apps/tangle-dapp/src/containers/VaultsAndBalancesTable.tsx b/apps/tangle-dapp/src/containers/VaultsAndBalancesTable.tsx index a0f0462869..526e901f9d 100644 --- a/apps/tangle-dapp/src/containers/VaultsAndBalancesTable.tsx +++ b/apps/tangle-dapp/src/containers/VaultsAndBalancesTable.tsx @@ -390,7 +390,7 @@ const VaultsAndBalancesTable: FC = () => { return ( ); } diff --git a/apps/tangle-dapp/src/types/liquidStaking.ts b/apps/tangle-dapp/src/types/liquidStaking.ts index dc9a927cb4..58081db72b 100644 --- a/apps/tangle-dapp/src/types/liquidStaking.ts +++ b/apps/tangle-dapp/src/types/liquidStaking.ts @@ -1,16 +1,3 @@ -import { - Collator, - Dapp, - PhalaVaultOrStakePool, - Validator, -} from '@webb-tools/tangle-shared-ui/types/liquidStaking'; - -export type LiquidStakingItemType = - | Validator - | PhalaVaultOrStakePool - | Dapp - | Collator; - export enum LiquidStakingToken { DOT = 'DOT', GLMR = 'GLMR', diff --git a/libs/tangle-shared-ui/src/components/NetworkSelectorDropdown/index.tsx b/libs/tangle-shared-ui/src/components/NetworkSelectorDropdown/index.tsx index 2116e2e325..92dfa04b0d 100644 --- a/libs/tangle-shared-ui/src/components/NetworkSelectorDropdown/index.tsx +++ b/libs/tangle-shared-ui/src/components/NetworkSelectorDropdown/index.tsx @@ -19,7 +19,7 @@ import { import { type FC, useCallback, useMemo } from 'react'; import { twMerge } from 'tailwind-merge'; import useNetworkStore from '../../context/useNetworkStore'; -import useNetworkSwitcher from '../../hooks/useNetworkSwitcher'; +import useSwitchNetwork from '../../hooks/useSwitchNetwork'; import createCustomNetwork from '../../utils/createCustomNetwork'; import { NetworkSelectorDropdown } from './NetworkSelectorDropdown'; @@ -28,7 +28,7 @@ const NetworkSelectionButton: FC = () => { useWebContext(); const { network } = useNetworkStore(); - const { switchNetwork, isCustom } = useNetworkSwitcher(); + const { switchNetwork, isCustom } = useSwitchNetwork(); // TODO: Handle switching network on EVM wallet here. const switchToCustomNetwork = useCallback( diff --git a/libs/tangle-shared-ui/src/context/useProviderStore.ts b/libs/tangle-shared-ui/src/context/useProviderStore.ts new file mode 100644 index 0000000000..c922def976 --- /dev/null +++ b/libs/tangle-shared-ui/src/context/useProviderStore.ts @@ -0,0 +1,28 @@ +'use client'; + +import { create } from 'zustand'; + +import { + InjectedAccountWithMeta, + InjectedExtension, +} from '@polkadot/extension-inject/types'; +import { + EvmAddress, + SubstrateAddress, +} from '@webb-tools/webb-ui-components/types/address'; +import { EIP1193Provider } from 'viem'; + +const useProviderStore = create<{ + accounts?: InjectedAccountWithMeta[]; + activeAccountAddress?: SubstrateAddress | EvmAddress; + provider?: InjectedExtension | EIP1193Provider; + setActiveAccountAddress: (address?: SubstrateAddress | EvmAddress) => void; + setAccounts: (accounts?: InjectedAccountWithMeta[]) => void; + setProvider: (provider?: InjectedExtension | EIP1193Provider) => void; +}>((set) => ({ + setActiveAccountAddress: (account) => set({ activeAccountAddress: account }), + setAccounts: (accounts) => set({ accounts }), + setProvider: (provider) => set({ provider }), +})); + +export default useProviderStore; diff --git a/libs/tangle-shared-ui/src/hooks/useLocalStorage.ts b/libs/tangle-shared-ui/src/hooks/useLocalStorage.ts index 06e60ee458..ec16fd4035 100644 --- a/libs/tangle-shared-ui/src/hooks/useLocalStorage.ts +++ b/libs/tangle-shared-ui/src/hooks/useLocalStorage.ts @@ -4,27 +4,14 @@ import { HexString } from '@polkadot/util/types'; import { useCallback, useEffect, useState } from 'react'; import { BridgeQueueTxItem, Payout, TangleTokenSymbol } from '../types'; -import { - Collator, - Dapp, - PhalaVaultOrStakePool, - Validator, -} from '../types/liquidStaking'; import Optional from '../utils/Optional'; export enum LocalStorageKey { - IS_BALANCES_TABLE_DETAILS_COLLAPSED = 'isBalancesTableDetailsCollapsed', - ACTIVE_AND_DELEGATION_COUNT = 'activeAndDelegationCount', - IDEAL_STAKE_PERCENTAGE = 'idealStakePercentage', - VALIDATOR_COUNTS = 'validatorCounts', - WAITING_COUNT = 'waitingCount', PAYOUTS = 'payouts', CUSTOM_RPC_ENDPOINT = 'customRpcEndpoint', KNOWN_NETWORK_ID = 'knownNetworkId', - SERVICES_CACHE = 'servicesCache', SUBSTRATE_WALLETS_METADATA = 'substrateWalletsMetadata', BRIDGE_TX_QUEUE_BY_ACC = 'bridgeTxQueue', - LIQUID_STAKING_TABLE_DATA = 'liquidStakingTableData', } export type PayoutsCache = { @@ -33,10 +20,6 @@ export type PayoutsCache = { }; }; -export type LiquidStakingTableData = { - [chain: string]: Validator[] | PhalaVaultOrStakePool[] | Dapp[] | Collator[]; -}; - export type SubstrateWalletsMetadataEntry = { tokenSymbol: TangleTokenSymbol; tokenDecimals: number; @@ -54,29 +37,17 @@ export type TxQueueByAccount = Record; * respective value types. */ export type LocalStorageValueOf = - T extends LocalStorageKey.IS_BALANCES_TABLE_DETAILS_COLLAPSED - ? boolean - : T extends LocalStorageKey.ACTIVE_AND_DELEGATION_COUNT - ? { value1: number | null; value2: number | null } - : T extends LocalStorageKey.IDEAL_STAKE_PERCENTAGE - ? { value1: number | null } - : T extends LocalStorageKey.VALIDATOR_COUNTS - ? { value1: number | null; value2: number | null } - : T extends LocalStorageKey.WAITING_COUNT - ? { value1: number | null } - : T extends LocalStorageKey.PAYOUTS - ? PayoutsCache - : T extends LocalStorageKey.CUSTOM_RPC_ENDPOINT - ? string - : T extends LocalStorageKey.KNOWN_NETWORK_ID - ? number - : T extends LocalStorageKey.SUBSTRATE_WALLETS_METADATA - ? SubstrateWalletsMetadataCache - : T extends LocalStorageKey.BRIDGE_TX_QUEUE_BY_ACC - ? TxQueueByAccount - : T extends LocalStorageKey.LIQUID_STAKING_TABLE_DATA - ? LiquidStakingTableData - : never; + T extends LocalStorageKey.PAYOUTS + ? PayoutsCache + : T extends LocalStorageKey.CUSTOM_RPC_ENDPOINT + ? string + : T extends LocalStorageKey.KNOWN_NETWORK_ID + ? number + : T extends LocalStorageKey.SUBSTRATE_WALLETS_METADATA + ? SubstrateWalletsMetadataCache + : T extends LocalStorageKey.BRIDGE_TX_QUEUE_BY_ACC + ? TxQueueByAccount + : never; export const getJsonFromLocalStorage = ( key: Key, diff --git a/libs/tangle-shared-ui/src/hooks/useProvider.ts b/libs/tangle-shared-ui/src/hooks/useProvider.ts new file mode 100644 index 0000000000..cea797ffcd --- /dev/null +++ b/libs/tangle-shared-ui/src/hooks/useProvider.ts @@ -0,0 +1,192 @@ +import { web3Accounts, web3Enable } from '@polkadot/extension-dapp'; +import useProviderStore from '../context/useProviderStore'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { + EvmAddress, + SubstrateAddress, +} from '@webb-tools/webb-ui-components/types/address'; +import { + assertEvmAddress, + assertSubstrateAddress, +} from '@webb-tools/webb-ui-components'; +import ensureError from '../utils/ensureError'; +import { EIP1193Provider } from 'viem'; + +declare global { + interface Window { + ethereum?: EIP1193Provider; + } +} + +const useProvider = () => { + const [isConnecting, setIsConnecting] = useState(false); + + const { + activeAccountAddress, + accounts, + provider, + setActiveAccountAddress, + setAccounts, + setProvider, + } = useProviderStore(); + + /** + * Switch accounts within the same provider/wallet. + */ + const trySwitchAccount = useCallback( + (newAccountAddress: SubstrateAddress | EvmAddress): boolean => { + if (accounts === undefined) { + return false; + } + + const newAccount = accounts.find( + (account) => account.address === newAccountAddress, + ); + + if (newAccount === undefined) { + return false; + } + + setActiveAccountAddress(newAccountAddress); + + return true; + }, + [accounts, setActiveAccountAddress], + ); + + const signOut = useCallback(() => { + setAccounts(undefined); + setActiveAccountAddress(undefined); + setProvider(undefined); + }, [setAccounts, setActiveAccountAddress, setProvider]); + + const connectSubstrateProvider = useCallback( + async (appName: string): Promise => { + setIsConnecting(true); + + // Request access to any installed Substrate wallet extensions. + const extensions = await web3Enable(appName); + + // No extension found or the user did not grant permission. + if (extensions.length === 0) { + setIsConnecting(false); + + return new Error('No wallet extensions detected or permission denied'); + } + + // Fetch all accounts across all enabled extensions. + const accounts = await web3Accounts(); + + const defaultAccount = accounts.at(0); + + // No accounts found. + if (accounts.length === 0 || defaultAccount === undefined) { + setIsConnecting(false); + signOut(); + + return new Error('No accounts found in the connected wallet'); + } + + // TODO: Obtain this from local storage, persist what was the last connected account. + + const newProvider = extensions.find( + (provider) => provider.name === defaultAccount.meta.source, + ); + + if (newProvider === undefined) { + setIsConnecting(false); + signOut(); + + return new Error( + `The associated provider for account ${defaultAccount.address} could not be located`, + ); + } + + setProvider(newProvider); + setAccounts(accounts); + setActiveAccountAddress(assertSubstrateAddress(defaultAccount.address)); + setIsConnecting(false); + + return true; + }, + [setAccounts, setActiveAccountAddress, setProvider, signOut], + ); + + const connectEvmProvider = useCallback(async (): Promise => { + try { + // Check if window.ethereum is available. + if (typeof window.ethereum === 'undefined') { + return new Error( + 'No EVM provider detected (window.ethereum is undefined)', + ); + } + + setIsConnecting(true); + + // Request account access. + const accounts = await window.ethereum.request({ + method: 'eth_requestAccounts', + }); + + // No accounts found. + if (!accounts || accounts.length === 0) { + setIsConnecting(false); + + return new Error('No EVM accounts found'); + } + + const evmAccounts = accounts.map((address) => ({ + address, + meta: { source: 'EVM' }, + })); + + // TODO: Obtain this from local storage, persist what was the last connected account. + const defaultAccount = assertEvmAddress(evmAccounts[0].address); + + setProvider(window.ethereum); + setAccounts(evmAccounts); + setActiveAccountAddress(defaultAccount); + + return true; + } catch (possibleError) { + const error = ensureError(possibleError); + + console.error('Failed to connect EVM wallet:', error); + + return error; + } finally { + setIsConnecting(false); + } + }, [setAccounts, setActiveAccountAddress, setProvider]); + + const activeAccount = useMemo(() => { + if (accounts === undefined || activeAccountAddress === undefined) { + return null; + } + + return ( + accounts.find((account) => account.address === activeAccountAddress) ?? + null + ); + }, [accounts, activeAccountAddress]); + + const isConnected = activeAccount !== null; + + useEffect(() => { + console.debug('IS CONNECTING', isConnecting); + console.debug('ACCOUNT', activeAccount); + }, [activeAccount, isConnecting]); + + return { + isConnected, + isConnecting, + activeAccount, + provider, + trySwitchAccount, + signOut, + connectSubstrateProvider, + connectEvmProvider, + }; +}; + +export default useProvider; diff --git a/libs/tangle-shared-ui/src/hooks/useNetworkSwitcher.ts b/libs/tangle-shared-ui/src/hooks/useSwitchNetwork.ts similarity index 96% rename from libs/tangle-shared-ui/src/hooks/useNetworkSwitcher.ts rename to libs/tangle-shared-ui/src/hooks/useSwitchNetwork.ts index ec1d859393..4f2a078f61 100644 --- a/libs/tangle-shared-ui/src/hooks/useNetworkSwitcher.ts +++ b/libs/tangle-shared-ui/src/hooks/useSwitchNetwork.ts @@ -25,12 +25,12 @@ import testRpcEndpointConnection from '../utils/testRpcEndpointConnection'; import ensureError from '../utils/ensureError'; import { getApiPromise } from '../utils/polkadot/api'; -const useNetworkSwitcher = () => { +const useSwitchNetwork = () => { const { switchChain, activeWallet } = useWebContext(); const [isCustom, setIsCustom] = useState(false); const { network, setNetwork } = useNetworkStore(); - // TODO: Should utilize the zustand middleware to cache this + // TODO: Should utilize the Zustand persistence middleware to cache this. // in instead of manually handling it here. // if we set the network by calling `setNetwork`, // the new network won't be cached in local storage. @@ -92,12 +92,11 @@ const useNetworkSwitcher = () => { if (activeWallet !== undefined) { try { const chain = await mapNetworkToChain(newNetwork, activeWallet); - const switchChainResult = await switchChain(chain, activeWallet); if (switchChainResult !== null) { console.debug( - `Switching to ${isCustom ? 'custom' : 'Webb'} network: ${ + `Switching to ${isCustom ? 'custom' : 'Tangle'} network: ${ newNetwork.name } (${newNetwork.nodeType}) with RPC endpoint: ${ newNetwork.wsRpcEndpoint @@ -241,4 +240,4 @@ function defineWebbChain( } satisfies Chain; } -export default useNetworkSwitcher; +export default useSwitchNetwork; diff --git a/libs/tangle-shared-ui/src/types/liquidStaking.ts b/libs/tangle-shared-ui/src/types/liquidStaking.ts index 8a5b56517a..8cc01f06b9 100644 --- a/libs/tangle-shared-ui/src/types/liquidStaking.ts +++ b/libs/tangle-shared-ui/src/types/liquidStaking.ts @@ -1,5 +1,3 @@ -import type { BN } from '@polkadot/util'; - export enum LsProtocolId { POLKADOT, PHALA, @@ -10,54 +8,3 @@ export enum LsProtocolId { TANGLE_TESTNET, TANGLE_LOCAL, } - -export enum LiquidStakingItem { - VALIDATOR = 'validator', - VAULT_OR_STAKE_POOL = 'vaultOrStakePool', - DAPP = 'dapp', - COLLATOR = 'collator', -} - -// All chains -type StakingItem = { - id: string; // address - Validator, contract address - DAPP, pool/vault ID - VaultOrStakePool - totalValueStaked: BN; - minimumStake?: BN; - chainId: LsProtocolId; - chainDecimals: number; - chainTokenSymbol: string; - itemType: LiquidStakingItem; - href: string; -}; - -// Chains - Polkadot, Kusama, Tangle etc. -export type Validator = { - validatorAddress: string; - validatorIdentity: string; - validatorCommission: BN; - validatorAPY?: number; -} & StakingItem; - -// Chain - Phala Network (Stake on Vaults or Stake Pools) -export type PhalaVaultOrStakePool = { - vaultOrStakePoolID: string; - vaultOrStakePoolName: string; - vaultOrStakePoolAccountID: string; - commission: BN; - type: string; -} & StakingItem; - -// Chain - Astar (dApp Staking) -export type Dapp = { - dappContractAddress: string; - dappName: string; - dappContractType: string; - commission: BN; -} & StakingItem; - -// Chains - Moonbeam, Manta -export type Collator = { - collatorAddress: string; - collatorIdentity: string; - collatorDelegationCount: number; -} & StakingItem; From 4e9bcb2482a8affec9e26f34e849f8d6593bae99 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Sun, 26 Jan 2025 22:11:34 -0500 Subject: [PATCH 03/28] feat(tangle-dapp): Implement `useEvmTxRelayer` --- .../src/components/Sidebar/sidebarProps.ts | 10 - .../src/data/balances/useTransferTx.ts | 2 +- .../liquidStaking/tangle/useLsCreatePoolTx.ts | 2 +- .../liquidStaking/tangle/useLsPoolJoinTx.ts | 2 +- .../liquidStaking/tangle/useLsPoolUnbondTx.ts | 2 +- .../tangle/useLsUpdateRolesTx.ts | 2 +- .../liquidStaking/useLsWithdrawUnbondedTx.ts | 2 +- .../src/data/payouts/usePayoutAllTx.ts | 2 +- .../src/data/payouts/usePayoutStakersTx.ts | 2 +- .../src/data/rewards/useClaimRewardsTx.ts | 2 +- .../src/data/staking/useBondExtraTx.ts | 2 +- .../src/data/staking/useChillTx.ts | 2 +- .../src/data/staking/useNominateTx.ts | 2 +- .../src/data/staking/useRebondTx.ts | 2 +- .../src/data/staking/useSetPayeeTx.ts | 2 +- .../src/data/staking/useSetupNominatorTx.ts | 2 +- .../src/data/staking/useUnbondTx.ts | 2 +- .../src/data/staking/useUpdateNominatorTx.ts | 5 +- .../src/data/staking/useWithdrawUnbondedTx.ts | 2 +- apps/tangle-dapp/src/hooks/useAgnosticTx.ts | 6 +- .../src/hooks/useEvmGasEstimate.ts | 37 +++ ...pileAbiCall.ts => useEvmPrecompileCall.ts} | 4 +- .../src/hooks/useEvmPrecompileFee.ts | 2 +- apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts | 234 +++++++++++++++++- .../src/hooks/useViemWalletClient.ts | 38 +++ .../src/utils/staking/createEvmBatchCall.ts | 2 +- .../utils/staking/createEvmBatchCallArgs.ts | 2 +- .../tangle-shared-ui/src/hooks/useProvider.ts | 1 + .../src/constants/networks.ts | 2 + 29 files changed, 337 insertions(+), 40 deletions(-) create mode 100644 apps/tangle-dapp/src/hooks/useEvmGasEstimate.ts rename apps/tangle-dapp/src/hooks/{useEvmPrecompileAbiCall.ts => useEvmPrecompileCall.ts} (98%) create mode 100644 apps/tangle-dapp/src/hooks/useViemWalletClient.ts diff --git a/apps/tangle-dapp/src/components/Sidebar/sidebarProps.ts b/apps/tangle-dapp/src/components/Sidebar/sidebarProps.ts index b9af987c6e..aa4223b4a3 100644 --- a/apps/tangle-dapp/src/components/Sidebar/sidebarProps.ts +++ b/apps/tangle-dapp/src/components/Sidebar/sidebarProps.ts @@ -4,7 +4,6 @@ import { DocumentationIcon, GiftLineIcon, GlobalLine, - GridFillIcon, HomeFillIcon, PolkadotJs, ShuffleLine, @@ -44,15 +43,6 @@ const SIDEBAR_STATIC_ITEMS: SideBarItemProps[] = [ Icon: TokenSwapFill, subItems: [], }, - { - name: 'Blueprints', - href: PagePath.BLUEPRINTS, - isInternal: true, - isNext: false, - Icon: GridFillIcon, - subItems: [], - hideInProduction: true, - }, { name: 'Liquid Stake', href: PagePath.LIQUID_STAKING, diff --git a/apps/tangle-dapp/src/data/balances/useTransferTx.ts b/apps/tangle-dapp/src/data/balances/useTransferTx.ts index b24ad03ec9..254c2be2ef 100644 --- a/apps/tangle-dapp/src/data/balances/useTransferTx.ts +++ b/apps/tangle-dapp/src/data/balances/useTransferTx.ts @@ -13,7 +13,7 @@ import { useCallback } from 'react'; import { TxName } from '../../constants'; import { PrecompileAddress } from '../../constants/evmPrecompiles'; import useAgnosticTx from '../../hooks/useAgnosticTx'; -import { AbiCall, EvmTxFactory } from '../../hooks/useEvmPrecompileAbiCall'; +import { AbiCall, EvmTxFactory } from '../../hooks/useEvmPrecompileCall'; import useEvmPrecompileFeeFetcher from '../../hooks/useEvmPrecompileFee'; import useFormatNativeTokenAmount from '../../hooks/useFormatNativeTokenAmount'; import { SubstrateTxFactory } from '../../hooks/useSubstrateTx'; diff --git a/apps/tangle-dapp/src/data/liquidStaking/tangle/useLsCreatePoolTx.ts b/apps/tangle-dapp/src/data/liquidStaking/tangle/useLsCreatePoolTx.ts index 3ea92655ac..d31bfd7ed3 100644 --- a/apps/tangle-dapp/src/data/liquidStaking/tangle/useLsCreatePoolTx.ts +++ b/apps/tangle-dapp/src/data/liquidStaking/tangle/useLsCreatePoolTx.ts @@ -7,7 +7,7 @@ import { useCallback } from 'react'; import { TxName } from '../../../constants'; import { PrecompileAddress } from '../../../constants/evmPrecompiles'; import useAgnosticTx from '../../../hooks/useAgnosticTx'; -import { EvmTxFactory } from '../../../hooks/useEvmPrecompileAbiCall'; +import { EvmTxFactory } from '../../../hooks/useEvmPrecompileCall'; import { SubstrateTxFactory } from '../../../hooks/useSubstrateTx'; import LST_PRECOMPILE_ABI from '../../../abi/lst'; diff --git a/apps/tangle-dapp/src/data/liquidStaking/tangle/useLsPoolJoinTx.ts b/apps/tangle-dapp/src/data/liquidStaking/tangle/useLsPoolJoinTx.ts index 46ab457bb1..934c242b28 100644 --- a/apps/tangle-dapp/src/data/liquidStaking/tangle/useLsPoolJoinTx.ts +++ b/apps/tangle-dapp/src/data/liquidStaking/tangle/useLsPoolJoinTx.ts @@ -4,7 +4,7 @@ import { useCallback } from 'react'; import { TxName } from '../../../constants'; import { PrecompileAddress } from '../../../constants/evmPrecompiles'; import useAgnosticTx from '../../../hooks/useAgnosticTx'; -import { EvmTxFactory } from '../../../hooks/useEvmPrecompileAbiCall'; +import { EvmTxFactory } from '../../../hooks/useEvmPrecompileCall'; import { SubstrateTxFactory } from '../../../hooks/useSubstrateTx'; import LST_PRECOMPILE_ABI from '../../../abi/lst'; diff --git a/apps/tangle-dapp/src/data/liquidStaking/tangle/useLsPoolUnbondTx.ts b/apps/tangle-dapp/src/data/liquidStaking/tangle/useLsPoolUnbondTx.ts index 2a8a6b3232..91bf21910b 100644 --- a/apps/tangle-dapp/src/data/liquidStaking/tangle/useLsPoolUnbondTx.ts +++ b/apps/tangle-dapp/src/data/liquidStaking/tangle/useLsPoolUnbondTx.ts @@ -4,7 +4,7 @@ import { useCallback } from 'react'; import { TxName } from '../../../constants'; import { PrecompileAddress } from '../../../constants/evmPrecompiles'; import useAgnosticTx from '../../../hooks/useAgnosticTx'; -import { EvmTxFactory } from '../../../hooks/useEvmPrecompileAbiCall'; +import { EvmTxFactory } from '../../../hooks/useEvmPrecompileCall'; import { SubstrateTxFactory } from '../../../hooks/useSubstrateTx'; import LST_PRECOMPILE_ABI from '../../../abi/lst'; import { convertAddressToBytes32 } from '@webb-tools/webb-ui-components'; diff --git a/apps/tangle-dapp/src/data/liquidStaking/tangle/useLsUpdateRolesTx.ts b/apps/tangle-dapp/src/data/liquidStaking/tangle/useLsUpdateRolesTx.ts index a2763c8180..6835ce7100 100644 --- a/apps/tangle-dapp/src/data/liquidStaking/tangle/useLsUpdateRolesTx.ts +++ b/apps/tangle-dapp/src/data/liquidStaking/tangle/useLsUpdateRolesTx.ts @@ -9,7 +9,7 @@ import { useCallback } from 'react'; import { TxName } from '../../../constants'; import { PrecompileAddress } from '../../../constants/evmPrecompiles'; import useAgnosticTx from '../../../hooks/useAgnosticTx'; -import { EvmTxFactory } from '../../../hooks/useEvmPrecompileAbiCall'; +import { EvmTxFactory } from '../../../hooks/useEvmPrecompileCall'; import { SubstrateTxFactory } from '../../../hooks/useSubstrateTx'; import LST_PRECOMPILE_ABI from '../../../abi/lst'; diff --git a/apps/tangle-dapp/src/data/liquidStaking/useLsWithdrawUnbondedTx.ts b/apps/tangle-dapp/src/data/liquidStaking/useLsWithdrawUnbondedTx.ts index b7ab8bcd52..6bc6e7b296 100644 --- a/apps/tangle-dapp/src/data/liquidStaking/useLsWithdrawUnbondedTx.ts +++ b/apps/tangle-dapp/src/data/liquidStaking/useLsWithdrawUnbondedTx.ts @@ -3,7 +3,7 @@ import { useCallback } from 'react'; import { TxName } from '../../constants'; import { PrecompileAddress } from '../../constants/evmPrecompiles'; import useAgnosticTx from '../../hooks/useAgnosticTx'; -import { EvmTxFactory } from '../../hooks/useEvmPrecompileAbiCall'; +import { EvmTxFactory } from '../../hooks/useEvmPrecompileCall'; import { SubstrateTxFactory } from '../../hooks/useSubstrateTx'; import LST_PRECOMPILE_ABI from '../../abi/lst'; import { convertAddressToBytes32 } from '@webb-tools/webb-ui-components'; diff --git a/apps/tangle-dapp/src/data/payouts/usePayoutAllTx.ts b/apps/tangle-dapp/src/data/payouts/usePayoutAllTx.ts index 478b00a50a..1dba0e98e7 100644 --- a/apps/tangle-dapp/src/data/payouts/usePayoutAllTx.ts +++ b/apps/tangle-dapp/src/data/payouts/usePayoutAllTx.ts @@ -6,7 +6,7 @@ import { useCallback } from 'react'; import { TxName } from '../../constants'; import { PrecompileAddress } from '../../constants/evmPrecompiles'; import useAgnosticTx from '../../hooks/useAgnosticTx'; -import { EvmTxFactory } from '../../hooks/useEvmPrecompileAbiCall'; +import { EvmTxFactory } from '../../hooks/useEvmPrecompileCall'; import { SubstrateTxFactory } from '../../hooks/useSubstrateTx'; import optimizeTxBatch from '../../utils/optimizeTxBatch'; import createEvmBatchCallArgs from '../../utils/staking/createEvmBatchCallArgs'; diff --git a/apps/tangle-dapp/src/data/payouts/usePayoutStakersTx.ts b/apps/tangle-dapp/src/data/payouts/usePayoutStakersTx.ts index c502c3aa99..3dc9d34b28 100644 --- a/apps/tangle-dapp/src/data/payouts/usePayoutStakersTx.ts +++ b/apps/tangle-dapp/src/data/payouts/usePayoutStakersTx.ts @@ -6,7 +6,7 @@ import { useCallback } from 'react'; import { TxName } from '../../constants'; import { PrecompileAddress } from '../../constants/evmPrecompiles'; import useAgnosticTx from '../../hooks/useAgnosticTx'; -import { EvmTxFactory } from '../../hooks/useEvmPrecompileAbiCall'; +import { EvmTxFactory } from '../../hooks/useEvmPrecompileCall'; import { SubstrateTxFactory } from '../../hooks/useSubstrateTx'; import STAKING_PRECOMPILE_ABI from '../../abi/staking'; diff --git a/apps/tangle-dapp/src/data/rewards/useClaimRewardsTx.ts b/apps/tangle-dapp/src/data/rewards/useClaimRewardsTx.ts index 80bdd9cf24..a822a503d0 100644 --- a/apps/tangle-dapp/src/data/rewards/useClaimRewardsTx.ts +++ b/apps/tangle-dapp/src/data/rewards/useClaimRewardsTx.ts @@ -9,7 +9,7 @@ import REWARDS_PRECOMPILE_ABI from '../../abi/rewards'; import { TxName } from '../../constants'; import { PrecompileAddress } from '../../constants/evmPrecompiles'; import useAgnosticTx from '../../hooks/useAgnosticTx'; -import { EvmTxFactory } from '../../hooks/useEvmPrecompileAbiCall'; +import { EvmTxFactory } from '../../hooks/useEvmPrecompileCall'; import { SubstrateTxFactory } from '../../hooks/useSubstrateTx'; import optimizeTxBatch from '../../utils/optimizeTxBatch'; import createEvmBatchCall from '../../utils/staking/createEvmBatchCall'; diff --git a/apps/tangle-dapp/src/data/staking/useBondExtraTx.ts b/apps/tangle-dapp/src/data/staking/useBondExtraTx.ts index a44825e050..0f667c7240 100644 --- a/apps/tangle-dapp/src/data/staking/useBondExtraTx.ts +++ b/apps/tangle-dapp/src/data/staking/useBondExtraTx.ts @@ -4,7 +4,7 @@ import { useCallback } from 'react'; import { TxName } from '../../constants'; import { PrecompileAddress } from '../../constants/evmPrecompiles'; import useAgnosticTx from '../../hooks/useAgnosticTx'; -import { EvmTxFactory } from '../../hooks/useEvmPrecompileAbiCall'; +import { EvmTxFactory } from '../../hooks/useEvmPrecompileCall'; import useFormatNativeTokenAmount from '../../hooks/useFormatNativeTokenAmount'; import { SubstrateTxFactory } from '../../hooks/useSubstrateTx'; import { GetSuccessMessageFn } from '../../types'; diff --git a/apps/tangle-dapp/src/data/staking/useChillTx.ts b/apps/tangle-dapp/src/data/staking/useChillTx.ts index 226ea562f2..f6931f0577 100644 --- a/apps/tangle-dapp/src/data/staking/useChillTx.ts +++ b/apps/tangle-dapp/src/data/staking/useChillTx.ts @@ -3,7 +3,7 @@ import { useCallback } from 'react'; import { TxName } from '../../constants'; import { PrecompileAddress } from '../../constants/evmPrecompiles'; import useAgnosticTx from '../../hooks/useAgnosticTx'; -import { EvmTxFactory } from '../../hooks/useEvmPrecompileAbiCall'; +import { EvmTxFactory } from '../../hooks/useEvmPrecompileCall'; import { SubstrateTxFactory } from '../../hooks/useSubstrateTx'; import STAKING_PRECOMPILE_ABI from '../../abi/staking'; diff --git a/apps/tangle-dapp/src/data/staking/useNominateTx.ts b/apps/tangle-dapp/src/data/staking/useNominateTx.ts index 7ad74c224d..61211e89bc 100644 --- a/apps/tangle-dapp/src/data/staking/useNominateTx.ts +++ b/apps/tangle-dapp/src/data/staking/useNominateTx.ts @@ -6,7 +6,7 @@ import { useCallback } from 'react'; import { TxName } from '../../constants'; import { PrecompileAddress } from '../../constants/evmPrecompiles'; import useAgnosticTx from '../../hooks/useAgnosticTx'; -import { EvmTxFactory } from '../../hooks/useEvmPrecompileAbiCall'; +import { EvmTxFactory } from '../../hooks/useEvmPrecompileCall'; import { SubstrateTxFactory } from '../../hooks/useSubstrateTx'; import STAKING_PRECOMPILE_ABI from '../../abi/staking'; diff --git a/apps/tangle-dapp/src/data/staking/useRebondTx.ts b/apps/tangle-dapp/src/data/staking/useRebondTx.ts index f7bfc8d58a..f9152a2811 100644 --- a/apps/tangle-dapp/src/data/staking/useRebondTx.ts +++ b/apps/tangle-dapp/src/data/staking/useRebondTx.ts @@ -4,7 +4,7 @@ import { useCallback } from 'react'; import { TxName } from '../../constants'; import { PrecompileAddress } from '../../constants/evmPrecompiles'; import useAgnosticTx from '../../hooks/useAgnosticTx'; -import { EvmTxFactory } from '../../hooks/useEvmPrecompileAbiCall'; +import { EvmTxFactory } from '../../hooks/useEvmPrecompileCall'; import useFormatNativeTokenAmount from '../../hooks/useFormatNativeTokenAmount'; import { SubstrateTxFactory } from '../../hooks/useSubstrateTx'; import { GetSuccessMessageFn } from '../../types'; diff --git a/apps/tangle-dapp/src/data/staking/useSetPayeeTx.ts b/apps/tangle-dapp/src/data/staking/useSetPayeeTx.ts index 45b3035f94..951614004d 100644 --- a/apps/tangle-dapp/src/data/staking/useSetPayeeTx.ts +++ b/apps/tangle-dapp/src/data/staking/useSetPayeeTx.ts @@ -3,7 +3,7 @@ import { useCallback } from 'react'; import { TxName } from '../../constants'; import { PrecompileAddress } from '../../constants/evmPrecompiles'; import useAgnosticTx from '../../hooks/useAgnosticTx'; -import { EvmTxFactory } from '../../hooks/useEvmPrecompileAbiCall'; +import { EvmTxFactory } from '../../hooks/useEvmPrecompileCall'; import { SubstrateTxFactory } from '../../hooks/useSubstrateTx'; import { StakingRewardsDestination } from '../../types'; import getEvmPayeeValue from '../../utils/staking/getEvmPayeeValue'; diff --git a/apps/tangle-dapp/src/data/staking/useSetupNominatorTx.ts b/apps/tangle-dapp/src/data/staking/useSetupNominatorTx.ts index 348768ef63..c54faf87a1 100644 --- a/apps/tangle-dapp/src/data/staking/useSetupNominatorTx.ts +++ b/apps/tangle-dapp/src/data/staking/useSetupNominatorTx.ts @@ -6,7 +6,7 @@ import { useCallback } from 'react'; import { TxName } from '../../constants'; import { PrecompileAddress } from '../../constants/evmPrecompiles'; import useAgnosticTx from '../../hooks/useAgnosticTx'; -import { EvmTxFactory } from '../../hooks/useEvmPrecompileAbiCall'; +import { EvmTxFactory } from '../../hooks/useEvmPrecompileCall'; import useFormatNativeTokenAmount from '../../hooks/useFormatNativeTokenAmount'; import { SubstrateTxFactory } from '../../hooks/useSubstrateTx'; import { GetSuccessMessageFn, StakingRewardsDestination } from '../../types'; diff --git a/apps/tangle-dapp/src/data/staking/useUnbondTx.ts b/apps/tangle-dapp/src/data/staking/useUnbondTx.ts index 1735e2f649..d0e675c81d 100644 --- a/apps/tangle-dapp/src/data/staking/useUnbondTx.ts +++ b/apps/tangle-dapp/src/data/staking/useUnbondTx.ts @@ -4,7 +4,7 @@ import { useCallback } from 'react'; import { TxName } from '../../constants'; import { PrecompileAddress } from '../../constants/evmPrecompiles'; import useAgnosticTx from '../../hooks/useAgnosticTx'; -import { EvmTxFactory } from '../../hooks/useEvmPrecompileAbiCall'; +import { EvmTxFactory } from '../../hooks/useEvmPrecompileCall'; import useFormatNativeTokenAmount from '../../hooks/useFormatNativeTokenAmount'; import { SubstrateTxFactory } from '../../hooks/useSubstrateTx'; import { GetSuccessMessageFn } from '../../types'; diff --git a/apps/tangle-dapp/src/data/staking/useUpdateNominatorTx.ts b/apps/tangle-dapp/src/data/staking/useUpdateNominatorTx.ts index feb72e9430..e8625af055 100644 --- a/apps/tangle-dapp/src/data/staking/useUpdateNominatorTx.ts +++ b/apps/tangle-dapp/src/data/staking/useUpdateNominatorTx.ts @@ -7,10 +7,7 @@ import { useCallback } from 'react'; import { TxName } from '../../constants'; import { PrecompileAddress } from '../../constants/evmPrecompiles'; import useAgnosticTx from '../../hooks/useAgnosticTx'; -import { - AbiBatchCall, - EvmTxFactory, -} from '../../hooks/useEvmPrecompileAbiCall'; +import { AbiBatchCall, EvmTxFactory } from '../../hooks/useEvmPrecompileCall'; import { SubstrateTxFactory } from '../../hooks/useSubstrateTx'; import optimizeTxBatch from '../../utils/optimizeTxBatch'; import createEvmBatchCallArgs from '../../utils/staking/createEvmBatchCallArgs'; diff --git a/apps/tangle-dapp/src/data/staking/useWithdrawUnbondedTx.ts b/apps/tangle-dapp/src/data/staking/useWithdrawUnbondedTx.ts index 51e32d0ace..1c6b6534f7 100644 --- a/apps/tangle-dapp/src/data/staking/useWithdrawUnbondedTx.ts +++ b/apps/tangle-dapp/src/data/staking/useWithdrawUnbondedTx.ts @@ -4,7 +4,7 @@ import { useCallback } from 'react'; import { TxName } from '../../constants'; import { PrecompileAddress } from '../../constants/evmPrecompiles'; import useAgnosticTx from '../../hooks/useAgnosticTx'; -import { EvmTxFactory } from '../../hooks/useEvmPrecompileAbiCall'; +import { EvmTxFactory } from '../../hooks/useEvmPrecompileCall'; import useFormatNativeTokenAmount from '../../hooks/useFormatNativeTokenAmount'; import { SubstrateTxFactory } from '../../hooks/useSubstrateTx'; import { GetSuccessMessageFn } from '../../types'; diff --git a/apps/tangle-dapp/src/hooks/useAgnosticTx.ts b/apps/tangle-dapp/src/hooks/useAgnosticTx.ts index f7ea7833ba..3785ee28c4 100644 --- a/apps/tangle-dapp/src/hooks/useAgnosticTx.ts +++ b/apps/tangle-dapp/src/hooks/useAgnosticTx.ts @@ -6,10 +6,10 @@ import { TxName } from '../constants'; import { GetSuccessMessageFn } from '../types'; import useActiveAccountAddress from './useActiveAccountAddress'; import useAgnosticAccountInfo from './useAgnosticAccountInfo'; -import useEvmPrecompileAbiCall, { +import useEvmPrecompileCall, { AbiCall, EvmTxFactory, -} from './useEvmPrecompileAbiCall'; +} from './useEvmPrecompileCall'; import useSubstrateTx, { SubstrateTxFactory, TxStatus } from './useSubstrateTx'; import useTxNotification from './useTxNotification'; import { AbiFunction } from 'viem'; @@ -94,7 +94,7 @@ function useAgnosticTx< reset: evmReset, txHash: evmTxHash, successMessage: evmSuccessMessage, - } = useEvmPrecompileAbiCall(abi, precompileAddress, evmTxFactory); + } = useEvmPrecompileCall(abi, precompileAddress, evmTxFactory); const { notifyProcessing, notifySuccess, notifyError } = useTxNotification(); diff --git a/apps/tangle-dapp/src/hooks/useEvmGasEstimate.ts b/apps/tangle-dapp/src/hooks/useEvmGasEstimate.ts new file mode 100644 index 0000000000..05b1f39247 --- /dev/null +++ b/apps/tangle-dapp/src/hooks/useEvmGasEstimate.ts @@ -0,0 +1,37 @@ +import { useCallback } from 'react'; +import useViemPublicClient from './useViemPublicClient'; +import { EvmAddress } from '@webb-tools/webb-ui-components/types/address'; +import { Hex } from 'viem'; +import useAgnosticAccountInfo from './useAgnosticAccountInfo'; + +/** + * Add a buffer to the gas estimate to ensure the + * transaction is successful. + */ +const BUFFER = BigInt(11); // +10% (1.1) + +const useEvmGasEstimate = () => { + const { evmAddress } = useAgnosticAccountInfo(); + const viemPublicClient = useViemPublicClient(); + + const estimateGas = useCallback( + async (contractAddress: EvmAddress, callData: Hex) => { + if (viemPublicClient === null || evmAddress === null) { + return null; + } + + const gasEstimate = await viemPublicClient.estimateContractGas({ + account: evmAddress, + to: contractAddress, + data: callData, + }); + + return (gasEstimate * BUFFER) / BigInt(10); + }, + [evmAddress, viemPublicClient], + ); + + return estimateGas; +}; + +export default useEvmGasEstimate; diff --git a/apps/tangle-dapp/src/hooks/useEvmPrecompileAbiCall.ts b/apps/tangle-dapp/src/hooks/useEvmPrecompileCall.ts similarity index 98% rename from apps/tangle-dapp/src/hooks/useEvmPrecompileAbiCall.ts rename to apps/tangle-dapp/src/hooks/useEvmPrecompileCall.ts index b970f33e96..7c021e3670 100644 --- a/apps/tangle-dapp/src/hooks/useEvmPrecompileAbiCall.ts +++ b/apps/tangle-dapp/src/hooks/useEvmPrecompileCall.ts @@ -54,7 +54,7 @@ export type EvmTxFactory< * This is used for performing actions from EVM accounts. Substrate accounts * should use `useSubstrateTx` for transactions instead, or `useApiRx` for queries. */ -function useEvmPrecompileAbiCall< +function useEvmPrecompileCall< Abi extends AbiFunction[], FunctionName extends ExtractAbiFunctionNames, Context = void, @@ -170,4 +170,4 @@ function useEvmPrecompileAbiCall< }; } -export default useEvmPrecompileAbiCall; +export default useEvmPrecompileCall; diff --git a/apps/tangle-dapp/src/hooks/useEvmPrecompileFee.ts b/apps/tangle-dapp/src/hooks/useEvmPrecompileFee.ts index e92cf1d08f..37a9125195 100644 --- a/apps/tangle-dapp/src/hooks/useEvmPrecompileFee.ts +++ b/apps/tangle-dapp/src/hooks/useEvmPrecompileFee.ts @@ -6,7 +6,7 @@ import { PrecompileAddress, } from '../constants/evmPrecompiles'; import useEvmAddress20 from './useEvmAddress'; -import { AbiCall } from './useEvmPrecompileAbiCall'; +import { AbiCall } from './useEvmPrecompileCall'; import useViemPublicClient from './useViemPublicClient'; import { AbiFunction } from 'viem'; diff --git a/apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts b/apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts index 717d15e01d..a4a6a85bda 100644 --- a/apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts +++ b/apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts @@ -1,3 +1,235 @@ -const useEvmTxRelayer = () => {}; +import useAgnosticAccountInfo from './useAgnosticAccountInfo'; +import axios, { AxiosResponse } from 'axios'; +import useNetworkStore from '@webb-tools/tangle-shared-ui/context/useNetworkStore'; +import { useCallback } from 'react'; +import { + AbiFunction, + encodeFunctionData, + Hash, + Hex, + parseSignature, +} from 'viem'; +import { + ExtractAbiFunctionNames, + FindAbiArgsOf, + PrecompileAddress, + ZERO_ADDRESS, +} from '../constants/evmPrecompiles'; +import { EvmAddress } from '@webb-tools/webb-ui-components/types/address'; +import { assertEvmAddress } from '@webb-tools/webb-ui-components'; +import assert from 'assert'; +import useViemWalletClient from './useViemWalletClient'; + +const PATHNAME = '/api/v1/relay'; +const DEADLINE_MINUTES = 10; + +const EIP712_TYPES = { + Permit: [ + { name: 'from', type: 'address' }, + { name: 'to', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'data', type: 'bytes' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + ], +}; + +/** + * @see https://github.com/tangle-network/txrelayer-blueprint/blob/main/API.md#request-body + */ +type RequestBody = { + /** + * The address initiating the transaction. + */ + from: EvmAddress; + + /** + * The target contract address. + */ + to: EvmAddress; + + /** + * Amount of native tokens to send (in hex) + */ + value: Hex; + + /** + * Encoded function call data. + */ + data: Hex; + + /** + * Maximum gas allowed for the transaction. + * + * Should be set appropriately for the transaction being executed. + */ + gaslimit: number; + + /** + * Timestamp when the signature expires (in hex). + * + * Should be a future timestamp to ensure the transaction doesn't expire. + */ + deadline: Hex; + + /** + * Recovery ID from the signature. + */ + v: number; + + /** + * R component of the signature. + */ + r: Hex; + + /** + * S component of the signature. + */ + s: Hex; +}; + +type SuccessResult = { + txHash: Hash; + simulatedOutcome: Hex; +}; + +type Response = + | ({ status: 'success' } & SuccessResult) + | { + status: 'failure'; + error: string; + + /** + * Additional error details if available. + */ + details?: string; + }; + +/** + * Obtain a function to send signed transactions to a relayer API. + * This acts as a subsidy for EVM accounts that do not have balances + * to pay for transaction fees. + */ +const useEvmTxRelayer = () => { + const { evmAddress } = useAgnosticAccountInfo(); + const { network } = useNetworkStore(); + const walletClient = useViemWalletClient(); + + const isReady = + evmAddress !== null && + network.evmTxRelayerEndpoint !== undefined && + walletClient !== null; + + const obtainSignatureParams = useCallback( + async (destination: EvmAddress, deadlineSeconds: number) => { + // TODO: Temp. assertion. + assert(walletClient !== null && evmAddress !== null); + + // EIP-712 domain, types, and message + const domain = { + // TODO: Name & version. + name: 'CallPermitExample', + version: '1', + chainId: network.evmChainId, + verifyingContract: destination, + }; + + const message = { + from: evmAddress, + to: destination, + value: 0, + // TODO: Nonce. + nonce: 0, + // For signing, the deadline should be in decimal form, not hex. + deadline: deadlineSeconds, + // TODO: Additional fields if the contract requires them. + }; + + const signature = await walletClient.signTypedData({ + account: evmAddress, + domain, + types: EIP712_TYPES, + primaryType: 'Permit', + message, + }); + + const { v, r, s } = parseSignature(signature); + + return { v, r, s }; + }, + [evmAddress, network.evmChainId, walletClient], + ); + + const relayEvmTx = useCallback( + async < + Abi extends AbiFunction[], + FunctionName extends ExtractAbiFunctionNames, + >( + abi: Abi, + precompileAddress: PrecompileAddress, + functionName: FunctionName, + args: FindAbiArgsOf, + ): Promise => { + assert( + evmAddress !== null && + network.evmTxRelayerEndpoint !== undefined && + walletClient !== null, + ); + + const currentTimeSeconds = Math.floor(Date.now() / 1000); + const deadlineSeconds = currentTimeSeconds + DEADLINE_MINUTES * 10; + const deadline = `0x${deadlineSeconds.toString(16)}` as const; + const destination = assertEvmAddress(precompileAddress); + + const { v, r, s } = await obtainSignatureParams( + destination, + deadlineSeconds, + ); + + const url = new URL(network.evmTxRelayerEndpoint); + + url.pathname = PATHNAME; + + const callData = encodeFunctionData({ + abi: abi satisfies AbiFunction[] as AbiFunction[], + functionName: functionName satisfies string as string, + args, + }); + + const response = await axios.post< + Response, + AxiosResponse, + RequestBody + >(url.toString(), { + from: evmAddress, + to: destination, + value: ZERO_ADDRESS, + // TODO: Should gas limit be estimated or set to zero since we're dealing with precompile calls here, not 'real' EVM? + gaslimit: 0, + data: callData, + deadline, + v, + r, + s, + }); + + if (response.data.status === 'success') { + return response.data; + } else { + return response.data.details !== undefined + ? new Error(`${response.data.error}; ${response.data.details}`) + : new Error(response.data.error); + } + }, + [ + evmAddress, + network.evmTxRelayerEndpoint, + obtainSignatureParams, + walletClient, + ], + ); + + return isReady ? relayEvmTx : null; +}; export default useEvmTxRelayer; diff --git a/apps/tangle-dapp/src/hooks/useViemWalletClient.ts b/apps/tangle-dapp/src/hooks/useViemWalletClient.ts new file mode 100644 index 0000000000..a7e7d9dbb7 --- /dev/null +++ b/apps/tangle-dapp/src/hooks/useViemWalletClient.ts @@ -0,0 +1,38 @@ +import useNetworkStore from '@webb-tools/tangle-shared-ui/context/useNetworkStore'; +import { useEffect, useState } from 'react'; +import { createWalletClient, http, WalletClient } from 'viem'; + +import createTangleViemChainFromNetwork from '../utils/evm/createTangleViemChainFromNetwork'; + +const useViemWalletClient = () => { + const [walletClient, setWalletClient] = useState(null); + const { network } = useNetworkStore(); + + // Update the wallet client when the network changes. + useEffect(() => { + if ( + network.evmChainId === undefined || + network.httpRpcEndpoint === undefined + ) { + return; + } + + const chain = createTangleViemChainFromNetwork({ + ...network, + evmChainId: network.evmChainId, + httpRpcEndpoint: network.httpRpcEndpoint, + }); + + const newWalletClient = createWalletClient({ + chain, + // TODO: Try with custom(window.ethereum). + transport: http(), + }); + + setWalletClient(newWalletClient); + }, [network]); + + return walletClient; +}; + +export default useViemWalletClient; diff --git a/apps/tangle-dapp/src/utils/staking/createEvmBatchCall.ts b/apps/tangle-dapp/src/utils/staking/createEvmBatchCall.ts index f262dd3a36..efbca7a43c 100644 --- a/apps/tangle-dapp/src/utils/staking/createEvmBatchCall.ts +++ b/apps/tangle-dapp/src/utils/staking/createEvmBatchCall.ts @@ -5,7 +5,7 @@ import { FindAbiArgsOf, PrecompileAddress, } from '../../constants/evmPrecompiles'; -import { AbiBatchCall } from '../../hooks/useEvmPrecompileAbiCall'; +import { AbiBatchCall } from '../../hooks/useEvmPrecompileCall'; import { assertEvmAddress } from '@webb-tools/webb-ui-components'; const createEvmBatchCall = < diff --git a/apps/tangle-dapp/src/utils/staking/createEvmBatchCallArgs.ts b/apps/tangle-dapp/src/utils/staking/createEvmBatchCallArgs.ts index 3d2e05c8d5..01ccbfc82f 100644 --- a/apps/tangle-dapp/src/utils/staking/createEvmBatchCallArgs.ts +++ b/apps/tangle-dapp/src/utils/staking/createEvmBatchCallArgs.ts @@ -1,5 +1,5 @@ import { EvmAddress } from '@webb-tools/webb-ui-components/types/address'; -import { AbiBatchCall } from '../../hooks/useEvmPrecompileAbiCall'; +import { AbiBatchCall } from '../../hooks/useEvmPrecompileCall'; import { Hex } from 'viem'; type AbiBatchCallArgs = [EvmAddress[], bigint[], Hex[], bigint[]]; diff --git a/libs/tangle-shared-ui/src/hooks/useProvider.ts b/libs/tangle-shared-ui/src/hooks/useProvider.ts index cea797ffcd..50cdafcffd 100644 --- a/libs/tangle-shared-ui/src/hooks/useProvider.ts +++ b/libs/tangle-shared-ui/src/hooks/useProvider.ts @@ -172,6 +172,7 @@ const useProvider = () => { const isConnected = activeAccount !== null; + // TODO: Debugging. useEffect(() => { console.debug('IS CONNECTING', isConnecting); console.debug('ACCOUNT', activeAccount); diff --git a/libs/webb-ui-components/src/constants/networks.ts b/libs/webb-ui-components/src/constants/networks.ts index 77b0b005ea..2d64d7faea 100644 --- a/libs/webb-ui-components/src/constants/networks.ts +++ b/libs/webb-ui-components/src/constants/networks.ts @@ -44,6 +44,7 @@ export type Network = { nativeExplorerUrl?: string; evmExplorerUrl?: string; avatar?: string; + evmTxRelayerEndpoint?: string; /** * The Web Socket RPC endpoint of the network. @@ -108,6 +109,7 @@ export const TANGLE_LOCAL_DEV_NETWORK = { httpRpcEndpoint: TANGLE_LOCAL_HTTP_RPC_ENDPOINT, polkadotJsDashboardUrl: TANGLE_LOCAL_POLKADOT_JS_DASHBOARD_URL, ss58Prefix: 42, + evmTxRelayerEndpoint: 'http://localhost:4500', } as const satisfies Network; export const TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK = { From a582e0a6eda86477e75f04895bffd6a28ef304d4 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Sun, 26 Jan 2025 22:40:18 -0500 Subject: [PATCH 04/28] refactor(tangle-dapp): Move things around a bit --- .../stakeAndUnstake/LsStakeCard.tsx | 2 +- .../stakeAndUnstake/LsUnstakeCard.tsx | 2 +- .../src/components/UnbondingStatsItem.tsx | 2 +- .../src/components/account/AccountAddress.tsx | 3 +-- .../src/components/account/Actions.tsx | 5 +---- .../account/WithdrawEvmBalanceAction.tsx | 2 +- .../BalancesTableContainer/BalanceCell.tsx | 2 +- apps/tangle-dapp/src/containers/DebugMetrics.tsx | 7 ------- .../DelegateTxContainer/BondTokens.tsx | 2 +- .../DelegateTxContainer/DelegateTxContainer.tsx | 2 +- .../src/containers/NominatorStatsContainer.tsx | 2 +- .../src/containers/TransferTxModal.tsx | 2 +- .../src/containers/VaultsAndBalancesTable.tsx | 2 +- .../src/containers/restaking/OperatorsTable.tsx | 3 +-- .../containers/restaking/UnstakeRequestTable.tsx | 2 +- .../src/containers/restaking/WithdrawModal.tsx | 2 +- .../restaking/WithdrawRequestTable.tsx | 2 +- .../src/data/balances/usePendingEvmBalance.ts | 5 ++--- .../src/data/bridge/useBridgeEvmBalances.ts | 4 ++-- .../src/data/claims/useAirdropEligibility.ts | 2 +- .../src/data/restake/useRestakeApi.ts | 2 +- .../src/data/restake/useRestakeAsset.ts | 2 +- apps/tangle-dapp/src/hooks/useAgnosticTx.ts | 4 ++-- apps/tangle-dapp/src/hooks/useEvmAddress.ts | 3 +-- apps/tangle-dapp/src/hooks/useEvmGasEstimate.ts | 4 ++-- .../tangle-dapp/src/hooks/useEvmPrecompileFee.ts | 2 +- apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts | 4 ++-- apps/tangle-dapp/src/hooks/useSubstrateTx.ts | 4 ++-- apps/tangle-dapp/src/hooks/useTxNotification.tsx | 2 +- .../tangle-dapp/src/hooks/useViemWalletClient.ts | 3 +-- .../src/pages/bridge/BridgeContainer.tsx | 2 +- .../src/pages/claim/EligibleSection.tsx | 2 +- .../src/pages/claim/NotEligibleSection.tsx | 2 +- apps/tangle-dapp/src/pages/claim/index.tsx | 2 +- .../src/pages/restake/delegate/index.tsx | 2 +- .../src/pages/restake/deposit/DepositForm.tsx | 2 +- .../src/hooks/useActiveAccountAddress.ts | 2 +- .../src/hooks/useAgnosticAccountInfo.ts | 0 .../src/hooks}/useTangleEvmErc20Balances.ts | 16 ++++++++-------- .../src/hooks/useViemPublicClient.ts | 4 ++-- .../src/queries/restake/assetDetails.ts | 11 ++--------- .../src/utils/convertDecimalToBn.ts | 0 .../utils}/createTangleViemChainFromNetwork.ts | 2 +- .../src/utils/fetchErc20TokenBalance.ts | 0 44 files changed, 55 insertions(+), 77 deletions(-) rename {apps/tangle-dapp => libs/tangle-shared-ui}/src/hooks/useActiveAccountAddress.ts (91%) rename {apps/tangle-dapp => libs/tangle-shared-ui}/src/hooks/useAgnosticAccountInfo.ts (100%) rename {apps/tangle-dapp/src/data/restake => libs/tangle-shared-ui/src/hooks}/useTangleEvmErc20Balances.ts (83%) rename {apps/tangle-dapp => libs/tangle-shared-ui}/src/hooks/useViemPublicClient.ts (82%) rename {apps/tangle-dapp => libs/tangle-shared-ui}/src/utils/convertDecimalToBn.ts (100%) rename {apps/tangle-dapp/src/utils/evm => libs/tangle-shared-ui/src/utils}/createTangleViemChainFromNetwork.ts (95%) rename {apps/tangle-dapp => libs/tangle-shared-ui}/src/utils/fetchErc20TokenBalance.ts (100%) diff --git a/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx b/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx index fe7077ac8f..fd41620e3b 100644 --- a/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx +++ b/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx @@ -18,7 +18,6 @@ import useLsExchangeRate from '../../../data/liquidStaking/useLsExchangeRate'; import useAssetAccounts from '../../../data/liquidStaking/useAssetAccounts'; import useLsPools from '../../../data/liquidStaking/useLsPools'; import { useLsStore } from '../../../data/liquidStaking/useLsStore'; -import useActiveAccountAddress from '../../../hooks/useActiveAccountAddress'; import { TxStatus } from '../../../hooks/useSubstrateTx'; import getLsProtocolDef from '../../../utils/liquidStaking/getLsProtocolDef'; import DetailsContainer from '../../DetailsContainer'; @@ -30,6 +29,7 @@ import useLsChangeNetwork from './useLsChangeNetwork'; import ListModal from '@webb-tools/tangle-shared-ui/components/ListModal'; import LstListItem from '../LstListItem'; import filterBy from '../../../utils/filterBy'; +import useActiveAccountAddress from '@webb-tools/tangle-shared-ui/hooks/useActiveAccountAddress'; const LsStakeCard: FC = () => { const lsPools = useLsPools(); diff --git a/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx b/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx index d1a03c41da..d7d775afed 100644 --- a/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx +++ b/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx @@ -17,7 +17,6 @@ import useLsPoolUnbondTx from '../../../data/liquidStaking/tangle/useLsPoolUnbon import useLsExchangeRate from '../../../data/liquidStaking/useLsExchangeRate'; import useLsMyPools from '../../../data/liquidStaking/useLsMyPools'; import { useLsStore } from '../../../data/liquidStaking/useLsStore'; -import useActiveAccountAddress from '../../../hooks/useActiveAccountAddress'; import useIsAccountConnected from '../../../hooks/useIsAccountConnected'; import { TxStatus } from '../../../hooks/useSubstrateTx'; import getLsProtocolDef from '../../../utils/liquidStaking/getLsProtocolDef'; @@ -28,6 +27,7 @@ import UnstakePeriodDetailItem from './UnstakePeriodDetailItem'; import useLsChangeNetwork from './useLsChangeNetwork'; import ListModal from '@webb-tools/tangle-shared-ui/components/ListModal'; import LstListItem from '../LstListItem'; +import useActiveAccountAddress from '@webb-tools/tangle-shared-ui/hooks/useActiveAccountAddress'; const LsUnstakeCard: FC = () => { const isAccountConnected = useIsAccountConnected(); diff --git a/apps/tangle-dapp/src/components/UnbondingStatsItem.tsx b/apps/tangle-dapp/src/components/UnbondingStatsItem.tsx index b43bed3dda..d6cdc309c6 100644 --- a/apps/tangle-dapp/src/components/UnbondingStatsItem.tsx +++ b/apps/tangle-dapp/src/components/UnbondingStatsItem.tsx @@ -4,11 +4,11 @@ import { EMPTY_VALUE_PLACEHOLDER } from '@webb-tools/webb-ui-components/constant import addCommasToNumber from '@webb-tools/webb-ui-components/utils/addCommasToNumber'; import { type FC, useMemo } from 'react'; -import useActiveAccountAddress from '../hooks/useActiveAccountAddress'; import useUnbondingAmount from '../data/nomination/useUnbondingAmount'; import useUnbonding from '../data/staking/useUnbonding'; import formatTangleBalance from '../utils/formatTangleBalance'; import { NominatorStatsItem } from './NominatorStatsItem'; +import useActiveAccountAddress from '@webb-tools/tangle-shared-ui/hooks/useActiveAccountAddress'; const UnbondingStatsItem: FC = () => { const activeAccountAddress = useActiveAccountAddress(); diff --git a/apps/tangle-dapp/src/components/account/AccountAddress.tsx b/apps/tangle-dapp/src/components/account/AccountAddress.tsx index 674bd6d06d..b4e8bcc2e1 100644 --- a/apps/tangle-dapp/src/components/account/AccountAddress.tsx +++ b/apps/tangle-dapp/src/components/account/AccountAddress.tsx @@ -1,6 +1,7 @@ import { isEthereumAddress } from '@polkadot/util-crypto'; import { LoopRightFillIcon } from '@webb-tools/icons'; import useNetworkStore from '@webb-tools/tangle-shared-ui/context/useNetworkStore'; +import useActiveAccountAddress from '@webb-tools/tangle-shared-ui/hooks/useActiveAccountAddress'; import { CopyWithTooltip, shortenHex, @@ -18,8 +19,6 @@ import { shortenString } from '@webb-tools/webb-ui-components/utils/shortenStrin import type { FC } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react'; -import useActiveAccountAddress from '../../hooks/useActiveAccountAddress'; - const AccountAddress: FC = () => { const { network } = useNetworkStore(); const activeAccountAddress = useActiveAccountAddress(); diff --git a/apps/tangle-dapp/src/components/account/Actions.tsx b/apps/tangle-dapp/src/components/account/Actions.tsx index 868a0262f3..b1c06ba5ca 100644 --- a/apps/tangle-dapp/src/components/account/Actions.tsx +++ b/apps/tangle-dapp/src/components/account/Actions.tsx @@ -6,19 +6,16 @@ import TransferTxModal from '../../containers/TransferTxModal'; import useBalances from '../../data/balances/useBalances'; import useVestingInfo from '../../data/vesting/useVestingInfo'; import useVestTx from '../../data/vesting/useVestTx'; -import useActiveAccountAddress from '../../hooks/useActiveAccountAddress'; import { TxStatus } from '../../hooks/useSubstrateTx'; import formatTangleBalance from '../../utils/formatTangleBalance'; import ActionItem from './ActionItem'; import WithdrawEvmBalanceAction from './WithdrawEvmBalanceAction'; +import useActiveAccountAddress from '@webb-tools/tangle-shared-ui/hooks/useActiveAccountAddress'; const Actions: FC = () => { const { nativeTokenSymbol } = useNetworkStore(); - const { execute: executeVestTx, status: vestTxStatus } = useVestTx(); - const activeAccountAddress = useActiveAccountAddress(); - const { transferable: transferableBalance } = useBalances(); const [isTransferModalOpen, setIsTransferModalOpen] = useState(false); diff --git a/apps/tangle-dapp/src/components/account/WithdrawEvmBalanceAction.tsx b/apps/tangle-dapp/src/components/account/WithdrawEvmBalanceAction.tsx index 3a647bc0c2..6538d58791 100644 --- a/apps/tangle-dapp/src/components/account/WithdrawEvmBalanceAction.tsx +++ b/apps/tangle-dapp/src/components/account/WithdrawEvmBalanceAction.tsx @@ -7,10 +7,10 @@ import { FC, useCallback, useMemo } from 'react'; import useEvmBalanceWithdrawTx from '../../data/balances/useEvmBalanceWithdrawTx'; import usePendingEvmBalance from '../../data/balances/usePendingEvmBalance'; -import useAgnosticAccountInfo from '../../hooks/useAgnosticAccountInfo'; import { TxStatus } from '../../hooks/useSubstrateTx'; import formatTangleBalance from '../../utils/formatTangleBalance'; import ActionItem from './ActionItem'; +import useAgnosticAccountInfo from '@webb-tools/tangle-shared-ui/hooks/useAgnosticAccountInfo'; const WithdrawEvmBalanceAction: FC = () => { const { nativeTokenSymbol } = useNetworkStore(); diff --git a/apps/tangle-dapp/src/containers/BalancesTableContainer/BalanceCell.tsx b/apps/tangle-dapp/src/containers/BalancesTableContainer/BalanceCell.tsx index 180fc5135f..efe96e4944 100644 --- a/apps/tangle-dapp/src/containers/BalancesTableContainer/BalanceCell.tsx +++ b/apps/tangle-dapp/src/containers/BalancesTableContainer/BalanceCell.tsx @@ -12,7 +12,7 @@ import { import { EMPTY_VALUE_PLACEHOLDER } from '@webb-tools/webb-ui-components/constants'; import { FC, ReactNode } from 'react'; -import useActiveAccountAddress from '../../hooks/useActiveAccountAddress'; +import useActiveAccountAddress from '@webb-tools/tangle-shared-ui/hooks/useActiveAccountAddress'; import formatTangleBalance from '../../utils/formatTangleBalance'; const BalanceCell: FC<{ diff --git a/apps/tangle-dapp/src/containers/DebugMetrics.tsx b/apps/tangle-dapp/src/containers/DebugMetrics.tsx index 5c2aeb22f0..4627ed0547 100644 --- a/apps/tangle-dapp/src/containers/DebugMetrics.tsx +++ b/apps/tangle-dapp/src/containers/DebugMetrics.tsx @@ -7,7 +7,6 @@ import { } from '@webb-tools/tangle-shared-ui/utils/polkadot/api'; import { SkeletonLoader, Typography } from '@webb-tools/webb-ui-components'; import { FC, useCallback, useEffect, useState } from 'react'; -import useProvider from '@webb-tools/tangle-shared-ui/hooks/useProvider'; import useDebugMetricsStore from '../context/useDebugMetricsStore'; @@ -59,8 +58,6 @@ const DebugMetrics: FC = () => { (apiRx?.stats?.active.subscriptions ?? 0) + subscriptionCount; - const session = useProvider(); - // Manually trigger a re-render every second, since the stats // are not automatically updated. useEffect(() => { @@ -98,10 +95,6 @@ const DebugMetrics: FC = () => { warnAt={1} isApiLoading={isApiLoading} /> - - ); diff --git a/apps/tangle-dapp/src/containers/DelegateTxContainer/BondTokens.tsx b/apps/tangle-dapp/src/containers/DelegateTxContainer/BondTokens.tsx index f6714ba6aa..d7826b7f4b 100644 --- a/apps/tangle-dapp/src/containers/DelegateTxContainer/BondTokens.tsx +++ b/apps/tangle-dapp/src/containers/DelegateTxContainer/BondTokens.tsx @@ -14,7 +14,7 @@ import { STAKING_PAYEE_VALUE_TO_TEXT_MAP, } from '../../constants'; import useBalances from '../../data/balances/useBalances'; -import useActiveAccountAddress from '../../hooks/useActiveAccountAddress'; +import useActiveAccountAddress from '@webb-tools/tangle-shared-ui/hooks/useActiveAccountAddress'; import { StakingRewardsDestinationDisplayText } from '../../types/index'; import { BondTokensProps } from './types'; diff --git a/apps/tangle-dapp/src/containers/DelegateTxContainer/DelegateTxContainer.tsx b/apps/tangle-dapp/src/containers/DelegateTxContainer/DelegateTxContainer.tsx index 848ac3a7e0..b96ca99d39 100644 --- a/apps/tangle-dapp/src/containers/DelegateTxContainer/DelegateTxContainer.tsx +++ b/apps/tangle-dapp/src/containers/DelegateTxContainer/DelegateTxContainer.tsx @@ -20,7 +20,7 @@ import useStakingRewardsDestination from '../../data/nomination/useStakingReward import useIsBondedOrNominating from '../../data/staking/useIsBondedOrNominating'; import useSetupNominatorTx from '../../data/staking/useSetupNominatorTx'; import useUpdateNominatorTx from '../../data/staking/useUpdateNominatorTx'; -import useActiveAccountAddress from '../../hooks/useActiveAccountAddress'; +import useActiveAccountAddress from '@webb-tools/tangle-shared-ui/hooks/useActiveAccountAddress'; import useMaxNominationQuota from '../../hooks/useMaxNominationQuota'; import { TxStatus } from '../../hooks/useSubstrateTx'; import { StakingRewardsDestination } from '../../types'; diff --git a/apps/tangle-dapp/src/containers/NominatorStatsContainer.tsx b/apps/tangle-dapp/src/containers/NominatorStatsContainer.tsx index 4e66952ccb..d688fcf752 100644 --- a/apps/tangle-dapp/src/containers/NominatorStatsContainer.tsx +++ b/apps/tangle-dapp/src/containers/NominatorStatsContainer.tsx @@ -21,7 +21,6 @@ import useBalances from '../data/balances/useBalances'; import useTotalPayoutRewards from '../data/nomination/useTotalPayoutRewards'; import useIsBondedOrNominating from '../data/staking/useIsBondedOrNominating'; import useStakingLedger from '../data/staking/useStakingLedger'; -import useActiveAccountAddress from '../hooks/useActiveAccountAddress'; import useNetworkFeatures from '../hooks/useNetworkFeatures'; import { NetworkFeature, PagePath } from '../types'; import formatTangleBalance from '../utils/formatTangleBalance'; @@ -31,6 +30,7 @@ import BondMoreTxModal from './BondMoreTxModal'; import RebondTxModal from './RebondTxModal'; import WithdrawUnbondedTxModal from './WithdrawUnbondedTxModal'; import UnbondTxModal from './UnbondTxModal'; +import useActiveAccountAddress from '@webb-tools/tangle-shared-ui/hooks/useActiveAccountAddress'; const NominatorStatsContainer: FC = () => { const [isDelegateModalOpen, setIsDelegateModalOpen] = useState(false); diff --git a/apps/tangle-dapp/src/containers/TransferTxModal.tsx b/apps/tangle-dapp/src/containers/TransferTxModal.tsx index 1d1642afa4..0a05c2ea7c 100644 --- a/apps/tangle-dapp/src/containers/TransferTxModal.tsx +++ b/apps/tangle-dapp/src/containers/TransferTxModal.tsx @@ -23,10 +23,10 @@ import AmountInput from '../components/AmountInput'; import useBalances from '../data/balances/useBalances'; import useExistentialDeposit from '../data/balances/useExistentialDeposit'; import useTransferTx from '../data/balances/useTransferTx'; -import useActiveAccountAddress from '../hooks/useActiveAccountAddress'; import { TxStatus } from '../hooks/useSubstrateTx'; import formatTangleBalance from '../utils/formatTangleBalance'; import { AddressType } from '../constants'; +import useActiveAccountAddress from '@webb-tools/tangle-shared-ui/hooks/useActiveAccountAddress'; type Props = { isModalOpen: boolean; diff --git a/apps/tangle-dapp/src/containers/VaultsAndBalancesTable.tsx b/apps/tangle-dapp/src/containers/VaultsAndBalancesTable.tsx index 526e901f9d..27ff23206c 100644 --- a/apps/tangle-dapp/src/containers/VaultsAndBalancesTable.tsx +++ b/apps/tangle-dapp/src/containers/VaultsAndBalancesTable.tsx @@ -40,13 +40,13 @@ import { Link } from 'react-router'; import StatItem from '../components/StatItem'; import { HeaderCell } from '../components/tableCells'; import useRestakeRewardConfig from '../data/restake/useRestakeRewardConfig'; -import useTangleEvmErc20Balances from '../data/restake/useTangleEvmErc20Balances'; import useIsAccountConnected from '../hooks/useIsAccountConnected'; import { PagePath, QueryParamKey } from '../types'; import sortByBn from '../utils/sortByBn'; import sortByLocaleCompare from '../utils/sortByLocaleCompare'; import useRestakeBalances from '@webb-tools/tangle-shared-ui/data/restake/useRestakeBalances'; import calculateBnRatio from '../utils/calculateBnRatio'; +import useTangleEvmErc20Balances from '@webb-tools/tangle-shared-ui/hooks/useTangleEvmErc20Balances'; type Row = { vaultId: number; diff --git a/apps/tangle-dapp/src/containers/restaking/OperatorsTable.tsx b/apps/tangle-dapp/src/containers/restaking/OperatorsTable.tsx index 1448031044..4b2bee5bd6 100644 --- a/apps/tangle-dapp/src/containers/restaking/OperatorsTable.tsx +++ b/apps/tangle-dapp/src/containers/restaking/OperatorsTable.tsx @@ -16,7 +16,6 @@ import { import { LinkProps } from 'react-router'; import { RestakeOperatorWrapper } from '../../components/tables/RestakeActionWrappers'; import useIdentities from '../../data/useIdentities'; -import useAgnosticAccountInfo from '../../hooks/useAgnosticAccountInfo'; import useIsAccountConnected from '../../hooks/useIsAccountConnected'; import JoinOperatorsModal from './JoinOperatorsModal'; import { @@ -24,6 +23,7 @@ import { ModalTrigger, } from '@webb-tools/webb-ui-components/components/Modal'; import { PropsWithChildren } from 'react'; +import useAgnosticAccountInfo from '@webb-tools/tangle-shared-ui/hooks/useAgnosticAccountInfo'; type OperatorUI = NonNullable< ComponentProps['data'] @@ -48,7 +48,6 @@ const OperatorsTable: FC = ({ const { isEvm } = useAgnosticAccountInfo(); const isAccountConnected = useIsAccountConnected(); - const { vaults } = useRestakeContext(); const { result: identities } = useIdentities( diff --git a/apps/tangle-dapp/src/containers/restaking/UnstakeRequestTable.tsx b/apps/tangle-dapp/src/containers/restaking/UnstakeRequestTable.tsx index 9e84b4d79e..edbf58877c 100644 --- a/apps/tangle-dapp/src/containers/restaking/UnstakeRequestTable.tsx +++ b/apps/tangle-dapp/src/containers/restaking/UnstakeRequestTable.tsx @@ -33,8 +33,8 @@ import { BN } from '@polkadot/util'; import { SubstrateAddress } from '@webb-tools/webb-ui-components/types/address'; import { RestakeAssetId } from '@webb-tools/tangle-shared-ui/types'; import useSessionDurationMs from '../../data/useSessionDurationMs'; -import { findErc20Token } from '../../data/restake/useTangleEvmErc20Balances'; import formatSessionDistance from '../../utils/formatSessionDistance'; +import { findErc20Token } from '@webb-tools/tangle-shared-ui/hooks/useTangleEvmErc20Balances'; export type UnstakeRequestTableRow = { amount: string; diff --git a/apps/tangle-dapp/src/containers/restaking/WithdrawModal.tsx b/apps/tangle-dapp/src/containers/restaking/WithdrawModal.tsx index e6d2d511f5..0991ca26dd 100644 --- a/apps/tangle-dapp/src/containers/restaking/WithdrawModal.tsx +++ b/apps/tangle-dapp/src/containers/restaking/WithdrawModal.tsx @@ -15,8 +15,8 @@ import { import { useMemo } from 'react'; import { formatUnits } from 'viem'; import LogoListItem from '../../components/Lists/LogoListItem'; -import { findErc20Token } from '../../data/restake/useTangleEvmErc20Balances'; import filterBy from '../../utils/filterBy'; +import { findErc20Token } from '@webb-tools/tangle-shared-ui/hooks/useTangleEvmErc20Balances'; type Props = { delegatorInfo: DelegatorInfo | null; diff --git a/apps/tangle-dapp/src/containers/restaking/WithdrawRequestTable.tsx b/apps/tangle-dapp/src/containers/restaking/WithdrawRequestTable.tsx index 6397736e4e..747692e02b 100644 --- a/apps/tangle-dapp/src/containers/restaking/WithdrawRequestTable.tsx +++ b/apps/tangle-dapp/src/containers/restaking/WithdrawRequestTable.tsx @@ -27,11 +27,11 @@ import { FC, useMemo } from 'react'; import TableCell from '../../components/restaking/TableCell'; import useRestakeConsts from '../../data/restake/useRestakeConsts'; import useRestakeCurrentRound from '../../data/restake/useRestakeCurrentRound'; -import { findErc20Token } from '../../data/restake/useTangleEvmErc20Balances'; import useSessionDurationMs from '../../data/useSessionDurationMs'; import { calculateTimeRemaining } from '../../pages/restake/utils'; import formatSessionDistance from '../../utils/formatSessionDistance'; import WithdrawRequestTableActions from './WithdrawRequestTableActions'; +import { findErc20Token } from '@webb-tools/tangle-shared-ui/hooks/useTangleEvmErc20Balances'; export type WithdrawRequestTableRow = { amount: string; diff --git a/apps/tangle-dapp/src/data/balances/usePendingEvmBalance.ts b/apps/tangle-dapp/src/data/balances/usePendingEvmBalance.ts index ddf6d36301..0b0610e6a0 100644 --- a/apps/tangle-dapp/src/data/balances/usePendingEvmBalance.ts +++ b/apps/tangle-dapp/src/data/balances/usePendingEvmBalance.ts @@ -1,10 +1,9 @@ +import useAgnosticAccountInfo from '@webb-tools/tangle-shared-ui/hooks/useAgnosticAccountInfo'; import usePromise from '@webb-tools/tangle-shared-ui/hooks/usePromise'; +import useViemPublicClient from '@webb-tools/tangle-shared-ui/hooks/useViemPublicClient'; import { toEvmAddress } from '@webb-tools/webb-ui-components'; import { useCallback, useMemo } from 'react'; -import useAgnosticAccountInfo from '../../hooks/useAgnosticAccountInfo'; -import useViemPublicClient from '../../hooks/useViemPublicClient'; - /** * See more here: * https://docs.tangle.tools/docs/use/addresses/#case-2-sending-from-evm-to-substrate diff --git a/apps/tangle-dapp/src/data/bridge/useBridgeEvmBalances.ts b/apps/tangle-dapp/src/data/bridge/useBridgeEvmBalances.ts index 5dbb16d199..d3b73ab2aa 100644 --- a/apps/tangle-dapp/src/data/bridge/useBridgeEvmBalances.ts +++ b/apps/tangle-dapp/src/data/bridge/useBridgeEvmBalances.ts @@ -12,8 +12,8 @@ import ensureError from '@webb-tools/tangle-shared-ui/utils/ensureError'; import { EvmAddress } from '@webb-tools/webb-ui-components/types/address'; import useEvmAddress20 from '../../hooks/useEvmAddress'; import { isSolanaAddress } from '@webb-tools/webb-ui-components'; -import fetchErc20TokenBalance from '../../utils/fetchErc20TokenBalance'; -import useViemPublicClient from '../../hooks/useViemPublicClient'; +import fetchErc20TokenBalance from '@webb-tools/tangle-shared-ui/utils/fetchErc20TokenBalance'; +import useViemPublicClient from '@webb-tools/tangle-shared-ui/hooks/useViemPublicClient'; export const useBridgeEvmBalances = ( sourceChainId: number, diff --git a/apps/tangle-dapp/src/data/claims/useAirdropEligibility.ts b/apps/tangle-dapp/src/data/claims/useAirdropEligibility.ts index 7122a5c468..40daa15c56 100644 --- a/apps/tangle-dapp/src/data/claims/useAirdropEligibility.ts +++ b/apps/tangle-dapp/src/data/claims/useAirdropEligibility.ts @@ -4,7 +4,7 @@ import { isEthereumAddress } from '@polkadot/util-crypto'; import useApiRx from '@webb-tools/tangle-shared-ui/hooks/useApiRx'; import { useCallback, useEffect, useState } from 'react'; -import useActiveAccountAddress from '../../hooks/useActiveAccountAddress'; +import useActiveAccountAddress from '@webb-tools/tangle-shared-ui/hooks/useActiveAccountAddress'; const useAirdropEligibility = () => { const [isEligible, setIsEligible] = useState(null); diff --git a/apps/tangle-dapp/src/data/restake/useRestakeApi.ts b/apps/tangle-dapp/src/data/restake/useRestakeApi.ts index 5ec3b3f6af..fa6464eb58 100644 --- a/apps/tangle-dapp/src/data/restake/useRestakeApi.ts +++ b/apps/tangle-dapp/src/data/restake/useRestakeApi.ts @@ -13,7 +13,7 @@ import useSubstrateExplorerUrl from '@webb-tools/tangle-shared-ui/hooks/useSubst import { Hash } from 'viem'; import getWagmiConfig from '@webb-tools/dapp-config/wagmi-config'; import { TxName } from '../../constants'; -import useAgnosticAccountInfo from '../../hooks/useAgnosticAccountInfo'; +import useAgnosticAccountInfo from '@webb-tools/tangle-shared-ui/hooks/useAgnosticAccountInfo'; const useRestakeApi = () => { const { apiPromise } = usePolkadotApi(); diff --git a/apps/tangle-dapp/src/data/restake/useRestakeAsset.ts b/apps/tangle-dapp/src/data/restake/useRestakeAsset.ts index eed353c315..a31a13010f 100644 --- a/apps/tangle-dapp/src/data/restake/useRestakeAsset.ts +++ b/apps/tangle-dapp/src/data/restake/useRestakeAsset.ts @@ -2,9 +2,9 @@ import { useRestakeContext } from '@webb-tools/tangle-shared-ui/context/RestakeC import { RestakeAsset } from '@webb-tools/tangle-shared-ui/types/restake'; import { isEvmAddress } from '@webb-tools/webb-ui-components'; import { useMemo } from 'react'; -import useTangleEvmErc20Balances from './useTangleEvmErc20Balances'; import { BN, BN_ZERO } from '@polkadot/util'; import { RestakeAssetId } from '@webb-tools/tangle-shared-ui/types'; +import useTangleEvmErc20Balances from '@webb-tools/tangle-shared-ui/hooks/useTangleEvmErc20Balances'; const useRestakeAsset = (id: RestakeAssetId | null | undefined) => { const { vaults, balances } = useRestakeContext(); diff --git a/apps/tangle-dapp/src/hooks/useAgnosticTx.ts b/apps/tangle-dapp/src/hooks/useAgnosticTx.ts index 3785ee28c4..9afdd6daaa 100644 --- a/apps/tangle-dapp/src/hooks/useAgnosticTx.ts +++ b/apps/tangle-dapp/src/hooks/useAgnosticTx.ts @@ -4,8 +4,6 @@ import { useCallback, useEffect, useState } from 'react'; import { TxName } from '../constants'; import { GetSuccessMessageFn } from '../types'; -import useActiveAccountAddress from './useActiveAccountAddress'; -import useAgnosticAccountInfo from './useAgnosticAccountInfo'; import useEvmPrecompileCall, { AbiCall, EvmTxFactory, @@ -17,6 +15,8 @@ import { ExtractAbiFunctionNames, PrecompileAddress, } from '../constants/evmPrecompiles'; +import useActiveAccountAddress from '@webb-tools/tangle-shared-ui/hooks/useActiveAccountAddress'; +import useAgnosticAccountInfo from '@webb-tools/tangle-shared-ui/hooks/useAgnosticAccountInfo'; export type AgnosticTxOptions< Abi extends AbiFunction[], diff --git a/apps/tangle-dapp/src/hooks/useEvmAddress.ts b/apps/tangle-dapp/src/hooks/useEvmAddress.ts index 59505ab373..1d6291acca 100644 --- a/apps/tangle-dapp/src/hooks/useEvmAddress.ts +++ b/apps/tangle-dapp/src/hooks/useEvmAddress.ts @@ -1,9 +1,8 @@ +import useActiveAccountAddress from '@webb-tools/tangle-shared-ui/hooks/useActiveAccountAddress'; import { isEvmAddress, toEvmAddress } from '@webb-tools/webb-ui-components'; import { EvmAddress } from '@webb-tools/webb-ui-components/types/address'; import { useMemo } from 'react'; -import useActiveAccountAddress from './useActiveAccountAddress'; - /** * Obtain the EVM address of the active account, if any. * diff --git a/apps/tangle-dapp/src/hooks/useEvmGasEstimate.ts b/apps/tangle-dapp/src/hooks/useEvmGasEstimate.ts index 05b1f39247..7ce8e1fbae 100644 --- a/apps/tangle-dapp/src/hooks/useEvmGasEstimate.ts +++ b/apps/tangle-dapp/src/hooks/useEvmGasEstimate.ts @@ -1,8 +1,8 @@ import { useCallback } from 'react'; -import useViemPublicClient from './useViemPublicClient'; import { EvmAddress } from '@webb-tools/webb-ui-components/types/address'; import { Hex } from 'viem'; -import useAgnosticAccountInfo from './useAgnosticAccountInfo'; +import useAgnosticAccountInfo from '@webb-tools/tangle-shared-ui/hooks/useAgnosticAccountInfo'; +import useViemPublicClient from '@webb-tools/tangle-shared-ui/hooks/useViemPublicClient'; /** * Add a buffer to the gas estimate to ensure the diff --git a/apps/tangle-dapp/src/hooks/useEvmPrecompileFee.ts b/apps/tangle-dapp/src/hooks/useEvmPrecompileFee.ts index 37a9125195..ae8ceded47 100644 --- a/apps/tangle-dapp/src/hooks/useEvmPrecompileFee.ts +++ b/apps/tangle-dapp/src/hooks/useEvmPrecompileFee.ts @@ -7,8 +7,8 @@ import { } from '../constants/evmPrecompiles'; import useEvmAddress20 from './useEvmAddress'; import { AbiCall } from './useEvmPrecompileCall'; -import useViemPublicClient from './useViemPublicClient'; import { AbiFunction } from 'viem'; +import useViemPublicClient from '@webb-tools/tangle-shared-ui/hooks/useViemPublicClient'; export type QueryStatus = 'idle' | 'loading' | 'success' | 'error'; diff --git a/apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts b/apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts index a4a6a85bda..db95dbbe86 100644 --- a/apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts +++ b/apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts @@ -1,6 +1,4 @@ -import useAgnosticAccountInfo from './useAgnosticAccountInfo'; import axios, { AxiosResponse } from 'axios'; -import useNetworkStore from '@webb-tools/tangle-shared-ui/context/useNetworkStore'; import { useCallback } from 'react'; import { AbiFunction, @@ -19,6 +17,8 @@ import { EvmAddress } from '@webb-tools/webb-ui-components/types/address'; import { assertEvmAddress } from '@webb-tools/webb-ui-components'; import assert from 'assert'; import useViemWalletClient from './useViemWalletClient'; +import useAgnosticAccountInfo from '@webb-tools/tangle-shared-ui/hooks/useAgnosticAccountInfo'; +import useNetworkStore from '@webb-tools/tangle-shared-ui/context/useNetworkStore'; const PATHNAME = '/api/v1/relay'; const DEADLINE_MINUTES = 10; diff --git a/apps/tangle-dapp/src/hooks/useSubstrateTx.ts b/apps/tangle-dapp/src/hooks/useSubstrateTx.ts index 324b576460..2a81899e57 100644 --- a/apps/tangle-dapp/src/hooks/useSubstrateTx.ts +++ b/apps/tangle-dapp/src/hooks/useSubstrateTx.ts @@ -17,9 +17,9 @@ import { Hash } from 'viem'; import { TxName } from '../constants'; import { GetSuccessMessageFn } from '../types'; import extractErrorFromTxStatus from '../utils/extractErrorFromStatus'; -import useActiveAccountAddress from './useActiveAccountAddress'; -import useAgnosticAccountInfo from './useAgnosticAccountInfo'; import useTxNotification from './useTxNotification'; +import useAgnosticAccountInfo from '@webb-tools/tangle-shared-ui/hooks/useAgnosticAccountInfo'; +import useActiveAccountAddress from '@webb-tools/tangle-shared-ui/hooks/useActiveAccountAddress'; export enum TxStatus { NOT_YET_INITIATED, diff --git a/apps/tangle-dapp/src/hooks/useTxNotification.tsx b/apps/tangle-dapp/src/hooks/useTxNotification.tsx index 84a1b90ee2..19787c5c97 100644 --- a/apps/tangle-dapp/src/hooks/useTxNotification.tsx +++ b/apps/tangle-dapp/src/hooks/useTxNotification.tsx @@ -5,7 +5,7 @@ import { useSnackbar } from 'notistack'; import { useCallback } from 'react'; import { TxName } from '../constants'; -import useAgnosticAccountInfo from './useAgnosticAccountInfo'; +import useAgnosticAccountInfo from '@webb-tools/tangle-shared-ui/hooks/useAgnosticAccountInfo'; const SUCCESS_TIMEOUT = 10_000; diff --git a/apps/tangle-dapp/src/hooks/useViemWalletClient.ts b/apps/tangle-dapp/src/hooks/useViemWalletClient.ts index a7e7d9dbb7..6a585257aa 100644 --- a/apps/tangle-dapp/src/hooks/useViemWalletClient.ts +++ b/apps/tangle-dapp/src/hooks/useViemWalletClient.ts @@ -1,9 +1,8 @@ import useNetworkStore from '@webb-tools/tangle-shared-ui/context/useNetworkStore'; +import createTangleViemChainFromNetwork from '@webb-tools/tangle-shared-ui/utils/createTangleViemChainFromNetwork'; import { useEffect, useState } from 'react'; import { createWalletClient, http, WalletClient } from 'viem'; -import createTangleViemChainFromNetwork from '../utils/evm/createTangleViemChainFromNetwork'; - const useViemWalletClient = () => { const [walletClient, setWalletClient] = useState(null); const { network } = useNetworkStore(); diff --git a/apps/tangle-dapp/src/pages/bridge/BridgeContainer.tsx b/apps/tangle-dapp/src/pages/bridge/BridgeContainer.tsx index 9848196b13..80dc504d32 100644 --- a/apps/tangle-dapp/src/pages/bridge/BridgeContainer.tsx +++ b/apps/tangle-dapp/src/pages/bridge/BridgeContainer.tsx @@ -43,7 +43,6 @@ import { BridgeTokenWithBalance } from '@webb-tools/tangle-shared-ui/types'; import useBridgeRouterQuote, { RouterQuoteParams, } from '../../data/bridge/useBridgeRouterQuote'; -import convertDecimalToBn from '../../utils/convertDecimalToBn'; import formatTangleBalance from '../../utils/formatTangleBalance'; import { HyperlaneQuoteProps, @@ -53,6 +52,7 @@ import { RouterTransferProps } from '../../data/bridge/useRouterTransfer'; import ErrorMessage from '../../components/ErrorMessage'; import { WalletFillIcon } from '@webb-tools/icons'; import { AddressType } from '../../constants'; +import convertDecimalToBn from '@webb-tools/tangle-shared-ui/utils/convertDecimalToBn'; interface BridgeContainerProps { className?: string; diff --git a/apps/tangle-dapp/src/pages/claim/EligibleSection.tsx b/apps/tangle-dapp/src/pages/claim/EligibleSection.tsx index c5dc20cf84..d278beb93c 100644 --- a/apps/tangle-dapp/src/pages/claim/EligibleSection.tsx +++ b/apps/tangle-dapp/src/pages/claim/EligibleSection.tsx @@ -25,11 +25,11 @@ import { isHex } from 'viem'; import ClaimingAccountInput from '../../components/claims/ClaimingAccountInput'; import ClaimRecipientInput from '../../components/claims/ClaimRecipientInput'; -import useActiveAccountAddress from '../../hooks/useActiveAccountAddress'; import toAsciiHex from '../../utils/claims/toAsciiHex'; import formatTangleBalance from '../../utils/formatTangleBalance'; import getStatement, { Statement } from '../../utils/getStatement'; import type { ClaimInfoType } from './types'; +import useActiveAccountAddress from '@webb-tools/tangle-shared-ui/hooks/useActiveAccountAddress'; enum Step { INPUT_ADDRESS, diff --git a/apps/tangle-dapp/src/pages/claim/NotEligibleSection.tsx b/apps/tangle-dapp/src/pages/claim/NotEligibleSection.tsx index 659fdb6be1..b1c03f8fc4 100644 --- a/apps/tangle-dapp/src/pages/claim/NotEligibleSection.tsx +++ b/apps/tangle-dapp/src/pages/claim/NotEligibleSection.tsx @@ -3,7 +3,7 @@ import { Button } from '@webb-tools/webb-ui-components'; import type { FC } from 'react'; import ClaimingAccountInput from '../../components/claims/ClaimingAccountInput'; -import useActiveAccountAddress from '../../hooks/useActiveAccountAddress'; +import useActiveAccountAddress from '@webb-tools/tangle-shared-ui/hooks/useActiveAccountAddress'; const NotEligibleSection: FC = () => { const activeAccountAddress = useActiveAccountAddress(); diff --git a/apps/tangle-dapp/src/pages/claim/index.tsx b/apps/tangle-dapp/src/pages/claim/index.tsx index f44ad4f531..8620f9821f 100644 --- a/apps/tangle-dapp/src/pages/claim/index.tsx +++ b/apps/tangle-dapp/src/pages/claim/index.tsx @@ -13,7 +13,7 @@ import { Typography } from '@webb-tools/webb-ui-components/typography/Typography import { FC, useEffect, useMemo, useState } from 'react'; import { combineLatest, Subscription } from 'rxjs'; -import useActiveAccountAddress from '../../hooks/useActiveAccountAddress'; +import useActiveAccountAddress from '@webb-tools/tangle-shared-ui/hooks/useActiveAccountAddress'; import EligibleSection from './EligibleSection'; import NotEligibleSection from './NotEligibleSection'; import type { ClaimInfoType } from './types'; diff --git a/apps/tangle-dapp/src/pages/restake/delegate/index.tsx b/apps/tangle-dapp/src/pages/restake/delegate/index.tsx index 7ac78ec5a0..b09787956d 100644 --- a/apps/tangle-dapp/src/pages/restake/delegate/index.tsx +++ b/apps/tangle-dapp/src/pages/restake/delegate/index.tsx @@ -43,8 +43,8 @@ import { BN } from '@polkadot/util'; import useRestakeApi from '../../../data/restake/useRestakeApi'; import assertRestakeAssetId from '@webb-tools/tangle-shared-ui/utils/assertRestakeAssetId'; import { RestakeAsset } from '@webb-tools/tangle-shared-ui/types/restake'; -import { findErc20Token } from '../../../data/restake/useTangleEvmErc20Balances'; import useRestakeAsset from '../../../data/restake/useRestakeAsset'; +import { findErc20Token } from '@webb-tools/tangle-shared-ui/hooks/useTangleEvmErc20Balances'; type RestakeOperator = { accountId: SubstrateAddress; diff --git a/apps/tangle-dapp/src/pages/restake/deposit/DepositForm.tsx b/apps/tangle-dapp/src/pages/restake/deposit/DepositForm.tsx index 8768c4932a..45dcf95722 100644 --- a/apps/tangle-dapp/src/pages/restake/deposit/DepositForm.tsx +++ b/apps/tangle-dapp/src/pages/restake/deposit/DepositForm.tsx @@ -48,8 +48,8 @@ import { PresetTypedChainId } from '@webb-tools/dapp-types'; import useRestakeApi from '../../../data/restake/useRestakeApi'; import assert from 'assert'; import { RestakeAsset } from '@webb-tools/tangle-shared-ui/types/restake'; -import useTangleEvmErc20Balances from '../../../data/restake/useTangleEvmErc20Balances'; import useRestakeAsset from '../../../data/restake/useRestakeAsset'; +import useTangleEvmErc20Balances from '@webb-tools/tangle-shared-ui/hooks/useTangleEvmErc20Balances'; const getDefaultTypedChainId = ( activeTypedChainId: number | null, diff --git a/apps/tangle-dapp/src/hooks/useActiveAccountAddress.ts b/libs/tangle-shared-ui/src/hooks/useActiveAccountAddress.ts similarity index 91% rename from apps/tangle-dapp/src/hooks/useActiveAccountAddress.ts rename to libs/tangle-shared-ui/src/hooks/useActiveAccountAddress.ts index 62b17ccceb..7a7730fe39 100644 --- a/apps/tangle-dapp/src/hooks/useActiveAccountAddress.ts +++ b/libs/tangle-shared-ui/src/hooks/useActiveAccountAddress.ts @@ -1,5 +1,4 @@ import { useActiveAccount } from '@webb-tools/api-provider-environment/hooks/useActiveAccount'; -import useNetworkStore from '@webb-tools/tangle-shared-ui/context/useNetworkStore'; import { isEvmAddress, isSubstrateAddress, @@ -10,6 +9,7 @@ import { SubstrateAddress, } from '@webb-tools/webb-ui-components/types/address'; import assert from 'assert'; +import useNetworkStore from '../context/useNetworkStore'; const useActiveAccountAddress = (): SubstrateAddress | EvmAddress | null => { const { network } = useNetworkStore(); diff --git a/apps/tangle-dapp/src/hooks/useAgnosticAccountInfo.ts b/libs/tangle-shared-ui/src/hooks/useAgnosticAccountInfo.ts similarity index 100% rename from apps/tangle-dapp/src/hooks/useAgnosticAccountInfo.ts rename to libs/tangle-shared-ui/src/hooks/useAgnosticAccountInfo.ts diff --git a/apps/tangle-dapp/src/data/restake/useTangleEvmErc20Balances.ts b/libs/tangle-shared-ui/src/hooks/useTangleEvmErc20Balances.ts similarity index 83% rename from apps/tangle-dapp/src/data/restake/useTangleEvmErc20Balances.ts rename to libs/tangle-shared-ui/src/hooks/useTangleEvmErc20Balances.ts index 8611efe1dd..b4294cdea9 100644 --- a/apps/tangle-dapp/src/data/restake/useTangleEvmErc20Balances.ts +++ b/libs/tangle-shared-ui/src/hooks/useTangleEvmErc20Balances.ts @@ -1,13 +1,13 @@ import { BN } from '@polkadot/util'; import { assertEvmAddress } from '@webb-tools/webb-ui-components'; import { EvmAddress } from '@webb-tools/webb-ui-components/types/address'; -import fetchErc20TokenBalance from '../../utils/fetchErc20TokenBalance'; -import useAgnosticAccountInfo from '../../hooks/useAgnosticAccountInfo'; import { useCallback, useEffect, useState } from 'react'; -import ERC20_ABI from '../../abi/erc20'; import { Decimal } from 'decimal.js'; -import useViemPublicClient from '../../hooks/useViemPublicClient'; -import convertDecimalToBN from '../../utils/convertDecimalToBn'; +import useViemPublicClient from './useViemPublicClient'; +import useAgnosticAccountInfo from './useAgnosticAccountInfo'; +import fetchErc20TokenBalance from '../utils/fetchErc20TokenBalance'; +import { erc20Abi } from 'viem'; +import convertDecimalToBN from '../utils/convertDecimalToBn'; type Erc20Token = { contractAddress: EvmAddress; @@ -20,10 +20,10 @@ export type Erc20Balance = Erc20Token & { balance: BN; }; -// TODO: Query from EVM instead of being hard-coded. Waiting for bridge to be implemented in order to do that. +// TODO: Waiting for bridge PR to be implemented in order to list ERC-20 assets here instead of these dummies. export const ERC20_TEST_TOKENS: Erc20Token[] = [ { - name: "Yuri's Local ERC-2 Dummy", + name: 'Local ERC-20 Dummy', symbol: 'USDC', decimals: 18, contractAddress: assertEvmAddress( @@ -64,7 +64,7 @@ const useTangleEvmErc20Balances = (): Erc20Balance[] | null => { viemPublicClient, evmAddress, token.contractAddress, - ERC20_ABI, + erc20Abi, token.decimals, ); }, diff --git a/apps/tangle-dapp/src/hooks/useViemPublicClient.ts b/libs/tangle-shared-ui/src/hooks/useViemPublicClient.ts similarity index 82% rename from apps/tangle-dapp/src/hooks/useViemPublicClient.ts rename to libs/tangle-shared-ui/src/hooks/useViemPublicClient.ts index b8bd9b7597..fa42af5aba 100644 --- a/apps/tangle-dapp/src/hooks/useViemPublicClient.ts +++ b/libs/tangle-shared-ui/src/hooks/useViemPublicClient.ts @@ -1,8 +1,8 @@ -import useNetworkStore from '@webb-tools/tangle-shared-ui/context/useNetworkStore'; import { useEffect, useState } from 'react'; import { createPublicClient, http, type PublicClient } from 'viem'; -import createTangleViemChainFromNetwork from '../utils/evm/createTangleViemChainFromNetwork'; +import createTangleViemChainFromNetwork from '../utils/createTangleViemChainFromNetwork'; +import useNetworkStore from '../context/useNetworkStore'; const useViemPublicClient = () => { const [publicClient, setPublicClient] = useState(null); diff --git a/libs/tangle-shared-ui/src/queries/restake/assetDetails.ts b/libs/tangle-shared-ui/src/queries/restake/assetDetails.ts index 4197999a58..8f84bb958f 100644 --- a/libs/tangle-shared-ui/src/queries/restake/assetDetails.ts +++ b/libs/tangle-shared-ui/src/queries/restake/assetDetails.ts @@ -7,7 +7,6 @@ import type { } from '@polkadot/types/lookup'; import { BN, formatBalance, hexToString } from '@polkadot/util'; import type { Chain } from '@webb-tools/dapp-config'; -import { assertEvmAddress } from '@webb-tools/webb-ui-components'; import { isEvmAddress } from '@webb-tools/webb-ui-components/utils/isEvmAddress20'; import { combineLatest, map, Observable, of, switchMap } from 'rxjs'; import { RestakeAssetId } from '../../types'; @@ -16,6 +15,7 @@ import assertRestakeAssetId from '../../utils/assertRestakeAssetId'; import createAssetIdEnum from '../../utils/createAssetIdEnum'; import { fetchTokenPriceBySymbol } from '../../utils/fetchTokenPrices'; import filterNativeAsset from '../../utils/restake/filterNativeAsset'; +import { findErc20Token } from '../../hooks/useTangleEvmErc20Balances'; function createVaultId(u32: Option): number | null { if (u32.isNone) { @@ -89,14 +89,7 @@ function processAssetDetailsRx( const price = null; if (isEvmAddress(assetId)) { - const erc20Token = { - name: "Yuri's Local ERC-2 Dummy", - symbol: 'USDC', - decimals: 18, - contractAddress: assertEvmAddress( - '0x2af9b184d0d42cd8d3c4fd0c953a06b6838c9357', - ), - }; + const erc20Token = findErc20Token(assetId); if (erc20Token === null) { return assetMap; diff --git a/apps/tangle-dapp/src/utils/convertDecimalToBn.ts b/libs/tangle-shared-ui/src/utils/convertDecimalToBn.ts similarity index 100% rename from apps/tangle-dapp/src/utils/convertDecimalToBn.ts rename to libs/tangle-shared-ui/src/utils/convertDecimalToBn.ts diff --git a/apps/tangle-dapp/src/utils/evm/createTangleViemChainFromNetwork.ts b/libs/tangle-shared-ui/src/utils/createTangleViemChainFromNetwork.ts similarity index 95% rename from apps/tangle-dapp/src/utils/evm/createTangleViemChainFromNetwork.ts rename to libs/tangle-shared-ui/src/utils/createTangleViemChainFromNetwork.ts index 265df8ac2e..dc99888753 100644 --- a/apps/tangle-dapp/src/utils/evm/createTangleViemChainFromNetwork.ts +++ b/libs/tangle-shared-ui/src/utils/createTangleViemChainFromNetwork.ts @@ -1,9 +1,9 @@ -import { TangleTokenSymbol } from '@webb-tools/tangle-shared-ui/types'; import { Network, NetworkId, } from '@webb-tools/webb-ui-components/constants/networks'; import { Chain } from 'viem'; +import { TangleTokenSymbol } from '../types'; const createTangleViemChainFromNetwork = ( network: Network & { evmChainId: number; httpRpcEndpoint: string }, diff --git a/apps/tangle-dapp/src/utils/fetchErc20TokenBalance.ts b/libs/tangle-shared-ui/src/utils/fetchErc20TokenBalance.ts similarity index 100% rename from apps/tangle-dapp/src/utils/fetchErc20TokenBalance.ts rename to libs/tangle-shared-ui/src/utils/fetchErc20TokenBalance.ts From b5da85946b3391b175b20852c5338683f667a1a5 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Sun, 26 Jan 2025 23:16:06 -0500 Subject: [PATCH 05/28] refactor(tangle-dapp): Remove logs --- .../src/containers/VaultsAndBalancesTable.tsx | 4 +--- libs/tangle-shared-ui/src/hooks/useApiRx.ts | 2 +- libs/tangle-shared-ui/src/hooks/useSwitchNetwork.ts | 12 ++++++------ 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/apps/tangle-dapp/src/containers/VaultsAndBalancesTable.tsx b/apps/tangle-dapp/src/containers/VaultsAndBalancesTable.tsx index 27ff23206c..41e8e8adf6 100644 --- a/apps/tangle-dapp/src/containers/VaultsAndBalancesTable.tsx +++ b/apps/tangle-dapp/src/containers/VaultsAndBalancesTable.tsx @@ -145,7 +145,7 @@ const COLUMNS = [ header: () => ( ), cell: (props) => { @@ -176,8 +176,6 @@ const COLUMNS = [ ? null : calculateBnRatio(tvl, depositCap); - console.debug('CAPACITY', capacityPercentage); - return (
diff --git a/libs/tangle-shared-ui/src/hooks/useApiRx.ts b/libs/tangle-shared-ui/src/hooks/useApiRx.ts index 59d0772387..d0ea2463fe 100644 --- a/libs/tangle-shared-ui/src/hooks/useApiRx.ts +++ b/libs/tangle-shared-ui/src/hooks/useApiRx.ts @@ -51,9 +51,9 @@ function useApiRx( setError(null); }, []); + // Create the subscription when the API is ready. useEffect(() => { if (apiRx === null) { - // Discard any previous data when the Promise API is not ready. resetData(); return; diff --git a/libs/tangle-shared-ui/src/hooks/useSwitchNetwork.ts b/libs/tangle-shared-ui/src/hooks/useSwitchNetwork.ts index 4f2a078f61..18ebe3a277 100644 --- a/libs/tangle-shared-ui/src/hooks/useSwitchNetwork.ts +++ b/libs/tangle-shared-ui/src/hooks/useSwitchNetwork.ts @@ -61,6 +61,10 @@ const useSwitchNetwork = () => { setIsCustom(true); } + console.debug( + `Set initial network: ${initialNetwork.name} | RPC: ${initialNetwork.wsRpcEndpoint}`, + ); + setNetwork(initialNetwork); }); }, [ @@ -96,17 +100,13 @@ const useSwitchNetwork = () => { if (switchChainResult !== null) { console.debug( - `Switching to ${isCustom ? 'custom' : 'Tangle'} network: ${ - newNetwork.name - } (${newNetwork.nodeType}) with RPC endpoint: ${ - newNetwork.wsRpcEndpoint - }`, + `Switched to network: ${newNetwork.name} | RPC: ${newNetwork.wsRpcEndpoint}`, ); } } catch (error) { notificationApi({ variant: 'error', - message: 'Switching network failed', + message: 'Switching chain failed', secondaryMessage: `Error: ${ensureError(error).message}`, }); } From e7089c000a0d726e01dffd3263b0c6aee832c1ad Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Sun, 26 Jan 2025 23:26:29 -0500 Subject: [PATCH 06/28] fix(tangle-dapp): Fix errors left over from LS --- .../LiquidStaking/stakeAndUnstake/LsStakeCard.tsx | 10 ++++++---- .../LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx | 12 +++++++++--- .../stakeAndUnstake/UnstakePeriodDetailItem.tsx | 7 ++----- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx b/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx index fd41620e3b..3e1c07a33f 100644 --- a/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx +++ b/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/LsStakeCard.tsx @@ -30,6 +30,7 @@ import ListModal from '@webb-tools/tangle-shared-ui/components/ListModal'; import LstListItem from '../LstListItem'; import filterBy from '../../../utils/filterBy'; import useActiveAccountAddress from '@webb-tools/tangle-shared-ui/hooks/useActiveAccountAddress'; +import useLsAgnosticBalance from './useLsAgnosticBalance'; const LsStakeCard: FC = () => { const lsPools = useLsPools(); @@ -107,12 +108,14 @@ const LsStakeCard: FC = () => { const canCallStake = isTangleNetwork && executeTanglePoolJoinTx !== null && lsPoolId !== null; + const balance = useLsAgnosticBalance(true); + const walletBalance = ( { - if (maxSpendable !== null) { - setFromAmount(maxSpendable); + if (balance instanceof BN) { + setFromAmount(balance); } }} /> @@ -162,8 +165,7 @@ const LsStakeCard: FC = () => { placeholder="Enter amount to stake" rightElement={walletBalance} setProtocolId={setLsProtocolId} - minAmount={minSpendable ?? undefined} - maxAmount={maxSpendable ?? undefined} + maxAmount={balance instanceof BN ? balance : undefined} setNetworkId={tryChangeNetwork} showPoolIndicator={false} /> diff --git a/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx b/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx index d7d775afed..32a000fa1e 100644 --- a/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx +++ b/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/LsUnstakeCard.tsx @@ -28,6 +28,7 @@ import useLsChangeNetwork from './useLsChangeNetwork'; import ListModal from '@webb-tools/tangle-shared-ui/components/ListModal'; import LstListItem from '../LstListItem'; import useActiveAccountAddress from '@webb-tools/tangle-shared-ui/hooks/useActiveAccountAddress'; +import useLsAgnosticBalance from './useLsAgnosticBalance'; const LsUnstakeCard: FC = () => { const isAccountConnected = useIsAccountConnected(); @@ -117,11 +118,17 @@ const LsUnstakeCard: FC = () => { } }, []); + const balance = useLsAgnosticBalance(false); + const stakedWalletBalance = ( setFromAmount(maxSpendable)} + onClick={() => { + if (balance instanceof BN) { + setFromAmount(balance); + } + }} /> ); @@ -150,8 +157,7 @@ const LsUnstakeCard: FC = () => { placeholder="Enter amount to unstake" rightElement={stakedWalletBalance} isDerivativeVariant - minAmount={minSpendable ?? undefined} - maxAmount={maxSpendable ?? undefined} + maxAmount={balance instanceof BN ? balance : undefined} maxErrorMessage="Not enough stake to redeem" // Disable the token click if there's no account connected // since it won't be possible to fetch the user's pools diff --git a/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/UnstakePeriodDetailItem.tsx b/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/UnstakePeriodDetailItem.tsx index e4230f9ff9..6cda30d7b1 100644 --- a/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/UnstakePeriodDetailItem.tsx +++ b/apps/tangle-dapp/src/components/LiquidStaking/stakeAndUnstake/UnstakePeriodDetailItem.tsx @@ -22,12 +22,9 @@ const UnstakePeriodDetailItem: FC = ({ const protocol = getLsProtocolDef(protocolId); const unlockPeriod = ((): UnstakePeriod | null => { - const unlockPeriod = new CrossChainTime( - protocol.timeUnit, - protocol.unstakingPeriod, - ); + // TODO: This is actually in eras, not days. May need conversion. + const days = protocol.unstakingPeriod; - const days = unlockPeriod.toDays(); const roundedDays = Math.round(days); return { From ffe40d06ee513449cb951d749289b326c1d1b00b Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Mon, 27 Jan 2025 20:00:56 -0500 Subject: [PATCH 07/28] feat(tangle-dapp): Integrate into `useEvmPrecompileCall` --- .../src/components/account/PointsReminder.tsx | 5 +- .../src/data/balances/useTransferTx.ts | 4 +- apps/tangle-dapp/src/hooks/useAgnosticTx.ts | 4 +- .../src/hooks/useEvmGasEstimate.ts | 5 +- .../src/hooks/useEvmPrecompileCall.ts | 51 +++++++++-- .../src/hooks/useEvmPrecompileFee.ts | 4 +- apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts | 87 ++++++++++++------- .../src/hooks/useViemWalletClient.ts | 62 +++++++++++-- .../src/ConnectWallet/index.ts | 1 - .../src/hooks/useWallets.ts | 3 - .../src/wallets/wallets-config.tsx | 4 +- .../WalletModalContainer.tsx | 14 +-- .../WalletConnectionCard.tsx | 36 ++++---- .../components/WalletModal/WalletModal.tsx | 55 ++++++------ .../src/constants/networks.ts | 3 +- 15 files changed, 224 insertions(+), 114 deletions(-) diff --git a/apps/tangle-dapp/src/components/account/PointsReminder.tsx b/apps/tangle-dapp/src/components/account/PointsReminder.tsx index 209aeaf347..b32219ec85 100644 --- a/apps/tangle-dapp/src/components/account/PointsReminder.tsx +++ b/apps/tangle-dapp/src/components/account/PointsReminder.tsx @@ -29,8 +29,9 @@ const PointsReminder: FC<{ className?: string }> = ({ className }) => {
- {/** TODO: Awaiting creation of the campaign docs page. Tracked by #2708. */} - Learn More + + Learn More +
); diff --git a/apps/tangle-dapp/src/data/balances/useTransferTx.ts b/apps/tangle-dapp/src/data/balances/useTransferTx.ts index 254c2be2ef..d3a4164a05 100644 --- a/apps/tangle-dapp/src/data/balances/useTransferTx.ts +++ b/apps/tangle-dapp/src/data/balances/useTransferTx.ts @@ -13,7 +13,7 @@ import { useCallback } from 'react'; import { TxName } from '../../constants'; import { PrecompileAddress } from '../../constants/evmPrecompiles'; import useAgnosticTx from '../../hooks/useAgnosticTx'; -import { AbiCall, EvmTxFactory } from '../../hooks/useEvmPrecompileCall'; +import { PrecompileCall, EvmTxFactory } from '../../hooks/useEvmPrecompileCall'; import useEvmPrecompileFeeFetcher from '../../hooks/useEvmPrecompileFee'; import useFormatNativeTokenAmount from '../../hooks/useFormatNativeTokenAmount'; import { SubstrateTxFactory } from '../../hooks/useSubstrateTx'; @@ -39,7 +39,7 @@ const useTransferTx = () => { const isMaxAmount = amount.eq(maxAmount); const recipientEvmAddress20 = toEvmAddress(recipientAddress); - const sharedAbiCallData: AbiCall< + const sharedAbiCallData: PrecompileCall< typeof BALANCES_ERC20_PRECOMPILE_ABI, 'transfer' > = { diff --git a/apps/tangle-dapp/src/hooks/useAgnosticTx.ts b/apps/tangle-dapp/src/hooks/useAgnosticTx.ts index 9afdd6daaa..53757971f7 100644 --- a/apps/tangle-dapp/src/hooks/useAgnosticTx.ts +++ b/apps/tangle-dapp/src/hooks/useAgnosticTx.ts @@ -5,7 +5,7 @@ import { useCallback, useEffect, useState } from 'react'; import { TxName } from '../constants'; import { GetSuccessMessageFn } from '../types'; import useEvmPrecompileCall, { - AbiCall, + PrecompileCall, EvmTxFactory, } from './useEvmPrecompileCall'; import useSubstrateTx, { SubstrateTxFactory, TxStatus } from './useSubstrateTx'; @@ -29,7 +29,7 @@ export type AgnosticTxOptions< evmTxFactory: | EvmTxFactory - | AbiCall; + | PrecompileCall; /** * An identifiable name shown on the toast notification to diff --git a/apps/tangle-dapp/src/hooks/useEvmGasEstimate.ts b/apps/tangle-dapp/src/hooks/useEvmGasEstimate.ts index 7ce8e1fbae..7dcbb59391 100644 --- a/apps/tangle-dapp/src/hooks/useEvmGasEstimate.ts +++ b/apps/tangle-dapp/src/hooks/useEvmGasEstimate.ts @@ -8,7 +8,7 @@ import useViemPublicClient from '@webb-tools/tangle-shared-ui/hooks/useViemPubli * Add a buffer to the gas estimate to ensure the * transaction is successful. */ -const BUFFER = BigInt(11); // +10% (1.1) +const BUFFER = BigInt(15); // +50% (1.5) const useEvmGasEstimate = () => { const { evmAddress } = useAgnosticAccountInfo(); @@ -22,7 +22,8 @@ const useEvmGasEstimate = () => { const gasEstimate = await viemPublicClient.estimateContractGas({ account: evmAddress, - to: contractAddress, + address: contractAddress, + // TODO: Proper params. data: callData, }); diff --git a/apps/tangle-dapp/src/hooks/useEvmPrecompileCall.ts b/apps/tangle-dapp/src/hooks/useEvmPrecompileCall.ts index 7c021e3670..881179d13a 100644 --- a/apps/tangle-dapp/src/hooks/useEvmPrecompileCall.ts +++ b/apps/tangle-dapp/src/hooks/useEvmPrecompileCall.ts @@ -18,6 +18,9 @@ import { import useEvmAddress20 from './useEvmAddress'; import { TxStatus } from './useSubstrateTx'; import { EvmAddress } from '@webb-tools/webb-ui-components/types/address'; +import useBalances from '../data/balances/useBalances'; +import useEvmTxRelayer from './useEvmTxRelayer'; +import useNetworkStore from '@webb-tools/tangle-shared-ui/context/useNetworkStore'; export type AbiBatchCall = { to: EvmAddress; @@ -26,7 +29,7 @@ export type AbiBatchCall = { callData: Hex; }; -export type AbiCall< +export type PrecompileCall< Abi extends AbiFunction[], FunctionName extends ExtractAbiFunctionNames, > = { @@ -41,7 +44,7 @@ export type EvmTxFactory< > = ( context: Context, activeEvmAddress20: EvmAddress, -) => PromiseOrT> | null; +) => PromiseOrT> | null; /** * Obtain a function that can be used to execute a precompile contract call. @@ -63,13 +66,16 @@ function useEvmPrecompileCall< precompileAddress: PrecompileAddress, factory: | EvmTxFactory - | AbiCall, + | PrecompileCall, getSuccessMessage?: (context: Context) => string, ) { const [status, setStatus] = useState(TxStatus.NOT_YET_INITIATED); const [error, setError] = useState(null); const [txHash, setTxHash] = useState(null); const [successMessage, setSuccessMessage] = useState(null); + const { free } = useBalances(); + const relayEvmTx = useEvmTxRelayer(); + const { network } = useNetworkStore(); const activeEvmAddress20 = useEvmAddress20(); const { data: connectorClient } = useConnectorClient(); @@ -89,8 +95,10 @@ function useEvmPrecompileCall< ? await factory(context, activeEvmAddress20) : factory; - // Factory isn't ready yet. - if (factoryResult === null) { + // Not yet ready. + if (factoryResult === null || relayEvmTx === null || free === null) { + console.warn('Attempted to execute EVM pre-compile call too early.'); + return; } @@ -99,10 +107,36 @@ function useEvmPrecompileCall< setTxHash(null); setStatus(TxStatus.PROCESSING); + // TODO: Compare against estimated contract execution gas against balance (there's a constant that allows conversion from gas to native token) instead of checking if it's zero? + // Relay the transaction if the EVM account doesn't have enough to + // cover the transaction fees. This is like a subsidy for EVM accounts + // without TNT. + if (!free.isZero() && network.evmTxRelayerEndpoint !== undefined) { + console.debug('Attempting to relay transaction for EVM account.'); + + const result = await relayEvmTx( + abi, + precompileAddress, + factoryResult.functionName, + factoryResult.arguments, + ); + + if (result instanceof Error) { + setStatus(TxStatus.ERROR); + setError(result); + + return; + } + + setStatus(TxStatus.COMPLETE); + setTxHash(result.txHash); + + return; + } + try { const { request } = await simulateContract(connectorClient, { address: precompileAddress, - // TODO: Find a way to avoid casting. abi: abi satisfies AbiFunction[] as AbiFunction[], functionName: factoryResult.functionName, args: factoryResult.arguments, @@ -147,8 +181,11 @@ function useEvmPrecompileCall< status, connectorClient, factory, - precompileAddress, + relayEvmTx, + free, + network.evmTxRelayerEndpoint, abi, + precompileAddress, getSuccessMessage, ], ); diff --git a/apps/tangle-dapp/src/hooks/useEvmPrecompileFee.ts b/apps/tangle-dapp/src/hooks/useEvmPrecompileFee.ts index ae8ceded47..87b81cc493 100644 --- a/apps/tangle-dapp/src/hooks/useEvmPrecompileFee.ts +++ b/apps/tangle-dapp/src/hooks/useEvmPrecompileFee.ts @@ -6,7 +6,7 @@ import { PrecompileAddress, } from '../constants/evmPrecompiles'; import useEvmAddress20 from './useEvmAddress'; -import { AbiCall } from './useEvmPrecompileCall'; +import { PrecompileCall } from './useEvmPrecompileCall'; import { AbiFunction } from 'viem'; import useViemPublicClient from '@webb-tools/tangle-shared-ui/hooks/useViemPublicClient'; @@ -26,7 +26,7 @@ function useEvmPrecompileFeeFetcher() { >( abi: Abi, precompileAddress: PrecompileAddress, - abiCallData: AbiCall, + abiCallData: PrecompileCall, ) => { setStatus('loading'); setError(null); diff --git a/apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts b/apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts index db95dbbe86..776b3805e9 100644 --- a/apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts +++ b/apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts @@ -16,9 +16,12 @@ import { import { EvmAddress } from '@webb-tools/webb-ui-components/types/address'; import { assertEvmAddress } from '@webb-tools/webb-ui-components'; import assert from 'assert'; -import useViemWalletClient from './useViemWalletClient'; +import useViemWalletClient, { + WalletClientTransport, +} from './useViemWalletClient'; import useAgnosticAccountInfo from '@webb-tools/tangle-shared-ui/hooks/useAgnosticAccountInfo'; import useNetworkStore from '@webb-tools/tangle-shared-ui/context/useNetworkStore'; +import ensureError from '@webb-tools/tangle-shared-ui/utils/ensureError'; const PATHNAME = '/api/v1/relay'; const DEADLINE_MINUTES = 10; @@ -113,15 +116,15 @@ type Response = const useEvmTxRelayer = () => { const { evmAddress } = useAgnosticAccountInfo(); const { network } = useNetworkStore(); - const walletClient = useViemWalletClient(); + const walletClient = useViemWalletClient(WalletClientTransport.WINDOW); const isReady = evmAddress !== null && network.evmTxRelayerEndpoint !== undefined && walletClient !== null; - const obtainSignatureParams = useCallback( - async (destination: EvmAddress, deadlineSeconds: number) => { + const signTx = useCallback( + async (data: Hex, destination: EvmAddress, deadlineSeconds: number) => { // TODO: Temp. assertion. assert(walletClient !== null && evmAddress !== null); @@ -135,6 +138,7 @@ const useEvmTxRelayer = () => { }; const message = { + data, from: evmAddress, to: destination, value: 0, @@ -155,7 +159,7 @@ const useEvmTxRelayer = () => { const { v, r, s } = parseSignature(signature); - return { v, r, s }; + return { v: v !== undefined ? Number(v) : undefined, r, s }; }, [evmAddress, network.evmChainId, walletClient], ); @@ -181,31 +185,30 @@ const useEvmTxRelayer = () => { const deadline = `0x${deadlineSeconds.toString(16)}` as const; const destination = assertEvmAddress(precompileAddress); - const { v, r, s } = await obtainSignatureParams( - destination, - deadlineSeconds, - ); - - const url = new URL(network.evmTxRelayerEndpoint); - - url.pathname = PATHNAME; - const callData = encodeFunctionData({ abi: abi satisfies AbiFunction[] as AbiFunction[], functionName: functionName satisfies string as string, args, }); - const response = await axios.post< - Response, - AxiosResponse, - RequestBody - >(url.toString(), { + const { v, r, s } = await signTx(callData, destination, deadlineSeconds); + const url = new URL(network.evmTxRelayerEndpoint); + + url.pathname = PATHNAME; + console.debug('Obtained signature:', { v, r, s }); + + if (v === undefined) { + return new Error( + 'Failed to obtain signature: recovery ID is undefined, possibly indicating an invalid or malformed signature.', + ); + } + + console.debug('BODY', { from: evmAddress, to: destination, value: ZERO_ADDRESS, - // TODO: Should gas limit be estimated or set to zero since we're dealing with precompile calls here, not 'real' EVM? - gaslimit: 0, + // TODO: Estimate gas limit. For now, using max limit allowed by the relayer: 60,000. + gaslimit: 60_000, data: callData, deadline, v, @@ -213,20 +216,38 @@ const useEvmTxRelayer = () => { s, }); - if (response.data.status === 'success') { - return response.data; - } else { - return response.data.details !== undefined - ? new Error(`${response.data.error}; ${response.data.details}`) - : new Error(response.data.error); + try { + const response = await axios.post< + Response, + AxiosResponse, + RequestBody + >(url.toString(), { + from: evmAddress, + to: destination, + value: ZERO_ADDRESS, + // TODO: Estimate gas limit. For now, using max limit allowed by the relayer: 60,000. + gaslimit: 60_000, + data: callData, + deadline, + v, + r, + s, + }); + + if (response.data.status === 'success') { + return response.data; + } else { + return response.data.details !== undefined + ? new Error(`${response.data.error}; ${response.data.details}`) + : new Error(response.data.error); + } + } catch (possibleError) { + const error = ensureError(possibleError); + + return error; } }, - [ - evmAddress, - network.evmTxRelayerEndpoint, - obtainSignatureParams, - walletClient, - ], + [evmAddress, network.evmTxRelayerEndpoint, signTx, walletClient], ); return isReady ? relayEvmTx : null; diff --git a/apps/tangle-dapp/src/hooks/useViemWalletClient.ts b/apps/tangle-dapp/src/hooks/useViemWalletClient.ts index 6a585257aa..21c8c316bd 100644 --- a/apps/tangle-dapp/src/hooks/useViemWalletClient.ts +++ b/apps/tangle-dapp/src/hooks/useViemWalletClient.ts @@ -1,9 +1,41 @@ import useNetworkStore from '@webb-tools/tangle-shared-ui/context/useNetworkStore'; import createTangleViemChainFromNetwork from '@webb-tools/tangle-shared-ui/utils/createTangleViemChainFromNetwork'; import { useEffect, useState } from 'react'; -import { createWalletClient, http, WalletClient } from 'viem'; +import { + createWalletClient, + custom, + EIP1193Provider, + http, + Transport, + WalletClient, +} from 'viem'; -const useViemWalletClient = () => { +export enum WalletClientTransport { + /** + * Used for interacting with blockchain nodes (e.g., Infura, Alchemy) that + * support the JSON-RPC API. Ideal for querying blockchain data (e.g., + * balances, blocks, transactions) and broadcasting signed transactions. + * Note: This transport cannot obtain wallet signatures as nodes do not manage + * private keys. + */ + HTTP_RPC, + + /** + * Used for interacting with browser-injected EVM wallets (e.g., MetaMask, + * Brave Wallet) via the `window.ethereum` object. Essential for obtaining + * signatures, requesting wallet addresses, and user-approved transactions. + * This transport securely interacts with wallets that manage private keys. + */ + WINDOW, +} + +declare global { + interface Window { + ethereum?: EIP1193Provider; + } +} + +const useViemWalletClient = (transport = WalletClientTransport.HTTP_RPC) => { const [walletClient, setWalletClient] = useState(null); const { network } = useNetworkStore(); @@ -22,14 +54,34 @@ const useViemWalletClient = () => { httpRpcEndpoint: network.httpRpcEndpoint, }); + let transport_: Transport; + + switch (transport) { + case WalletClientTransport.HTTP_RPC: + transport_ = http(network.httpRpcEndpoint); + + break; + case WalletClientTransport.WINDOW: + if (window.ethereum === undefined) { + console.warn( + 'Could not create Viem wallet client due to Ethereum provider not found on window object.', + ); + + return; + } + + transport_ = custom(window.ethereum); + + break; + } + const newWalletClient = createWalletClient({ chain, - // TODO: Try with custom(window.ethereum). - transport: http(), + transport: transport_, }); setWalletClient(newWalletClient); - }, [network]); + }, [network, transport]); return walletClient; }; diff --git a/libs/api-provider-environment/src/ConnectWallet/index.ts b/libs/api-provider-environment/src/ConnectWallet/index.ts index 50cbad0d8e..a656612e32 100644 --- a/libs/api-provider-environment/src/ConnectWallet/index.ts +++ b/libs/api-provider-environment/src/ConnectWallet/index.ts @@ -221,7 +221,6 @@ const useConnectWallet = (): UseConnectWalletReturnType => { ); } }, - // prettier-ignore [appName, switchChain, typedChainId], ); diff --git a/libs/api-provider-environment/src/hooks/useWallets.ts b/libs/api-provider-environment/src/hooks/useWallets.ts index 03f1a34bb7..6fb06b9a72 100644 --- a/libs/api-provider-environment/src/hooks/useWallets.ts +++ b/libs/api-provider-environment/src/hooks/useWallets.ts @@ -2,9 +2,6 @@ import { ManagedWallet } from '@webb-tools/dapp-config/wallets'; import { useEffect, useState } from 'react'; import { useWebContext } from '../webb-context'; -/** - * @name useWallets - */ export const useWallets = () => { const [wallets, setWallets] = useState([]); diff --git a/libs/dapp-config/src/wallets/wallets-config.tsx b/libs/dapp-config/src/wallets/wallets-config.tsx index d74c170c93..aeb2dcf903 100644 --- a/libs/dapp-config/src/wallets/wallets-config.tsx +++ b/libs/dapp-config/src/wallets/wallets-config.tsx @@ -81,9 +81,9 @@ export const walletsConfig: Record = { id: WalletId.WalletConnectV2, Logo: , name: 'WalletConnect', - title: 'Wallet Connect', + title: 'WalletConnect', platform: 'EVM', - enabled: true, + enabled: false, async detect() { return true; }, diff --git a/libs/tangle-shared-ui/src/components/ConnectWalletButton/WalletModalContainer.tsx b/libs/tangle-shared-ui/src/components/ConnectWalletButton/WalletModalContainer.tsx index 0c5fee0ebd..01ff638e16 100644 --- a/libs/tangle-shared-ui/src/components/ConnectWalletButton/WalletModalContainer.tsx +++ b/libs/tangle-shared-ui/src/components/ConnectWalletButton/WalletModalContainer.tsx @@ -5,7 +5,6 @@ import { useWebContext, } from '@webb-tools/api-provider-environment'; import getPlatformMetaData from '@webb-tools/browser-utils/platform/getPlatformMetaData'; -import { WalletId } from '@webb-tools/dapp-types'; import { calculateTypedChainId, ChainType, @@ -37,15 +36,6 @@ const WalletModalContainer = () => { [network], ); - // TODO: Fix the issue with WalletConnectV2 and re-enable it. - const wallets = useMemo(() => { - // Exclude WalletConnectV2 from the list of supported wallets, as it - // is currently broken in the dApp. - return supportedWallets.filter( - (wallet) => wallet.id !== WalletId.WalletConnectV2, - ); - }, [supportedWallets]); - return ( { connectWallet={connectWallet} toggleModal={toggleModal} connectError={connectError} - supportedWallets={wallets} + supportedWallets={supportedWallets} notificationApi={notificationApi} apiConfig={apiConfig} targetTypedChainIds={targetTypedChainIds} - contentDefaultText="Connect your EVM or Substrate wallet to interact with the Tangle Network." + contentDefaultText="Connect your Substrate or EVM wallet to interact with the Tangle Network." /> ); }; diff --git a/libs/webb-ui-components/src/components/WalletConnectionCard/WalletConnectionCard.tsx b/libs/webb-ui-components/src/components/WalletConnectionCard/WalletConnectionCard.tsx index bef99c124f..6a3326da63 100644 --- a/libs/webb-ui-components/src/components/WalletConnectionCard/WalletConnectionCard.tsx +++ b/libs/webb-ui-components/src/components/WalletConnectionCard/WalletConnectionCard.tsx @@ -53,7 +53,6 @@ export const WalletConnectionCard = forwardRef< }, [failedWalletId, wallets]); const showList = !isMdOrLess || (!connectingWallet && !failedWallet); - const showContent = !isMdOrLess || connectingWallet || failedWallet; return ( @@ -216,21 +215,28 @@ const WalletList: FC< > = ({ wallets, onWalletSelect, className }) => { return (
    - {wallets.map((wallet) => ( - onWalletSelect?.(wallet)} - > -
    - {wallet.Logo} + {wallets.flatMap((wallet) => { + // Skip disabled wallets. + if (!wallet.enabled) { + return []; + } - - {wallet.title} - -
    -
    - ))} + return ( + onWalletSelect?.(wallet)} + > +
    + {wallet.Logo} + + + {wallet.title} + +
    +
    + ); + })}
); }; diff --git a/libs/webb-ui-components/src/components/WalletModal/WalletModal.tsx b/libs/webb-ui-components/src/components/WalletModal/WalletModal.tsx index f6a11be2e6..f300da961a 100644 --- a/libs/webb-ui-components/src/components/WalletModal/WalletModal.tsx +++ b/libs/webb-ui-components/src/components/WalletModal/WalletModal.tsx @@ -22,9 +22,10 @@ export const WalletModal: FC = ({ targetTypedChainIds, contentDefaultText, }) => { - // Get the current failed or connecting wallet + // Get the current failed or connecting wallet. const getCurrentWallet = useCallback(() => { const walletId = failedWalletId ?? connectingWalletId; + if (!walletId) { return; } @@ -32,16 +33,18 @@ export const WalletModal: FC = ({ return apiConfig.wallets[walletId]; }, [apiConfig.wallets, connectingWalletId, failedWalletId]); - const errorMessage = useMemo(() => { + const errorMessage = (() => { if (!connectError) { return; } return connectError.message; - }, [connectError]); + })(); - const downloadURL = useMemo(() => { - if (platformId == null) return; + const downloadUrl = useMemo(() => { + if (platformId == null) { + return; + } const wallet = getCurrentWallet(); @@ -50,26 +53,28 @@ export const WalletModal: FC = ({ } }, [getCurrentWallet, platformId]); - const handleTryAgainBtnClick = useCallback( - async () => { - if (connectError instanceof WalletNotInstalledError) { - return; - } + const handleTryAgainBtnClick = useCallback(async () => { + if (connectError instanceof WalletNotInstalledError) { + return; + } - if (!selectedWallet) { - notificationApi.addToQueue({ - variant: 'warning', - message: 'Failed to switch wallet', - secondaryMessage: 'No wallet selected. Please try again.', - }); - return; - } + if (!selectedWallet) { + notificationApi.addToQueue({ + variant: 'warning', + message: 'Failed to switch wallet', + secondaryMessage: 'No wallet selected. Please try again.', + }); + return; + } - await connectWallet(selectedWallet, targetTypedChainIds); - }, - // prettier-ignore - [connectError, selectedWallet, connectWallet, targetTypedChainIds, notificationApi], - ); + await connectWallet(selectedWallet, targetTypedChainIds); + }, [ + connectError, + selectedWallet, + connectWallet, + targetTypedChainIds, + notificationApi, + ]); return ( @@ -99,12 +104,12 @@ export const WalletModal: FC = ({ tryAgainBtnProps={ connectError instanceof WalletNotInstalledError ? { - href: downloadURL?.toString(), + href: downloadUrl?.toString(), target: '_blank', } : {} } - downloadWalletURL={downloadURL} + downloadWalletURL={downloadUrl} contentDefaultText={contentDefaultText} /> diff --git a/libs/webb-ui-components/src/constants/networks.ts b/libs/webb-ui-components/src/constants/networks.ts index 2d64d7faea..b8230ba016 100644 --- a/libs/webb-ui-components/src/constants/networks.ts +++ b/libs/webb-ui-components/src/constants/networks.ts @@ -92,6 +92,7 @@ export const TANGLE_TESTNET_NATIVE_NETWORK = { nativeExplorerUrl: TANGLE_TESTNET_NATIVE_EXPLORER_URL, evmExplorerUrl: TANGLE_TESTNET_EVM_EXPLORER_URL, ss58Prefix: TANGLE_SS58_PREFIX, + evmTxRelayerEndpoint: 'https://testnet-txrelayer.tangle.tools/', } as const satisfies Network; /** @@ -109,7 +110,7 @@ export const TANGLE_LOCAL_DEV_NETWORK = { httpRpcEndpoint: TANGLE_LOCAL_HTTP_RPC_ENDPOINT, polkadotJsDashboardUrl: TANGLE_LOCAL_POLKADOT_JS_DASHBOARD_URL, ss58Prefix: 42, - evmTxRelayerEndpoint: 'http://localhost:4500', + evmTxRelayerEndpoint: 'http://localhost:3000', } as const satisfies Network; export const TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK = { From 3c9971d2812aaf56092a224964f7161934b4ee14 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Mon, 27 Jan 2025 20:26:01 -0500 Subject: [PATCH 08/28] feat(tangle-dapp): Add `isEvmTxRelayerEligible` --- .../src/hooks/useEvmPrecompileCall.ts | 12 ++++-- apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts | 41 +++++++++++++------ 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/apps/tangle-dapp/src/hooks/useEvmPrecompileCall.ts b/apps/tangle-dapp/src/hooks/useEvmPrecompileCall.ts index 881179d13a..13756e27ba 100644 --- a/apps/tangle-dapp/src/hooks/useEvmPrecompileCall.ts +++ b/apps/tangle-dapp/src/hooks/useEvmPrecompileCall.ts @@ -19,7 +19,7 @@ import useEvmAddress20 from './useEvmAddress'; import { TxStatus } from './useSubstrateTx'; import { EvmAddress } from '@webb-tools/webb-ui-components/types/address'; import useBalances from '../data/balances/useBalances'; -import useEvmTxRelayer from './useEvmTxRelayer'; +import useEvmTxRelayer, { isEvmTxRelayerEligible } from './useEvmTxRelayer'; import useNetworkStore from '@webb-tools/tangle-shared-ui/context/useNetworkStore'; export type AbiBatchCall = { @@ -107,11 +107,17 @@ function useEvmPrecompileCall< setTxHash(null); setStatus(TxStatus.PROCESSING); + const isEligibleForEvmTxRelayer = + free.isZero() && + network.evmTxRelayerEndpoint !== undefined && + isEvmTxRelayerEligible(precompileAddress, factoryResult.functionName); + // TODO: Compare against estimated contract execution gas against balance (there's a constant that allows conversion from gas to native token) instead of checking if it's zero? // Relay the transaction if the EVM account doesn't have enough to // cover the transaction fees. This is like a subsidy for EVM accounts - // without TNT. - if (!free.isZero() && network.evmTxRelayerEndpoint !== undefined) { + // without TNT. Not all precompile functions are eligible for this; only + // those that are whitelisted by the EVM transaction relayer. + if (isEligibleForEvmTxRelayer) { console.debug('Attempting to relay transaction for EVM account.'); const result = await relayEvmTx( diff --git a/apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts b/apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts index 776b3805e9..4ad98865ca 100644 --- a/apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts +++ b/apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts @@ -22,10 +22,20 @@ import useViemWalletClient, { import useAgnosticAccountInfo from '@webb-tools/tangle-shared-ui/hooks/useAgnosticAccountInfo'; import useNetworkStore from '@webb-tools/tangle-shared-ui/context/useNetworkStore'; import ensureError from '@webb-tools/tangle-shared-ui/utils/ensureError'; +import RESTAKING_PRECOMPILE_ABI from '../abi/restaking'; const PATHNAME = '/api/v1/relay'; const DEADLINE_MINUTES = 10; +const ALLOWED_CALLS: Partial> = { + [PrecompileAddress.RESTAKING]: [ + 'deposit', + 'delegate', + ] as const satisfies ExtractAbiFunctionNames< + typeof RESTAKING_PRECOMPILE_ABI + >[], +}; + const EIP712_TYPES = { Permit: [ { name: 'from', type: 'address' }, @@ -108,6 +118,24 @@ type Response = details?: string; }; +/** + * Check whether a specific EVM precompile function is eligible to be + * subsidized by the EVM transaction relayer. + */ +export const isEvmTxRelayerEligible = < + Abi extends AbiFunction[], + FunctionName extends ExtractAbiFunctionNames, +>( + precompileAddress: PrecompileAddress, + functionName: FunctionName, +): boolean => { + if (ALLOWED_CALLS[precompileAddress] === undefined) { + return false; + } + + return ALLOWED_CALLS[precompileAddress].includes(functionName); +}; + /** * Obtain a function to send signed transactions to a relayer API. * This acts as a subsidy for EVM accounts that do not have balances @@ -203,19 +231,6 @@ const useEvmTxRelayer = () => { ); } - console.debug('BODY', { - from: evmAddress, - to: destination, - value: ZERO_ADDRESS, - // TODO: Estimate gas limit. For now, using max limit allowed by the relayer: 60,000. - gaslimit: 60_000, - data: callData, - deadline, - v, - r, - s, - }); - try { const response = await axios.post< Response, From f8e51500d53b877326917ff9ad1a597af799981a Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Mon, 27 Jan 2025 20:46:40 -0500 Subject: [PATCH 09/28] feat(tangle-dapp): Estimate gas with `useEvmGasEstimate` --- .../src/hooks/useEvmGasEstimate.ts | 24 +++++++++--- .../src/hooks/useEvmPrecompileFee.ts | 1 - apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts | 38 ++++++++++++++----- 3 files changed, 47 insertions(+), 16 deletions(-) diff --git a/apps/tangle-dapp/src/hooks/useEvmGasEstimate.ts b/apps/tangle-dapp/src/hooks/useEvmGasEstimate.ts index 7dcbb59391..77e1dfb4ce 100644 --- a/apps/tangle-dapp/src/hooks/useEvmGasEstimate.ts +++ b/apps/tangle-dapp/src/hooks/useEvmGasEstimate.ts @@ -1,8 +1,12 @@ import { useCallback } from 'react'; -import { EvmAddress } from '@webb-tools/webb-ui-components/types/address'; -import { Hex } from 'viem'; +import { AbiFunction } from 'viem'; import useAgnosticAccountInfo from '@webb-tools/tangle-shared-ui/hooks/useAgnosticAccountInfo'; import useViemPublicClient from '@webb-tools/tangle-shared-ui/hooks/useViemPublicClient'; +import { + ExtractAbiFunctionNames, + PrecompileAddress, +} from '../constants/evmPrecompiles'; +import { PrecompileCall } from './useEvmPrecompileCall'; /** * Add a buffer to the gas estimate to ensure the @@ -15,16 +19,24 @@ const useEvmGasEstimate = () => { const viemPublicClient = useViemPublicClient(); const estimateGas = useCallback( - async (contractAddress: EvmAddress, callData: Hex) => { + async < + Abi extends AbiFunction[], + FunctionName extends ExtractAbiFunctionNames, + >( + abi: Abi, + precompileAddress: PrecompileAddress, + call: PrecompileCall, + ) => { if (viemPublicClient === null || evmAddress === null) { return null; } const gasEstimate = await viemPublicClient.estimateContractGas({ + abi: abi satisfies AbiFunction[] as AbiFunction[], account: evmAddress, - address: contractAddress, - // TODO: Proper params. - data: callData, + address: precompileAddress, + functionName: call.functionName, + args: call.arguments, }); return (gasEstimate * BUFFER) / BigInt(10); diff --git a/apps/tangle-dapp/src/hooks/useEvmPrecompileFee.ts b/apps/tangle-dapp/src/hooks/useEvmPrecompileFee.ts index 87b81cc493..4d6b65dd69 100644 --- a/apps/tangle-dapp/src/hooks/useEvmPrecompileFee.ts +++ b/apps/tangle-dapp/src/hooks/useEvmPrecompileFee.ts @@ -41,7 +41,6 @@ function useEvmPrecompileFeeFetcher() { const [gas, fees] = await Promise.all([ client.estimateContractGas({ address: precompileAddress, - // TODO: Find a way to avoid casting. abi: abi satisfies AbiFunction[] as AbiFunction[], functionName: abiCallData.functionName, args: abiCallData.arguments, diff --git a/apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts b/apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts index 4ad98865ca..158fa66e0d 100644 --- a/apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts +++ b/apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts @@ -23,9 +23,11 @@ import useAgnosticAccountInfo from '@webb-tools/tangle-shared-ui/hooks/useAgnost import useNetworkStore from '@webb-tools/tangle-shared-ui/context/useNetworkStore'; import ensureError from '@webb-tools/tangle-shared-ui/utils/ensureError'; import RESTAKING_PRECOMPILE_ABI from '../abi/restaking'; +import useEvmGasEstimate from './useEvmGasEstimate'; const PATHNAME = '/api/v1/relay'; const DEADLINE_MINUTES = 10; +const MAX_GAS_LIMIT = 60_000; const ALLOWED_CALLS: Partial> = { [PrecompileAddress.RESTAKING]: [ @@ -145,6 +147,7 @@ const useEvmTxRelayer = () => { const { evmAddress } = useAgnosticAccountInfo(); const { network } = useNetworkStore(); const walletClient = useViemWalletClient(WalletClientTransport.WINDOW); + const estimateGas = useEvmGasEstimate(); const isReady = evmAddress !== null && @@ -156,10 +159,9 @@ const useEvmTxRelayer = () => { // TODO: Temp. assertion. assert(walletClient !== null && evmAddress !== null); - // EIP-712 domain, types, and message + // EIP-712 domain, types, and message. const domain = { - // TODO: Name & version. - name: 'CallPermitExample', + name: 'TangleEvmTxRelayer', version: '1', chainId: network.evmChainId, verifyingContract: destination, @@ -231,6 +233,21 @@ const useEvmTxRelayer = () => { ); } + const gasEstimate = await estimateGas(abi, precompileAddress, { + functionName, + arguments: args, + }); + + // If the estimate is null, or exceeds the maximum gas limit, + // use the maximum. Otherwise, it must be lower than the maximum; + // and thus it can fit within a 32-bit integer. + const gasLimit = + gasEstimate === null + ? MAX_GAS_LIMIT + : gasEstimate > BigInt(MAX_GAS_LIMIT) + ? MAX_GAS_LIMIT + : Number(gasEstimate); + try { const response = await axios.post< Response, @@ -240,8 +257,7 @@ const useEvmTxRelayer = () => { from: evmAddress, to: destination, value: ZERO_ADDRESS, - // TODO: Estimate gas limit. For now, using max limit allowed by the relayer: 60,000. - gaslimit: 60_000, + gaslimit: gasLimit, data: callData, deadline, v, @@ -257,12 +273,16 @@ const useEvmTxRelayer = () => { : new Error(response.data.error); } } catch (possibleError) { - const error = ensureError(possibleError); - - return error; + return ensureError(possibleError); } }, - [evmAddress, network.evmTxRelayerEndpoint, signTx, walletClient], + [ + estimateGas, + evmAddress, + network.evmTxRelayerEndpoint, + signTx, + walletClient, + ], ); return isReady ? relayEvmTx : null; From eff2e61461c5643d0ab2a5b76a95e8d7b52e0a12 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Mon, 27 Jan 2025 20:57:58 -0500 Subject: [PATCH 10/28] feat(tangle-dapp): Use TX relayer in `useRestakeApi` --- .../src/data/restake/RestakeEvmApi.ts | 37 +++++++++++++++++++ .../src/data/restake/useRestakeApi.ts | 17 ++++++++- apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts | 6 +-- 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/apps/tangle-dapp/src/data/restake/RestakeEvmApi.ts b/apps/tangle-dapp/src/data/restake/RestakeEvmApi.ts index 1b068246ee..491422ffea 100644 --- a/apps/tangle-dapp/src/data/restake/RestakeEvmApi.ts +++ b/apps/tangle-dapp/src/data/restake/RestakeEvmApi.ts @@ -20,6 +20,7 @@ import { Config } from 'wagmi'; import ensureError from '@webb-tools/tangle-shared-ui/utils/ensureError'; import RESTAKING_PRECOMPILE_ABI from '../../abi/restaking'; import { + ExtractAbiFunctionNames, FindAbiArgsOf, PrecompileAddress, ZERO_ADDRESS, @@ -32,9 +33,27 @@ import createEvmBatchCall from '../../utils/staking/createEvmBatchCall'; import BATCH_PRECOMPILE_ABI from '../../abi/batch'; import createEvmBatchCallArgs from '../../utils/staking/createEvmBatchCallArgs'; import { TxName } from '../../constants'; +import { + EvmTxRelaySuccessResult, + isEvmTxRelayerEligible, +} from '../../hooks/useEvmTxRelayer'; + +type RelayEvmTxFn = + | (< + Abi extends AbiFunction[], + FunctionName extends ExtractAbiFunctionNames, + >( + abi: Abi, + precompileAddress: PrecompileAddress, + functionName: FunctionName, + args: FindAbiArgsOf, + ) => Promise) + | null; class RestakeEvmApi extends RestakeApiBase { constructor( + readonly relay: RelayEvmTxFn, + readonly hasNoBalance: boolean, readonly activeAccount: EvmAddress, readonly signer: Account | EvmAddress, readonly provider: Config, @@ -55,6 +74,24 @@ class RestakeEvmApi extends RestakeApiBase { functionName: FunctionName, args: Args, ) { + const isEligibleForTxRelay = + this.relay !== null && + this.hasNoBalance && + isEvmTxRelayerEligible(address, functionName); + + if (isEligibleForTxRelay) { + const result = await this.relay(abi, address, functionName, args); + + if (result instanceof Error) { + this.onFailure(txName, result); + } else { + // TODO: No block hash available in this case. Will it affect the 'View explorer' link? + this.onSuccess(result.txHash, '0x0', txName); + } + + return; + } + try { const connector = (() => { if (this.provider.state.current === null) { diff --git a/apps/tangle-dapp/src/data/restake/useRestakeApi.ts b/apps/tangle-dapp/src/data/restake/useRestakeApi.ts index fa6464eb58..d3041d0a74 100644 --- a/apps/tangle-dapp/src/data/restake/useRestakeApi.ts +++ b/apps/tangle-dapp/src/data/restake/useRestakeApi.ts @@ -14,6 +14,8 @@ import { Hash } from 'viem'; import getWagmiConfig from '@webb-tools/dapp-config/wagmi-config'; import { TxName } from '../../constants'; import useAgnosticAccountInfo from '@webb-tools/tangle-shared-ui/hooks/useAgnosticAccountInfo'; +import useEvmTxRelayer from '../../hooks/useEvmTxRelayer'; +import useBalances from '../balances/useBalances'; const useRestakeApi = () => { const { apiPromise } = usePolkadotApi(); @@ -22,6 +24,8 @@ const useRestakeApi = () => { const { resolveExplorerUrl } = useSubstrateExplorerUrl(); const { notifySuccess, notifyError } = useTxNotification(); const { isEvm } = useAgnosticAccountInfo(); + const relayEvmTx = useEvmTxRelayer(); + const { free } = useBalances(); const onSuccess = useCallback( (txHash: Hash, blockHash: Hash, txName: TxName) => { @@ -67,6 +71,8 @@ const useRestakeApi = () => { const evmAddress = assertEvmAddress(activeAccount.address); return new RestakeEvmApi( + relayEvmTx, + free?.isZero() === true ? true : false, evmAddress, evmAddress, getWagmiConfig(), @@ -75,7 +81,16 @@ const useRestakeApi = () => { ); } } - }, [activeAccount, activeWallet, apiPromise, injector, onFailure, onSuccess]); + }, [ + activeAccount, + activeWallet, + apiPromise, + free, + injector, + onFailure, + onSuccess, + relayEvmTx, + ]); return api; }; diff --git a/apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts b/apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts index 158fa66e0d..576d8356cd 100644 --- a/apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts +++ b/apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts @@ -103,13 +103,13 @@ type RequestBody = { s: Hex; }; -type SuccessResult = { +export type EvmTxRelaySuccessResult = { txHash: Hash; simulatedOutcome: Hex; }; type Response = - | ({ status: 'success' } & SuccessResult) + | ({ status: 'success' } & EvmTxRelaySuccessResult) | { status: 'failure'; error: string; @@ -203,7 +203,7 @@ const useEvmTxRelayer = () => { precompileAddress: PrecompileAddress, functionName: FunctionName, args: FindAbiArgsOf, - ): Promise => { + ): Promise => { assert( evmAddress !== null && network.evmTxRelayerEndpoint !== undefined && From 5e9b62e16d859553c2ca142ae2221ae28cc1d644 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Mon, 27 Jan 2025 21:37:02 -0500 Subject: [PATCH 11/28] fix(tangle-dapp): Fix verifying contract address --- apps/tangle-dapp/src/constants/evmPrecompiles.ts | 1 + apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/tangle-dapp/src/constants/evmPrecompiles.ts b/apps/tangle-dapp/src/constants/evmPrecompiles.ts index 12e464d719..e8de973e62 100644 --- a/apps/tangle-dapp/src/constants/evmPrecompiles.ts +++ b/apps/tangle-dapp/src/constants/evmPrecompiles.ts @@ -97,6 +97,7 @@ export enum PrecompileAddress { LST = '0x0000000000000000000000000000000000000824', RESTAKING = '0x0000000000000000000000000000000000000822', REWARDS = '0x0000000000000000000000000000000000000823', + CALL_PERMIT = '0x0000000000000000000000000000000000000805', } export const ZERO_ADDRESS = assertEvmAddress( diff --git a/apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts b/apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts index 576d8356cd..bc062d02c1 100644 --- a/apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts +++ b/apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts @@ -26,7 +26,7 @@ import RESTAKING_PRECOMPILE_ABI from '../abi/restaking'; import useEvmGasEstimate from './useEvmGasEstimate'; const PATHNAME = '/api/v1/relay'; -const DEADLINE_MINUTES = 10; +const DEADLINE_MINUTES = 30; const MAX_GAS_LIMIT = 60_000; const ALLOWED_CALLS: Partial> = { @@ -164,7 +164,7 @@ const useEvmTxRelayer = () => { name: 'TangleEvmTxRelayer', version: '1', chainId: network.evmChainId, - verifyingContract: destination, + verifyingContract: PrecompileAddress.CALL_PERMIT, }; const message = { @@ -172,7 +172,7 @@ const useEvmTxRelayer = () => { from: evmAddress, to: destination, value: 0, - // TODO: Nonce. + // TODO: Nonce: await callPermit.read.nonces([FROM.address]). nonce: 0, // For signing, the deadline should be in decimal form, not hex. deadline: deadlineSeconds, @@ -211,7 +211,7 @@ const useEvmTxRelayer = () => { ); const currentTimeSeconds = Math.floor(Date.now() / 1000); - const deadlineSeconds = currentTimeSeconds + DEADLINE_MINUTES * 10; + const deadlineSeconds = currentTimeSeconds + DEADLINE_MINUTES * 60; const deadline = `0x${deadlineSeconds.toString(16)}` as const; const destination = assertEvmAddress(precompileAddress); From b6c3c0ba2b131256169c3fd33cc6dfe2f083e333 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Tue, 28 Jan 2025 23:24:50 -0500 Subject: [PATCH 12/28] fix(tangle-dapp): Fix revert error --- apps/tangle-dapp/src/abi/callPermit.ts | 37 +++++ .../src/data/evm/useContractReadOnce.ts | 27 ++-- .../src/data/liquidStaking/usePolling.ts | 43 +++-- .../src/data/restake/useRestakeApi.ts | 14 +- .../src/hooks/useEvmCallPermitNonce.ts | 30 ++++ .../src/hooks/useEvmPrecompileCall.ts | 21 ++- apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts | 147 ++++++++++++------ .../src/hooks/useIsEvmTxRelayerCandidate.ts | 15 ++ 8 files changed, 241 insertions(+), 93 deletions(-) create mode 100644 apps/tangle-dapp/src/abi/callPermit.ts create mode 100644 apps/tangle-dapp/src/hooks/useEvmCallPermitNonce.ts create mode 100644 apps/tangle-dapp/src/hooks/useIsEvmTxRelayerCandidate.ts diff --git a/apps/tangle-dapp/src/abi/callPermit.ts b/apps/tangle-dapp/src/abi/callPermit.ts new file mode 100644 index 0000000000..65e2026342 --- /dev/null +++ b/apps/tangle-dapp/src/abi/callPermit.ts @@ -0,0 +1,37 @@ +import { AbiFunction } from 'viem'; + +const CALL_PERMIT_PRECOMPILE_ABI = [ + { + inputs: [ + { internalType: 'address', name: 'from', type: 'address' }, + { internalType: 'address', name: 'to', type: 'address' }, + { internalType: 'uint256', name: 'value', type: 'uint256' }, + { internalType: 'bytes', name: 'data', type: 'bytes' }, + { internalType: 'uint64', name: 'gaslimit', type: 'uint64' }, + { internalType: 'uint256', name: 'deadline', type: 'uint256' }, + { internalType: 'uint8', name: 'v', type: 'uint8' }, + { internalType: 'bytes32', name: 'r', type: 'bytes32' }, + { internalType: 'bytes32', name: 's', type: 'bytes32' }, + ], + name: 'dispatch', + outputs: [{ internalType: 'bytes', name: 'output', type: 'bytes' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'owner', type: 'address' }], + name: 'nonces', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'DOMAIN_SEPARATOR', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, +] as const satisfies AbiFunction[]; + +export default CALL_PERMIT_PRECOMPILE_ABI; diff --git a/apps/tangle-dapp/src/data/evm/useContractReadOnce.ts b/apps/tangle-dapp/src/data/evm/useContractReadOnce.ts index 2310f283c6..1e67f5aa82 100644 --- a/apps/tangle-dapp/src/data/evm/useContractReadOnce.ts +++ b/apps/tangle-dapp/src/data/evm/useContractReadOnce.ts @@ -1,4 +1,3 @@ -import { HexString } from '@polkadot/util/types'; import ensureError from '@webb-tools/tangle-shared-ui/utils/ensureError'; import assert from 'assert'; import { useCallback } from 'react'; @@ -7,30 +6,26 @@ import { ContractFunctionArgs, ContractFunctionName, } from 'viem'; -import { mainnet, sepolia } from 'viem/chains'; import { ReadContractReturnType } from 'wagmi/actions'; -import { IS_PRODUCTION_ENV } from '../../constants/env'; import useDebugMetricsStore from '../../context/useDebugMetricsStore'; -import useViemPublicClientWithChain from './useViemPublicClientWithChain'; +import { EvmAddress } from '@webb-tools/webb-ui-components/types/address'; +import useViemPublicClient from '@webb-tools/tangle-shared-ui/hooks/useViemPublicClient'; export type ContractReadOptions< Abi extends ViemAbi, FunctionName extends ContractFunctionName, > = { - address: HexString; + address: EvmAddress; functionName: FunctionName; args: ContractFunctionArgs; }; const useContractReadOnce = (abi: Abi) => { const { incrementRequestCount } = useDebugMetricsStore(); + const viemPublicClient = useViemPublicClient(); - // Use Sepolia testnet for development, and mainnet for production. - // Some dummy contracts were deployed on Sepolia for testing purposes. - const chain = IS_PRODUCTION_ENV ? mainnet : sepolia; - - const publicClient = useViemPublicClientWithChain(chain); + const isReady = viemPublicClient !== null; const read = useCallback( async >({ @@ -45,15 +40,11 @@ const useContractReadOnce = (abi: Abi) => { > | Error > => { - assert( - publicClient !== null, - "Should not be able to call this function if the client isn't ready yet", - ); - + assert(isReady); incrementRequestCount(); try { - return await publicClient.readContract({ + return await viemPublicClient.readContract({ address, abi, functionName, @@ -70,11 +61,11 @@ const useContractReadOnce = (abi: Abi) => { return error; } }, - [abi, incrementRequestCount, publicClient], + [abi, incrementRequestCount, isReady, viemPublicClient], ); // Only provide the read functions once the public client is ready. - return publicClient === null ? null : read; + return isReady ? read : null; }; export default useContractReadOnce; diff --git a/apps/tangle-dapp/src/data/liquidStaking/usePolling.ts b/apps/tangle-dapp/src/data/liquidStaking/usePolling.ts index 0c23d96a4b..de430e3cbf 100644 --- a/apps/tangle-dapp/src/data/liquidStaking/usePolling.ts +++ b/apps/tangle-dapp/src/data/liquidStaking/usePolling.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; export type PollingOptions = { effect: (() => Promise | unknown) | null; @@ -13,6 +13,9 @@ const usePolling = ({ refreshInterval = 12_000, }: PollingOptions) => { const [isRefreshing, setIsRefreshing] = useState(false); + const intervalHandleRef = useRef | null>(null); + const effectRef = useRef(effect); + const isMountedRef = useRef(true); const refresh = useCallback(async () => { // Fetcher isn't ready to be called yet. @@ -25,22 +28,42 @@ const usePolling = ({ setIsRefreshing(false); }, [effect]); + // Update the effect ref whenever the effect changes. useEffect(() => { - let intervalHandle: ReturnType | null = null; + effectRef.current = effect; + }, [effect]); + + // Track whether the component is mounted to avoid state + // or component unmounts. + useEffect(() => { + return () => { + isMountedRef.current = false; + if (intervalHandleRef.current !== null) { + clearInterval(intervalHandleRef.current); + } + }; + }, []); + + useEffect(() => { + // Clear any existing interval to prevent multiple intervals. + if (intervalHandleRef.current !== null) { + clearInterval(intervalHandleRef.current); + } - (async () => { - // Call it immediately to avoid initial delay. - await refresh(); + // Immediately invoke the effect to avoid initial delay. + refresh(); - intervalHandle = setInterval(refresh, refreshInterval); - })(); + intervalHandleRef.current = setInterval(refresh, refreshInterval); + // Clear the interval when dependencies change or component + // unmounts. return () => { - if (intervalHandle !== null) { - clearInterval(intervalHandle); + if (intervalHandleRef.current !== null) { + clearInterval(intervalHandleRef.current); + intervalHandleRef.current = null; } }; - }, [effect, refresh, refreshInterval]); + }, [refresh, refreshInterval]); return isRefreshing; }; diff --git a/apps/tangle-dapp/src/data/restake/useRestakeApi.ts b/apps/tangle-dapp/src/data/restake/useRestakeApi.ts index d3041d0a74..511c5a27f7 100644 --- a/apps/tangle-dapp/src/data/restake/useRestakeApi.ts +++ b/apps/tangle-dapp/src/data/restake/useRestakeApi.ts @@ -15,7 +15,7 @@ import getWagmiConfig from '@webb-tools/dapp-config/wagmi-config'; import { TxName } from '../../constants'; import useAgnosticAccountInfo from '@webb-tools/tangle-shared-ui/hooks/useAgnosticAccountInfo'; import useEvmTxRelayer from '../../hooks/useEvmTxRelayer'; -import useBalances from '../balances/useBalances'; +import useIsEvmTxRelayerCandidate from '../../hooks/useIsEvmTxRelayerCandidate'; const useRestakeApi = () => { const { apiPromise } = usePolkadotApi(); @@ -25,7 +25,7 @@ const useRestakeApi = () => { const { notifySuccess, notifyError } = useTxNotification(); const { isEvm } = useAgnosticAccountInfo(); const relayEvmTx = useEvmTxRelayer(); - const { free } = useBalances(); + const isEvmTxRelayerCandidate = useIsEvmTxRelayerCandidate(); const onSuccess = useCallback( (txHash: Hash, blockHash: Hash, txName: TxName) => { @@ -47,7 +47,11 @@ const useRestakeApi = () => { const api = useMemo(() => { // Not yet ready. - if (activeWallet === undefined || activeAccount === null) { + if ( + activeWallet === undefined || + activeAccount === null || + isEvmTxRelayerCandidate === null + ) { return null; } @@ -72,7 +76,7 @@ const useRestakeApi = () => { return new RestakeEvmApi( relayEvmTx, - free?.isZero() === true ? true : false, + isEvmTxRelayerCandidate, evmAddress, evmAddress, getWagmiConfig(), @@ -85,8 +89,8 @@ const useRestakeApi = () => { activeAccount, activeWallet, apiPromise, - free, injector, + isEvmTxRelayerCandidate, onFailure, onSuccess, relayEvmTx, diff --git a/apps/tangle-dapp/src/hooks/useEvmCallPermitNonce.ts b/apps/tangle-dapp/src/hooks/useEvmCallPermitNonce.ts new file mode 100644 index 0000000000..4c088b7989 --- /dev/null +++ b/apps/tangle-dapp/src/hooks/useEvmCallPermitNonce.ts @@ -0,0 +1,30 @@ +import CALL_PERMIT_PRECOMPILE_ABI from '../abi/callPermit'; +import useAgnosticAccountInfo from '@webb-tools/tangle-shared-ui/hooks/useAgnosticAccountInfo'; +import useContractRead from '../data/evm/useContractRead'; +import { PrecompileAddress } from '../constants/evmPrecompiles'; +import { assertEvmAddress } from '@webb-tools/webb-ui-components'; +import { useCallback } from 'react'; + +const useEvmCallPermitNonce = (): bigint | null => { + const { evmAddress } = useAgnosticAccountInfo(); + + const { value: nonce } = useContractRead( + CALL_PERMIT_PRECOMPILE_ABI, + useCallback(() => { + if (evmAddress === null) { + return null; + } + + return { + address: assertEvmAddress(PrecompileAddress.CALL_PERMIT), + functionName: 'nonces', + args: [evmAddress], + } as const; + }, [evmAddress]), + ); + + // TODO: Handle error case explicitly. + return nonce instanceof Error ? null : nonce; +}; + +export default useEvmCallPermitNonce; diff --git a/apps/tangle-dapp/src/hooks/useEvmPrecompileCall.ts b/apps/tangle-dapp/src/hooks/useEvmPrecompileCall.ts index 13756e27ba..c03dafe16c 100644 --- a/apps/tangle-dapp/src/hooks/useEvmPrecompileCall.ts +++ b/apps/tangle-dapp/src/hooks/useEvmPrecompileCall.ts @@ -18,9 +18,8 @@ import { import useEvmAddress20 from './useEvmAddress'; import { TxStatus } from './useSubstrateTx'; import { EvmAddress } from '@webb-tools/webb-ui-components/types/address'; -import useBalances from '../data/balances/useBalances'; import useEvmTxRelayer, { isEvmTxRelayerEligible } from './useEvmTxRelayer'; -import useNetworkStore from '@webb-tools/tangle-shared-ui/context/useNetworkStore'; +import useIsEvmTxRelayerCandidate from './useIsEvmTxRelayerCandidate'; export type AbiBatchCall = { to: EvmAddress; @@ -73,9 +72,8 @@ function useEvmPrecompileCall< const [error, setError] = useState(null); const [txHash, setTxHash] = useState(null); const [successMessage, setSuccessMessage] = useState(null); - const { free } = useBalances(); const relayEvmTx = useEvmTxRelayer(); - const { network } = useNetworkStore(); + const isEvmTxRelayerCandidate = useIsEvmTxRelayerCandidate(); const activeEvmAddress20 = useEvmAddress20(); const { data: connectorClient } = useConnectorClient(); @@ -96,7 +94,11 @@ function useEvmPrecompileCall< : factory; // Not yet ready. - if (factoryResult === null || relayEvmTx === null || free === null) { + if ( + factoryResult === null || + relayEvmTx === null || + isEvmTxRelayerCandidate === null + ) { console.warn('Attempted to execute EVM pre-compile call too early.'); return; @@ -108,11 +110,9 @@ function useEvmPrecompileCall< setStatus(TxStatus.PROCESSING); const isEligibleForEvmTxRelayer = - free.isZero() && - network.evmTxRelayerEndpoint !== undefined && + isEvmTxRelayerCandidate && isEvmTxRelayerEligible(precompileAddress, factoryResult.functionName); - // TODO: Compare against estimated contract execution gas against balance (there's a constant that allows conversion from gas to native token) instead of checking if it's zero? // Relay the transaction if the EVM account doesn't have enough to // cover the transaction fees. This is like a subsidy for EVM accounts // without TNT. Not all precompile functions are eligible for this; only @@ -188,10 +188,9 @@ function useEvmPrecompileCall< connectorClient, factory, relayEvmTx, - free, - network.evmTxRelayerEndpoint, - abi, + isEvmTxRelayerCandidate, precompileAddress, + abi, getSuccessMessage, ], ); diff --git a/apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts b/apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts index bc062d02c1..3c3a04a31e 100644 --- a/apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts +++ b/apps/tangle-dapp/src/hooks/useEvmTxRelayer.ts @@ -24,6 +24,7 @@ import useNetworkStore from '@webb-tools/tangle-shared-ui/context/useNetworkStor import ensureError from '@webb-tools/tangle-shared-ui/utils/ensureError'; import RESTAKING_PRECOMPILE_ABI from '../abi/restaking'; import useEvmGasEstimate from './useEvmGasEstimate'; +import useEvmCallPermitNonce from './useEvmCallPermitNonce'; const PATHNAME = '/api/v1/relay'; const DEADLINE_MINUTES = 30; @@ -38,16 +39,56 @@ const ALLOWED_CALLS: Partial> = { >[], }; -const EIP712_TYPES = { - Permit: [ - { name: 'from', type: 'address' }, - { name: 'to', type: 'address' }, - { name: 'value', type: 'uint256' }, - { name: 'data', type: 'bytes' }, - { name: 'nonce', type: 'uint256' }, - { name: 'deadline', type: 'uint256' }, +const TYPES = { + EIP712Domain: [ + { + name: 'name', + type: 'string', + }, + { + name: 'version', + type: 'string', + }, + { + name: 'chainId', + type: 'uint256', + }, + { + name: 'verifyingContract', + type: 'address', + }, ], -}; + CallPermit: [ + { + name: 'from', + type: 'address', + }, + { + name: 'to', + type: 'address', + }, + { + name: 'value', + type: 'uint256', + }, + { + name: 'data', + type: 'bytes', + }, + { + name: 'gaslimit', + type: 'uint64', + }, + { + name: 'nonce', + type: 'uint256', + }, + { + name: 'deadline', + type: 'uint256', + }, + ], +} as const; /** * @see https://github.com/tangle-network/txrelayer-blueprint/blob/main/API.md#request-body @@ -145,25 +186,37 @@ export const isEvmTxRelayerEligible = < */ const useEvmTxRelayer = () => { const { evmAddress } = useAgnosticAccountInfo(); - const { network } = useNetworkStore(); + + const { + network: { evmChainId, evmTxRelayerEndpoint }, + } = useNetworkStore(); + const walletClient = useViemWalletClient(WalletClientTransport.WINDOW); const estimateGas = useEvmGasEstimate(); + const nonce = useEvmCallPermitNonce(); const isReady = evmAddress !== null && - network.evmTxRelayerEndpoint !== undefined && - walletClient !== null; + evmTxRelayerEndpoint !== undefined && + walletClient !== null && + evmChainId !== undefined && + nonce !== null; const signTx = useCallback( - async (data: Hex, destination: EvmAddress, deadlineSeconds: number) => { - // TODO: Temp. assertion. - assert(walletClient !== null && evmAddress !== null); + async ( + data: Hex, + destination: EvmAddress, + deadlineSeconds: number, + nonce: bigint, + gasLimit: bigint, + ) => { + assert(isReady); // EIP-712 domain, types, and message. const domain = { - name: 'TangleEvmTxRelayer', + name: 'Call Permit Precompile', version: '1', - chainId: network.evmChainId, + chainId: BigInt(evmChainId), verifyingContract: PrecompileAddress.CALL_PERMIT, }; @@ -171,19 +224,18 @@ const useEvmTxRelayer = () => { data, from: evmAddress, to: destination, - value: 0, - // TODO: Nonce: await callPermit.read.nonces([FROM.address]). - nonce: 0, + value: BigInt(0), + nonce, // For signing, the deadline should be in decimal form, not hex. - deadline: deadlineSeconds, - // TODO: Additional fields if the contract requires them. + deadline: BigInt(deadlineSeconds), + gaslimit: gasLimit, }; const signature = await walletClient.signTypedData({ account: evmAddress, domain, - types: EIP712_TYPES, - primaryType: 'Permit', + types: TYPES, + primaryType: 'CallPermit', message, }); @@ -191,7 +243,7 @@ const useEvmTxRelayer = () => { return { v: v !== undefined ? Number(v) : undefined, r, s }; }, - [evmAddress, network.evmChainId, walletClient], + [evmAddress, isReady, evmChainId, walletClient], ); const relayEvmTx = useCallback( @@ -204,11 +256,7 @@ const useEvmTxRelayer = () => { functionName: FunctionName, args: FindAbiArgsOf, ): Promise => { - assert( - evmAddress !== null && - network.evmTxRelayerEndpoint !== undefined && - walletClient !== null, - ); + assert(isReady); const currentTimeSeconds = Math.floor(Date.now() / 1000); const deadlineSeconds = currentTimeSeconds + DEADLINE_MINUTES * 60; @@ -221,18 +269,6 @@ const useEvmTxRelayer = () => { args, }); - const { v, r, s } = await signTx(callData, destination, deadlineSeconds); - const url = new URL(network.evmTxRelayerEndpoint); - - url.pathname = PATHNAME; - console.debug('Obtained signature:', { v, r, s }); - - if (v === undefined) { - return new Error( - 'Failed to obtain signature: recovery ID is undefined, possibly indicating an invalid or malformed signature.', - ); - } - const gasEstimate = await estimateGas(abi, precompileAddress, { functionName, arguments: args, @@ -248,6 +284,25 @@ const useEvmTxRelayer = () => { ? MAX_GAS_LIMIT : Number(gasEstimate); + const { v, r, s } = await signTx( + callData, + destination, + deadlineSeconds, + nonce, + BigInt(gasLimit), + ); + + const url = new URL(evmTxRelayerEndpoint); + + url.pathname = PATHNAME; + console.debug('Obtained signature:', { v, r, s }); + + if (v === undefined) { + return new Error( + 'Failed to obtain signature: recovery ID is undefined, possibly indicating an invalid or malformed signature.', + ); + } + try { const response = await axios.post< Response, @@ -276,13 +331,7 @@ const useEvmTxRelayer = () => { return ensureError(possibleError); } }, - [ - estimateGas, - evmAddress, - network.evmTxRelayerEndpoint, - signTx, - walletClient, - ], + [estimateGas, evmAddress, evmTxRelayerEndpoint, isReady, nonce, signTx], ); return isReady ? relayEvmTx : null; diff --git a/apps/tangle-dapp/src/hooks/useIsEvmTxRelayerCandidate.ts b/apps/tangle-dapp/src/hooks/useIsEvmTxRelayerCandidate.ts new file mode 100644 index 0000000000..a82dd0323b --- /dev/null +++ b/apps/tangle-dapp/src/hooks/useIsEvmTxRelayerCandidate.ts @@ -0,0 +1,15 @@ +import useNetworkStore from '@webb-tools/tangle-shared-ui/context/useNetworkStore'; +import useBalances from '../data/balances/useBalances'; + +const useIsEvmTxRelayerCandidate = (): boolean | null => { + const { free } = useBalances(); + const { network } = useNetworkStore(); + + if (free === null) { + return null; + } + + return free.isZero() && network.evmTxRelayerEndpoint !== undefined; +}; + +export default useIsEvmTxRelayerCandidate; From 16a581b0eed5b950f1d302cbe1f0f859629c88e2 Mon Sep 17 00:00:00 2001 From: yurixander <101931215+yurixander@users.noreply.github.com> Date: Tue, 28 Jan 2025 23:53:26 -0500 Subject: [PATCH 13/28] style(tangle-dapp): Disable `Execute All` button sometimes --- .../src/containers/restaking/UnstakeRequestTableActions.tsx | 2 +- .../src/containers/restaking/WithdrawRequestTableActions.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/tangle-dapp/src/containers/restaking/UnstakeRequestTableActions.tsx b/apps/tangle-dapp/src/containers/restaking/UnstakeRequestTableActions.tsx index 23e0328b8a..a168018991 100644 --- a/apps/tangle-dapp/src/containers/restaking/UnstakeRequestTableActions.tsx +++ b/apps/tangle-dapp/src/containers/restaking/UnstakeRequestTableActions.tsx @@ -75,7 +75,7 @@ const UnstakeRequestTableActions: FC = ({