diff --git a/apps/tangle-dapp/src/pages/claim/migration/components/SubstrateWalletModal.tsx b/apps/tangle-dapp/src/pages/claim/migration/components/SubstrateWalletModal.tsx new file mode 100644 index 0000000000..c382c578c2 --- /dev/null +++ b/apps/tangle-dapp/src/pages/claim/migration/components/SubstrateWalletModal.tsx @@ -0,0 +1,203 @@ +import type { + InjectedAccount, + InjectedExtension, +} from '@polkadot/extension-inject/types'; +import Spinner from '@tangle-network/icons/Spinner'; +import { + PolkadotJsIcon, + SubWalletIcon, + TalismanIcon, +} from '@tangle-network/icons/wallets'; +import { + Modal, + ModalContent, + ModalHeader, +} from '@tangle-network/ui-components/components/Modal'; +import { Typography } from '@tangle-network/ui-components/typography/Typography'; +import { type FC, useCallback, useEffect, useState } from 'react'; +import { twMerge } from 'tailwind-merge'; +import { findSubstrateWallet } from '../utils/walletUtils'; + +const SUBSTRATE_WALLETS = [ + { + id: 'polkadot-js', + name: 'polkadot-js', + title: 'Polkadot{.js}', + Icon: PolkadotJsIcon, + }, + { + id: 'talisman', + name: 'talisman', + title: 'Talisman', + Icon: TalismanIcon, + }, + { + id: 'subwallet-js', + name: 'subwallet-js', + title: 'SubWallet', + Icon: SubWalletIcon, + }, +] as const; + +export type SubstrateWalletName = (typeof SUBSTRATE_WALLETS)[number]['name']; + +interface SubstrateWalletModalProps { + isOpen: boolean; + onClose: () => void; + onWalletConnect: ( + walletName: SubstrateWalletName, + accounts: InjectedAccount[], + extension: InjectedExtension, + ) => void; +} + +const SubstrateWalletModal: FC = ({ + isOpen, + onClose, + onWalletConnect, +}) => { + const [connectingWalletId, setConnectingWalletId] = useState( + null, + ); + const [error, setError] = useState(null); + const [installedWallets, setInstalledWallets] = useState>( + new Set(), + ); + + // Detect installed wallets when modal opens + useEffect(() => { + if (!isOpen) return; + + setError(null); + + const detected = new Set(); + for (const wallet of SUBSTRATE_WALLETS) { + const ext = window.injectedWeb3?.[wallet.name]; + if (ext && (ext.enable || ext.connect)) { + detected.add(wallet.id); + } + } + setInstalledWallets(detected); + }, [isOpen]); + + const handleConnect = useCallback( + async (wallet: (typeof SUBSTRATE_WALLETS)[number]) => { + setConnectingWalletId(wallet.id); + setError(null); + + try { + const extension = await findSubstrateWallet(wallet.name); + const accounts = await extension.accounts.get(); + + if (accounts.length === 0) { + throw new Error( + `No accounts found. Please authorize accounts for this site in ${wallet.title} settings.`, + ); + } + + onWalletConnect(wallet.name, accounts, extension); + onClose(); + } catch (err) { + const message = + err instanceof Error ? err.message : 'Failed to connect wallet'; + + if (message.toLowerCase().includes('rejected')) { + setError('Connection rejected. Please try again.'); + } else { + setError(message); + } + } finally { + setConnectingWalletId(null); + } + }, + [onWalletConnect, onClose], + ); + + return ( + !open && onClose()}> + + + Connect Substrate Wallet + + +
+
+ {SUBSTRATE_WALLETS.map((wallet) => { + const isInstalled = installedWallets.has(wallet.id); + const isConnecting = connectingWalletId === wallet.id; + const Icon = wallet.Icon; + + return ( + + ); + })} +
+ + {error && ( +
+ + {error} + +
+ )} + + {installedWallets.size === 0 && ( +
+ + No Substrate wallets detected. Please install Polkadot.js, + Talisman, or SubWallet. + +
+ )} +
+
+
+ ); +}; + +export default SubstrateWalletModal; diff --git a/apps/tangle-dapp/src/pages/claim/migration/components/SubstrateWalletSelector.tsx b/apps/tangle-dapp/src/pages/claim/migration/components/SubstrateWalletSelector.tsx index 7999986195..2532032ef6 100644 --- a/apps/tangle-dapp/src/pages/claim/migration/components/SubstrateWalletSelector.tsx +++ b/apps/tangle-dapp/src/pages/claim/migration/components/SubstrateWalletSelector.tsx @@ -1,15 +1,45 @@ -import { type InjectedAccountWithMeta } from '@polkadot/extension-inject/types'; -import { WalletLineIcon, ChevronDown } from '@tangle-network/icons'; +import type { + InjectedAccount, + InjectedAccountWithMeta, + InjectedExtension, +} from '@polkadot/extension-inject/types'; +import { ChevronDown, WalletLineIcon } from '@tangle-network/icons'; +import { + PolkadotJsIcon, + SubWalletIcon, + TalismanIcon, +} from '@tangle-network/icons/wallets'; import { Avatar } from '@tangle-network/ui-components/components/Avatar'; import { Typography } from '@tangle-network/ui-components/typography/Typography'; import { shortenString } from '@tangle-network/ui-components/utils/shortenString'; -import { type FC, useCallback, useState } from 'react'; -import { twMerge } from 'tailwind-merge'; import { AnimatePresence, motion } from 'framer-motion'; +import { type FC, useCallback, useEffect, useMemo, useState } from 'react'; +import { twMerge } from 'tailwind-merge'; +import { findSubstrateWallet } from '../utils/walletUtils'; +import SubstrateWalletModal, { + type SubstrateWalletName, +} from './SubstrateWalletModal'; + +const STORAGE_KEY = 'tangle-migration-claim-substrate-wallet'; + +interface SavedConnection { + walletName: SubstrateWalletName; + accountAddress: string; +} + +const WALLET_INFO: Record< + SubstrateWalletName, + { title: string; Icon: FC<{ className?: string }> } +> = { + 'polkadot-js': { title: 'Polkadot{.js}', Icon: PolkadotJsIcon }, + talisman: { title: 'Talisman', Icon: TalismanIcon }, + 'subwallet-js': { title: 'SubWallet', Icon: SubWalletIcon }, +}; interface Props { selectedAccount: InjectedAccountWithMeta | null; onAccountSelect: (account: InjectedAccountWithMeta | null) => void; + onExtensionChange?: (extension: InjectedExtension | null) => void; disabled?: boolean; className?: string; } @@ -17,69 +47,169 @@ interface Props { const SubstrateWalletSelector: FC = ({ selectedAccount, onAccountSelect, + onExtensionChange, disabled = false, className, }) => { const [accounts, setAccounts] = useState([]); - const [isConnecting, setIsConnecting] = useState(false); const [isConnected, setIsConnected] = useState(false); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [error, setError] = useState(null); + const [isWalletModalOpen, setIsWalletModalOpen] = useState(false); + const [connectedWalletName, setConnectedWalletName] = + useState(null); + const [isReconnecting, setIsReconnecting] = useState(false); - const connectExtension = useCallback(async () => { - setIsConnecting(true); - setError(null); + const walletInfo = useMemo(() => { + if (!connectedWalletName) return null; + return WALLET_INFO[connectedWalletName]; + }, [connectedWalletName]); - try { - const { web3Enable, web3Accounts } = await import( - '@polkadot/extension-dapp' + // Convert InjectedAccount to InjectedAccountWithMeta + const convertAccounts = useCallback( + ( + rawAccounts: InjectedAccount[], + source: string, + ): InjectedAccountWithMeta[] => { + return rawAccounts.map((account) => ({ + address: account.address, + meta: { + name: account.name, + source, + genesisHash: account.genesisHash, + }, + type: account.type, + })); + }, + [], + ); + + // localStorage helpers + const saveConnection = useCallback( + (walletName: SubstrateWalletName, accountAddress: string) => { + localStorage.setItem( + STORAGE_KEY, + JSON.stringify({ walletName, accountAddress }), ); + }, + [], + ); - const extensions = await web3Enable('Tangle Migration Claim'); + const clearConnection = useCallback(() => { + localStorage.removeItem(STORAGE_KEY); + }, []); - if (extensions.length === 0) { - throw new Error( - 'No Polkadot wallet found. Please install Polkadot.js, Talisman, or SubWallet.', - ); + const loadSavedConnection = useCallback((): SavedConnection | null => { + try { + const saved = localStorage.getItem(STORAGE_KEY); + return saved ? JSON.parse(saved) : null; + } catch { + return null; + } + }, []); + + // Try to reconnect to saved wallet on mount + useEffect(() => { + const reconnect = async () => { + const saved = loadSavedConnection(); + if (!saved) return; + + // Check if wallet is still installed + if (!window.injectedWeb3?.[saved.walletName]) { + clearConnection(); + return; } - const allAccounts = await web3Accounts(); + setIsReconnecting(true); + + try { + const extension = await findSubstrateWallet(saved.walletName); + const rawAccounts = await extension.accounts.get(); - if (allAccounts.length === 0) { - throw new Error( - 'No accounts found. Please create or import an account in your wallet.', + if (rawAccounts.length === 0) { + clearConnection(); + return; + } + + const convertedAccounts = convertAccounts( + rawAccounts, + saved.walletName, ); + setAccounts(convertedAccounts); + setConnectedWalletName(saved.walletName); + setIsConnected(true); + onExtensionChange?.(extension); + + // Restore previously selected account + const savedAccount = convertedAccounts.find( + (acc) => acc.address === saved.accountAddress, + ); + if (savedAccount) { + onAccountSelect(savedAccount); + } + } catch (err) { + console.error('Failed to reconnect wallet:', err); + clearConnection(); + } finally { + setIsReconnecting(false); } + }; + + reconnect(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); - setAccounts(allAccounts); + // Handle wallet connection from modal + const handleWalletConnect = useCallback( + ( + walletName: SubstrateWalletName, + rawAccounts: InjectedAccount[], + extension: InjectedExtension, + ) => { + const convertedAccounts = convertAccounts(rawAccounts, walletName); + setAccounts(convertedAccounts); + setConnectedWalletName(walletName); setIsConnected(true); + setError(null); + onExtensionChange?.(extension); - if (allAccounts.length === 1) { - onAccountSelect(allAccounts[0]); + // Auto-select if only one account + if (convertedAccounts.length === 1) { + onAccountSelect(convertedAccounts[0]); + saveConnection(walletName, convertedAccounts[0].address); } - } catch (err) { - console.error('Failed to connect extension:', err); - setError(err instanceof Error ? err.message : 'Failed to connect wallet'); - setIsConnected(false); - } finally { - setIsConnecting(false); - } - }, [onAccountSelect]); + }, + [convertAccounts, onAccountSelect, onExtensionChange, saveConnection], + ); + // Handle account selection const handleAccountClick = useCallback( (account: InjectedAccountWithMeta) => { onAccountSelect(account); setIsDropdownOpen(false); + if (connectedWalletName) { + saveConnection(connectedWalletName, account.address); + } }, - [onAccountSelect], + [connectedWalletName, onAccountSelect, saveConnection], ); + // Handle disconnect const handleDisconnect = useCallback(() => { setIsConnected(false); setAccounts([]); + setConnectedWalletName(null); onAccountSelect(null); + onExtensionChange?.(null); + setIsDropdownOpen(false); + clearConnection(); + }, [clearConnection, onAccountSelect, onExtensionChange]); + + // Handle change wallet + const handleChangeWallet = useCallback(() => { setIsDropdownOpen(false); - }, [onAccountSelect]); + setIsWalletModalOpen(true); + }, []); // Not connected - show connect button if (!isConnected) { @@ -98,8 +228,8 @@ const SubstrateWalletSelector: FC = ({ )} + + setIsWalletModalOpen(false)} + onWalletConnect={handleWalletConnect} + /> ); } // Connected - show account selector + const WalletIcon = walletInfo?.Icon; + return (
) : ( <>
- + {WalletIcon ? ( + + ) : ( + + )}
= ({ ))} -
+
+
)} + + setIsWalletModalOpen(false)} + onWalletConnect={handleWalletConnect} + />
); }; diff --git a/apps/tangle-dapp/src/pages/claim/migration/hooks/useClaimEligibility.ts b/apps/tangle-dapp/src/pages/claim/migration/hooks/useClaimEligibility.ts index f27ee9745a..f86bac401a 100644 --- a/apps/tangle-dapp/src/pages/claim/migration/hooks/useClaimEligibility.ts +++ b/apps/tangle-dapp/src/pages/claim/migration/hooks/useClaimEligibility.ts @@ -807,6 +807,10 @@ export interface ClaimEligibility { formattedBalance: string | null; /** Whether claims are paused */ isPaused: boolean; + /** Percentage unlocked immediately (basis points, 1000 = 10%) */ + unlockedBps: number; + /** Timestamp when locked tokens become withdrawable */ + unlockTimestamp: bigint; } interface UseClaimEligibilityOptions { @@ -963,6 +967,40 @@ const useClaimEligibility = ({ ss58Address }: UseClaimEligibilityOptions) => { enabled: !!migrationAddress, }); + // Get unlock configuration (percentage unlocked immediately) + const { data: unlockedBps, isLoading: isLoadingUnlockedBps } = + useQuery({ + queryKey: ['migration-unlocked-bps', migrationAddress], + queryFn: async () => { + if (!migrationAddress) return 10000; // Default 100% if not configured + const result = await publicClient.readContract({ + address: migrationAddress, + abi: TANGLE_MIGRATION_ABI, + functionName: 'unlockedBps', + }); + return Number(result); + }, + enabled: !!migrationAddress, + staleTime: Infinity, // Lock config doesn't change after first claim + }); + + // Get unlock timestamp (when locked tokens become withdrawable) + const { data: unlockTimestamp, isLoading: isLoadingUnlockTimestamp } = + useQuery({ + queryKey: ['migration-unlock-timestamp', migrationAddress], + queryFn: async () => { + if (!migrationAddress) return BigInt(0); + const result = await publicClient.readContract({ + address: migrationAddress, + abi: TANGLE_MIGRATION_ABI, + functionName: 'unlockTimestamp', + }); + return result as bigint; + }, + enabled: !!migrationAddress, + staleTime: Infinity, // Lock config doesn't change after first claim + }); + // Get merkle root for verification using viem directly const { data: merkleRoot } = useQuery({ queryKey: ['migration-merkle-root', migrationAddress], @@ -1002,6 +1040,10 @@ const useClaimEligibility = ({ ss58Address }: UseClaimEligibilityOptions) => { const actualClaimedAmount = claimedAmount ?? BigInt(0); const hasClaimed = actualClaimedAmount > BigInt(0); + // Default lock config values + const actualUnlockedBps = unlockedBps ?? 10000; // 100% if not set + const actualUnlockTimestamp = unlockTimestamp ?? BigInt(0); + // In dev mode without proofs, provide mock eligibility for UI testing if (isDevMode && !claimData && ss58Address) { const mockAmount = BigInt('1000000000000000000000'); // 1000 TNT @@ -1015,6 +1057,10 @@ const useClaimEligibility = ({ ss58Address }: UseClaimEligibilityOptions) => { timeRemaining: BigInt(365 * 24 * 60 * 60), // 1 year formattedBalance: '1,000', isPaused: false, + unlockedBps: 1000, // Mock 10% unlocked for dev mode + unlockTimestamp: BigInt( + Math.floor(Date.now() / 1000) + 180 * 24 * 60 * 60, + ), // 180 days from now }; } @@ -1029,6 +1075,8 @@ const useClaimEligibility = ({ ss58Address }: UseClaimEligibilityOptions) => { timeRemaining, formattedBalance: null, isPaused: isPaused ?? false, + unlockedBps: actualUnlockedBps, + unlockTimestamp: actualUnlockTimestamp, }; } @@ -1044,6 +1092,8 @@ const useClaimEligibility = ({ ss58Address }: UseClaimEligibilityOptions) => { timeRemaining, formattedBalance, isPaused: isPaused ?? false, + unlockedBps: actualUnlockedBps, + unlockTimestamp: actualUnlockTimestamp, }; }, [ claimData, @@ -1052,6 +1102,8 @@ const useClaimEligibility = ({ ss58Address }: UseClaimEligibilityOptions) => { isPaused, isDevMode, ss58Address, + unlockedBps, + unlockTimestamp, ]); // In dev mode, don't wait for contract reads since they're disabled anyway @@ -1060,7 +1112,9 @@ const useClaimEligibility = ({ ss58Address }: UseClaimEligibilityOptions) => { : isLoadingProofs || isLoadingClaimed || isLoadingDeadline || - isLoadingPaused; + isLoadingPaused || + isLoadingUnlockedBps || + isLoadingUnlockTimestamp; return { eligibility, diff --git a/apps/tangle-dapp/src/pages/claim/migration/index.tsx b/apps/tangle-dapp/src/pages/claim/migration/index.tsx index 5d1ffef97b..99d4109520 100644 --- a/apps/tangle-dapp/src/pages/claim/migration/index.tsx +++ b/apps/tangle-dapp/src/pages/claim/migration/index.tsx @@ -3,6 +3,7 @@ import { CheckboxCircleFill, EditLine, Alert, + ExternalLinkLine, } from '@tangle-network/icons'; import { Card, CopyWithTooltip } from '@tangle-network/ui-components'; import Button from '@tangle-network/ui-components/components/buttons/Button'; @@ -15,13 +16,18 @@ import { } from '@tangle-network/ui-components/components/Tooltip'; import { Typography } from '@tangle-network/ui-components/typography/Typography'; import { EvmWalletModal } from '@tangle-network/tangle-shared-ui/components/EvmWalletModal'; +import { makeExplorerUrl } from '@tangle-network/api-provider-environment/transaction/utils/makeExplorerUrl'; import { type FC, useCallback, useState, useMemo, useEffect } from 'react'; -import { useAccount, useChainId } from 'wagmi'; +import { useAccount, useChainId, useConfig } from 'wagmi'; import { type Hex, formatUnits, isAddress, keccak256, toHex } from 'viem'; import { twMerge } from 'tailwind-merge'; -import type { InjectedAccountWithMeta } from '@polkadot/extension-inject/types'; +import type { + InjectedAccountWithMeta, + InjectedExtension, +} from '@polkadot/extension-inject/types'; import { AnimatePresence, motion } from 'framer-motion'; import parseTransactionError from '@tangle-network/tangle-shared-ui/utils/parseTransactionError'; +import formatTangleBalance from '../../../utils/formatTangleBalance'; import SubstrateWalletSelector from './components/SubstrateWalletSelector'; import useClaimEligibility, { generateChallenge, @@ -41,10 +47,13 @@ enum ClaimStep { const MigrationClaimPage: FC = () => { const { address: evmAddress, isConnected } = useAccount(); const chainId = useChainId(); + const config = useConfig(); // State const [substrateAccount, setSubstrateAccount] = useState(null); + const [substrateExtension, setSubstrateExtension] = + useState(null); const [signature, setSignature] = useState(null); const [currentStep, setCurrentStep] = useState( ClaimStep.CONNECT_WALLETS, @@ -98,6 +107,23 @@ const MigrationClaimPage: FC = () => { switchedToWalletMode, } = useSubmitClaim(); + // Generate explorer URL for the transaction based on connected chain + const explorerUrl = useMemo(() => { + if (!txHash) { + return null; + } + + // Get the chain's block explorer URL from wagmi config + const chain = config.chains.find((c) => c.id === chainId); + const baseUrl = chain?.blockExplorers?.default?.url; + + if (!baseUrl) { + return null; + } + + return makeExplorerUrl(baseUrl, txHash, 'tx', 'web3'); + }, [txHash, chainId, config.chains]); + // Validate recipient address const isRecipientValid = useMemo(() => { return recipientAddress && isAddress(recipientAddress); @@ -134,21 +160,30 @@ const MigrationClaimPage: FC = () => { ); // Check if ready to proceed to signing - // Core requirements: eligibility loaded, user is eligible, hasn't claimed, has valid recipient + // Core requirements: eligibility loaded, user is eligible, hasn't claimed, has valid recipient, and extension ready const canSign = useMemo(() => { return ( substrateAccount && + substrateExtension && !isLoadingEligibility && eligibility.isEligible && !eligibility.hasClaimed && !eligibility.isPaused && validRecipient ); - }, [substrateAccount, isLoadingEligibility, eligibility, validRecipient]); + }, [ + substrateAccount, + substrateExtension, + isLoadingEligibility, + eligibility, + validRecipient, + ]); // Handle signing the challenge const handleSignChallenge = useCallback(async () => { - if (!substrateAccount) return; + if (!substrateAccount || !substrateExtension) { + return; + } setCurrentStep(ClaimStep.SIGN_CHALLENGE); @@ -157,14 +192,11 @@ const MigrationClaimPage: FC = () => { const challengeToSign = challenge || keccak256(toHex('dev-mode-challenge')); - const { web3FromAddress } = await import('@polkadot/extension-dapp'); - const injector = await web3FromAddress(substrateAccount.address); - - if (!injector.signer.signRaw) { + if (!substrateExtension.signer?.signRaw) { throw new Error('Signer does not support raw signing'); } - const result = await injector.signer.signRaw({ + const result = await substrateExtension.signer.signRaw({ address: substrateAccount.address, data: challengeToSign, type: 'bytes', @@ -180,7 +212,7 @@ const MigrationClaimPage: FC = () => { console.error('Failed to sign challenge:', err); setCurrentStep(ClaimStep.CHECK_ELIGIBILITY); } - }, [challenge, substrateAccount]); + }, [challenge, substrateAccount, substrateExtension]); const handleCopySignature = useCallback(() => { if ( @@ -262,6 +294,15 @@ const MigrationClaimPage: FC = () => { ? formatUnits(eligibility.amount, 18) : '0'; + // Calculate unlocked and locked amounts for display + const unlockedAmount = eligibility.amount + ? (eligibility.amount * BigInt(eligibility.unlockedBps)) / BigInt(10000) + : BigInt(0); + const lockedAmount = eligibility.amount + ? eligibility.amount - unlockedAmount + : BigInt(0); + const hasLockSplit = eligibility.unlockedBps < 10000; + // Render claim success if (isConfirmed) { return ( @@ -283,13 +324,57 @@ const MigrationClaimPage: FC = () => { Claim Successful! - + Your TNT tokens have been claimed successfully. + + {/* Unlock breakdown in success state */} + {hasLockSplit && eligibility.amount !== null && ( +
+
+ + Sent to your wallet + + + {formatTangleBalance(unlockedAmount)} TNT + +
+
+ + Locked until{' '} + {new Date( + Number(eligibility.unlockTimestamp) * 1000, + ).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + })} + + + {formatTangleBalance(lockedAmount)} TNT + +
+
+ )} +
{ {txHash && ( )} + + {explorerUrl && ( + + + + )}
@@ -569,6 +665,7 @@ const MigrationClaimPage: FC = () => { = ClaimStep.SIGN_CHALLENGE} /> @@ -660,10 +757,66 @@ const MigrationClaimPage: FC = () => { > {Number(formattedAmount).toLocaleString()} TNT
+ + {/* Unlock breakdown - only show if not 100% unlocked */} + {eligibility.amount !== null && + eligibility.unlockedBps < 10000 && ( +
+
+ + Available immediately + + + {formatTangleBalance( + (eligibility.amount * + BigInt(eligibility.unlockedBps)) / + BigInt(10000), + )}{' '} + TNT ({eligibility.unlockedBps / 100}%) + +
+
+ + Locked until{' '} + {new Date( + Number(eligibility.unlockTimestamp) * 1000, + ).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + })} + + + {formatTangleBalance( + eligibility.amount - + (eligibility.amount * + BigInt(eligibility.unlockedBps)) / + BigInt(10000), + )}{' '} + TNT ({100 - eligibility.unlockedBps / 100}%) + +
+
+ )} + {eligibility.timeRemaining > BigInt(0) && ( {Math.floor( Number(eligibility.timeRemaining) / 86400, diff --git a/apps/tangle-dapp/src/pages/claim/migration/utils/walletUtils.ts b/apps/tangle-dapp/src/pages/claim/migration/utils/walletUtils.ts new file mode 100644 index 0000000000..69b0d05f40 --- /dev/null +++ b/apps/tangle-dapp/src/pages/claim/migration/utils/walletUtils.ts @@ -0,0 +1,66 @@ +import type { + InjectedAccount, + InjectedExtension, + Unsubcall, +} from '@polkadot/extension-inject/types'; + +export const APP_NAME = 'Tangle dApp'; + +/** + * Ensures extension has accounts.subscribe method. + * Some wallet extensions don't implement subscribe, so we polyfill it + * by falling back to a one-time get() call. + */ +export const ensureAccountsSubscribe = ( + extension: InjectedExtension, +): InjectedExtension => { + if (!extension.accounts.subscribe) { + return { + ...extension, + accounts: { + ...extension.accounts, + subscribe: ( + cb: (accounts: InjectedAccount[]) => void | Promise, + ): Unsubcall => { + extension.accounts.get().then(cb).catch(console.error); + return Function.prototype as Unsubcall; + }, + }, + }; + } + return extension; +}; + +/** + * Find and connect to a substrate wallet by name. + * Supports both connect() and enable() methods for wallet extensions. + */ +export const findSubstrateWallet = async ( + walletName: string, +): Promise => { + if (typeof window === 'undefined' || !window.injectedWeb3) { + throw new Error('No wallet extensions detected'); + } + + const extension = window.injectedWeb3[walletName]; + + if (extension === undefined) { + throw new Error(`${walletName} is not installed`); + } + + if (extension.connect !== undefined) { + const ex = await extension.connect(APP_NAME); + return ensureAccountsSubscribe(ex); + } + + if (extension.enable !== undefined) { + const injected = await extension.enable(APP_NAME); + return ensureAccountsSubscribe({ + name: walletName, + version: extension.version || 'unknown', + ...injected, + }); + } + + throw new Error(`${walletName} does not support connect() or enable()`); +}; diff --git a/libs/dapp-config/src/contracts.ts b/libs/dapp-config/src/contracts.ts index 3a97ad149f..5cce7acb2c 100644 --- a/libs/dapp-config/src/contracts.ts +++ b/libs/dapp-config/src/contracts.ts @@ -43,7 +43,7 @@ export const LOCAL_CONTRACTS: ContractAddresses = { operatorStatusRegistry: '0x8f86403A4DE0BB5791fa46B8e795C547942fE4Cf', rewardVaults: '0x21dF544947ba3E8b3c32561399E88B52Dc8b2823', inflationPool: '0xD8a5a9b31c3C0232E196d518E89Fd8bF83AcAd43', - credits: '0x5081a39b8A5f0E35a8D959395a630b68B74Dd30f', + credits: '0x162A433068F51e18b7d13932F27e66a3f99E6890', liquidDelegationFactory: '0x8F4ec854Dd12F1fe79500a1f53D0cbB30f9b6134', }; diff --git a/libs/icons/src/wallets/PolkadotJsIcon.tsx b/libs/icons/src/wallets/PolkadotJsIcon.tsx new file mode 100644 index 0000000000..b9cb864f91 --- /dev/null +++ b/libs/icons/src/wallets/PolkadotJsIcon.tsx @@ -0,0 +1,34 @@ +import { createIcon } from '../create-icon'; +import { IconBase } from '../types'; + +export const PolkadotJsIcon = (props: IconBase) => { + return createIcon({ + ...props, + size: props.size ?? 'lg', // Override the default size to `lg` (24px) + displayName: 'PolkadotJsIcon', + path: ( + <> + + + + + + + + + ), + }); +}; diff --git a/libs/icons/src/wallets/SubWalletIcon.tsx b/libs/icons/src/wallets/SubWalletIcon.tsx new file mode 100644 index 0000000000..88f6818a96 --- /dev/null +++ b/libs/icons/src/wallets/SubWalletIcon.tsx @@ -0,0 +1,183 @@ +import { createIcon } from '../create-icon'; +import { IconBase } from '../types'; + +export const SubWalletIcon = (props: IconBase) => { + return createIcon({ + ...props, + size: props.size ?? 'lg', // Override the default size to `lg` (24px) + displayName: 'SubWalletIcon', + viewBox: '0 0 134 134', + path: ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ), + }); +}; diff --git a/libs/icons/src/wallets/TalismanIcon.tsx b/libs/icons/src/wallets/TalismanIcon.tsx index 1be99d200a..ddb74416e7 100644 --- a/libs/icons/src/wallets/TalismanIcon.tsx +++ b/libs/icons/src/wallets/TalismanIcon.tsx @@ -4,26 +4,28 @@ import { IconBase } from '../types'; export const TalismanIcon = (props: IconBase) => { return createIcon({ ...props, - size: props.size ?? 'lg', + size: props.size ?? 'lg', // Override the default size to `lg` (24px) displayName: 'TalismanIcon', path: ( - - - - - - - - + <> + + + + + + + + ), }); }; diff --git a/libs/icons/src/wallets/index.ts b/libs/icons/src/wallets/index.ts index 517c7934ec..c9db822c73 100644 --- a/libs/icons/src/wallets/index.ts +++ b/libs/icons/src/wallets/index.ts @@ -1,8 +1,10 @@ export * from './CoinbaseIcon'; export * from './KeplrIcon'; export * from './MetaMaskIcon'; +export * from './PolkadotJsIcon'; export * from './RainbowIcon'; export * from './SafeIcon'; +export * from './SubWalletIcon'; export * from './TalismanIcon'; export * from './TrustWalletIcon'; export * from './WalletConnectIcon'; diff --git a/libs/tangle-shared-ui/src/components/EvmWalletModal/EvmWalletModal.tsx b/libs/tangle-shared-ui/src/components/EvmWalletModal/EvmWalletModal.tsx index ea7d94129f..797871aebf 100644 --- a/libs/tangle-shared-ui/src/components/EvmWalletModal/EvmWalletModal.tsx +++ b/libs/tangle-shared-ui/src/components/EvmWalletModal/EvmWalletModal.tsx @@ -24,12 +24,22 @@ const EvmWalletModal: FC = ({ isOpen, onClose }) => { const [connectingId, setConnectingId] = useState(null); // Filter and deduplicate connectors + // Exclude Substrate-only wallets (they're only for the claim migration page) const availableConnectors = useMemo(() => { const seen = new Set(); + const substrateWalletNames = ['subwallet', 'talisman', 'polkadot']; + return connectors.filter((connector) => { // Skip duplicates by name if (seen.has(connector.name)) return false; seen.add(connector.name); + + // Skip Substrate wallets - they're only for claim migration page + const lowerName = connector.name.toLowerCase(); + if (substrateWalletNames.some((name) => lowerName.includes(name))) { + return false; + } + return true; }); }, [connectors]);