From 97359d8a9d4a83d81352ec40f84a2e6a364a0f22 Mon Sep 17 00:00:00 2001 From: Rinat Fihtengolts <9-b-rinat@rambler.ru> Date: Wed, 27 Aug 2025 22:58:50 +0300 Subject: [PATCH 001/141] feat: use hardcoded chains (#1117) * feat: hardcoded chains for keplr * chore: self review --- apps/shell/messages/common/en.json | 2 +- .../src/components/web3-connect-button.tsx | 44 +++++++++++-------- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/apps/shell/messages/common/en.json b/apps/shell/messages/common/en.json index d7481bc28..fbf41fa0f 100644 --- a/apps/shell/messages/common/en.json +++ b/apps/shell/messages/common/en.json @@ -95,7 +95,7 @@ "unbonding-amount": "Unbonding: {amount}", "undelegate": "Undelegate", "unsupported-network-message": "Your current action cannot be performed as the application is connected to an unsupported network. Please select one of the supported networks from the list below to proceed.", - "unsupported-network-title": "Unsupported
Network", + "unsupported-network-title": "Unsupported Network", "version": "version: ", "vested-amount": "Vested: {amount}", "vote-option-abstain": "Abstain", diff --git a/apps/shell/src/components/web3-connect-button.tsx b/apps/shell/src/components/web3-connect-button.tsx index 4581890a9..f547da2e3 100644 --- a/apps/shell/src/components/web3-connect-button.tsx +++ b/apps/shell/src/components/web3-connect-button.tsx @@ -1,4 +1,5 @@ 'use client'; +import { useMemo } from 'react'; import { useTranslate } from '@tolgee/react'; import { useAccount, useChains } from 'wagmi'; import { @@ -9,14 +10,37 @@ import { } from '@haqq/shell-shared'; import { Button, AccountButton, SelectChainButton } from '@haqq/shell-ui-kit'; import { formatNumber } from '@haqq/shell-ui-kit/server'; +import { supportedChains } from '../config/wagmi-config'; + +function useChainArray() { + const chains = useChains(); + + return useMemo(() => { + if (chains.length === 0) { + return supportedChains.map((chain) => { + return { + id: chain.id, + name: chain.name, + }; + }); + } + + return chains.map((chain) => { + return { + id: chain.id, + name: chain.name, + }; + }); + }, [chains]); +} export function Web3ConnectButtons() { const { t } = useTranslate('common'); const { isConnected, chain } = useAccount(); const { haqqAddress, ethAddress } = useAddress(); - const chains = useChains(); const { openSelectWallet, disconnect, selectNetwork } = useWallet(); const { data: balance } = useIndexerBalanceQuery(haqqAddress); + const chainArray = useChainArray(); console.log({ balance }); if (!isConnected || !ethAddress) { @@ -29,14 +53,6 @@ export function Web3ConnectButtons() { ); } - const chainArray = chains.map((chain) => { - return { - id: chain.id, - name: chain.name, - // name: chain.name.replace('HAQQ', '').trim(), - }; - }); - return (
@@ -71,9 +87,9 @@ export function Web3ConnectButtonsMobile() { const { t } = useTranslate('common'); const { isConnected, chain } = useAccount(); const { haqqAddress, ethAddress } = useAddress(); - const chains = useChains(); const { openSelectWallet, disconnect, selectNetwork } = useWallet(); const { data: balance } = useIndexerBalanceQuery(haqqAddress); + const chainArray = useChainArray(); if (!isConnected || !ethAddress) { return ( @@ -85,14 +101,6 @@ export function Web3ConnectButtonsMobile() { ); } - const chainArray = chains.map((chain) => { - return { - id: chain.id, - name: chain.name, - // name: chain.name.replace('HAQQ', '').trim(), - }; - }); - return (
From d5c290b5859dd9cf20d3daec7023ec36dcf93db4 Mon Sep 17 00:00:00 2001 From: Rinat Fihtengolts <9-b-rinat@rambler.ru> Date: Tue, 30 Sep 2025 18:12:16 +0300 Subject: [PATCH 002/141] feat (HQI-1973): L2 bridge (#1119) * chore: savepoint * chore: savepoint * feat: erc20 bridge * chore: self review * fix: devnet url * feat: add devnet * chore: self review * fix: addresses * feat: tokens fetcher * chore: savepoint * chore: api for tokens * fix: bridge allowance * chore: savepoint * feat: use bridge state * chore: self review * fix: balances from sepolia * chore: prettier * fix: explorer for devnet * feat: https * feat: token deployment page * fix: rpc url * fix: balances and token deployment * chore: cleanup logs * feat: logs parser * feat: correct handle remote token address * chore: self review * chore: precommit hook * chore: handle allowance with timers * chore: savepoint * feat: l2 to l1 orders list * chore: self review for bridge tokens/links * chore: self review * feat: correct devnet configs for time to prove checks * chore: correct chain switch for proving * chore: correct finilize steps * chore: review * feat: self review UI for bridge page * fix: header btns for bridge page * fix: reloading timers * chore: self review * chore: replace local storage usage * chore: review comments --- .../app/[locale]/bridge/deploy-token/page.tsx | 21 + apps/shell/src/app/[locale]/bridge/page.tsx | 21 + .../src/app/api/tokens/balances/route.ts | 135 ++ .../app/api/tokens/native-balance/route.ts | 110 ++ .../src/components/web3-connect-button.tsx | 57 +- apps/shell/src/config/wagmi-config.ts | 12 +- libs/bridge/.eslintrc.json | 18 + libs/bridge/README.md | 7 + libs/bridge/jest.config.ts | 18 + libs/bridge/project.json | 9 + libs/bridge/src/index.ts | 5 + libs/bridge/src/lib/abi/erc20-factory.ts | 141 ++ libs/bridge/src/lib/bridge-page.tsx | 398 ++++ .../lib/components/bridge-amount-input.tsx | 37 + .../bridge/src/lib/components/bridge-form.tsx | 134 ++ .../lib/components/bridge-receive-input.tsx | 29 + .../lib/components/bridge-status-messages.tsx | 140 ++ .../lib/components/bridge-success-message.tsx | 15 + .../components/challenge-period-warning.tsx | 37 + libs/bridge/src/lib/components/index.ts | 26 + .../components/network-mismatch-warning.tsx | 24 + .../lib/components/pending-withdrawals.tsx | 43 + .../lib/components/token-deployment-page.tsx | 431 +++++ .../src/lib/components/token-selector.tsx | 182 ++ .../components/wallet-connection-warning.tsx | 11 + .../lib/components/withdrawal-order-card.tsx | 353 ++++ .../src/lib/constants/op-stack-config.ts | 19 + libs/bridge/src/lib/hooks/index.ts | 12 + libs/bridge/src/lib/hooks/use-bridge-state.ts | 223 +++ .../src/lib/hooks/use-bridge-token-manager.ts | 244 +++ .../src/lib/hooks/use-bridge-transaction.ts | 177 ++ .../src/lib/hooks/use-bridge-url-state.ts | 122 ++ .../src/lib/hooks/use-l2-to-l1-withdrawal.ts | 353 ++++ .../src/lib/hooks/use-op-stack-clients.ts | 117 ++ .../src/lib/hooks/use-token-allowance.ts | 89 + .../src/lib/hooks/use-token-approval.ts | 99 + .../src/lib/hooks/use-token-balances.ts | 64 + .../src/lib/hooks/use-token-deployment.ts | 111 ++ .../src/lib/hooks/use-withdrawal-orders.ts | 167 ++ .../src/lib/hooks/use-withdrawal-timers.ts | 180 ++ libs/bridge/src/lib/services/explorer-api.ts | 146 ++ libs/bridge/src/lib/services/scanner-api.ts | 157 ++ libs/bridge/src/lib/types/withdrawal-order.ts | 39 + libs/bridge/src/server.ts | 1 + libs/bridge/tsconfig.json | 23 + libs/bridge/tsconfig.lib.json | 25 + libs/bridge/tsconfig.spec.json | 20 + libs/shared/src/abi/L1StandartBridge.ts | 52 + .../use-local-storage/use-local-storage.ts | 41 - libs/shared/src/index.ts | 5 +- libs/shared/src/utils/bridge.ts | 317 ++++ libs/shared/src/utils/chain-utils.ts | 33 + libs/ui-kit/src/lib/modal-input.tsx | 3 + lint-staged.config.js | 2 +- package.json | 5 +- pnpm-lock.yaml | 1624 ++++++++++++----- tsconfig.base.json | 1 + 57 files changed, 6375 insertions(+), 510 deletions(-) create mode 100644 apps/shell/src/app/[locale]/bridge/deploy-token/page.tsx create mode 100644 apps/shell/src/app/[locale]/bridge/page.tsx create mode 100644 apps/shell/src/app/api/tokens/balances/route.ts create mode 100644 apps/shell/src/app/api/tokens/native-balance/route.ts create mode 100644 libs/bridge/.eslintrc.json create mode 100644 libs/bridge/README.md create mode 100644 libs/bridge/jest.config.ts create mode 100644 libs/bridge/project.json create mode 100644 libs/bridge/src/index.ts create mode 100644 libs/bridge/src/lib/abi/erc20-factory.ts create mode 100644 libs/bridge/src/lib/bridge-page.tsx create mode 100644 libs/bridge/src/lib/components/bridge-amount-input.tsx create mode 100644 libs/bridge/src/lib/components/bridge-form.tsx create mode 100644 libs/bridge/src/lib/components/bridge-receive-input.tsx create mode 100644 libs/bridge/src/lib/components/bridge-status-messages.tsx create mode 100644 libs/bridge/src/lib/components/bridge-success-message.tsx create mode 100644 libs/bridge/src/lib/components/challenge-period-warning.tsx create mode 100644 libs/bridge/src/lib/components/index.ts create mode 100644 libs/bridge/src/lib/components/network-mismatch-warning.tsx create mode 100644 libs/bridge/src/lib/components/pending-withdrawals.tsx create mode 100644 libs/bridge/src/lib/components/token-deployment-page.tsx create mode 100644 libs/bridge/src/lib/components/token-selector.tsx create mode 100644 libs/bridge/src/lib/components/wallet-connection-warning.tsx create mode 100644 libs/bridge/src/lib/components/withdrawal-order-card.tsx create mode 100644 libs/bridge/src/lib/constants/op-stack-config.ts create mode 100644 libs/bridge/src/lib/hooks/index.ts create mode 100644 libs/bridge/src/lib/hooks/use-bridge-state.ts create mode 100644 libs/bridge/src/lib/hooks/use-bridge-token-manager.ts create mode 100644 libs/bridge/src/lib/hooks/use-bridge-transaction.ts create mode 100644 libs/bridge/src/lib/hooks/use-bridge-url-state.ts create mode 100644 libs/bridge/src/lib/hooks/use-l2-to-l1-withdrawal.ts create mode 100644 libs/bridge/src/lib/hooks/use-op-stack-clients.ts create mode 100644 libs/bridge/src/lib/hooks/use-token-allowance.ts create mode 100644 libs/bridge/src/lib/hooks/use-token-approval.ts create mode 100644 libs/bridge/src/lib/hooks/use-token-balances.ts create mode 100644 libs/bridge/src/lib/hooks/use-token-deployment.ts create mode 100644 libs/bridge/src/lib/hooks/use-withdrawal-orders.ts create mode 100644 libs/bridge/src/lib/hooks/use-withdrawal-timers.ts create mode 100644 libs/bridge/src/lib/services/explorer-api.ts create mode 100644 libs/bridge/src/lib/services/scanner-api.ts create mode 100644 libs/bridge/src/lib/types/withdrawal-order.ts create mode 100644 libs/bridge/src/server.ts create mode 100644 libs/bridge/tsconfig.json create mode 100644 libs/bridge/tsconfig.lib.json create mode 100644 libs/bridge/tsconfig.spec.json create mode 100644 libs/shared/src/abi/L1StandartBridge.ts delete mode 100644 libs/shared/src/hooks/use-local-storage/use-local-storage.ts create mode 100644 libs/shared/src/utils/bridge.ts create mode 100644 libs/shared/src/utils/chain-utils.ts diff --git a/apps/shell/src/app/[locale]/bridge/deploy-token/page.tsx b/apps/shell/src/app/[locale]/bridge/deploy-token/page.tsx new file mode 100644 index 000000000..3abce318b --- /dev/null +++ b/apps/shell/src/app/[locale]/bridge/deploy-token/page.tsx @@ -0,0 +1,21 @@ +import { + HydrationBoundary, + QueryClient, + dehydrate, +} from '@tanstack/react-query'; +import { TokenDeploymentPage } from '@haqq/shell-bridge'; + +export const dynamic = 'force-dynamic'; +export const fetchCache = 'force-no-store'; + +export default async function TokenDeployment() { + const queryClient = new QueryClient(); + + const dehydratedState = dehydrate(queryClient); + + return ( + + + + ); +} diff --git a/apps/shell/src/app/[locale]/bridge/page.tsx b/apps/shell/src/app/[locale]/bridge/page.tsx new file mode 100644 index 000000000..89e3b3b06 --- /dev/null +++ b/apps/shell/src/app/[locale]/bridge/page.tsx @@ -0,0 +1,21 @@ +import { + HydrationBoundary, + QueryClient, + dehydrate, +} from '@tanstack/react-query'; +import { BridgePage } from '@haqq/shell-bridge'; + +export const dynamic = 'force-dynamic'; +export const fetchCache = 'force-no-store'; + +export default async function ValidatorList() { + const queryClient = new QueryClient(); + + const dehydratedState = dehydrate(queryClient); + + return ( + + + + ); +} diff --git a/apps/shell/src/app/api/tokens/balances/route.ts b/apps/shell/src/app/api/tokens/balances/route.ts new file mode 100644 index 000000000..87a29106a --- /dev/null +++ b/apps/shell/src/app/api/tokens/balances/route.ts @@ -0,0 +1,135 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { haqqMainnet, haqqTestedge2, sepolia } from 'viem/chains'; +import { haqqDevnet1 } from '@haqq/shell-shared'; + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const address = searchParams.get('address'); + const chainId = searchParams.get('chainId'); + + if (!address || !chainId) { + return NextResponse.json( + { error: 'Missing required parameters: address and chainId' }, + { status: 400 }, + ); + } + + // Get chain configuration + const chainConfig = getChainConfig(parseInt(chainId)); + if (!chainConfig) { + return NextResponse.json( + { error: `Unsupported chain ID: ${chainId}` }, + { status: 400 }, + ); + } + + // Make the API request to the explorer + const url = `${chainConfig.apiUrl}/v2/addresses/${address}/tokens?type=ERC-20`; + + console.log('Explorer API URL:', url); + + const response = await fetch(url, { + headers: { + Accept: 'application/json', + }, + }); + + console.log('Explorer API response status:', response.status); + + if (!response.ok) { + console.log('Explorer API error:', response.status, response.statusText); + + return NextResponse.json( + { + error: `Explorer API error: ${response.status} ${response.statusText}`, + }, + { status: response.status }, + ); + } + + const data = await response.json(); + + console.log('Explorer API data items count:', data.items?.length || 0); + + // Filter out tokens with zero balance + const filteredTokens = data.items.filter((item: any) => { + return item.value !== '0' && item.value !== '0x0'; + }); + + console.log( + `Found ${filteredTokens.length} tokens with non-zero balance out of ${data.items.length} total tokens`, + ); + + // Process token balances + const tokenBalances = filteredTokens.map((item: any) => { + const decimals = parseInt(item.token.decimals, 10); + const balanceWei = BigInt(item.value); + const formattedBalance = Number(balanceWei) / Math.pow(10, decimals); + + return { + symbol: item.token.symbol, + address: item.token.address_hash || item.token.address, + name: item.token.name, + balance: item.value, + decimals, + formattedBalance, + }; + }); + + console.log( + 'Processed token balances:', + tokenBalances.map((t: any) => { + return `${t.symbol}: ${t.formattedBalance}`; + }), + ); + + return NextResponse.json({ + tokens: tokenBalances, + total: filteredTokens.length, + }); + } catch (error) { + console.error('Error in token balances API:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 }, + ); + } +} + +interface ChainConfig { + apiUrl: string; + nativeSymbol: string; + nativeName: string; +} + +function getChainConfig(chainId: number): ChainConfig | null { + const configs: Record = { + [haqqDevnet1.id]: { + // HAQQ Devnet1 + apiUrl: haqqDevnet1.blockExplorers.default.apiUrl, + nativeSymbol: 'ISLM', + nativeName: 'Islamic Coin', + }, + [sepolia.id]: { + // Sepolia + apiUrl: 'https://eth-sepolia.blockscout.com/api', + nativeSymbol: 'ETH', + nativeName: 'Ethereum', + }, + [haqqMainnet.id]: { + // HAQQ Mainnet + apiUrl: haqqMainnet.blockExplorers.default.apiUrl, + nativeSymbol: 'ISLM', + nativeName: 'Islamic Coin', + }, + [haqqTestedge2.id]: { + // HAQQ Testedge2 + apiUrl: haqqTestedge2.blockExplorers.default.apiUrl, + nativeSymbol: 'ISLM', + nativeName: 'Islamic Coin', + }, + }; + + return configs[chainId] || null; +} diff --git a/apps/shell/src/app/api/tokens/native-balance/route.ts b/apps/shell/src/app/api/tokens/native-balance/route.ts new file mode 100644 index 000000000..ed76f3ba4 --- /dev/null +++ b/apps/shell/src/app/api/tokens/native-balance/route.ts @@ -0,0 +1,110 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { createPublicClient, http, formatEther } from 'viem'; +import { haqqMainnet, haqqTestedge2, sepolia } from 'viem/chains'; +import { haqqDevnet1 } from '@haqq/shell-shared'; + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const address = searchParams.get('address'); + const chainId = searchParams.get('chainId'); + + if (!address || !chainId) { + return NextResponse.json( + { error: 'Missing required parameters: address and chainId' }, + { status: 400 }, + ); + } + + // Get chain configuration + const chainConfig = getChainConfig(parseInt(chainId)); + if (!chainConfig) { + return NextResponse.json( + { error: `Unsupported chain ID: ${chainId}` }, + { status: 400 }, + ); + } + + // Create RPC client for the chain + const client = createPublicClient({ + chain: chainConfig.chain, + transport: http(chainConfig.rpcUrl), + }); + + try { + // Get native token balance using RPC + const balance = await client.getBalance({ + address: address as `0x${string}`, + }); + + const formattedBalance = parseFloat(formatEther(balance)); + + const nativeToken = { + symbol: chainConfig.nativeSymbol, + address: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', // ETH placeholder + name: chainConfig.nativeName, + balance: balance.toString(), + decimals: 18, + formattedBalance, + }; + + console.log( + `Native token balance: ${formattedBalance} ${chainConfig.nativeSymbol}`, + ); + + return NextResponse.json({ + token: nativeToken, + }); + } catch (rpcError) { + console.error('RPC error:', rpcError); + return NextResponse.json( + { error: 'Failed to fetch native token balance via RPC' }, + { status: 500 }, + ); + } + } catch (error) { + console.error('Error in native token balance API:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 }, + ); + } +} + +interface ChainConfig { + chain: any; + rpcUrl: string; + nativeSymbol: string; + nativeName: string; +} + +function getChainConfig(chainId: number): ChainConfig | null { + const configs: Record = { + [haqqDevnet1.id]: { + chain: haqqDevnet1, + rpcUrl: haqqDevnet1.rpcUrls.default.http[0], + nativeSymbol: 'ISLM', + nativeName: 'Islamic Coin', + }, + [sepolia.id]: { + chain: sepolia, + rpcUrl: sepolia.rpcUrls.default.http[0], + nativeSymbol: 'ETH', + nativeName: 'Ethereum', + }, + [haqqMainnet.id]: { + chain: haqqMainnet, + rpcUrl: haqqMainnet.rpcUrls.default.http[0], + nativeSymbol: 'ISLM', + nativeName: 'Islamic Coin', + }, + [haqqTestedge2.id]: { + chain: haqqTestedge2, + rpcUrl: haqqTestedge2.rpcUrls.default.http[0], + nativeSymbol: 'ISLM', + nativeName: 'Islamic Coin', + }, + }; + + return configs[chainId] || null; +} diff --git a/apps/shell/src/components/web3-connect-button.tsx b/apps/shell/src/components/web3-connect-button.tsx index f547da2e3..87fd57457 100644 --- a/apps/shell/src/components/web3-connect-button.tsx +++ b/apps/shell/src/components/web3-connect-button.tsx @@ -1,6 +1,7 @@ 'use client'; import { useMemo } from 'react'; import { useTranslate } from '@tolgee/react'; +import { usePathname } from 'next/navigation'; import { useAccount, useChains } from 'wagmi'; import { getFormattedAddress, @@ -10,14 +11,26 @@ import { } from '@haqq/shell-shared'; import { Button, AccountButton, SelectChainButton } from '@haqq/shell-ui-kit'; import { formatNumber } from '@haqq/shell-ui-kit/server'; -import { supportedChains } from '../config/wagmi-config'; +import { supportedChains, bridgeSupportedChains } from '../config/wagmi-config'; + +function useIsBridgePage() { + const pathname = usePathname(); + return useMemo(() => { + return pathname?.startsWith('/bridge'); + }, [pathname]); +} function useChainArray() { const chains = useChains(); + const isBridgePage = useIsBridgePage(); return useMemo(() => { + const availableChains = isBridgePage + ? bridgeSupportedChains + : supportedChains; + if (chains.length === 0) { - return supportedChains.map((chain) => { + return availableChains.map((chain) => { return { id: chain.id, name: chain.name, @@ -25,13 +38,13 @@ function useChainArray() { }); } - return chains.map((chain) => { + return (isBridgePage ? bridgeSupportedChains : chains).map((chain) => { return { id: chain.id, name: chain.name, }; }); - }, [chains]); + }, [chains, isBridgePage]); } export function Web3ConnectButtons() { @@ -41,7 +54,7 @@ export function Web3ConnectButtons() { const { openSelectWallet, disconnect, selectNetwork } = useWallet(); const { data: balance } = useIndexerBalanceQuery(haqqAddress); const chainArray = useChainArray(); - console.log({ balance }); + const isBridgePage = useIsBridgePage(); if (!isConnected || !ethAddress) { return ( @@ -72,13 +85,15 @@ export function Web3ConnectButtons() { chains={chainArray} />
-
- -
+ {!isBridgePage && ( +
+ +
+ )}
); } @@ -91,6 +106,8 @@ export function Web3ConnectButtonsMobile() { const { data: balance } = useIndexerBalanceQuery(haqqAddress); const chainArray = useChainArray(); + const isBridgePage = useIsBridgePage(); + if (!isConnected || !ethAddress) { return (
@@ -121,13 +138,15 @@ export function Web3ConnectButtonsMobile() { dropdownClassName="end-auto start-0" />
-
- -
+ {!isBridgePage && ( +
+ +
+ )}
diff --git a/apps/shell/src/config/wagmi-config.ts b/apps/shell/src/config/wagmi-config.ts index 46be24bd4..5e31f2d47 100644 --- a/apps/shell/src/config/wagmi-config.ts +++ b/apps/shell/src/config/wagmi-config.ts @@ -6,10 +6,18 @@ import { cookieStorage, CreateConnectorFn, } from 'wagmi'; -import { haqqMainnet, haqqTestedge2 } from 'wagmi/chains'; +import { haqqMainnet, haqqTestedge2, sepolia } from 'wagmi/chains'; import { safe, walletConnect } from 'wagmi/connectors'; +import { haqqDevnet1 } from '@haqq/shell-shared'; + +export const bridgeSupportedChains = [haqqDevnet1, sepolia]; + +export const supportedChains = [ + haqqMainnet, + haqqTestedge2, + ...bridgeSupportedChains, +] as const; -export const supportedChains = [haqqMainnet, haqqTestedge2] as const; export const supportedChainsIds = supportedChains.map((chain): number => { return chain.id; }); diff --git a/libs/bridge/.eslintrc.json b/libs/bridge/.eslintrc.json new file mode 100644 index 000000000..3ebb9c6f3 --- /dev/null +++ b/libs/bridge/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nx/react", "../../.eslintrc.base.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/bridge/README.md b/libs/bridge/README.md new file mode 100644 index 000000000..6c5e15a0c --- /dev/null +++ b/libs/bridge/README.md @@ -0,0 +1,7 @@ +# shell-bridge + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test shell-bridge` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/bridge/jest.config.ts b/libs/bridge/jest.config.ts new file mode 100644 index 000000000..b86376332 --- /dev/null +++ b/libs/bridge/jest.config.ts @@ -0,0 +1,18 @@ +/* eslint-disable */ +export default { + displayName: 'shell-staking', + preset: '../../jest.preset.js', + transform: { + '^.+\\.[tj]sx?$': [ + '@swc/jest', + { + jsc: { + parser: { syntax: 'typescript', tsx: true }, + transform: { react: { runtime: 'automatic' } }, + }, + }, + ], + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../coverage/libs/staking', +}; diff --git a/libs/bridge/project.json b/libs/bridge/project.json new file mode 100644 index 000000000..2fe63b100 --- /dev/null +++ b/libs/bridge/project.json @@ -0,0 +1,9 @@ +{ + "name": "shell-bridge", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/bridge/src", + "projectType": "library", + "tags": ["shell-lib-bridge"], + "// targets": "to see all targets run: nx show project shell-bridge --web", + "targets": {} +} diff --git a/libs/bridge/src/index.ts b/libs/bridge/src/index.ts new file mode 100644 index 000000000..5a6fdc228 --- /dev/null +++ b/libs/bridge/src/index.ts @@ -0,0 +1,5 @@ +export * from './lib/bridge-page'; +export * from './lib/hooks'; +export * from './lib/services/explorer-api'; +export * from './lib/services/scanner-api'; +export * from './lib/components/token-deployment-page'; diff --git a/libs/bridge/src/lib/abi/erc20-factory.ts b/libs/bridge/src/lib/abi/erc20-factory.ts new file mode 100644 index 000000000..06f3dd779 --- /dev/null +++ b/libs/bridge/src/lib/abi/erc20-factory.ts @@ -0,0 +1,141 @@ +export const ERC20FactoryAbi = [ + { + inputs: [ + { + internalType: 'address', + name: '_bridge', + type: 'address', + }, + ], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'localToken', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'remoteToken', + type: 'address', + }, + { + indexed: false, + internalType: 'address', + name: 'deployer', + type: 'address', + }, + ], + name: 'OptimismMintableERC20Created', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'remoteToken', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'localToken', + type: 'address', + }, + ], + name: 'StandardL2TokenCreated', + type: 'event', + }, + { + inputs: [], + name: 'BRIDGE', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: '_remoteToken', + type: 'address', + }, + { + internalType: 'string', + name: '_name', + type: 'string', + }, + { + internalType: 'string', + name: '_symbol', + type: 'string', + }, + ], + name: 'createOptimismMintableERC20', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: '_remoteToken', + type: 'address', + }, + { + internalType: 'string', + name: '_name', + type: 'string', + }, + { + internalType: 'string', + name: '_symbol', + type: 'string', + }, + ], + name: 'createStandardL2Token', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'version', + outputs: [ + { + internalType: 'string', + name: '', + type: 'string', + }, + ], + stateMutability: 'view', + type: 'function', + }, +]; diff --git a/libs/bridge/src/lib/bridge-page.tsx b/libs/bridge/src/lib/bridge-page.tsx new file mode 100644 index 000000000..5513932b4 --- /dev/null +++ b/libs/bridge/src/lib/bridge-page.tsx @@ -0,0 +1,398 @@ +'use client'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useTranslate } from '@tolgee/react'; +import { useRouter } from 'next/navigation'; +import { useSwitchChain, useWaitForTransactionReceipt } from 'wagmi'; +import { + L1_STANDARD_BRIDGE_ADDRESS, + BRIDGE_ADDRESSES, + CHAIN_CONFIG, + SUPPORTED_CHAINS, + L2_STANDARD_BRIDGE_ADDRESS, +} from '@haqq/shell-shared'; +import { Container } from '@haqq/shell-ui-kit/server'; +import { + WalletConnectionWarning, + NetworkMismatchWarning, + BridgeForm, + BridgeStatusMessages, + ChallengePeriodWarning, + PendingWithdrawals, +} from './components'; +import { + useBridgeState, + useTokenAllowance, + useTokenApproval, + useBridgeTransaction, + useBridgeTokenManager, + useBridgeUrlState, + useWithdrawalOrders, +} from './hooks'; + +// SUPPORTED_CHAINS is now imported from @haqq/shell-shared + +export const useChainProxyAddress = (chainId: number) => { + if (chainId === CHAIN_CONFIG.l1ChainId) { + return L1_STANDARD_BRIDGE_ADDRESS; + } + if (chainId === CHAIN_CONFIG.l2ChainId) { + return L2_STANDARD_BRIDGE_ADDRESS; + } + return ''; +}; + +export function BridgePage() { + const { t } = useTranslate('common'); + const router = useRouter(); + const { switchChainAsync } = useSwitchChain(); + + // URL state management + const { updateUrlState, buildDeploymentUrl, clearUrlState } = + useBridgeUrlState(); + + // State management + const [txHash, setTxHash] = useState(null); + + // Withdrawal orders management + const { pendingOrders } = useWithdrawalOrders(); + + // Use bridge state hook + const { + address, + chain, + isConnected, + availableTokens, + selectedToken, + bridgeAmount, + receivedAmount, + availableBalance, + isLoadingTokens, + tokensError, + balance, + formatNumber, + handleInputChange, + handleMaxButtonClick, + handleTokenSelect, + } = useBridgeState(); + + // Determine source and target chains + const sourceChainId = chain?.id; + const targetChainId = useMemo(() => { + // For now, assume L1 -> L2 bridging (Sepolia -> HAQQ Testedge2) + if (sourceChainId === CHAIN_CONFIG.l1ChainId) { + return CHAIN_CONFIG.l2ChainId; + } + // For L2 -> L1 bridging + if (sourceChainId === CHAIN_CONFIG.l2ChainId) { + return CHAIN_CONFIG.l1ChainId; + } + // Default fallback + return CHAIN_CONFIG.l2ChainId; + }, [sourceChainId]); + + // Check if this is an L2 to L1 transfer + const isL2ToL1 = useMemo(() => { + return ( + sourceChainId === CHAIN_CONFIG.l2ChainId && + targetChainId === CHAIN_CONFIG.l1ChainId + ); + }, [sourceChainId, targetChainId]); + + // Sync URL state with bridge state + useEffect(() => { + if (selectedToken && chain?.id && bridgeAmount) { + updateUrlState({ + tokenIn: selectedToken.address, + chainIn: chain.id, + chainOut: targetChainId, + amount: bridgeAmount.toString(), + }); + } + }, [selectedToken, chain?.id, targetChainId, bridgeAmount, updateUrlState]); + + const bridgeAddress = useChainProxyAddress(sourceChainId); + + // Use bridge token manager for token validation + const { + remoteTokenAddress, + needsDeployment, + isCheckingRemoteToken, + getRemoteTokenForBridge, + } = useBridgeTokenManager({ + localToken: selectedToken || undefined, + factoryAddress: + BRIDGE_ADDRESSES.opChainDeployment + .optimismMintableERC20FactoryProxyAddress, + sourceChainId, + targetChainId, + }); + + // Use allowance hook + const { needsApproval, refetch: refetchAllowance } = useTokenAllowance({ + tokenAddress: selectedToken?.address, + ownerAddress: address, + spenderAddress: bridgeAddress, + bridgeAmount, + tokenDecimals: selectedToken?.decimals || balance?.decimals || 18, + }); + + const [tId, setTId] = useState(null); + + // Use approval hook + const { approve, isApproving } = useTokenApproval({ + tokenAddress: selectedToken?.address, + spenderAddress: bridgeAddress, + onSuccess: (hash) => { + console.log('Approval successful:', hash); + + if (tId) { + clearTimeout(tId); + } + // Refetch allowance after successful approval + const newTId = setTimeout(() => { + refetchAllowance(); + }, 3000); + + setTId(newTId); + }, + onError: (error) => { + console.error('Approval failed:', error); + }, + }); + + // Use bridge transaction hook + const { bridgeTokens, isProcessing, isProving, isFinalizing } = + useBridgeTransaction({ + bridgeAddress: bridgeAddress, + sourceChainId, + targetChainId, + onSuccess: (hash) => { + console.log('Bridge successful:', hash); + setTxHash(hash); + }, + onError: (error) => { + console.error('Bridge failed:', error); + }, + onProveSuccess: (hash) => { + console.log('Prove successful:', hash); + }, + onFinalizeSuccess: (hash) => { + console.log('Finalize successful:', hash); + }, + }); + + // Wait for transaction receipt + const { isLoading: isWaitingForReceipt, isSuccess: isTxSuccess } = + useWaitForTransactionReceipt({ + hash: txHash as `0x${string}` | undefined, + }); + + // Memoize isChainMismatch for performance and referential stability + const isChainMismatch = useMemo(() => { + return ( + chain != null && + !SUPPORTED_CHAINS.some((supportedChain) => { + return supportedChain.id === chain.id; + }) + ); + }, [chain]); + + const targetChainIdNumber = useMemo(() => { + return isChainMismatch ? SUPPORTED_CHAINS[0].id : chain?.id; + }, [chain, isChainMismatch]); + + // Memoize canBridge for performance + const canBridge = useMemo(() => { + return ( + isConnected && + selectedToken && + bridgeAmount !== undefined && + bridgeAmount > 0 && + bridgeAmount <= availableBalance && + !isChainMismatch && + !isCheckingRemoteToken + ); + }, [ + isConnected, + selectedToken, + bridgeAmount, + availableBalance, + isChainMismatch, + isCheckingRemoteToken, + ]); + + const amountHint = useMemo(() => { + if (!bridgeAmount) { + return ( + + Available: {formatNumber(availableBalance)}{' '} + {selectedToken?.symbol || 'ETH'} + + ); + } + + if (bridgeAmount > availableBalance) { + return ( + + {t('insufficient-balance', 'Insufficient balance')} + + ); + } + + return ( + + Available: {formatNumber(availableBalance)}{' '} + {selectedToken?.symbol || 'ETH'} + + ); + }, [bridgeAmount, availableBalance, selectedToken, formatNumber, t]); + + const handleApprove = useCallback(async () => { + if (!selectedToken || !bridgeAmount) return; + + const tokenDecimals = selectedToken.decimals || balance?.decimals || 18; + await approve(bridgeAmount, tokenDecimals); + }, [selectedToken, bridgeAmount, balance, approve]); + + const handleTokenDeployment = useCallback(() => { + if (!needsDeployment || !selectedToken || !sourceChainId || !targetChainId) + return; + + // Redirect to deployment page instead of deploying inline + const deploymentUrl = buildDeploymentUrl( + selectedToken.address, + sourceChainId, + targetChainId, + ); + router.push(deploymentUrl); + }, [ + needsDeployment, + selectedToken, + sourceChainId, + targetChainId, + buildDeploymentUrl, + router, + ]); + + const handleBridge = useCallback(async () => { + if (!canBridge || !address || !bridgeAmount || !selectedToken) return; + + // Check if token needs deployment first + if (needsDeployment) { + // Redirect to deployment page + if (!sourceChainId) { + console.error('Source chain ID is required for deployment'); + return; + } + const deploymentUrl = buildDeploymentUrl( + selectedToken.address, + sourceChainId, + targetChainId, + ); + router.push(deploymentUrl); + return; + } + + // Proceed with bridge if remote token exists + try { + const remoteTokenAddr = await getRemoteTokenForBridge(); + console.log('Remote token address for bridging:', remoteTokenAddr); + + const fallbackDecimals = balance?.decimals || 18; + await bridgeTokens( + selectedToken, + bridgeAmount, + address, + remoteTokenAddr, + fallbackDecimals, + ); + } catch (error) { + console.error('Bridge operation failed:', error); + } + }, [ + canBridge, + address, + bridgeAmount, + selectedToken, + balance, + bridgeTokens, + getRemoteTokenForBridge, + needsDeployment, + buildDeploymentUrl, + targetChainId, + router, + ]); + + const handleSwitchChain = useCallback(async () => { + if (!switchChainAsync || !targetChainIdNumber) return; + + try { + await switchChainAsync({ chainId: targetChainIdNumber }); + // Reset URL query parameters after successful chain switch + clearUrlState(); + } catch (error) { + console.error('Failed to switch chain:', error); + } + }, [switchChainAsync, targetChainIdNumber, clearUrlState]); + + return ( + +
+
+ {!isConnected && } + + {isChainMismatch && isConnected && ( + + )} + + {isConnected && ( + <> + + + + + + + {isL2ToL1 && pendingOrders.length > 0 && ( + + )} + + )} +
+
+
+ ); +} diff --git a/libs/bridge/src/lib/components/bridge-amount-input.tsx b/libs/bridge/src/lib/components/bridge-amount-input.tsx new file mode 100644 index 000000000..1d6cebfdc --- /dev/null +++ b/libs/bridge/src/lib/components/bridge-amount-input.tsx @@ -0,0 +1,37 @@ +'use client'; +import { ReactNode } from 'react'; +import { ModalInput } from '@haqq/shell-ui-kit'; + +export interface BridgeAmountInputProps { + value: number | undefined; + onChange: (value: string | undefined) => void; + onMaxButtonClick: () => void; + hint: ReactNode; + isMaxButtonDisabled: boolean; + tokenSymbol?: string; +} + +export function BridgeAmountInput({ + value, + onChange, + onMaxButtonClick, + hint, + isMaxButtonDisabled, + tokenSymbol = 'ETH', +}: BridgeAmountInputProps) { + return ( +
+ + +
+ ); +} diff --git a/libs/bridge/src/lib/components/bridge-form.tsx b/libs/bridge/src/lib/components/bridge-form.tsx new file mode 100644 index 000000000..08ab9b01b --- /dev/null +++ b/libs/bridge/src/lib/components/bridge-form.tsx @@ -0,0 +1,134 @@ +'use client'; +import { Button } from '@haqq/shell-ui-kit'; +import { BridgeAmountInput } from './bridge-amount-input'; +import { BridgeReceiveInput } from './bridge-receive-input'; +import { BridgeSuccessMessage } from './bridge-success-message'; +import { TokenSelector } from './token-selector'; + +interface Token { + symbol: string; + address: string; + name?: string; + balance?: string; + decimals?: number; + formattedBalance?: number; +} + +export interface BridgeFormProps { + bridgeAmount: number | undefined; + receivedAmount: number | undefined; + availableBalance: number; + canBridge: boolean; + isProcessing: boolean; + isProving: boolean; + isFinalizing: boolean; + isWaitingForReceipt: boolean; + isTxSuccess: boolean; + onInputChange: (value: string | undefined) => void; + onMaxButtonClick: () => void; + onBridge: () => void; + amountHint: React.ReactNode; + // Token selector props + tokens: Token[]; + selectedToken: Token | null; + onTokenSelect: (token: Token) => void; + isLoadingTokens?: boolean; + // Approval props + needsApproval?: boolean; + isApproving?: boolean; + onApprove?: () => void; +} + +export function BridgeForm({ + bridgeAmount, + receivedAmount, + availableBalance, + canBridge, + isProcessing, + isProving, + isFinalizing, + isWaitingForReceipt, + isTxSuccess, + onInputChange, + onMaxButtonClick, + onBridge, + amountHint, + tokens, + selectedToken, + onTokenSelect, + isLoadingTokens = false, + needsApproval = false, + isApproving = false, + onApprove, +}: BridgeFormProps) { + return ( +
+ + + + + + +
+ {needsApproval && onApprove ? ( + + ) : ( + + )} +
+ + {isTxSuccess && ( + + )} +
+ ); +} diff --git a/libs/bridge/src/lib/components/bridge-receive-input.tsx b/libs/bridge/src/lib/components/bridge-receive-input.tsx new file mode 100644 index 000000000..c2bab7e03 --- /dev/null +++ b/libs/bridge/src/lib/components/bridge-receive-input.tsx @@ -0,0 +1,29 @@ +'use client'; +import { StringInput } from '@haqq/shell-ui-kit'; + +export interface BridgeReceiveInputProps { + receivedAmount: number | undefined; + tokenSymbol: string; +} + +export function BridgeReceiveInput({ + receivedAmount, + tokenSymbol, +}: BridgeReceiveInputProps) { + return ( +
+ + { + // Read-only input + }} + readOnly={true} + placeholder={`0 ${tokenSymbol}`} + className="!bg-[#F5F5F5] !text-[#0D0D0E80]" + /> +
+ ); +} diff --git a/libs/bridge/src/lib/components/bridge-status-messages.tsx b/libs/bridge/src/lib/components/bridge-status-messages.tsx new file mode 100644 index 000000000..0d21b67af --- /dev/null +++ b/libs/bridge/src/lib/components/bridge-status-messages.tsx @@ -0,0 +1,140 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; +import { getAddressExplorerUrl } from '@haqq/shell-shared'; + +interface Token { + symbol: string; + address: string; + name?: string; + decimals?: number; + formattedBalance?: number; +} + +interface BridgeStatusMessagesProps { + tokensError?: string | null; + isLoadingTokens?: boolean; + needsApproval?: boolean; + selectedToken?: Token | null; + isCheckingRemoteToken?: boolean; + needsDeployment?: boolean; + isDeploying?: boolean; + deploymentError?: string | null; + onDeployToken?: () => void; + remoteTokenAddress?: string | null; + remoteTokenChainId?: number; + isProving?: boolean; + isFinalizing?: boolean; +} + +const ETH_TOKEN_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'; + +export function BridgeStatusMessages({ + tokensError, + isLoadingTokens, + needsApproval, + selectedToken, + isCheckingRemoteToken, + needsDeployment, + isProving, + isFinalizing, + onDeployToken, + remoteTokenAddress, + remoteTokenChainId, +}: BridgeStatusMessagesProps) { + if (tokensError) { + console.warn('Token Loading Error', tokensError); + } + return ( + <> + {isProving && ( +
+

Proving your transaction...

+
+ )} + {isFinalizing && ( +
+

Finalizing your transaction...

+
+ )} + {isLoadingTokens && ( +
+

Loading your token balances...

+
+ )} + + {isCheckingRemoteToken && + selectedToken && + selectedToken.address !== ETH_TOKEN_ADDRESS && ( +
+

+ Checking Token on Destination Chain +

+

+ Verifying if {selectedToken.symbol} exists on the destination + chain... +

+
+ )} + + {needsDeployment && + selectedToken && + selectedToken.address !== ETH_TOKEN_ADDRESS && ( +
+

Token Deployment Required

+

+ {selectedToken.symbol} needs to be deployed on the destination + chain before bridging. This is a one-time setup required for new + tokens. +

+ {onDeployToken && ( + + )} +
+ )} + + {remoteTokenAddress && + selectedToken && + selectedToken.address !== ETH_TOKEN_ADDRESS && ( +
+

+ Token Available on Destination Chain +

+

+ {selectedToken.symbol} is available at:{' '} + + {remoteTokenAddress} + +

+
+ )} + + {needsApproval && + selectedToken && + selectedToken.address !== ETH_TOKEN_ADDRESS && ( +
+

Approval Required

+

+ You need to approve the bridge contract to spend your{' '} + {selectedToken.symbol} tokens. This is a one-time transaction + required before bridging ERC-20 tokens. +

+
+ )} + + ); +} diff --git a/libs/bridge/src/lib/components/bridge-success-message.tsx b/libs/bridge/src/lib/components/bridge-success-message.tsx new file mode 100644 index 000000000..3f23d6ff0 --- /dev/null +++ b/libs/bridge/src/lib/components/bridge-success-message.tsx @@ -0,0 +1,15 @@ +'use client'; + +export function BridgeSuccessMessage({ tokenSymbol }: { tokenSymbol: string }) { + return ( +
+
+ Bridge Successful! +
+
+ Your {tokenSymbol} has been successfully bridged. The transaction should + be confirmed within 1-3 minutes. +
+
+ ); +} diff --git a/libs/bridge/src/lib/components/challenge-period-warning.tsx b/libs/bridge/src/lib/components/challenge-period-warning.tsx new file mode 100644 index 000000000..3a2915e39 --- /dev/null +++ b/libs/bridge/src/lib/components/challenge-period-warning.tsx @@ -0,0 +1,37 @@ +'use client'; + +import { useTranslate } from '@tolgee/react'; +import { AlertTriangle } from 'lucide-react'; + +interface ChallengePeriodWarningProps { + isL2ToL1?: boolean; +} + +export function ChallengePeriodWarning({ + isL2ToL1 = false, +}: ChallengePeriodWarningProps) { + const { t } = useTranslate('common'); + + if (!isL2ToL1) { + return null; + } + + return ( +
+
+ +
+

+ {t('challenge-period-warning-title', 'Challenge Period Notice')} +

+

+ {t( + 'challenge-period-warning-message', + 'The 7-day withdrawal challenge period is crucial for security. Your withdrawal will be available to finalize after this period expires.', + )} +

+
+
+
+ ); +} diff --git a/libs/bridge/src/lib/components/index.ts b/libs/bridge/src/lib/components/index.ts new file mode 100644 index 000000000..bc46bfa68 --- /dev/null +++ b/libs/bridge/src/lib/components/index.ts @@ -0,0 +1,26 @@ +export { WalletConnectionWarning } from './wallet-connection-warning'; +export { NetworkMismatchWarning } from './network-mismatch-warning'; +export { BridgeSuccessMessage } from './bridge-success-message'; +export { BridgeAmountInput } from './bridge-amount-input'; +export { BridgeReceiveInput } from './bridge-receive-input'; +export { BridgeForm } from './bridge-form'; +export { TokenSelector } from './token-selector'; +export { BridgeStatusMessages } from './bridge-status-messages'; +export { ChallengePeriodWarning } from './challenge-period-warning'; +export { PendingWithdrawals } from './pending-withdrawals'; +export { WithdrawalOrderCard } from './withdrawal-order-card'; + +export type { NetworkMismatchWarningProps } from './network-mismatch-warning'; +export type { BridgeAmountInputProps } from './bridge-amount-input'; +export type { BridgeReceiveInputProps } from './bridge-receive-input'; +export type { BridgeFormProps } from './bridge-form'; + +// Export Token interface for external use +export interface Token { + symbol: string; + address: string; + name?: string; + balance?: string; + decimals?: number; + formattedBalance?: number; +} diff --git a/libs/bridge/src/lib/components/network-mismatch-warning.tsx b/libs/bridge/src/lib/components/network-mismatch-warning.tsx new file mode 100644 index 000000000..1e29de496 --- /dev/null +++ b/libs/bridge/src/lib/components/network-mismatch-warning.tsx @@ -0,0 +1,24 @@ +'use client'; +import { Button } from '@haqq/shell-ui-kit'; + +export interface NetworkMismatchWarningProps { + onSwitchChain: () => void; +} + +export function NetworkMismatchWarning({ + onSwitchChain, +}: NetworkMismatchWarningProps) { + return ( +
+
+ Wrong Network +
+
+ Please switch to the correct network to continue +
+ +
+ ); +} diff --git a/libs/bridge/src/lib/components/pending-withdrawals.tsx b/libs/bridge/src/lib/components/pending-withdrawals.tsx new file mode 100644 index 000000000..2fd4288cd --- /dev/null +++ b/libs/bridge/src/lib/components/pending-withdrawals.tsx @@ -0,0 +1,43 @@ +'use client'; + +import { useTranslate } from '@tolgee/react'; +import { WithdrawalOrderCard } from './withdrawal-order-card'; +import { WithdrawalOrder } from '../types/withdrawal-order'; + +interface PendingWithdrawalsProps { + orders: WithdrawalOrder[]; + onOrderUpdate?: () => void; +} + +export function PendingWithdrawals({ + orders, + onOrderUpdate, +}: PendingWithdrawalsProps) { + const { t } = useTranslate('common'); + + if (orders.length === 0) { + return null; + } + + console.log('orders', orders); + + return ( +
+

+ {t('pending-withdrawals', 'Pending Withdrawals')}: +

+ +
+ {orders.map((order) => { + return ( + + ); + })} +
+
+ ); +} diff --git a/libs/bridge/src/lib/components/token-deployment-page.tsx b/libs/bridge/src/lib/components/token-deployment-page.tsx new file mode 100644 index 000000000..9ca340780 --- /dev/null +++ b/libs/bridge/src/lib/components/token-deployment-page.tsx @@ -0,0 +1,431 @@ +'use client'; + +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { erc20Abi } from 'viem'; +import { + useSwitchChain, + useAccount, + useWaitForTransactionReceipt, + useReadContract, +} from 'wagmi'; +import { BRIDGE_ADDRESSES, getChainById } from '@haqq/shell-shared'; +import { Button } from '@haqq/shell-ui-kit'; +import { Container } from '@haqq/shell-ui-kit/server'; +import { useBridgeUrlState } from '../hooks/use-bridge-url-state'; +import { useTokenDeployment } from '../hooks/use-token-deployment'; + +interface TokenInfo { + address: string; + name: string; + symbol: string; + decimals: number; +} + +export function TokenDeploymentPage() { + const router = useRouter(); + const searchParams = useSearchParams(); + const { buildBridgeUrl } = useBridgeUrlState(); + + const { isConnected, chain: currentChain } = useAccount(); + const { switchChainAsync } = useSwitchChain(); + + const [deploymentHash, setDeploymentHash] = useState(null); + const [error, setError] = useState(null); + + // Parse URL parameters + const tokenAddress = searchParams.get('token'); + const sourceChainId = searchParams.get('sourceChain'); + const targetChainId = searchParams.get('targetChain'); + const returnTokenIn = searchParams.get('returnTokenIn'); + const returnTokenOut = searchParams.get('returnTokenOut'); + const returnChainIn = searchParams.get('returnChainIn'); + const returnChainOut = searchParams.get('returnChainOut'); + const returnAmount = searchParams.get('returnAmount'); + + const sourceChainIdNumber = sourceChainId + ? parseInt(sourceChainId, 10) + : null; + const targetChainIdNumber = targetChainId + ? parseInt(targetChainId, 10) + : null; + const sourceChain = sourceChainIdNumber + ? getChainById(sourceChainIdNumber) + : null; + const targetChain = targetChainIdNumber + ? getChainById(targetChainIdNumber) + : null; + + // Get token information from source chain + const { data: tokenName } = useReadContract({ + address: tokenAddress as `0x${string}`, + abi: erc20Abi, + functionName: 'name', + chainId: sourceChainIdNumber || undefined, + query: { + enabled: Boolean(tokenAddress && sourceChainIdNumber), + }, + }); + + const { data: tokenSymbol } = useReadContract({ + address: tokenAddress as `0x${string}`, + abi: erc20Abi, + functionName: 'symbol', + chainId: sourceChainIdNumber || undefined, + query: { + enabled: Boolean(tokenAddress && sourceChainIdNumber), + }, + }); + + const { data: tokenDecimals } = useReadContract({ + address: tokenAddress as `0x${string}`, + abi: erc20Abi, + functionName: 'decimals', + chainId: sourceChainIdNumber || undefined, + query: { + enabled: Boolean(tokenAddress && sourceChainIdNumber), + }, + }); + + console.log({ + tokenName, + tokenSymbol, + tokenDecimals, + }); + + const tokenInfo: TokenInfo | null = useMemo(() => { + if ( + !tokenAddress || + !tokenName || + !tokenSymbol || + tokenDecimals === undefined + ) { + return null; + } + return { + address: tokenAddress, + name: tokenName as string, + symbol: tokenSymbol as string, + decimals: tokenDecimals as number, + }; + }, [tokenAddress, tokenName, tokenSymbol, tokenDecimals]); + + // Token deployment hook + const { + deployToken, + isDeploying, + error: deploymentError, + reset: resetDeployment, + } = useTokenDeployment({ + factoryAddress: + BRIDGE_ADDRESSES.opChainDeployment + .optimismMintableERC20FactoryProxyAddress, + targetChainId: targetChainIdNumber || 0, + }); + + // Wait for deployment transaction + const { isLoading: isWaitingForDeployment, isSuccess: isDeploymentSuccess } = + useWaitForTransactionReceipt({ + hash: deploymentHash as `0x${string}` | undefined, + chainId: targetChainIdNumber || undefined, + }); + + // Check if user is on correct chain + const isOnCorrectChain = currentChain?.id === targetChainIdNumber; + const needsChainSwitch = isConnected && !isOnCorrectChain; + + // Handle chain switch + const handleSwitchChain = useCallback(async () => { + if (!switchChainAsync || !targetChainIdNumber) return; + + try { + setError(null); + await switchChainAsync({ chainId: targetChainIdNumber }); + } catch (err) { + const errorMessage = + err instanceof Error ? err.message : 'Failed to switch chain'; + setError(errorMessage); + } + }, [switchChainAsync, targetChainIdNumber]); + + // Handle token deployment + const handleDeployToken = useCallback(async () => { + if (!tokenInfo || !isOnCorrectChain) return; + + try { + setError(null); + const hash = await deployToken( + tokenInfo.address, + tokenInfo.name, + tokenInfo.symbol, + ); + setDeploymentHash(hash); + } catch (err) { + const errorMessage = + err instanceof Error ? err.message : 'Token deployment failed'; + setError(errorMessage); + } + }, [tokenInfo, isOnCorrectChain, deployToken]); + + // Handle return to bridge + const handleReturnToBridge = useCallback(() => { + const bridgeState = { + tokenIn: returnTokenIn || undefined, + tokenOut: returnTokenOut || undefined, + chainIn: returnChainIn ? parseInt(returnChainIn, 10) : undefined, + chainOut: returnChainOut ? parseInt(returnChainOut, 10) : undefined, + amount: returnAmount || undefined, + }; + + const bridgeUrl = buildBridgeUrl(bridgeState); + router.push(bridgeUrl); + }, [ + returnTokenIn, + returnTokenOut, + returnChainIn, + returnChainOut, + returnAmount, + buildBridgeUrl, + router, + ]); + + // Auto-redirect on successful deployment + useEffect(() => { + if (isDeploymentSuccess) { + // Wait a moment before redirecting to show success message + const timer = setTimeout(() => { + handleReturnToBridge(); + }, 3000); + + return () => { + return clearTimeout(timer); + }; + } + }, [isDeploymentSuccess, handleReturnToBridge]); + + console.log({ + isConnected, + tokenInfo, + needsChainSwitch, + isDeploying, + isWaitingForDeployment, + isDeploymentSuccess, + }); + // Validation + if (!tokenAddress || !sourceChainIdNumber || !targetChainIdNumber) { + return ( + +
+
+
+

+ Invalid Parameters +

+

+ Missing required token address, source chain ID, or target chain + ID. +

+ +
+
+
+
+ ); + } + + if (!targetChain) { + return ( + +
+
+
+

+ Unsupported Chain +

+

+ Target chain ID {targetChainIdNumber} is not supported. +

+ +
+
+
+
+ ); + } + + return ( + +
+
+ {/* Header */} +
+

+ Deploy Token +

+

+ Deploy {tokenInfo?.symbol || 'token'} on {targetChain.name} to + enable bridging +

+
+ + {/* Token Info */} + {tokenInfo && ( +
+

Token Details

+
+

+ Name: {tokenInfo.name} +

+

+ Symbol:{' '} + {tokenInfo.symbol} +

+

+ Address:{' '} + {tokenInfo.address} +

+

+ Decimals:{' '} + {tokenInfo.decimals} +

+

+ Source Chain:{' '} + {sourceChain?.name || `Chain ${sourceChainIdNumber}`} +

+
+
+ )} + + {/* Target Chain Info */} +
+

Target Chain

+
+

+ Network: {targetChain.name} +

+

+ Chain ID: {targetChain.id} +

+
+
+ + {/* Wallet Connection Warning */} + {!isConnected && ( +
+

Wallet Not Connected

+

+ Please connect your wallet to deploy the token. +

+
+ )} + + {/* Chain Switch Warning */} + {needsChainSwitch && ( +
+

Wrong Network

+

+ You need to switch to {targetChain.name} to deploy the token. +

+ +
+ )} + + {/* Deployment Status */} + {(isDeploying || isWaitingForDeployment) && ( +
+

+ {isDeploying + ? 'Deploying Token...' + : 'Waiting for Confirmation...'} +

+

+ This may take a few minutes. Please don't close this page. +

+ {deploymentHash && ( +

+ Transaction:{' '} + {deploymentHash.slice(0, 10)}... +

+ )} +
+ )} + + {/* Success Message */} + {isDeploymentSuccess && ( +
+

+ Token Deployed Successfully! +

+

+ {tokenInfo?.symbol} has been deployed on {targetChain.name}. + Redirecting you back to the bridge... +

+
+ )} + + {/* Error Message */} + {(error || deploymentError) && ( +
+

Error

+

{error || deploymentError}

+ +
+ )} + + {/* Action Buttons */} +
+ + + +
+
+
+
+ ); +} diff --git a/libs/bridge/src/lib/components/token-selector.tsx b/libs/bridge/src/lib/components/token-selector.tsx new file mode 100644 index 000000000..d8476b26f --- /dev/null +++ b/libs/bridge/src/lib/components/token-selector.tsx @@ -0,0 +1,182 @@ +'use client'; + +import { useCallback, useMemo } from 'react'; +import { useTranslate } from '@tolgee/react'; +import clsx from 'clsx'; +import Select, { + components as tokenSelectComponents, + OptionProps, + ClassNamesConfig, +} from 'react-select'; + +interface Token { + symbol: string; + address: string; + name?: string; + balance?: string; + decimals?: number; + formattedBalance?: number; +} + +interface TokenSelectOption { + label: string; + value: string; + token: Token; +} + +interface TokenSelectorProps { + tokens: Token[]; + selectedToken: Token | null; + onTokenSelect: (token: Token) => void; + disabled?: boolean; +} + +export function TokenSelectOption({ + children: _, + ...rest +}: OptionProps) { + const { token } = rest.data; + const balanceText = + token.formattedBalance !== undefined + ? ` - ${token.formattedBalance.toFixed(6)}` + : ''; + + return ( + +
+
+
+ {token.symbol} {token.name && `(${token.name})`} +
+
+ {token.address} +
+
+ {balanceText && ( +
+ {token.formattedBalance?.toFixed(6)} +
+ )} +
+
+ ); +} + +export function TokenSelector({ + tokens, + selectedToken, + onTokenSelect, + disabled = false, +}: TokenSelectorProps) { + const { t } = useTranslate('bridge'); + + const handleFilterOption = useCallback((option: any, inputValue: string) => { + const { label, value } = option.data; + const inputLower = inputValue.toLowerCase(); + const labelLower = label.toLowerCase(); + const valueLower = value.toLowerCase(); + + return labelLower.includes(inputLower) || valueLower.includes(inputLower); + }, []); + + const classNames = useMemo>(() => { + return { + control: () => { + return clsx( + 'w-full rounded-[12px] border border-[#E8E8E8] bg-white', + 'transition-colors duration-100 ease-in', + 'focus-within:border-[#04D484] focus-within:outline-none', + { + 'opacity-50 cursor-not-allowed': disabled, + }, + ); + }, + container: () => { + return ''; + }, + placeholder: () => { + return 'text-[#0D0D0E80]'; + }, + valueContainer: () => { + return 'text-[16px] font-[500] text-[#0D0D0E] min-h-[54px] px-[16px] py-[18px]'; + }, + indicatorsContainer: () => { + return 'pe-[16px] text-[#0D0D0E80]'; + }, + menu: () => { + return clsx( + 'border-[1px] border-[#E8E8E8] bg-white rounded-[12px] mt-[4px] shadow-lg', + 'text-[#0D0D0E] text-[14px] leading-[20px]', + 'overflow-hidden', + ); + }, + option: ({ isFocused, isSelected }) => { + return clsx( + 'px-[16px] py-[12px] text-start', + 'transition-colors duration-150 ease-out', + { + 'bg-[#04D48410]': isFocused || isSelected, + }, + ); + }, + noOptionsMessage: () => { + return 'px-[16px] py-[12px] text-[#0D0D0E80]'; + }, + }; + }, [disabled]); + + const options = useMemo(() => { + return tokens + .filter((token) => { + return token.address; + }) + .map((token) => { + return { + label: `${token.symbol}${token.name ? ` (${token.name})` : ''}`, + value: token.address.toLowerCase(), + token, + }; + }); + }, [tokens]); + + const selectedOption = useMemo(() => { + if (!selectedToken) return null; + return ( + options.find((option) => { + return option.value === selectedToken.address.toLowerCase(); + }) || null + ); + }, [selectedToken, options]); + + return ( +
+ + +
+ ); +} diff --git a/libs/faucet/src/lib/faucet-page.tsx b/libs/faucet/src/lib/faucet-page.tsx index 80cd1ab40..81fc0c61b 100644 --- a/libs/faucet/src/lib/faucet-page.tsx +++ b/libs/faucet/src/lib/faucet-page.tsx @@ -1,11 +1,13 @@ 'use client'; -import { ReactElement, useMemo } from 'react'; +import { ReactElement, useMemo, useState, useCallback, useEffect } from 'react'; import { useTranslate } from '@tolgee/react'; import clsx from 'clsx'; import { notFound } from 'next/navigation'; +import { Chain, sepolia } from 'viem/chains'; import { useAccount, useChains } from 'wagmi'; import { FAUCET_CHAINS, useWallet } from '@haqq/shell-shared'; import { Container } from '@haqq/shell-ui-kit/server'; +import { ChainSelector } from './components/chain-selector'; import { ClaimTokensSection } from './components/claim-tokens-section'; import { GithubSection } from './components/github-section'; import { WalletSection } from './components/wallet-section'; @@ -23,9 +25,32 @@ export function FaucetPage({ }): ReactElement { const { t } = useTranslate(); const chains = useChains(); - const { chain = chains[0], address } = useAccount(); + const { chain = chains[0], address, isConnected } = useAccount(); const { isHaqqWallet } = useWallet(); + // Filter chains to only include faucet-supported chains + const faucetChains = useMemo(() => { + return chains.filter((c) => { + return FAUCET_CHAINS.includes(c.id) && c.id !== sepolia.id; + }); + }, [chains]); + + // State for selected target chain (defaults to current chain if supported, otherwise first faucet chain) + const [selectedChain, setSelectedChain] = useState(() => { + const isSupportedChain = FAUCET_CHAINS.includes(chain.id); + return isSupportedChain ? chain : faucetChains[0] || chain; + }); + // Ensure that `selectedChain` is kept in sync with the connected chain. + // When the user switches wallet network, update selectedChain to match. + // This keeps the faucet logic and UI in sync with user's wallet. + // This effect only runs when chain.id changes (i.e., wallet network changes). + + useEffect(() => { + if (FAUCET_CHAINS.includes(chain.id)) { + setSelectedChain(chain); + } + }, [chain, setSelectedChain]); + const isTestedge = useMemo(() => { return FAUCET_CHAINS.includes(chain.id); }, [chain.id]); @@ -53,11 +78,23 @@ export function FaucetPage({ getAccessTokenSilently, address, recaptchaToken, - chainId: chain.id, + chainId: selectedChain.id, }); const { handleNetworkSwitch } = useNetworkSwitch(); + // Handler for chain selection + const handleChainSelect = useCallback( + (newChain: Chain) => { + setSelectedChain(newChain); + // If connected and the new chain is different, switch to it + if (isConnected && newChain.id !== chain.id) { + handleNetworkSwitch(newChain.id); + } + }, + [isConnected, chain.id, handleNetworkSwitch], + ); + if (!isTestedge) { notFound(); } @@ -83,9 +120,18 @@ export function FaucetPage({ !(isAuthenticated && address) && 'rounded-b-[8px]', )} > + + { + return handleNetworkSwitch(selectedChain.id); + }} /> { - if (switchChainAsync) { - try { - await switchChainAsync({ chainId: chains[0].id }); - } catch (error) { - console.error((error as Error).message); + const handleNetworkSwitch = useCallback( + async (chainId: number) => { + if (switchChainAsync) { + try { + await switchChainAsync({ chainId }); + } catch (error) { + console.error((error as Error).message); + } } - } - }, [chains, switchChainAsync]); + }, + [switchChainAsync], + ); return { handleNetworkSwitch }; } From 781de4176e2311a1451df166e924bb1a8df63d25 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Mon, 27 Oct 2025 13:36:42 +0300 Subject: [PATCH 027/141] fix: balances view and notFound page --- agreement.txt | 0 libs/faucet/src/lib/components/account-info.tsx | 15 ++++++++++----- libs/faucet/src/lib/faucet-page.tsx | 8 -------- 3 files changed, 10 insertions(+), 13 deletions(-) create mode 100644 agreement.txt diff --git a/agreement.txt b/agreement.txt new file mode 100644 index 000000000..e69de29bb diff --git a/libs/faucet/src/lib/components/account-info.tsx b/libs/faucet/src/lib/components/account-info.tsx index e52b8e442..61b0c7e08 100644 --- a/libs/faucet/src/lib/components/account-info.tsx +++ b/libs/faucet/src/lib/components/account-info.tsx @@ -1,14 +1,16 @@ 'use client'; import { PropsWithChildren, useCallback, useState } from 'react'; import { useTranslate } from '@tolgee/react'; +import { Hex } from 'viem'; +import { useBalance } from 'wagmi'; import { + formatEthDecimal, getFormattedAddress, useAddress, useClipboard, - useIndexerBalanceQuery, } from '@haqq/shell-shared'; import { Tooltip } from '@haqq/shell-ui-kit'; -import { CopyIcon, formatNumber } from '@haqq/shell-ui-kit/server'; +import { CopyIcon } from '@haqq/shell-ui-kit/server'; export function MyAccountCardBlock({ title, @@ -30,10 +32,12 @@ export function AccountInfo() { const { t } = useTranslate(); const { ethAddress, haqqAddress } = useAddress(); const { copyText } = useClipboard(); - const { data: balance } = useIndexerBalanceQuery(haqqAddress); + const balancesData = useBalance({ address: ethAddress as Hex }); const [isEthAddressCopy, setEthAddressCopy] = useState(false); const [isHaqqAddressCopy, setHaqqAddressCopy] = useState(false); + const balance = balancesData.data?.value; + const handleEthAddressCopy = useCallback(async () => { if (ethAddress) { await copyText(ethAddress); @@ -107,10 +111,11 @@ export function AccountInfo() {
)} - {balance?.balance !== undefined && ( + {balance !== undefined && (
- {formatNumber(balance.balance)} ISLM + {formatEthDecimal(balance, 2, balancesData.data?.decimals ?? 18)} +   {balancesData.data?.symbol}
)} diff --git a/libs/faucet/src/lib/faucet-page.tsx b/libs/faucet/src/lib/faucet-page.tsx index 81fc0c61b..9559107ee 100644 --- a/libs/faucet/src/lib/faucet-page.tsx +++ b/libs/faucet/src/lib/faucet-page.tsx @@ -51,10 +51,6 @@ export function FaucetPage({ } }, [chain, setSelectedChain]); - const isTestedge = useMemo(() => { - return FAUCET_CHAINS.includes(chain.id); - }, [chain.id]); - const { isAuthenticated, getAccessTokenSilently, @@ -95,10 +91,6 @@ export function FaucetPage({ [isConnected, chain.id, handleNetworkSwitch], ); - if (!isTestedge) { - notFound(); - } - return (
{!isHaqqWallet && ( From 3506e53f2434e1227386ad7b94312ea359e406e6 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Mon, 27 Oct 2025 13:43:54 +0300 Subject: [PATCH 028/141] chore: remove file .txt --- agreement.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 agreement.txt diff --git a/agreement.txt b/agreement.txt deleted file mode 100644 index e69de29bb..000000000 From 6afb06983722613a2248439d3fd673574aa5043e Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Fri, 28 Nov 2025 14:47:40 +0300 Subject: [PATCH 029/141] chore: self review, removed unused vars --- libs/shared/src/utils/bridge.ts | 49 ++------------------------------- 1 file changed, 3 insertions(+), 46 deletions(-) diff --git a/libs/shared/src/utils/bridge.ts b/libs/shared/src/utils/bridge.ts index 708451d3c..862d1aeae 100644 --- a/libs/shared/src/utils/bridge.ts +++ b/libs/shared/src/utils/bridge.ts @@ -1,28 +1,6 @@ import { haqqTestedge2, sepolia } from 'viem/chains'; -const haqqTestethiqRpcUrl = 'https://rpc.testethiq.haqq.network/'; - -export const SWAPPABLE_TOKENS: { - [chainId: number]: { - symbol: string; - address: string; - }[]; -} = { - [sepolia.id]: [ - { - symbol: 'ETH', - address: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - }, - { - symbol: 'USDC', - address: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238', - }, - { - symbol: 'EURC', - address: '0x08210f9170f89ab7658f0b5e3ff39b0e03c594d4', - }, - ], -}; +const haqqTestethiqRpcUrl = 'https://rpc.testnet.ethiq.network/'; /** * Mapping of L1 token addresses to their corresponding L2 token addresses @@ -65,27 +43,6 @@ export interface NetworkConfig { explorerUrl: string; } -// Role addresses -export const ROLE_ADDRESSES = { - proxyAdminOwner: '0xBaaa30f95cf147522F44FBBfAbAD1e1763824b76', - protocolVersionsOwner: '0xBaaa30f95cf147522F44FBBfAbAD1e1763824b76', - guardian: '0xBaaa30f95cf147522F44FBBfAbAD1e1763824b76', - l1ProxyAdminOwner: '0xBaaa30f95cf147522F44FBBfAbAD1e1763824b76', - l2ProxyAdminOwner: '0xBaaa30f95cf147522F44FBBfAbAD1e1763824b76', - systemConfigOwner: '0xBaaa30f95cf147522F44FBBfAbAD1e1763824b76', - unsafeBlockSigner: '0x12cA30dE061379E02d42b9303d5498bCb8Dd909B', - batcher: '0x49180059AF02dB7660A3d09d7796459b0F181a69', - proposer: '0x7876adF8a936347A543d1C41859e23034C021f2C', - challenger: '0xBaaa30f95cf147522F44FBBfAbAD1e1763824b76', -}; - -// Fee vault recipients -export const FEE_VAULT_RECIPIENTS = { - baseFeeVaultRecipient: '0xBaaa30f95cf147522F44FBBfAbAD1e1763824b76', - l1FeeVaultRecipient: '0xBaaa30f95cf147522F44FBBfAbAD1e1763824b76', - sequencerFeeVaultRecipient: '0xBaaa30f95cf147522F44FBBfAbAD1e1763824b76', -}; - export const BRIDGE_ADDRESSES: { SuperchainProxyAdminImpl: string; SuperchainConfigProxy: string; @@ -233,8 +190,8 @@ export const haqqTestethiq = { blockExplorers: { default: { name: 'HAQQ Testethiq', - url: 'https://explorer.testethiq.haqq.network', - apiUrl: 'https://explorer.testethiq.haqq.network/api', + url: 'https://explorer.testnet.ethiq.network/', + apiUrl: 'https://explorer.testnet.ethiq.network/api', }, }, contracts: { From 4604fa42ac11573b8aa3c3e57f73f3a2a4d3bae7 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Fri, 28 Nov 2025 15:26:43 +0300 Subject: [PATCH 030/141] fix: bridge addresses --- libs/shared/src/utils/bridge.ts | 88 +++++++++++++++++++++------------ 1 file changed, 57 insertions(+), 31 deletions(-) diff --git a/libs/shared/src/utils/bridge.ts b/libs/shared/src/utils/bridge.ts index 862d1aeae..815eb5b70 100644 --- a/libs/shared/src/utils/bridge.ts +++ b/libs/shared/src/utils/bridge.ts @@ -2,6 +2,28 @@ import { haqqTestedge2, sepolia } from 'viem/chains'; const haqqTestethiqRpcUrl = 'https://rpc.testnet.ethiq.network/'; +export const SWAPPABLE_TOKENS: { + [chainId: number]: { + symbol: string; + address: string; + }[]; +} = { + [sepolia.id]: [ + { + symbol: 'ETH', + address: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + }, + { + symbol: 'USDC', + address: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238', + }, + { + symbol: 'EURC', + address: '0x08210f9170f89ab7658f0b5e3ff39b0e03c594d4', + }, + ], +}; + /** * Mapping of L1 token addresses to their corresponding L2 token addresses */ @@ -55,8 +77,10 @@ export const BRIDGE_ADDRESSES: { OpcmDeployerImpl: string; OpcmUpgraderImpl: string; OpcmInteropMigratorImpl: string; + OpcmStandardValidatorImpl: string; DelayedWethImpl: string; OptimismPortalImpl: string; + OptimismPortalInteropImpl: string; EthLockboxImpl: string; PreimageOracleImpl: string; MipsImpl: string; @@ -86,46 +110,48 @@ export const BRIDGE_ADDRESSES: { AltDAChallengeImpl: string; L2OutputOracleProxy: string; } = { - SuperchainProxyAdminImpl: '0x03389e1d90d16db8c8aab7b7b5388e0661ad1b20', - SuperchainConfigProxy: '0x7779c5b626bd93b444deee5296f1336c9a076a70', - SuperchainConfigImpl: '0xce28685eb204186b557133766eca00334eb441e4', - ProtocolVersionsProxy: '0x3d22989e9b049a073cb9f72cb3357367291508b6', + SuperchainProxyAdminImpl: '0x189abaaaa82dfc015a588a7dbad6f13b1d3485bc', + SuperchainConfigProxy: '0xc2be75506d5724086deb7245bd260cc9753911be', + SuperchainConfigImpl: '0xb08cc720f511062537ca78bdb0ae691f04f5a957', + ProtocolVersionsProxy: '0x79add5713b383daa0a138d3c4780c7a1804a8090', ProtocolVersionsImpl: '0x37e15e4d6dffa9e5e320ee1ec036922e563cb76c', - OpcmImpl: '0xeb816af3d7b9a61bca3415015b8e208c0be445e5', + OpcmImpl: '0xc69e4c24db479191676611a25d977203c3bdca62', OpcmContractsContainerImpl: '0x0000000000000000000000000000000000000000', - OpcmGameTypeAdderImpl: '0x77be751385562ec5f5074f1d3d80b9b7df0af77c', - OpcmDeployerImpl: '0x4859c22632ac5ad6506df5f996098b73a11bba75', - OpcmUpgraderImpl: '0x5b6820529748d5001c1a999176bfedabbf5fa64d', - OpcmInteropMigratorImpl: '0x01b2f6aa2adc77c9a4a91d09a6e806ad51b0290a', + OpcmGameTypeAdderImpl: '0x0000000000000000000000000000000000000000', + OpcmDeployerImpl: '0x0000000000000000000000000000000000000000', + OpcmUpgraderImpl: '0x0000000000000000000000000000000000000000', + OpcmInteropMigratorImpl: '0x0000000000000000000000000000000000000000', + OpcmStandardValidatorImpl: '0x0000000000000000000000000000000000000000', DelayedWethImpl: '0x33dadc2d1aa9bb613a7ae6b28425ea00d44c6998', - OptimismPortalImpl: '0xefed7f38bb9be74bba583a1a5b7d0fe7c9d5787a', + OptimismPortalImpl: '0x7cf803296662e8c72a6c1d6450572209acf7f202', + OptimismPortalInteropImpl: '0x5cb365a10e99335d8fedfa225aac5e21287302dd', EthLockboxImpl: '0x784d2f03593a42a6e4676a012762f18775ecbbe6', PreimageOracleImpl: '0x1fb8cdfc6831fc866ed9c51af8817da5c287add3', - MipsImpl: '0xa1b54d89e305bcd322ba0c9c094093173c0d6b3a', - SystemConfigImpl: '0xfaa660bf783cbaa55e1b7f3475c20db74a53b9fa', - L1CrossDomainMessengerImpl: '0xd26bb3aaaa4cb5638a8581a4c4b1d937d8e05c54', - L1Erc721BridgeImpl: '0x25d6cedeb277ad7ebee71226ed7877768e0b7a2f', - L1StandardBridgeImpl: '0x44afb7722af276a601d524f429016a18b6923df0', + MipsImpl: '0x6463dee3828677f6270d83d45408044fc5edb908', + SystemConfigImpl: '0x2fa28989fc559836e9d66dff3010c7f7f41c65ed', + L1CrossDomainMessengerImpl: '0xb686f13aff1e427a1f993f29ab0f2e7383729fe0', + L1Erc721BridgeImpl: '0x74f1ac50eb0be98853805d381c884f5f9abdecf9', + L1StandardBridgeImpl: '0x61525eaacddb97d9184afc205827e6a4fd0bf62a', OptimismMintableErc20FactoryImpl: - '0x5493f4677a186f64805fe7317d6993ba4863988f', - DisputeGameFactoryImpl: '0x33d1e8571a85a538ed3d5a4d88f46c112383439d', - AnchorStateRegistryImpl: '0xeb69cc681e8d4a557b30dffbad85affd47a2cf2e', - OpChainProxyAdminImpl: '0x2a0dc32ae8675792dfa23fd1526af5ae7907ef69', - OptimismPortalProxy: '0xcef83e2c029f1bdfefbfd4cb908ac333f420e209', - AddressManagerImpl: '0xe9a877d1712f17da500dac544a15616c2476e4d1', - L1Erc721BridgeProxy: '0x67c74a530eab1f7b21adc441a836430a1ef792a9', - SystemConfigProxy: '0xda718df88b54460dd4834b29c01658dd976c9e09', + '0x8ee6fb13c6c9a7e401531168e196fbf8b05ceabb', + DisputeGameFactoryImpl: '0x74fac1d45b98bae058f8f566201c9a81b85c7d50', + AnchorStateRegistryImpl: '0x0000000000000000000000000000000000000000', + OpChainProxyAdminImpl: '0x8b942e404116cfebee8fb13b339609ace97ca58f', + OptimismPortalProxy: '0x69a3d0177fa023842bf275eb6f7d279c53c1a1bc', + AddressManagerImpl: '0x680a407d3a42d9420a867708e9a87700f32babff', + L1Erc721BridgeProxy: '0xbbf7584928c829101a345ab97f3fdb35183b7c81', + SystemConfigProxy: '0xf735afb2513203709b1f95ea3a6834ae54e5ff16', OptimismMintableErc20FactoryProxy: - '0x65df5d4aa6371f63aa4ec935ed93a99f097e4abd', - L1StandardBridgeProxy: '0xe6260411feffbff8a38ad32484ea01cbf1658a0e', - L1CrossDomainMessengerProxy: '0xf8d52efc21fe3db1c1d651ee03d19c5f5b83597f', - EthLockboxProxy: '0xfe3123d5157b9d104e34cbf73a9171854244d218', - DisputeGameFactoryProxy: '0x1d15a66521bdb3043335734039d428f97bab3f7e', - AnchorStateRegistryProxy: '0xc7c945a172b36efc6b6165f4d70a5b93a9f109d9', + '0xaf7696edce2bb6a9fa384045d74ef2bba140d471', + L1StandardBridgeProxy: '0xf50e3728ff51d0276e92dff61595c6e94e533ac4', + L1CrossDomainMessengerProxy: '0x804875f04a33f11cd2dfaeaad5cc9c81a9bb5d98', + EthLockboxProxy: '0x867ac75de1cc6ab487305e04e2a92de8a5ea0a54', + DisputeGameFactoryProxy: '0x03a129b2bb0bfdfa7d5e7fbfcad7c1960e07389c', + AnchorStateRegistryProxy: '0xdffdb1ea0c6a8e84781a9604423b4549f79886f7', FaultDisputeGameImpl: '0x0000000000000000000000000000000000000000', - PermissionedDisputeGameImpl: '0x563b50dc93936597974fbd59f055c3be151b297e', + PermissionedDisputeGameImpl: '0x8fe3b7e258b3d7d45094b1424237ae4a5fc7b368', DelayedWethPermissionedGameProxy: - '0x44c85f79783be56d7db48c060a9721a320fc57c7', + '0xa909e9c7f6d66bd40227e3b6f647615e696c7115', DelayedWethPermissionlessGameProxy: '0x0000000000000000000000000000000000000000', AltDAChallengeProxy: '0x0000000000000000000000000000000000000000', From 01429f8291845bb1cb52792b2430efa8cc659e2b Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Sun, 30 Nov 2025 10:33:29 +0300 Subject: [PATCH 031/141] chore: trigger build --- libs/shared/src/utils/bridge.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/shared/src/utils/bridge.ts b/libs/shared/src/utils/bridge.ts index 815eb5b70..2dba19d81 100644 --- a/libs/shared/src/utils/bridge.ts +++ b/libs/shared/src/utils/bridge.ts @@ -278,4 +278,4 @@ export const getAddressExplorerUrl = (address: string, chainId: number) => { return '#'; }; -export const FAUCET_CHAINS = [haqqTestedge2.id, sepolia.id, haqqTestethiq.id]; +export const FAUCET_CHAINS = [haqqTestedge2.id, haqqTestethiq.id, sepolia.id]; From ee0f5ac8fde8062d7dddf6b4967bd9aa1bf2045b Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Sun, 30 Nov 2025 11:53:19 +0300 Subject: [PATCH 032/141] chore: remove self hosted ubuntu --- .github/workflows/ci.yml | 3 --- .github/workflows/preview-vercel-shell.yml | 3 --- .github/workflows/preview-vercel-storybook.yml | 3 --- .github/workflows/preview-vercel-vesting.yml | 3 --- .github/workflows/production-vercel-shell.yml | 3 --- .github/workflows/production-vercel-storybook.yml | 3 --- .github/workflows/production-vercel-vesting.yml | 3 --- 7 files changed, 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5751eab35..c47c4c103 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,9 +22,6 @@ env: jobs: test-and-lint: name: Test and Lint - runs-on: - - ubuntu - - self-hosted if: (github.event_name == 'pull_request' && !github.event.pull_request.draft) || (github.event_name == 'push' && github.ref == 'refs/heads/dev') timeout-minutes: 15 steps: diff --git a/.github/workflows/preview-vercel-shell.yml b/.github/workflows/preview-vercel-shell.yml index 26167e788..45c80286f 100644 --- a/.github/workflows/preview-vercel-shell.yml +++ b/.github/workflows/preview-vercel-shell.yml @@ -11,9 +11,6 @@ on: jobs: Preview: - runs-on: - - ubuntu - - self-hosted if: (github.event_name == 'pull_request' && !github.event.pull_request.draft) || (github.event_name == 'push' && github.ref == 'refs/heads/dev') env: VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} diff --git a/.github/workflows/preview-vercel-storybook.yml b/.github/workflows/preview-vercel-storybook.yml index 3586a9b3b..2435be0ba 100644 --- a/.github/workflows/preview-vercel-storybook.yml +++ b/.github/workflows/preview-vercel-storybook.yml @@ -11,9 +11,6 @@ on: jobs: Preview: - runs-on: - - ubuntu - - self-hosted if: (github.event_name == 'pull_request' && !github.event.pull_request.draft) || (github.event_name == 'push' && github.ref == 'refs/heads/dev') env: VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} diff --git a/.github/workflows/preview-vercel-vesting.yml b/.github/workflows/preview-vercel-vesting.yml index f9310e4a6..ac293add5 100644 --- a/.github/workflows/preview-vercel-vesting.yml +++ b/.github/workflows/preview-vercel-vesting.yml @@ -11,9 +11,6 @@ on: jobs: Preview: - runs-on: - - ubuntu - - self-hosted if: (github.event_name == 'pull_request' && !github.event.pull_request.draft) || (github.event_name == 'push' && github.ref == 'refs/heads/dev') env: VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} diff --git a/.github/workflows/production-vercel-shell.yml b/.github/workflows/production-vercel-shell.yml index 690e518a4..2e2b72748 100644 --- a/.github/workflows/production-vercel-shell.yml +++ b/.github/workflows/production-vercel-shell.yml @@ -8,9 +8,6 @@ on: jobs: Production: - runs-on: - - ubuntu - - self-hosted env: VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_SHELL_PROJECT_ID }} diff --git a/.github/workflows/production-vercel-storybook.yml b/.github/workflows/production-vercel-storybook.yml index ccd747fa9..18e56b249 100644 --- a/.github/workflows/production-vercel-storybook.yml +++ b/.github/workflows/production-vercel-storybook.yml @@ -8,9 +8,6 @@ on: jobs: Production: - runs-on: - - ubuntu - - self-hosted env: VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_STORYBOOK_PROJECT_ID }} diff --git a/.github/workflows/production-vercel-vesting.yml b/.github/workflows/production-vercel-vesting.yml index 9418949c5..36dc4b791 100644 --- a/.github/workflows/production-vercel-vesting.yml +++ b/.github/workflows/production-vercel-vesting.yml @@ -8,9 +8,6 @@ on: jobs: Production: - runs-on: - - ubuntu - - self-hosted env: VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_VESTING_PROJECT_ID }} From bc76d38de00fa1ceca6330ee95496636b11e0a94 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Sun, 30 Nov 2025 11:57:44 +0300 Subject: [PATCH 033/141] chore: use ubuntu-latest --- .github/workflows/ci.yml | 1 + .github/workflows/preview-vercel-shell.yml | 1 + .github/workflows/preview-vercel-storybook.yml | 1 + .github/workflows/preview-vercel-vesting.yml | 1 + .github/workflows/production-vercel-shell.yml | 1 + .github/workflows/production-vercel-storybook.yml | 1 + .github/workflows/production-vercel-vesting.yml | 1 + 7 files changed, 7 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c47c4c103..9675b4088 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,7 @@ env: jobs: test-and-lint: name: Test and Lint + runs-on: ubuntu-latest if: (github.event_name == 'pull_request' && !github.event.pull_request.draft) || (github.event_name == 'push' && github.ref == 'refs/heads/dev') timeout-minutes: 15 steps: diff --git a/.github/workflows/preview-vercel-shell.yml b/.github/workflows/preview-vercel-shell.yml index 45c80286f..2d4c6d591 100644 --- a/.github/workflows/preview-vercel-shell.yml +++ b/.github/workflows/preview-vercel-shell.yml @@ -11,6 +11,7 @@ on: jobs: Preview: + runs-on: ubuntu-latest if: (github.event_name == 'pull_request' && !github.event.pull_request.draft) || (github.event_name == 'push' && github.ref == 'refs/heads/dev') env: VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} diff --git a/.github/workflows/preview-vercel-storybook.yml b/.github/workflows/preview-vercel-storybook.yml index 2435be0ba..dfaa4889c 100644 --- a/.github/workflows/preview-vercel-storybook.yml +++ b/.github/workflows/preview-vercel-storybook.yml @@ -11,6 +11,7 @@ on: jobs: Preview: + runs-on: ubuntu-latest if: (github.event_name == 'pull_request' && !github.event.pull_request.draft) || (github.event_name == 'push' && github.ref == 'refs/heads/dev') env: VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} diff --git a/.github/workflows/preview-vercel-vesting.yml b/.github/workflows/preview-vercel-vesting.yml index ac293add5..86b4d7c9f 100644 --- a/.github/workflows/preview-vercel-vesting.yml +++ b/.github/workflows/preview-vercel-vesting.yml @@ -11,6 +11,7 @@ on: jobs: Preview: + runs-on: ubuntu-latest if: (github.event_name == 'pull_request' && !github.event.pull_request.draft) || (github.event_name == 'push' && github.ref == 'refs/heads/dev') env: VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} diff --git a/.github/workflows/production-vercel-shell.yml b/.github/workflows/production-vercel-shell.yml index 2e2b72748..cc18d1483 100644 --- a/.github/workflows/production-vercel-shell.yml +++ b/.github/workflows/production-vercel-shell.yml @@ -8,6 +8,7 @@ on: jobs: Production: + runs-on: ubuntu-latest env: VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_SHELL_PROJECT_ID }} diff --git a/.github/workflows/production-vercel-storybook.yml b/.github/workflows/production-vercel-storybook.yml index 18e56b249..f162b1ab0 100644 --- a/.github/workflows/production-vercel-storybook.yml +++ b/.github/workflows/production-vercel-storybook.yml @@ -8,6 +8,7 @@ on: jobs: Production: + runs-on: ubuntu-latest env: VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_STORYBOOK_PROJECT_ID }} diff --git a/.github/workflows/production-vercel-vesting.yml b/.github/workflows/production-vercel-vesting.yml index 36dc4b791..bd71d6a57 100644 --- a/.github/workflows/production-vercel-vesting.yml +++ b/.github/workflows/production-vercel-vesting.yml @@ -8,6 +8,7 @@ on: jobs: Production: + runs-on: ubuntu-latest env: VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_VESTING_PROJECT_ID }} From 2b63dd6c990d44a4f90f0d2c73e96533f9bf34e0 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Mon, 8 Dec 2025 17:41:49 +0300 Subject: [PATCH 034/141] feat: contracts API --- libs/bridge/src/lib/services/scanner-api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/bridge/src/lib/services/scanner-api.ts b/libs/bridge/src/lib/services/scanner-api.ts index 48d9d2dd6..df1c51983 100644 --- a/libs/bridge/src/lib/services/scanner-api.ts +++ b/libs/bridge/src/lib/services/scanner-api.ts @@ -5,7 +5,7 @@ import { sepolia } from 'viem/chains'; import { haqqTestethiq } from '@haqq/shell-shared'; -const SCANNER_API_BASE_URL = 'https://scanner.dev.haqq.network/api/v1'; +const SCANNER_API_BASE_URL = 'https://scanner.ethiq.network/api/v1'; export interface TokenPair { id: number; From 09d89cfa1996f67bb991d84cb58318e6f9330ff8 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Mon, 8 Dec 2025 20:29:22 +0300 Subject: [PATCH 035/141] fix: correct bridge addresses --- libs/shared/src/utils/bridge.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/libs/shared/src/utils/bridge.ts b/libs/shared/src/utils/bridge.ts index 2dba19d81..cfcaeed6b 100644 --- a/libs/shared/src/utils/bridge.ts +++ b/libs/shared/src/utils/bridge.ts @@ -136,22 +136,22 @@ export const BRIDGE_ADDRESSES: { '0x8ee6fb13c6c9a7e401531168e196fbf8b05ceabb', DisputeGameFactoryImpl: '0x74fac1d45b98bae058f8f566201c9a81b85c7d50', AnchorStateRegistryImpl: '0x0000000000000000000000000000000000000000', - OpChainProxyAdminImpl: '0x8b942e404116cfebee8fb13b339609ace97ca58f', - OptimismPortalProxy: '0x69a3d0177fa023842bf275eb6f7d279c53c1a1bc', - AddressManagerImpl: '0x680a407d3a42d9420a867708e9a87700f32babff', - L1Erc721BridgeProxy: '0xbbf7584928c829101a345ab97f3fdb35183b7c81', - SystemConfigProxy: '0xf735afb2513203709b1f95ea3a6834ae54e5ff16', + OpChainProxyAdminImpl: '0xcd9c1ab8aa13c69f72cd68520a353bd54cf5ab18', + OptimismPortalProxy: '0x5b5f73ebcda96d9c4ca3315497e370c49573cf62', + AddressManagerImpl: '0x801a839da752289f448e0c9cc3bfb8d861077faa', + L1Erc721BridgeProxy: '0xfa74bcf421580f5ba021a3f3e270004743fdaa26', + SystemConfigProxy: '0xb8ad3a6beb0301f057c33f2a039a1044ec4d9bf9', OptimismMintableErc20FactoryProxy: - '0xaf7696edce2bb6a9fa384045d74ef2bba140d471', - L1StandardBridgeProxy: '0xf50e3728ff51d0276e92dff61595c6e94e533ac4', - L1CrossDomainMessengerProxy: '0x804875f04a33f11cd2dfaeaad5cc9c81a9bb5d98', - EthLockboxProxy: '0x867ac75de1cc6ab487305e04e2a92de8a5ea0a54', - DisputeGameFactoryProxy: '0x03a129b2bb0bfdfa7d5e7fbfcad7c1960e07389c', - AnchorStateRegistryProxy: '0xdffdb1ea0c6a8e84781a9604423b4549f79886f7', + '0xf197dd37d128b451ed546c0a88153d0eaaf1d7f2', + L1StandardBridgeProxy: '0x611bc60e604803b4f064810b4630290515bfba8c', + L1CrossDomainMessengerProxy: '0x96c9dcddc1cfd08ea3848395c390df815d8ec581', + EthLockboxProxy: '0x879122273ff3ee266fe58a8da8913f01c1065276', + DisputeGameFactoryProxy: '0xa34af21d8b896ea86bd3c5dd350f04ec8719e9f9', + AnchorStateRegistryProxy: '0x804bfedd469ff6b5669378d6c82e32517c010136', FaultDisputeGameImpl: '0x0000000000000000000000000000000000000000', - PermissionedDisputeGameImpl: '0x8fe3b7e258b3d7d45094b1424237ae4a5fc7b368', + PermissionedDisputeGameImpl: '0x3369335fdc75f5cfd377dc63e9e3c570bbdb2123', DelayedWethPermissionedGameProxy: - '0xa909e9c7f6d66bd40227e3b6f647615e696c7115', + '0xca17f9597d2f85db54a603dae9761f180dded6e8', DelayedWethPermissionlessGameProxy: '0x0000000000000000000000000000000000000000', AltDAChallengeProxy: '0x0000000000000000000000000000000000000000', From 278b568b73c1cf59cbe6bdba925f469e93901abc Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Tue, 9 Dec 2025 18:18:23 +0300 Subject: [PATCH 036/141] fix: bridge state amount input --- libs/bridge/src/lib/hooks/use-bridge-state.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/bridge/src/lib/hooks/use-bridge-state.ts b/libs/bridge/src/lib/hooks/use-bridge-state.ts index 9ca9d093e..0f649565a 100644 --- a/libs/bridge/src/lib/hooks/use-bridge-state.ts +++ b/libs/bridge/src/lib/hooks/use-bridge-state.ts @@ -251,7 +251,7 @@ export function useBridgeState({ setBridgeAmount(undefined); return; } - const numValue = parseFloat(value); + const numValue = Number(value); if (!isNaN(numValue)) { setBridgeAmount(numValue); } From 276aa348be5f66a39832497d655116f37f681bf0 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Wed, 10 Dec 2025 12:17:48 +0300 Subject: [PATCH 037/141] chore: typo approve msg --- libs/bridge/src/lib/components/bridge-status-messages.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/bridge/src/lib/components/bridge-status-messages.tsx b/libs/bridge/src/lib/components/bridge-status-messages.tsx index 0d21b67af..c2c8e6be6 100644 --- a/libs/bridge/src/lib/components/bridge-status-messages.tsx +++ b/libs/bridge/src/lib/components/bridge-status-messages.tsx @@ -130,7 +130,7 @@ export function BridgeStatusMessages({

Approval Required

You need to approve the bridge contract to spend your{' '} - {selectedToken.symbol} tokens. This is a one-time transaction + {selectedToken.symbol} tokens. This is a transaction required before bridging ERC-20 tokens.

From 8cd519f8f9d61250285e0c5dffc487d362d196d3 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Wed, 10 Dec 2025 20:28:52 +0300 Subject: [PATCH 038/141] feat: left only en locale --- apps/shell/src/tolgee/shared.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/shell/src/tolgee/shared.ts b/apps/shell/src/tolgee/shared.ts index 23a7090b3..f98519eb9 100644 --- a/apps/shell/src/tolgee/shared.ts +++ b/apps/shell/src/tolgee/shared.ts @@ -2,14 +2,14 @@ import { FormatIcu } from '@tolgee/format-icu'; import { DevTools, Tolgee, FormatSimple, TolgeeStaticData } from '@tolgee/web'; import { env } from '../env/client'; -export const AVAILABLE_LOCALES = ['en', 'ar', 'id', 'tr'] as const; +export const AVAILABLE_LOCALES = ['en'] as const; export type Locale = (typeof AVAILABLE_LOCALES)[number]; export const LOCALE_LABELS: Record = { en: { label: 'English', emoji: '🇬🇧' }, - ar: { label: 'العربية', emoji: '🇸🇦' }, - id: { label: 'Bahasa Indonesia', emoji: '🇮🇩' }, - tr: { label: 'Türkçe', emoji: '🇹🇷' }, + // ar: { label: 'العربية', emoji: '🇸🇦' }, + // id: { label: 'Bahasa Indonesia', emoji: '🇮🇩' }, + // tr: { label: 'Türkçe', emoji: '🇹🇷' }, }; export type AllNamespaces = (typeof ALL_NAMESPACES)[number]; From fff7632a9847343e3e6479cdf34895d3383c81c7 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Wed, 10 Dec 2025 20:30:12 +0300 Subject: [PATCH 039/141] fix: bridge status msg --- libs/bridge/src/lib/components/bridge-status-messages.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/bridge/src/lib/components/bridge-status-messages.tsx b/libs/bridge/src/lib/components/bridge-status-messages.tsx index c2c8e6be6..f1e463a3f 100644 --- a/libs/bridge/src/lib/components/bridge-status-messages.tsx +++ b/libs/bridge/src/lib/components/bridge-status-messages.tsx @@ -130,8 +130,8 @@ export function BridgeStatusMessages({

Approval Required

You need to approve the bridge contract to spend your{' '} - {selectedToken.symbol} tokens. This is a transaction - required before bridging ERC-20 tokens. + {selectedToken.symbol} tokens. This is a transaction required + before bridging ERC-20 tokens.

)} From 5e0e7e435e5531541c1fb9d2c78a051749707eea Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Thu, 11 Dec 2025 15:57:21 +0300 Subject: [PATCH 040/141] feat: proves for withdrawals --- libs/bridge/src/lib/bridge-page.tsx | 7 +- .../lib/components/pending-withdrawals.tsx | 2 +- .../src/lib/components/recovery-link.tsx | 4 +- .../hooks/use-bridge-transaction-receipt.ts | 2 +- .../src/lib/hooks/use-bridge-transaction.ts | 28 +++- .../src/lib/hooks/use-l2-to-l1-withdrawal.ts | 100 ++++++++++++- .../src/lib/hooks/use-withdrawal-recovery.ts | 136 +++++++++++++++++- libs/shared/src/abi/L2StandardBridge.ts | 27 ++++ libs/shared/src/index.ts | 1 + package.json | 1 + pnpm-lock.yaml | 12 ++ 11 files changed, 302 insertions(+), 18 deletions(-) create mode 100644 libs/shared/src/abi/L2StandardBridge.ts diff --git a/libs/bridge/src/lib/bridge-page.tsx b/libs/bridge/src/lib/bridge-page.tsx index 54eb025f6..a7ef21821 100644 --- a/libs/bridge/src/lib/bridge-page.tsx +++ b/libs/bridge/src/lib/bridge-page.tsx @@ -1,5 +1,5 @@ 'use client'; -import { useState, useLayoutEffect } from 'react'; +import { useState, useLayoutEffect, useEffect } from 'react'; import { useTranslate } from '@tolgee/react'; import { sepolia } from 'viem/chains'; import { useSwitchChain } from 'wagmi'; @@ -142,6 +142,11 @@ export function BridgePage() { const { setTxHash, isWaitingForReceipt, isTxSuccess } = useBridgeTransactionReceipt(); + // Reset transaction success state when user changes network, amount, or token + useEffect(() => { + setTxHash(null); + }, [chain?.id, bridgeAmount, selectedToken?.address, setTxHash]); + // Bridge transaction hook const { bridgeTokens, isProcessing, isProving, isFinalizing } = useBridgeTransaction({ diff --git a/libs/bridge/src/lib/components/pending-withdrawals.tsx b/libs/bridge/src/lib/components/pending-withdrawals.tsx index e054fa329..130a4e8ff 100644 --- a/libs/bridge/src/lib/components/pending-withdrawals.tsx +++ b/libs/bridge/src/lib/components/pending-withdrawals.tsx @@ -18,7 +18,7 @@ export function PendingWithdrawals({ orders }: PendingWithdrawalsProps) { return (

- {t('pending-withdrawals', 'Pending ETH Withdrawals')}: + {t('pending-withdrawals', 'Pending Withdrawals')}:

diff --git a/libs/bridge/src/lib/components/recovery-link.tsx b/libs/bridge/src/lib/components/recovery-link.tsx index c4bfbf06f..c54cff553 100644 --- a/libs/bridge/src/lib/components/recovery-link.tsx +++ b/libs/bridge/src/lib/components/recovery-link.tsx @@ -22,8 +22,8 @@ export interface RecoveryLinkProps { */ export function RecoveryLink({ href = '/bridge/recovery', - title = 'Lost track of your ETH withdrawal?', - description = 'Use transaction hash to recover and complete your ETH withdrawal.', + title = 'Lost track of your withdrawal?', + description = 'Use transaction hash to recover and complete your withdrawal.', linkText = 'Recover Withdrawal', className = '', }: RecoveryLinkProps) { diff --git a/libs/bridge/src/lib/hooks/use-bridge-transaction-receipt.ts b/libs/bridge/src/lib/hooks/use-bridge-transaction-receipt.ts index 1d2f7ab40..142dda177 100644 --- a/libs/bridge/src/lib/hooks/use-bridge-transaction-receipt.ts +++ b/libs/bridge/src/lib/hooks/use-bridge-transaction-receipt.ts @@ -27,6 +27,6 @@ export function useBridgeTransactionReceipt(): UseBridgeTransactionReceiptReturn txHash, setTxHash, isWaitingForReceipt: Boolean(isWaitingForReceipt), - isTxSuccess: Boolean(isTxSuccess), + isTxSuccess: Boolean(isTxSuccess) && !!txHash, }; } diff --git a/libs/bridge/src/lib/hooks/use-bridge-transaction.ts b/libs/bridge/src/lib/hooks/use-bridge-transaction.ts index e03bf448d..e58686108 100644 --- a/libs/bridge/src/lib/hooks/use-bridge-transaction.ts +++ b/libs/bridge/src/lib/hooks/use-bridge-transaction.ts @@ -66,6 +66,7 @@ export function useBridgeTransaction({ // Use L2 to L1 withdrawal hook for L2 -> L1 transfers const { initiateWithdrawal, + initiateERC20Withdrawal, isProcessing: isL2Processing, isProving, isFinalizing, @@ -100,12 +101,27 @@ export function useBridgeTransaction({ sourceChainId === CHAIN_CONFIG.l2ChainId && targetChainId === CHAIN_CONFIG.l1ChainId; - if (isL2ToL1 && token.address === ETH_ADDRESS) { - // Handle L2 to L1 native ETH withdrawal - console.log(`Initiating L2 to L1 withdrawal of ${amount} ETH`); - await initiateWithdrawal(amount, userAddress); - - return; + if (isL2ToL1) { + if (token.address === ETH_ADDRESS) { + // Handle L2 to L1 native ETH withdrawal + console.log(`Initiating L2 to L1 withdrawal of ${amount} ETH`); + await initiateWithdrawal(amount, userAddress); + return; + } else { + // Handle L2 to L1 ERC20 token withdrawal + console.log( + `Initiating L2 to L1 withdrawal of ${amount} ${token.symbol}`, + ); + const tokenDecimals = token.decimals || fallbackDecimals; + await initiateERC20Withdrawal( + token.address, + amount, + userAddress, + tokenDecimals, + token.symbol, + ); + return; + } } // Handle L1 to L2 transfers or ERC-20 tokens diff --git a/libs/bridge/src/lib/hooks/use-l2-to-l1-withdrawal.ts b/libs/bridge/src/lib/hooks/use-l2-to-l1-withdrawal.ts index 4afe380fd..0960d6379 100644 --- a/libs/bridge/src/lib/hooks/use-l2-to-l1-withdrawal.ts +++ b/libs/bridge/src/lib/hooks/use-l2-to-l1-withdrawal.ts @@ -1,10 +1,14 @@ 'use client'; import { useCallback, useState } from 'react'; -import { parseEther } from 'viem'; +import { parseEther, parseUnits } from 'viem'; import { getWithdrawals } from 'viem/op-stack'; import { useSwitchChain } from 'wagmi'; -import { useToast } from '@haqq/shell-shared'; +import { + useToast, + L2StandardBridgeAbi, + L2_STANDARD_BRIDGE_ADDRESS, +} from '@haqq/shell-shared'; import { useOpStackClients } from './use-op-stack-clients'; import { useWithdrawalOrders } from './use-withdrawal-orders'; import { useWithdrawalTimers } from './use-withdrawal-timers'; @@ -20,6 +24,13 @@ interface UseL2ToL1WithdrawalParams { interface UseL2ToL1WithdrawalReturn { initiateWithdrawal: (amount: number, toAddress: string) => Promise; + initiateERC20Withdrawal: ( + tokenAddress: string, + amount: number, + toAddress: string, + tokenDecimals?: number, + tokenSymbol?: string, + ) => Promise; proveWithdrawal: (withdrawalHash: string) => Promise; finalizeWithdrawal: (withdrawalHash: string) => Promise; isProcessing: boolean; @@ -44,7 +55,7 @@ interface UseL2ToL1WithdrawalReturn { } /** - * Hook to handle L2 to L1 native ETH withdrawals with prove and finalize states + * Hook to handle L2 to L1 withdrawals (both ETH and ERC20 tokens) with prove and finalize states */ export function useL2ToL1Withdrawal({ onSuccess, @@ -147,6 +158,88 @@ export function useL2ToL1Withdrawal({ onError, ], ); + + const initiateERC20Withdrawal = useCallback( + async ( + tokenAddress: string, + amount: number, + toAddress: string, + tokenDecimals = 18, + tokenSymbolOverride?: string, + ): Promise => { + if (!walletClientL2) { + throw new Error('Wallet not connected or wallet client not available'); + } + + setIsProcessing(true); + setError(null); + + try { + // Step 1: Parse the token amount with correct decimals + const amountWei = parseUnits(amount.toString(), tokenDecimals); + + // Step 2: Call withdrawTo on L2StandardBridge contract + // According to Optimism docs: withdrawTo initiates ERC20 withdrawal from L2 to L1 + const hash = await walletClientL2.writeContract({ + address: L2_STANDARD_BRIDGE_ADDRESS as `0x${string}`, + abi: L2StandardBridgeAbi, + functionName: 'withdrawTo', + args: [ + tokenAddress as `0x${string}`, // _l2Token + toAddress as `0x${string}`, // _to + amountWei, // _amount + 200000, // _minGasLimit + '0x' as `0x${string}`, // _extraData + ], + }); + + // Step 3: Wait for the withdrawal transaction receipt + const receipt = await publicClientReadonlyL2.waitForTransactionReceipt({ + hash, + }); + + console.log(`ERC20 withdrawal initiated successfully: ${hash}`); + + // Save withdrawal order to local storage + addWithdrawalOrder({ + amount, + toAddress, + fromAddress: walletClientL2.account.address, + initiateHash: hash, + status: WithdrawalStatus.INITIATED, + sourceChainId: chains.L2.id, + targetChainId: chains.L1.id, + tokenSymbol: tokenSymbolOverride || tokenSymbol, + }); + + onSuccess?.(receipt.transactionHash); + return hash; + } catch (err) { + const errorMessage = + err instanceof Error + ? err.message + : 'ERC20 withdrawal initiation failed'; + console.error('ERC20 withdrawal initiation failed:', err); + setError(errorMessage); + toast.error('ERC20 withdrawal initiation failed'); + onError?.(err as Error); + throw err; + } finally { + setIsProcessing(false); + } + }, + [ + walletClientL2, + publicClientReadonlyL2, + chains, + addWithdrawalOrder, + tokenSymbol, + onSuccess, + onError, + toast, + ], + ); + const { switchChainAsync } = useSwitchChain(); const proveWithdrawal = useCallback( @@ -338,6 +431,7 @@ export function useL2ToL1Withdrawal({ return { initiateWithdrawal, + initiateERC20Withdrawal, proveWithdrawal, finalizeWithdrawal, isProcessing, diff --git a/libs/bridge/src/lib/hooks/use-withdrawal-recovery.ts b/libs/bridge/src/lib/hooks/use-withdrawal-recovery.ts index b7c553ea6..b663b3ea1 100644 --- a/libs/bridge/src/lib/hooks/use-withdrawal-recovery.ts +++ b/libs/bridge/src/lib/hooks/use-withdrawal-recovery.ts @@ -2,11 +2,34 @@ import { useCallback, useState, useEffect } from 'react'; import { useLocalStorage } from 'usehooks-ts'; +import { decodeFunctionData, formatUnits } from 'viem'; import { getWithdrawals } from 'viem/op-stack'; +import { + L2StandardBridgeAbi, + L2_STANDARD_BRIDGE_ADDRESS, +} from '@haqq/shell-shared'; import { useOpStackClients } from './use-op-stack-clients'; import { useWithdrawalOrders } from './use-withdrawal-orders'; import { WithdrawalStatus, WithdrawalOrder } from '../types/withdrawal-order'; +// ERC20 ABI for reading token info +const ERC20_ABI = [ + { + inputs: [], + name: 'symbol', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'decimals', + outputs: [{ internalType: 'uint8', name: '', type: 'uint8' }], + stateMutability: 'view', + type: 'function', + }, +] as const; + interface UseWithdrawalRecoveryReturn { recoverWithdrawal: (txHash: string) => Promise; recoveredOrder: WithdrawalOrder | null; @@ -102,7 +125,109 @@ export function useWithdrawalRecovery(): UseWithdrawalRecoveryReturn { throw new Error('Transaction not found'); } - // Step 4: Check withdrawal status using viem's getWithdrawalStatus + // Step 4: Determine if this is an ERC20 or ETH withdrawal + let amount: number; + let tokenSymbol: string; + const isERC20Withdrawal = + transaction.to?.toLowerCase() === + L2_STANDARD_BRIDGE_ADDRESS.toLowerCase(); + + if (isERC20Withdrawal && transaction.input) { + // ERC20 withdrawal: decode transaction input to get token address and amount + try { + const decoded = decodeFunctionData({ + abi: L2StandardBridgeAbi, + data: transaction.input, + }); + + if (decoded.functionName === 'withdrawTo') { + // withdrawTo(_l2Token, _to, _amount, _minGasLimit, _extraData) + const args = decoded.args as readonly [ + `0x${string}`, + `0x${string}`, + bigint, + number, + `0x${string}`, + ]; + const [l2Token, , amountWei] = args; + + // Get token symbol and decimals + try { + const [symbol, decimals] = await Promise.all([ + publicClientReadonlyL2.readContract({ + address: l2Token, + abi: ERC20_ABI, + functionName: 'symbol', + }), + publicClientReadonlyL2.readContract({ + address: l2Token, + abi: ERC20_ABI, + functionName: 'decimals', + }), + ]); + + tokenSymbol = symbol as string; + amount = Number(formatUnits(amountWei, Number(decimals))); + } catch (tokenError) { + console.warn( + 'Failed to read token info, using defaults:', + tokenError, + ); + tokenSymbol = 'UNKNOWN'; + amount = Number(formatUnits(amountWei, 18)); + } + } else if (decoded.functionName === 'withdraw') { + // withdraw(_l2Token, _amount, _minGasLimit, _extraData) + const args = decoded.args as readonly [ + `0x${string}`, + bigint, + number, + `0x${string}`, + ]; + const [l2Token, amountWei] = args; + + // Get token symbol and decimals + try { + const [symbol, decimals] = await Promise.all([ + publicClientReadonlyL2.readContract({ + address: l2Token, + abi: ERC20_ABI, + functionName: 'symbol', + }), + publicClientReadonlyL2.readContract({ + address: l2Token, + abi: ERC20_ABI, + functionName: 'decimals', + }), + ]); + + tokenSymbol = symbol as string; + amount = Number(formatUnits(amountWei, Number(decimals))); + } catch (tokenError) { + console.warn( + 'Failed to read token info, using defaults:', + tokenError, + ); + tokenSymbol = 'UNKNOWN'; + amount = Number(formatUnits(amountWei, 18)); + } + } else { + throw new Error( + 'Transaction is not a valid ERC20 withdrawal (withdraw/withdrawTo)', + ); + } + } catch (decodeError) { + throw new Error( + `Failed to decode ERC20 withdrawal transaction: ${decodeError instanceof Error ? decodeError.message : 'Unknown error'}`, + ); + } + } else { + // ETH withdrawal: use withdrawal.value + tokenSymbol = 'ETH'; + amount = Number(withdrawal.value) / 1e18; + } + + // Step 5: Check withdrawal status using viem's getWithdrawalStatus let status; try { @@ -140,18 +265,21 @@ export function useWithdrawalRecovery(): UseWithdrawalRecoveryReturn { txHash, withdrawal, status, + isERC20Withdrawal, + tokenSymbol, + amount, }); - // Step 5: Add to withdrawal orders + // Step 6: Add to withdrawal orders const order = addWithdrawalOrder({ - amount: Number(withdrawal.value) / 1e18, // Convert from wei + amount, toAddress: withdrawal.target, fromAddress: transaction.from, initiateHash: txHash, status, sourceChainId: chains.L2.id, targetChainId: chains.L1.id, - tokenSymbol: 'ETH', // Assuming ETH for now, could be extended for tokens + tokenSymbol, }); // Get the newly created order diff --git a/libs/shared/src/abi/L2StandardBridge.ts b/libs/shared/src/abi/L2StandardBridge.ts new file mode 100644 index 000000000..a31d05f7c --- /dev/null +++ b/libs/shared/src/abi/L2StandardBridge.ts @@ -0,0 +1,27 @@ +export const L2StandardBridgeAbi = [ + { + inputs: [ + { internalType: 'address', name: '_l2Token', type: 'address' }, + { internalType: 'uint256', name: '_amount', type: 'uint256' }, + { internalType: 'uint32', name: '_minGasLimit', type: 'uint32' }, + { internalType: 'bytes', name: '_extraData', type: 'bytes' }, + ], + name: 'withdraw', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '_l2Token', type: 'address' }, + { internalType: 'address', name: '_to', type: 'address' }, + { internalType: 'uint256', name: '_amount', type: 'uint256' }, + { internalType: 'uint32', name: '_minGasLimit', type: 'uint32' }, + { internalType: 'bytes', name: '_extraData', type: 'bytes' }, + ], + name: 'withdrawTo', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, +] as const; diff --git a/libs/shared/src/index.ts b/libs/shared/src/index.ts index 222a5ca06..627ecfdee 100644 --- a/libs/shared/src/index.ts +++ b/libs/shared/src/index.ts @@ -49,3 +49,4 @@ export * from './utils/bridge'; export * from './utils/chain-utils'; export * from './utils/chains'; export * from './abi/L1StandartBridge'; +export * from './abi/L2StandardBridge'; diff --git a/package.json b/package.json index f1d243a81..b2cca5cf9 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "dependencies": { "@auth0/auth0-react": "2.3.0", "@bufbuild/protobuf": "1.10.0", + "@eth-optimism/viem": "0.4.14", "@ethersproject/hash": "5.8.0", "@ethersproject/signing-key": "5.8.0", "@evmos/eip712": "0.2.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e8eb7579b..f020b30d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@bufbuild/protobuf': specifier: 1.10.0 version: 1.10.0 + '@eth-optimism/viem': + specifier: 0.4.14 + version: 0.4.14(viem@2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)) '@ethersproject/hash': specifier: 5.8.0 version: 5.8.0 @@ -1963,6 +1966,11 @@ packages: resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@eth-optimism/viem@0.4.14': + resolution: {integrity: sha512-TKC40inv/cZWi7+eEuFXiGayHkO+bILNbRH89ZmOjygb6CH5YZOo3TyiOCmkdPaQVgMeaYQlycEsFbPCY5o6pw==} + peerDependencies: + viem: ^2.17.9 + '@ethereumjs/common@3.2.0': resolution: {integrity: sha512-pksvzI0VyLgmuEF2FA/JR/4/y6hcPq8OUail3/AvycBaW1d5VSauOZzqGvJ3RTmR4MU35lWE8KseKOsEhrFRBA==} @@ -14941,6 +14949,10 @@ snapshots: '@eslint/js@8.57.0': {} + '@eth-optimism/viem@0.4.14(viem@2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))': + dependencies: + viem: 2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) + '@ethereumjs/common@3.2.0': dependencies: '@ethereumjs/util': 8.1.0 From fa910a589c689a39efbf5c9492497da85ce69199 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Thu, 11 Dec 2025 16:05:24 +0300 Subject: [PATCH 041/141] fix: locale direction --- apps/shell/src/app/[locale]/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/shell/src/app/[locale]/layout.tsx b/apps/shell/src/app/[locale]/layout.tsx index 228758c24..243225f09 100644 --- a/apps/shell/src/app/[locale]/layout.tsx +++ b/apps/shell/src/app/[locale]/layout.tsx @@ -119,7 +119,7 @@ export default async function RootLayout({ return ( From b40e3a68cb477dc46283517e5bfb8ecca8394dfc Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Thu, 11 Dec 2025 17:41:31 +0300 Subject: [PATCH 042/141] fix: proves state --- .../lib/components/withdrawal-order-card.tsx | 7 +++- .../src/lib/hooks/use-l2-to-l1-withdrawal.ts | 35 +++++++++++++++++-- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/libs/bridge/src/lib/components/withdrawal-order-card.tsx b/libs/bridge/src/lib/components/withdrawal-order-card.tsx index 034a5a21f..5aca8deef 100644 --- a/libs/bridge/src/lib/components/withdrawal-order-card.tsx +++ b/libs/bridge/src/lib/components/withdrawal-order-card.tsx @@ -10,6 +10,7 @@ import { ExternalLink, } from 'lucide-react'; import Link from 'next/link'; +import { useAccount } from 'wagmi'; import { getAddressExplorerUrl, getTxExplorerUrl } from '@haqq/shell-shared'; import { Button, Tooltip } from '@haqq/shell-ui-kit'; import { OP_STACK_CHAINS } from '../constants/op-stack-config'; @@ -30,6 +31,7 @@ const formatAmount = (amount: number, symbol: string) => { export function WithdrawalOrderCard({ order }: WithdrawalOrderCardProps) { const { t } = useTranslate('common'); + const { isConnected } = useAccount(); const [isProcessing, setIsProcessing] = useState(false); const [timerInfo, setTimerInfo] = useState<{ seconds: number; @@ -214,7 +216,10 @@ export function WithdrawalOrderCard({ order }: WithdrawalOrderCardProps) { onClick={nextAction.action} variant={5} disabled={ - nextAction.disabled || isProcessing || !timerInfo?.isReady + nextAction.disabled || + isProcessing || + !timerInfo?.isReady || + !isConnected } className="rounded-md px-4 py-2 text-sm font-medium text-white disabled:cursor-not-allowed disabled:opacity-50" > diff --git a/libs/bridge/src/lib/hooks/use-l2-to-l1-withdrawal.ts b/libs/bridge/src/lib/hooks/use-l2-to-l1-withdrawal.ts index 0960d6379..13f536449 100644 --- a/libs/bridge/src/lib/hooks/use-l2-to-l1-withdrawal.ts +++ b/libs/bridge/src/lib/hooks/use-l2-to-l1-withdrawal.ts @@ -3,7 +3,7 @@ import { useCallback, useState } from 'react'; import { parseEther, parseUnits } from 'viem'; import { getWithdrawals } from 'viem/op-stack'; -import { useSwitchChain } from 'wagmi'; +import { useAccount, useSwitchChain } from 'wagmi'; import { useToast, L2StandardBridgeAbi, @@ -86,6 +86,7 @@ export function useL2ToL1Withdrawal({ publicClientReadonlyL2, } = useOpStackClients(); + const { isConnected } = useAccount(); const toast = useToast(); const initiateWithdrawal = useCallback( @@ -244,8 +245,20 @@ export function useL2ToL1Withdrawal({ const proveWithdrawal = useCallback( async (withdrawalHash: string): Promise => { + if (!isConnected) { + const errorMessage = + 'Wallet not connected. Please connect your wallet to prove the withdrawal.'; + console.error('Prove withdrawal failed:', errorMessage); + toast.error(errorMessage); + throw new Error(errorMessage); + } + if (!walletClientL1) { - throw new Error('Wallet client not available for proving'); + const errorMessage = + 'Wallet client not available. Please ensure your wallet is connected and try again.'; + console.error('Prove withdrawal failed:', errorMessage); + toast.error(errorMessage); + throw new Error(errorMessage); } setIsProving(true); @@ -312,6 +325,7 @@ export function useL2ToL1Withdrawal({ } }, [ + isConnected, walletClientL1, publicClientL1, publicClientL2, @@ -321,13 +335,26 @@ export function useL2ToL1Withdrawal({ updateOrderByInitiateHash, onProveSuccess, onError, + toast, ], ); const finalizeWithdrawal = useCallback( async (withdrawalHash: string): Promise => { + if (!isConnected) { + const errorMessage = + 'Wallet not connected. Please connect your wallet to finalize the withdrawal.'; + console.error('Finalize withdrawal failed:', errorMessage); + toast.error(errorMessage); + throw new Error(errorMessage); + } + if (!walletClientL1) { - throw new Error('Wallet client not available for finalizing'); + const errorMessage = + 'Wallet client not available. Please ensure your wallet is connected and try again.'; + console.error('Finalize withdrawal failed:', errorMessage); + toast.error(errorMessage); + throw new Error(errorMessage); } setIsFinalizing(true); @@ -391,6 +418,7 @@ export function useL2ToL1Withdrawal({ } }, [ + isConnected, walletClientL1, publicClientL1, publicClientL2, @@ -400,6 +428,7 @@ export function useL2ToL1Withdrawal({ updateOrderByInitiateHash, onFinalizeSuccess, onError, + toast, ], ); From 844b3f4a6d82afd1692ffe1510f780d91a57fd70 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Fri, 12 Dec 2025 13:02:44 +0300 Subject: [PATCH 043/141] fix: bridge block --- .../bridge/src/lib/components/bridge-receive-input.tsx | 10 ++++------ libs/bridge/src/lib/hooks/use-bridge-state.ts | 6 ++++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/libs/bridge/src/lib/components/bridge-receive-input.tsx b/libs/bridge/src/lib/components/bridge-receive-input.tsx index c2bab7e03..2f1eb3b96 100644 --- a/libs/bridge/src/lib/components/bridge-receive-input.tsx +++ b/libs/bridge/src/lib/components/bridge-receive-input.tsx @@ -1,5 +1,5 @@ 'use client'; -import { StringInput } from '@haqq/shell-ui-kit'; +import { ModalInput, StringInput } from '@haqq/shell-ui-kit'; export interface BridgeReceiveInputProps { receivedAmount: number | undefined; @@ -15,14 +15,12 @@ export function BridgeReceiveInput({ - { // Read-only input }} - readOnly={true} - placeholder={`0 ${tokenSymbol}`} - className="!bg-[#F5F5F5] !text-[#0D0D0E80]" />
); diff --git a/libs/bridge/src/lib/hooks/use-bridge-state.ts b/libs/bridge/src/lib/hooks/use-bridge-state.ts index 0f649565a..0d90acb68 100644 --- a/libs/bridge/src/lib/hooks/use-bridge-state.ts +++ b/libs/bridge/src/lib/hooks/use-bridge-state.ts @@ -251,8 +251,10 @@ export function useBridgeState({ setBridgeAmount(undefined); return; } - const numValue = Number(value); - if (!isNaN(numValue)) { + // Remove commas and other formatting characters, but keep decimal point + const cleanedValue = value.replace(/,/g, '').trim(); + const numValue = Number(cleanedValue); + if (!isNaN(numValue) && isFinite(numValue)) { setBridgeAmount(numValue); } }, []); From 9b5e42685cf637feeaa23417c3a932ededc94abd Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Fri, 12 Dec 2025 22:20:31 +0300 Subject: [PATCH 044/141] feat: enable precompiles --- libs/staking/src/lib/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/staking/src/lib/constants.ts b/libs/staking/src/lib/constants.ts index e2cbcbf2f..e3ed3c2a9 100644 --- a/libs/staking/src/lib/constants.ts +++ b/libs/staking/src/lib/constants.ts @@ -1,4 +1,4 @@ -export const shouldUsePrecompile = false; +export const shouldUsePrecompile = true; export const stISLM_MAINNET = '0x12fEFEAc0568503F7C0D934c149f29a42B05C48f'; From 4608a689f21c2d0522f2fc6af90d51d0893fc85d Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Thu, 18 Dec 2025 19:13:00 +0300 Subject: [PATCH 045/141] chore: update viem package to version 2.43.1 and refactor wallet client handling in withdrawal hooks --- .../lib/components/pending-withdrawals.tsx | 2 + .../src/lib/hooks/use-l2-to-l1-withdrawal.ts | 53 +++---- .../src/lib/hooks/use-op-stack-clients.ts | 36 ++++- package.json | 2 +- pnpm-lock.yaml | 132 +++++++++++------- 5 files changed, 145 insertions(+), 80 deletions(-) diff --git a/libs/bridge/src/lib/components/pending-withdrawals.tsx b/libs/bridge/src/lib/components/pending-withdrawals.tsx index 130a4e8ff..f023acca7 100644 --- a/libs/bridge/src/lib/components/pending-withdrawals.tsx +++ b/libs/bridge/src/lib/components/pending-withdrawals.tsx @@ -15,6 +15,8 @@ export function PendingWithdrawals({ orders }: PendingWithdrawalsProps) { return null; } + console.log('orders', orders); + return (

diff --git a/libs/bridge/src/lib/hooks/use-l2-to-l1-withdrawal.ts b/libs/bridge/src/lib/hooks/use-l2-to-l1-withdrawal.ts index 13f536449..6245c36ae 100644 --- a/libs/bridge/src/lib/hooks/use-l2-to-l1-withdrawal.ts +++ b/libs/bridge/src/lib/hooks/use-l2-to-l1-withdrawal.ts @@ -78,9 +78,8 @@ export function useL2ToL1Withdrawal({ useWithdrawalTimers(); const { publicClientL1, - publicClientL2, - walletClientL1, - walletClientL2, + getWalletClientL1, + getWalletClientL2, chains, publicClientReadonlyL1, publicClientReadonlyL2, @@ -91,7 +90,9 @@ export function useL2ToL1Withdrawal({ const initiateWithdrawal = useCallback( async (amount: number, toAddress: string): Promise => { - if (!walletClientL2) { + const walletClient = await getWalletClientL2(); + + if (!walletClient) { throw new Error('Wallet not connected or wallet client not available'); } @@ -102,15 +103,14 @@ export function useL2ToL1Withdrawal({ // Step 1: Build parameters to initiate the withdrawal transaction on the L1 // According to Viem docs: "Build parameters to initiate the withdrawal transaction on the L1" const args = await publicClientL1.buildInitiateWithdrawal({ - account: walletClientL2.account, + account: walletClient.account, to: toAddress as `0x${string}`, value: parseEther(amount.toString()), - gas: 21_000n, // Gas limit for transaction execution on the L1 }); // Step 2: Execute the initiate withdrawal transaction on the L2 // According to Viem docs: "Execute the initiate withdrawal transaction on the L2" - const hash = await walletClientL2.initiateWithdrawal({ + const hash = await walletClient.initiateWithdrawal({ ...args, }); @@ -126,7 +126,7 @@ export function useL2ToL1Withdrawal({ addWithdrawalOrder({ amount, toAddress, - fromAddress: walletClientL2.account.address, + fromAddress: walletClient.account.address, initiateHash: hash, status: WithdrawalStatus.INITIATED, sourceChainId: chains.L2.id, @@ -149,14 +149,15 @@ export function useL2ToL1Withdrawal({ } }, [ - walletClientL2, + getWalletClientL2, publicClientL1, - publicClientL2, publicClientReadonlyL2, chains, addWithdrawalOrder, onSuccess, onError, + tokenSymbol, + toast, ], ); @@ -168,7 +169,9 @@ export function useL2ToL1Withdrawal({ tokenDecimals = 18, tokenSymbolOverride?: string, ): Promise => { - if (!walletClientL2) { + const walletClient = await getWalletClientL2(); + + if (!walletClient) { throw new Error('Wallet not connected or wallet client not available'); } @@ -181,7 +184,7 @@ export function useL2ToL1Withdrawal({ // Step 2: Call withdrawTo on L2StandardBridge contract // According to Optimism docs: withdrawTo initiates ERC20 withdrawal from L2 to L1 - const hash = await walletClientL2.writeContract({ + const hash = await walletClient.writeContract({ address: L2_STANDARD_BRIDGE_ADDRESS as `0x${string}`, abi: L2StandardBridgeAbi, functionName: 'withdrawTo', @@ -205,7 +208,7 @@ export function useL2ToL1Withdrawal({ addWithdrawalOrder({ amount, toAddress, - fromAddress: walletClientL2.account.address, + fromAddress: walletClient.account.address, initiateHash: hash, status: WithdrawalStatus.INITIATED, sourceChainId: chains.L2.id, @@ -230,7 +233,7 @@ export function useL2ToL1Withdrawal({ } }, [ - walletClientL2, + getWalletClientL2, publicClientReadonlyL2, chains, addWithdrawalOrder, @@ -253,7 +256,9 @@ export function useL2ToL1Withdrawal({ throw new Error(errorMessage); } - if (!walletClientL1) { + const walletClient = await getWalletClientL1(); + + if (!walletClient) { const errorMessage = 'Wallet client not available. Please ensure your wallet is connected and try again.'; console.error('Prove withdrawal failed:', errorMessage); @@ -290,7 +295,7 @@ export function useL2ToL1Withdrawal({ // Step 4: Prove the withdrawal on the L1 // According to Viem docs: "Prove the withdrawal on the L1" - const proveHash = await walletClientL1.proveWithdrawal({ + const proveHash = await walletClient.proveWithdrawal({ ...proveArgs, targetChain: chains.L2_WITH_CONTRACTS, }); @@ -326,9 +331,7 @@ export function useL2ToL1Withdrawal({ }, [ isConnected, - walletClientL1, - publicClientL1, - publicClientL2, + getWalletClientL1, publicClientReadonlyL1, publicClientReadonlyL2, chains, @@ -336,6 +339,7 @@ export function useL2ToL1Withdrawal({ onProveSuccess, onError, toast, + switchChainAsync, ], ); @@ -349,7 +353,9 @@ export function useL2ToL1Withdrawal({ throw new Error(errorMessage); } - if (!walletClientL1) { + const walletClient = await getWalletClientL1(); + + if (!walletClient) { const errorMessage = 'Wallet client not available. Please ensure your wallet is connected and try again.'; console.error('Finalize withdrawal failed:', errorMessage); @@ -381,7 +387,7 @@ export function useL2ToL1Withdrawal({ // Step 4: Finalize the withdrawal // According to Viem docs: "Finalize the withdrawal" - const finalizeHash = await walletClientL1.finalizeWithdrawal({ + const finalizeHash = await walletClient.finalizeWithdrawal({ targetChain: chains.L2_WITH_CONTRACTS, withdrawal, }); @@ -419,9 +425,7 @@ export function useL2ToL1Withdrawal({ }, [ isConnected, - walletClientL1, - publicClientL1, - publicClientL2, + getWalletClientL1, publicClientReadonlyL1, publicClientReadonlyL2, chains, @@ -429,6 +433,7 @@ export function useL2ToL1Withdrawal({ onFinalizeSuccess, onError, toast, + switchChainAsync, ], ); diff --git a/libs/bridge/src/lib/hooks/use-op-stack-clients.ts b/libs/bridge/src/lib/hooks/use-op-stack-clients.ts index d33bfe729..b3e11afbf 100644 --- a/libs/bridge/src/lib/hooks/use-op-stack-clients.ts +++ b/libs/bridge/src/lib/hooks/use-op-stack-clients.ts @@ -1,6 +1,6 @@ 'use client'; -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { createPublicClient, createWalletClient, custom, http } from 'viem'; import { publicActionsL1, @@ -73,6 +73,22 @@ export function useOpStackClients() { : null; }, [walletClient, address]); + const getWalletClientL1 = useCallback(async () => { + if (walletClientL1) { + return walletClientL1; + } + + if (address && typeof window !== 'undefined' && window.ethereum) { + return createWalletClient({ + account: address as `0x${string}`, + chain: OP_STACK_CHAINS.L1, + transport: custom(window.ethereum), + }).extend(walletActionsL1()); + } + + return null; + }, [address, walletClientL1]); + const walletClientReadonlyL1 = useMemo(() => { return walletClient && address ? createWalletClient({ @@ -97,6 +113,22 @@ export function useOpStackClients() { : null; }, [walletClient, address]); + const getWalletClientL2 = useCallback(async () => { + if (walletClientL2) { + return walletClientL2; + } + + if (address && typeof window !== 'undefined' && window.ethereum) { + return createWalletClient({ + account: address as `0x${string}`, + chain: OP_STACK_CHAINS.L2, + transport: custom(window.ethereum), + }).extend(walletActionsL2()); + } + + return null; + }, [address, walletClientL2]); + const walletClientReadonlyL2 = useMemo(() => { return walletClient && address ? createWalletClient({ @@ -112,6 +144,8 @@ export function useOpStackClients() { publicClientL2, walletClientL1, walletClientL2, + getWalletClientL1, + getWalletClientL2, publicClientReadonlyL1, publicClientReadonlyL2, walletClientReadonlyL1, diff --git a/package.json b/package.json index b2cca5cf9..58a23e8ac 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "tslib": "2.8.1", "usehooks-ts": "3.1.1", "vanilla-cookieconsent": "2.9.2", - "viem": "2.37.9", + "viem": "2.43.1", "wagmi": "2.17.5", "zod": "3.24.2" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f020b30d8..ab0cd4ab1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,7 +16,7 @@ importers: version: 1.10.0 '@eth-optimism/viem': specifier: 0.4.14 - version: 0.4.14(viem@2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)) + version: 0.4.14(viem@2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)) '@ethersproject/hash': specifier: 5.8.0 version: 5.8.0 @@ -79,7 +79,7 @@ importers: version: 1.2.0(next@14.2.23(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.80.5))(react@18.3.1) '@wagmi/core': specifier: 2.21.2 - version: 2.21.2(@tanstack/query-core@5.67.1)(@types/react@18.3.12)(react@18.3.1)(typescript@5.8.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)) + version: 2.21.2(@tanstack/query-core@5.67.1)(@types/react@18.3.12)(react@18.3.1)(typescript@5.8.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)) bech32: specifier: 2.0.0 version: 2.0.0 @@ -177,11 +177,11 @@ importers: specifier: 2.9.2 version: 2.9.2 viem: - specifier: 2.37.9 - version: 2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) + specifier: 2.43.1 + version: 2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) wagmi: specifier: 2.17.5 - version: 2.17.5(@tanstack/query-core@5.67.1)(@tanstack/react-query@5.67.1(react@18.3.1))(@types/react@18.3.12)(bufferutil@4.0.9)(encoding@0.1.13)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(viem@2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))(zod@3.24.2) + version: 2.17.5(@tanstack/query-core@5.67.1)(@tanstack/react-query@5.67.1(react@18.3.1))(@types/react@18.3.12)(bufferutil@4.0.9)(encoding@0.1.13)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(viem@2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))(zod@3.24.2) zod: specifier: 3.24.2 version: 3.24.2 @@ -5179,6 +5179,17 @@ packages: zod: optional: true + abitype@1.2.3: + resolution: {integrity: sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==} + peerDependencies: + typescript: '>=5.0.4' + zod: ^3.22.0 || ^4.0.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -9797,6 +9808,14 @@ packages: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} + ox@0.10.5: + resolution: {integrity: sha512-mXJRiZswmX46abrzNkJpTN9sPJ/Rhevsp5Dfg0z80D55aoLNmEV4oN+/+feSNW593c2CnHavMqSVBanpJ0lUkQ==} + peerDependencies: + typescript: '>=5.4.0' + peerDependenciesMeta: + typescript: + optional: true + ox@0.6.7: resolution: {integrity: sha512-17Gk/eFsFRAZ80p5eKqv89a57uXjd3NgIf1CaXojATPBuujVc/fQSVhBeAU9JCRB+k7J50WQAyWTxK19T9GgbA==} peerDependencies: @@ -12541,8 +12560,8 @@ packages: typescript: optional: true - viem@2.37.9: - resolution: {integrity: sha512-XXUOE5yJcjr9/M9kRoQcPMUfetwHprO9aTho6vNELjBKJIBx7rYq1fjvBw+xEnhsRjhh5lsORi6B0h8fYFB7NA==} + viem@2.43.1: + resolution: {integrity: sha512-S33pBNlRvOlVv4+L94Z8ydCMDB1j0cuHFUvaC28i6OTxw3uY1P4M3h1YDFK8YC1H9/lIbeBTTvCRhi0FqU/2iw==} peerDependencies: typescript: '>=5.0.4' peerDependenciesMeta: @@ -14196,7 +14215,7 @@ snapshots: idb-keyval: 6.2.1 ox: 0.6.9(typescript@5.8.2)(zod@3.24.2) preact: 10.24.2 - viem: 2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) + viem: 2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) zustand: 5.0.3(@types/react@18.3.12)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) transitivePeerDependencies: - '@types/react' @@ -14234,7 +14253,7 @@ snapshots: idb-keyval: 6.2.1 ox: 0.6.9(typescript@5.8.2)(zod@3.24.2) preact: 10.24.2 - viem: 2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) + viem: 2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) zustand: 5.0.3(@types/react@18.3.12)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) transitivePeerDependencies: - '@types/react' @@ -14949,9 +14968,9 @@ snapshots: '@eslint/js@8.57.0': {} - '@eth-optimism/viem@0.4.14(viem@2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))': + '@eth-optimism/viem@0.4.14(viem@2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))': dependencies: - viem: 2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) + viem: 2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) '@ethereumjs/common@3.2.0': dependencies: @@ -15181,11 +15200,11 @@ snapshots: dependencies: tslib: 2.8.1 - '@gemini-wallet/core@0.2.0(viem@2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))': + '@gemini-wallet/core@0.2.0(viem@2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))': dependencies: '@metamask/rpc-errors': 7.0.2 eventemitter3: 5.0.1 - viem: 2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) + viem: 2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) transitivePeerDependencies: - supports-color @@ -17244,7 +17263,7 @@ snapshots: dependencies: big.js: 6.2.2 dayjs: 1.11.13 - viem: 2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.22.4) + viem: 2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.22.4) transitivePeerDependencies: - bufferutil - typescript @@ -17255,7 +17274,7 @@ snapshots: dependencies: big.js: 6.2.2 dayjs: 1.11.13 - viem: 2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) + viem: 2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) transitivePeerDependencies: - bufferutil - typescript @@ -17268,7 +17287,7 @@ snapshots: '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10) '@walletconnect/universal-provider': 2.21.0(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) valtio: 1.13.2(@types/react@18.3.12)(react@18.3.1) - viem: 2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) + viem: 2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -17414,7 +17433,7 @@ snapshots: '@walletconnect/logger': 2.1.2 '@walletconnect/universal-provider': 2.21.0(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) valtio: 1.13.2(@types/react@18.3.12)(react@18.3.1) - viem: 2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) + viem: 2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -17467,7 +17486,7 @@ snapshots: '@walletconnect/universal-provider': 2.21.0(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) bs58: 6.0.0 valtio: 1.13.2(@types/react@18.3.12)(react@18.3.1) - viem: 2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) + viem: 2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -17652,7 +17671,7 @@ snapshots: '@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)': dependencies: '@safe-global/safe-gateway-typescript-sdk': 3.22.2 - viem: 2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) + viem: 2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) transitivePeerDependencies: - bufferutil - typescript @@ -19394,19 +19413,19 @@ snapshots: loupe: 3.1.2 tinyrainbow: 1.2.0 - '@wagmi/connectors@5.11.2(@tanstack/react-query@5.67.1(react@18.3.1))(@types/react@18.3.12)(@wagmi/core@2.21.2(@tanstack/query-core@5.67.1)(@types/react@18.3.12)(react@18.3.1)(typescript@5.8.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)))(bufferutil@4.0.9)(encoding@0.1.13)(react@18.3.1)(typescript@5.8.2)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(viem@2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))(wagmi@2.17.5(@tanstack/query-core@5.67.1)(@tanstack/react-query@5.67.1(react@18.3.1))(@types/react@18.3.12)(bufferutil@4.0.9)(encoding@0.1.13)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(viem@2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))(zod@3.24.2))(zod@3.24.2)': + '@wagmi/connectors@5.11.2(@tanstack/react-query@5.67.1(react@18.3.1))(@types/react@18.3.12)(@wagmi/core@2.21.2(@tanstack/query-core@5.67.1)(@types/react@18.3.12)(react@18.3.1)(typescript@5.8.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)))(bufferutil@4.0.9)(encoding@0.1.13)(react@18.3.1)(typescript@5.8.2)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(viem@2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))(wagmi@2.17.5(@tanstack/query-core@5.67.1)(@tanstack/react-query@5.67.1(react@18.3.1))(@types/react@18.3.12)(bufferutil@4.0.9)(encoding@0.1.13)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(viem@2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))(zod@3.24.2))(zod@3.24.2)': dependencies: '@base-org/account': 1.1.1(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.2)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@3.24.2) '@coinbase/wallet-sdk': 4.3.6(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.2)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@3.24.2) - '@gemini-wallet/core': 0.2.0(viem@2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)) + '@gemini-wallet/core': 0.2.0(viem@2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)) '@metamask/sdk': 0.33.1(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10) '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) - '@wagmi/core': 2.21.2(@tanstack/query-core@5.67.1)(@types/react@18.3.12)(react@18.3.1)(typescript@5.8.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)) + '@wagmi/core': 2.21.2(@tanstack/query-core@5.67.1)(@types/react@18.3.12)(react@18.3.1)(typescript@5.8.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)) '@walletconnect/ethereum-provider': 2.21.1(@types/react@18.3.12)(bufferutil@4.0.9)(encoding@0.1.13)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) cbw-sdk: '@coinbase/wallet-sdk@3.9.3' - porto: 0.2.19(@tanstack/react-query@5.67.1(react@18.3.1))(@types/react@18.3.12)(@wagmi/core@2.21.2(@tanstack/query-core@5.67.1)(@types/react@18.3.12)(react@18.3.1)(typescript@5.8.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)))(react@18.3.1)(typescript@5.8.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))(wagmi@2.17.5(@tanstack/query-core@5.67.1)(@tanstack/react-query@5.67.1(react@18.3.1))(@types/react@18.3.12)(bufferutil@4.0.9)(encoding@0.1.13)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(viem@2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))(zod@3.24.2)) - viem: 2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) + porto: 0.2.19(@tanstack/react-query@5.67.1(react@18.3.1))(@types/react@18.3.12)(@wagmi/core@2.21.2(@tanstack/query-core@5.67.1)(@types/react@18.3.12)(react@18.3.1)(typescript@5.8.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)))(react@18.3.1)(typescript@5.8.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))(wagmi@2.17.5(@tanstack/query-core@5.67.1)(@tanstack/react-query@5.67.1(react@18.3.1))(@types/react@18.3.12)(bufferutil@4.0.9)(encoding@0.1.13)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(viem@2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))(zod@3.24.2)) + viem: 2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) optionalDependencies: typescript: 5.8.2 transitivePeerDependencies: @@ -19440,11 +19459,11 @@ snapshots: - wagmi - zod - '@wagmi/core@2.21.2(@tanstack/query-core@5.67.1)(@types/react@18.3.12)(react@18.3.1)(typescript@5.8.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))': + '@wagmi/core@2.21.2(@tanstack/query-core@5.67.1)(@types/react@18.3.12)(react@18.3.1)(typescript@5.8.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))': dependencies: eventemitter3: 5.0.1 mipd: 0.0.7(typescript@5.8.2) - viem: 2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) + viem: 2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) zustand: 5.0.0(@types/react@18.3.12)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) optionalDependencies: '@tanstack/query-core': 5.67.1 @@ -20092,11 +20111,6 @@ snapshots: typescript: 5.8.2 zod: 3.24.2 - abitype@1.1.0(typescript@5.8.2)(zod@3.22.4): - optionalDependencies: - typescript: 5.8.2 - zod: 3.22.4 - abitype@1.1.0(typescript@5.8.2)(zod@3.24.2): optionalDependencies: typescript: 5.8.2 @@ -20107,6 +20121,16 @@ snapshots: typescript: 5.8.2 zod: 4.1.11 + abitype@1.2.3(typescript@5.8.2)(zod@3.22.4): + optionalDependencies: + typescript: 5.8.2 + zod: 3.22.4 + + abitype@1.2.3(typescript@5.8.2)(zod@3.24.2): + optionalDependencies: + typescript: 5.8.2 + zod: 3.24.2 + abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 @@ -25727,53 +25751,53 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 - ox@0.6.7(typescript@5.8.2)(zod@3.24.2): + ox@0.10.5(typescript@5.8.2)(zod@3.22.4): dependencies: '@adraffy/ens-normalize': 1.11.0 + '@noble/ciphers': 1.3.0 '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.1.0(typescript@5.8.2)(zod@3.24.2) + abitype: 1.2.3(typescript@5.8.2)(zod@3.22.4) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.8.2 transitivePeerDependencies: - zod - ox@0.6.9(typescript@5.8.2)(zod@3.24.2): + ox@0.10.5(typescript@5.8.2)(zod@3.24.2): dependencies: '@adraffy/ens-normalize': 1.11.0 + '@noble/ciphers': 1.3.0 '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.1.0(typescript@5.8.2)(zod@3.24.2) + abitype: 1.2.3(typescript@5.8.2)(zod@3.24.2) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.8.2 transitivePeerDependencies: - zod - ox@0.9.6(typescript@5.8.2)(zod@3.22.4): + ox@0.6.7(typescript@5.8.2)(zod@3.24.2): dependencies: '@adraffy/ens-normalize': 1.11.0 - '@noble/ciphers': 1.3.0 '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.1.0(typescript@5.8.2)(zod@3.22.4) + abitype: 1.1.0(typescript@5.8.2)(zod@3.24.2) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.8.2 transitivePeerDependencies: - zod - ox@0.9.6(typescript@5.8.2)(zod@3.24.2): + ox@0.6.9(typescript@5.8.2)(zod@3.24.2): dependencies: '@adraffy/ens-normalize': 1.11.0 - '@noble/ciphers': 1.3.0 '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 @@ -26114,21 +26138,21 @@ snapshots: transitivePeerDependencies: - supports-color - porto@0.2.19(@tanstack/react-query@5.67.1(react@18.3.1))(@types/react@18.3.12)(@wagmi/core@2.21.2(@tanstack/query-core@5.67.1)(@types/react@18.3.12)(react@18.3.1)(typescript@5.8.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)))(react@18.3.1)(typescript@5.8.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))(wagmi@2.17.5(@tanstack/query-core@5.67.1)(@tanstack/react-query@5.67.1(react@18.3.1))(@types/react@18.3.12)(bufferutil@4.0.9)(encoding@0.1.13)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(viem@2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))(zod@3.24.2)): + porto@0.2.19(@tanstack/react-query@5.67.1(react@18.3.1))(@types/react@18.3.12)(@wagmi/core@2.21.2(@tanstack/query-core@5.67.1)(@types/react@18.3.12)(react@18.3.1)(typescript@5.8.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)))(react@18.3.1)(typescript@5.8.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))(wagmi@2.17.5(@tanstack/query-core@5.67.1)(@tanstack/react-query@5.67.1(react@18.3.1))(@types/react@18.3.12)(bufferutil@4.0.9)(encoding@0.1.13)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(viem@2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))(zod@3.24.2)): dependencies: - '@wagmi/core': 2.21.2(@tanstack/query-core@5.67.1)(@types/react@18.3.12)(react@18.3.1)(typescript@5.8.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)) + '@wagmi/core': 2.21.2(@tanstack/query-core@5.67.1)(@types/react@18.3.12)(react@18.3.1)(typescript@5.8.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)) hono: 4.9.9 idb-keyval: 6.2.1 mipd: 0.0.7(typescript@5.8.2) ox: 0.9.6(typescript@5.8.2)(zod@4.1.11) - viem: 2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) + viem: 2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) zod: 4.1.11 zustand: 5.0.8(@types/react@18.3.12)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) optionalDependencies: '@tanstack/react-query': 5.67.1(react@18.3.1) react: 18.3.1 typescript: 5.8.2 - wagmi: 2.17.5(@tanstack/query-core@5.67.1)(@tanstack/react-query@5.67.1(react@18.3.1))(@types/react@18.3.12)(bufferutil@4.0.9)(encoding@0.1.13)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(viem@2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))(zod@3.24.2) + wagmi: 2.17.5(@tanstack/query-core@5.67.1)(@tanstack/react-query@5.67.1(react@18.3.1))(@types/react@18.3.12)(bufferutil@4.0.9)(encoding@0.1.13)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(viem@2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))(zod@3.24.2) transitivePeerDependencies: - '@types/react' - immer @@ -28706,15 +28730,15 @@ snapshots: - utf-8-validate - zod - viem@2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.22.4): + viem@2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.22.4): dependencies: '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.1.0(typescript@5.8.2)(zod@3.22.4) + abitype: 1.2.3(typescript@5.8.2)(zod@3.22.4) isows: 1.0.7(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - ox: 0.9.6(typescript@5.8.2)(zod@3.22.4) + ox: 0.10.5(typescript@5.8.2)(zod@3.22.4) ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) optionalDependencies: typescript: 5.8.2 @@ -28723,15 +28747,15 @@ snapshots: - utf-8-validate - zod - viem@2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2): + viem@2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2): dependencies: '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.1.0(typescript@5.8.2)(zod@3.24.2) + abitype: 1.2.3(typescript@5.8.2)(zod@3.24.2) isows: 1.0.7(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - ox: 0.9.6(typescript@5.8.2)(zod@3.24.2) + ox: 0.10.5(typescript@5.8.2)(zod@3.24.2) ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) optionalDependencies: typescript: 5.8.2 @@ -28832,14 +28856,14 @@ snapshots: xml-name-validator: 5.0.0 optional: true - wagmi@2.17.5(@tanstack/query-core@5.67.1)(@tanstack/react-query@5.67.1(react@18.3.1))(@types/react@18.3.12)(bufferutil@4.0.9)(encoding@0.1.13)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(viem@2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))(zod@3.24.2): + wagmi@2.17.5(@tanstack/query-core@5.67.1)(@tanstack/react-query@5.67.1(react@18.3.1))(@types/react@18.3.12)(bufferutil@4.0.9)(encoding@0.1.13)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(viem@2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))(zod@3.24.2): dependencies: '@tanstack/react-query': 5.67.1(react@18.3.1) - '@wagmi/connectors': 5.11.2(@tanstack/react-query@5.67.1(react@18.3.1))(@types/react@18.3.12)(@wagmi/core@2.21.2(@tanstack/query-core@5.67.1)(@types/react@18.3.12)(react@18.3.1)(typescript@5.8.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)))(bufferutil@4.0.9)(encoding@0.1.13)(react@18.3.1)(typescript@5.8.2)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(viem@2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))(wagmi@2.17.5(@tanstack/query-core@5.67.1)(@tanstack/react-query@5.67.1(react@18.3.1))(@types/react@18.3.12)(bufferutil@4.0.9)(encoding@0.1.13)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(viem@2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))(zod@3.24.2))(zod@3.24.2) - '@wagmi/core': 2.21.2(@tanstack/query-core@5.67.1)(@types/react@18.3.12)(react@18.3.1)(typescript@5.8.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)) + '@wagmi/connectors': 5.11.2(@tanstack/react-query@5.67.1(react@18.3.1))(@types/react@18.3.12)(@wagmi/core@2.21.2(@tanstack/query-core@5.67.1)(@types/react@18.3.12)(react@18.3.1)(typescript@5.8.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)))(bufferutil@4.0.9)(encoding@0.1.13)(react@18.3.1)(typescript@5.8.2)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(viem@2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))(wagmi@2.17.5(@tanstack/query-core@5.67.1)(@tanstack/react-query@5.67.1(react@18.3.1))(@types/react@18.3.12)(bufferutil@4.0.9)(encoding@0.1.13)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(viem@2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))(zod@3.24.2))(zod@3.24.2) + '@wagmi/core': 2.21.2(@tanstack/query-core@5.67.1)(@types/react@18.3.12)(react@18.3.1)(typescript@5.8.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)) react: 18.3.1 use-sync-external-store: 1.4.0(react@18.3.1) - viem: 2.37.9(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) + viem: 2.43.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) optionalDependencies: typescript: 5.8.2 transitivePeerDependencies: From 0b6fe500d64b7362c42052af4ed39ab6227c53b6 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Fri, 19 Dec 2025 12:41:01 +0300 Subject: [PATCH 046/141] feat: evmos beta proposals issue --- .../src/app/[locale]/governance/page.tsx | 2 +- libs/data-access/cosmos/package.json | 2 +- .../cosmos/src/lib/data-access-cosmos.ts | 9 +- .../src/lib/components/proposal-list-card.tsx | 2 +- .../src/lib/proposal-details-page.tsx | 11 +- .../governance/src/lib/proposal-list-page.tsx | 12 +- .../lib/components/proposal-list-block.tsx | 6 +- package.json | 2 +- pnpm-lock.yaml | 285 ++++++++++++++---- 9 files changed, 248 insertions(+), 83 deletions(-) diff --git a/apps/shell/src/app/[locale]/governance/page.tsx b/apps/shell/src/app/[locale]/governance/page.tsx index a9421faf5..68fc54f6b 100644 --- a/apps/shell/src/app/[locale]/governance/page.tsx +++ b/apps/shell/src/app/[locale]/governance/page.tsx @@ -36,7 +36,7 @@ export default async function ProposalList() { return proposal.status === ProposalStatus.Voting; }) .map((proposal) => { - return proposal.proposal_id; + return proposal.proposal_id || proposal.id; }); for (const proposalId of ongoingProposals) { diff --git a/libs/data-access/cosmos/package.json b/libs/data-access/cosmos/package.json index e4cacb3df..350261efa 100644 --- a/libs/data-access/cosmos/package.json +++ b/libs/data-access/cosmos/package.json @@ -4,7 +4,7 @@ "dependencies": { "@swc/helpers": "~0.5.11", "@evmos/proto": "0.1.27", - "@evmos/provider": "0.2.8", + "@evmos/provider": "0.3.1", "@evmos/transactions": "0.2.13", "@haqq/data-access-falconer": "0.0.1" }, diff --git a/libs/data-access/cosmos/src/lib/data-access-cosmos.ts b/libs/data-access/cosmos/src/lib/data-access-cosmos.ts index 868f8f575..3e62e924b 100644 --- a/libs/data-access/cosmos/src/lib/data-access-cosmos.ts +++ b/libs/data-access/cosmos/src/lib/data-access-cosmos.ts @@ -304,7 +304,8 @@ export function createCosmosService(cosmosRestEndpoint: string): CosmosService { async function getProposals() { const proposalsUrl = new URL( - `${cosmosRestEndpoint}${generateEndpointProposals()}`, + // `${cosmosRestEndpoint}${generateEndpointProposals()}`, + `${cosmosRestEndpoint}/cosmos/gov/v1/proposals`, ); proposalsUrl.searchParams.append('pagination.reverse', 'true'); @@ -321,7 +322,8 @@ export function createCosmosService(cosmosRestEndpoint: string): CosmosService { } async function getProposalDetails(id: string) { - const getProposalDetailsUrl = `${cosmosRestEndpoint}${generateEndpointProposal(id)}`; + // const getProposalDetailsUrl = `${cosmosRestEndpoint}${generateEndpointProposal(id)}`; + const getProposalDetailsUrl = `${cosmosRestEndpoint}/cosmos/gov/v1/proposals/${id}`; const response = await fetch(getProposalDetailsUrl); @@ -559,7 +561,8 @@ export function createCosmosService(cosmosRestEndpoint: string): CosmosService { async function getProposalTally(id: string) { const getProposalTallyUrl = new URL( - `${cosmosRestEndpoint}${generateEndpointProposalTally(id)}`, + // `${cosmosRestEndpoint}${generateEndpointProposalTally(id)}`, + `${cosmosRestEndpoint}/cosmos/gov/v1/proposals/${id}/tally`, ); const response = await fetch(getProposalTallyUrl); diff --git a/libs/governance/src/lib/components/proposal-list-card.tsx b/libs/governance/src/lib/components/proposal-list-card.tsx index 418e232a1..e3c98f954 100644 --- a/libs/governance/src/lib/components/proposal-list-card.tsx +++ b/libs/governance/src/lib/components/proposal-list-card.tsx @@ -43,7 +43,7 @@ export function ProposalListCard({ return ( { @@ -371,7 +371,7 @@ export function ProposalDetailsComponent({
- #{proposalDetails.proposal_id} + #{proposalDetails.proposal_id || proposalDetails.id}
@@ -754,7 +754,7 @@ export function ProposalDetailsComponent({
diff --git a/libs/governance/src/lib/proposal-list-page.tsx b/libs/governance/src/lib/proposal-list-page.tsx index 62e1a1800..b557e4a58 100644 --- a/libs/governance/src/lib/proposal-list-page.tsx +++ b/libs/governance/src/lib/proposal-list-page.tsx @@ -37,7 +37,7 @@ export function ProposalListPage() { return proposal.status === ProposalStatus.Voting; }) .map((proposal) => { - return proposal.proposal_id; + return proposal.proposal_id || proposal.id; }); }, [proposals]); @@ -72,7 +72,7 @@ export function ProposalListPage() { if (updatetProposalData.status === ProposalStatus.Voting) { const ongoingTally = ongoingProposalTallysResultMap.get( - updatetProposalData.proposal_id, + updatetProposalData.proposal_id || updatetProposalData.id, ); if (ongoingTally) { @@ -81,7 +81,7 @@ export function ProposalListPage() { } const userVote = ongoingProposalVotesResultMap.get( - updatetProposalData.proposal_id, + updatetProposalData.proposal_id || updatetProposalData.id, ); return { @@ -96,6 +96,8 @@ export function ProposalListPage() { proposals, ]); + console.log('Proposals proposalsToRender:', proposalsToRender); + return (
{!isHaqqWallet && ( @@ -126,8 +128,8 @@ export function ProposalListPage() { {proposalsToRender.map(({ userVote, proposal, voteResults }) => { return ( { - return proposal.proposal_id; + return proposal.proposal_id || proposal.id; }); }, [proposals]); @@ -58,7 +58,7 @@ export function ProposalListBlock() { if (proposal.status === ProposalStatus.Voting) { const ongoingTally = ongoingProposalTallysResultMap.get( - proposal.proposal_id, + proposal.proposal_id || proposal.id, ); if (ongoingTally) { @@ -99,7 +99,7 @@ export function ProposalListBlock() { {proposalsToRender.map((proposal) => { return ( diff --git a/package.json b/package.json index 58a23e8ac..7559798c2 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "@ethersproject/signing-key": "5.8.0", "@evmos/eip712": "0.2.11", "@evmos/proto": "0.1.27", - "@evmos/provider": "0.2.8", + "@evmos/provider": "0.3.1", "@evmos/transactions": "0.2.13", "@floating-ui/react": "0.26.26", "@headlessui/react": "2.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ab0cd4ab1..f1d1a6b19 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,8 +30,8 @@ importers: specifier: 0.1.27 version: 0.1.27 '@evmos/provider': - specifier: 0.2.8 - version: 0.2.8 + specifier: 0.3.1 + version: 0.3.1 '@evmos/transactions': specifier: 0.2.13 version: 0.2.13 @@ -467,8 +467,8 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} - '@asamuzakjp/css-color@2.8.3': - resolution: {integrity: sha512-GIc76d9UI1hCvOATjZPyHFmE5qhRccp3/zGfMPapK3jBi+yocEzp6BBB0UnfRYP9NP4FANqUZYb0hnfs3TM3hw==} + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} '@auth0/auth0-react@2.3.0': resolution: {integrity: sha512-YYTc/DWWigKC9fURufR/79h3+3DAnIzbfEzJLZ8Z4Q0BXE0azru3pKUbU+vYzS4lMAJkclwLuAbUnLjK81vCpA==} @@ -1330,6 +1330,10 @@ packages: resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} engines: {node: '>=18'} + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + '@csstools/css-calc@2.1.2': resolution: {integrity: sha512-TklMyb3uBB28b5uQdxjReG4L80NxAqgrECqLZFQbyLekwwlcDDS8r3f07DKqeo8C4926Br0gf/ZDe17Zv4wIuw==} engines: {node: '>=18'} @@ -1337,6 +1341,13 @@ packages: '@csstools/css-parser-algorithms': ^3.0.4 '@csstools/css-tokenizer': ^3.0.3 + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-color-parser@3.0.8': resolution: {integrity: sha512-pdwotQjCCnRPuNi06jFuP68cykU1f3ZWExLe/8MQ1LOs8Xq+fTkYgd+2V8mWUWMrOn9iS2HftPVaMZDaXzGbhQ==} engines: {node: '>=18'} @@ -1344,16 +1355,33 @@ packages: '@csstools/css-parser-algorithms': ^3.0.4 '@csstools/css-tokenizer': ^3.0.3 + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-parser-algorithms@3.0.4': resolution: {integrity: sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==} engines: {node: '>=18'} peerDependencies: '@csstools/css-tokenizer': ^3.0.3 + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-tokenizer@3.0.3': resolution: {integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==} engines: {node: '>=18'} + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + '@csstools/media-query-list-parser@4.0.2': resolution: {integrity: sha512-EUos465uvVvMJehckATTlNqGj4UJWkTmdWuDMjqvSUkjGpmOyFZBVwb4knxCm/k2GMTXY+c/5RkdndzFYWeX5A==} engines: {node: '>=18'} @@ -2044,8 +2072,8 @@ packages: '@evmos/proto@0.1.27': resolution: {integrity: sha512-lBOZhQFsIUz3on/4H+Rj1aDj2OWaZnz7OJjUFklkPZQ3tEtAATBOQiVCjkPvIX3jh/H5DACahmi8BmhcNF+7UA==} - '@evmos/provider@0.2.8': - resolution: {integrity: sha512-LOml6RtabQafhNn38IaREXB2oClHTUBYF0VttQRB3qxaOEqBy9WbH0Dz7fvqqGFbcanMdQKhBQ4NfNMeslWQgQ==} + '@evmos/provider@0.3.1': + resolution: {integrity: sha512-Km2BYQvD1PWZHUMwyqjBiM3loSTzr/t+03FAZqrTF4ffqhJSIOCW6SW9X4bvuAICMLbbeM4l1C6cNeLfa1KTlQ==} '@evmos/transactions@0.2.13': resolution: {integrity: sha512-O+SoWUXkg3XoP80PB01BSKBh+HMGZuWbQ2M07yscGfSxgmSsKrTPliOmai3yM8CnDnzUmPSNZk/oWaKaCDl8kw==} @@ -5250,6 +5278,10 @@ packages: resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} engines: {node: '>= 14'} + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + aggregate-error@3.1.0: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} @@ -5661,6 +5693,9 @@ packages: bn.js@5.2.1: resolution: {integrity: sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==} + bn.js@5.2.2: + resolution: {integrity: sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==} + body-parser@1.20.3: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -6415,8 +6450,8 @@ packages: resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} engines: {node: '>=8'} - cssstyle@4.2.1: - resolution: {integrity: sha512-9+vem03dMXG7gDmZ62uqmRiMRNtinIZ9ZyuF6BdxzfOD+FdN5hretzynkn0ReS2DO2GSw76RWHs0UmJPI2zUjw==} + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} engines: {node: '>=18'} csstype@3.1.3: @@ -6527,6 +6562,15 @@ packages: supports-color: optional: true + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decamelize@1.2.0: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} @@ -6537,6 +6581,9 @@ packages: decimal.js@10.5.0: resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==} + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + decode-uri-component@0.2.2: resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} engines: {node: '>=0.10'} @@ -6870,6 +6917,10 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -7489,8 +7540,8 @@ packages: picomatch: optional: true - fetch-cookie@3.1.0: - resolution: {integrity: sha512-s/XhhreJpqH0ftkGVcQt8JE9bqk+zRn4jF5mPJXWZeQMCI5odV9K+wEWYbnzFPHgQZlvPSMjS4n4yawWE8RINw==} + fetch-cookie@3.2.0: + resolution: {integrity: sha512-n61pQIxP25C6DRhcJxn7BDzgHP/+S56Urowb5WFxtcRMpU6drqXD90xjyAsVQYsNSNNVbaCcYY1DuHsdkZLuiA==} fflate@0.4.8: resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} @@ -7653,8 +7704,8 @@ packages: resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} engines: {node: '>= 6'} - form-data@4.0.2: - resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} forwarded-parse@2.1.2: @@ -9140,8 +9191,8 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - lossless-json@4.0.2: - resolution: {integrity: sha512-+z0EaLi2UcWi8MZRxA5iTb6m4Ys4E80uftGY+yG5KNFJb5EceQXOhdW/pWJZ8m97s26u7yZZAYMcKWNztSZssA==} + lossless-json@4.3.0: + resolution: {integrity: sha512-ToxOC+SsduRmdSuoLZLYAr5zy1Qu7l5XhmPWM3zefCZ5IcrzW/h108qbJUKfOlDlhvhjUK84+8PSVX0kxnit0g==} loupe@3.1.2: resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} @@ -9638,8 +9689,8 @@ packages: nwsapi@2.2.12: resolution: {integrity: sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==} - nwsapi@2.2.18: - resolution: {integrity: sha512-p1TRH/edngVEHVbwqWnxUViEmq5znDvyB+Sik5cmuLpGOIfDf/39zLiq3swPF8Vakqn+gvNiOQAZu8djYlQILA==} + nwsapi@2.2.23: + resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} nx@20.4.6: resolution: {integrity: sha512-gXRw3urAq4glK6B1+jxHjzXRyuNrFFI7L3ggNg34UmQ46AyT7a6FgjZp2OZ779urwnoQSTvxNfBuD4+RrB31MQ==} @@ -9941,6 +9992,9 @@ packages: parse5@7.2.1: resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -10630,6 +10684,9 @@ packages: preact@10.26.4: resolution: {integrity: sha512-KJhO7LBFTjP71d83trW+Ilnjbo+ySsaAgCfXOXUlmGzJ4ygYPWmysm77yg4emwfmoz3b22yvH5IsVFHbhUaH5w==} + preact@10.28.0: + resolution: {integrity: sha512-rytDAoiXr3+t6OIP3WGlDd0ouCUG1iCWzkcY3++Nreuoi17y6T5i/zRhe6uYfoVcxq6YU+sBtJouuRDsq8vvqA==} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -11324,6 +11381,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + send@0.19.0: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} @@ -11342,8 +11404,8 @@ packages: set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} - set-cookie-parser@2.7.1: - resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} @@ -11373,6 +11435,11 @@ packages: resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==} hasBin: true + sha.js@2.4.12: + resolution: {integrity: sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==} + engines: {node: '>= 0.10'} + hasBin: true + sha3@2.1.4: resolution: {integrity: sha512-S8cNxbyb0UGUM2VhRD4Poe5N58gJnJsLJ5vC7FYWGUmGhcsj4++WaIOBFVDxlG0W3To6xBuiRh+i0Qp2oNCOtg==} @@ -11986,15 +12053,22 @@ packages: tldts-core@6.1.72: resolution: {integrity: sha512-FW3H9aCaGTJ8l8RVCR3EX8GxsxDbQXuwetwwgXA2chYdsX+NY1ytCBl61narjjehWmCw92tc1AxlcY3668CU8g==} - tldts-core@6.1.83: - resolution: {integrity: sha512-I2wb9OJc6rXyh9d4aInhSNWChNI+ra6qDnFEGEwe9OoA68lE4Temw29bOkf1Uvwt8VZS079t1BFZdXVBmmB4dw==} + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts-core@7.0.19: + resolution: {integrity: sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==} tldts@6.1.72: resolution: {integrity: sha512-QNtgIqSUb9o2CoUjX9T5TwaIvUUJFU1+12PJkgt42DFV2yf9J6549yTF2uGloQsJ/JOC8X+gIB81ind97hRiIQ==} hasBin: true - tldts@6.1.83: - resolution: {integrity: sha512-FHxxNJJ0WNsEBPHyC1oesQb3rRoxpuho/z2g3zIIAhw1WHJeQsUzK1jYK8TI1/iClaa4fS3Z2TCA9mtxXsENSg==} + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + + tldts@7.0.19: + resolution: {integrity: sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==} hasBin: true tmp@0.2.3: @@ -12004,6 +12078,10 @@ packages: tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + to-buffer@1.2.2: + resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==} + engines: {node: '>= 0.4'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -12036,6 +12114,10 @@ packages: resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} engines: {node: '>=16'} + tough-cookie@6.0.0: + resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} + engines: {node: '>=16'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -12043,8 +12125,8 @@ packages: resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} engines: {node: '>=12'} - tr46@5.0.0: - resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} tree-dump@1.0.2: @@ -12797,8 +12879,8 @@ packages: resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} engines: {node: '>=12'} - whatwg-url@14.1.1: - resolution: {integrity: sha512-mDGf9diDad/giZ/Sm9Xi2YcyzaFpbdLpJPr+E9fSkyQ7KpQD4SdFcugkRQYzhmfI4KeV4Qpnn2sKPdo+kmsgRQ==} + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} engines: {node: '>=18'} whatwg-url@5.0.0: @@ -13106,12 +13188,12 @@ snapshots: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 - '@asamuzakjp/css-color@2.8.3': + '@asamuzakjp/css-color@3.2.0': dependencies: - '@csstools/css-calc': 2.1.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-color-parser': 3.0.8(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 lru-cache: 10.4.3 optional: true @@ -14233,15 +14315,15 @@ snapshots: '@coinbase/wallet-sdk@3.9.3': dependencies: - bn.js: 5.2.1 + bn.js: 5.2.2 buffer: 6.0.3 clsx: 1.2.1 eth-block-tracker: 7.1.0 eth-json-rpc-filters: 6.0.1 eventemitter3: 5.0.1 keccak: 3.0.4 - preact: 10.26.4 - sha.js: 2.4.11 + preact: 10.28.0 + sha.js: 2.4.12 transitivePeerDependencies: - supports-color @@ -14428,11 +14510,20 @@ snapshots: '@csstools/color-helpers@5.0.2': {} + '@csstools/color-helpers@5.1.0': + optional: true + '@csstools/css-calc@2.1.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': dependencies: '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) '@csstools/css-tokenizer': 3.0.3 + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + optional: true + '@csstools/css-color-parser@3.0.8(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': dependencies: '@csstools/color-helpers': 5.0.2 @@ -14440,12 +14531,28 @@ snapshots: '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) '@csstools/css-tokenizer': 3.0.3 + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + optional: true + '@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3)': dependencies: '@csstools/css-tokenizer': 3.0.3 + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + optional: true + '@csstools/css-tokenizer@3.0.3': {} + '@csstools/css-tokenizer@3.0.4': + optional: true + '@csstools/media-query-list-parser@4.0.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': dependencies: '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) @@ -15120,7 +15227,7 @@ snapshots: sha3: 2.1.4 shx: 0.3.4 - '@evmos/provider@0.2.8': + '@evmos/provider@0.3.1': dependencies: link-module-alias: 1.2.0 shx: 0.3.4 @@ -15733,8 +15840,8 @@ snapshots: dependencies: '@ethereumjs/tx': 4.2.0 '@types/debug': 4.1.12 - debug: 4.4.0(supports-color@8.1.1) - semver: 7.7.1 + debug: 4.4.3 + semver: 7.7.3 superstruct: 1.0.4 transitivePeerDependencies: - supports-color @@ -20184,6 +20291,9 @@ snapshots: agent-base@7.1.3: {} + agent-base@7.1.4: + optional: true + aggregate-error@3.1.0: dependencies: clean-stack: 2.2.0 @@ -20688,6 +20798,8 @@ snapshots: bn.js@5.2.1: {} + bn.js@5.2.2: {} + body-parser@1.20.3: dependencies: bytes: 3.1.2 @@ -21520,9 +21632,9 @@ snapshots: dependencies: cssom: 0.3.8 - cssstyle@4.2.1: + cssstyle@4.6.0: dependencies: - '@asamuzakjp/css-color': 2.8.3 + '@asamuzakjp/css-color': 3.2.0 rrweb-cssom: 0.8.0 optional: true @@ -21591,7 +21703,7 @@ snapshots: data-urls@5.0.0: dependencies: whatwg-mimetype: 4.0.0 - whatwg-url: 14.1.1 + whatwg-url: 14.2.0 optional: true data-view-buffer@1.0.1: @@ -21664,12 +21776,19 @@ snapshots: optionalDependencies: supports-color: 8.1.1 + debug@4.4.3: + dependencies: + ms: 2.1.3 + decamelize@1.2.0: {} decimal.js@10.4.3: {} decimal.js@10.5.0: {} + decimal.js@10.6.0: + optional: true + decode-uri-component@0.2.2: {} decompress-response@6.0.0: @@ -21995,6 +22114,9 @@ snapshots: entities@4.5.0: {} + entities@6.0.1: + optional: true + env-paths@2.2.1: {} envinfo@7.14.0: {} @@ -22972,10 +23094,10 @@ snapshots: optionalDependencies: picomatch: 4.0.2 - fetch-cookie@3.1.0: + fetch-cookie@3.2.0: dependencies: - set-cookie-parser: 2.7.1 - tough-cookie: 5.1.2 + set-cookie-parser: 2.7.2 + tough-cookie: 6.0.0 fflate@0.4.8: {} @@ -23164,11 +23286,12 @@ snapshots: combined-stream: 1.0.8 mime-types: 2.1.35 - form-data@4.0.2: + form-data@4.0.5: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 es-set-tostringtag: 2.1.0 + hasown: 2.0.2 mime-types: 2.1.35 optional: true @@ -23663,8 +23786,8 @@ snapshots: http-proxy-agent@7.0.2: dependencies: - agent-base: 7.1.3 - debug: 4.4.0(supports-color@8.1.1) + agent-base: 7.1.4 + debug: 4.4.3 transitivePeerDependencies: - supports-color optional: true @@ -24644,16 +24767,16 @@ snapshots: jsdom@25.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10): dependencies: - cssstyle: 4.2.1 + cssstyle: 4.6.0 data-urls: 5.0.0 - decimal.js: 10.5.0 - form-data: 4.0.2 + decimal.js: 10.6.0 + form-data: 4.0.5 html-encoding-sniffer: 4.0.0 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.18 - parse5: 7.2.1 + nwsapi: 2.2.23 + parse5: 7.3.0 rrweb-cssom: 0.7.1 saxes: 6.0.0 symbol-tree: 3.2.4 @@ -24662,7 +24785,7 @@ snapshots: webidl-conversions: 7.0.0 whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 - whatwg-url: 14.1.1 + whatwg-url: 14.2.0 ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) xml-name-validator: 5.0.0 transitivePeerDependencies: @@ -25043,7 +25166,7 @@ snapshots: dependencies: js-tokens: 4.0.0 - lossless-json@4.0.2: {} + lossless-json@4.3.0: {} loupe@3.1.2: {} @@ -25513,7 +25636,7 @@ snapshots: nwsapi@2.2.12: {} - nwsapi@2.2.18: + nwsapi@2.2.23: optional: true nx@20.4.6(@swc-node/register@1.10.9(@swc/core@1.11.7(@swc/helpers@0.5.13))(@swc/types@0.1.19)(typescript@5.8.2))(@swc/core@1.11.7(@swc/helpers@0.5.13)): @@ -25931,6 +26054,11 @@ snapshots: dependencies: entities: 4.5.0 + parse5@7.3.0: + dependencies: + entities: 6.0.1 + optional: true + parseurl@1.3.3: {} pascal-case@3.1.2: @@ -26649,6 +26777,8 @@ snapshots: preact@10.26.4: {} + preact@10.28.0: {} + prelude-ls@1.2.1: {} prettier-linter-helpers@1.0.0: @@ -27339,6 +27469,8 @@ snapshots: semver@7.7.1: {} + semver@7.7.3: {} + send@0.19.0: dependencies: debug: 2.6.9 @@ -27384,7 +27516,7 @@ snapshots: set-blocking@2.0.0: {} - set-cookie-parser@2.7.1: {} + set-cookie-parser@2.7.2: {} set-function-length@1.2.2: dependencies: @@ -27421,6 +27553,12 @@ snapshots: inherits: 2.0.4 safe-buffer: 5.2.1 + sha.js@2.4.12: + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + to-buffer: 1.2.2 + sha3@2.1.4: dependencies: buffer: 6.0.3 @@ -27699,10 +27837,10 @@ snapshots: '@scure/base': 1.1.9 '@scure/starknet': 1.0.0 abi-wan-kanabi: 2.2.4 - fetch-cookie: 3.1.0 + fetch-cookie: 3.2.0 get-starknet-core: 4.0.0 isomorphic-fetch: 3.0.0(encoding@0.1.13) - lossless-json: 4.0.2 + lossless-json: 4.3.0 pako: 2.1.0 starknet-types-07: '@starknet-io/types-js@0.7.10' ts-mixer: 6.0.4 @@ -28155,20 +28293,34 @@ snapshots: tldts-core@6.1.72: {} - tldts-core@6.1.83: {} + tldts-core@6.1.86: + optional: true + + tldts-core@7.0.19: {} tldts@6.1.72: dependencies: tldts-core: 6.1.72 - tldts@6.1.83: + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + optional: true + + tldts@7.0.19: dependencies: - tldts-core: 6.1.83 + tldts-core: 7.0.19 tmp@0.2.3: {} tmpl@1.0.5: {} + to-buffer@1.2.2: + dependencies: + isarray: 2.0.5 + safe-buffer: 5.2.1 + typed-array-buffer: 1.0.3 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -28197,7 +28349,12 @@ snapshots: tough-cookie@5.1.2: dependencies: - tldts: 6.1.83 + tldts: 6.1.86 + optional: true + + tough-cookie@6.0.0: + dependencies: + tldts: 7.0.19 tr46@0.0.3: {} @@ -28205,7 +28362,7 @@ snapshots: dependencies: punycode: 2.3.1 - tr46@5.0.0: + tr46@5.1.1: dependencies: punycode: 2.3.1 optional: true @@ -29068,9 +29225,9 @@ snapshots: tr46: 3.0.0 webidl-conversions: 7.0.0 - whatwg-url@14.1.1: + whatwg-url@14.2.0: dependencies: - tr46: 5.0.0 + tr46: 5.1.1 webidl-conversions: 7.0.0 optional: true From 135ac3c289cd4db9f596ce3b3261adddb85ac130 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Fri, 19 Dec 2025 12:45:01 +0300 Subject: [PATCH 047/141] chore: self review --- libs/bridge/src/lib/bridge-page.tsx | 52 +++++++++++++++-------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/libs/bridge/src/lib/bridge-page.tsx b/libs/bridge/src/lib/bridge-page.tsx index a7ef21821..803411787 100644 --- a/libs/bridge/src/lib/bridge-page.tsx +++ b/libs/bridge/src/lib/bridge-page.tsx @@ -250,31 +250,33 @@ export function BridgePage() { isFinalizing={isFinalizing} /> - + {!isChainMismatch && ( + + )} From c6b9e13e32787b2254823812b24bc2c189b9b4b7 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Fri, 19 Dec 2025 12:52:23 +0300 Subject: [PATCH 048/141] fix: revert evmos version --- libs/data-access/cosmos/package.json | 2 +- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/libs/data-access/cosmos/package.json b/libs/data-access/cosmos/package.json index 350261efa..e4cacb3df 100644 --- a/libs/data-access/cosmos/package.json +++ b/libs/data-access/cosmos/package.json @@ -4,7 +4,7 @@ "dependencies": { "@swc/helpers": "~0.5.11", "@evmos/proto": "0.1.27", - "@evmos/provider": "0.3.1", + "@evmos/provider": "0.2.8", "@evmos/transactions": "0.2.13", "@haqq/data-access-falconer": "0.0.1" }, diff --git a/package.json b/package.json index 7559798c2..58a23e8ac 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "@ethersproject/signing-key": "5.8.0", "@evmos/eip712": "0.2.11", "@evmos/proto": "0.1.27", - "@evmos/provider": "0.3.1", + "@evmos/provider": "0.2.8", "@evmos/transactions": "0.2.13", "@floating-ui/react": "0.26.26", "@headlessui/react": "2.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f1d1a6b19..b98c905be 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,8 +30,8 @@ importers: specifier: 0.1.27 version: 0.1.27 '@evmos/provider': - specifier: 0.3.1 - version: 0.3.1 + specifier: 0.2.8 + version: 0.2.8 '@evmos/transactions': specifier: 0.2.13 version: 0.2.13 @@ -2072,8 +2072,8 @@ packages: '@evmos/proto@0.1.27': resolution: {integrity: sha512-lBOZhQFsIUz3on/4H+Rj1aDj2OWaZnz7OJjUFklkPZQ3tEtAATBOQiVCjkPvIX3jh/H5DACahmi8BmhcNF+7UA==} - '@evmos/provider@0.3.1': - resolution: {integrity: sha512-Km2BYQvD1PWZHUMwyqjBiM3loSTzr/t+03FAZqrTF4ffqhJSIOCW6SW9X4bvuAICMLbbeM4l1C6cNeLfa1KTlQ==} + '@evmos/provider@0.2.8': + resolution: {integrity: sha512-LOml6RtabQafhNn38IaREXB2oClHTUBYF0VttQRB3qxaOEqBy9WbH0Dz7fvqqGFbcanMdQKhBQ4NfNMeslWQgQ==} '@evmos/transactions@0.2.13': resolution: {integrity: sha512-O+SoWUXkg3XoP80PB01BSKBh+HMGZuWbQ2M07yscGfSxgmSsKrTPliOmai3yM8CnDnzUmPSNZk/oWaKaCDl8kw==} @@ -15227,7 +15227,7 @@ snapshots: sha3: 2.1.4 shx: 0.3.4 - '@evmos/provider@0.3.1': + '@evmos/provider@0.2.8': dependencies: link-module-alias: 1.2.0 shx: 0.3.4 From 0daae5aa8c337324f92696de49ca62f8da22c4cc Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Fri, 19 Dec 2025 12:56:23 +0300 Subject: [PATCH 049/141] fix: types --- apps/shell/src/app/[locale]/governance/page.tsx | 4 ++-- libs/governance/src/lib/proposal-list-page.tsx | 4 ++-- libs/main/src/lib/components/proposal-list-block.tsx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/shell/src/app/[locale]/governance/page.tsx b/apps/shell/src/app/[locale]/governance/page.tsx index 68fc54f6b..3c43eca7a 100644 --- a/apps/shell/src/app/[locale]/governance/page.tsx +++ b/apps/shell/src/app/[locale]/governance/page.tsx @@ -1,4 +1,4 @@ -import { ProposalStatus } from '@evmos/provider'; +import { Proposal, ProposalStatus } from '@evmos/provider'; import { HydrationBoundary, QueryClient, @@ -35,7 +35,7 @@ export default async function ProposalList() { .filter((proposal) => { return proposal.status === ProposalStatus.Voting; }) - .map((proposal) => { + .map((proposal: Proposal & { id: string }) => { return proposal.proposal_id || proposal.id; }); diff --git a/libs/governance/src/lib/proposal-list-page.tsx b/libs/governance/src/lib/proposal-list-page.tsx index b557e4a58..7b69d1d81 100644 --- a/libs/governance/src/lib/proposal-list-page.tsx +++ b/libs/governance/src/lib/proposal-list-page.tsx @@ -1,6 +1,6 @@ 'use client'; import { useMemo } from 'react'; -import { ProposalStatus } from '@evmos/provider'; +import { Proposal, ProposalStatus } from '@evmos/provider'; import { useTranslate } from '@tolgee/react'; import Link from 'next/link'; import { @@ -36,7 +36,7 @@ export function ProposalListPage() { .filter((proposal) => { return proposal.status === ProposalStatus.Voting; }) - .map((proposal) => { + .map((proposal: Proposal & { id: string }) => { return proposal.proposal_id || proposal.id; }); }, [proposals]); diff --git a/libs/main/src/lib/components/proposal-list-block.tsx b/libs/main/src/lib/components/proposal-list-block.tsx index 934fefec2..ab13ca0b1 100644 --- a/libs/main/src/lib/components/proposal-list-block.tsx +++ b/libs/main/src/lib/components/proposal-list-block.tsx @@ -1,5 +1,5 @@ import { useMemo } from 'react'; -import { ProposalStatus } from '@evmos/provider'; +import { Proposal, ProposalStatus } from '@evmos/provider'; import { useTranslate } from '@tolgee/react'; import Link from 'next/link'; import { ProposalListCard } from '@haqq/shell-governance'; @@ -36,7 +36,7 @@ export function ProposalListBlock() { .filter((proposal) => { return proposal.status === ProposalStatus.Voting; }) - .map((proposal) => { + .map((proposal: Proposal & { id: string }) => { return proposal.proposal_id || proposal.id; }); }, [proposals]); From f6e2d152a945e62dd39e0c84fe94f2ca51d49c25 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Fri, 19 Dec 2025 13:05:29 +0300 Subject: [PATCH 050/141] fix: switch to any from evmos type --- apps/shell/src/app/[locale]/governance/page.tsx | 2 +- libs/governance/src/lib/proposal-list-page.tsx | 2 +- libs/main/src/lib/components/proposal-list-block.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/shell/src/app/[locale]/governance/page.tsx b/apps/shell/src/app/[locale]/governance/page.tsx index 3c43eca7a..ea3499db5 100644 --- a/apps/shell/src/app/[locale]/governance/page.tsx +++ b/apps/shell/src/app/[locale]/governance/page.tsx @@ -35,7 +35,7 @@ export default async function ProposalList() { .filter((proposal) => { return proposal.status === ProposalStatus.Voting; }) - .map((proposal: Proposal & { id: string }) => { + .map((proposal: any) => { return proposal.proposal_id || proposal.id; }); diff --git a/libs/governance/src/lib/proposal-list-page.tsx b/libs/governance/src/lib/proposal-list-page.tsx index 7b69d1d81..1caa8e34f 100644 --- a/libs/governance/src/lib/proposal-list-page.tsx +++ b/libs/governance/src/lib/proposal-list-page.tsx @@ -36,7 +36,7 @@ export function ProposalListPage() { .filter((proposal) => { return proposal.status === ProposalStatus.Voting; }) - .map((proposal: Proposal & { id: string }) => { + .map((proposal: any) => { return proposal.proposal_id || proposal.id; }); }, [proposals]); diff --git a/libs/main/src/lib/components/proposal-list-block.tsx b/libs/main/src/lib/components/proposal-list-block.tsx index ab13ca0b1..fce8ffb93 100644 --- a/libs/main/src/lib/components/proposal-list-block.tsx +++ b/libs/main/src/lib/components/proposal-list-block.tsx @@ -36,7 +36,7 @@ export function ProposalListBlock() { .filter((proposal) => { return proposal.status === ProposalStatus.Voting; }) - .map((proposal: Proposal & { id: string }) => { + .map((proposal: any) => { return proposal.proposal_id || proposal.id; }); }, [proposals]); From 24ac907339a41a0ef3567f38c4205c97d14b18ff Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Fri, 19 Dec 2025 13:07:00 +0300 Subject: [PATCH 051/141] fix: update proposal ID handling to use 'any' type for better compatibility --- .../src/lib/components/proposal-list-card.tsx | 5 ++++- libs/governance/src/lib/proposal-details-page.tsx | 12 ++++++++---- libs/governance/src/lib/proposal-list-page.tsx | 12 +++++++----- libs/main/src/lib/components/proposal-list-block.tsx | 6 +++--- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/libs/governance/src/lib/components/proposal-list-card.tsx b/libs/governance/src/lib/components/proposal-list-card.tsx index e3c98f954..b0b8f354f 100644 --- a/libs/governance/src/lib/components/proposal-list-card.tsx +++ b/libs/governance/src/lib/components/proposal-list-card.tsx @@ -43,7 +43,10 @@ export function ProposalListCard({ return ( { @@ -371,7 +371,9 @@ export function ProposalDetailsComponent({
- #{proposalDetails.proposal_id || proposalDetails.id} + # + {(proposalDetails as any).proposal_id || + (proposalDetails as any).id}
@@ -754,7 +756,8 @@ export function ProposalDetailsComponent({
{ - return proposal.proposal_id || proposal.id; + return (proposal as any).proposal_id || (proposal as any).id; }); }, [proposals]); @@ -72,7 +72,8 @@ export function ProposalListPage() { if (updatetProposalData.status === ProposalStatus.Voting) { const ongoingTally = ongoingProposalTallysResultMap.get( - updatetProposalData.proposal_id || updatetProposalData.id, + (updatetProposalData as any).proposal_id || + (updatetProposalData as any).id, ); if (ongoingTally) { @@ -81,7 +82,8 @@ export function ProposalListPage() { } const userVote = ongoingProposalVotesResultMap.get( - updatetProposalData.proposal_id || updatetProposalData.id, + (updatetProposalData as any).proposal_id || + (updatetProposalData as any).id, ); return { @@ -128,8 +130,8 @@ export function ProposalListPage() { {proposalsToRender.map(({ userVote, proposal, voteResults }) => { return ( { - return proposal.proposal_id || proposal.id; + return (proposal as any).proposal_id || (proposal as any).id; }); }, [proposals]); @@ -58,7 +58,7 @@ export function ProposalListBlock() { if (proposal.status === ProposalStatus.Voting) { const ongoingTally = ongoingProposalTallysResultMap.get( - proposal.proposal_id || proposal.id, + (proposal as any).proposal_id || (proposal as any).id, ); if (ongoingTally) { @@ -99,7 +99,7 @@ export function ProposalListBlock() { {proposalsToRender.map((proposal) => { return ( From 17403e26b3cfac5536b5e0ab10cabd34911e25ca Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Fri, 19 Dec 2025 13:58:02 +0300 Subject: [PATCH 052/141] fix: tally results display --- libs/data-access/cosmos/src/types.ts | 7 ++++- .../src/lib/proposal-details-page.tsx | 1 + .../governance/src/lib/proposal-list-page.tsx | 3 ++- .../lib/components/proposal-list-block.tsx | 3 ++- libs/shared/src/utils/vote-calculations.ts | 27 ++++++++++--------- .../ui-kit/src/lib/proposal-vote-progress.tsx | 1 + 6 files changed, 27 insertions(+), 15 deletions(-) diff --git a/libs/data-access/cosmos/src/types.ts b/libs/data-access/cosmos/src/types.ts index b38b52bad..77a461a9b 100644 --- a/libs/data-access/cosmos/src/types.ts +++ b/libs/data-access/cosmos/src/types.ts @@ -13,7 +13,12 @@ import type { EstimatedFeeResponse } from '@haqq/data-access-falconer'; export { type Proposal }; -export type TallyResults = TallyResponse['tally']; +export type TallyResults = { + yes_count: string; + abstain_count: string; + no_count: string; + no_with_veto_count: string; +}; export interface RedelegationResponse { redelegation_responses: Array<{ diff --git a/libs/governance/src/lib/proposal-details-page.tsx b/libs/governance/src/lib/proposal-details-page.tsx index 44d57d488..3c7581d74 100644 --- a/libs/governance/src/lib/proposal-details-page.tsx +++ b/libs/governance/src/lib/proposal-details-page.tsx @@ -932,6 +932,7 @@ function ProposalInfo({ proposalId }: { proposalId: string }) { const { data: proposalDetails, isFetched } = useProposalDetailsQuery(proposalId); const { data: proposalTally } = useProposalTallyQuery(proposalId); + console.log('ProposalInfo proposalTally:', proposalTally, proposalId); const { data: govParams } = useGovernanceParamsQuery(); const { ethAddress, haqqAddress } = useAddress(); const chains = useChains(); diff --git a/libs/governance/src/lib/proposal-list-page.tsx b/libs/governance/src/lib/proposal-list-page.tsx index c816092ce..a8a500119 100644 --- a/libs/governance/src/lib/proposal-list-page.tsx +++ b/libs/governance/src/lib/proposal-list-page.tsx @@ -3,6 +3,7 @@ import { useMemo } from 'react'; import { Proposal, ProposalStatus } from '@evmos/provider'; import { useTranslate } from '@tolgee/react'; import Link from 'next/link'; +import { TallyResults } from '@haqq/data-access-cosmos'; import { formatVoteResults, useAddress, @@ -68,7 +69,7 @@ export function ProposalListPage() { const proposalsToRender = useMemo(() => { return proposals.map((proposal) => { const updatetProposalData = proposal; - let tallyResults = proposal.final_tally_result; + let tallyResults = proposal.final_tally_result as any as TallyResults; if (updatetProposalData.status === ProposalStatus.Voting) { const ongoingTally = ongoingProposalTallysResultMap.get( diff --git a/libs/main/src/lib/components/proposal-list-block.tsx b/libs/main/src/lib/components/proposal-list-block.tsx index d26fe7b9e..cc6fd1ad7 100644 --- a/libs/main/src/lib/components/proposal-list-block.tsx +++ b/libs/main/src/lib/components/proposal-list-block.tsx @@ -2,6 +2,7 @@ import { useMemo } from 'react'; import { Proposal, ProposalStatus } from '@evmos/provider'; import { useTranslate } from '@tolgee/react'; import Link from 'next/link'; +import { TallyResults } from '@haqq/data-access-cosmos'; import { ProposalListCard } from '@haqq/shell-governance'; import { formatVoteResults, @@ -54,7 +55,7 @@ export function ProposalListBlock() { const proposalsToRender = useMemo(() => { return proposals.map((proposal) => { - let tallyResults = proposal.final_tally_result; + let tallyResults = proposal.final_tally_result as any as TallyResults; if (proposal.status === ProposalStatus.Voting) { const ongoingTally = ongoingProposalTallysResultMap.get( diff --git a/libs/shared/src/utils/vote-calculations.ts b/libs/shared/src/utils/vote-calculations.ts index 2a0a236b8..1d32a6b95 100644 --- a/libs/shared/src/utils/vote-calculations.ts +++ b/libs/shared/src/utils/vote-calculations.ts @@ -12,10 +12,10 @@ export function parseVoteCount(value: string): bigint { // Calculate total votes export function calculateTotalVotes(results: TallyResults): bigint { return ( - parseVoteCount(results.yes) + - parseVoteCount(results.abstain) + - parseVoteCount(results.no) + - parseVoteCount(results.no_with_veto) + parseVoteCount(results.yes_count) + + parseVoteCount(results.abstain_count) + + parseVoteCount(results.no_count) + + parseVoteCount(results.no_with_veto_count) ); } @@ -75,20 +75,23 @@ export function formatVoteResults( return { yes: { - value: results.yes, - ...calculateVotePercentageAndBigInt(results.yes, totalVotes), + value: results.yes_count, + ...calculateVotePercentageAndBigInt(results.yes_count, totalVotes), }, abstain: { - value: results.abstain, - ...calculateVotePercentageAndBigInt(results.abstain, totalVotes), + value: results.abstain_count, + ...calculateVotePercentageAndBigInt(results.abstain_count, totalVotes), }, no: { - value: results.no, - ...calculateVotePercentageAndBigInt(results.no, totalVotes), + value: results.no_count, + ...calculateVotePercentageAndBigInt(results.no_count, totalVotes), }, noWithVeto: { - value: results.no_with_veto, - ...calculateVotePercentageAndBigInt(results.no_with_veto, totalVotes), + value: results.no_with_veto_count, + ...calculateVotePercentageAndBigInt( + results.no_with_veto_count, + totalVotes, + ), }, total: totalVotes.toString(), totalBigInt: totalVotes, diff --git a/libs/ui-kit/src/lib/proposal-vote-progress.tsx b/libs/ui-kit/src/lib/proposal-vote-progress.tsx index 671cf3c20..9b1827f25 100644 --- a/libs/ui-kit/src/lib/proposal-vote-progress.tsx +++ b/libs/ui-kit/src/lib/proposal-vote-progress.tsx @@ -72,6 +72,7 @@ export function ProposalVoteProgress({ }): ReactElement { const { t } = useTranslate('common'); + console.log('VoteResults voteResults:', voteResults); return (
From 64a7473600f1865052601dd9095249c3ad860702 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Fri, 19 Dec 2025 14:04:14 +0300 Subject: [PATCH 053/141] fix: update tally response type to TallyResults for improved type safety --- libs/data-access/cosmos/src/lib/data-access-cosmos.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/libs/data-access/cosmos/src/lib/data-access-cosmos.ts b/libs/data-access/cosmos/src/lib/data-access-cosmos.ts index 3e62e924b..12d9bb54d 100644 --- a/libs/data-access/cosmos/src/lib/data-access-cosmos.ts +++ b/libs/data-access/cosmos/src/lib/data-access-cosmos.ts @@ -11,7 +11,6 @@ import { generateEndpointGetValidators, generateEndpointGetUndelegations, generateEndpointDistributionRewardsByAddress, - generateEndpointProposals, Proposal, Validator, GetDelegationsResponse, @@ -19,7 +18,6 @@ import { BroadcastMode, TxToSend, GetUndelegationsResponse, - generateEndpointProposalTally, TallyResponse, generateEndpointBalances, BalancesResponse, @@ -53,6 +51,7 @@ import { TokenPairsResponse, TransactionStatusResponse, CoinomicsParams, + TallyResults, } from '../types'; export function generateEndpointValidatorInfo(address: string) { @@ -573,7 +572,7 @@ export function createCosmosService(cosmosRestEndpoint: string): CosmosService { const responseJson: TallyResponse = await response.json(); - return responseJson.tally; + return responseJson.tally as any as TallyResults; } async function getBankBalances(address: string) { From ca99969e134fe3997dd628c4000b6d0b06c30a3a Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Fri, 19 Dec 2025 14:09:32 +0300 Subject: [PATCH 054/141] chore: self review --- apps/shell/src/app/[locale]/governance/page.tsx | 2 +- libs/governance/src/lib/proposal-list-page.tsx | 2 +- libs/main/src/lib/components/proposal-list-block.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/shell/src/app/[locale]/governance/page.tsx b/apps/shell/src/app/[locale]/governance/page.tsx index ea3499db5..7605c1fb3 100644 --- a/apps/shell/src/app/[locale]/governance/page.tsx +++ b/apps/shell/src/app/[locale]/governance/page.tsx @@ -1,4 +1,4 @@ -import { Proposal, ProposalStatus } from '@evmos/provider'; +import { ProposalStatus } from '@evmos/provider'; import { HydrationBoundary, QueryClient, diff --git a/libs/governance/src/lib/proposal-list-page.tsx b/libs/governance/src/lib/proposal-list-page.tsx index a8a500119..15ac02276 100644 --- a/libs/governance/src/lib/proposal-list-page.tsx +++ b/libs/governance/src/lib/proposal-list-page.tsx @@ -1,6 +1,6 @@ 'use client'; import { useMemo } from 'react'; -import { Proposal, ProposalStatus } from '@evmos/provider'; +import { ProposalStatus } from '@evmos/provider'; import { useTranslate } from '@tolgee/react'; import Link from 'next/link'; import { TallyResults } from '@haqq/data-access-cosmos'; diff --git a/libs/main/src/lib/components/proposal-list-block.tsx b/libs/main/src/lib/components/proposal-list-block.tsx index cc6fd1ad7..ae1c009c8 100644 --- a/libs/main/src/lib/components/proposal-list-block.tsx +++ b/libs/main/src/lib/components/proposal-list-block.tsx @@ -1,5 +1,5 @@ import { useMemo } from 'react'; -import { Proposal, ProposalStatus } from '@evmos/provider'; +import { ProposalStatus } from '@evmos/provider'; import { useTranslate } from '@tolgee/react'; import Link from 'next/link'; import { TallyResults } from '@haqq/data-access-cosmos'; From 9d4a67f7c39abf1cda5efdf59d649dbdc9036e5e Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Fri, 19 Dec 2025 15:41:40 +0300 Subject: [PATCH 055/141] fix: eth label in bridge selector --- .../src/lib/components/token-selector.tsx | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/libs/bridge/src/lib/components/token-selector.tsx b/libs/bridge/src/lib/components/token-selector.tsx index 163a20026..bbb27d803 100644 --- a/libs/bridge/src/lib/components/token-selector.tsx +++ b/libs/bridge/src/lib/components/token-selector.tsx @@ -31,6 +31,12 @@ interface TokenSelectorProps { disabled?: boolean; } +const NATIVE_ETH_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'; + +const isNativeToken = (address: string): boolean => { + return address.toLowerCase() === NATIVE_ETH_ADDRESS.toLowerCase(); +}; + export function TokenSelectOption({ children: _, ...rest @@ -42,6 +48,10 @@ export function TokenSelectOption({ : ''; }, [token.formattedBalance]); + const displayAddress = useMemo(() => { + return isNativeToken(token.address) ? 'native' : token.address; + }, [token.address]); + return (
@@ -50,7 +60,7 @@ export function TokenSelectOption({ {token.symbol} {token.name && `(${token.name})`}
- {token.address} + {displayAddress}
{balanceText && ( @@ -76,12 +86,21 @@ export function TokenSelector({ const { t } = useTranslate('bridge'); const handleFilterOption = useCallback((option: any, inputValue: string) => { - const { label, value } = option.data; + const { label, value, token } = option.data; const inputLower = inputValue.toLowerCase(); const labelLower = label.toLowerCase(); const valueLower = value.toLowerCase(); - return labelLower.includes(inputLower) || valueLower.includes(inputLower); + // Check if it matches label or address + const matchesLabelOrAddress = + labelLower.includes(inputLower) || valueLower.includes(inputLower); + + // If searching for "native" and token is native ETH, include it + if (inputLower === 'native' && isNativeToken(token.address)) { + return true; + } + + return matchesLabelOrAddress; }, []); const classNames = useMemo>(() => { From 70ef5dc4b89aa288f390f1b807cbcabc461b57b6 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Fri, 19 Dec 2025 15:48:41 +0300 Subject: [PATCH 056/141] feat: add order delete btn --- .../lib/components/withdrawal-order-card.tsx | 335 +++++++++++------- .../src/lib/hooks/use-withdrawal-orders.ts | 18 + 2 files changed, 228 insertions(+), 125 deletions(-) diff --git a/libs/bridge/src/lib/components/withdrawal-order-card.tsx b/libs/bridge/src/lib/components/withdrawal-order-card.tsx index 5aca8deef..09869d8e7 100644 --- a/libs/bridge/src/lib/components/withdrawal-order-card.tsx +++ b/libs/bridge/src/lib/components/withdrawal-order-card.tsx @@ -8,13 +8,21 @@ import { XCircle, AlertTriangle, ExternalLink, + Trash2, } from 'lucide-react'; import Link from 'next/link'; import { useAccount } from 'wagmi'; import { getAddressExplorerUrl, getTxExplorerUrl } from '@haqq/shell-shared'; -import { Button, Tooltip } from '@haqq/shell-ui-kit'; +import { + Button, + Tooltip, + Modal, + ModalCloseButton, + ModalHeading, +} from '@haqq/shell-ui-kit'; import { OP_STACK_CHAINS } from '../constants/op-stack-config'; import { useL2ToL1Withdrawal } from '../hooks/use-l2-to-l1-withdrawal'; +import { useWithdrawalOrders } from '../hooks/use-withdrawal-orders'; import { WithdrawalOrder, WithdrawalStatus } from '../types/withdrawal-order'; interface WithdrawalOrderCardProps { @@ -33,12 +41,15 @@ export function WithdrawalOrderCard({ order }: WithdrawalOrderCardProps) { const { t } = useTranslate('common'); const { isConnected } = useAccount(); const [isProcessing, setIsProcessing] = useState(false); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [timerInfo, setTimerInfo] = useState<{ seconds: number; isReady: boolean; formattedTime: string; } | null>(null); + const { deleteOrderByInitiateHash } = useWithdrawalOrders(); + const reset = useCallback(() => { setIsProcessing(false); }, []); @@ -190,6 +201,19 @@ export function WithdrawalOrderCard({ order }: WithdrawalOrderCardProps) { [finalizeWithdrawal], ); + const handleDelete = useCallback(() => { + setIsDeleteModalOpen(true); + }, []); + + const confirmDelete = useCallback(() => { + deleteOrderByInitiateHash(order.initiateHash); + setIsDeleteModalOpen(false); + }, [deleteOrderByInitiateHash, order.initiateHash]); + + const cancelDelete = useCallback(() => { + setIsDeleteModalOpen(false); + }, []); + const nextAction = useMemo(() => { return getNextAction(order); }, [order, getNextAction]); @@ -198,158 +222,219 @@ export function WithdrawalOrderCard({ order }: WithdrawalOrderCardProps) { }, [order.status, getWaitingTimeWarning]); return ( -
-
-
-
- {getStatusIcon(order.status)} - - {formatAmount(order.amount, order.tokenSymbol)} - - - {getStatusText(order.status)} - - - {nextAction && ( -
- -
- )} -
+ <> +
+
+
+
+ {getStatusIcon(order.status)} + + {formatAmount(order.amount, order.tokenSymbol)} + + + {getStatusText(order.status)} + - {/* Timer Information */} - {timerInfo && ( -
-
- {warning && !timerInfo?.isReady && ( - - - {order.status === WithdrawalStatus.INITIATED - ? 'Time to prove:' - : 'Time to finalize:'} - - +
+ {nextAction && ( + )} - - {timerInfo.formattedTime} - + +
- {timerInfo.isReady && ( -
- ✅ Ready to{' '} - {order.status === WithdrawalStatus.INITIATED - ? 'prove' - : 'finalize'} - ! -
- )}
- )} - -
-
-
- From:{' '} - - {order.fromAddress.slice(0, 6)}... - {order.fromAddress.slice(-4)} - -
-
- To:{' '} - +
+ {warning && !timerInfo?.isReady && ( + + + {order.status === WithdrawalStatus.INITIATED + ? 'Time to prove:' + : 'Time to finalize:'} + + )} - target="_blank" - rel="noopener noreferrer" - className="font-mono text-blue-600 hover:text-blue-800" - title={order.toAddress} - > - {order.toAddress.slice(0, 6)}...{order.toAddress.slice(-4)} - -
-
- Initiated:{' '} - {formatDate(order.createdAt)} + + {timerInfo.formattedTime} + +
+ {timerInfo.isReady && ( +
+ ✅ Ready to{' '} + {order.status === WithdrawalStatus.INITIATED + ? 'prove' + : 'finalize'} + ! +
+ )}
+ )} - {order.proveHash && ( -
- Prove: +
+
+
+ From:{' '} - {order.proveHash.slice(0, 10)}... - + {order.fromAddress.slice(0, 6)}... + {order.fromAddress.slice(-4)}
- )} - - {order.finalizeHash && ( -
- Finalize: +
+ To:{' '} - {order.finalizeHash.slice(0, 10)}... - + {order.toAddress.slice(0, 6)}...{order.toAddress.slice(-4)}
+
+ Initiated:{' '} + {formatDate(order.createdAt)} +
+ + {order.proveHash && ( +
+ Prove: + + {order.proveHash.slice(0, 10)}... + + +
+ )} + + {order.finalizeHash && ( +
+ Finalize: + + {order.finalizeHash.slice(0, 10)}... + + +
+ )} +
+ + {order.error && ( +
+ Error: {order.error} +
)}
+
+
+
+ + {/* Delete Confirmation Modal */} + +
+ + +
+
+ + {t('delete-withdrawal-order', 'Delete Withdrawal Order')} + +
- {order.error && ( -
- Error: {order.error} +
+
+ {t( + 'delete-withdrawal-confirmation', + 'Are you sure you want to delete this withdrawal order? This action cannot be undone. The withdrawal transaction will remain on the blockchain, but it will be removed from your order list.', + )}
- )} +
+
+ {formatAmount(order.amount, order.tokenSymbol)} +
+
+ {t('initiated', 'Initiated')}: {formatDate(order.createdAt)} +
+
+
+ +
+ + +
-
-
+ + ); } diff --git a/libs/bridge/src/lib/hooks/use-withdrawal-orders.ts b/libs/bridge/src/lib/hooks/use-withdrawal-orders.ts index 9a9d7a33e..aad29ed4a 100644 --- a/libs/bridge/src/lib/hooks/use-withdrawal-orders.ts +++ b/libs/bridge/src/lib/hooks/use-withdrawal-orders.ts @@ -159,11 +159,29 @@ export function useWithdrawalOrders() { [getWaitingTimeWarning], ); + // Delete order by initiate hash + const deleteOrderByInitiateHash = useCallback( + (initiateHash: string) => { + setStorage((prev: string) => { + const parsedPrev = JSON.parse(prev) as WithdrawalOrderStorage; + return JSON.stringify({ + ...parsedPrev, + orders: parsedPrev.orders.filter((order) => { + return order.initiateHash !== initiateHash; + }), + lastUpdated: Date.now(), + }); + }); + }, + [setStorage], + ); + return { orders, pendingOrders, addWithdrawalOrder, updateOrderByInitiateHash, + deleteOrderByInitiateHash, getOrderById, getOrderByInitiateHash, updateOrderTimers, From 7141a94e0745c609c94eef36f31dbd8193f52023 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Sat, 20 Dec 2025 22:15:02 +0300 Subject: [PATCH 057/141] fix: proposals --- libs/governance/src/lib/components/proposal-list-card.tsx | 2 +- libs/governance/src/lib/proposal-details-page.tsx | 4 ++-- libs/ui-kit/src/lib/proposal-vote-progress.tsx | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/libs/governance/src/lib/components/proposal-list-card.tsx b/libs/governance/src/lib/components/proposal-list-card.tsx index b0b8f354f..4986d4f9c 100644 --- a/libs/governance/src/lib/components/proposal-list-card.tsx +++ b/libs/governance/src/lib/components/proposal-list-card.tsx @@ -42,7 +42,7 @@ export function ProposalListCard({ return ( - {proposalDetails.content?.description && ( + {(proposalDetails as any).summary && (
- {proposalDetails.content?.description?.replace( + {(proposalDetails as any).summary?.replace( /\\n/g, '\n', )} diff --git a/libs/ui-kit/src/lib/proposal-vote-progress.tsx b/libs/ui-kit/src/lib/proposal-vote-progress.tsx index 9b1827f25..671cf3c20 100644 --- a/libs/ui-kit/src/lib/proposal-vote-progress.tsx +++ b/libs/ui-kit/src/lib/proposal-vote-progress.tsx @@ -72,7 +72,6 @@ export function ProposalVoteProgress({ }): ReactElement { const { t } = useTranslate('common'); - console.log('VoteResults voteResults:', voteResults); return (
From a0f418208135bda8fe9f201e173af60be9044a14 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Mon, 22 Dec 2025 10:44:13 +0300 Subject: [PATCH 058/141] feat: add supported ETHIQ swaps --- .../src/app/api/tokens/balances/route.ts | 12 +- .../app/api/tokens/native-balance/route.ts | 8 +- libs/bridge/src/lib/bridge-page.tsx | 20 +- .../lib/components/token-deployment-page.tsx | 1 - .../lib/components/withdrawal-order-card.tsx | 10 +- .../src/lib/constants/op-stack-config.ts | 35 ++- .../bridge/src/lib/hooks/use-bridge-chains.ts | 12 +- .../src/lib/hooks/use-bridge-transaction.ts | 6 +- .../src/lib/hooks/use-l2-to-l1-withdrawal.ts | 6 +- .../src/lib/hooks/use-op-stack-clients.ts | 64 ++--- .../src/lib/hooks/use-withdrawal-orders.ts | 21 +- libs/bridge/src/lib/services/scanner-api.ts | 10 +- libs/shared/src/index.ts | 2 + libs/shared/src/utils/bridge-testethiq.ts | 99 +++++++ libs/shared/src/utils/bridge.ts | 253 +++--------------- libs/shared/src/utils/chain-utils.ts | 7 +- libs/shared/src/utils/chains.ts | 12 +- libs/shared/src/utils/common-bridge-utils.ts | 139 ++++++++++ 18 files changed, 420 insertions(+), 297 deletions(-) create mode 100644 libs/shared/src/utils/bridge-testethiq.ts create mode 100644 libs/shared/src/utils/common-bridge-utils.ts diff --git a/apps/shell/src/app/api/tokens/balances/route.ts b/apps/shell/src/app/api/tokens/balances/route.ts index ab9af81d6..862ae9bd0 100644 --- a/apps/shell/src/app/api/tokens/balances/route.ts +++ b/apps/shell/src/app/api/tokens/balances/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; import { haqqMainnet, haqqTestedge2, sepolia } from 'viem/chains'; -import { haqqTestethiq } from '@haqq/shell-shared'; +import { haqqEthiq, haqqTestethiq } from '@haqq/shell-shared'; export interface TokenBalance { symbol: string; @@ -132,8 +132,14 @@ function getChainConfig(chainId: number): ChainConfig | null { [haqqTestethiq.id]: { // HAQQ Devnet1 apiUrl: haqqTestethiq.blockExplorers.default.apiUrl, - nativeSymbol: 'ISLM', - nativeName: 'Islamic Coin', + nativeSymbol: 'ETH', + nativeName: 'Ethereum', + }, + [haqqEthiq.id]: { + // HAQQ Ethiq + apiUrl: haqqEthiq.blockExplorers.default.apiUrl, + nativeSymbol: 'ETH', + nativeName: 'Ethereum', }, [sepolia.id]: { // Sepolia diff --git a/apps/shell/src/app/api/tokens/native-balance/route.ts b/apps/shell/src/app/api/tokens/native-balance/route.ts index 4391b04b9..cca29943b 100644 --- a/apps/shell/src/app/api/tokens/native-balance/route.ts +++ b/apps/shell/src/app/api/tokens/native-balance/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { createPublicClient, http, formatEther, Chain } from 'viem'; import { haqqMainnet, haqqTestedge2, sepolia } from 'viem/chains'; -import { haqqTestethiq } from '@haqq/shell-shared'; +import { haqqEthiq, haqqTestethiq } from '@haqq/shell-shared'; export async function GET(request: NextRequest) { try { @@ -80,6 +80,12 @@ function getChainConfig(chainId: number): ChainConfig | null { nativeSymbol: 'ETH', nativeName: 'Ethereum', }, + [haqqEthiq.id]: { + chain: haqqEthiq, + rpcUrl: haqqEthiq.rpcUrls.default.http[0], + nativeSymbol: 'ETH', + nativeName: 'Ethereum', + }, [sepolia.id]: { chain: sepolia, rpcUrl: sepolia.rpcUrls.default.http[0], diff --git a/libs/bridge/src/lib/bridge-page.tsx b/libs/bridge/src/lib/bridge-page.tsx index 803411787..b24f44fdb 100644 --- a/libs/bridge/src/lib/bridge-page.tsx +++ b/libs/bridge/src/lib/bridge-page.tsx @@ -21,7 +21,7 @@ import { RecoveryLink, FaucetLinksCard, } from './components'; -import { OP_STACK_CHAINS } from './constants/op-stack-config'; +import { getOpStackChains } from './constants/op-stack-config'; import { useBridgeState, useTokenAllowance, @@ -39,10 +39,16 @@ import { // SUPPORTED_CHAINS is now imported from @haqq/shell-shared export const useChainProxyAddress = (chainId: number | undefined) => { - if (chainId === CHAIN_CONFIG.l1ChainId) { + if ( + chainId === CHAIN_CONFIG.l1ChainId || + chainId === CHAIN_CONFIG.l1TestChainId + ) { return L1_STANDARD_BRIDGE_ADDRESS; } - if (chainId === CHAIN_CONFIG.l2ChainId) { + if ( + chainId === CHAIN_CONFIG.l2ChainId || + chainId === CHAIN_CONFIG.l2TestChainId + ) { return L2_STANDARD_BRIDGE_ADDRESS; } return ''; @@ -213,9 +219,11 @@ export function BridgePage() { return chainItem.id === chain?.id; }) ) { - switchChainAsync({ chainId: OP_STACK_CHAINS.L1.id }).catch((error) => { - console.error('Failed to switch to Sepolia on page load:', error); - }); + switchChainAsync({ chainId: getOpStackChains(chain?.id).L1.id }).catch( + (error) => { + console.error('Failed to switch to Sepolia on page load:', error); + }, + ); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Run only on mount diff --git a/libs/bridge/src/lib/components/token-deployment-page.tsx b/libs/bridge/src/lib/components/token-deployment-page.tsx index 10d478c66..f3e788635 100644 --- a/libs/bridge/src/lib/components/token-deployment-page.tsx +++ b/libs/bridge/src/lib/components/token-deployment-page.tsx @@ -10,7 +10,6 @@ import { useReadContract, } from 'wagmi'; import { - BRIDGE_ADDRESSES, getChainById, L2_OPTIMISM_MINTABLE_ERC20_FACTORY_ADDRESS, } from '@haqq/shell-shared'; diff --git a/libs/bridge/src/lib/components/withdrawal-order-card.tsx b/libs/bridge/src/lib/components/withdrawal-order-card.tsx index 09869d8e7..c652ed760 100644 --- a/libs/bridge/src/lib/components/withdrawal-order-card.tsx +++ b/libs/bridge/src/lib/components/withdrawal-order-card.tsx @@ -20,7 +20,7 @@ import { ModalCloseButton, ModalHeading, } from '@haqq/shell-ui-kit'; -import { OP_STACK_CHAINS } from '../constants/op-stack-config'; +import { getOpStackChains } from '../constants/op-stack-config'; import { useL2ToL1Withdrawal } from '../hooks/use-l2-to-l1-withdrawal'; import { useWithdrawalOrders } from '../hooks/use-withdrawal-orders'; import { WithdrawalOrder, WithdrawalStatus } from '../types/withdrawal-order'; @@ -306,7 +306,7 @@ export function WithdrawalOrderCard({ order }: WithdrawalOrderCardProps) { { + if (chainId === sepolia.id) { + return OP_STACK_TESTNET_CHAINS; + } else if (chainId === mainnet.id) { + return OP_STACK_MAINNET_CHAINS; + } else if (chainId === haqqEthiq.id) { + return OP_STACK_MAINNET_CHAINS; + } else if (chainId === haqqTestethiq.id) { + return OP_STACK_TESTNET_CHAINS; + } + return OP_STACK_TESTNET_CHAINS; +}; diff --git a/libs/bridge/src/lib/hooks/use-bridge-chains.ts b/libs/bridge/src/lib/hooks/use-bridge-chains.ts index 8c23492a2..885739fbe 100644 --- a/libs/bridge/src/lib/hooks/use-bridge-chains.ts +++ b/libs/bridge/src/lib/hooks/use-bridge-chains.ts @@ -30,10 +30,16 @@ export function useBridgeChains({ if (sourceChainId === CHAIN_CONFIG.l1ChainId) { return CHAIN_CONFIG.l2ChainId; } + if (sourceChainId === CHAIN_CONFIG.l1TestChainId) { + return CHAIN_CONFIG.l2TestChainId; + } // L2 -> L1 bridging if (sourceChainId === CHAIN_CONFIG.l2ChainId) { return CHAIN_CONFIG.l1ChainId; } + if (sourceChainId === CHAIN_CONFIG.l2TestChainId) { + return CHAIN_CONFIG.l1TestChainId; + } // Default fallback return CHAIN_CONFIG.l2ChainId; }, [sourceChainId]); @@ -41,8 +47,10 @@ export function useBridgeChains({ // Check if this is an L2 to L1 transfer const isL2ToL1 = useMemo(() => { return ( - sourceChainId === CHAIN_CONFIG.l2ChainId && - targetChainId === CHAIN_CONFIG.l1ChainId + (sourceChainId === CHAIN_CONFIG.l2ChainId && + targetChainId === CHAIN_CONFIG.l1ChainId) || + (sourceChainId === CHAIN_CONFIG.l2TestChainId && + targetChainId === CHAIN_CONFIG.l1TestChainId) ); }, [sourceChainId, targetChainId]); diff --git a/libs/bridge/src/lib/hooks/use-bridge-transaction.ts b/libs/bridge/src/lib/hooks/use-bridge-transaction.ts index e58686108..82770e414 100644 --- a/libs/bridge/src/lib/hooks/use-bridge-transaction.ts +++ b/libs/bridge/src/lib/hooks/use-bridge-transaction.ts @@ -98,8 +98,10 @@ export function useBridgeTransaction({ // Check if this is an L2 to L1 transfer const isL2ToL1 = - sourceChainId === CHAIN_CONFIG.l2ChainId && - targetChainId === CHAIN_CONFIG.l1ChainId; + (sourceChainId === CHAIN_CONFIG.l2ChainId && + targetChainId === CHAIN_CONFIG.l1ChainId) || + (sourceChainId === CHAIN_CONFIG.l2TestChainId && + targetChainId === CHAIN_CONFIG.l1TestChainId); if (isL2ToL1) { if (token.address === ETH_ADDRESS) { diff --git a/libs/bridge/src/lib/hooks/use-l2-to-l1-withdrawal.ts b/libs/bridge/src/lib/hooks/use-l2-to-l1-withdrawal.ts index 6245c36ae..d750c5a23 100644 --- a/libs/bridge/src/lib/hooks/use-l2-to-l1-withdrawal.ts +++ b/libs/bridge/src/lib/hooks/use-l2-to-l1-withdrawal.ts @@ -297,7 +297,8 @@ export function useL2ToL1Withdrawal({ // According to Viem docs: "Prove the withdrawal on the L1" const proveHash = await walletClient.proveWithdrawal({ ...proveArgs, - targetChain: chains.L2_WITH_CONTRACTS, + // TODO: Fix this + targetChain: chains.L2_WITH_CONTRACTS as any, }); // Step 5: Wait until the prove withdrawal is processed @@ -388,7 +389,8 @@ export function useL2ToL1Withdrawal({ // Step 4: Finalize the withdrawal // According to Viem docs: "Finalize the withdrawal" const finalizeHash = await walletClient.finalizeWithdrawal({ - targetChain: chains.L2_WITH_CONTRACTS, + // TODO: Fix this + targetChain: chains.L2_WITH_CONTRACTS as any, withdrawal, }); diff --git a/libs/bridge/src/lib/hooks/use-op-stack-clients.ts b/libs/bridge/src/lib/hooks/use-op-stack-clients.ts index b3e11afbf..2a8132518 100644 --- a/libs/bridge/src/lib/hooks/use-op-stack-clients.ts +++ b/libs/bridge/src/lib/hooks/use-op-stack-clients.ts @@ -9,69 +9,69 @@ import { walletActionsL2, } from 'viem/op-stack'; import { useAccount, useWalletClient } from 'wagmi'; -import { OP_STACK_CHAINS } from '../constants/op-stack-config'; +import { getOpStackChains } from '../constants/op-stack-config'; /** * Hook to create and manage OP Stack compatible clients for L1 and L2 chains * Provides both public and wallet clients with proper OP Stack extensions */ export function useOpStackClients() { - const { address } = useAccount(); + const { address, chain } = useAccount(); const { data: walletClient } = useWalletClient(); // Create L1 public client (Sepolia) with OP Stack contracts const publicClientL1 = useMemo(() => { return createPublicClient({ - chain: OP_STACK_CHAINS.L1, + chain: getOpStackChains(chain?.id).L1, transport: typeof window !== 'undefined' && window.ethereum ? custom(window.ethereum) - : http(OP_STACK_CHAINS.L1.rpcUrls.default.http[0]), + : http(getOpStackChains(chain?.id).L1.rpcUrls.default.http[0]), batch: { multicall: true }, }).extend(publicActionsL1()); - }, []); + }, [chain?.id]); const publicClientReadonlyL1 = useMemo(() => { return createPublicClient({ - chain: OP_STACK_CHAINS.L1, - transport: http(OP_STACK_CHAINS.L1.rpcUrls.default.http[0]), + chain: getOpStackChains(chain?.id).L1, + transport: http(getOpStackChains(chain?.id).L1.rpcUrls.default.http[0]), batch: { multicall: true }, }).extend(publicActionsL1()); - }, []); + }, [chain?.id]); // Create L2 public client (HAQQ Devnet) with OP Stack contracts const publicClientL2 = useMemo(() => { return createPublicClient({ - chain: OP_STACK_CHAINS.L2, + chain: getOpStackChains(chain?.id).L2, transport: typeof window !== 'undefined' && window.ethereum ? custom(window.ethereum) - : http(OP_STACK_CHAINS.L2.rpcUrls.default.http[0]), + : http(getOpStackChains(chain?.id).L2.rpcUrls.default.http[0]), batch: { multicall: true }, }).extend(publicActionsL2()); - }, []); + }, [chain?.id]); const publicClientReadonlyL2 = useMemo(() => { return createPublicClient({ - chain: OP_STACK_CHAINS.L2, - transport: http(OP_STACK_CHAINS.L2.rpcUrls.default.http[0]), + chain: getOpStackChains(chain?.id).L2, + transport: http(getOpStackChains(chain?.id).L2.rpcUrls.default.http[0]), batch: { multicall: true }, }).extend(publicActionsL2()); - }, []); + }, [chain?.id]); // Create L1 wallet client (Sepolia) with OP Stack contracts const walletClientL1 = useMemo(() => { return walletClient && address ? createWalletClient({ account: address as `0x${string}`, - chain: OP_STACK_CHAINS.L1, + chain: getOpStackChains(chain?.id).L1, transport: typeof window !== 'undefined' && window.ethereum ? custom(window.ethereum) - : http(OP_STACK_CHAINS.L1.rpcUrls.default.http[0]), + : http(getOpStackChains(chain?.id).L1.rpcUrls.default.http[0]), }).extend(walletActionsL1()) : null; - }, [walletClient, address]); + }, [walletClient, address, chain?.id]); const getWalletClientL1 = useCallback(async () => { if (walletClientL1) { @@ -81,7 +81,7 @@ export function useOpStackClients() { if (address && typeof window !== 'undefined' && window.ethereum) { return createWalletClient({ account: address as `0x${string}`, - chain: OP_STACK_CHAINS.L1, + chain: getOpStackChains(chain?.id).L1, transport: custom(window.ethereum), }).extend(walletActionsL1()); } @@ -93,25 +93,27 @@ export function useOpStackClients() { return walletClient && address ? createWalletClient({ account: address as `0x${string}`, - chain: OP_STACK_CHAINS.L1, - transport: http(OP_STACK_CHAINS.L1.rpcUrls.default.http[0]), + chain: getOpStackChains(chain?.id).L1, + transport: http( + getOpStackChains(chain?.id).L1.rpcUrls.default.http[0], + ), }).extend(walletActionsL1()) : null; - }, [walletClient, address]); + }, [walletClient, address, chain?.id]); // Create L2 wallet client (HAQQ Devnet) with OP Stack contracts const walletClientL2 = useMemo(() => { return walletClient && address ? createWalletClient({ account: address as `0x${string}`, - chain: OP_STACK_CHAINS.L2, + chain: getOpStackChains(chain?.id).L2, transport: typeof window !== 'undefined' && window.ethereum ? custom(window.ethereum) - : http(OP_STACK_CHAINS.L2.rpcUrls.default.http[0]), + : http(getOpStackChains(chain?.id).L2.rpcUrls.default.http[0]), }).extend(walletActionsL2()) : null; - }, [walletClient, address]); + }, [walletClient, address, chain?.id]); const getWalletClientL2 = useCallback(async () => { if (walletClientL2) { @@ -121,23 +123,25 @@ export function useOpStackClients() { if (address && typeof window !== 'undefined' && window.ethereum) { return createWalletClient({ account: address as `0x${string}`, - chain: OP_STACK_CHAINS.L2, + chain: getOpStackChains(chain?.id).L2, transport: custom(window.ethereum), }).extend(walletActionsL2()); } return null; - }, [address, walletClientL2]); + }, [address, walletClientL2, chain?.id]); const walletClientReadonlyL2 = useMemo(() => { return walletClient && address ? createWalletClient({ account: address as `0x${string}`, - chain: OP_STACK_CHAINS.L2, - transport: http(OP_STACK_CHAINS.L2.rpcUrls.default.http[0]), + chain: getOpStackChains(chain?.id).L2, + transport: http( + getOpStackChains(chain?.id).L2.rpcUrls.default.http[0], + ), }).extend(walletActionsL2()) : null; - }, [walletClient, address]); + }, [walletClient, address, chain?.id]); return { publicClientL1, @@ -150,6 +154,6 @@ export function useOpStackClients() { publicClientReadonlyL2, walletClientReadonlyL1, walletClientReadonlyL2, - chains: OP_STACK_CHAINS, + chains: getOpStackChains(chain?.id), }; } diff --git a/libs/bridge/src/lib/hooks/use-withdrawal-orders.ts b/libs/bridge/src/lib/hooks/use-withdrawal-orders.ts index aad29ed4a..e57ad49ad 100644 --- a/libs/bridge/src/lib/hooks/use-withdrawal-orders.ts +++ b/libs/bridge/src/lib/hooks/use-withdrawal-orders.ts @@ -4,6 +4,7 @@ import { useCallback, useMemo } from 'react'; import { useLocalStorage } from 'usehooks-ts'; import { useChainId } from 'wagmi'; import { useWithdrawalTimers } from './use-withdrawal-timers'; +import { getOpStackChains } from '../constants/op-stack-config'; import { WithdrawalOrder, WithdrawalOrderStorage, @@ -39,14 +40,22 @@ export function useWithdrawalOrders() { }); }, [storage.orders]); + const opChainId = useMemo(() => { + return getOpStackChains(chainId).L1.id; + }, [chainId]); + // Get pending orders (not finalized or failed) const pendingOrders = useMemo(() => { - return storage.orders.filter((order) => { - return ![WithdrawalStatus.FINALIZED, WithdrawalStatus.FAILED].includes( - order.status, - ); - }); - }, [storage.orders]); + return storage.orders + .filter((order) => { + return ![WithdrawalStatus.FINALIZED, WithdrawalStatus.FAILED].includes( + order.status, + ); + }) + .filter((order) => { + return order.targetChainId === opChainId; + }); + }, [storage.orders, opChainId]); // Add new withdrawal order const addWithdrawalOrder = useCallback( diff --git a/libs/bridge/src/lib/services/scanner-api.ts b/libs/bridge/src/lib/services/scanner-api.ts index df1c51983..13209d479 100644 --- a/libs/bridge/src/lib/services/scanner-api.ts +++ b/libs/bridge/src/lib/services/scanner-api.ts @@ -2,8 +2,8 @@ * Scanner API service for retrieving token pair information */ -import { sepolia } from 'viem/chains'; -import { haqqTestethiq } from '@haqq/shell-shared'; +import { mainnet, sepolia } from 'viem/chains'; +import { haqqEthiq, haqqTestethiq } from '@haqq/shell-shared'; const SCANNER_API_BASE_URL = 'https://scanner.ethiq.network/api/v1'; @@ -163,7 +163,11 @@ export function getChainNameFromId(chainId: number): string { case sepolia.id: // Sepolia return 'Sepolia'; case haqqTestethiq.id: - return 'HAQQ'; + return 'HAQQ Testethiq'; + case haqqEthiq.id: + return 'HAQQ Ethiq'; + case mainnet.id: + return 'Mainnet'; default: throw new Error(`Unsupported chain ID: ${chainId}`); } diff --git a/libs/shared/src/index.ts b/libs/shared/src/index.ts index 627ecfdee..38de78c5b 100644 --- a/libs/shared/src/index.ts +++ b/libs/shared/src/index.ts @@ -46,6 +46,8 @@ export * from './precompile/distribution-abi'; export * from './precompile/adresses'; export * from './utils/bridge'; +export * from './utils/bridge-testethiq'; +export * from './utils/common-bridge-utils'; export * from './utils/chain-utils'; export * from './utils/chains'; export * from './abi/L1StandartBridge'; diff --git a/libs/shared/src/utils/bridge-testethiq.ts b/libs/shared/src/utils/bridge-testethiq.ts new file mode 100644 index 000000000..428a1b71c --- /dev/null +++ b/libs/shared/src/utils/bridge-testethiq.ts @@ -0,0 +1,99 @@ +import { sepolia } from 'viem/chains'; +import { IBridgeAddresses } from './common-bridge-utils'; + +const haqqTestethiqRpcUrl = 'https://rpc.testnet.ethiq.network/'; + +export const ETHIQ_TESTNET_BRIDGE_ADDRESSES: IBridgeAddresses = { + SuperchainProxyAdminImpl: '0x189abaaaa82dfc015a588a7dbad6f13b1d3485bc', + SuperchainConfigProxy: '0xc2be75506d5724086deb7245bd260cc9753911be', + SuperchainConfigImpl: '0xb08cc720f511062537ca78bdb0ae691f04f5a957', + ProtocolVersionsProxy: '0x79add5713b383daa0a138d3c4780c7a1804a8090', + ProtocolVersionsImpl: '0x37e15e4d6dffa9e5e320ee1ec036922e563cb76c', + OpcmImpl: '0xc69e4c24db479191676611a25d977203c3bdca62', + OpcmContractsContainerImpl: '0x0000000000000000000000000000000000000000', + OpcmGameTypeAdderImpl: '0x0000000000000000000000000000000000000000', + OpcmDeployerImpl: '0x0000000000000000000000000000000000000000', + OpcmUpgraderImpl: '0x0000000000000000000000000000000000000000', + OpcmInteropMigratorImpl: '0x0000000000000000000000000000000000000000', + OpcmStandardValidatorImpl: '0x0000000000000000000000000000000000000000', + DelayedWethImpl: '0x33dadc2d1aa9bb613a7ae6b28425ea00d44c6998', + OptimismPortalImpl: '0x7cf803296662e8c72a6c1d6450572209acf7f202', + OptimismPortalInteropImpl: '0x5cb365a10e99335d8fedfa225aac5e21287302dd', + EthLockboxImpl: '0x784d2f03593a42a6e4676a012762f18775ecbbe6', + PreimageOracleImpl: '0x1fb8cdfc6831fc866ed9c51af8817da5c287add3', + MipsImpl: '0x6463dee3828677f6270d83d45408044fc5edb908', + SystemConfigImpl: '0x2fa28989fc559836e9d66dff3010c7f7f41c65ed', + L1CrossDomainMessengerImpl: '0xb686f13aff1e427a1f993f29ab0f2e7383729fe0', + L1Erc721BridgeImpl: '0x74f1ac50eb0be98853805d381c884f5f9abdecf9', + L1StandardBridgeImpl: '0x61525eaacddb97d9184afc205827e6a4fd0bf62a', + OptimismMintableErc20FactoryImpl: + '0x8ee6fb13c6c9a7e401531168e196fbf8b05ceabb', + DisputeGameFactoryImpl: '0x74fac1d45b98bae058f8f566201c9a81b85c7d50', + AnchorStateRegistryImpl: '0x0000000000000000000000000000000000000000', + OpChainProxyAdminImpl: '0xcd9c1ab8aa13c69f72cd68520a353bd54cf5ab18', + OptimismPortalProxy: '0x5b5f73ebcda96d9c4ca3315497e370c49573cf62', + AddressManagerImpl: '0x801a839da752289f448e0c9cc3bfb8d861077faa', + L1Erc721BridgeProxy: '0xfa74bcf421580f5ba021a3f3e270004743fdaa26', + SystemConfigProxy: '0xb8ad3a6beb0301f057c33f2a039a1044ec4d9bf9', + OptimismMintableErc20FactoryProxy: + '0xf197dd37d128b451ed546c0a88153d0eaaf1d7f2', + L1StandardBridgeProxy: '0x611bc60e604803b4f064810b4630290515bfba8c', + L1CrossDomainMessengerProxy: '0x96c9dcddc1cfd08ea3848395c390df815d8ec581', + EthLockboxProxy: '0x879122273ff3ee266fe58a8da8913f01c1065276', + DisputeGameFactoryProxy: '0xa34af21d8b896ea86bd3c5dd350f04ec8719e9f9', + AnchorStateRegistryProxy: '0x804bfedd469ff6b5669378d6c82e32517c010136', + FaultDisputeGameImpl: '0x0000000000000000000000000000000000000000', + PermissionedDisputeGameImpl: '0x3369335fdc75f5cfd377dc63e9e3c570bbdb2123', + DelayedWethPermissionedGameProxy: + '0xca17f9597d2f85db54a603dae9761f180dded6e8', + DelayedWethPermissionlessGameProxy: + '0x0000000000000000000000000000000000000000', + AltDAChallengeProxy: '0x0000000000000000000000000000000000000000', + AltDAChallengeImpl: '0x0000000000000000000000000000000000000000', + L2OutputOracleProxy: '0x0000000000000000000000000000000000000000', +}; + +export const L1_STANDARD_TESTETHIQ_BRIDGE_ADDRESS = + ETHIQ_TESTNET_BRIDGE_ADDRESSES.L1StandardBridgeProxy; + +export const haqqTestethiq = { + id: 853211, + name: 'Testethiq', + nativeCurrency: { + decimals: 18, + name: 'Ethereum', + symbol: 'ETH', + }, + rpcUrls: { + default: { + http: [haqqTestethiqRpcUrl], + }, + }, + blockExplorers: { + default: { + name: 'HAQQ Testethiq', + url: 'https://explorer.testnet.ethiq.network/', + apiUrl: 'https://explorer.testnet.ethiq.network/api', + }, + }, + contracts: { + portal: { + [sepolia.id]: { + address: + ETHIQ_TESTNET_BRIDGE_ADDRESSES.OptimismPortalProxy as `0x${string}`, + }, + }, + disputeGameFactory: { + [sepolia.id]: { + address: + ETHIQ_TESTNET_BRIDGE_ADDRESSES.DisputeGameFactoryProxy as `0x${string}`, + }, + }, + l2OutputOracle: { + [sepolia.id]: { + // deprecated https://docs.optimism.io/stack/smart-contracts/smart-contracts + address: '0x0000000000000000000000000000000000000000' as `0x${string}`, // Placeholder + }, + }, + }, +}; diff --git a/libs/shared/src/utils/bridge.ts b/libs/shared/src/utils/bridge.ts index cfcaeed6b..e79cfaac3 100644 --- a/libs/shared/src/utils/bridge.ts +++ b/libs/shared/src/utils/bridge.ts @@ -1,121 +1,15 @@ -import { haqqTestedge2, sepolia } from 'viem/chains'; +import { mainnet } from 'viem/chains'; +import { IBridgeAddresses } from './common-bridge-utils'; -const haqqTestethiqRpcUrl = 'https://rpc.testnet.ethiq.network/'; +const haqqEthiqRpcUrl = 'https://rpc.internal.ethiq.network/'; -export const SWAPPABLE_TOKENS: { - [chainId: number]: { - symbol: string; - address: string; - }[]; -} = { - [sepolia.id]: [ - { - symbol: 'ETH', - address: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - }, - { - symbol: 'USDC', - address: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238', - }, - { - symbol: 'EURC', - address: '0x08210f9170f89ab7658f0b5e3ff39b0e03c594d4', - }, - ], -}; - -/** - * Mapping of L1 token addresses to their corresponding L2 token addresses - */ -export const L1_TO_L2_TOKEN_MAP: { - [l1Address: string]: string; -} = { - // ETH remains the same address on both chains - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee': - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - // USDC mapping from L1 to L2 -}; - -/** - * Get the L2 token address for a given L1 token address - * @param l1TokenAddress - The L1 token address - * @returns The corresponding L2 token address or null if not found - */ -export function getL2TokenAddress(l1TokenAddress: string): string | null { - return L1_TO_L2_TOKEN_MAP[l1TokenAddress.toLowerCase()] || null; -} - -/** - * Check if a token has a corresponding L2 deployment - * @param l1TokenAddress - The L1 token address - * @returns True if the token has an L2 counterpart - */ -export function hasL2Token(l1TokenAddress: string): boolean { - return l1TokenAddress.toLowerCase() in L1_TO_L2_TOKEN_MAP; -} - -/** - * Network configuration for Superbridge HAQQ Devnet1 - * - * @see https://explorer.devnet1.dev.haqq.network/ - */ -export interface NetworkConfig { - name: string; - rpcUrl: string; - explorerUrl: string; -} - -export const BRIDGE_ADDRESSES: { - SuperchainProxyAdminImpl: string; - SuperchainConfigProxy: string; - SuperchainConfigImpl: string; - ProtocolVersionsProxy: string; - ProtocolVersionsImpl: string; - OpcmImpl: string; - OpcmContractsContainerImpl: string; - OpcmGameTypeAdderImpl: string; - OpcmDeployerImpl: string; - OpcmUpgraderImpl: string; - OpcmInteropMigratorImpl: string; - OpcmStandardValidatorImpl: string; - DelayedWethImpl: string; - OptimismPortalImpl: string; - OptimismPortalInteropImpl: string; - EthLockboxImpl: string; - PreimageOracleImpl: string; - MipsImpl: string; - SystemConfigImpl: string; - L1CrossDomainMessengerImpl: string; - L1Erc721BridgeImpl: string; - L1StandardBridgeImpl: string; - OptimismMintableErc20FactoryImpl: string; - DisputeGameFactoryImpl: string; - AnchorStateRegistryImpl: string; - OpChainProxyAdminImpl: string; - OptimismPortalProxy: string; - AddressManagerImpl: string; - L1Erc721BridgeProxy: string; - SystemConfigProxy: string; - OptimismMintableErc20FactoryProxy: string; - L1StandardBridgeProxy: string; - L1CrossDomainMessengerProxy: string; - EthLockboxProxy: string; - DisputeGameFactoryProxy: string; - AnchorStateRegistryProxy: string; - FaultDisputeGameImpl: string; - PermissionedDisputeGameImpl: string; - DelayedWethPermissionedGameProxy: string; - DelayedWethPermissionlessGameProxy: string; - AltDAChallengeProxy: string; - AltDAChallengeImpl: string; - L2OutputOracleProxy: string; -} = { - SuperchainProxyAdminImpl: '0x189abaaaa82dfc015a588a7dbad6f13b1d3485bc', - SuperchainConfigProxy: '0xc2be75506d5724086deb7245bd260cc9753911be', +export const ETHIQ_BRIDGE_ADDRESSES: IBridgeAddresses = { + SuperchainProxyAdminImpl: '0x543ba4aadbab8f9025686bd03993043599c6fb04', + SuperchainConfigProxy: '0x95703e0982140d16f8eba6d158fccede42f04a4c', SuperchainConfigImpl: '0xb08cc720f511062537ca78bdb0ae691f04f5a957', - ProtocolVersionsProxy: '0x79add5713b383daa0a138d3c4780c7a1804a8090', + ProtocolVersionsProxy: '0x8062abc286f5e7d9428a0ccb9abd71e50d93b935', ProtocolVersionsImpl: '0x37e15e4d6dffa9e5e320ee1ec036922e563cb76c', - OpcmImpl: '0xc69e4c24db479191676611a25d977203c3bdca62', + OpcmImpl: '0xfa1ef97fb02b0da2ee2346b8e310907ab5519449', OpcmContractsContainerImpl: '0x0000000000000000000000000000000000000000', OpcmGameTypeAdderImpl: '0x0000000000000000000000000000000000000000', OpcmDeployerImpl: '0x0000000000000000000000000000000000000000', @@ -136,22 +30,22 @@ export const BRIDGE_ADDRESSES: { '0x8ee6fb13c6c9a7e401531168e196fbf8b05ceabb', DisputeGameFactoryImpl: '0x74fac1d45b98bae058f8f566201c9a81b85c7d50', AnchorStateRegistryImpl: '0x0000000000000000000000000000000000000000', - OpChainProxyAdminImpl: '0xcd9c1ab8aa13c69f72cd68520a353bd54cf5ab18', - OptimismPortalProxy: '0x5b5f73ebcda96d9c4ca3315497e370c49573cf62', - AddressManagerImpl: '0x801a839da752289f448e0c9cc3bfb8d861077faa', - L1Erc721BridgeProxy: '0xfa74bcf421580f5ba021a3f3e270004743fdaa26', - SystemConfigProxy: '0xb8ad3a6beb0301f057c33f2a039a1044ec4d9bf9', + OpChainProxyAdminImpl: '0x004ea6bad47a7b51cb248e17b39b1a53cac663c0', + OptimismPortalProxy: '0xea857195ff569a0c623464e1a3062dd398d53046', + AddressManagerImpl: '0x5bc146f89c67f1fa4334efb689a4a926b521ac0c', + L1Erc721BridgeProxy: '0xfb96b6c6a70ed0e1e1f9b26cbaf85f2ecbf26aec', + SystemConfigProxy: '0x3f715f77cc105fca0936f479bded4a035eefe974', OptimismMintableErc20FactoryProxy: - '0xf197dd37d128b451ed546c0a88153d0eaaf1d7f2', - L1StandardBridgeProxy: '0x611bc60e604803b4f064810b4630290515bfba8c', - L1CrossDomainMessengerProxy: '0x96c9dcddc1cfd08ea3848395c390df815d8ec581', - EthLockboxProxy: '0x879122273ff3ee266fe58a8da8913f01c1065276', - DisputeGameFactoryProxy: '0xa34af21d8b896ea86bd3c5dd350f04ec8719e9f9', - AnchorStateRegistryProxy: '0x804bfedd469ff6b5669378d6c82e32517c010136', + '0x2045bc5f56e5b40978bab717c274aa2db9ea019d', + L1StandardBridgeProxy: '0xfb30129241e7520e66b96426259e95359c3e2800', + L1CrossDomainMessengerProxy: '0xad4e4387b7f53b326d0d797f5342a120e8b427d2', + EthLockboxProxy: '0xe26cd6ffdbc21f7294cf56245162be431b17c8e5', + DisputeGameFactoryProxy: '0xd68f5ce839e1325401a9deab56fa1c14cddb1cea', + AnchorStateRegistryProxy: '0x159725154956a4097afdca285e4684764d7a02a5', FaultDisputeGameImpl: '0x0000000000000000000000000000000000000000', - PermissionedDisputeGameImpl: '0x3369335fdc75f5cfd377dc63e9e3c570bbdb2123', + PermissionedDisputeGameImpl: '0xe85f5174c2043444aa09141885d8d72b9b175d81', DelayedWethPermissionedGameProxy: - '0xca17f9597d2f85db54a603dae9761f180dded6e8', + '0xfbc084a037d510d1cb1d1aac08e010ecd8a42f5e', DelayedWethPermissionlessGameProxy: '0x0000000000000000000000000000000000000000', AltDAChallengeProxy: '0x0000000000000000000000000000000000000000', @@ -160,49 +54,11 @@ export const BRIDGE_ADDRESSES: { }; export const L1_STANDARD_BRIDGE_ADDRESS = - BRIDGE_ADDRESSES.L1StandardBridgeProxy; - -// l2StandardBridgeProxyAddress https://github.com/ethereum-optimism/ecosystem/blob/8c0ceae82d8e909c0d00b4601d7c7276090774cc/packages/viem/src/actions/withdrawOptimismERC20.ts#L90 -export const L2_STANDARD_BRIDGE_ADDRESS = - '0x4200000000000000000000000000000000000010'; - -export const L2_OPTIMISM_MINTABLE_ERC20_FACTORY_ADDRESS = - '0x4200000000000000000000000000000000000012'; - -// Additional configuration from TOML -export const DEPLOYMENT_CONFIG = { - configType: 'standard-overrides', - l1ChainId: sepolia.id, // Sepolia - fundDevAccounts: false, - useInterop: false, - l1ContractsLocator: 'tag://op-contracts/v3.0.0-rc.2', - l2ContractsLocator: 'tag://op-contracts/v3.0.0-rc.2', - useFaultProofs: false, - useCustomGasToken: false, - enableGovernance: true, -}; - -// Time offsets for L2 genesis -export const L2_GENESIS_TIME_OFFSETS = { - l2GenesisRegolithTimeOffset: '0x0', - l2GenesisCanyonTimeOffset: '0x0', - l2GenesisDeltaTimeOffset: '0x0', - l2GenesisEcotoneTimeOffset: '0x0', - l2GenesisFjordTimeOffset: '0x0', - l2GenesisGraniteTimeOffset: '0x0', - l2GenesisHoloceneTimeOffset: '0x0', - l2GenesisIsthmusTimeOffset: '0x0', -}; + ETHIQ_BRIDGE_ADDRESSES.L1StandardBridgeProxy; -// L1 time offsets -export const L1_TIME_OFFSETS = { - l1CancunTimeOffset: '0x0', - l1PragueTimeOffset: '0x0', -}; - -export const haqqTestethiq = { - id: 853211, - name: 'Testethiq', +export const haqqEthiq = { + id: 30303, + name: 'Ethiq', nativeCurrency: { decimals: 18, name: 'Ethereum', @@ -210,72 +66,33 @@ export const haqqTestethiq = { }, rpcUrls: { default: { - http: [haqqTestethiqRpcUrl], + http: [haqqEthiqRpcUrl], }, }, blockExplorers: { default: { - name: 'HAQQ Testethiq', - url: 'https://explorer.testnet.ethiq.network/', - apiUrl: 'https://explorer.testnet.ethiq.network/api', + name: 'HAQQ Ethiq', + url: 'https://explorer.ethiq.network/', + apiUrl: 'https://explorer.ethiq.network/api', }, }, contracts: { portal: { - [sepolia.id]: { - address: BRIDGE_ADDRESSES.OptimismPortalProxy as `0x${string}`, + [mainnet.id]: { + address: ETHIQ_BRIDGE_ADDRESSES.OptimismPortalProxy as `0x${string}`, }, }, disputeGameFactory: { - [sepolia.id]: { - address: BRIDGE_ADDRESSES.DisputeGameFactoryProxy as `0x${string}`, + [mainnet.id]: { + address: + ETHIQ_BRIDGE_ADDRESSES.DisputeGameFactoryProxy as `0x${string}`, }, }, l2OutputOracle: { - [sepolia.id]: { + [mainnet.id]: { // deprecated https://docs.optimism.io/stack/smart-contracts/smart-contracts address: '0x0000000000000000000000000000000000000000' as `0x${string}`, // Placeholder }, }, }, }; - -// Chain configuration constants -export const CHAIN_CONFIG = { - l1ChainId: sepolia.id, // Sepolia - l2ChainId: haqqTestethiq.id, - l2BlockTime: 3, - batchInboxAddress: '0xff94b3795acce6d3691fb4538646a91561e95e7d', - eip1559Denominator: 50, - eip1559DenominatorCanyon: 250, - eip1559Elasticity: 6, - operatorFeeScalar: 0, - operatorFeeConstant: 0, -}; - -export const L1_CHAINS = [sepolia]; -export const L2_CHAINS = [haqqTestethiq]; - -export const getTxExplorerUrl = (hash: string, chainId: number) => { - if (chainId === sepolia.id) { - // Sepolia - return `${sepolia.blockExplorers.default.url}/tx/${hash}`; - } else if (chainId === haqqTestethiq.id) { - // HAQQ Devnet - return `${haqqTestethiq.blockExplorers.default.url}/tx/${hash}`; - } - return '#'; -}; - -export const getAddressExplorerUrl = (address: string, chainId: number) => { - if (chainId === sepolia.id) { - // Sepolia - return `${sepolia.blockExplorers.default.url}/address/${address}`; - } else if (chainId === haqqTestethiq.id) { - // HAQQ Devnet - return `${haqqTestethiq.blockExplorers.default.url}/address/${address}`; - } - return '#'; -}; - -export const FAUCET_CHAINS = [haqqTestedge2.id, haqqTestethiq.id, sepolia.id]; diff --git a/libs/shared/src/utils/chain-utils.ts b/libs/shared/src/utils/chain-utils.ts index cd3438cf0..a5d6ca5d1 100644 --- a/libs/shared/src/utils/chain-utils.ts +++ b/libs/shared/src/utils/chain-utils.ts @@ -1,8 +1,9 @@ -import { sepolia } from 'viem/chains'; -import { haqqTestethiq } from './bridge'; +import { mainnet, sepolia } from 'viem/chains'; +import { haqqEthiq } from './bridge'; +import { haqqTestethiq } from './bridge-testethiq'; // Supported chains for bridge operations -export const SUPPORTED_CHAINS = [sepolia, haqqTestethiq]; +export const SUPPORTED_CHAINS = [sepolia, haqqTestethiq, haqqEthiq, mainnet]; /** * Get chain configuration by chain ID diff --git a/libs/shared/src/utils/chains.ts b/libs/shared/src/utils/chains.ts index 78e6c17c0..3c64aeac3 100644 --- a/libs/shared/src/utils/chains.ts +++ b/libs/shared/src/utils/chains.ts @@ -1,7 +1,13 @@ -import { haqqMainnet, haqqTestedge2, sepolia } from 'viem/chains'; -import { haqqTestethiq } from './bridge'; +import { haqqMainnet, haqqTestedge2, mainnet, sepolia } from 'viem/chains'; +import { haqqEthiq } from './bridge'; +import { haqqTestethiq } from './bridge-testethiq'; -export const bridgeSupportedChains = [haqqTestethiq, sepolia]; +export const bridgeSupportedChains = [ + haqqTestethiq, + sepolia, + haqqEthiq, + mainnet, +]; export const faucetSupportedChains = [haqqTestedge2, haqqTestethiq]; export const baseSupportedChains = [haqqMainnet, haqqTestedge2] as const; diff --git a/libs/shared/src/utils/common-bridge-utils.ts b/libs/shared/src/utils/common-bridge-utils.ts new file mode 100644 index 000000000..0693f4d21 --- /dev/null +++ b/libs/shared/src/utils/common-bridge-utils.ts @@ -0,0 +1,139 @@ +import { haqqTestedge2, mainnet, sepolia } from 'viem/chains'; +import { haqqEthiq } from './bridge'; +import { haqqTestethiq } from './bridge-testethiq'; + +export interface IBridgeAddresses { + SuperchainProxyAdminImpl: string; + SuperchainConfigProxy: string; + SuperchainConfigImpl: string; + ProtocolVersionsProxy: string; + ProtocolVersionsImpl: string; + OpcmImpl: string; + OpcmContractsContainerImpl: string; + OpcmGameTypeAdderImpl: string; + OpcmDeployerImpl: string; + OpcmUpgraderImpl: string; + OpcmInteropMigratorImpl: string; + OpcmStandardValidatorImpl: string; + DelayedWethImpl: string; + OptimismPortalImpl: string; + OptimismPortalInteropImpl: string; + EthLockboxImpl: string; + PreimageOracleImpl: string; + MipsImpl: string; + SystemConfigImpl: string; + L1CrossDomainMessengerImpl: string; + L1Erc721BridgeImpl: string; + L1StandardBridgeImpl: string; + OptimismMintableErc20FactoryImpl: string; + DisputeGameFactoryImpl: string; + AnchorStateRegistryImpl: string; + OpChainProxyAdminImpl: string; + OptimismPortalProxy: string; + AddressManagerImpl: string; + L1Erc721BridgeProxy: string; + SystemConfigProxy: string; + OptimismMintableErc20FactoryProxy: string; + L1StandardBridgeProxy: string; + L1CrossDomainMessengerProxy: string; + EthLockboxProxy: string; + DisputeGameFactoryProxy: string; + AnchorStateRegistryProxy: string; + FaultDisputeGameImpl: string; + PermissionedDisputeGameImpl: string; + DelayedWethPermissionedGameProxy: string; + DelayedWethPermissionlessGameProxy: string; + AltDAChallengeProxy: string; + AltDAChallengeImpl: string; + L2OutputOracleProxy: string; +} + +export const getAddressExplorerUrl = (address: string, chainId: number) => { + if (chainId === sepolia.id) { + // Sepolia + return `${sepolia.blockExplorers.default.url}/address/${address}`; + } else if (chainId === haqqTestethiq.id) { + // HAQQ Devnet + return `${haqqTestethiq.blockExplorers.default.url}/address/${address}`; + } else if (chainId === mainnet.id) { + // Mainnet + return `${mainnet.blockExplorers.default.url}/address/${address}`; + } else if (chainId === haqqEthiq.id) { + // Ethiq + return `${haqqEthiq.blockExplorers.default.url}/address/${address}`; + } + return '#'; +}; + +export const getTxExplorerUrl = (hash: string, chainId: number) => { + if (chainId === sepolia.id) { + // Sepolia + return `${sepolia.blockExplorers.default.url}/tx/${hash}`; + } else if (chainId === haqqTestethiq.id) { + // HAQQ Devnet + return `${haqqTestethiq.blockExplorers.default.url}/tx/${hash}`; + } else if (chainId === mainnet.id) { + // Mainnet + return `${mainnet.blockExplorers.default.url}/tx/${hash}`; + } else if (chainId === haqqEthiq.id) { + // Ethiq + return `${haqqEthiq.blockExplorers.default.url}/tx/${hash}`; + } + return '#'; +}; + +export const SWAPPABLE_TOKENS: { + [chainId: number]: { + symbol: string; + address: string; + }[]; +} = { + [sepolia.id]: [ + { + symbol: 'ETH', + address: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + }, + { + symbol: 'USDC', + address: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238', + }, + { + symbol: 'EURC', + address: '0x08210f9170f89ab7658f0b5e3ff39b0e03c594d4', + }, + ], + [mainnet.id]: [ + { + symbol: 'ETH', + address: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + }, + ], +}; + +/** + * Network configuration for Superbridge HAQQ Devnet1 + * + * @see https://explorer.devnet1.dev.haqq.network/ + */ +export interface NetworkConfig { + name: string; + rpcUrl: string; + explorerUrl: string; +} + +// Chain configuration constants +export const CHAIN_CONFIG = { + l1ChainId: mainnet.id, // Mainnet + l1TestChainId: sepolia.id, // Sepolia + l2ChainId: haqqEthiq.id, + l2TestChainId: haqqTestethiq.id, +}; + +// l2StandardBridgeProxyAddress https://github.com/ethereum-optimism/ecosystem/blob/8c0ceae82d8e909c0d00b4601d7c7276090774cc/packages/viem/src/actions/withdrawOptimismERC20.ts#L90 +export const L2_STANDARD_BRIDGE_ADDRESS = + '0x4200000000000000000000000000000000000010'; + +export const L2_OPTIMISM_MINTABLE_ERC20_FACTORY_ADDRESS = + '0x4200000000000000000000000000000000000012'; + +export const FAUCET_CHAINS = [haqqTestedge2.id, haqqTestethiq.id, sepolia.id]; From b2c131d4a477097942b3393a28213380431a82c9 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Mon, 22 Dec 2025 16:12:14 +0300 Subject: [PATCH 059/141] feat: add mainnet support and USDC token to bridge utilities --- apps/shell/src/app/api/tokens/balances/route.ts | 8 +++++++- apps/shell/src/app/api/tokens/native-balance/route.ts | 8 +++++++- libs/bridge/src/lib/hooks/use-bridge-transaction.ts | 4 ---- libs/shared/src/utils/common-bridge-utils.ts | 4 ++++ 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/apps/shell/src/app/api/tokens/balances/route.ts b/apps/shell/src/app/api/tokens/balances/route.ts index 862ae9bd0..54b53f0d5 100644 --- a/apps/shell/src/app/api/tokens/balances/route.ts +++ b/apps/shell/src/app/api/tokens/balances/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; -import { haqqMainnet, haqqTestedge2, sepolia } from 'viem/chains'; +import { haqqMainnet, haqqTestedge2, mainnet, sepolia } from 'viem/chains'; import { haqqEthiq, haqqTestethiq } from '@haqq/shell-shared'; export interface TokenBalance { @@ -159,6 +159,12 @@ function getChainConfig(chainId: number): ChainConfig | null { nativeSymbol: 'ISLM', nativeName: 'Islamic Coin', }, + [mainnet.id]: { + // Mainnet + apiUrl: mainnet.blockExplorers.default.apiUrl, + nativeSymbol: 'ETH', + nativeName: 'Ethereum', + }, }; return configs[chainId] || null; diff --git a/apps/shell/src/app/api/tokens/native-balance/route.ts b/apps/shell/src/app/api/tokens/native-balance/route.ts index cca29943b..e6e93e8fc 100644 --- a/apps/shell/src/app/api/tokens/native-balance/route.ts +++ b/apps/shell/src/app/api/tokens/native-balance/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; import { createPublicClient, http, formatEther, Chain } from 'viem'; -import { haqqMainnet, haqqTestedge2, sepolia } from 'viem/chains'; +import { haqqMainnet, haqqTestedge2, mainnet, sepolia } from 'viem/chains'; import { haqqEthiq, haqqTestethiq } from '@haqq/shell-shared'; export async function GET(request: NextRequest) { @@ -104,6 +104,12 @@ function getChainConfig(chainId: number): ChainConfig | null { nativeSymbol: 'ISLM', nativeName: 'Islamic Coin', }, + [mainnet.id]: { + chain: mainnet, + rpcUrl: mainnet.rpcUrls.default.http[0], + nativeSymbol: 'ETH', + nativeName: 'Ethereum', + }, }; return configs[chainId] || null; diff --git a/libs/bridge/src/lib/hooks/use-bridge-transaction.ts b/libs/bridge/src/lib/hooks/use-bridge-transaction.ts index 82770e414..8a915163c 100644 --- a/libs/bridge/src/lib/hooks/use-bridge-transaction.ts +++ b/libs/bridge/src/lib/hooks/use-bridge-transaction.ts @@ -136,10 +136,6 @@ export function useBridgeTransaction({ const is100PercentOfAvailableBalance = availableBalance === amount; - console.log( - 'is100PercentOfAvailableBalance', - is100PercentOfAvailableBalance, - ); try { let hash: string; diff --git a/libs/shared/src/utils/common-bridge-utils.ts b/libs/shared/src/utils/common-bridge-utils.ts index 0693f4d21..5ff1c2bec 100644 --- a/libs/shared/src/utils/common-bridge-utils.ts +++ b/libs/shared/src/utils/common-bridge-utils.ts @@ -107,6 +107,10 @@ export const SWAPPABLE_TOKENS: { symbol: 'ETH', address: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', }, + { + symbol: 'USDC', + address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + }, ], }; From 242b2614cd98543c50827e1d55df42a1675ae928 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Mon, 22 Dec 2025 16:30:56 +0300 Subject: [PATCH 060/141] refactor: improve withdrawal hooks and modal input precision --- .../lib/components/pending-withdrawals.tsx | 2 - .../src/lib/hooks/use-l2-to-l1-withdrawal.ts | 1 + .../src/lib/hooks/use-op-stack-clients.ts | 72 ++++++++++--------- libs/ui-kit/src/lib/modal-input.tsx | 29 +++++--- 4 files changed, 61 insertions(+), 43 deletions(-) diff --git a/libs/bridge/src/lib/components/pending-withdrawals.tsx b/libs/bridge/src/lib/components/pending-withdrawals.tsx index f023acca7..130a4e8ff 100644 --- a/libs/bridge/src/lib/components/pending-withdrawals.tsx +++ b/libs/bridge/src/lib/components/pending-withdrawals.tsx @@ -15,8 +15,6 @@ export function PendingWithdrawals({ orders }: PendingWithdrawalsProps) { return null; } - console.log('orders', orders); - return (

diff --git a/libs/bridge/src/lib/hooks/use-l2-to-l1-withdrawal.ts b/libs/bridge/src/lib/hooks/use-l2-to-l1-withdrawal.ts index d750c5a23..553a70c50 100644 --- a/libs/bridge/src/lib/hooks/use-l2-to-l1-withdrawal.ts +++ b/libs/bridge/src/lib/hooks/use-l2-to-l1-withdrawal.ts @@ -85,6 +85,7 @@ export function useL2ToL1Withdrawal({ publicClientReadonlyL2, } = useOpStackClients(); + console.log('publicClientL1', publicClientL1); const { isConnected } = useAccount(); const toast = useToast(); diff --git a/libs/bridge/src/lib/hooks/use-op-stack-clients.ts b/libs/bridge/src/lib/hooks/use-op-stack-clients.ts index 2a8132518..4de4bf408 100644 --- a/libs/bridge/src/lib/hooks/use-op-stack-clients.ts +++ b/libs/bridge/src/lib/hooks/use-op-stack-clients.ts @@ -19,59 +19,67 @@ export function useOpStackClients() { const { address, chain } = useAccount(); const { data: walletClient } = useWalletClient(); + const opChainL1 = useMemo(() => { + return getOpStackChains(chain?.id).L1; + }, [chain?.id]); + + const opChainL2 = useMemo(() => { + return getOpStackChains(chain?.id).L2; + }, [chain?.id]); + // Create L1 public client (Sepolia) with OP Stack contracts const publicClientL1 = useMemo(() => { return createPublicClient({ - chain: getOpStackChains(chain?.id).L1, + chain: opChainL1, transport: typeof window !== 'undefined' && window.ethereum ? custom(window.ethereum) - : http(getOpStackChains(chain?.id).L1.rpcUrls.default.http[0]), + : http(opChainL1.rpcUrls.default.http[0]), batch: { multicall: true }, }).extend(publicActionsL1()); - }, [chain?.id]); + }, [opChainL1]); const publicClientReadonlyL1 = useMemo(() => { return createPublicClient({ - chain: getOpStackChains(chain?.id).L1, - transport: http(getOpStackChains(chain?.id).L1.rpcUrls.default.http[0]), + chain: opChainL1, + transport: http(opChainL1.rpcUrls.default.http[0]), batch: { multicall: true }, }).extend(publicActionsL1()); - }, [chain?.id]); + }, [opChainL1]); // Create L2 public client (HAQQ Devnet) with OP Stack contracts const publicClientL2 = useMemo(() => { return createPublicClient({ - chain: getOpStackChains(chain?.id).L2, + chain: opChainL2, transport: typeof window !== 'undefined' && window.ethereum ? custom(window.ethereum) - : http(getOpStackChains(chain?.id).L2.rpcUrls.default.http[0]), + : http(opChainL2.rpcUrls.default.http[0]), batch: { multicall: true }, }).extend(publicActionsL2()); - }, [chain?.id]); + }, [opChainL2]); const publicClientReadonlyL2 = useMemo(() => { return createPublicClient({ - chain: getOpStackChains(chain?.id).L2, + chain: opChainL2, transport: http(getOpStackChains(chain?.id).L2.rpcUrls.default.http[0]), batch: { multicall: true }, }).extend(publicActionsL2()); - }, [chain?.id]); + }, [opChainL2]); // Create L1 wallet client (Sepolia) with OP Stack contracts const walletClientL1 = useMemo(() => { return walletClient && address ? createWalletClient({ account: address as `0x${string}`, - chain: getOpStackChains(chain?.id).L1, + chain: opChainL1, transport: typeof window !== 'undefined' && window.ethereum ? custom(window.ethereum) - : http(getOpStackChains(chain?.id).L1.rpcUrls.default.http[0]), + : http(opChainL1.rpcUrls.default.http[0]), }).extend(walletActionsL1()) : null; - }, [walletClient, address, chain?.id]); + }, [walletClient, address, opChainL1]); const getWalletClientL1 = useCallback(async () => { if (walletClientL1) { @@ -81,39 +89,37 @@ export function useOpStackClients() { if (address && typeof window !== 'undefined' && window.ethereum) { return createWalletClient({ account: address as `0x${string}`, - chain: getOpStackChains(chain?.id).L1, + chain: opChainL1, transport: custom(window.ethereum), }).extend(walletActionsL1()); } return null; - }, [address, walletClientL1]); + }, [address, walletClientL1, opChainL1]); const walletClientReadonlyL1 = useMemo(() => { return walletClient && address ? createWalletClient({ account: address as `0x${string}`, - chain: getOpStackChains(chain?.id).L1, - transport: http( - getOpStackChains(chain?.id).L1.rpcUrls.default.http[0], - ), + chain: opChainL1, + transport: http(opChainL1.rpcUrls.default.http[0]), }).extend(walletActionsL1()) : null; - }, [walletClient, address, chain?.id]); + }, [walletClient, address, opChainL1]); // Create L2 wallet client (HAQQ Devnet) with OP Stack contracts const walletClientL2 = useMemo(() => { return walletClient && address ? createWalletClient({ account: address as `0x${string}`, - chain: getOpStackChains(chain?.id).L2, + chain: opChainL2, transport: typeof window !== 'undefined' && window.ethereum ? custom(window.ethereum) - : http(getOpStackChains(chain?.id).L2.rpcUrls.default.http[0]), + : http(opChainL2.rpcUrls.default.http[0]), }).extend(walletActionsL2()) : null; - }, [walletClient, address, chain?.id]); + }, [walletClient, address, opChainL2]); const getWalletClientL2 = useCallback(async () => { if (walletClientL2) { @@ -123,25 +129,27 @@ export function useOpStackClients() { if (address && typeof window !== 'undefined' && window.ethereum) { return createWalletClient({ account: address as `0x${string}`, - chain: getOpStackChains(chain?.id).L2, + chain: opChainL2, transport: custom(window.ethereum), }).extend(walletActionsL2()); } return null; - }, [address, walletClientL2, chain?.id]); + }, [address, walletClientL2, opChainL2]); const walletClientReadonlyL2 = useMemo(() => { return walletClient && address ? createWalletClient({ account: address as `0x${string}`, - chain: getOpStackChains(chain?.id).L2, - transport: http( - getOpStackChains(chain?.id).L2.rpcUrls.default.http[0], - ), + chain: opChainL2, + transport: http(opChainL2.rpcUrls.default.http[0]), }).extend(walletActionsL2()) : null; - }, [walletClient, address, chain?.id]); + }, [walletClient, address, opChainL2]); + + const chains = useMemo(() => { + return getOpStackChains(chain?.id); + }, [chain?.id]); return { publicClientL1, @@ -154,6 +162,6 @@ export function useOpStackClients() { publicClientReadonlyL2, walletClientReadonlyL1, walletClientReadonlyL2, - chains: getOpStackChains(chain?.id), + chains, }; } diff --git a/libs/ui-kit/src/lib/modal-input.tsx b/libs/ui-kit/src/lib/modal-input.tsx index 988fc8da3..19763b4e4 100644 --- a/libs/ui-kit/src/lib/modal-input.tsx +++ b/libs/ui-kit/src/lib/modal-input.tsx @@ -10,7 +10,7 @@ import clsx from 'clsx'; import MaskedInput from 'react-text-mask'; import { createNumberMask } from 'text-mask-addons'; -const DEFAULT_DECIMAL_LIMIT = 3; +const DEFAULT_DECIMAL_LIMIT = 18; const defaultMaskOptions = { prefix: '', suffix: '', @@ -25,14 +25,25 @@ export const usePreparedMaskValue = ( value: string | readonly string[] | number | undefined, ) => { const inputValue = useMemo(() => { - // Hack, because react-text-mask doesn't work correctly with decimals - // ex: it converts 0.0709 to 0.070 (not 0.071!) - // Additionally, remove trailing zeros and only fix to decimal limit if it has decimals - return value - ? Number(value).toString().includes('.') - ? Number(value).toFixed(DEFAULT_DECIMAL_LIMIT).replace(/0+$/, '') - : value - : undefined; + if (!value && value !== 0) return undefined; + + // Convert to string, preserving precision + let stringValue: string; + if (typeof value === 'number') { + // Use toFixed with high precision to avoid scientific notation + // This preserves values like 0.0001 without rounding + stringValue = value.toFixed(18); + } else if (Array.isArray(value)) { + stringValue = value.join(''); + } else { + stringValue = String(value); + } + + // Remove trailing zeros for cleaner display, but preserve the decimal point if needed + // This allows values like 0.0001 to be displayed correctly + const cleaned = stringValue.replace(/\.?0+$/, ''); + // If cleaning removed everything (e.g., value was 0), return "0" + return cleaned || '0'; }, [value]); return { From 16058dced6d955aac437c4b67384d9b59fc0b019 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Mon, 22 Dec 2025 16:51:25 +0300 Subject: [PATCH 061/141] refactor: update withdrawal hook to use readonly client and improve account handling --- libs/bridge/src/lib/hooks/use-l2-to-l1-withdrawal.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/libs/bridge/src/lib/hooks/use-l2-to-l1-withdrawal.ts b/libs/bridge/src/lib/hooks/use-l2-to-l1-withdrawal.ts index 553a70c50..877e04be4 100644 --- a/libs/bridge/src/lib/hooks/use-l2-to-l1-withdrawal.ts +++ b/libs/bridge/src/lib/hooks/use-l2-to-l1-withdrawal.ts @@ -85,7 +85,6 @@ export function useL2ToL1Withdrawal({ publicClientReadonlyL2, } = useOpStackClients(); - console.log('publicClientL1', publicClientL1); const { isConnected } = useAccount(); const toast = useToast(); @@ -103,8 +102,8 @@ export function useL2ToL1Withdrawal({ try { // Step 1: Build parameters to initiate the withdrawal transaction on the L1 // According to Viem docs: "Build parameters to initiate the withdrawal transaction on the L1" - const args = await publicClientL1.buildInitiateWithdrawal({ - account: walletClient.account, + const args = await publicClientReadonlyL1.buildInitiateWithdrawal({ + account: walletClient.account.address, to: toAddress as `0x${string}`, value: parseEther(amount.toString()), }); @@ -151,7 +150,7 @@ export function useL2ToL1Withdrawal({ }, [ getWalletClientL2, - publicClientL1, + publicClientReadonlyL1, publicClientReadonlyL2, chains, addWithdrawalOrder, From 465b6f78b3454bd13ee8dd605fcb2b2aed7921fe Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Mon, 22 Dec 2025 16:53:16 +0300 Subject: [PATCH 062/141] feat: filter orders by author --- libs/bridge/src/lib/hooks/use-withdrawal-orders.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/libs/bridge/src/lib/hooks/use-withdrawal-orders.ts b/libs/bridge/src/lib/hooks/use-withdrawal-orders.ts index e57ad49ad..4174539fa 100644 --- a/libs/bridge/src/lib/hooks/use-withdrawal-orders.ts +++ b/libs/bridge/src/lib/hooks/use-withdrawal-orders.ts @@ -2,7 +2,7 @@ import { useCallback, useMemo } from 'react'; import { useLocalStorage } from 'usehooks-ts'; -import { useChainId } from 'wagmi'; +import { useAccount, useChainId } from 'wagmi'; import { useWithdrawalTimers } from './use-withdrawal-timers'; import { getOpStackChains } from '../constants/op-stack-config'; import { @@ -32,6 +32,7 @@ export function useWithdrawalOrders() { useWithdrawalTimers(); const chainId = useChainId(); + const { address } = useAccount(); // Get all orders const orders = useMemo(() => { @@ -53,9 +54,12 @@ export function useWithdrawalOrders() { ); }) .filter((order) => { - return order.targetChainId === opChainId; + return ( + order.targetChainId === opChainId && + order.fromAddress?.toLowerCase() === address?.toLowerCase() + ); }); - }, [storage.orders, opChainId]); + }, [storage.orders, opChainId, address]); // Add new withdrawal order const addWithdrawalOrder = useCallback( From 7cdea9cc56a0f996cdc9a885e9bdaacf2e67e5d4 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Mon, 22 Dec 2025 18:08:11 +0300 Subject: [PATCH 063/141] feat: show address on bridge page --- .../src/components/web3-connect-button.tsx | 34 ++++++++++++++----- libs/bridge/src/lib/hooks/use-bridge-state.ts | 13 ------- libs/ui-kit/src/lib/account-button.tsx | 1 + .../ui-kit/src/lib/proposal-vote-progress.tsx | 1 - 4 files changed, 26 insertions(+), 23 deletions(-) diff --git a/apps/shell/src/components/web3-connect-button.tsx b/apps/shell/src/components/web3-connect-button.tsx index c7853d390..ceaf2f8cf 100644 --- a/apps/shell/src/components/web3-connect-button.tsx +++ b/apps/shell/src/components/web3-connect-button.tsx @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import { useTranslate } from '@tolgee/react'; import { usePathname } from 'next/navigation'; -import { useAccount, useChains } from 'wagmi'; +import { useAccount, useBalance, useChains } from 'wagmi'; import { faucetSupportedChains, getFormattedAddress, @@ -104,12 +104,12 @@ export function Web3ConnectButtons() { chains={chainArray} />

- {!isBridgePage && ( + {isBridgePage ? ( + + ) : (
-
)} @@ -157,12 +157,12 @@ export function Web3ConnectButtonsMobile() { dropdownClassName="end-auto start-0" />
- {!isBridgePage && ( + {isBridgePage ? ( + + ) : (
-
)} @@ -172,3 +172,19 @@ export function Web3ConnectButtonsMobile() {
); } + +const BridgePageAccountBtn = () => { + const { data: balance } = useBalance(); + return ; +}; + +const AccountBtnWrapper = ({ balance }: { balance: string | undefined }) => { + const { ethAddress } = useAddress(); + return ( + + ); +}; diff --git a/libs/bridge/src/lib/hooks/use-bridge-state.ts b/libs/bridge/src/lib/hooks/use-bridge-state.ts index 0d90acb68..4920fc3e9 100644 --- a/libs/bridge/src/lib/hooks/use-bridge-state.ts +++ b/libs/bridge/src/lib/hooks/use-bridge-state.ts @@ -169,26 +169,13 @@ export function useBridgeState({ console.log( 'API token fetch failed or returned empty list, using SWAPPABLE_TOKENS fallback', ); - console.log('Error:', tokensError); - console.log('User tokens count:', userTokens.length); const chainTokens = SWAPPABLE_TOKENS[chain.id] || []; if (chainTokens.length === 0) { - console.log( - 'No SWAPPABLE_TOKENS for chain', - chain.id, - ', using ETH token only', - ); return [ETH_TOKEN]; } - console.log( - 'Using SWAPPABLE_TOKENS for chain', - chain.id, - ':', - chainTokens, - ); return [ ...chainTokens.map((token) => { return { diff --git a/libs/ui-kit/src/lib/account-button.tsx b/libs/ui-kit/src/lib/account-button.tsx index 4da97cbd6..c2c9aa48d 100644 --- a/libs/ui-kit/src/lib/account-button.tsx +++ b/libs/ui-kit/src/lib/account-button.tsx @@ -18,6 +18,7 @@ export function AccountButton({ withoutDropdown?: boolean; }) { const { t } = useTranslate('common'); + console.log('AccountButton balance:', balance); return (
From 3ead27285cf359f1b7fb5fb0287475afb983de60 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Tue, 23 Dec 2025 11:48:33 +0300 Subject: [PATCH 064/141] fix: usage blockscout api for eth --- apps/shell/src/app/api/tokens/balances/route.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/shell/src/app/api/tokens/balances/route.ts b/apps/shell/src/app/api/tokens/balances/route.ts index 54b53f0d5..48fa4b7cb 100644 --- a/apps/shell/src/app/api/tokens/balances/route.ts +++ b/apps/shell/src/app/api/tokens/balances/route.ts @@ -38,7 +38,7 @@ export async function GET(request: NextRequest) { { status: 400 }, ); } - + // // Get chain configuration const chainConfig = getChainConfig(Number(chainId)); if (!chainConfig) { @@ -161,7 +161,7 @@ function getChainConfig(chainId: number): ChainConfig | null { }, [mainnet.id]: { // Mainnet - apiUrl: mainnet.blockExplorers.default.apiUrl, + apiUrl: 'https://eth.blockscout.com/api', nativeSymbol: 'ETH', nativeName: 'Ethereum', }, From 6ad9c73f4f02644ced9e93c712793119a07d1142 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Tue, 23 Dec 2025 12:24:38 +0300 Subject: [PATCH 065/141] fix: inputs --- libs/ui-kit/src/lib/modal-input.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/ui-kit/src/lib/modal-input.tsx b/libs/ui-kit/src/lib/modal-input.tsx index 19763b4e4..4f02685b5 100644 --- a/libs/ui-kit/src/lib/modal-input.tsx +++ b/libs/ui-kit/src/lib/modal-input.tsx @@ -10,7 +10,7 @@ import clsx from 'clsx'; import MaskedInput from 'react-text-mask'; import { createNumberMask } from 'text-mask-addons'; -const DEFAULT_DECIMAL_LIMIT = 18; +const DEFAULT_DECIMAL_LIMIT = 6; const defaultMaskOptions = { prefix: '', suffix: '', From d99404ed66d4f0db4ab2115d307a33ad80d1d8aa Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Tue, 23 Dec 2025 12:29:39 +0300 Subject: [PATCH 066/141] fix: improve precision handling in modal input for numeric values --- libs/ui-kit/src/lib/modal-input.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/libs/ui-kit/src/lib/modal-input.tsx b/libs/ui-kit/src/lib/modal-input.tsx index 4f02685b5..2aae85763 100644 --- a/libs/ui-kit/src/lib/modal-input.tsx +++ b/libs/ui-kit/src/lib/modal-input.tsx @@ -30,9 +30,16 @@ export const usePreparedMaskValue = ( // Convert to string, preserving precision let stringValue: string; if (typeof value === 'number') { - // Use toFixed with high precision to avoid scientific notation + // Round to the mask's decimal limit to avoid floating-point precision issues + // This fixes cases like 1.2 being stored as 1.19999999999999995559 + // or 2222.2 being stored as 2222.1999999999998 + // We round to DEFAULT_DECIMAL_LIMIT (6) since that's what the mask allows + const rounded = + Math.round(value * 10 ** DEFAULT_DECIMAL_LIMIT) / + 10 ** DEFAULT_DECIMAL_LIMIT; + // Use toFixed with the decimal limit to avoid scientific notation // This preserves values like 0.0001 without rounding - stringValue = value.toFixed(18); + stringValue = rounded.toFixed(DEFAULT_DECIMAL_LIMIT); } else if (Array.isArray(value)) { stringValue = value.join(''); } else { From 4a50dac8fcc4fb436e8f4b8601a629a16af52063 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Tue, 23 Dec 2025 12:38:53 +0300 Subject: [PATCH 067/141] fix: url state changes --- libs/bridge/src/lib/hooks/use-bridge-state.ts | 84 +++++++++++++++---- 1 file changed, 67 insertions(+), 17 deletions(-) diff --git a/libs/bridge/src/lib/hooks/use-bridge-state.ts b/libs/bridge/src/lib/hooks/use-bridge-state.ts index 4920fc3e9..bad4c5347 100644 --- a/libs/bridge/src/lib/hooks/use-bridge-state.ts +++ b/libs/bridge/src/lib/hooks/use-bridge-state.ts @@ -91,6 +91,31 @@ export function useBridgeState({ const [selectedToken, setSelectedToken] = useState(); const [previousChainId, setPreviousChainId] = useState(); + // Reset tokenIn and amount if URL chain doesn't match current chain + useEffect(() => { + if ( + chain?.id && + urlState?.chainIn && + chain.id !== urlState.chainIn && + (urlState.tokenIn || urlState.amount) + ) { + // URL chain doesn't match current chain - reset token and amount + updateUrlState({ + tokenIn: undefined, + amount: undefined, + }); + // Also reset local state + setBridgeAmount(undefined); + setSelectedToken(ETH_TOKEN); + } + }, [ + chain?.id, + urlState?.chainIn, + urlState?.tokenIn, + urlState?.amount, + updateUrlState, + ]); + // Clear URL state and reset amount/selected token when chain changes useEffect(() => { // Skip on initial mount or if chain is undefined @@ -104,20 +129,27 @@ export function useBridgeState({ // If chain actually changed (not just a re-render) if (chain.id !== previousChainId) { - // Clear URL state + // Update URL state with new chain and clear token/amount updateUrlState({ + chainIn: chain.id, tokenIn: undefined, amount: undefined, }); // Reset local state - setBridgeAmount(0); + setBridgeAmount(undefined); setSelectedToken(ETH_TOKEN); // Update previous chain ID setPreviousChainId(chain.id); } - }, [chain?.id, previousChainId, updateUrlState]); + }, [ + chain?.id, + previousChainId, + updateUrlState, + urlState?.chainIn, + switchChainAsync, + ]); // Initialize state from URL parameters useEffect(() => { @@ -135,25 +167,43 @@ export function useBridgeState({ // Sync state changes to URL useLayoutEffect(() => { - // Wait until tokens are finished loading before syncing to URL - if (isLoadingTokens || userTokens.length === 0) { - return; - } + // Skip if chain is not supported if ( - selectedToken && - chain?.id && - bridgeAmount !== undefined && - SUPPORTED_CHAINS.some((itemChain) => { + !chain?.id || + !SUPPORTED_CHAINS.some((itemChain) => { return chain.id === itemChain.id; }) ) { - updateUrlState({ - tokenIn: selectedToken.address, - chainIn: chain.id, - amount: bridgeAmount.toString(), - }); + return; + } + + // Always update chainIn when chain changes + const urlUpdates: Partial = { + chainIn: chain.id, + }; + + // Update tokenIn if we have a selected token and tokens are loaded + if (selectedToken && !isLoadingTokens && userTokens.length > 0) { + urlUpdates.tokenIn = selectedToken.address; + } + + // Update amount (or clear it if undefined) + if (bridgeAmount !== undefined) { + urlUpdates.amount = bridgeAmount.toString(); + } else { + // Explicitly clear amount if it's undefined + urlUpdates.amount = undefined; } - }, [selectedToken, chain?.id, bridgeAmount, updateUrlState, isLoadingTokens]); + + updateUrlState(urlUpdates); + }, [ + selectedToken, + chain?.id, + bridgeAmount, + updateUrlState, + isLoadingTokens, + userTokens.length, + ]); // Get available tokens for current chain (dynamically fetched with fallback) const availableTokens = useMemo(() => { From a276bd10a268b22ec36c00ef71ebd254c4e9d433 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Tue, 23 Dec 2025 13:25:21 +0300 Subject: [PATCH 068/141] refactor: enhance AccountButton and Web3ConnectButtons to support withoutDropdown prop --- .../src/components/web3-connect-button.tsx | 29 +++++++++++++++---- libs/ui-kit/src/lib/account-button.tsx | 2 +- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/apps/shell/src/components/web3-connect-button.tsx b/apps/shell/src/components/web3-connect-button.tsx index ceaf2f8cf..1d1fbd6ab 100644 --- a/apps/shell/src/components/web3-connect-button.tsx +++ b/apps/shell/src/components/web3-connect-button.tsx @@ -105,11 +105,12 @@ export function Web3ConnectButtons() { />
{isBridgePage ? ( - + ) : (
)} @@ -158,11 +159,12 @@ export function Web3ConnectButtonsMobile() { />
{isBridgePage ? ( - + ) : (
)} @@ -173,18 +175,33 @@ export function Web3ConnectButtonsMobile() { ); } -const BridgePageAccountBtn = () => { +const BridgePageAccountBtn = ({ + withoutDropdown, +}: { + withoutDropdown?: boolean; +}) => { const { data: balance } = useBalance(); - return ; + return ( + + ); }; -const AccountBtnWrapper = ({ balance }: { balance: string | undefined }) => { +const AccountBtnWrapper = ({ + balance, + withoutDropdown, +}: { + balance: string | undefined; + withoutDropdown?: boolean; +}) => { const { ethAddress } = useAddress(); return ( ); }; diff --git a/libs/ui-kit/src/lib/account-button.tsx b/libs/ui-kit/src/lib/account-button.tsx index c2c9aa48d..35bff0672 100644 --- a/libs/ui-kit/src/lib/account-button.tsx +++ b/libs/ui-kit/src/lib/account-button.tsx @@ -18,7 +18,7 @@ export function AccountButton({ withoutDropdown?: boolean; }) { const { t } = useTranslate('common'); - console.log('AccountButton balance:', balance); + return (
Date: Wed, 24 Dec 2025 12:51:20 +0300 Subject: [PATCH 069/141] feat: add native token balance fetching for supported chains in token balances API --- .../src/app/api/tokens/balances/route.ts | 98 ++++++++++++++++++- 1 file changed, 97 insertions(+), 1 deletion(-) diff --git a/apps/shell/src/app/api/tokens/balances/route.ts b/apps/shell/src/app/api/tokens/balances/route.ts index 48fa4b7cb..79afa117f 100644 --- a/apps/shell/src/app/api/tokens/balances/route.ts +++ b/apps/shell/src/app/api/tokens/balances/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; +import { createPublicClient, http, formatEther, Chain } from 'viem'; import { haqqMainnet, haqqTestedge2, mainnet, sepolia } from 'viem/chains'; import { haqqEthiq, haqqTestethiq } from '@haqq/shell-shared'; @@ -101,6 +102,47 @@ export async function GET(request: NextRequest) { }; }); + // Check if native token (0xeee...) is already in results + const NATIVE_TOKEN_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'; + const hasNativeToken = tokenBalances.some((token) => { + return token.address.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase(); + }); + + // If native token is not in results, fetch and add it + if (!hasNativeToken) { + try { + const chainConfigWithRpc = getChainConfigWithRpc(Number(chainId)); + if (chainConfigWithRpc) { + const client = createPublicClient({ + chain: chainConfigWithRpc.chain, + transport: http(chainConfigWithRpc.rpcUrl), + }); + + const balance = await client.getBalance({ + address: address as `0x${string}`, + }); + + const formattedBalance = parseFloat(formatEther(balance)); + + // Only add native token if balance is greater than 0 + const nativeToken: TokenBalance = { + symbol: chainConfigWithRpc.nativeSymbol, + address: NATIVE_TOKEN_ADDRESS, + name: chainConfigWithRpc.nativeName, + balance: balance.toString(), + decimals: 18, + formattedBalance, + }; + + // Add native token at the beginning of the array + tokenBalances.unshift(nativeToken); + } + } catch (error) { + console.error('Failed to fetch native token balance:', error); + // Continue without native token if fetch fails + } + } + console.log( 'Processed token balances:', tokenBalances.map((t) => { @@ -110,7 +152,7 @@ export async function GET(request: NextRequest) { return NextResponse.json({ tokens: tokenBalances, - total: filteredTokens.length, + total: tokenBalances.length, }); } catch (error) { console.error('Error in token balances API:', error); @@ -127,6 +169,11 @@ interface ChainConfig { nativeName: string; } +interface ChainConfigWithRpc extends ChainConfig { + chain: Chain; + rpcUrl: string; +} + function getChainConfig(chainId: number): ChainConfig | null { const configs: Record = { [haqqTestethiq.id]: { @@ -169,3 +216,52 @@ function getChainConfig(chainId: number): ChainConfig | null { return configs[chainId] || null; } + +function getChainConfigWithRpc(chainId: number): ChainConfigWithRpc | null { + const configs: Record = { + [haqqTestethiq.id]: { + chain: haqqTestethiq, + apiUrl: haqqTestethiq.blockExplorers.default.apiUrl, + rpcUrl: haqqTestethiq.rpcUrls.default.http[0], + nativeSymbol: 'ETH', + nativeName: 'Ethereum', + }, + [haqqEthiq.id]: { + chain: haqqEthiq, + apiUrl: haqqEthiq.blockExplorers.default.apiUrl, + rpcUrl: haqqEthiq.rpcUrls.default.http[0], + nativeSymbol: 'ETH', + nativeName: 'Ethereum', + }, + [sepolia.id]: { + chain: sepolia, + apiUrl: 'https://eth-sepolia.blockscout.com/api', + rpcUrl: sepolia.rpcUrls.default.http[0], + nativeSymbol: 'ETH', + nativeName: 'Ethereum', + }, + [haqqMainnet.id]: { + chain: haqqMainnet, + apiUrl: haqqMainnet.blockExplorers.default.apiUrl, + rpcUrl: haqqMainnet.rpcUrls.default.http[0], + nativeSymbol: 'ISLM', + nativeName: 'Islamic Coin', + }, + [haqqTestedge2.id]: { + chain: haqqTestedge2, + apiUrl: haqqTestedge2.blockExplorers.default.apiUrl, + rpcUrl: haqqTestedge2.rpcUrls.default.http[0], + nativeSymbol: 'ISLM', + nativeName: 'Islamic Coin', + }, + [mainnet.id]: { + chain: mainnet, + apiUrl: 'https://eth.blockscout.com/api', + rpcUrl: mainnet.rpcUrls.default.http[0], + nativeSymbol: 'ETH', + nativeName: 'Ethereum', + }, + }; + + return configs[chainId] || null; +} From 6d85c09f68897d773e39cd7b596ca936c45d61d9 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Wed, 24 Dec 2025 12:54:18 +0300 Subject: [PATCH 070/141] refactor: recover link --- libs/bridge/src/lib/components/recovery-link.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/bridge/src/lib/components/recovery-link.tsx b/libs/bridge/src/lib/components/recovery-link.tsx index c54cff553..5f1597241 100644 --- a/libs/bridge/src/lib/components/recovery-link.tsx +++ b/libs/bridge/src/lib/components/recovery-link.tsx @@ -22,8 +22,8 @@ export interface RecoveryLinkProps { */ export function RecoveryLink({ href = '/bridge/recovery', - title = 'Lost track of your withdrawal?', - description = 'Use transaction hash to recover and complete your withdrawal.', + title = 'Lost track of your L2 to L1 withdrawal?', + description = 'Use transaction hash to recover and complete your L2 to L1 withdrawal.', linkText = 'Recover Withdrawal', className = '', }: RecoveryLinkProps) { From 192f1f937b50a8673ec7d122239edc75fa5a79a3 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Wed, 24 Dec 2025 13:41:27 +0300 Subject: [PATCH 071/141] fix: correct precision handling in modal input to retain full decimal values --- libs/ui-kit/src/lib/modal-input.spec.ts | 4 +-- libs/ui-kit/src/lib/modal-input.tsx | 33 ++++++------------------- 2 files changed, 10 insertions(+), 27 deletions(-) diff --git a/libs/ui-kit/src/lib/modal-input.spec.ts b/libs/ui-kit/src/lib/modal-input.spec.ts index 404cdda64..2b3111809 100644 --- a/libs/ui-kit/src/lib/modal-input.spec.ts +++ b/libs/ui-kit/src/lib/modal-input.spec.ts @@ -20,7 +20,7 @@ describe('usePreparedMaskValue', () => { const { result } = renderHook(() => { return usePreparedMaskValue('1000.1234'); }); - expect(result.current.inputValue).toBe('1000.123'); + expect(result.current.inputValue).toBe('1000.1234'); }); it('should remove trailing zeros from decimal values', () => { @@ -62,6 +62,6 @@ describe('usePreparedMaskValue', () => { const { result } = renderHook(() => { return usePreparedMaskValue('1000000.12345'); }); - expect(result.current.inputValue).toBe('1000000.123'); + expect(result.current.inputValue).toBe('1000000.12345'); }); }); diff --git a/libs/ui-kit/src/lib/modal-input.tsx b/libs/ui-kit/src/lib/modal-input.tsx index 2aae85763..d64a66bf9 100644 --- a/libs/ui-kit/src/lib/modal-input.tsx +++ b/libs/ui-kit/src/lib/modal-input.tsx @@ -26,31 +26,14 @@ export const usePreparedMaskValue = ( ) => { const inputValue = useMemo(() => { if (!value && value !== 0) return undefined; - - // Convert to string, preserving precision - let stringValue: string; - if (typeof value === 'number') { - // Round to the mask's decimal limit to avoid floating-point precision issues - // This fixes cases like 1.2 being stored as 1.19999999999999995559 - // or 2222.2 being stored as 2222.1999999999998 - // We round to DEFAULT_DECIMAL_LIMIT (6) since that's what the mask allows - const rounded = - Math.round(value * 10 ** DEFAULT_DECIMAL_LIMIT) / - 10 ** DEFAULT_DECIMAL_LIMIT; - // Use toFixed with the decimal limit to avoid scientific notation - // This preserves values like 0.0001 without rounding - stringValue = rounded.toFixed(DEFAULT_DECIMAL_LIMIT); - } else if (Array.isArray(value)) { - stringValue = value.join(''); - } else { - stringValue = String(value); - } - - // Remove trailing zeros for cleaner display, but preserve the decimal point if needed - // This allows values like 0.0001 to be displayed correctly - const cleaned = stringValue.replace(/\.?0+$/, ''); - // If cleaning removed everything (e.g., value was 0), return "0" - return cleaned || '0'; + // Hack, because react-text-mask doesn't work correctly with decimals + // ex: it converts 0.0709 to 0.070 (not 0.071!) + // Additionally, remove trailing zeros and only fix to decimal limit if it has decimals + return value + ? Number(value).toString().includes('.') + ? Number(value).toFixed(DEFAULT_DECIMAL_LIMIT).replace(/0+$/, '') + : value + : undefined; }, [value]); return { From d93e68892af49b27011f06e197991e7bd6172ae6 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Thu, 25 Dec 2025 13:12:20 +0300 Subject: [PATCH 072/141] refactor: simplify token balance fetching by removing native token balance handling --- libs/bridge/src/lib/services/explorer-api.ts | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/libs/bridge/src/lib/services/explorer-api.ts b/libs/bridge/src/lib/services/explorer-api.ts index 5bde1f604..9e60abba3 100644 --- a/libs/bridge/src/lib/services/explorer-api.ts +++ b/libs/bridge/src/lib/services/explorer-api.ts @@ -118,25 +118,12 @@ export async function fetchAllTokenBalances( `Fetching all token balances for address ${address} on chain ${chainId}`, ); - const [erc20Tokens, nativeToken] = await Promise.all([ - fetchUserTokenBalances(address, chainId), - fetchNativeTokenBalance(address, chainId), - ]); + const erc20Tokens = await fetchUserTokenBalances(address, chainId); console.log(`ERC-20 tokens found: ${erc20Tokens.length}`); - console.log(`Native token found: ${nativeToken ? 'Yes' : 'No'}`); const allTokens = [...erc20Tokens]; - if (nativeToken && nativeToken.formattedBalance > 0) { - console.log( - `Adding native token with balance: ${nativeToken.formattedBalance} ${nativeToken.symbol}`, - ); - allTokens.unshift(nativeToken); - } else if (nativeToken) { - console.log(`Native token has zero balance, not adding to list`); - } - console.log(`Total tokens to return: ${allTokens.length}`); return allTokens; } catch (error) { From b23675f7b621393a2bf71f1139df10c9b5b5af03 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Fri, 26 Dec 2025 16:23:28 +0300 Subject: [PATCH 073/141] feat: fetch txs from explorer --- libs/bridge/src/lib/bridge-page.tsx | 56 +++++++- .../lib/components/withdrawal-order-card.tsx | 4 - .../src/lib/hooks/use-withdrawal-orders.ts | 77 +++++++++- .../src/lib/hooks/use-withdrawal-recovery.ts | 5 +- libs/bridge/src/lib/services/explorer-api.ts | 135 +++++++++++++++++- 5 files changed, 266 insertions(+), 11 deletions(-) diff --git a/libs/bridge/src/lib/bridge-page.tsx b/libs/bridge/src/lib/bridge-page.tsx index b24f44fdb..aeb7e3534 100644 --- a/libs/bridge/src/lib/bridge-page.tsx +++ b/libs/bridge/src/lib/bridge-page.tsx @@ -1,5 +1,5 @@ 'use client'; -import { useState, useLayoutEffect, useEffect } from 'react'; +import { useState, useLayoutEffect, useEffect, useRef } from 'react'; import { useTranslate } from '@tolgee/react'; import { sepolia } from 'viem/chains'; import { useSwitchChain } from 'wagmi'; @@ -34,6 +34,7 @@ import { useBridgeValidation, useBridgeHandlers, useBridgeTransactionReceipt, + useWithdrawalRecovery, } from './hooks'; // SUPPORTED_CHAINS is now imported from @haqq/shell-shared @@ -62,7 +63,21 @@ export function BridgePage() { const { updateUrlState, buildDeploymentUrl, urlState } = useBridgeUrlState(); // Withdrawal orders management - const { pendingOrders } = useWithdrawalOrders(); + const { pendingOrders, orders, syncWithdrawalsFromExplorer } = + useWithdrawalOrders(); + + // Withdrawal recovery for syncing withdrawals with full details + const { recoverWithdrawal } = useWithdrawalRecovery(); + + // Track if sync is in progress to prevent infinite loops + const isSyncingRef = useRef(false); + const lastSyncRef = useRef<{ address: string; chainId: number } | null>(null); + + // Store recoverWithdrawal in ref to prevent dependency changes + const recoverWithdrawalRef = useRef(recoverWithdrawal); + useEffect(() => { + recoverWithdrawalRef.current = recoverWithdrawal; + }, [recoverWithdrawal]); // Bridge state hook - handles token selection and amounts const { @@ -153,6 +168,43 @@ export function BridgePage() { setTxHash(null); }, [chain?.id, bridgeAmount, selectedToken?.address, setTxHash]); + // Sync withdrawals from explorer API when connected and on L2 + useEffect(() => { + if ( + isConnected && + address && + isL2ToL1 && + !isSyncingRef.current && + chain?.id && + orders + ) { + // Check if we've already synced for this address/chain combination + const lastSync = lastSyncRef.current; + if ( + lastSync && + lastSync.address === address && + lastSync.chainId === chain.id + ) { + return; // Already synced for this address/chain + } + + isSyncingRef.current = true; + syncWithdrawalsFromExplorer(address, recoverWithdrawalRef.current) + .then(() => { + lastSyncRef.current = { address, chainId: chain.id }; + }) + .catch((error) => { + console.error('Failed to sync withdrawals from explorer:', error); + }) + .finally(() => { + // Add a small delay before allowing next sync to prevent rapid re-syncing + setTimeout(() => { + isSyncingRef.current = false; + }, 1000); + }); + } + }, [isConnected, address, isL2ToL1, chain?.id, syncWithdrawalsFromExplorer]); + // Bridge transaction hook const { bridgeTokens, isProcessing, isProving, isFinalizing } = useBridgeTransaction({ diff --git a/libs/bridge/src/lib/components/withdrawal-order-card.tsx b/libs/bridge/src/lib/components/withdrawal-order-card.tsx index c652ed760..0c136b0cd 100644 --- a/libs/bridge/src/lib/components/withdrawal-order-card.tsx +++ b/libs/bridge/src/lib/components/withdrawal-order-card.tsx @@ -332,10 +332,6 @@ export function WithdrawalOrderCard({ order }: WithdrawalOrderCardProps) { {order.toAddress.slice(0, 6)}...{order.toAddress.slice(-4)}
-
- Initiated:{' '} - {formatDate(order.createdAt)} -
{order.proveHash && (
diff --git a/libs/bridge/src/lib/hooks/use-withdrawal-orders.ts b/libs/bridge/src/lib/hooks/use-withdrawal-orders.ts index 4174539fa..bb6fee9d0 100644 --- a/libs/bridge/src/lib/hooks/use-withdrawal-orders.ts +++ b/libs/bridge/src/lib/hooks/use-withdrawal-orders.ts @@ -5,6 +5,7 @@ import { useLocalStorage } from 'usehooks-ts'; import { useAccount, useChainId } from 'wagmi'; import { useWithdrawalTimers } from './use-withdrawal-timers'; import { getOpStackChains } from '../constants/op-stack-config'; +import { fetchWithdrawals } from '../services/explorer-api'; import { WithdrawalOrder, WithdrawalOrderStorage, @@ -37,9 +38,9 @@ export function useWithdrawalOrders() { // Get all orders const orders = useMemo(() => { return storage.orders.filter((order) => { - return order.targetChainId === chainId; + return +order.targetChainId === +getOpStackChains(chainId).L1.id; }); - }, [storage.orders]); + }, [storage.orders, chainId]); const opChainId = useMemo(() => { return getOpStackChains(chainId).L1.id; @@ -189,6 +190,77 @@ export function useWithdrawalOrders() { [setStorage], ); + // Sync withdrawals from explorer API + const syncWithdrawalsFromExplorer = useCallback( + async ( + userAddress: string, + recoverWithdrawal: (txHash: string) => Promise, + ) => { + const addressToSync = userAddress || address; + if (!addressToSync) { + console.warn('No address provided for syncing withdrawals'); + return; + } + + try { + console.log( + `Syncing withdrawals from explorer for address: ${addressToSync}`, + ); + const response = await fetchWithdrawals(addressToSync); + + // Track withdrawals that need recovery + const withdrawalsToRecover: string[] = []; + + // Update existing orders or create new ones based on explorer data + for (const explorerWithdrawal of response.items) { + const order = orders.find((order) => { + return ( + order.initiateHash.toLowerCase() === + explorerWithdrawal.l2_transaction_hash.toLowerCase() + ); + }); + if (order) { + continue; + } + // Check if this withdrawal should be recovered + const belongsToUser = + explorerWithdrawal.from.hash.toLowerCase() === + addressToSync.toLowerCase(); + const hasL2Hash = Boolean(explorerWithdrawal.l2_transaction_hash); + const hasNoL1Hash = !explorerWithdrawal.l1_transaction_hash; + + if (belongsToUser && hasL2Hash && hasNoL1Hash) { + // This withdrawal should be recovered to get full details + withdrawalsToRecover.push(explorerWithdrawal.l2_transaction_hash); + } + } + + // Recover withdrawals that need full details + if (withdrawalsToRecover.length > 0 && recoverWithdrawal) { + console.log( + `Recovering ${withdrawalsToRecover.length} withdrawals with full details`, + ); + // Recover withdrawals in parallel, but limit concurrency + const recoveryPromises = withdrawalsToRecover.map((txHash) => { + return recoverWithdrawal(txHash).catch((error) => { + console.error(`Failed to recover withdrawal ${txHash}:`, error); + return null; + }); + }); + await Promise.all(recoveryPromises); + } + + console.log( + `Synced ${response.items.length} withdrawals from explorer API`, + ); + } catch (error) { + console.error('Failed to sync withdrawals from explorer:', error); + throw error; + } + }, + [address, chainId, setStorage, orders], + ); + return { orders, pendingOrders, @@ -199,5 +271,6 @@ export function useWithdrawalOrders() { getOrderByInitiateHash, updateOrderTimers, getOrderWarning, + syncWithdrawalsFromExplorer, }; } diff --git a/libs/bridge/src/lib/hooks/use-withdrawal-recovery.ts b/libs/bridge/src/lib/hooks/use-withdrawal-recovery.ts index b663b3ea1..b4e7fe09b 100644 --- a/libs/bridge/src/lib/hooks/use-withdrawal-recovery.ts +++ b/libs/bridge/src/lib/hooks/use-withdrawal-recovery.ts @@ -2,7 +2,7 @@ import { useCallback, useState, useEffect } from 'react'; import { useLocalStorage } from 'usehooks-ts'; -import { decodeFunctionData, formatUnits } from 'viem'; +import { Address, decodeFunctionData, formatUnits, Hash } from 'viem'; import { getWithdrawals } from 'viem/op-stack'; import { L2StandardBridgeAbi, @@ -235,6 +235,9 @@ export function useWithdrawalRecovery(): UseWithdrawalRecoveryReturn { await publicClientReadonlyL1.getWithdrawalStatus({ receipt, targetChain: chains.L2_WITH_CONTRACTS, + gameLimit: 50, + sender: transaction.from as Address as any, + withdrawalHash: withdrawal.withdrawalHash as any, }); // Map viem status to our WithdrawalStatus enum diff --git a/libs/bridge/src/lib/services/explorer-api.ts b/libs/bridge/src/lib/services/explorer-api.ts index 9e60abba3..2470e101f 100644 --- a/libs/bridge/src/lib/services/explorer-api.ts +++ b/libs/bridge/src/lib/services/explorer-api.ts @@ -1,8 +1,11 @@ /** - * HAQQ Explorer API service for fetching user token balances - * Uses Next.js API routes as proxy to avoid CORS issues + * HAQQ Explorer API service for fetching user token balances and withdrawals + * Uses Next.js API routes as proxy to avoid CORS issues for token balances + * Direct API calls for withdrawals endpoint */ +import { WithdrawalStatus } from '../types/withdrawal-order'; + // These interfaces are no longer needed since we're using the proxy API // but keeping them for potential future use @@ -15,6 +18,44 @@ export interface TokenBalance { formattedBalance: number; // Balance in human-readable format } +/** + * Explorer API withdrawal response types + */ +export interface ExplorerWithdrawalAddress { + ens_domain_name: string | null; + hash: string; + implementations: unknown[]; + is_contract: boolean; + is_scam: boolean; + is_verified: boolean; + metadata: unknown | null; + name: string | null; + private_tags: unknown[]; + proxy_type: string | null; + public_tags: unknown[]; + watchlist_names: unknown[]; +} + +export interface ExplorerWithdrawal { + challenge_period_end: string | null; + from: ExplorerWithdrawalAddress; + l1_transaction_hash: string | null; + l2_timestamp: string; + l2_transaction_hash: string; + msg_nonce: number; + msg_nonce_raw: string; + msg_nonce_version: number; + status: string; +} + +export interface ExplorerWithdrawalsResponse { + items: ExplorerWithdrawal[]; + next_page_params: { + index?: number; + items_count?: number; + } | null; +} + /** * Makes a request to our Next.js API proxy */ @@ -131,3 +172,93 @@ export async function fetchAllTokenBalances( throw error; } } + +/** + * Maps explorer API status string to WithdrawalStatus enum + * @param explorerStatus - Status string from explorer API + * @returns Mapped WithdrawalStatus + */ +export function mapExplorerStatusToWithdrawalStatus( + explorerStatus: string, +): WithdrawalStatus { + const statusLower = explorerStatus.toLowerCase(); + + // Map common explorer status strings to our status enum + if (statusLower.includes('waiting') || statusLower.includes('game')) { + // "Waiting a game to resolve" or similar - withdrawal is initiated but not yet proved + return WithdrawalStatus.INITIATED; + } + if (statusLower.includes('proving') || statusLower.includes('challenge')) { + return WithdrawalStatus.PROVING; + } + if (statusLower.includes('proved') || statusLower.includes('ready')) { + return WithdrawalStatus.PROVED; + } + if (statusLower.includes('finalizing')) { + return WithdrawalStatus.FINALIZING; + } + if (statusLower.includes('finalized') || statusLower.includes('completed')) { + return WithdrawalStatus.FINALIZED; + } + if (statusLower.includes('failed') || statusLower.includes('error')) { + return WithdrawalStatus.FAILED; + } + + // Default to initiated if status is unknown + console.warn( + `Unknown explorer status: ${explorerStatus}, defaulting to INITIATED`, + ); + return WithdrawalStatus.INITIATED; +} + +/** + * Fetches withdrawals from the explorer API + * @param address - User address to fetch withdrawals for + * @param page - Optional page parameter for pagination + * @returns Withdrawals response from explorer API + */ +export async function fetchWithdrawals( + address: string, + page?: { index?: number; items_count?: number }, +): Promise { + try { + const baseUrl = + 'https://explorer.ethiq.network/api/v2/optimism/withdrawals'; + const url = new URL(baseUrl); + + // Add address filter if provided + url.searchParams.set('address', address); + + // Add pagination params if provided + if (page?.index !== undefined) { + url.searchParams.set('index', page.index.toString()); + } + if (page?.items_count !== undefined) { + url.searchParams.set('items_count', page.items_count.toString()); + } + + console.log(`Fetching withdrawals from explorer API: ${url.toString()}`); + + const response = await fetch(url.toString(), { + headers: { + Accept: 'application/json', + }, + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => { + return {}; + }); + throw new Error( + errorData.error || `HTTP error! status: ${response.status}`, + ); + } + + const data: ExplorerWithdrawalsResponse = await response.json(); + console.log(`Found ${data.items.length} withdrawals from explorer API`); + return data; + } catch (error) { + console.error('Failed to fetch withdrawals from explorer API:', error); + throw error; + } +} From 0bd717092a0f203109a9bc5c665d9263008a3170 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Fri, 26 Dec 2025 16:29:07 +0300 Subject: [PATCH 074/141] feat: implement pagination for fetching all withdrawal orders from explorer API --- .../src/lib/hooks/use-withdrawal-orders.ts | 6 +- libs/bridge/src/lib/services/explorer-api.ts | 62 +++++++++++++++++-- 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/libs/bridge/src/lib/hooks/use-withdrawal-orders.ts b/libs/bridge/src/lib/hooks/use-withdrawal-orders.ts index bb6fee9d0..8b791b614 100644 --- a/libs/bridge/src/lib/hooks/use-withdrawal-orders.ts +++ b/libs/bridge/src/lib/hooks/use-withdrawal-orders.ts @@ -206,13 +206,13 @@ export function useWithdrawalOrders() { console.log( `Syncing withdrawals from explorer for address: ${addressToSync}`, ); - const response = await fetchWithdrawals(addressToSync); + const allWithdrawals = await fetchWithdrawals(addressToSync); // Track withdrawals that need recovery const withdrawalsToRecover: string[] = []; // Update existing orders or create new ones based on explorer data - for (const explorerWithdrawal of response.items) { + for (const explorerWithdrawal of allWithdrawals) { const order = orders.find((order) => { return ( order.initiateHash.toLowerCase() === @@ -251,7 +251,7 @@ export function useWithdrawalOrders() { } console.log( - `Synced ${response.items.length} withdrawals from explorer API`, + `Synced ${allWithdrawals.length} withdrawals from explorer API`, ); } catch (error) { console.error('Failed to sync withdrawals from explorer:', error); diff --git a/libs/bridge/src/lib/services/explorer-api.ts b/libs/bridge/src/lib/services/explorer-api.ts index 2470e101f..cd29bf90c 100644 --- a/libs/bridge/src/lib/services/explorer-api.ts +++ b/libs/bridge/src/lib/services/explorer-api.ts @@ -212,12 +212,12 @@ export function mapExplorerStatusToWithdrawalStatus( } /** - * Fetches withdrawals from the explorer API + * Fetches a single page of withdrawals from the explorer API * @param address - User address to fetch withdrawals for * @param page - Optional page parameter for pagination * @returns Withdrawals response from explorer API */ -export async function fetchWithdrawals( +export async function fetchWithdrawalsPage( address: string, page?: { index?: number; items_count?: number }, ): Promise { @@ -237,7 +237,9 @@ export async function fetchWithdrawals( url.searchParams.set('items_count', page.items_count.toString()); } - console.log(`Fetching withdrawals from explorer API: ${url.toString()}`); + console.log( + `Fetching withdrawals page from explorer API: ${url.toString()}`, + ); const response = await fetch(url.toString(), { headers: { @@ -255,10 +257,62 @@ export async function fetchWithdrawals( } const data: ExplorerWithdrawalsResponse = await response.json(); - console.log(`Found ${data.items.length} withdrawals from explorer API`); + console.log(`Found ${data.items.length} withdrawals in this page`); return data; } catch (error) { console.error('Failed to fetch withdrawals from explorer API:', error); throw error; } } + +/** + * Fetches all withdrawals from the explorer API (all pages) + * @param address - User address to fetch withdrawals for + * @returns All withdrawals from all pages + */ +export async function fetchWithdrawals( + address: string, +): Promise { + try { + const allWithdrawals: ExplorerWithdrawal[] = []; + let nextPageParams: { index?: number; items_count?: number } | null = null; + let pageNumber = 0; + + do { + pageNumber++; + console.log( + `Fetching withdrawals page ${pageNumber} for address: ${address}`, + ); + + const response = await fetchWithdrawalsPage( + address, + nextPageParams || undefined, + ); + + // Add items from this page to the collection + allWithdrawals.push(...response.items); + + // Check if there's a next page + nextPageParams = response.next_page_params; + + console.log( + `Page ${pageNumber}: Found ${response.items.length} withdrawals. Total so far: ${allWithdrawals.length}`, + ); + + // Safety limit to prevent infinite loops + if (pageNumber > 100) { + console.warn('Reached maximum page limit (100), stopping pagination'); + break; + } + } while (nextPageParams !== null); + + console.log( + `Fetched all withdrawals: ${allWithdrawals.length} total across ${pageNumber} page(s)`, + ); + + return allWithdrawals; + } catch (error) { + console.error('Failed to fetch all withdrawals from explorer API:', error); + throw error; + } +} From 6c172b271ba8b45d1be0c2697abd44498b9f98d8 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Sat, 27 Dec 2025 19:16:39 +0300 Subject: [PATCH 075/141] fix: testethiq withdrawals --- .../src/lib/hooks/use-withdrawal-orders.ts | 4 +- libs/bridge/src/lib/services/explorer-api.ts | 67 ++++++++----------- 2 files changed, 30 insertions(+), 41 deletions(-) diff --git a/libs/bridge/src/lib/hooks/use-withdrawal-orders.ts b/libs/bridge/src/lib/hooks/use-withdrawal-orders.ts index 8b791b614..5ff12b4f8 100644 --- a/libs/bridge/src/lib/hooks/use-withdrawal-orders.ts +++ b/libs/bridge/src/lib/hooks/use-withdrawal-orders.ts @@ -206,7 +206,9 @@ export function useWithdrawalOrders() { console.log( `Syncing withdrawals from explorer for address: ${addressToSync}`, ); - const allWithdrawals = await fetchWithdrawals(addressToSync); + // Get L2 chain ID to determine which explorer to use + const l2ChainId = getOpStackChains(chainId).L2.id; + const allWithdrawals = await fetchWithdrawals(addressToSync, l2ChainId); // Track withdrawals that need recovery const withdrawalsToRecover: string[] = []; diff --git a/libs/bridge/src/lib/services/explorer-api.ts b/libs/bridge/src/lib/services/explorer-api.ts index cd29bf90c..22f47dc02 100644 --- a/libs/bridge/src/lib/services/explorer-api.ts +++ b/libs/bridge/src/lib/services/explorer-api.ts @@ -4,6 +4,7 @@ * Direct API calls for withdrawals endpoint */ +import { haqqEthiq, haqqTestethiq } from '@haqq/shell-shared'; import { WithdrawalStatus } from '../types/withdrawal-order'; // These interfaces are no longer needed since we're using the proxy API @@ -211,19 +212,35 @@ export function mapExplorerStatusToWithdrawalStatus( return WithdrawalStatus.INITIATED; } +/** + * Gets the explorer API base URL based on chain ID + * @param chainId - Chain ID to determine which explorer to use + * @returns Base URL for the explorer API + */ +function getExplorerApiUrl(chainId: number): string { + // Check if it's testnet (testethiq) + if (chainId === haqqTestethiq.id) { + return 'https://explorer.testnet.ethiq.network/api/v2/optimism/withdrawals'; + } + + // Default to mainnet (ethiq) + return 'https://explorer.ethiq.network/api/v2/optimism/withdrawals'; +} + /** * Fetches a single page of withdrawals from the explorer API * @param address - User address to fetch withdrawals for + * @param chainId - Chain ID to determine which explorer to use * @param page - Optional page parameter for pagination * @returns Withdrawals response from explorer API */ export async function fetchWithdrawalsPage( address: string, + chainId: number, page?: { index?: number; items_count?: number }, ): Promise { try { - const baseUrl = - 'https://explorer.ethiq.network/api/v2/optimism/withdrawals'; + const baseUrl = getExplorerApiUrl(chainId); const url = new URL(baseUrl); // Add address filter if provided @@ -266,53 +283,23 @@ export async function fetchWithdrawalsPage( } /** - * Fetches all withdrawals from the explorer API (all pages) + * Fetches withdrawals from the explorer API (single page) * @param address - User address to fetch withdrawals for - * @returns All withdrawals from all pages + * @param chainId - Chain ID to determine which explorer to use + * @returns Withdrawals from the first page */ export async function fetchWithdrawals( address: string, + chainId: number, ): Promise { try { - const allWithdrawals: ExplorerWithdrawal[] = []; - let nextPageParams: { index?: number; items_count?: number } | null = null; - let pageNumber = 0; - - do { - pageNumber++; - console.log( - `Fetching withdrawals page ${pageNumber} for address: ${address}`, - ); - - const response = await fetchWithdrawalsPage( - address, - nextPageParams || undefined, - ); - - // Add items from this page to the collection - allWithdrawals.push(...response.items); - - // Check if there's a next page - nextPageParams = response.next_page_params; - - console.log( - `Page ${pageNumber}: Found ${response.items.length} withdrawals. Total so far: ${allWithdrawals.length}`, - ); - - // Safety limit to prevent infinite loops - if (pageNumber > 100) { - console.warn('Reached maximum page limit (100), stopping pagination'); - break; - } - } while (nextPageParams !== null); - + const response = await fetchWithdrawalsPage(address, chainId); console.log( - `Fetched all withdrawals: ${allWithdrawals.length} total across ${pageNumber} page(s)`, + `Fetched ${response.items.length} withdrawals from explorer API`, ); - - return allWithdrawals; + return response.items; } catch (error) { - console.error('Failed to fetch all withdrawals from explorer API:', error); + console.error('Failed to fetch withdrawals from explorer API:', error); throw error; } } From 96a61d07cc4ded811b4347262a4875ad5e8e51c9 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Tue, 30 Dec 2025 12:51:19 +0300 Subject: [PATCH 076/141] chore: savecommit --- .../src/lib/hooks/use-op-stack-clients.ts | 83 +++++++------------ 1 file changed, 28 insertions(+), 55 deletions(-) diff --git a/libs/bridge/src/lib/hooks/use-op-stack-clients.ts b/libs/bridge/src/lib/hooks/use-op-stack-clients.ts index 4de4bf408..e721f9e38 100644 --- a/libs/bridge/src/lib/hooks/use-op-stack-clients.ts +++ b/libs/bridge/src/lib/hooks/use-op-stack-clients.ts @@ -14,10 +14,13 @@ import { getOpStackChains } from '../constants/op-stack-config'; /** * Hook to create and manage OP Stack compatible clients for L1 and L2 chains * Provides both public and wallet clients with proper OP Stack extensions + * + * Note: Wallet clients are extended from wagmi's wallet client to ensure proper + * signing compatibility with all wallets (MetaMask, Kepler, etc.) */ export function useOpStackClients() { const { address, chain } = useAccount(); - const { data: walletClient } = useWalletClient(); + const { data: wagmiWalletClient } = useWalletClient(); const opChainL1 = useMemo(() => { return getOpStackChains(chain?.id).L1; @@ -68,84 +71,54 @@ export function useOpStackClients() { }, [opChainL2]); // Create L1 wallet client (Sepolia) with OP Stack contracts + // Extended from wagmi's wallet client to ensure proper signing with all wallets const walletClientL1 = useMemo(() => { - return walletClient && address - ? createWalletClient({ - account: address as `0x${string}`, - chain: opChainL1, - transport: - typeof window !== 'undefined' && window.ethereum - ? custom(window.ethereum) - : http(opChainL1.rpcUrls.default.http[0]), - }).extend(walletActionsL1()) - : null; - }, [walletClient, address, opChainL1]); - - const getWalletClientL1 = useCallback(async () => { - if (walletClientL1) { - return walletClientL1; + if (!wagmiWalletClient || !address) { + return null; } + // Extend wagmi's wallet client with OP Stack L1 actions + // This preserves the proper signing capabilities from the wallet connector + return wagmiWalletClient.extend(walletActionsL1()); + }, [wagmiWalletClient, address]); - if (address && typeof window !== 'undefined' && window.ethereum) { - return createWalletClient({ - account: address as `0x${string}`, - chain: opChainL1, - transport: custom(window.ethereum), - }).extend(walletActionsL1()); - } - - return null; - }, [address, walletClientL1, opChainL1]); + const getWalletClientL1 = useCallback(async () => { + return walletClientL1; + }, [walletClientL1]); const walletClientReadonlyL1 = useMemo(() => { - return walletClient && address + return wagmiWalletClient && address ? createWalletClient({ account: address as `0x${string}`, chain: opChainL1, transport: http(opChainL1.rpcUrls.default.http[0]), }).extend(walletActionsL1()) : null; - }, [walletClient, address, opChainL1]); + }, [wagmiWalletClient, address, opChainL1]); // Create L2 wallet client (HAQQ Devnet) with OP Stack contracts + // Extended from wagmi's wallet client to ensure proper signing with all wallets const walletClientL2 = useMemo(() => { - return walletClient && address - ? createWalletClient({ - account: address as `0x${string}`, - chain: opChainL2, - transport: - typeof window !== 'undefined' && window.ethereum - ? custom(window.ethereum) - : http(opChainL2.rpcUrls.default.http[0]), - }).extend(walletActionsL2()) - : null; - }, [walletClient, address, opChainL2]); - - const getWalletClientL2 = useCallback(async () => { - if (walletClientL2) { - return walletClientL2; + if (!wagmiWalletClient || !address) { + return null; } + // Extend wagmi's wallet client with OP Stack L2 actions + // This preserves the proper signing capabilities from the wallet connector + return wagmiWalletClient.extend(walletActionsL2()); + }, [wagmiWalletClient, address]); - if (address && typeof window !== 'undefined' && window.ethereum) { - return createWalletClient({ - account: address as `0x${string}`, - chain: opChainL2, - transport: custom(window.ethereum), - }).extend(walletActionsL2()); - } - - return null; - }, [address, walletClientL2, opChainL2]); + const getWalletClientL2 = useCallback(async () => { + return walletClientL2; + }, [walletClientL2]); const walletClientReadonlyL2 = useMemo(() => { - return walletClient && address + return wagmiWalletClient && address ? createWalletClient({ account: address as `0x${string}`, chain: opChainL2, transport: http(opChainL2.rpcUrls.default.http[0]), }).extend(walletActionsL2()) : null; - }, [walletClient, address, opChainL2]); + }, [wagmiWalletClient, address, opChainL2]); const chains = useMemo(() => { return getOpStackChains(chain?.id); From a0d2060bf6fecdc18c041989fecd70a82d98b938 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Tue, 30 Dec 2025 12:56:52 +0300 Subject: [PATCH 077/141] Revert "chore: savecommit" This reverts commit 96a61d07cc4ded811b4347262a4875ad5e8e51c9. --- .../src/lib/hooks/use-op-stack-clients.ts | 83 ++++++++++++------- 1 file changed, 55 insertions(+), 28 deletions(-) diff --git a/libs/bridge/src/lib/hooks/use-op-stack-clients.ts b/libs/bridge/src/lib/hooks/use-op-stack-clients.ts index e721f9e38..4de4bf408 100644 --- a/libs/bridge/src/lib/hooks/use-op-stack-clients.ts +++ b/libs/bridge/src/lib/hooks/use-op-stack-clients.ts @@ -14,13 +14,10 @@ import { getOpStackChains } from '../constants/op-stack-config'; /** * Hook to create and manage OP Stack compatible clients for L1 and L2 chains * Provides both public and wallet clients with proper OP Stack extensions - * - * Note: Wallet clients are extended from wagmi's wallet client to ensure proper - * signing compatibility with all wallets (MetaMask, Kepler, etc.) */ export function useOpStackClients() { const { address, chain } = useAccount(); - const { data: wagmiWalletClient } = useWalletClient(); + const { data: walletClient } = useWalletClient(); const opChainL1 = useMemo(() => { return getOpStackChains(chain?.id).L1; @@ -71,54 +68,84 @@ export function useOpStackClients() { }, [opChainL2]); // Create L1 wallet client (Sepolia) with OP Stack contracts - // Extended from wagmi's wallet client to ensure proper signing with all wallets const walletClientL1 = useMemo(() => { - if (!wagmiWalletClient || !address) { - return null; - } - // Extend wagmi's wallet client with OP Stack L1 actions - // This preserves the proper signing capabilities from the wallet connector - return wagmiWalletClient.extend(walletActionsL1()); - }, [wagmiWalletClient, address]); + return walletClient && address + ? createWalletClient({ + account: address as `0x${string}`, + chain: opChainL1, + transport: + typeof window !== 'undefined' && window.ethereum + ? custom(window.ethereum) + : http(opChainL1.rpcUrls.default.http[0]), + }).extend(walletActionsL1()) + : null; + }, [walletClient, address, opChainL1]); const getWalletClientL1 = useCallback(async () => { - return walletClientL1; - }, [walletClientL1]); + if (walletClientL1) { + return walletClientL1; + } + + if (address && typeof window !== 'undefined' && window.ethereum) { + return createWalletClient({ + account: address as `0x${string}`, + chain: opChainL1, + transport: custom(window.ethereum), + }).extend(walletActionsL1()); + } + + return null; + }, [address, walletClientL1, opChainL1]); const walletClientReadonlyL1 = useMemo(() => { - return wagmiWalletClient && address + return walletClient && address ? createWalletClient({ account: address as `0x${string}`, chain: opChainL1, transport: http(opChainL1.rpcUrls.default.http[0]), }).extend(walletActionsL1()) : null; - }, [wagmiWalletClient, address, opChainL1]); + }, [walletClient, address, opChainL1]); // Create L2 wallet client (HAQQ Devnet) with OP Stack contracts - // Extended from wagmi's wallet client to ensure proper signing with all wallets const walletClientL2 = useMemo(() => { - if (!wagmiWalletClient || !address) { - return null; - } - // Extend wagmi's wallet client with OP Stack L2 actions - // This preserves the proper signing capabilities from the wallet connector - return wagmiWalletClient.extend(walletActionsL2()); - }, [wagmiWalletClient, address]); + return walletClient && address + ? createWalletClient({ + account: address as `0x${string}`, + chain: opChainL2, + transport: + typeof window !== 'undefined' && window.ethereum + ? custom(window.ethereum) + : http(opChainL2.rpcUrls.default.http[0]), + }).extend(walletActionsL2()) + : null; + }, [walletClient, address, opChainL2]); const getWalletClientL2 = useCallback(async () => { - return walletClientL2; - }, [walletClientL2]); + if (walletClientL2) { + return walletClientL2; + } + + if (address && typeof window !== 'undefined' && window.ethereum) { + return createWalletClient({ + account: address as `0x${string}`, + chain: opChainL2, + transport: custom(window.ethereum), + }).extend(walletActionsL2()); + } + + return null; + }, [address, walletClientL2, opChainL2]); const walletClientReadonlyL2 = useMemo(() => { - return wagmiWalletClient && address + return walletClient && address ? createWalletClient({ account: address as `0x${string}`, chain: opChainL2, transport: http(opChainL2.rpcUrls.default.http[0]), }).extend(walletActionsL2()) : null; - }, [wagmiWalletClient, address, opChainL2]); + }, [walletClient, address, opChainL2]); const chains = useMemo(() => { return getOpStackChains(chain?.id); From be84445a7ae91f9dac2f6ebb20f46a66f0a6a028 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Tue, 30 Dec 2025 12:58:12 +0300 Subject: [PATCH 078/141] feat: enhance L2 to L1 withdrawal hook with contract writing capabilities --- .../src/lib/hooks/use-l2-to-l1-withdrawal.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/libs/bridge/src/lib/hooks/use-l2-to-l1-withdrawal.ts b/libs/bridge/src/lib/hooks/use-l2-to-l1-withdrawal.ts index 877e04be4..03bacf0ab 100644 --- a/libs/bridge/src/lib/hooks/use-l2-to-l1-withdrawal.ts +++ b/libs/bridge/src/lib/hooks/use-l2-to-l1-withdrawal.ts @@ -3,7 +3,7 @@ import { useCallback, useState } from 'react'; import { parseEther, parseUnits } from 'viem'; import { getWithdrawals } from 'viem/op-stack'; -import { useAccount, useSwitchChain } from 'wagmi'; +import { useAccount, useSwitchChain, useWriteContract } from 'wagmi'; import { useToast, L2StandardBridgeAbi, @@ -77,7 +77,6 @@ export function useL2ToL1Withdrawal({ const { getTimeToProve, getTimeToFinalize, getWaitingTimeWarning } = useWithdrawalTimers(); const { - publicClientL1, getWalletClientL1, getWalletClientL2, chains, @@ -85,8 +84,9 @@ export function useL2ToL1Withdrawal({ publicClientReadonlyL2, } = useOpStackClients(); - const { isConnected } = useAccount(); + const { isConnected, address } = useAccount(); const toast = useToast(); + const { writeContractAsync } = useWriteContract(); const initiateWithdrawal = useCallback( async (amount: number, toAddress: string): Promise => { @@ -169,9 +169,7 @@ export function useL2ToL1Withdrawal({ tokenDecimals = 18, tokenSymbolOverride?: string, ): Promise => { - const walletClient = await getWalletClientL2(); - - if (!walletClient) { + if (!address || !writeContractAsync) { throw new Error('Wallet not connected or wallet client not available'); } @@ -184,7 +182,8 @@ export function useL2ToL1Withdrawal({ // Step 2: Call withdrawTo on L2StandardBridge contract // According to Optimism docs: withdrawTo initiates ERC20 withdrawal from L2 to L1 - const hash = await walletClient.writeContract({ + // Using wagmi's writeContractAsync for proper wallet signing compatibility (works with Kepler, MetaMask, etc.) + const hash = await writeContractAsync({ address: L2_STANDARD_BRIDGE_ADDRESS as `0x${string}`, abi: L2StandardBridgeAbi, functionName: 'withdrawTo', @@ -208,7 +207,7 @@ export function useL2ToL1Withdrawal({ addWithdrawalOrder({ amount, toAddress, - fromAddress: walletClient.account.address, + fromAddress: address, initiateHash: hash, status: WithdrawalStatus.INITIATED, sourceChainId: chains.L2.id, @@ -233,7 +232,8 @@ export function useL2ToL1Withdrawal({ } }, [ - getWalletClientL2, + address, + writeContractAsync, publicClientReadonlyL2, chains, addWithdrawalOrder, From e76826ebfdc59410aedec9b6a8c89f33fa600b8b Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Tue, 30 Dec 2025 17:53:13 +0300 Subject: [PATCH 079/141] fix: correct testethiq bridge addresses --- libs/bridge/src/lib/bridge-page.tsx | 10 +++++----- libs/shared/src/utils/bridge.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/libs/bridge/src/lib/bridge-page.tsx b/libs/bridge/src/lib/bridge-page.tsx index aeb7e3534..6c7c55afd 100644 --- a/libs/bridge/src/lib/bridge-page.tsx +++ b/libs/bridge/src/lib/bridge-page.tsx @@ -4,7 +4,7 @@ import { useTranslate } from '@tolgee/react'; import { sepolia } from 'viem/chains'; import { useSwitchChain } from 'wagmi'; import { - L1_STANDARD_BRIDGE_ADDRESS, + L1_STANDARD_MAINNET_BRIDGE_ADDRESS, CHAIN_CONFIG, L2_STANDARD_BRIDGE_ADDRESS, bridgeSupportedChains, @@ -42,13 +42,13 @@ import { export const useChainProxyAddress = (chainId: number | undefined) => { if ( chainId === CHAIN_CONFIG.l1ChainId || - chainId === CHAIN_CONFIG.l1TestChainId + chainId === CHAIN_CONFIG.l2ChainId ) { - return L1_STANDARD_BRIDGE_ADDRESS; + return L1_STANDARD_MAINNET_BRIDGE_ADDRESS; } if ( - chainId === CHAIN_CONFIG.l2ChainId || - chainId === CHAIN_CONFIG.l2TestChainId + chainId === CHAIN_CONFIG.l2TestChainId || + chainId === CHAIN_CONFIG.l1TestChainId ) { return L2_STANDARD_BRIDGE_ADDRESS; } diff --git a/libs/shared/src/utils/bridge.ts b/libs/shared/src/utils/bridge.ts index e79cfaac3..a83f690e3 100644 --- a/libs/shared/src/utils/bridge.ts +++ b/libs/shared/src/utils/bridge.ts @@ -53,7 +53,7 @@ export const ETHIQ_BRIDGE_ADDRESSES: IBridgeAddresses = { L2OutputOracleProxy: '0x0000000000000000000000000000000000000000', }; -export const L1_STANDARD_BRIDGE_ADDRESS = +export const L1_STANDARD_MAINNET_BRIDGE_ADDRESS = ETHIQ_BRIDGE_ADDRESSES.L1StandardBridgeProxy; export const haqqEthiq = { From 527a08f9d77f0652fa0e4775ede1e28fa89faea6 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Tue, 30 Dec 2025 17:56:28 +0300 Subject: [PATCH 080/141] chore: fix address for proxy --- libs/bridge/src/lib/bridge-page.tsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/libs/bridge/src/lib/bridge-page.tsx b/libs/bridge/src/lib/bridge-page.tsx index 6c7c55afd..d59ee8e60 100644 --- a/libs/bridge/src/lib/bridge-page.tsx +++ b/libs/bridge/src/lib/bridge-page.tsx @@ -5,6 +5,7 @@ import { sepolia } from 'viem/chains'; import { useSwitchChain } from 'wagmi'; import { L1_STANDARD_MAINNET_BRIDGE_ADDRESS, + L1_STANDARD_TESTETHIQ_BRIDGE_ADDRESS, CHAIN_CONFIG, L2_STANDARD_BRIDGE_ADDRESS, bridgeSupportedChains, @@ -40,15 +41,17 @@ import { // SUPPORTED_CHAINS is now imported from @haqq/shell-shared export const useChainProxyAddress = (chainId: number | undefined) => { - if ( - chainId === CHAIN_CONFIG.l1ChainId || - chainId === CHAIN_CONFIG.l2ChainId - ) { + if (chainId === CHAIN_CONFIG.l1ChainId) { return L1_STANDARD_MAINNET_BRIDGE_ADDRESS; } + + if (chainId === CHAIN_CONFIG.l1TestChainId) { + return L1_STANDARD_TESTETHIQ_BRIDGE_ADDRESS; + } + if ( - chainId === CHAIN_CONFIG.l2TestChainId || - chainId === CHAIN_CONFIG.l1TestChainId + chainId === CHAIN_CONFIG.l2ChainId || + chainId === CHAIN_CONFIG.l2TestChainId ) { return L2_STANDARD_BRIDGE_ADDRESS; } From 074861bddae83763808be31890672b2fe705ac66 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Tue, 30 Dec 2025 19:59:25 +0300 Subject: [PATCH 081/141] feat: add script for bridging ETH from CSV and export transaction list --- .gitignore | 3 + scripts/recover-bridge-txs/README.md | 118 ++++++ .../export-transaction-list.csv | 33 ++ .../recover-bridge-txs/send-eth-from-csv.ts | 371 ++++++++++++++++++ 4 files changed, 525 insertions(+) create mode 100644 scripts/recover-bridge-txs/README.md create mode 100644 scripts/recover-bridge-txs/export-transaction-list.csv create mode 100644 scripts/recover-bridge-txs/send-eth-from-csv.ts diff --git a/.gitignore b/.gitignore index c0cfea4c5..cfe0e3384 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,6 @@ package-lock.json **/vite.config.{js,ts,mjs,mts,cjs,cts}.timestamp* vite.config.*.timestamp* vitest.config.*.timestamp* + +# Scripts +scripts/test.txt diff --git a/scripts/recover-bridge-txs/README.md b/scripts/recover-bridge-txs/README.md new file mode 100644 index 000000000..c8458c202 --- /dev/null +++ b/scripts/recover-bridge-txs/README.md @@ -0,0 +1,118 @@ +# Bridge ETH from CSV Script + +This script reads addresses from a CSV file and bridges ETH from Sepolia (L1) to a target L2 chain using the Optimism L1StandardBridge contract. + +## Features + +- **Automatic Grouping**: Addresses from the 'From' column are automatically grouped and amounts are summed +- **One Transaction Per Address**: If an address appears multiple times in the CSV, all amounts are summed and bridged in a single transaction +- **Sepolia Network**: Uses Sepolia testnet with default RPC endpoint +- **L1 to L2 Bridging**: Uses `bridgeETHTo` method from L1StandardBridge to bridge ETH to L2 + +## Prerequisites + +- Node.js and pnpm installed +- A private key OR seed phrase (mnemonic) with sufficient ETH balance on Sepolia +- L1 Standard Bridge contract address on Sepolia +- CSV file with 'From' and 'Amount' columns + +## Usage + +### Using Private Key + +```bash +PRIVATE_KEY=0x... BRIDGE_ADDRESS=0x611bc60e604803b4f064810b4630290515bfba8c pnpm exec ts-node scripts/send-eth-from-csv.ts /path/to/export-transaction-list-1767107237768.csv +``` + +### Using Seed Phrase (Mnemonic) + +```bash +MNEMONIC="word1 word2 word3 ... word12" BRIDGE_ADDRESS=0x611bc60e604803b4f064810b4630290515bfba8c pnpm exec ts-node scripts/send-eth-from-csv.ts /path/to/export-transaction-list-1767107237768.csv +``` + +### Using Seed Phrase with Account Index + +```bash +MNEMONIC="word1 word2 word3 ... word12" ACCOUNT_INDEX=1 BRIDGE_ADDRESS=0x... pnpm exec ts-node scripts/send-eth-from-csv.ts /path/to/file.csv +``` + +### Dry Run (Preview Without Bridging) + +```bash +PRIVATE_KEY=0x... BRIDGE_ADDRESS=0x... pnpm exec ts-node scripts/send-eth-from-csv.ts /path/to/file.csv --dry-run +# or +MNEMONIC="word1 word2 ..." BRIDGE_ADDRESS=0x... pnpm exec ts-node scripts/send-eth-from-csv.ts /path/to/file.csv --dry-run +``` + +## Options + +- `--dry-run`: Preview what would be bridged without actually sending transactions + +## Environment Variables + +- `PRIVATE_KEY`: Private key of the wallet to send from (required if MNEMONIC not set) +- `MNEMONIC` or `SEED_PHRASE`: Seed phrase (12 or 24 words) to derive account from (required if PRIVATE_KEY not set) +- `ACCOUNT_INDEX`: Account index to use from mnemonic (default: 0). Use 0 for first account, 1 for second, etc. +- `BRIDGE_ADDRESS`: L1 Standard Bridge contract address on Sepolia (required) + - Testethiq: `0x611bc60e604803b4f064810b4630290515bfba8c` + +**Note**: You must provide either `PRIVATE_KEY` OR `MNEMONIC`/`SEED_PHRASE`, but not both. + +## CSV Format + +The CSV file must contain: + +- `From` column: Address to send ETH to +- `Amount` column: Amount in format like "0.1 ETH" + +Example: + +```csv +Transaction Hash,Status,Method,Blockno,DateTime (UTC),From,From_Nametag,To,To_Nametag,Amount,Value (USD),Txn Fee +"0x...","Success","Transfer","9945548","2025-12-30 13:42:12","0xda80ffc27aa531e41b8d14603c8501dd20963da9","","0xfb30129241e7520e66b96426259e95359c3e2800","","0.1 ETH","$0.00","0.0000317" +``` + +## Examples + +### Using Private Key + +Bridge ETH to addresses from transaction list (amounts are automatically grouped and summed per address): + +```bash +PRIVATE_KEY=0x1234567890abcdef... BRIDGE_ADDRESS=0x611bc60e604803b4f064810b4630290515bfba8c pnpm exec ts-node scripts/send-eth-from-csv.ts ~/Downloads/export-transaction-list-1767107237768.csv +``` + +### Using Seed Phrase + +```bash +MNEMONIC="abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" BRIDGE_ADDRESS=0x611bc60e604803b4f064810b4630290515bfba8c pnpm exec ts-node scripts/send-eth-from-csv.ts ~/Downloads/export-transaction-list-1767107237768.csv +``` + +## How It Works + +1. Reads the CSV file and extracts addresses from the 'From' column +2. **Groups addresses**: If the same address appears multiple times, it groups them together +3. **Sums amounts**: All amounts for the same address are summed into a single total +4. Bridges ETH using `L1StandardBridge.bridgeETHTo()` method: + - Calls the bridge contract on Sepolia (L1) + - Bridges ETH to the target L2 chain + - Sends to the address from the 'From' column on L2 +5. Uses Sepolia testnet with the default RPC endpoint + +## Bridge Method + +The script uses `bridgeETHTo(address _to, uint32 _minGasLimit, bytes _extraData)` from the L1StandardBridge contract: + +- `_to`: Destination address on L2 (from CSV 'From' column) +- `_minGasLimit`: 200000 (minimum gas for L2 execution) +- `_extraData`: Empty bytes (`0x`) + +## Security Warning + +⚠️ **Never commit your private key or seed phrase to version control!** + +- Use environment variables +- Consider using a dedicated wallet with limited funds +- Test with small amounts first +- Keep your seed phrase secure - anyone with access can control all derived accounts +- When using mnemonic, the script uses account index 0 by default (first account) diff --git a/scripts/recover-bridge-txs/export-transaction-list.csv b/scripts/recover-bridge-txs/export-transaction-list.csv new file mode 100644 index 000000000..e0ae69d13 --- /dev/null +++ b/scripts/recover-bridge-txs/export-transaction-list.csv @@ -0,0 +1,33 @@ +Transaction Hash,Status,Method,Blockno,DateTime (UTC),From,From_Nametag,To,To_Nametag,Amount,Value (USD),Txn Fee +"0x6fd0fdd0dda1dba521d173cb3e66a642b36d861ed932dbe1766d078445e74bbd","Success","Transfer","9945548","2025-12-30 13:42:12","0xda80ffc27aa531e41b8d14603c8501dd20963da9","","0xfb30129241e7520e66b96426259e95359c3e2800","","0.1 ETH","$0.00","0.0000317" +"0x0b54930363b47d86a9e36bd00912b12b21ab38504e024af8fba1896c4c7985a3","Success","Transfer","9945010","2025-12-30 11:48:00","0x08179cf0c93acac485607498705ab1054e0993d4","","0xfb30129241e7520e66b96426259e95359c3e2800","","0.048399973 ETH","$0.00","0.0000315" +"0x1eb7c94a13b8075f47a5a9059ec6a5182c4531bb80b6a47742d6bcea0edf2902","Success","Transfer","9944819","2025-12-30 11:07:36","0xef47b75615a0304de69ea3e3630b2ed12083f44d","","0xfb30129241e7520e66b96426259e95359c3e2800","","0.5 ETH","$0.00","0.00000002" +"0x3e561aa0fcf9b4a9b1ca4906fc23e7d5403058f9d2d3b1feb92b153997c94af5","Success","Transfer","9944529","2025-12-30 10:06:00","0x99e8c8c1148866444a84529db76e28644caaea7a","","0xfb30129241e7520e66b96426259e95359c3e2800","","0.01 ETH","$0.00","0.0000315" +"0x7d69c5b5d6a6cd7c3eaf2003efe2611656272649a0ef36bfc4029b21e050c7e1","Success","Transfer","9944394","2025-12-30 09:38:12","0x99e8c8c1148866444a84529db76e28644caaea7a","","0xfb30129241e7520e66b96426259e95359c3e2800","","1 ETH","$0.00","0.0000315" +"0xe12c764c6b3f8411c3cfe7f4ca62b768ec185ef554a9916876433793cf4942b5","Success","Transfer","9943918","2025-12-30 07:57:00","0xa7f0724e00ff544f5b2bcec459dee3b66020e664","","0xfb30129241e7520e66b96426259e95359c3e2800","","0.01 ETH","$0.00","0.00000006" +"0xaf8489496ea89d7ec48c9c67c5c05eaed1baee4855b93c3fca5eb1dab627f0f0","Success","Transfer","9943912","2025-12-30 07:55:36","0xa7f0724e00ff544f5b2bcec459dee3b66020e664","","0xfb30129241e7520e66b96426259e95359c3e2800","","0.01 ETH","$0.00","0.00000004" +"0x0ac7d636ec854f8de2826a1c8dd55c2550e159c2d951364cad337a784012ef9b","Success","Transfer","9940846","2025-12-29 21:05:48","0x4ca36fffb082c5bfc3eee48e041f269c242b5908","","0xfb30129241e7520e66b96426259e95359c3e2800","","0.01 ETH","$0.00","0.00000053" +"0x79f24fb34fc0756267d9f73a27ce81aa7550f2e58d324d0f806a1e9767aef2c8","Success","Transfer","9938475","2025-12-29 12:41:12","0x9584f9fd6871da57fff1cbff6f224209012bcaae","","0xfb30129241e7520e66b96426259e95359c3e2800","","0.2 ETH","$0.00","0.0000315" +"0x7e0a22984e0f61037bef172569c7bce1f2020fc855b33ce60a7ba01afc62de8d","Success","Transfer","9937411","2025-12-29 08:56:36","0xe9f5b7cc603f989e242543f1122a401059735860","","0xfb30129241e7520e66b96426259e95359c3e2800","","0.03 ETH","$0.00","0.00003151" +"0x9ff59de7d9f63c25d491259c4763447bda78b132b0d5cf1f23df5d8aa09f6ee8","Success","Transfer","9932131","2025-12-28 14:13:36","0x9347c2df8e3f56706abaabf4ed9083c02e7bb74c","","0xfb30129241e7520e66b96426259e95359c3e2800","","0.02 ETH","$0.00","0.00000003" +"0x112eb1421ed4749292769cba31a63f939c8348caf516dd35d3c941c062f2eb10","Success","Transfer","9931902","2025-12-28 13:26:00","0x9347c2df8e3f56706abaabf4ed9083c02e7bb74c","","0xfb30129241e7520e66b96426259e95359c3e2800","","0.638593292 ETH","$0.00","0.00000005" +"0x4cd582edc42586a23808a5adac9285734132eedea4999b9e76a86004c616e380","Success","Transfer","9922645","2025-12-27 04:31:36","0xc53c0b6caaf941e650daf0fa15179bf713a5f474","","0xfb30129241e7520e66b96426259e95359c3e2800","","0.149066246 ETH","$0.00","0.0000315" +"0x8f8a1b66dca6aba31cc8c685036eaf3d84cb8e1a9b550afcdbb2736d62130985","Success","Transfer","9922536","2025-12-27 04:09:36","0x6c9e8d969fd92051836635a07fc8bb94af46e554","","0xfb30129241e7520e66b96426259e95359c3e2800","","0.01 ETH","$0.00","0.00000002" +"0xe2999eb0b6fb6c91734c4616f3cd24ffee72da855eaab8a9c13db7eef8d3d84d","Success","Transfer","9922527","2025-12-27 04:07:48","0x89a378aca906345c8f5596d311768ee08a9b3542","","0xfb30129241e7520e66b96426259e95359c3e2800","","0.013259 ETH","$0.00","0.0000315" +"0xefe0f76daeb6cb8d8593bc0d47f0d516cec757a97489eebea86802e7c427e45e","Success","Transfer","9922490","2025-12-27 03:58:48","0x89a378aca906345c8f5596d311768ee08a9b3542","","0xfb30129241e7520e66b96426259e95359c3e2800","","0.01 ETH","$0.00","0.00000008" +"0xf34aa355a19c7b3adeaedc2d1adf5d4bf0f3c735fd24a8953f11505d8fafb391","Success","Transfer","9922487","2025-12-27 03:58:12","0x89a378aca906345c8f5596d311768ee08a9b3542","","0xfb30129241e7520e66b96426259e95359c3e2800","","0.01 ETH","$0.00","0.00000008" +"0x52e27cad69e4f48a7268f86c1ed88618124b029a1f0fbba4cea99ee32c76c110","Success","Transfer","9922462","2025-12-27 03:53:12","0x89a378aca906345c8f5596d311768ee08a9b3542","","0xfb30129241e7520e66b96426259e95359c3e2800","","0.01 ETH","$0.00","0.0000315" +"0xa78d512ecd9f6cbd5c7cc62fdbe2da303399c378bf49b26c051aef849dc4b3ad","Success","Transfer","9922459","2025-12-27 03:52:36","0x89a378aca906345c8f5596d311768ee08a9b3542","","0xfb30129241e7520e66b96426259e95359c3e2800","","0.01 ETH","$0.00","0.0000315" +"0xf05b7e1a7ec070e72359934aaee6136acd173f5292f7fb4bae44c4557c8821cc","Success","Transfer","9922443","2025-12-27 03:49:12","0x89a378aca906345c8f5596d311768ee08a9b3542","","0xfb30129241e7520e66b96426259e95359c3e2800","","0.0123 ETH","$0.00","0.0000315" +"0x8d8fdf3475b579042c2364f2025499093fcdfd34f86ebfb6f98796397399407a","Success","Transfer","9920777","2025-12-26 21:53:00","0x3167b530a4317d1a88a7bf60ca22039a9ca48097","","0xfb30129241e7520e66b96426259e95359c3e2800","","0.1 ETH","$0.00","0.0000315" +"0x2613c869defd2a17e94901ad092b192b35c33e048d34eabb051013b8d584c408","Success","Transfer","9919391","2025-12-26 17:00:12","0x89a378aca906345c8f5596d311768ee08a9b3542","","0xfb30129241e7520e66b96426259e95359c3e2800","","0.005 ETH","$0.00","0.0000315" +"0xf68fc4c3276fb4384791815bb63da120d22d3605ba41f02726e8d64670888408","Success","Transfer","9917047","2025-12-26 08:26:24","0xa7f0724e00ff544f5b2bcec459dee3b66020e664","","0xfb30129241e7520e66b96426259e95359c3e2800","","0.044 ETH","$0.00","0.00000059" +"0x76ed1a74c2b65832c4da20429fd8959869082591fe45fb67988e8ed725747dbc","Success","Transfer","9916839","2025-12-26 07:40:12","0xe3f48b77030cc5574d577908d923fb662d082ef0","","0xfb30129241e7520e66b96426259e95359c3e2800","","0.05 ETH","$0.00","0.0000315" +"0x6fb7b94684b07a503cd3eebf6c5044d273e4a7b9657594fc8c7132105791a9da","Success","Transfer","9916604","2025-12-26 06:46:48","0x06505ae893f1696c9a6ca7e619cd8586c823f5b9","","0xfb30129241e7520e66b96426259e95359c3e2800","","0.03 ETH","$0.00","0.0000315" +"0xd1a3b877a073a7a5759b26e2281470818e589b32b6693ed16fdbb7c33b8100a4","Success","Transfer","9916583","2025-12-26 06:41:48","0x06505ae893f1696c9a6ca7e619cd8586c823f5b9","","0xfb30129241e7520e66b96426259e95359c3e2800","","0.001 ETH","$0.00","0.0000315" +"0xf68cb49b6073a5c13fa188c0c470e4aba034dd4d2638aebe3c016acaa9a880cf","Success","Transfer","9912701","2025-12-25 16:06:48","0x89a378aca906345c8f5596d311768ee08a9b3542","","0xfb30129241e7520e66b96426259e95359c3e2800","","0.02 ETH","$0.00","0.0000315" +"0x790c1c5afc57cb545fd9c2fe7a93d592449b0874ff6caf919249430cd4d32912","Success","Transfer","9907013","2025-12-24 18:37:00","0x89a378aca906345c8f5596d311768ee08a9b3542","","0xfb30129241e7520e66b96426259e95359c3e2800","","0.0128 ETH","$0.00","0.00003188" +"0xcd852747edb3e17ca042e6bd97ad36331971f43ecbe0d3da536c0c940dad9180","Success","Transfer","9906370","2025-12-24 16:08:48","0x89a378aca906345c8f5596d311768ee08a9b3542","","0xfb30129241e7520e66b96426259e95359c3e2800","","0.011 ETH","$0.00","0.0000315" +"0x5c84ea024ca8268ff73dee2bc98f00f532e0a14cabc6a862c9ddb974dffa50fe","Success","Transfer","9906337","2025-12-24 16:01:24","0x89a378aca906345c8f5596d311768ee08a9b3542","","0xfb30129241e7520e66b96426259e95359c3e2800","","0.01 ETH","$0.00","0.0000315" +"0xc88df3aa3807395904561817b5c186deb71e23bd2ea50ecef1dc446f4bc5e532","Success","Transfer","9906154","2025-12-24 15:20:12","0x793cbfba6f986871b4c208fb73a76e94a914ef67","","0xfb30129241e7520e66b96426259e95359c3e2800","","0.01 ETH","$0.00","0.00000007" +"0x432bf49b4d22ccf4ad39f4a13b2e8026eed32adfe2a4e03a957b156fb32cca4f","Success","Transfer","9906150","2025-12-24 15:19:12","0x793cbfba6f986871b4c208fb73a76e94a914ef67","","0xfb30129241e7520e66b96426259e95359c3e2800","","0.01 ETH","$0.00","0.00000008" \ No newline at end of file diff --git a/scripts/recover-bridge-txs/send-eth-from-csv.ts b/scripts/recover-bridge-txs/send-eth-from-csv.ts new file mode 100644 index 000000000..e23438678 --- /dev/null +++ b/scripts/recover-bridge-txs/send-eth-from-csv.ts @@ -0,0 +1,371 @@ +#!/usr/bin/env tsx + +/** + * Script to bridge ETH from Sepolia to target L2 chain using L1StandardBridge + * + * Usage: + * PRIVATE_KEY=0x... BRIDGE_ADDRESS=0x... tsx scripts/send-eth-from-csv.ts [--dry-run] + * MNEMONIC="word1 word2 ..." BRIDGE_ADDRESS=0x... tsx scripts/send-eth-from-csv.ts [--dry-run] + * + * Options: + * --dry-run Show what would be sent without actually sending + * + * Environment Variables: + * PRIVATE_KEY Private key of the wallet to send from (required if MNEMONIC not set) + * MNEMONIC Seed phrase (12 or 24 words) to derive account from (required if PRIVATE_KEY not set) + * ACCOUNT_INDEX Account index to use from mnemonic (default: 0) + * BRIDGE_ADDRESS L1 Standard Bridge contract address on Sepolia (required) + * + * Note: Addresses are automatically grouped by 'From' column and amounts are summed + * before bridging (one transaction per unique address) + */ + +import { + createWalletClient, + createPublicClient, + http, + parseEther, + formatEther, +} from 'viem'; +import { privateKeyToAccount, mnemonicToAccount } from 'viem/accounts'; +import { sepolia } from 'viem/chains'; +import * as fs from 'fs'; +import * as path from 'path'; + +export const L1StandardBridgeAbi = [ + { + inputs: [ + { internalType: 'address', name: '_localToken', type: 'address' }, + { internalType: 'address', name: '_remoteToken', type: 'address' }, + { internalType: 'address', name: '_to', type: 'address' }, + { internalType: 'uint256', name: '_amount', type: 'uint256' }, + { internalType: 'uint32', name: '_minGasLimit', type: 'uint32' }, + { internalType: 'bytes', name: '_extraData', type: 'bytes' }, + ], + name: 'bridgeERC20To', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '_to', type: 'address' }, + { internalType: 'uint32', name: '_minGasLimit', type: 'uint32' }, + { internalType: 'bytes', name: '_extraData', type: 'bytes' }, + ], + name: 'bridgeETHTo', + outputs: [], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '_l1Token', type: 'address' }, + { internalType: 'address', name: '_l2Token', type: 'address' }, + { internalType: 'address', name: '_to', type: 'address' }, + { internalType: 'uint256', name: '_amount', type: 'uint256' }, + { internalType: 'uint32', name: '_minGasLimit', type: 'uint32' }, + { internalType: 'bytes', name: '_extraData', type: 'bytes' }, + ], + name: 'depositERC20To', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '_to', type: 'address' }, + { internalType: 'uint32', name: '_minGasLimit', type: 'uint32' }, + { internalType: 'bytes', name: '_extraData', type: 'bytes' }, + ], + name: 'depositETHTo', + outputs: [], + stateMutability: 'payable', + type: 'function', + }, +] as const; + +interface AddressInfo { + address: string; + totalAmount: bigint; + transactionCount: number; +} + +// Parse command line arguments +const args = process.argv.slice(2); +let csvFile = args[0]; +const dryRun = args.includes('--dry-run'); + +if (!csvFile) { + console.error('Error: CSV file path is required'); + console.error( + 'Usage: PRIVATE_KEY=0x... BRIDGE_ADDRESS=0x... tsx scripts/send-eth-from-csv.ts [--dry-run]', + ); + console.error( + ' or: MNEMONIC="word1 word2 ..." BRIDGE_ADDRESS=0x... tsx scripts/send-eth-from-csv.ts [--dry-run]', + ); + console.error('\nExamples:'); + console.error(' scripts/send-eth-from-csv.ts export-transaction-list.csv'); + console.error(' scripts/send-eth-from-csv.ts ./export-transaction-list.csv'); + console.error( + ' scripts/send-eth-from-csv.ts scripts/export-transaction-list.csv', + ); + process.exit(1); +} + +// Resolve file path: if not absolute, try relative to current directory, then scripts folder +if (!path.isAbsolute(csvFile)) { + // First try relative to current working directory + const cwdPath = path.resolve(process.cwd(), csvFile); + if (fs.existsSync(cwdPath)) { + csvFile = cwdPath; + } else { + // Try in scripts folder (same folder as this script) + const scriptsDir = path.join(process.cwd(), 'scripts'); + const scriptsPath = path.join(scriptsDir, csvFile); + if (fs.existsSync(scriptsPath)) { + csvFile = scriptsPath; + } else { + // Use resolved path (will show error if file doesn't exist) + csvFile = path.resolve(csvFile); + } + } +} + +const privateKey = process.env.PRIVATE_KEY; +const mnemonic = process.env.MNEMONIC || process.env.SEED_PHRASE; +const accountIndex = parseInt(process.env.ACCOUNT_INDEX || '0', 10); +const bridgeAddress = process.env.BRIDGE_ADDRESS; + +if (!privateKey && !mnemonic) { + console.error( + 'Error: Either PRIVATE_KEY or MNEMONIC/SEED_PHRASE environment variable is required', + ); + console.error(' PRIVATE_KEY=0x... (private key in hex format)'); + console.error( + ' MNEMONIC="word1 word2 word3 ..." (12 or 24 word seed phrase)', + ); + process.exit(1); +} + +if (privateKey && mnemonic) { + console.error( + 'Error: Both PRIVATE_KEY and MNEMONIC are set. Please use only one.', + ); + process.exit(1); +} + +if (!bridgeAddress) { + console.error('Error: BRIDGE_ADDRESS environment variable is required'); + console.error( + 'Example: BRIDGE_ADDRESS=0x611bc60e604803b4f064810b4630290515bfba8c', + ); + process.exit(1); +} + +// Use Sepolia chain with default RPC +const chain = sepolia; +const rpcUrl = sepolia.rpcUrls.default.http[0]; + +// Parse CSV and extract addresses +function parseCSV(filePath: string): AddressInfo[] { + if (!fs.existsSync(filePath)) { + throw new Error( + `CSV file not found: ${filePath}\nMake sure the file exists in the current directory or scripts folder.`, + ); + } + const content = fs.readFileSync(filePath, 'utf-8'); + const lines = content.trim().split('\n'); + const headers = lines[0].split(',').map((h) => h.replace(/"/g, '').trim()); + + const fromIndex = headers.indexOf('From'); + const amountIndex = headers.indexOf('Amount'); + + if (fromIndex === -1 || amountIndex === -1) { + throw new Error('CSV must contain "From" and "Amount" columns'); + } + + const addressMap = new Map(); + const transactionCounts = new Map(); + + // Skip header row + for (let i = 1; i < lines.length; i++) { + const line = lines[i]; + if (!line.trim()) continue; + + // Parse CSV line (handling quoted values) + const values: string[] = []; + let current = ''; + let inQuotes = false; + + for (let j = 0; j < line.length; j++) { + const char = line[j]; + if (char === '"') { + inQuotes = !inQuotes; + } else if (char === ',' && !inQuotes) { + values.push(current.trim()); + current = ''; + } else { + current += char; + } + } + values.push(current.trim()); + + const fromAddress = values[fromIndex]?.replace(/"/g, '').trim(); + const amountStr = values[amountIndex]?.replace(/"/g, '').trim(); + + if (!fromAddress || !amountStr) continue; + + // Parse amount (remove " ETH" suffix) + const amountValue = amountStr.replace(' ETH', '').trim(); + const amountWei = parseEther(amountValue); + + // Aggregate amounts per address + const currentTotal = addressMap.get(fromAddress) || 0n; + addressMap.set(fromAddress, currentTotal + amountWei); + + const count = transactionCounts.get(fromAddress) || 0; + transactionCounts.set(fromAddress, count + 1); + } + + return Array.from(addressMap.entries()).map(([address, totalAmount]) => ({ + address: address as `0x${string}`, + totalAmount, + transactionCount: transactionCounts.get(address) || 0, + })); +} + +async function main() { + console.log('📄 Reading CSV file:', csvFile); + const addresses = parseCSV(csvFile); + + const totalTransactions = addresses.reduce( + (sum, a) => sum + a.transactionCount, + 0, + ); + console.log( + `\n📊 Found ${addresses.length} unique addresses (grouped from ${totalTransactions} transactions)`, + ); + console.log(`💡 Amounts are automatically summed per address`); + + // Create account from private key or mnemonic + const account = privateKey + ? privateKeyToAccount(privateKey as `0x${string}`) + : mnemonicToAccount(mnemonic!, { accountIndex }); + + const publicClient = createPublicClient({ + chain, + transport: http(rpcUrl), + }); + const walletClient = createWalletClient({ + account, + chain, + transport: http(rpcUrl), + }); + + const senderAddress = account.address; + console.log(`\n👤 Sender address: ${senderAddress}`); + if (mnemonic) { + console.log(`🔑 Using mnemonic (account index: ${accountIndex})`); + } else { + console.log(`🔑 Using private key`); + } + console.log(`🌐 Network: ${chain.name} (Chain ID: ${chain.id})`); + console.log(`🔗 RPC URL: ${rpcUrl}`); + console.log(`🌉 Bridge address: ${bridgeAddress}`); + console.log(`📋 Method: bridgeETHTo (bridging to L2)`); + + if (dryRun) { + console.log('\n🔍 DRY RUN MODE - No transactions will be sent\n'); + } + + // Calculate total amount needed + const totalNeeded = addresses.reduce((sum, a) => sum + a.totalAmount, 0n); + + console.log(`\n💰 Total ETH needed: ${formatEther(totalNeeded)} ETH`); + + // Check balance + const balance = await publicClient.getBalance({ address: senderAddress }); + console.log(`💵 Current balance: ${formatEther(balance)} ETH`); + + if (balance < totalNeeded) { + console.error( + `\n❌ Error: Insufficient balance. Need ${formatEther(totalNeeded)} ETH, have ${formatEther(balance)} ETH`, + ); + process.exit(1); + } + + console.log('\n🌉 Bridging ETH to L2...\n'); + + let successCount = 0; + let failCount = 0; + + for (let i = 0; i < addresses.length; i++) { + const { address, totalAmount, transactionCount } = addresses[i]; + + const groupedInfo = + transactionCount > 1 + ? `(sum of ${transactionCount} transactions: ${formatEther(totalAmount)} ETH)` + : ''; + + console.log( + `[${i + 1}/${addresses.length}] Bridging ${formatEther(totalAmount)} ETH to ${address} on L2 ${groupedInfo}`, + ); + + if (dryRun) { + console.log( + ` ✓ Would bridge ${formatEther(totalAmount)} ETH to ${address} using bridgeETHTo`, + ); + successCount++; + continue; + } + + try { + // Use bridgeETHTo to bridge ETH from L1 (Sepolia) to L2 + // bridgeETHTo(address _to, uint32 _minGasLimit, bytes _extraData) + const hash = await walletClient.writeContract({ + address: bridgeAddress as `0x${string}`, + abi: L1StandardBridgeAbi, + functionName: 'bridgeETHTo', + args: [ + address as `0x${string}`, // _to: destination address on L2 + 200000, // _minGasLimit: minimum gas for L2 execution + '0x' as `0x${string}`, // _extraData: optional extra data + ], + value: totalAmount, // ETH amount to bridge + }); + + console.log(` ✓ Bridge transaction sent: ${hash}`); + + // Wait for confirmation + const receipt = await publicClient.waitForTransactionReceipt({ + hash, + }); + console.log(` ✓ Confirmed in block ${receipt.blockNumber}`); + + successCount++; + } catch (error) { + console.error( + ` ❌ Failed: ${error instanceof Error ? error.message : String(error)}`, + ); + failCount++; + } + + // Small delay to avoid rate limiting + if (i < addresses.length - 1) { + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + } + + console.log(`\n✅ Summary:`); + console.log(` Successful bridges: ${successCount}`); + console.log(` Failed: ${failCount}`); + console.log( + ` Total ETH bridged: ${formatEther(addresses.slice(0, successCount).reduce((sum, a) => sum + a.totalAmount, 0n))} ETH`, + ); +} + +main().catch((error) => { + console.error('Fatal error:', error); + process.exit(1); +}); From b3008148d3ee0b848723328c00efb4e2f13ff994 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Mon, 5 Jan 2026 12:54:34 +0300 Subject: [PATCH 082/141] chore: whitelist --- .../src/app/[locale]/burn-waitlist/page.tsx | 21 + libs/burn-waitlist/README.md | 7 + libs/burn-waitlist/jest.config.ts | 18 + libs/burn-waitlist/project.json | 9 + libs/burn-waitlist/src/index.ts | 4 + libs/burn-waitlist/src/lib/abi/waitlist.ts | 364 ++++++++++++++++++ .../burn-waitlist/src/lib/components/index.ts | 7 + .../src/lib/components/network-warning.tsx | 36 ++ .../src/lib/components/participation-form.tsx | 133 +++++++ .../src/lib/components/request-item.tsx | 79 ++++ .../src/lib/components/requests-list.tsx | 46 +++ .../src/lib/components/status-messages.tsx | 87 +++++ .../src/lib/components/waitlist-balances.tsx | 94 +++++ .../components/wallet-connection-warning.tsx | 11 + .../src/lib/constants/waitlist-config.ts | 82 ++++ libs/burn-waitlist/src/lib/hooks/index.ts | 4 + .../src/lib/hooks/use-backend-signature.ts | 79 ++++ .../src/lib/hooks/use-waitlist-contract.ts | 273 +++++++++++++ .../src/lib/hooks/use-waitlist-form.ts | 155 ++++++++ .../src/lib/hooks/use-waitlist-requests.ts | 72 ++++ libs/burn-waitlist/src/lib/server.ts | 1 + libs/burn-waitlist/src/lib/waitlist-page.tsx | 295 ++++++++++++++ libs/burn-waitlist/tsconfig.json | 23 ++ libs/burn-waitlist/tsconfig.lib.json | 25 ++ libs/burn-waitlist/tsconfig.spec.json | 20 + package.json | 1 + pnpm-lock.yaml | 58 ++- tsconfig.base.json | 1 + 28 files changed, 1993 insertions(+), 12 deletions(-) create mode 100644 apps/shell/src/app/[locale]/burn-waitlist/page.tsx create mode 100644 libs/burn-waitlist/README.md create mode 100644 libs/burn-waitlist/jest.config.ts create mode 100644 libs/burn-waitlist/project.json create mode 100644 libs/burn-waitlist/src/index.ts create mode 100644 libs/burn-waitlist/src/lib/abi/waitlist.ts create mode 100644 libs/burn-waitlist/src/lib/components/index.ts create mode 100644 libs/burn-waitlist/src/lib/components/network-warning.tsx create mode 100644 libs/burn-waitlist/src/lib/components/participation-form.tsx create mode 100644 libs/burn-waitlist/src/lib/components/request-item.tsx create mode 100644 libs/burn-waitlist/src/lib/components/requests-list.tsx create mode 100644 libs/burn-waitlist/src/lib/components/status-messages.tsx create mode 100644 libs/burn-waitlist/src/lib/components/waitlist-balances.tsx create mode 100644 libs/burn-waitlist/src/lib/components/wallet-connection-warning.tsx create mode 100644 libs/burn-waitlist/src/lib/constants/waitlist-config.ts create mode 100644 libs/burn-waitlist/src/lib/hooks/index.ts create mode 100644 libs/burn-waitlist/src/lib/hooks/use-backend-signature.ts create mode 100644 libs/burn-waitlist/src/lib/hooks/use-waitlist-contract.ts create mode 100644 libs/burn-waitlist/src/lib/hooks/use-waitlist-form.ts create mode 100644 libs/burn-waitlist/src/lib/hooks/use-waitlist-requests.ts create mode 100644 libs/burn-waitlist/src/lib/server.ts create mode 100644 libs/burn-waitlist/src/lib/waitlist-page.tsx create mode 100644 libs/burn-waitlist/tsconfig.json create mode 100644 libs/burn-waitlist/tsconfig.lib.json create mode 100644 libs/burn-waitlist/tsconfig.spec.json diff --git a/apps/shell/src/app/[locale]/burn-waitlist/page.tsx b/apps/shell/src/app/[locale]/burn-waitlist/page.tsx new file mode 100644 index 000000000..167d6602d --- /dev/null +++ b/apps/shell/src/app/[locale]/burn-waitlist/page.tsx @@ -0,0 +1,21 @@ +import { + HydrationBoundary, + QueryClient, + dehydrate, +} from '@tanstack/react-query'; +import { WaitlistPage } from '@haqq/shell-burn-waitlist'; + +export const dynamic = 'force-dynamic'; +export const fetchCache = 'force-no-store'; + +export default async function BurnWaitlistPage() { + const queryClient = new QueryClient(); + + const dehydratedState = dehydrate(queryClient); + + return ( + + + + ); +} diff --git a/libs/burn-waitlist/README.md b/libs/burn-waitlist/README.md new file mode 100644 index 000000000..63d6687da --- /dev/null +++ b/libs/burn-waitlist/README.md @@ -0,0 +1,7 @@ +# shell-burn-waitlist + +This library provides functionality for participating in burn waitlists. + +## Running unit tests + +Run `nx test shell-burn-waitlist` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/burn-waitlist/jest.config.ts b/libs/burn-waitlist/jest.config.ts new file mode 100644 index 000000000..25806aac8 --- /dev/null +++ b/libs/burn-waitlist/jest.config.ts @@ -0,0 +1,18 @@ +/* eslint-disable */ +export default { + displayName: 'shell-burn-waitlist', + preset: '../../jest.preset.js', + transform: { + '^.+\\.[tj]sx?$': [ + '@swc/jest', + { + jsc: { + parser: { syntax: 'typescript', tsx: true }, + transform: { react: { runtime: 'automatic' } }, + }, + }, + ], + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../coverage/libs/burn-waitlist', +}; diff --git a/libs/burn-waitlist/project.json b/libs/burn-waitlist/project.json new file mode 100644 index 000000000..e481cbf44 --- /dev/null +++ b/libs/burn-waitlist/project.json @@ -0,0 +1,9 @@ +{ + "name": "shell-burn-waitlist", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/burn-waitlist/src", + "projectType": "library", + "tags": ["shell-lib-burn-waitlist"], + "// targets": "to see all targets run: nx show project shell-burn-waitlist --web", + "targets": {} +} diff --git a/libs/burn-waitlist/src/index.ts b/libs/burn-waitlist/src/index.ts new file mode 100644 index 000000000..12f040977 --- /dev/null +++ b/libs/burn-waitlist/src/index.ts @@ -0,0 +1,4 @@ +export * from './lib/waitlist-page'; +export * from './lib/hooks'; +export * from './lib/components'; +export * from './lib/constants/waitlist-config'; diff --git a/libs/burn-waitlist/src/lib/abi/waitlist.ts b/libs/burn-waitlist/src/lib/abi/waitlist.ts new file mode 100644 index 000000000..11b22ef0e --- /dev/null +++ b/libs/burn-waitlist/src/lib/abi/waitlist.ts @@ -0,0 +1,364 @@ +// ABI for Waitlist contract +export const WaitlistAbi = [ + { + inputs: [ + { + internalType: 'address', + name: 'initialOwner', + type: 'address', + }, + ], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { + inputs: [], + name: 'AlreadyFinalized', + type: 'error', + }, + { + inputs: [], + name: 'AmountMustBeGreaterThanZero', + type: 'error', + }, + { + inputs: [], + name: 'CannotCancelInCurrentState', + type: 'error', + }, + { + inputs: [], + name: 'CannotCloseRequests', + type: 'error', + }, + { + inputs: [], + name: 'CannotOpenRequests', + type: 'error', + }, + { + inputs: [], + name: 'CannotSubmitRequests', + type: 'error', + }, + { + inputs: [], + name: 'InvalidBackendSignature', + type: 'error', + }, + { + inputs: [], + name: 'InvalidState', + type: 'error', + }, + { + inputs: [], + name: 'OnlyAuthorCanCancel', + type: 'error', + }, + { + inputs: [], + name: 'RequestAlreadyCancelled', + type: 'error', + }, + { + inputs: [], + name: 'RequestDoesNotExist', + type: 'error', + }, + { + inputs: [], + name: 'RequestsAlreadyFinalized', + type: 'error', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'oldSigner', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'newSigner', + type: 'address', + }, + ], + name: 'BackendSignerChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'uint256', + name: 'requestId', + type: 'uint256', + }, + { + indexed: true, + internalType: 'address', + name: 'author', + type: 'address', + }, + ], + name: 'RequestCancelled', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'uint256', + name: 'requestId', + type: 'uint256', + }, + { + indexed: true, + internalType: 'address', + name: 'author', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + { + indexed: false, + internalType: 'enum Waitlist.FundsSource', + name: 'source', + type: 'uint8', + }, + { + indexed: false, + internalType: 'bytes', + name: 'backendSignature', + type: 'bytes', + }, + ], + name: 'RequestCreated', + type: 'event', + }, + { + anonymous: false, + inputs: [], + name: 'RequestsClosed', + type: 'event', + }, + { + anonymous: false, + inputs: [], + name: 'RequestsFinalized', + type: 'event', + }, + { + anonymous: false, + inputs: [], + name: 'RequestsOpened', + type: 'event', + }, + { + inputs: [], + name: 'backendSigner', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'canSubmit', + outputs: [ + { + internalType: 'bool', + name: '', + type: 'bool', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'canWithdraw', + outputs: [ + { + internalType: 'bool', + name: '', + type: 'bool', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint256', + name: 'requestId', + type: 'uint256', + }, + ], + name: 'cancelRequest', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + { + internalType: 'enum Waitlist.FundsSource', + name: 'source', + type: 'uint8', + }, + { + internalType: 'bytes', + name: 'backendSignature', + type: 'bytes', + }, + ], + name: 'createRequest', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'currentState', + outputs: [ + { + internalType: 'enum Waitlist.RequestsState', + name: '', + type: 'uint8', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint256', + name: 'requestId', + type: 'uint256', + }, + ], + name: 'getRequestById', + outputs: [ + { + components: [ + { + internalType: 'uint256', + name: 'requestId', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + { + internalType: 'address', + name: 'author', + type: 'address', + }, + { + internalType: 'enum Waitlist.FundsSource', + name: 'source', + type: 'uint8', + }, + { + internalType: 'bool', + name: 'cancelled', + type: 'bool', + }, + { + internalType: 'bytes', + name: 'backendSignature', + type: 'bytes', + }, + ], + internalType: 'struct Waitlist.Request', + name: '', + type: 'tuple', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'user', + type: 'address', + }, + ], + name: 'getRequestsByUser', + outputs: [ + { + internalType: 'uint256[]', + name: '', + type: 'uint256[]', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getTotalAmount', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getTotalCount', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'paused', + outputs: [ + { + internalType: 'bool', + name: '', + type: 'bool', + }, + ], + stateMutability: 'view', + type: 'function', + }, +] as const; diff --git a/libs/burn-waitlist/src/lib/components/index.ts b/libs/burn-waitlist/src/lib/components/index.ts new file mode 100644 index 000000000..afda1d803 --- /dev/null +++ b/libs/burn-waitlist/src/lib/components/index.ts @@ -0,0 +1,7 @@ +export * from './participation-form'; +export * from './requests-list'; +export * from './request-item'; +export * from './status-messages'; +export * from './wallet-connection-warning'; +export * from './network-warning'; +export * from './waitlist-balances'; diff --git a/libs/burn-waitlist/src/lib/components/network-warning.tsx b/libs/burn-waitlist/src/lib/components/network-warning.tsx new file mode 100644 index 000000000..061077ac9 --- /dev/null +++ b/libs/burn-waitlist/src/lib/components/network-warning.tsx @@ -0,0 +1,36 @@ +'use client'; + +import { Button } from '@haqq/shell-ui-kit'; +import { useSwitchChain } from 'wagmi'; +import { WAITLIST_DEFAULT_CHAIN_ID } from '../constants/waitlist-config'; + +export interface NetworkWarningProps { + onSwitchChain?: () => void; +} + +export function NetworkWarning({ onSwitchChain }: NetworkWarningProps) { + const { switchChainAsync } = useSwitchChain(); + + const handleSwitch = async () => { + if (onSwitchChain) { + onSwitchChain(); + } else { + try { + await switchChainAsync({ chainId: WAITLIST_DEFAULT_CHAIN_ID }); + } catch (error) { + console.error('Failed to switch chain:', error); + } + } + }; + + return ( +
+
+ Please switch to HAQQ Testethiq network to participate +
+ +
+ ); +} diff --git a/libs/burn-waitlist/src/lib/components/participation-form.tsx b/libs/burn-waitlist/src/lib/components/participation-form.tsx new file mode 100644 index 000000000..bb7f8edbd --- /dev/null +++ b/libs/burn-waitlist/src/lib/components/participation-form.tsx @@ -0,0 +1,133 @@ +'use client'; + +import { useMemo } from 'react'; +import { Button } from '@haqq/shell-ui-kit'; +import { ModalInput } from '@haqq/shell-ui-kit'; +import { FundsSource } from '../constants/waitlist-config'; +import { formatEther } from 'viem'; +import { WaitlistBalances } from './waitlist-balances'; + +export interface ParticipationFormProps { + amount: string; + source: FundsSource; + availableBalance?: bigint; + walletBalance?: bigint; + onAmountChange: (amount: string) => void; + onSourceChange: (source: FundsSource) => void; + onMaxClick: () => void; + onSubmit: () => void; + isValid: boolean; + isSubmitting: boolean; + error?: string; + amountError?: string; +} + +export function ParticipationForm({ + amount, + source, + availableBalance, + walletBalance, + onAmountChange, + onSourceChange, + onMaxClick, + onSubmit, + isValid, + isSubmitting, + error, + amountError, +}: ParticipationFormProps) { + const formattedBalance = availableBalance + ? formatEther(availableBalance) + : '0'; + + // Determine which balance to show based on source + const displayBalance = useMemo(() => { + if (source === FundsSource.OwnBalance) { + return walletBalance; + } + // For ucDAO, we'll show the available balance from indexer + return availableBalance; + }, [source, walletBalance, availableBalance]); + + const balanceLabel = useMemo(() => { + if (source === FundsSource.OwnBalance) { + return 'Wallet Balance'; + } + return 'Available Balance'; + }, [source]); + + return ( +
+ +
+ + onAmountChange(value?.toString() || '')} + onMaxButtonClick={onMaxClick} + hint={ + amountError ? ( + {amountError} + ) : ( + + {balanceLabel}: {formattedBalance} ISLM + + ) + } + isMaxButtonDisabled={!availableBalance || availableBalance === 0n} + /> +
+ +
+ +
+ + +
+
+ + {error && ( +
+
{error}
+
+ )} + +
+ +
+
+ ); +} diff --git a/libs/burn-waitlist/src/lib/components/request-item.tsx b/libs/burn-waitlist/src/lib/components/request-item.tsx new file mode 100644 index 000000000..130e2f1a0 --- /dev/null +++ b/libs/burn-waitlist/src/lib/components/request-item.tsx @@ -0,0 +1,79 @@ +'use client'; + +import { useWaitlistRequest } from '../hooks/use-waitlist-contract'; +import { formatEther } from 'viem'; +import { FundsSource } from '../constants/waitlist-config'; +import { Button } from '@haqq/shell-ui-kit'; + +export interface RequestItemProps { + requestId: bigint; + canCancel: boolean; + onCancel: (requestId: bigint) => void; + isCancelling?: boolean; +} + +export function RequestItem({ + requestId, + canCancel, + onCancel, + isCancelling = false, +}: RequestItemProps) { + const { request, isLoading } = useWaitlistRequest(requestId); + + if (isLoading) { + return ( +
+
Loading...
+
+ ); + } + + if (!request) { + return null; + } + + const amount = formatEther(request.amount); + const sourceLabel = + request.source === FundsSource.OwnBalance ? 'Own Balance' : 'ucDAO'; + const isCancelled = request.cancelled; + + return ( +
+
+
+
+ + Request #{requestId.toString()} + + {isCancelled && ( + + Cancelled + + )} +
+
+
+ Amount:{' '} + {amount} ISLM +
+
+ Source:{' '} + {sourceLabel} +
+
+
+ {canCancel && !isCancelled && ( + + )} +
+
+ ); +} diff --git a/libs/burn-waitlist/src/lib/components/requests-list.tsx b/libs/burn-waitlist/src/lib/components/requests-list.tsx new file mode 100644 index 000000000..bcba6ca34 --- /dev/null +++ b/libs/burn-waitlist/src/lib/components/requests-list.tsx @@ -0,0 +1,46 @@ +'use client'; + +import { RequestItem } from './request-item'; + +export interface RequestsListProps { + requestIds: bigint[]; + canCancel: boolean; + onCancel: (requestId: bigint) => void; + isCancelling?: boolean; + cancellingRequestId?: bigint; +} + +export function RequestsList({ + requestIds, + canCancel, + onCancel, + isCancelling = false, + cancellingRequestId, +}: RequestsListProps) { + if (requestIds.length === 0) { + return ( +
+
+ You haven't created any requests yet +
+
+ ); + } + + return ( +
+

Your Requests

+
+ {requestIds.map((id) => ( + + ))} +
+
+ ); +} diff --git a/libs/burn-waitlist/src/lib/components/status-messages.tsx b/libs/burn-waitlist/src/lib/components/status-messages.tsx new file mode 100644 index 000000000..257610637 --- /dev/null +++ b/libs/burn-waitlist/src/lib/components/status-messages.tsx @@ -0,0 +1,87 @@ +'use client'; + +import { RequestsState } from '../constants/waitlist-config'; + +export interface StatusMessagesProps { + currentState?: RequestsState; + canSubmit?: boolean; + canWithdraw?: boolean; + paused?: boolean; + isCorrectChain: boolean; + isConnected: boolean; +} + +export function StatusMessages({ + currentState, + canSubmit, + canWithdraw, + paused, + isCorrectChain, + isConnected, +}: StatusMessagesProps) { + if (!isConnected) { + return null; + } + + if (!isCorrectChain) { + return ( +
+
+ Please switch to HAQQ Testethiq network to participate +
+
+ ); + } + + if (paused) { + return ( +
+
+ Waitlist is currently paused +
+
+ ); + } + + if (currentState === RequestsState.Initialed) { + return ( +
+
+ Waitlist has not been opened yet +
+
+ ); + } + + if (currentState === RequestsState.Closed) { + return ( +
+
+ Waitlist is closed. You can still cancel your requests. +
+
+ ); + } + + if (currentState === RequestsState.Finalized) { + return ( +
+
+ Waitlist has been finalized +
+
+ ); + } + + if (currentState === RequestsState.Started && canSubmit) { + return ( +
+
+ Waitlist is open. You can participate now. +
+
+ ); + } + + return null; +} diff --git a/libs/burn-waitlist/src/lib/components/waitlist-balances.tsx b/libs/burn-waitlist/src/lib/components/waitlist-balances.tsx new file mode 100644 index 000000000..73d9fde39 --- /dev/null +++ b/libs/burn-waitlist/src/lib/components/waitlist-balances.tsx @@ -0,0 +1,94 @@ +'use client'; + +import { useMemo } from 'react'; +import { formatEther } from 'viem'; +import { useAddress, useIndexerBalanceQuery } from '@haqq/shell-shared'; + +export interface WaitlistBalancesProps { + walletBalance?: bigint; +} + +export function WaitlistBalances({ walletBalance }: WaitlistBalancesProps) { + const { haqqAddress } = useAddress(); + const { data: indexerBalances } = useIndexerBalanceQuery(haqqAddress); + + const balances = useMemo(() => { + const walletBalanceFormatted = walletBalance + ? formatEther(walletBalance) + : '0'; + + return { + wallet: { + label: 'Wallet Balance', + value: walletBalanceFormatted, + valueBn: walletBalance || 0n, + }, + locked: { + label: 'Locked', + value: indexerBalances?.locked.toFixed(6) || '0', + valueBn: indexerBalances?.lockedBn || 0n, + }, + vested: { + label: 'Vested', + value: indexerBalances?.vested.toFixed(6) || '0', + valueBn: indexerBalances?.vestedBn || 0n, + }, + daoLocked: { + label: 'DAO Locked', + value: indexerBalances?.daoLocked.toFixed(6) || '0', + valueBn: indexerBalances?.daoLockedBn || 0n, + }, + available: { + label: 'Available', + value: indexerBalances?.available.toFixed(6) || '0', + valueBn: indexerBalances?.availableBn || 0n, + }, + }; + }, [walletBalance, indexerBalances]); + + const hasAnyBalance = useMemo(() => { + return ( + balances.wallet.valueBn > 0n || + balances.locked.valueBn > 0n || + balances.vested.valueBn > 0n || + balances.daoLocked.valueBn > 0n || + balances.available.valueBn > 0n + ); + }, [balances]); + + if (!hasAnyBalance) { + return null; + } + + return ( +
+
+ Your Balances +
+
+ {Object.entries(balances).map(([key, balance]) => { + if (balance.valueBn === 0n) { + return null; + } + + return ( +
+
+ {balance.label} +
+
+ {parseFloat(balance.value).toLocaleString('en-US', { + maximumFractionDigits: 6, + })}{' '} + ISLM +
+
+ ); + })} +
+
+ ); +} diff --git a/libs/burn-waitlist/src/lib/components/wallet-connection-warning.tsx b/libs/burn-waitlist/src/lib/components/wallet-connection-warning.tsx new file mode 100644 index 000000000..f85b50b24 --- /dev/null +++ b/libs/burn-waitlist/src/lib/components/wallet-connection-warning.tsx @@ -0,0 +1,11 @@ +'use client'; + +export function WalletConnectionWarning() { + return ( +
+
+ Please connect your wallet to participate in the waitlist +
+
+ ); +} diff --git a/libs/burn-waitlist/src/lib/constants/waitlist-config.ts b/libs/burn-waitlist/src/lib/constants/waitlist-config.ts new file mode 100644 index 000000000..f42d8e1f5 --- /dev/null +++ b/libs/burn-waitlist/src/lib/constants/waitlist-config.ts @@ -0,0 +1,82 @@ +import { haqqTestethiq, haqqEthiq } from '@haqq/shell-shared'; + +/** + * Waitlist contract addresses mapped by chain ID + * Key: chain ID, Value: contract address + */ +export const WAITLIST_CONTRACT_ADDRESSES: Record = { + [haqqTestethiq.id]: + '0xAFb74983668ff5cBE7340cb0DA014D7221c7947e' as `0x${string}`, + // Add more chain deployments here as they become available + // [haqqEthiq.id]: '0x...' as `0x${string}`, +}; + +/** + * Supported chain IDs for waitlist + */ +export const WAITLIST_SUPPORTED_CHAIN_IDS = Object.keys( + WAITLIST_CONTRACT_ADDRESSES, +).map(Number); + +/** + * Get waitlist contract address for a given chain ID + * @param chainId - The chain ID to get the contract address for + * @returns The contract address for the chain, or undefined if not supported + */ +export function getWaitlistContractAddress( + chainId?: number, +): `0x${string}` | undefined { + if (!chainId) { + return undefined; + } + return WAITLIST_CONTRACT_ADDRESSES[chainId]; +} + +/** + * Check if a chain ID is supported for waitlist + * @param chainId - The chain ID to check + * @returns True if the chain is supported + */ +export function isWaitlistChainSupported(chainId?: number): boolean { + if (!chainId) { + return false; + } + return chainId in WAITLIST_CONTRACT_ADDRESSES; +} + +/** + * Default chain ID for waitlist (first supported chain) + */ +export const WAITLIST_DEFAULT_CHAIN_ID = + WAITLIST_SUPPORTED_CHAIN_IDS[0] || haqqTestethiq.id; + +/** + * Backend API base URL + * Can be overridden via environment variable + */ +export const getBackendApiUrl = (): string => { + if (typeof window !== 'undefined') { + return ( + process.env.NEXT_PUBLIC_BURN_WAITLIST_API_URL || 'http://localhost:8080' + ); + } + return process.env.BURN_WAITLIST_API_URL || 'http://localhost:8080'; +}; + +/** + * Funds source enum values + */ +export enum FundsSource { + OwnBalance = 0, + ucDAO = 1, +} + +/** + * Request state enum values + */ +export enum RequestsState { + Initialed = 0, + Started = 1, + Closed = 2, + Finalized = 3, +} diff --git a/libs/burn-waitlist/src/lib/hooks/index.ts b/libs/burn-waitlist/src/lib/hooks/index.ts new file mode 100644 index 000000000..ebe4c0ed8 --- /dev/null +++ b/libs/burn-waitlist/src/lib/hooks/index.ts @@ -0,0 +1,4 @@ +export * from './use-waitlist-contract'; +export * from './use-backend-signature'; +export * from './use-waitlist-requests'; +export * from './use-waitlist-form'; diff --git a/libs/burn-waitlist/src/lib/hooks/use-backend-signature.ts b/libs/burn-waitlist/src/lib/hooks/use-backend-signature.ts new file mode 100644 index 000000000..65af18453 --- /dev/null +++ b/libs/burn-waitlist/src/lib/hooks/use-backend-signature.ts @@ -0,0 +1,79 @@ +'use client'; + +import { useCallback, useState } from 'react'; +import { getBackendApiUrl } from '../constants/waitlist-config'; +import { FundsSource } from '../constants/waitlist-config'; + +interface BackendSignatureResponse { + signature: string; +} + +interface UseBackendSignatureReturn { + getSignature: ( + userAddress: string, + amount: bigint, + source: FundsSource, + ) => Promise<`0x${string}`>; + isLoading: boolean; + error: Error | null; +} + +/** + * Hook to get backend signature for waitlist request + */ +export function useBackendSignature(): UseBackendSignatureReturn { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const getSignature = useCallback( + async ( + userAddress: string, + amount: bigint, + source: FundsSource, + ): Promise<`0x${string}`> => { + setIsLoading(true); + setError(null); + + try { + const apiUrl = getBackendApiUrl(); + const response = await fetch(`${apiUrl}/api/v1/signature`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + user: userAddress, + amount: amount.toString(), + source: source, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error( + `Failed to get signature: ${response.status} ${errorText}`, + ); + } + + const data: BackendSignatureResponse = await response.json(); + return data.signature as `0x${string}`; + } catch (err) { + const error = + err instanceof Error + ? err + : new Error('Failed to get backend signature'); + setError(error); + throw error; + } finally { + setIsLoading(false); + } + }, + [], + ); + + return { + getSignature, + isLoading, + error, + }; +} diff --git a/libs/burn-waitlist/src/lib/hooks/use-waitlist-contract.ts b/libs/burn-waitlist/src/lib/hooks/use-waitlist-contract.ts new file mode 100644 index 000000000..936b1bc32 --- /dev/null +++ b/libs/burn-waitlist/src/lib/hooks/use-waitlist-contract.ts @@ -0,0 +1,273 @@ +'use client'; + +import { + useReadContract, + useWriteContract, + useWaitForTransactionReceipt, +} from 'wagmi'; +import { useAccount } from 'wagmi'; +import { + getWaitlistContractAddress, + isWaitlistChainSupported, + WAITLIST_DEFAULT_CHAIN_ID, +} from '../constants/waitlist-config'; +import { WaitlistAbi } from '../abi/waitlist'; +import { FundsSource, RequestsState } from '../constants/waitlist-config'; + +export interface WaitlistRequest { + requestId: bigint; + amount: bigint; + author: `0x${string}`; + source: FundsSource; + cancelled: boolean; + backendSignature: `0x${string}`; +} + +/** + * Hook to read waitlist contract state + */ +export function useWaitlistContractState() { + const { chain } = useAccount(); + const chainId = chain?.id; + const contractAddress = getWaitlistContractAddress(chainId); + + const { data: currentState, refetch: refetchState } = useReadContract({ + address: contractAddress, + abi: WaitlistAbi, + functionName: 'currentState', + chainId: chainId || WAITLIST_DEFAULT_CHAIN_ID, + query: { + enabled: !!contractAddress && !!chainId, + }, + }); + + const { data: canSubmit, refetch: refetchCanSubmit } = useReadContract({ + address: contractAddress, + abi: WaitlistAbi, + functionName: 'canSubmit', + chainId: chainId || WAITLIST_DEFAULT_CHAIN_ID, + query: { + enabled: !!contractAddress && !!chainId, + }, + }); + + const { data: canWithdraw, refetch: refetchCanWithdraw } = useReadContract({ + address: contractAddress, + abi: WaitlistAbi, + functionName: 'canWithdraw', + chainId: chainId || WAITLIST_DEFAULT_CHAIN_ID, + query: { + enabled: !!contractAddress && !!chainId, + }, + }); + + const { data: paused, refetch: refetchPaused } = useReadContract({ + address: contractAddress, + abi: WaitlistAbi, + functionName: 'paused', + chainId: chainId || WAITLIST_DEFAULT_CHAIN_ID, + query: { + enabled: !!contractAddress && !!chainId, + }, + }); + + const { data: totalAmount, refetch: refetchTotalAmount } = useReadContract({ + address: contractAddress, + abi: WaitlistAbi, + functionName: 'getTotalAmount', + chainId: chainId || WAITLIST_DEFAULT_CHAIN_ID, + query: { + enabled: !!contractAddress && !!chainId, + }, + }); + + const { data: totalCount, refetch: refetchTotalCount } = useReadContract({ + address: contractAddress, + abi: WaitlistAbi, + functionName: 'getTotalCount', + chainId: chainId || WAITLIST_DEFAULT_CHAIN_ID, + query: { + enabled: !!contractAddress && !!chainId, + }, + }); + + const isCorrectChain = isWaitlistChainSupported(chainId); + + const refetchAll = () => { + refetchState(); + refetchCanSubmit(); + refetchCanWithdraw(); + refetchPaused(); + refetchTotalAmount(); + refetchTotalCount(); + }; + + return { + currentState: currentState as RequestsState | undefined, + canSubmit: canSubmit as boolean | undefined, + canWithdraw: canWithdraw as boolean | undefined, + paused: paused as boolean | undefined, + totalAmount: totalAmount as bigint | undefined, + totalCount: totalCount as bigint | undefined, + isCorrectChain, + refetchAll, + }; +} + +/** + * Hook to get user's request IDs + */ +export function useUserRequestIds(userAddress: `0x${string}` | undefined) { + const { chain } = useAccount(); + const chainId = chain?.id; + const contractAddress = getWaitlistContractAddress(chainId); + + const { data: requestIds, refetch } = useReadContract({ + address: contractAddress, + abi: WaitlistAbi, + functionName: 'getRequestsByUser', + args: userAddress ? [userAddress] : undefined, + chainId: chainId || WAITLIST_DEFAULT_CHAIN_ID, + query: { + enabled: !!userAddress && !!contractAddress && !!chainId, + }, + }); + + return { + requestIds: requestIds as bigint[] | undefined, + refetch, + }; +} + +/** + * Hook to get a specific request by ID + */ +export function useWaitlistRequest(requestId: bigint | undefined) { + const { chain } = useAccount(); + const chainId = chain?.id; + const contractAddress = getWaitlistContractAddress(chainId); + + const { data: request, refetch } = useReadContract({ + address: contractAddress, + abi: WaitlistAbi, + functionName: 'getRequestById', + args: requestId !== undefined ? [requestId] : undefined, + chainId: chainId || WAITLIST_DEFAULT_CHAIN_ID, + query: { + enabled: requestId !== undefined && !!contractAddress && !!chainId, + }, + }); + + return { + request: request as WaitlistRequest | undefined, + refetch, + }; +} + +/** + * Hook to create a waitlist request + */ +export function useCreateWaitlistRequest() { + const { chain } = useAccount(); + const chainId = chain?.id; + const contractAddress = getWaitlistContractAddress(chainId); + + const { + writeContractAsync, + data: hash, + isPending, + error, + } = useWriteContract(); + + const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ + hash, + chainId: chainId || WAITLIST_DEFAULT_CHAIN_ID, + }); + + const createRequest = async ( + amount: bigint, + source: FundsSource, + backendSignature: `0x${string}`, + ) => { + if (!writeContractAsync) { + throw new Error('Wallet not connected'); + } + + if (!contractAddress) { + throw new Error('Waitlist contract not available on this chain'); + } + + if (!chainId) { + throw new Error('Chain ID not available'); + } + + return writeContractAsync({ + address: contractAddress, + abi: WaitlistAbi, + functionName: 'createRequest', + args: [amount, source, backendSignature], + chainId, + }); + }; + + return { + createRequest, + hash, + isPending, + isConfirming, + isSuccess, + error, + }; +} + +/** + * Hook to cancel a waitlist request + */ +export function useCancelWaitlistRequest() { + const { chain } = useAccount(); + const chainId = chain?.id; + const contractAddress = getWaitlistContractAddress(chainId); + + const { + writeContractAsync, + data: hash, + isPending, + error, + } = useWriteContract(); + + const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ + hash, + chainId: chainId || WAITLIST_DEFAULT_CHAIN_ID, + }); + + const cancelRequest = async (requestId: bigint) => { + if (!writeContractAsync) { + throw new Error('Wallet not connected'); + } + + if (!contractAddress) { + throw new Error('Waitlist contract not available on this chain'); + } + + if (!chainId) { + throw new Error('Chain ID not available'); + } + + return writeContractAsync({ + address: contractAddress, + abi: WaitlistAbi, + functionName: 'cancelRequest', + args: [requestId], + chainId, + }); + }; + + return { + cancelRequest, + hash, + isPending, + isConfirming, + isSuccess, + error, + }; +} diff --git a/libs/burn-waitlist/src/lib/hooks/use-waitlist-form.ts b/libs/burn-waitlist/src/lib/hooks/use-waitlist-form.ts new file mode 100644 index 000000000..18be3c3e1 --- /dev/null +++ b/libs/burn-waitlist/src/lib/hooks/use-waitlist-form.ts @@ -0,0 +1,155 @@ +'use client'; + +import { useState, useCallback, useMemo } from 'react'; +import { parseEther, formatEther } from 'viem'; +import { FundsSource } from '../constants/waitlist-config'; + +export interface WaitlistFormState { + amount: string; + source: FundsSource; + errors: { + amount?: string; + }; +} + +interface UseWaitlistFormParams { + availableBalance?: bigint; + onSubmit?: (amount: bigint, source: FundsSource) => Promise; +} + +interface UseWaitlistFormReturn { + formState: WaitlistFormState; + setAmount: (amount: string) => void; + setSource: (source: FundsSource) => void; + handleMaxClick: () => void; + handleSubmit: () => Promise; + isValid: boolean; + formattedAmount: bigint | undefined; +} + +/** + * Hook to manage waitlist participation form state + * Note: availableBalance should be recalculated externally when source changes + */ +export function useWaitlistForm({ + availableBalance, + onSubmit, +}: UseWaitlistFormParams = {}): UseWaitlistFormReturn { + const [formState, setFormState] = useState({ + amount: '', + source: FundsSource.OwnBalance, + errors: {}, + }); + + const setAmount = useCallback((amount: string) => { + setFormState((prev) => ({ + ...prev, + amount, + errors: { + ...prev.errors, + amount: undefined, + }, + })); + }, []); + + const setSource = useCallback((source: FundsSource) => { + setFormState((prev) => ({ + ...prev, + source, + })); + }, []); + + const handleMaxClick = useCallback(() => { + if (availableBalance) { + const formatted = formatEther(availableBalance); + setAmount(formatted); + } + }, [availableBalance, setAmount]); + + const formattedAmount = useMemo(() => { + if (!formState.amount || formState.amount === '') { + return undefined; + } + + try { + const parsed = parseFloat(formState.amount); + if (isNaN(parsed) || parsed <= 0) { + return undefined; + } + return parseEther(formState.amount); + } catch { + return undefined; + } + }, [formState.amount]); + + const validate = useCallback((): boolean => { + const errors: WaitlistFormState['errors'] = {}; + + if (!formState.amount || formState.amount === '') { + errors.amount = 'Amount is required'; + } else { + const parsed = parseFloat(formState.amount); + if (isNaN(parsed) || parsed <= 0) { + errors.amount = 'Amount must be greater than 0'; + } else if ( + formattedAmount && + availableBalance && + formattedAmount > availableBalance + ) { + errors.amount = 'Insufficient balance'; + } + } + + setFormState((prev) => ({ + ...prev, + errors, + })); + + return Object.keys(errors).length === 0; + }, [formState.amount, formattedAmount, availableBalance]); + + const handleSubmit = useCallback(async () => { + if (!validate()) { + return; + } + + if (!formattedAmount) { + return; + } + + if (onSubmit) { + await onSubmit(formattedAmount, formState.source); + } + }, [validate, formattedAmount, formState.source, onSubmit]); + + const isValid = useMemo(() => { + if (!formState.amount || formState.amount === '') { + return false; + } + + const parsed = parseFloat(formState.amount); + if (isNaN(parsed) || parsed <= 0) { + return false; + } + + if ( + formattedAmount && + availableBalance && + formattedAmount > availableBalance + ) { + return false; + } + + return true; + }, [formState.amount, formattedAmount, availableBalance]); + + return { + formState, + setAmount, + setSource, + handleMaxClick, + handleSubmit, + isValid, + formattedAmount, + }; +} diff --git a/libs/burn-waitlist/src/lib/hooks/use-waitlist-requests.ts b/libs/burn-waitlist/src/lib/hooks/use-waitlist-requests.ts new file mode 100644 index 000000000..954fb7aee --- /dev/null +++ b/libs/burn-waitlist/src/lib/hooks/use-waitlist-requests.ts @@ -0,0 +1,72 @@ +'use client'; + +import { useMemo } from 'react'; +import { useAccount } from 'wagmi'; +import { useUserRequestIds, useWaitlistRequest } from './use-waitlist-contract'; + +/** + * Hook to fetch and manage all user requests + */ +export function useWaitlistRequests() { + const { address } = useAccount(); + + const { requestIds, refetch: refetchRequestIds } = useUserRequestIds( + address as `0x${string}` | undefined, + ); + + // Fetch all request details + const requests = useMemo(() => { + if (!requestIds || requestIds.length === 0) { + return []; + } + + return requestIds.map((id) => ({ id })); + }, [requestIds]); + + const refetchAll = () => { + refetchRequestIds(); + }; + + return { + requestIds: requestIds || [], + requests, + refetchAll, + hasRequests: (requestIds?.length ?? 0) > 0, + }; +} + +/** + * Hook to get a single request with details + */ +export function useWaitlistRequestDetails(requestId: bigint | undefined) { + const { request, refetch } = useWaitlistRequest(requestId); + + return { + request, + refetch, + isLoading: request === undefined && requestId !== undefined, + }; +} + +/** + * Hook to get all user requests with full details + */ +export function useUserWaitlistRequests() { + const { address } = useAccount(); + const { requestIds, refetch: refetchRequestIds } = useUserRequestIds( + address as `0x${string}` | undefined, + ); + + // This would ideally batch fetch all requests, but for now we'll fetch them individually + // In a production app, you might want to use multicall or a backend API + + const refetchAll = () => { + refetchRequestIds(); + }; + + return { + requestIds: requestIds || [], + refetchAll, + hasRequests: (requestIds?.length ?? 0) > 0, + }; +} diff --git a/libs/burn-waitlist/src/lib/server.ts b/libs/burn-waitlist/src/lib/server.ts new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/libs/burn-waitlist/src/lib/server.ts @@ -0,0 +1 @@ +export {}; diff --git a/libs/burn-waitlist/src/lib/waitlist-page.tsx b/libs/burn-waitlist/src/lib/waitlist-page.tsx new file mode 100644 index 000000000..b071b7cc5 --- /dev/null +++ b/libs/burn-waitlist/src/lib/waitlist-page.tsx @@ -0,0 +1,295 @@ +'use client'; + +import { useEffect, useState, useCallback, useMemo } from 'react'; +import { useAccount, useBalance, useSwitchChain } from 'wagmi'; +import { formatEther } from 'viem'; +import { Container } from '@haqq/shell-ui-kit/server'; +import { useAddress, useDaoAllBalancesQuery } from '@haqq/shell-shared'; +import { + useWaitlistContractState, + useCreateWaitlistRequest, + useCancelWaitlistRequest, + useUserRequestIds, +} from './hooks'; +import { useBackendSignature } from './hooks/use-backend-signature'; +import { useWaitlistForm } from './hooks/use-waitlist-form'; +import { + ParticipationForm, + RequestsList, + StatusMessages, + WalletConnectionWarning, + NetworkWarning, +} from './components'; +import { + FundsSource, + WAITLIST_DEFAULT_CHAIN_ID, +} from './constants/waitlist-config'; + +export function WaitlistPage() { + const { address, isConnected, chain } = useAccount(); + const { switchChainAsync } = useSwitchChain(); + + // Contract state + const { + currentState, + canSubmit, + canWithdraw, + paused, + isCorrectChain, + refetchAll: refetchContractState, + } = useWaitlistContractState(); + + // User address conversion + const { haqqAddress } = useAddress(); + + // User wallet balance (EVM) + const { data: walletBalance, refetch: refetchBalance } = useBalance({ + address: address as `0x${string}` | undefined, + chainId: chain?.id || WAITLIST_DEFAULT_CHAIN_ID, + }); + + // DAO balances (Cosmos) + const { data: daoBalances } = useDaoAllBalancesQuery(haqqAddress); + + // Create request + const { + createRequest: createRequestTx, + isPending: isCreating, + isConfirming: isConfirmingCreate, + isSuccess: isCreateSuccess, + error: createError, + } = useCreateWaitlistRequest(); + + // Cancel request + const { + cancelRequest: cancelRequestTx, + isPending: isCancelling, + isConfirming: isConfirmingCancel, + error: cancelError, + } = useCancelWaitlistRequest(); + + const [cancellingRequestId, setCancellingRequestId] = useState< + bigint | undefined + >(); + + // Backend signature + const { getSignature, isLoading: isLoadingSignature } = useBackendSignature(); + + const onSubmit = useCallback( + async (amount: bigint, source: FundsSource) => { + if (!address) { + throw new Error('Wallet not connected'); + } + + try { + // Get backend signature + const signature = await getSignature(address, amount, source); + + // Create request on chain + await createRequestTx(amount, source, signature); + } catch (error) { + console.error('Failed to create request:', error); + throw error; + } + }, + [address, getSignature, createRequestTx], + ); + + // Form state - initialize with default balance + const { + formState, + setAmount, + setSource, + handleMaxClick, + isValid, + formattedAmount, + } = useWaitlistForm({ + availableBalance: walletBalance?.value, + onSubmit: onSubmit, + }); + + // Calculate available balance based on source + const availableBalance = useMemo(() => { + if (formState.source === FundsSource.OwnBalance) { + return walletBalance?.value; + } else { + // For ucDAO, get the native token (aISLM) balance + const nativeToken = daoBalances?.find((coin) => { + return coin.denom === 'aISLM'; + }); + if (nativeToken) { + return BigInt(nativeToken.amount); + } + return 0n; + } + }, [formState.source, walletBalance?.value, daoBalances]); + + // User requests + const { requestIds, refetch: refetchRequestIds } = useUserRequestIds( + address as `0x${string}` | undefined, + ); + + // Handle form submission + const handleSubmit = useCallback(async () => { + if (!isValid || !formattedAmount) { + return; + } + + try { + if (!address) { + throw new Error('Wallet not connected'); + } + + // Get backend signature + const signature = await getSignature( + address, + formattedAmount, + formState.source, + ); + + // Create request on chain + await createRequestTx(formattedAmount, formState.source, signature); + } catch (error) { + console.error('Failed to create request:', error); + } + }, [ + isValid, + formattedAmount, + address, + formState.source, + getSignature, + createRequestTx, + ]); + + // Handle cancel request + const handleCancel = useCallback( + async (requestId: bigint) => { + try { + setCancellingRequestId(requestId); + await cancelRequestTx(requestId); + // Refetch requests after cancellation + setTimeout(() => { + refetchRequestIds(); + refetchContractState(); + }, 2000); + } catch (error) { + console.error('Failed to cancel request:', error); + } finally { + setCancellingRequestId(undefined); + } + }, + [cancelRequestTx, refetchRequestIds, refetchContractState], + ); + + // Refetch after successful creation + useEffect(() => { + if (isCreateSuccess) { + refetchRequestIds(); + refetchContractState(); + refetchBalance(); + // Reset form + setAmount(''); + } + }, [ + isCreateSuccess, + refetchRequestIds, + refetchContractState, + refetchBalance, + setAmount, + ]); + + // Switch chain handler - switch to first supported chain + const handleSwitchChain = useCallback(async () => { + try { + await switchChainAsync({ chainId: WAITLIST_DEFAULT_CHAIN_ID }); + } catch (error) { + console.error('Failed to switch chain:', error); + } + }, [switchChainAsync]); + + const isSubmitting = isCreating || isConfirmingCreate || isLoadingSignature; + const errorMessage = + createError?.message || cancelError?.message || undefined; + + const showForm = canSubmit && !paused && isCorrectChain; + + return ( + +
+
+

+ Burn Waitlist +

+ + {!isConnected && } + + {isConnected && ( + <> + + + {!isCorrectChain && ( + + )} + + {showForm && ( +
+

+ Participate in Waitlist +

+ { + setAmount(amount); + }} + onSourceChange={(source) => { + setSource(source); + // Reset amount when source changes + setAmount(''); + }} + onMaxClick={() => { + if (availableBalance) { + const formatted = formatEther(availableBalance); + setAmount(formatted); + } + }} + onSubmit={handleSubmit} + isValid={ + isValid && + formattedAmount !== undefined && + formattedAmount <= (availableBalance || 0n) + } + isSubmitting={isSubmitting} + error={errorMessage} + amountError={formState.errors.amount} + /> +
+ )} + + {isConnected && requestIds && requestIds.length > 0 && ( +
+ +
+ )} + + )} +
+
+
+ ); +} diff --git a/libs/burn-waitlist/tsconfig.json b/libs/burn-waitlist/tsconfig.json new file mode 100644 index 000000000..5b656e873 --- /dev/null +++ b/libs/burn-waitlist/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "composite": true, + "declaration": true, + "resolveJsonModule": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../tsconfig.base.json" +} diff --git a/libs/burn-waitlist/tsconfig.lib.json b/libs/burn-waitlist/tsconfig.lib.json new file mode 100644 index 000000000..08e579bc8 --- /dev/null +++ b/libs/burn-waitlist/tsconfig.lib.json @@ -0,0 +1,25 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": [ + "node", + "@nx/react/typings/cssmodule.d.ts", + "@nx/react/typings/image.d.ts", + "next", + "@nx/next/typings/image.d.ts" + ] + }, + "exclude": [ + "jest.config.ts", + "src/**/*.spec.ts", + "src/**/*.test.ts", + "src/**/*.spec.tsx", + "src/**/*.test.tsx", + "src/**/*.spec.js", + "src/**/*.test.js", + "src/**/*.spec.jsx", + "src/**/*.test.jsx" + ], + "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] +} diff --git a/libs/burn-waitlist/tsconfig.spec.json b/libs/burn-waitlist/tsconfig.spec.json new file mode 100644 index 000000000..26ef046ac --- /dev/null +++ b/libs/burn-waitlist/tsconfig.spec.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] +} diff --git a/package.json b/package.json index 58a23e8ac..3a8368152 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "next": "14.2.23", "next-intl": "3.26.5", "posthog-js": "1.229.0", + "prettier-plugin-solidity": "2.2.1", "react": "18.3.1", "react-countdown": "2.3.6", "react-dom": "18.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b98c905be..20c358a31 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -125,6 +125,9 @@ importers: posthog-js: specifier: 1.229.0 version: 1.229.0 + prettier-plugin-solidity: + specifier: 2.2.1 + version: 2.2.1(prettier@3.5.3) react: specifier: 18.3.1 version: 18.3.1 @@ -1218,6 +1221,9 @@ packages: '@bufbuild/protobuf@1.10.0': resolution: {integrity: sha512-QDdVFLoN93Zjg36NoQPZfsVH9tZew7wKDKyV5qRdj8ntT4wQCOradQjRaTdwMhWUYsgKsvCINKKm87FdEk96Ag==} + '@bytecodealliance/preview2-shim@0.17.6': + resolution: {integrity: sha512-n3cM88gTen5980UOBAD6xDcNNL3ocTK8keab21bpx1ONdA+ARj7uD1qoFxOWCyKlkpSi195FH+GeAut7Oc6zZw==} + '@coinbase/wallet-sdk@3.9.3': resolution: {integrity: sha512-N/A2DRIf0Y3PHc1XAMvbBUu4zisna6qAdqABMZwBMNEfWrXpAwx16pZGkYCLGE+Rvv1edbcB2LYDRnACNcmCiw==} @@ -2927,6 +2933,9 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} + '@nomicfoundation/slang@1.3.1': + resolution: {integrity: sha512-gh0+JDjazmevEYCcwVgtuyfBJcV1209gIORZNRjUxbGzbQN0MOhQO9T0ptkzHKCf854gUy27SMxPbAyAu63fvQ==} + '@nx/cypress@20.4.6': resolution: {integrity: sha512-Wh9OXWXBjEpt1ZMh4QGrGNkI0AyjeFaxrP2cSEqfFro8YFG1L081GeGXnJ5WzhYcr4bhIMSqflXg/q4W0x1YLw==} peerDependencies: @@ -3941,6 +3950,9 @@ packages: '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + '@solidity-parser/parser@0.20.2': + resolution: {integrity: sha512-rbu0bzwNvMcwAjH86hiEAcOeRI2EeK8zCkHDrFykh/Al8mvJeFmjy3UrE7GYQjNwOgbGUUtCn5/k8CB8zIu7QA==} + '@starknet-io/types-js@0.7.10': resolution: {integrity: sha512-1VtCqX4AHWJlRRSYGSn+4X1mqolI1Tdq62IwzoU2vUuEE72S1OlEeGhpvd6XsdqXcfHmVzYfj8k1XtKBQqwo9w==} @@ -10695,6 +10707,12 @@ packages: resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} engines: {node: '>=6.0.0'} + prettier-plugin-solidity@2.2.1: + resolution: {integrity: sha512-LOHfxECJ/gHsY7qi4D7vanz8cVsCf6yFotBapJ5O0qaX0ZR1sGUzbWfMd4JeQYOItFl+wXW9IcjZOdfr6bmSvQ==} + engines: {node: '>=20'} + peerDependencies: + prettier: '>=3.0.0' + prettier-plugin-tailwindcss@0.6.10: resolution: {integrity: sha512-ndj2WLDaMzACnr1gAYZiZZLs5ZdOeBYgOsbBmHj3nvW/6q8h8PymsXiEnKvj/9qgCCAoHyvLOisoQdIcsDvIgw==} engines: {node: '>=14.21.3'} @@ -12863,6 +12881,7 @@ packages: whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-fetch@3.6.20: resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} @@ -14313,6 +14332,8 @@ snapshots: '@bufbuild/protobuf@1.10.0': {} + '@bytecodealliance/preview2-shim@0.17.6': {} + '@coinbase/wallet-sdk@3.9.3': dependencies: bn.js: 5.2.2 @@ -15669,7 +15690,7 @@ snapshots: https-proxy-agent: 7.0.6 node-fetch: 2.7.0(encoding@0.1.13) nopt: 8.0.0 - semver: 7.7.1 + semver: 7.7.3 tar: 7.4.3 transitivePeerDependencies: - encoding @@ -15831,7 +15852,7 @@ snapshots: debug: 4.4.0(supports-color@8.1.1) lodash: 4.17.21 pony-cause: 2.1.11 - semver: 7.7.1 + semver: 7.7.3 uuid: 9.0.1 transitivePeerDependencies: - supports-color @@ -15855,7 +15876,7 @@ snapshots: '@types/debug': 4.1.12 debug: 4.4.0(supports-color@8.1.1) pony-cause: 2.1.11 - semver: 7.7.1 + semver: 7.7.3 uuid: 9.0.1 transitivePeerDependencies: - supports-color @@ -15869,7 +15890,7 @@ snapshots: '@types/debug': 4.1.12 debug: 4.4.0(supports-color@8.1.1) pony-cause: 2.1.11 - semver: 7.7.1 + semver: 7.7.3 uuid: 9.0.1 transitivePeerDependencies: - supports-color @@ -16375,6 +16396,10 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} + '@nomicfoundation/slang@1.3.1': + dependencies: + '@bytecodealliance/preview2-shim': 0.17.6 + '@nx/cypress@20.4.6(@babel/traverse@7.26.9)(@swc-node/register@1.10.9(@swc/core@1.11.7(@swc/helpers@0.5.13))(@swc/types@0.1.19)(typescript@5.8.2))(@swc/core@1.11.7(@swc/helpers@0.5.13))(@types/node@22.13.9)(@zkochan/js-yaml@0.0.7)(cypress@13.17.0)(eslint@8.57.0)(nx@20.4.6(@swc-node/register@1.10.9(@swc/core@1.11.7(@swc/helpers@0.5.13))(@swc/types@0.1.19)(typescript@5.8.2))(@swc/core@1.11.7(@swc/helpers@0.5.13)))(typescript@5.8.2)': dependencies: '@nx/devkit': 20.4.6(nx@20.4.6(@swc-node/register@1.10.9(@swc/core@1.11.7(@swc/helpers@0.5.13))(@swc/types@0.1.19)(typescript@5.8.2))(@swc/core@1.11.7(@swc/helpers@0.5.13))) @@ -16983,7 +17008,7 @@ snapshots: '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.28.0 forwarded-parse: 2.1.2 - semver: 7.7.1 + semver: 7.7.3 transitivePeerDependencies: - supports-color @@ -17108,7 +17133,7 @@ snapshots: '@types/shimmer': 1.2.0 import-in-the-middle: 1.13.1 require-in-the-middle: 7.5.2 - semver: 7.7.1 + semver: 7.7.3 shimmer: 1.2.1 transitivePeerDependencies: - supports-color @@ -18053,6 +18078,8 @@ snapshots: '@socket.io/component-emitter@3.1.2': {} + '@solidity-parser/parser@0.20.2': {} + '@starknet-io/types-js@0.7.10': {} '@storybook/addon-actions@8.6.4(storybook@8.6.4(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@5.0.10))': @@ -20766,7 +20793,7 @@ snapshots: bin-version-check@5.1.0: dependencies: bin-version: 6.0.0 - semver: 7.6.3 + semver: 7.7.3 semver-truncate: 3.0.0 bin-version@6.0.0: @@ -23269,7 +23296,7 @@ snapshots: minimatch: 3.1.2 node-abort-controller: 3.1.1 schema-utils: 3.3.0 - semver: 7.6.3 + semver: 7.7.3 tapable: 2.2.1 typescript: 5.8.2 webpack: 5.96.1(@swc/core@1.11.7(@swc/helpers@0.5.13))(esbuild@0.23.1) @@ -24037,7 +24064,7 @@ snapshots: is-bun-module@1.2.1: dependencies: - semver: 7.7.1 + semver: 7.7.3 is-callable@1.2.7: {} @@ -24624,7 +24651,7 @@ snapshots: jest-util: 29.7.0 natural-compare: 1.4.0 pretty-format: 29.7.0 - semver: 7.7.1 + semver: 7.7.3 transitivePeerDependencies: - supports-color @@ -25222,7 +25249,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.6.3 + semver: 7.7.3 make-error@1.3.6: {} @@ -26785,6 +26812,13 @@ snapshots: dependencies: fast-diff: 1.3.0 + prettier-plugin-solidity@2.2.1(prettier@3.5.3): + dependencies: + '@nomicfoundation/slang': 1.3.1 + '@solidity-parser/parser': 0.20.2 + prettier: 3.5.3 + semver: 7.7.3 + prettier-plugin-tailwindcss@0.6.10(prettier@3.5.3): dependencies: prettier: 3.5.3 @@ -27455,7 +27489,7 @@ snapshots: semver-truncate@3.0.0: dependencies: - semver: 7.6.3 + semver: 7.7.3 semver@5.7.2: {} diff --git a/tsconfig.base.json b/tsconfig.base.json index e3ca1fbb1..1bce69ebf 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -24,6 +24,7 @@ "@haqq/shell-authz": ["libs/authz/src/index.ts"], "@haqq/shell-authz/server": ["libs/authz/src/server.ts"], "@haqq/shell-bridge": ["libs/bridge/src/index.ts"], + "@haqq/shell-burn-waitlist": ["libs/burn-waitlist/src/index.ts"], "@haqq/shell-faucet": ["libs/faucet/src/index.ts"], "@haqq/shell-faucet/server": ["libs/faucet/src/server.ts"], "@haqq/shell-governance": ["libs/governance/src/index.ts"], From 0fbabd316972dd6f270b2c5b07fcdd2862e1bbfc Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Mon, 5 Jan 2026 14:58:12 +0300 Subject: [PATCH 083/141] refactor: disconnect btn and waitlist form enhancements --- .../src/components/web3-connect-button.tsx | 25 +- libs/burn-waitlist/src/lib/abi/waitlist.ts | 255 +++++++++++++++++- .../src/lib/components/network-warning.tsx | 4 +- .../src/lib/components/status-messages.tsx | 2 +- .../src/lib/constants/waitlist-config.ts | 9 +- .../src/lib/hooks/use-backend-signature.ts | 3 + .../src/lib/hooks/use-waitlist-contract.ts | 38 ++- libs/burn-waitlist/src/lib/waitlist-page.tsx | 42 ++- libs/ui-kit/src/lib/account-button.tsx | 18 +- 9 files changed, 362 insertions(+), 34 deletions(-) diff --git a/apps/shell/src/components/web3-connect-button.tsx b/apps/shell/src/components/web3-connect-button.tsx index 1d1fbd6ab..78a96e9d4 100644 --- a/apps/shell/src/components/web3-connect-button.tsx +++ b/apps/shell/src/components/web3-connect-button.tsx @@ -14,7 +14,7 @@ import { baseSupportedChains, bridgeSupportedChains } from '@haqq/shell-shared'; import { Button, AccountButton, SelectChainButton } from '@haqq/shell-ui-kit'; import { formatNumber } from '@haqq/shell-ui-kit/server'; -function useIsBridgePage() { +function useIsEthiqSupportedPage() { const pathname = usePathname(); return useMemo(() => { return pathname?.startsWith('/bridge'); @@ -30,13 +30,13 @@ function useIsFaucetPage() { function useChainArray() { const chains = useChains(); - const isBridgePage = useIsBridgePage(); + const isEthiqSupportedPage = useIsEthiqSupportedPage(); const isFaucetPage = useIsFaucetPage(); return useMemo(() => { const availableChains = isFaucetPage ? faucetSupportedChains - : isBridgePage + : isEthiqSupportedPage ? bridgeSupportedChains : baseSupportedChains; @@ -49,7 +49,7 @@ function useChainArray() { }); } - return (isBridgePage ? bridgeSupportedChains : availableChains).map( + return (isEthiqSupportedPage ? bridgeSupportedChains : availableChains).map( (chain) => { return { id: chain.id, @@ -57,7 +57,7 @@ function useChainArray() { }; }, ); - }, [chains, isBridgePage, isFaucetPage]); + }, [chains, isEthiqSupportedPage, isFaucetPage]); } export function Web3ConnectButtons() { @@ -67,7 +67,7 @@ export function Web3ConnectButtons() { const { openSelectWallet, disconnect, selectNetwork } = useWallet(); const { data: balance } = useIndexerBalanceQuery(haqqAddress); const chainArray = useChainArray(); - const isBridgePage = useIsBridgePage(); + const isEthiqSupportedPage = useIsEthiqSupportedPage(); if (!isConnected || !ethAddress) { return ( @@ -104,13 +104,14 @@ export function Web3ConnectButtons() { chains={chainArray} />
- {isBridgePage ? ( + {isEthiqSupportedPage ? ( ) : (
)} @@ -126,7 +127,7 @@ export function Web3ConnectButtonsMobile() { const { data: balance } = useIndexerBalanceQuery(haqqAddress); const chainArray = useChainArray(); - const isBridgePage = useIsBridgePage(); + const isEthiqSupportedPage = useIsEthiqSupportedPage(); if (!isConnected || !ethAddress) { return ( @@ -158,13 +159,14 @@ export function Web3ConnectButtonsMobile() { dropdownClassName="end-auto start-0" />
- {isBridgePage ? ( + {isEthiqSupportedPage ? ( ) : (
)} @@ -181,10 +183,12 @@ const BridgePageAccountBtn = ({ withoutDropdown?: boolean; }) => { const { data: balance } = useBalance(); + const { disconnect } = useWallet(); return ( ); }; @@ -192,9 +196,11 @@ const BridgePageAccountBtn = ({ const AccountBtnWrapper = ({ balance, withoutDropdown, + onDisconnectClick, }: { balance: string | undefined; withoutDropdown?: boolean; + onDisconnectClick?: () => void; }) => { const { ethAddress } = useAddress(); return ( @@ -202,6 +208,7 @@ const AccountBtnWrapper = ({ balance={balance} address={getFormattedAddress(ethAddress, 3, 2)} withoutDropdown={withoutDropdown} + onDisconnectClick={onDisconnectClick} /> ); }; diff --git a/libs/burn-waitlist/src/lib/abi/waitlist.ts b/libs/burn-waitlist/src/lib/abi/waitlist.ts index 11b22ef0e..9d863130e 100644 --- a/libs/burn-waitlist/src/lib/abi/waitlist.ts +++ b/libs/burn-waitlist/src/lib/abi/waitlist.ts @@ -1,4 +1,4 @@ -// ABI for Waitlist contract +// ABI for Waitlist contract - Updated from deployments/54211/abi.json export const WaitlistAbi = [ { inputs: [ @@ -41,11 +41,53 @@ export const WaitlistAbi = [ name: 'CannotSubmitRequests', type: 'error', }, + { + inputs: [], + name: 'ECDSAInvalidSignature', + type: 'error', + }, + { + inputs: [ + { + internalType: 'uint256', + name: 'length', + type: 'uint256', + }, + ], + name: 'ECDSAInvalidSignatureLength', + type: 'error', + }, + { + inputs: [ + { + internalType: 'bytes32', + name: 's', + type: 'bytes32', + }, + ], + name: 'ECDSAInvalidSignatureS', + type: 'error', + }, + { + inputs: [], + name: 'EnforcedPause', + type: 'error', + }, + { + inputs: [], + name: 'ExpectedPause', + type: 'error', + }, { inputs: [], name: 'InvalidBackendSignature', type: 'error', }, + { + inputs: [], + name: 'InvalidNonce', + type: 'error', + }, { inputs: [], name: 'InvalidState', @@ -56,6 +98,28 @@ export const WaitlistAbi = [ name: 'OnlyAuthorCanCancel', type: 'error', }, + { + inputs: [ + { + internalType: 'address', + name: 'owner', + type: 'address', + }, + ], + name: 'OwnableInvalidOwner', + type: 'error', + }, + { + inputs: [ + { + internalType: 'address', + name: 'account', + type: 'address', + }, + ], + name: 'OwnableUnauthorizedAccount', + type: 'error', + }, { inputs: [], name: 'RequestAlreadyCancelled', @@ -90,6 +154,38 @@ export const WaitlistAbi = [ name: 'BackendSignerChanged', type: 'event', }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'previousOwner', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'newOwner', + type: 'address', + }, + ], + name: 'OwnershipTransferred', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'address', + name: 'account', + type: 'address', + }, + ], + name: 'Paused', + type: 'event', + }, { anonymous: false, inputs: [ @@ -164,6 +260,32 @@ export const WaitlistAbi = [ name: 'RequestsOpened', type: 'event', }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'address', + name: 'account', + type: 'address', + }, + ], + name: 'Unpaused', + type: 'event', + }, + { + inputs: [], + name: 'VERSION', + outputs: [ + { + internalType: 'string', + name: '', + type: 'string', + }, + ], + stateMutability: 'view', + type: 'function', + }, { inputs: [], name: 'backendSigner', @@ -216,6 +338,13 @@ export const WaitlistAbi = [ stateMutability: 'nonpayable', type: 'function', }, + { + inputs: [], + name: 'closeRequests', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, { inputs: [ { @@ -228,6 +357,11 @@ export const WaitlistAbi = [ name: 'source', type: 'uint8', }, + { + internalType: 'uint256', + name: 'nonce', + type: 'uint256', + }, { internalType: 'bytes', name: 'backendSignature', @@ -252,6 +386,13 @@ export const WaitlistAbi = [ stateMutability: 'view', type: 'function', }, + { + inputs: [], + name: 'finalizeRequests', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, { inputs: [ { @@ -348,6 +489,65 @@ export const WaitlistAbi = [ stateMutability: 'view', type: 'function', }, + { + inputs: [], + name: 'getTotalRequestsCount', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'user', + type: 'address', + }, + ], + name: 'getUserNonce', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'openRequests', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'owner', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'pause', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, { inputs: [], name: 'paused', @@ -361,4 +561,57 @@ export const WaitlistAbi = [ stateMutability: 'view', type: 'function', }, + { + inputs: [], + name: 'renounceOwnership', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'newSigner', + type: 'address', + }, + ], + name: 'setBackendSigner', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'newOwner', + type: 'address', + }, + ], + name: 'transferOwnership', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'unpause', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'version', + outputs: [ + { + internalType: 'string', + name: '', + type: 'string', + }, + ], + stateMutability: 'pure', + type: 'function', + }, ] as const; diff --git a/libs/burn-waitlist/src/lib/components/network-warning.tsx b/libs/burn-waitlist/src/lib/components/network-warning.tsx index 061077ac9..709dfde07 100644 --- a/libs/burn-waitlist/src/lib/components/network-warning.tsx +++ b/libs/burn-waitlist/src/lib/components/network-warning.tsx @@ -26,9 +26,9 @@ export function NetworkWarning({ onSwitchChain }: NetworkWarningProps) { return (
- Please switch to HAQQ Testethiq network to participate + Please switch to HAQQ Testedge2 network to participate
-
diff --git a/libs/burn-waitlist/src/lib/components/status-messages.tsx b/libs/burn-waitlist/src/lib/components/status-messages.tsx index 257610637..12d2def11 100644 --- a/libs/burn-waitlist/src/lib/components/status-messages.tsx +++ b/libs/burn-waitlist/src/lib/components/status-messages.tsx @@ -27,7 +27,7 @@ export function StatusMessages({ return (
- Please switch to HAQQ Testethiq network to participate + Please switch to HAQQ Testedge2 network to participate
); diff --git a/libs/burn-waitlist/src/lib/constants/waitlist-config.ts b/libs/burn-waitlist/src/lib/constants/waitlist-config.ts index f42d8e1f5..2d9a96582 100644 --- a/libs/burn-waitlist/src/lib/constants/waitlist-config.ts +++ b/libs/burn-waitlist/src/lib/constants/waitlist-config.ts @@ -1,14 +1,13 @@ -import { haqqTestethiq, haqqEthiq } from '@haqq/shell-shared'; +import { haqqTestedge2 } from 'viem/chains'; /** * Waitlist contract addresses mapped by chain ID * Key: chain ID, Value: contract address */ export const WAITLIST_CONTRACT_ADDRESSES: Record = { - [haqqTestethiq.id]: - '0xAFb74983668ff5cBE7340cb0DA014D7221c7947e' as `0x${string}`, + [haqqTestedge2.id]: + '0xafc36779f695a2946523C1B056A0Dd7583a0e019' as `0x${string}`, // Add more chain deployments here as they become available - // [haqqEthiq.id]: '0x...' as `0x${string}`, }; /** @@ -48,7 +47,7 @@ export function isWaitlistChainSupported(chainId?: number): boolean { * Default chain ID for waitlist (first supported chain) */ export const WAITLIST_DEFAULT_CHAIN_ID = - WAITLIST_SUPPORTED_CHAIN_IDS[0] || haqqTestethiq.id; + WAITLIST_SUPPORTED_CHAIN_IDS[0] || haqqTestedge2.id; /** * Backend API base URL diff --git a/libs/burn-waitlist/src/lib/hooks/use-backend-signature.ts b/libs/burn-waitlist/src/lib/hooks/use-backend-signature.ts index 65af18453..78b3f55e3 100644 --- a/libs/burn-waitlist/src/lib/hooks/use-backend-signature.ts +++ b/libs/burn-waitlist/src/lib/hooks/use-backend-signature.ts @@ -13,6 +13,7 @@ interface UseBackendSignatureReturn { userAddress: string, amount: bigint, source: FundsSource, + nonce: bigint, ) => Promise<`0x${string}`>; isLoading: boolean; error: Error | null; @@ -30,6 +31,7 @@ export function useBackendSignature(): UseBackendSignatureReturn { userAddress: string, amount: bigint, source: FundsSource, + nonce: bigint, ): Promise<`0x${string}`> => { setIsLoading(true); setError(null); @@ -45,6 +47,7 @@ export function useBackendSignature(): UseBackendSignatureReturn { user: userAddress, amount: amount.toString(), source: source, + nonce: nonce.toString(), }), }); diff --git a/libs/burn-waitlist/src/lib/hooks/use-waitlist-contract.ts b/libs/burn-waitlist/src/lib/hooks/use-waitlist-contract.ts index 936b1bc32..d7aca2d5b 100644 --- a/libs/burn-waitlist/src/lib/hooks/use-waitlist-contract.ts +++ b/libs/burn-waitlist/src/lib/hooks/use-waitlist-contract.ts @@ -31,7 +31,12 @@ export function useWaitlistContractState() { const chainId = chain?.id; const contractAddress = getWaitlistContractAddress(chainId); - const { data: currentState, refetch: refetchState } = useReadContract({ + const { + data: currentState, + error: currentStateError, + isLoading: currentStateLoading, + refetch: refetchState, + } = useReadContract({ address: contractAddress, abi: WaitlistAbi, functionName: 'currentState', @@ -41,6 +46,9 @@ export function useWaitlistContractState() { }, }); + console.log('currentState', contractAddress, chainId, currentState); + console.log('currentStateError', currentStateError); + console.log('currentStateLoading', currentStateLoading); const { data: canSubmit, refetch: refetchCanSubmit } = useReadContract({ address: contractAddress, abi: WaitlistAbi, @@ -164,6 +172,31 @@ export function useWaitlistRequest(requestId: bigint | undefined) { }; } +/** + * Hook to get user's nonce + */ +export function useUserNonce(userAddress: `0x${string}` | undefined) { + const { chain } = useAccount(); + const chainId = chain?.id; + const contractAddress = getWaitlistContractAddress(chainId); + + const { data: nonce, refetch } = useReadContract({ + address: contractAddress, + abi: WaitlistAbi, + functionName: 'getUserNonce', + args: userAddress ? [userAddress] : undefined, + chainId: chainId || WAITLIST_DEFAULT_CHAIN_ID, + query: { + enabled: !!userAddress && !!contractAddress && !!chainId, + }, + }); + + return { + nonce: nonce as bigint | undefined, + refetch, + }; +} + /** * Hook to create a waitlist request */ @@ -187,6 +220,7 @@ export function useCreateWaitlistRequest() { const createRequest = async ( amount: bigint, source: FundsSource, + nonce: bigint, backendSignature: `0x${string}`, ) => { if (!writeContractAsync) { @@ -205,7 +239,7 @@ export function useCreateWaitlistRequest() { address: contractAddress, abi: WaitlistAbi, functionName: 'createRequest', - args: [amount, source, backendSignature], + args: [amount, source, nonce, backendSignature], chainId, }); }; diff --git a/libs/burn-waitlist/src/lib/waitlist-page.tsx b/libs/burn-waitlist/src/lib/waitlist-page.tsx index b071b7cc5..993d04be1 100644 --- a/libs/burn-waitlist/src/lib/waitlist-page.tsx +++ b/libs/burn-waitlist/src/lib/waitlist-page.tsx @@ -10,6 +10,7 @@ import { useCreateWaitlistRequest, useCancelWaitlistRequest, useUserRequestIds, + useUserNonce, } from './hooks'; import { useBackendSignature } from './hooks/use-backend-signature'; import { useWaitlistForm } from './hooks/use-waitlist-form'; @@ -72,6 +73,11 @@ export function WaitlistPage() { bigint | undefined >(); + // User nonce - must be declared before onSubmit callback + const { nonce: userNonce, refetch: refetchNonce } = useUserNonce( + address as `0x${string}` | undefined, + ); + // Backend signature const { getSignature, isLoading: isLoadingSignature } = useBackendSignature(); @@ -81,18 +87,27 @@ export function WaitlistPage() { throw new Error('Wallet not connected'); } + if (userNonce === undefined) { + throw new Error('Nonce not available'); + } + try { - // Get backend signature - const signature = await getSignature(address, amount, source); + // Get backend signature with nonce + const signature = await getSignature( + address, + amount, + source, + userNonce, + ); // Create request on chain - await createRequestTx(amount, source, signature); + await createRequestTx(amount, source, userNonce, signature); } catch (error) { console.error('Failed to create request:', error); throw error; } }, - [address, getSignature, createRequestTx], + [address, getSignature, createRequestTx, userNonce], ); // Form state - initialize with default balance @@ -140,15 +155,25 @@ export function WaitlistPage() { throw new Error('Wallet not connected'); } - // Get backend signature + if (userNonce === undefined) { + throw new Error('Nonce not available'); + } + + // Get backend signature with nonce const signature = await getSignature( address, formattedAmount, formState.source, + userNonce, ); // Create request on chain - await createRequestTx(formattedAmount, formState.source, signature); + await createRequestTx( + formattedAmount, + formState.source, + userNonce, + signature, + ); } catch (error) { console.error('Failed to create request:', error); } @@ -157,6 +182,7 @@ export function WaitlistPage() { formattedAmount, address, formState.source, + userNonce, getSignature, createRequestTx, ]); @@ -187,6 +213,7 @@ export function WaitlistPage() { refetchRequestIds(); refetchContractState(); refetchBalance(); + refetchNonce(); // Reset form setAmount(''); } @@ -195,6 +222,7 @@ export function WaitlistPage() { refetchRequestIds, refetchContractState, refetchBalance, + refetchNonce, setAmount, ]); @@ -213,6 +241,8 @@ export function WaitlistPage() { const showForm = canSubmit && !paused && isCorrectChain; + console.log('isConnected', isConnected); + console.log('currentState', currentState); return (
diff --git a/libs/ui-kit/src/lib/account-button.tsx b/libs/ui-kit/src/lib/account-button.tsx index 35bff0672..2c4f8eef9 100644 --- a/libs/ui-kit/src/lib/account-button.tsx +++ b/libs/ui-kit/src/lib/account-button.tsx @@ -32,7 +32,7 @@ export function AccountButton({
)} - {!withoutDropdown && onDisconnectClick ? ( + {!withoutDropdown ? ( - - {t('disconnect', 'Disconnect')} - + {onDisconnectClick && ( + + {t('disconnect', 'Disconnect')} + + )} ) : ( From 832f4d703deeb456ff84ee198d8a0d5c903bbf2b Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Tue, 6 Jan 2026 11:18:59 +0300 Subject: [PATCH 084/141] feat: implement waitlist endpoints data fetching --- .../app/api/waitlist/applications/route.ts | 55 ++++++ .../api/waitlist/balances/[address]/route.ts | 48 +++++ .../src/app/api/waitlist/signature/route.ts | 47 +++++ libs/burn-waitlist/src/lib/abi/waitlist.ts | 32 +++- .../src/lib/components/participation-form.tsx | 24 ++- .../src/lib/components/requests-list.tsx | 89 ++++++++-- .../src/lib/components/waitlist-balances.tsx | 115 +++++++----- .../src/lib/constants/waitlist-config.ts | 7 +- libs/burn-waitlist/src/lib/hooks/index.ts | 2 + .../src/lib/hooks/use-backend-signature.ts | 9 +- .../lib/hooks/use-waitlist-applications.ts | 72 ++++++++ .../src/lib/hooks/use-waitlist-balances.ts | 42 +++++ .../src/lib/hooks/use-waitlist-contract.ts | 3 +- .../src/lib/hooks/use-waitlist-form.ts | 19 +- libs/burn-waitlist/src/lib/waitlist-page.tsx | 167 +++++++++--------- 15 files changed, 558 insertions(+), 173 deletions(-) create mode 100644 apps/shell/src/app/api/waitlist/applications/route.ts create mode 100644 apps/shell/src/app/api/waitlist/balances/[address]/route.ts create mode 100644 apps/shell/src/app/api/waitlist/signature/route.ts create mode 100644 libs/burn-waitlist/src/lib/hooks/use-waitlist-applications.ts create mode 100644 libs/burn-waitlist/src/lib/hooks/use-waitlist-balances.ts diff --git a/apps/shell/src/app/api/waitlist/applications/route.ts b/apps/shell/src/app/api/waitlist/applications/route.ts new file mode 100644 index 000000000..ef0f6ff38 --- /dev/null +++ b/apps/shell/src/app/api/waitlist/applications/route.ts @@ -0,0 +1,55 @@ +import { NextRequest, NextResponse } from 'next/server'; + +const WAITLIST_API_URL = + process.env.BURN_WAITLIST_API_URL || 'https://waitlist.vorobevsa.com'; + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + + // Forward query parameters to waitlist API + const params = new URLSearchParams(); + if (searchParams.get('address')) { + params.append('address', searchParams.get('address')!); + } + if (searchParams.get('status')) { + params.append('status', searchParams.get('status')!); + } + if (searchParams.get('page')) { + params.append('page', searchParams.get('page')!); + } + if (searchParams.get('pageSize')) { + params.append('pageSize', searchParams.get('pageSize')!); + } + + const queryString = params.toString(); + const url = queryString + ? `${WAITLIST_API_URL}/api/v1/applications?${queryString}` + : `${WAITLIST_API_URL}/api/v1/applications`; + + // Forward request to waitlist API + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + return NextResponse.json( + { error: errorText || `API error: ${response.status}` }, + { status: response.status }, + ); + } + + const data = await response.json(); + return NextResponse.json(data); + } catch (error) { + console.error('Error in waitlist applications API:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 }, + ); + } +} diff --git a/apps/shell/src/app/api/waitlist/balances/[address]/route.ts b/apps/shell/src/app/api/waitlist/balances/[address]/route.ts new file mode 100644 index 000000000..29614a1f9 --- /dev/null +++ b/apps/shell/src/app/api/waitlist/balances/[address]/route.ts @@ -0,0 +1,48 @@ +import { NextRequest, NextResponse } from 'next/server'; + +const WAITLIST_API_URL = + process.env.BURN_WAITLIST_API_URL || 'https://waitlist.vorobevsa.com'; + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ address: string }> }, +) { + try { + const { address } = await params; + + if (!address) { + return NextResponse.json( + { error: 'Address is required' }, + { status: 400 }, + ); + } + + // Forward request to waitlist API + const response = await fetch( + `${WAITLIST_API_URL}/api/v1/balances/${address}`, + { + method: 'GET', + headers: { + Accept: 'application/json', + }, + }, + ); + + if (!response.ok) { + const errorText = await response.text(); + return NextResponse.json( + { error: errorText || `API error: ${response.status}` }, + { status: response.status }, + ); + } + + const data = await response.json(); + return NextResponse.json(data); + } catch (error) { + console.error('Error in waitlist balances API:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 }, + ); + } +} diff --git a/apps/shell/src/app/api/waitlist/signature/route.ts b/apps/shell/src/app/api/waitlist/signature/route.ts new file mode 100644 index 000000000..e138974b2 --- /dev/null +++ b/apps/shell/src/app/api/waitlist/signature/route.ts @@ -0,0 +1,47 @@ +import { NextRequest, NextResponse } from 'next/server'; + +const WAITLIST_API_URL = + process.env.BURN_WAITLIST_API_URL || 'https://waitlist.vorobevsa.com'; + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + + // Validate required fields + if (!body.user || !body.amount || body.source === undefined) { + return NextResponse.json( + { error: 'Missing required fields: user, amount, source' }, + { status: 400 }, + ); + } + + // Forward request to waitlist API + const response = await fetch( + `${WAITLIST_API_URL}/api/v1/signer/signature`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }, + ); + + if (!response.ok) { + const errorText = await response.text(); + return NextResponse.json( + { error: errorText || `API error: ${response.status}` }, + { status: response.status }, + ); + } + + const data = await response.json(); + return NextResponse.json(data); + } catch (error) { + console.error('Error in waitlist signature API:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 }, + ); + } +} diff --git a/libs/burn-waitlist/src/lib/abi/waitlist.ts b/libs/burn-waitlist/src/lib/abi/waitlist.ts index 9d863130e..279c673d3 100644 --- a/libs/burn-waitlist/src/lib/abi/waitlist.ts +++ b/libs/burn-waitlist/src/lib/abi/waitlist.ts @@ -85,7 +85,7 @@ export const WaitlistAbi = [ }, { inputs: [], - name: 'InvalidNonce', + name: 'InvalidInitialOwner', type: 'error', }, { @@ -273,6 +273,31 @@ export const WaitlistAbi = [ name: 'Unpaused', type: 'event', }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'user', + type: 'address', + }, + { + indexed: true, + internalType: 'uint256', + name: 'oldNonce', + type: 'uint256', + }, + { + indexed: true, + internalType: 'uint256', + name: 'newNonce', + type: 'uint256', + }, + ], + name: 'UserNonceIncremented', + type: 'event', + }, { inputs: [], name: 'VERSION', @@ -357,11 +382,6 @@ export const WaitlistAbi = [ name: 'source', type: 'uint8', }, - { - internalType: 'uint256', - name: 'nonce', - type: 'uint256', - }, { internalType: 'bytes', name: 'backendSignature', diff --git a/libs/burn-waitlist/src/lib/components/participation-form.tsx b/libs/burn-waitlist/src/lib/components/participation-form.tsx index bb7f8edbd..e42570833 100644 --- a/libs/burn-waitlist/src/lib/components/participation-form.tsx +++ b/libs/burn-waitlist/src/lib/components/participation-form.tsx @@ -6,12 +6,14 @@ import { ModalInput } from '@haqq/shell-ui-kit'; import { FundsSource } from '../constants/waitlist-config'; import { formatEther } from 'viem'; import { WaitlistBalances } from './waitlist-balances'; +import type { WaitlistBalancesResponse } from '../hooks/use-waitlist-balances'; export interface ParticipationFormProps { amount: string; source: FundsSource; availableBalance?: bigint; walletBalance?: bigint; + balances?: WaitlistBalancesResponse; onAmountChange: (amount: string) => void; onSourceChange: (source: FundsSource) => void; onMaxClick: () => void; @@ -27,6 +29,7 @@ export function ParticipationForm({ source, availableBalance, walletBalance, + balances, onAmountChange, onSourceChange, onMaxClick, @@ -36,9 +39,16 @@ export function ParticipationForm({ error, amountError, }: ParticipationFormProps) { - const formattedBalance = availableBalance - ? formatEther(availableBalance) - : '0'; + const formattedBalance = useMemo(() => { + if (!availableBalance) { + return '0'; + } + // Handle negative balances + if (availableBalance < 0n) { + return `-${formatEther(-availableBalance)}`; + } + return formatEther(availableBalance); + }, [availableBalance]); // Determine which balance to show based on source const displayBalance = useMemo(() => { @@ -58,7 +68,7 @@ export function ParticipationForm({ return (
- +
diff --git a/libs/burn-waitlist/src/lib/components/requests-list.tsx b/libs/burn-waitlist/src/lib/components/requests-list.tsx index bcba6ca34..f38b08eb1 100644 --- a/libs/burn-waitlist/src/lib/components/requests-list.tsx +++ b/libs/burn-waitlist/src/lib/components/requests-list.tsx @@ -1,9 +1,12 @@ 'use client'; -import { RequestItem } from './request-item'; +import { formatEther } from 'viem'; +import { Button } from '@haqq/shell-ui-kit'; +import { FundsSource } from '../constants/waitlist-config'; +import type { Application } from '../hooks/use-waitlist-applications'; export interface RequestsListProps { - requestIds: bigint[]; + applications: Application[]; canCancel: boolean; onCancel: (requestId: bigint) => void; isCancelling?: boolean; @@ -11,13 +14,13 @@ export interface RequestsListProps { } export function RequestsList({ - requestIds, + applications, canCancel, onCancel, isCancelling = false, cancellingRequestId, }: RequestsListProps) { - if (requestIds.length === 0) { + if (applications.length === 0) { return (
@@ -31,15 +34,75 @@ export function RequestsList({

Your Requests

- {requestIds.map((id) => ( - - ))} + {applications.map((app) => { + const requestId = BigInt(app.requestId); + const amount = formatEther(BigInt(app.amount)); + const sourceLabel = + app.source === FundsSource.OwnBalance ? 'Own Balance' : 'ucDAO'; + const isCancelled = app.cancelled; + const isCancellingThis = cancellingRequestId === requestId; + + return ( +
+
+
+
+ + Request #{app.requestId} + + {isCancelled && ( + + Cancelled + + )} + {!isCancelled && !app.valid && ( + + Invalid + + )} + {!isCancelled && app.valid && app.ready && ( + + Ready + + )} + {!isCancelled && app.valid && !app.ready && ( + + Not Ready + + )} +
+
+
+ Amount:{' '} + + {amount} ISLM + +
+
+ Source:{' '} + + {sourceLabel} + +
+
+
+ {canCancel && !isCancelled && ( + + )} +
+
+ ); + })}
); diff --git a/libs/burn-waitlist/src/lib/components/waitlist-balances.tsx b/libs/burn-waitlist/src/lib/components/waitlist-balances.tsx index 73d9fde39..1dec46124 100644 --- a/libs/burn-waitlist/src/lib/components/waitlist-balances.tsx +++ b/libs/burn-waitlist/src/lib/components/waitlist-balances.tsx @@ -2,61 +2,73 @@ import { useMemo } from 'react'; import { formatEther } from 'viem'; -import { useAddress, useIndexerBalanceQuery } from '@haqq/shell-shared'; +import type { WaitlistBalancesResponse } from '../hooks/use-waitlist-balances'; export interface WaitlistBalancesProps { - walletBalance?: bigint; + balances?: WaitlistBalancesResponse; } -export function WaitlistBalances({ walletBalance }: WaitlistBalancesProps) { - const { haqqAddress } = useAddress(); - const { data: indexerBalances } = useIndexerBalanceQuery(haqqAddress); - - const balances = useMemo(() => { - const walletBalanceFormatted = walletBalance - ? formatEther(walletBalance) - : '0'; +export function WaitlistBalances({ balances }: WaitlistBalancesProps) { + const formattedBalances = useMemo(() => { + if (!balances) { + return null; + } return { - wallet: { - label: 'Wallet Balance', - value: walletBalanceFormatted, - valueBn: walletBalance || 0n, + balance: { + label: 'Balance', + value: formatEther(BigInt(balances.balance)), + valueBn: BigInt(balances.balance), + }, + delegations: { + label: 'Delegations', + value: formatEther(BigInt(balances.delegations)), + valueBn: BigInt(balances.delegations), + }, + rewards: { + label: 'Rewards', + value: formatEther(BigInt(balances.rewards)), + valueBn: BigInt(balances.rewards), }, - locked: { - label: 'Locked', - value: indexerBalances?.locked.toFixed(6) || '0', - valueBn: indexerBalances?.lockedBn || 0n, + unbonding_delegations: { + label: 'Unbonding', + value: formatEther(BigInt(balances.unbonding_delegations)), + valueBn: BigInt(balances.unbonding_delegations), }, - vested: { - label: 'Vested', - value: indexerBalances?.vested.toFixed(6) || '0', - valueBn: indexerBalances?.vestedBn || 0n, + ucdao: { + label: 'ucDAO', + value: formatEther(BigInt(balances.ucdao)), + valueBn: BigInt(balances.ucdao), }, - daoLocked: { - label: 'DAO Locked', - value: indexerBalances?.daoLocked.toFixed(6) || '0', - valueBn: indexerBalances?.daoLockedBn || 0n, + total_balance: { + label: 'Total Balance', + value: formatEther(BigInt(balances.total_balance)), + valueBn: BigInt(balances.total_balance), }, - available: { - label: 'Available', - value: indexerBalances?.available.toFixed(6) || '0', - valueBn: indexerBalances?.availableBn || 0n, + available_balance: { + label: 'Available Balance', + value: formatEther(BigInt(balances.available_balance)), + valueBn: BigInt(balances.available_balance), + }, + available_ucdao_balance: { + label: 'Available ucDAO', + value: formatEther(BigInt(balances.available_ucdao_balance)), + valueBn: BigInt(balances.available_ucdao_balance), }, }; - }, [walletBalance, indexerBalances]); + }, [balances]); const hasAnyBalance = useMemo(() => { - return ( - balances.wallet.valueBn > 0n || - balances.locked.valueBn > 0n || - balances.vested.valueBn > 0n || - balances.daoLocked.valueBn > 0n || - balances.available.valueBn > 0n + if (!formattedBalances) { + return false; + } + // Show balances even if they're negative (for transparency) + return Object.values(formattedBalances).some( + (balance) => balance.valueBn !== 0n, ); - }, [balances]); + }, [formattedBalances]); - if (!hasAnyBalance) { + if (!formattedBalances || !hasAnyBalance) { return null; } @@ -66,11 +78,23 @@ export function WaitlistBalances({ walletBalance }: WaitlistBalancesProps) { Your Balances
- {Object.entries(balances).map(([key, balance]) => { + {Object.entries(formattedBalances).map(([key, balance]) => { if (balance.valueBn === 0n) { return null; } + const isNegative = balance.valueBn < 0n; + const displayValue = isNegative + ? `-${parseFloat(balance.value.replace('-', '')).toLocaleString( + 'en-US', + { + maximumFractionDigits: 6, + }, + )}` + : parseFloat(balance.value).toLocaleString('en-US', { + maximumFractionDigits: 6, + }); + return (
{balance.label}
-
- {parseFloat(balance.value).toLocaleString('en-US', { - maximumFractionDigits: 6, - })}{' '} - ISLM +
+ {displayValue} ISLM
); diff --git a/libs/burn-waitlist/src/lib/constants/waitlist-config.ts b/libs/burn-waitlist/src/lib/constants/waitlist-config.ts index 2d9a96582..041d5ccdc 100644 --- a/libs/burn-waitlist/src/lib/constants/waitlist-config.ts +++ b/libs/burn-waitlist/src/lib/constants/waitlist-config.ts @@ -6,7 +6,7 @@ import { haqqTestedge2 } from 'viem/chains'; */ export const WAITLIST_CONTRACT_ADDRESSES: Record = { [haqqTestedge2.id]: - '0xafc36779f695a2946523C1B056A0Dd7583a0e019' as `0x${string}`, + '0xCAFec7F6C482507fB5E6B5ed02250487Fd883C9f' as `0x${string}`, // Add more chain deployments here as they become available }; @@ -56,10 +56,11 @@ export const WAITLIST_DEFAULT_CHAIN_ID = export const getBackendApiUrl = (): string => { if (typeof window !== 'undefined') { return ( - process.env.NEXT_PUBLIC_BURN_WAITLIST_API_URL || 'http://localhost:8080' + process.env.NEXT_PUBLIC_BURN_WAITLIST_API_URL || + 'https://waitlist.vorobevsa.com' ); } - return process.env.BURN_WAITLIST_API_URL || 'http://localhost:8080'; + return process.env.BURN_WAITLIST_API_URL || 'https://waitlist.vorobevsa.com'; }; /** diff --git a/libs/burn-waitlist/src/lib/hooks/index.ts b/libs/burn-waitlist/src/lib/hooks/index.ts index ebe4c0ed8..374434625 100644 --- a/libs/burn-waitlist/src/lib/hooks/index.ts +++ b/libs/burn-waitlist/src/lib/hooks/index.ts @@ -2,3 +2,5 @@ export * from './use-waitlist-contract'; export * from './use-backend-signature'; export * from './use-waitlist-requests'; export * from './use-waitlist-form'; +export * from './use-waitlist-balances'; +export * from './use-waitlist-applications'; diff --git a/libs/burn-waitlist/src/lib/hooks/use-backend-signature.ts b/libs/burn-waitlist/src/lib/hooks/use-backend-signature.ts index 78b3f55e3..dc07efa49 100644 --- a/libs/burn-waitlist/src/lib/hooks/use-backend-signature.ts +++ b/libs/burn-waitlist/src/lib/hooks/use-backend-signature.ts @@ -1,11 +1,11 @@ 'use client'; import { useCallback, useState } from 'react'; -import { getBackendApiUrl } from '../constants/waitlist-config'; import { FundsSource } from '../constants/waitlist-config'; interface BackendSignatureResponse { signature: string; + nonce?: string; // Nonce is returned but not needed by frontend } interface UseBackendSignatureReturn { @@ -13,7 +13,6 @@ interface UseBackendSignatureReturn { userAddress: string, amount: bigint, source: FundsSource, - nonce: bigint, ) => Promise<`0x${string}`>; isLoading: boolean; error: Error | null; @@ -21,6 +20,7 @@ interface UseBackendSignatureReturn { /** * Hook to get backend signature for waitlist request + * Note: Backend manages nonce internally, frontend doesn't need to pass it */ export function useBackendSignature(): UseBackendSignatureReturn { const [isLoading, setIsLoading] = useState(false); @@ -31,14 +31,12 @@ export function useBackendSignature(): UseBackendSignatureReturn { userAddress: string, amount: bigint, source: FundsSource, - nonce: bigint, ): Promise<`0x${string}`> => { setIsLoading(true); setError(null); try { - const apiUrl = getBackendApiUrl(); - const response = await fetch(`${apiUrl}/api/v1/signature`, { + const response = await fetch('/api/waitlist/signature', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -47,7 +45,6 @@ export function useBackendSignature(): UseBackendSignatureReturn { user: userAddress, amount: amount.toString(), source: source, - nonce: nonce.toString(), }), }); diff --git a/libs/burn-waitlist/src/lib/hooks/use-waitlist-applications.ts b/libs/burn-waitlist/src/lib/hooks/use-waitlist-applications.ts new file mode 100644 index 000000000..d9700f491 --- /dev/null +++ b/libs/burn-waitlist/src/lib/hooks/use-waitlist-applications.ts @@ -0,0 +1,72 @@ +'use client'; + +import { useQuery } from '@tanstack/react-query'; + +export interface Application { + requestId: string; + amount: string; + author: string; + source: number; // 0 for OwnBalance, 1 for ucDAO + cancelled: boolean; + valid: boolean; + ready: boolean; +} + +export interface ApplicationsListResponse { + applications: Application[]; + total: number; + page: number; + pageSize: number; + hasMore: boolean; +} + +interface UseWaitlistApplicationsParams { + address?: string; + status?: 'active' | 'cancelled'; + page?: number; + pageSize?: number; +} + +/** + * Hook to fetch user applications from backend API + */ +export function useWaitlistApplications({ + address, + status, + page = 1, + pageSize = 50, +}: UseWaitlistApplicationsParams = {}) { + return useQuery({ + queryKey: ['waitlist-applications', address, status, page, pageSize], + queryFn: async () => { + const params = new URLSearchParams(); + + if (address) { + params.append('address', address); + } + if (status) { + params.append('status', status); + } + params.append('page', page.toString()); + params.append('pageSize', pageSize.toString()); + + const queryString = params.toString(); + const url = queryString + ? `/api/waitlist/applications?${queryString}` + : '/api/waitlist/applications'; + + const response = await fetch(url); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error( + `Failed to fetch applications: ${response.status} ${errorText}`, + ); + } + + return response.json(); + }, + enabled: !!address, // Only fetch if address is provided + staleTime: 30000, // Cache for 30 seconds + }); +} diff --git a/libs/burn-waitlist/src/lib/hooks/use-waitlist-balances.ts b/libs/burn-waitlist/src/lib/hooks/use-waitlist-balances.ts new file mode 100644 index 000000000..e4d68e5e5 --- /dev/null +++ b/libs/burn-waitlist/src/lib/hooks/use-waitlist-balances.ts @@ -0,0 +1,42 @@ +'use client'; + +import { useQuery } from '@tanstack/react-query'; + +export interface WaitlistBalancesResponse { + address: string; + balance: string; + delegations: string; + rewards: string; + unbonding_delegations: string; + ucdao: string; + total_balance: string; + available_balance: string; + available_ucdao_balance: string; +} + +/** + * Hook to fetch user balances from backend API + */ +export function useWaitlistBalances(userAddress: string | undefined) { + return useQuery({ + queryKey: ['waitlist-balances', userAddress], + queryFn: async () => { + if (!userAddress) { + throw new Error('User address is required'); + } + + const response = await fetch(`/api/waitlist/balances/${userAddress}`); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error( + `Failed to fetch balances: ${response.status} ${errorText}`, + ); + } + + return response.json(); + }, + enabled: !!userAddress, + staleTime: 30000, // Cache for 30 seconds + }); +} diff --git a/libs/burn-waitlist/src/lib/hooks/use-waitlist-contract.ts b/libs/burn-waitlist/src/lib/hooks/use-waitlist-contract.ts index d7aca2d5b..63c3b9a69 100644 --- a/libs/burn-waitlist/src/lib/hooks/use-waitlist-contract.ts +++ b/libs/burn-waitlist/src/lib/hooks/use-waitlist-contract.ts @@ -220,7 +220,6 @@ export function useCreateWaitlistRequest() { const createRequest = async ( amount: bigint, source: FundsSource, - nonce: bigint, backendSignature: `0x${string}`, ) => { if (!writeContractAsync) { @@ -239,7 +238,7 @@ export function useCreateWaitlistRequest() { address: contractAddress, abi: WaitlistAbi, functionName: 'createRequest', - args: [amount, source, nonce, backendSignature], + args: [amount, source, backendSignature], chainId, }); }; diff --git a/libs/burn-waitlist/src/lib/hooks/use-waitlist-form.ts b/libs/burn-waitlist/src/lib/hooks/use-waitlist-form.ts index 18be3c3e1..5d925290b 100644 --- a/libs/burn-waitlist/src/lib/hooks/use-waitlist-form.ts +++ b/libs/burn-waitlist/src/lib/hooks/use-waitlist-form.ts @@ -91,11 +91,9 @@ export function useWaitlistForm({ const parsed = parseFloat(formState.amount); if (isNaN(parsed) || parsed <= 0) { errors.amount = 'Amount must be greater than 0'; - } else if ( - formattedAmount && - availableBalance && - formattedAmount > availableBalance - ) { + } else if (!availableBalance || availableBalance < 0n) { + errors.amount = 'Insufficient available balance'; + } else if (formattedAmount && formattedAmount > availableBalance) { errors.amount = 'Insufficient balance'; } } @@ -132,11 +130,12 @@ export function useWaitlistForm({ return false; } - if ( - formattedAmount && - availableBalance && - formattedAmount > availableBalance - ) { + // If availableBalance is negative or undefined, form is invalid + if (!availableBalance || availableBalance < 0n) { + return false; + } + + if (formattedAmount && formattedAmount > availableBalance) { return false; } diff --git a/libs/burn-waitlist/src/lib/waitlist-page.tsx b/libs/burn-waitlist/src/lib/waitlist-page.tsx index 993d04be1..bf57a1c59 100644 --- a/libs/burn-waitlist/src/lib/waitlist-page.tsx +++ b/libs/burn-waitlist/src/lib/waitlist-page.tsx @@ -4,13 +4,12 @@ import { useEffect, useState, useCallback, useMemo } from 'react'; import { useAccount, useBalance, useSwitchChain } from 'wagmi'; import { formatEther } from 'viem'; import { Container } from '@haqq/shell-ui-kit/server'; -import { useAddress, useDaoAllBalancesQuery } from '@haqq/shell-shared'; import { useWaitlistContractState, useCreateWaitlistRequest, useCancelWaitlistRequest, - useUserRequestIds, - useUserNonce, + useWaitlistBalances, + useWaitlistApplications, } from './hooks'; import { useBackendSignature } from './hooks/use-backend-signature'; import { useWaitlistForm } from './hooks/use-waitlist-form'; @@ -40,18 +39,23 @@ export function WaitlistPage() { refetchAll: refetchContractState, } = useWaitlistContractState(); - // User address conversion - const { haqqAddress } = useAddress(); + // Get balances from backend API + const { data: waitlistBalances, refetch: refetchBalances } = + useWaitlistBalances(address); - // User wallet balance (EVM) + // Get applications from backend API + const { data: applicationsData, refetch: refetchApplications } = + useWaitlistApplications({ + address: address, + status: 'active', + }); + + // User wallet balance (EVM) - for display purposes const { data: walletBalance, refetch: refetchBalance } = useBalance({ address: address as `0x${string}` | undefined, chainId: chain?.id || WAITLIST_DEFAULT_CHAIN_ID, }); - // DAO balances (Cosmos) - const { data: daoBalances } = useDaoAllBalancesQuery(haqqAddress); - // Create request const { createRequest: createRequestTx, @@ -73,44 +77,58 @@ export function WaitlistPage() { bigint | undefined >(); - // User nonce - must be declared before onSubmit callback - const { nonce: userNonce, refetch: refetchNonce } = useUserNonce( - address as `0x${string}` | undefined, - ); - // Backend signature const { getSignature, isLoading: isLoadingSignature } = useBackendSignature(); + // Track source separately to calculate available balance before form is initialized + const [selectedSource, setSelectedSource] = useState( + FundsSource.OwnBalance, + ); + + // Calculate available balance based on source using API data + const availableBalance = useMemo(() => { + if (!waitlistBalances) { + // Fallback to wallet balance if API data not available + return walletBalance?.value; + } + + // Use the appropriate available balance based on source + // Note: available_balance can be negative if user has more active requests than total balance + if (selectedSource === FundsSource.OwnBalance) { + return BigInt(waitlistBalances.available_balance); + } else { + // For ucDAO, use available_ucdao_balance from API + // This can also be negative + return BigInt(waitlistBalances.available_ucdao_balance); + } + }, [ + selectedSource, + waitlistBalances?.available_balance, + waitlistBalances?.available_ucdao_balance, + walletBalance?.value, + ]); + const onSubmit = useCallback( async (amount: bigint, source: FundsSource) => { if (!address) { throw new Error('Wallet not connected'); } - if (userNonce === undefined) { - throw new Error('Nonce not available'); - } - try { - // Get backend signature with nonce - const signature = await getSignature( - address, - amount, - source, - userNonce, - ); - - // Create request on chain - await createRequestTx(amount, source, userNonce, signature); + // Get backend signature (nonce is managed by backend) + const signature = await getSignature(address, amount, source); + + // Create request on chain (nonce is managed by contract) + await createRequestTx(amount, source, signature); } catch (error) { console.error('Failed to create request:', error); throw error; } }, - [address, getSignature, createRequestTx, userNonce], + [address, getSignature, createRequestTx], ); - // Form state - initialize with default balance + // Form state - initialize with available balance from API const { formState, setAmount, @@ -119,30 +137,14 @@ export function WaitlistPage() { isValid, formattedAmount, } = useWaitlistForm({ - availableBalance: walletBalance?.value, + availableBalance: availableBalance, onSubmit: onSubmit, }); - // Calculate available balance based on source - const availableBalance = useMemo(() => { - if (formState.source === FundsSource.OwnBalance) { - return walletBalance?.value; - } else { - // For ucDAO, get the native token (aISLM) balance - const nativeToken = daoBalances?.find((coin) => { - return coin.denom === 'aISLM'; - }); - if (nativeToken) { - return BigInt(nativeToken.amount); - } - return 0n; - } - }, [formState.source, walletBalance?.value, daoBalances]); - - // User requests - const { requestIds, refetch: refetchRequestIds } = useUserRequestIds( - address as `0x${string}` | undefined, - ); + // Sync selectedSource with formState.source and update availableBalance + useEffect(() => { + setSelectedSource(formState.source); + }, [formState.source]); // Handle form submission const handleSubmit = useCallback(async () => { @@ -155,25 +157,15 @@ export function WaitlistPage() { throw new Error('Wallet not connected'); } - if (userNonce === undefined) { - throw new Error('Nonce not available'); - } - - // Get backend signature with nonce + // Get backend signature (nonce is managed by backend) const signature = await getSignature( address, formattedAmount, formState.source, - userNonce, ); - // Create request on chain - await createRequestTx( - formattedAmount, - formState.source, - userNonce, - signature, - ); + // Create request on chain (nonce is managed by contract) + await createRequestTx(formattedAmount, formState.source, signature); } catch (error) { console.error('Failed to create request:', error); } @@ -182,7 +174,6 @@ export function WaitlistPage() { formattedAmount, address, formState.source, - userNonce, getSignature, createRequestTx, ]); @@ -193,9 +184,10 @@ export function WaitlistPage() { try { setCancellingRequestId(requestId); await cancelRequestTx(requestId); - // Refetch requests after cancellation + // Refetch requests and balances after cancellation setTimeout(() => { - refetchRequestIds(); + refetchApplications(); + refetchBalances(); refetchContractState(); }, 2000); } catch (error) { @@ -204,25 +196,30 @@ export function WaitlistPage() { setCancellingRequestId(undefined); } }, - [cancelRequestTx, refetchRequestIds, refetchContractState], + [ + cancelRequestTx, + refetchApplications, + refetchBalances, + refetchContractState, + ], ); // Refetch after successful creation useEffect(() => { if (isCreateSuccess) { - refetchRequestIds(); + refetchApplications(); + refetchBalances(); refetchContractState(); refetchBalance(); - refetchNonce(); // Reset form setAmount(''); } }, [ isCreateSuccess, - refetchRequestIds, + refetchApplications, + refetchBalances, refetchContractState, refetchBalance, - refetchNonce, setAmount, ]); @@ -278,11 +275,13 @@ export function WaitlistPage() { source={formState.source} availableBalance={availableBalance} walletBalance={walletBalance?.value} + balances={waitlistBalances} onAmountChange={(amount) => { setAmount(amount); }} onSourceChange={(source) => { setSource(source); + setSelectedSource(source); // Reset amount when source changes setAmount(''); }} @@ -305,17 +304,19 @@ export function WaitlistPage() {
)} - {isConnected && requestIds && requestIds.length > 0 && ( -
- -
- )} + {isConnected && + applicationsData && + applicationsData.applications.length > 0 && ( +
+ +
+ )} )}
From ce888108e8eabec307accc63ffad14a2515775fe Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Tue, 6 Jan 2026 12:40:16 +0300 Subject: [PATCH 085/141] refactor: ienhance data fetching for waitlist --- .../src/lib/components/requests-list.tsx | 2 +- .../src/lib/components/status-messages.tsx | 8 +--- .../lib/hooks/use-waitlist-applications.ts | 2 +- .../src/lib/hooks/use-waitlist-contract.ts | 3 -- libs/burn-waitlist/src/lib/waitlist-page.tsx | 41 ++++++++++++++++--- 5 files changed, 38 insertions(+), 18 deletions(-) diff --git a/libs/burn-waitlist/src/lib/components/requests-list.tsx b/libs/burn-waitlist/src/lib/components/requests-list.tsx index f38b08eb1..9ba141573 100644 --- a/libs/burn-waitlist/src/lib/components/requests-list.tsx +++ b/libs/burn-waitlist/src/lib/components/requests-list.tsx @@ -91,7 +91,7 @@ export function RequestsList({
{canCancel && !isCancelled && (
-
- -
- -
+ )} {error && (
diff --git a/libs/burn-waitlist/src/lib/components/requests-list.tsx b/libs/burn-waitlist/src/lib/components/requests-list.tsx index 176040ce1..dbd70127c 100644 --- a/libs/burn-waitlist/src/lib/components/requests-list.tsx +++ b/libs/burn-waitlist/src/lib/components/requests-list.tsx @@ -1,16 +1,25 @@ 'use client'; import { formatEther } from 'viem'; +import Link from 'next/link'; import { Button } from '@haqq/shell-ui-kit'; import { FundsSource } from '../constants/waitlist-config'; import type { Application } from '../hooks/use-waitlist-applications'; +import type { WaitlistBalancesResponse } from '../hooks/use-waitlist-balances'; export interface RequestsListProps { - applications: Application[]; + applications: Array< + Application & { + isPending?: boolean; + txHash?: string; + } + >; canCancel: boolean; onCancel: (requestId: bigint) => void; isCancelling?: boolean; cancellingRequestId?: bigint; + balances?: WaitlistBalancesResponse; + locale?: string; } export function RequestsList({ @@ -19,6 +28,8 @@ export function RequestsList({ onCancel, isCancelling = false, cancellingRequestId, + balances, + locale = 'en', }: RequestsListProps) { if (applications.length === 0) { return ( @@ -34,40 +45,49 @@ export function RequestsList({
{applications.map((app) => { - const requestId = BigInt(app.requestId); + const requestId = + app.requestId === 'pending' ? 0n : BigInt(app.requestId); const amount = formatEther(BigInt(app.amount)); const sourceLabel = app.source === FundsSource.OwnBalance ? 'Own Balance' : 'ucDAO'; const isCancelled = app.cancelled; const isCancellingThis = cancellingRequestId === requestId; + const isPending = app.isPending || false; return (
- Request #{app.requestId} + {app.requestId === 'pending' + ? 'Request (Pending)' + : `Request #${app.requestId}`} - {isCancelled && ( + {isPending && ( + + Waiting + + )} + {!isPending && isCancelled && ( Cancelled )} - {!isCancelled && !app.valid && ( + {!isPending && !isCancelled && !app.valid && ( Invalid )} - {!isCancelled && app.valid && app.ready && ( + {!isPending && !isCancelled && app.valid && app.ready && ( Ready )} - {!isCancelled && app.valid && !app.ready && ( + {!isPending && !isCancelled && app.valid && !app.ready && ( Not Ready @@ -86,9 +106,25 @@ export function RequestsList({ {sourceLabel}
+ {/* Show undelegate link if not ready and has delegations */} + {!isPending && + !isCancelled && + app.valid && + !app.ready && + balances && + BigInt(balances.delegations) > 0n && ( +
+ + Undelegate + +
+ )}
- {canCancel && !isCancelled && ( + {canCancel && !isCancelled && !isPending && (

+ {/* Display total stats before wallet connection */} + {!isConnected && ( +
+
+
+
+ Total Applications +
+
+ {totalCount !== undefined ? totalCount.toString() : '—'} +
+
+
+
Total Amount
+
+ {totalAmount !== undefined + ? `${formatEther(totalAmount)} ISLM` + : '—'} +
+
+
+
+ )} + {!isConnected && } {isConnected && ( @@ -299,6 +517,28 @@ export function WaitlistPage() { )} + {/* User Aggregates (5.1) */} +
+
+
+
+ Your Applications +
+
+ {userAggregates.totalCount} +
+
+
+
+ Your Total Amount +
+
+ {formatEther(userAggregates.totalAmount)} ISLM +
+
+
+
+
{/* First Column: Form */} {showForm && ( @@ -310,7 +550,6 @@ export function WaitlistPage() { amount={formState.amount} source={formState.source} availableBalance={availableBalance} - walletBalance={walletBalance?.value} balances={waitlistBalances} onAmountChange={(amount) => { setAmount(amount); @@ -344,18 +583,22 @@ export function WaitlistPage() {

Your Requests

- {applicationsData ? ( + {isLoadingApplications && !applicationsData ? ( + + ) : applicationsData || pendingApplications.length > 0 ? ( ) : (
- Loading applications... + You haven't created any requests yet
)} diff --git a/libs/ui-kit/src/lib/modal-input.tsx b/libs/ui-kit/src/lib/modal-input.tsx index d64a66bf9..b1fc586a0 100644 --- a/libs/ui-kit/src/lib/modal-input.tsx +++ b/libs/ui-kit/src/lib/modal-input.tsx @@ -19,6 +19,8 @@ const defaultMaskOptions = { decimalLimit: DEFAULT_DECIMAL_LIMIT, allowNegative: false, allowLeadingZeroes: false, + // Remove any integer limit to allow values > 999 + integerLimit: undefined, }; export const usePreparedMaskValue = ( From d6fc14c26506ae58e2acf7a6b8336db48909d64c Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Wed, 7 Jan 2026 16:27:47 +0300 Subject: [PATCH 090/141] feat: add skeleton for form and list --- .../burn-waitlist/src/lib/components/index.ts | 2 + .../participation-form-skeleton.tsx | 51 +++++++++++++++++++ .../lib/components/requests-list-skeleton.tsx | 28 ++++++++++ .../src/lib/components/requests-list.tsx | 30 +++++------ libs/burn-waitlist/src/lib/waitlist-page.tsx | 26 ++++++---- 5 files changed, 109 insertions(+), 28 deletions(-) create mode 100644 libs/burn-waitlist/src/lib/components/participation-form-skeleton.tsx create mode 100644 libs/burn-waitlist/src/lib/components/requests-list-skeleton.tsx diff --git a/libs/burn-waitlist/src/lib/components/index.ts b/libs/burn-waitlist/src/lib/components/index.ts index afda1d803..d183f1c17 100644 --- a/libs/burn-waitlist/src/lib/components/index.ts +++ b/libs/burn-waitlist/src/lib/components/index.ts @@ -1,5 +1,7 @@ export * from './participation-form'; +export * from './participation-form-skeleton'; export * from './requests-list'; +export * from './requests-list-skeleton'; export * from './request-item'; export * from './status-messages'; export * from './wallet-connection-warning'; diff --git a/libs/burn-waitlist/src/lib/components/participation-form-skeleton.tsx b/libs/burn-waitlist/src/lib/components/participation-form-skeleton.tsx new file mode 100644 index 000000000..ba0577855 --- /dev/null +++ b/libs/burn-waitlist/src/lib/components/participation-form-skeleton.tsx @@ -0,0 +1,51 @@ +'use client'; + +export function ParticipationFormSkeleton() { + return ( +
+ {/* Balances skeleton */} +
+
+
+ {[1, 2, 3].map((index) => ( +
+
+
+
+ ))} +
+
+ + {/* Amount input skeleton */} +
+
+
+
+
+
+
+
+ + {/* Funds Source skeleton */} +
+
+
+ {[1, 2].map((index) => ( +
+
+
+
+ ))} +
+
+ + {/* Submit button skeleton */} +
+
+
+
+ ); +} diff --git a/libs/burn-waitlist/src/lib/components/requests-list-skeleton.tsx b/libs/burn-waitlist/src/lib/components/requests-list-skeleton.tsx new file mode 100644 index 000000000..886b76c6e --- /dev/null +++ b/libs/burn-waitlist/src/lib/components/requests-list-skeleton.tsx @@ -0,0 +1,28 @@ +'use client'; + +export function RequestsListSkeleton() { + return ( +
+ {[1, 2, 3].map((index) => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))} +
+ ); +} diff --git a/libs/burn-waitlist/src/lib/components/requests-list.tsx b/libs/burn-waitlist/src/lib/components/requests-list.tsx index dbd70127c..2525e57d9 100644 --- a/libs/burn-waitlist/src/lib/components/requests-list.tsx +++ b/libs/burn-waitlist/src/lib/components/requests-list.tsx @@ -59,7 +59,7 @@ export function RequestsList({ key={app.requestId || app.txHash || `pending-${app.amount}`} className="rounded-[8px] border border-[#E5E7EB] bg-white p-[16px]" > -
+
@@ -106,22 +106,17 @@ export function RequestsList({ {sourceLabel}
- {/* Show undelegate link if not ready and has delegations */} - {!isPending && - !isCancelled && - app.valid && - !app.ready && - balances && - BigInt(balances.delegations) > 0n && ( -
- - Undelegate - -
- )} + {/* Show undelegate link if not ready (happens when request uses staking funds) */} + {!isPending && !isCancelled && app.valid && !app.ready && ( +
+ + Start undelegate + +
+ )}
{canCancel && !isCancelled && !isPending && ( @@ -130,6 +125,7 @@ export function RequestsList({ onClick={() => onCancel(requestId)} disabled={isCancelling} isLoading={isCancellingThis} + className="w-full sm:w-auto" > Cancel diff --git a/libs/burn-waitlist/src/lib/waitlist-page.tsx b/libs/burn-waitlist/src/lib/waitlist-page.tsx index 5aa513d29..d21e97403 100644 --- a/libs/burn-waitlist/src/lib/waitlist-page.tsx +++ b/libs/burn-waitlist/src/lib/waitlist-page.tsx @@ -16,6 +16,7 @@ import { useBackendSignature } from './hooks/use-backend-signature'; import { useWaitlistForm } from './hooks/use-waitlist-form'; import { ParticipationForm, + ParticipationFormSkeleton, RequestsList, RequestsListSkeleton, StatusMessages, @@ -48,8 +49,11 @@ export function WaitlistPage({ locale = 'en' }: WaitlistPageProps = {}) { } = useWaitlistContractState(); // Get balances from backend API - const { data: waitlistBalances, refetch: refetchBalances } = - useWaitlistBalances(address); + const { + data: waitlistBalances, + isLoading: isLoadingBalances, + refetch: refetchBalances, + } = useWaitlistBalances(address); // Get applications from backend API const { @@ -402,8 +406,6 @@ export function WaitlistPage({ locale = 'en' }: WaitlistPageProps = {}) { const errorMessage = createError?.message || cancelError?.message || undefined; - const showForm = canSubmit && !paused && isCorrectChain; - // Merge applications with pending ones, sort by requestId descending (newest first) const mergedApplications = useMemo(() => { type MergedApplication = Application & { @@ -541,11 +543,13 @@ export function WaitlistPage({ locale = 'en' }: WaitlistPageProps = {}) {
{/* First Column: Form */} - {showForm && ( -
-

- Participate in Waitlist -

+
+

+ Participate in Waitlist +

+ {isLoadingBalances && !waitlistBalances ? ( + + ) : ( -
- )} + )} +
{/* Second Column: Applications List */} {isConnected && ( From 25444b1cc0310673ee8812837df7b717a46a5949 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Wed, 7 Jan 2026 16:40:37 +0300 Subject: [PATCH 091/141] feat: reserve fees when setting max amount in waitlist page --- libs/burn-waitlist/src/lib/waitlist-page.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/libs/burn-waitlist/src/lib/waitlist-page.tsx b/libs/burn-waitlist/src/lib/waitlist-page.tsx index d21e97403..37927da46 100644 --- a/libs/burn-waitlist/src/lib/waitlist-page.tsx +++ b/libs/burn-waitlist/src/lib/waitlist-page.tsx @@ -2,7 +2,7 @@ import { useEffect, useState, useCallback, useMemo, useRef } from 'react'; import { useAccount, useBalance, useSwitchChain } from 'wagmi'; -import { formatEther } from 'viem'; +import { formatEther, parseEther } from 'viem'; import { Container } from '@haqq/shell-ui-kit/server'; import { useWaitlistContractState, @@ -564,8 +564,16 @@ export function WaitlistPage({ locale = 'en' }: WaitlistPageProps = {}) { }} onMaxClick={() => { if (availableBalance) { - const formatted = formatEther(availableBalance); - setAmount(formatted); + // Reserve 0.01 ISLM for fees + const feeReserve = parseEther('0.01'); + const maxAmount = + availableBalance > feeReserve + ? availableBalance - feeReserve + : 0n; + if (maxAmount > 0n) { + const formatted = formatEther(maxAmount); + setAmount(formatted); + } } }} onSubmit={handleSubmit} From 62066f859bfb1b5d4f28273f55c2fecb4933d324 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Wed, 7 Jan 2026 17:55:00 +0300 Subject: [PATCH 092/141] refactor: update waitlist page to refetch balances and applications after request creation --- .../app/api/waitlist/applications/route.ts | 55 ------------------- .../api/waitlist/balances/[address]/route.ts | 48 ---------------- .../src/app/api/waitlist/signature/route.ts | 47 ---------------- .../src/lib/hooks/use-backend-signature.ts | 5 +- .../lib/hooks/use-waitlist-applications.ts | 13 ++++- .../src/lib/hooks/use-waitlist-balances.ts | 11 +++- .../src/lib/hooks/use-waitlist-form.ts | 47 ++++++++++------ libs/burn-waitlist/src/lib/waitlist-page.tsx | 19 ++----- 8 files changed, 57 insertions(+), 188 deletions(-) delete mode 100644 apps/shell/src/app/api/waitlist/applications/route.ts delete mode 100644 apps/shell/src/app/api/waitlist/balances/[address]/route.ts delete mode 100644 apps/shell/src/app/api/waitlist/signature/route.ts diff --git a/apps/shell/src/app/api/waitlist/applications/route.ts b/apps/shell/src/app/api/waitlist/applications/route.ts deleted file mode 100644 index ef0f6ff38..000000000 --- a/apps/shell/src/app/api/waitlist/applications/route.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; - -const WAITLIST_API_URL = - process.env.BURN_WAITLIST_API_URL || 'https://waitlist.vorobevsa.com'; - -export async function GET(request: NextRequest) { - try { - const { searchParams } = new URL(request.url); - - // Forward query parameters to waitlist API - const params = new URLSearchParams(); - if (searchParams.get('address')) { - params.append('address', searchParams.get('address')!); - } - if (searchParams.get('status')) { - params.append('status', searchParams.get('status')!); - } - if (searchParams.get('page')) { - params.append('page', searchParams.get('page')!); - } - if (searchParams.get('pageSize')) { - params.append('pageSize', searchParams.get('pageSize')!); - } - - const queryString = params.toString(); - const url = queryString - ? `${WAITLIST_API_URL}/api/v1/applications?${queryString}` - : `${WAITLIST_API_URL}/api/v1/applications`; - - // Forward request to waitlist API - const response = await fetch(url, { - method: 'GET', - headers: { - Accept: 'application/json', - }, - }); - - if (!response.ok) { - const errorText = await response.text(); - return NextResponse.json( - { error: errorText || `API error: ${response.status}` }, - { status: response.status }, - ); - } - - const data = await response.json(); - return NextResponse.json(data); - } catch (error) { - console.error('Error in waitlist applications API:', error); - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 }, - ); - } -} diff --git a/apps/shell/src/app/api/waitlist/balances/[address]/route.ts b/apps/shell/src/app/api/waitlist/balances/[address]/route.ts deleted file mode 100644 index 29614a1f9..000000000 --- a/apps/shell/src/app/api/waitlist/balances/[address]/route.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; - -const WAITLIST_API_URL = - process.env.BURN_WAITLIST_API_URL || 'https://waitlist.vorobevsa.com'; - -export async function GET( - request: NextRequest, - { params }: { params: Promise<{ address: string }> }, -) { - try { - const { address } = await params; - - if (!address) { - return NextResponse.json( - { error: 'Address is required' }, - { status: 400 }, - ); - } - - // Forward request to waitlist API - const response = await fetch( - `${WAITLIST_API_URL}/api/v1/balances/${address}`, - { - method: 'GET', - headers: { - Accept: 'application/json', - }, - }, - ); - - if (!response.ok) { - const errorText = await response.text(); - return NextResponse.json( - { error: errorText || `API error: ${response.status}` }, - { status: response.status }, - ); - } - - const data = await response.json(); - return NextResponse.json(data); - } catch (error) { - console.error('Error in waitlist balances API:', error); - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 }, - ); - } -} diff --git a/apps/shell/src/app/api/waitlist/signature/route.ts b/apps/shell/src/app/api/waitlist/signature/route.ts deleted file mode 100644 index e138974b2..000000000 --- a/apps/shell/src/app/api/waitlist/signature/route.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; - -const WAITLIST_API_URL = - process.env.BURN_WAITLIST_API_URL || 'https://waitlist.vorobevsa.com'; - -export async function POST(request: NextRequest) { - try { - const body = await request.json(); - - // Validate required fields - if (!body.user || !body.amount || body.source === undefined) { - return NextResponse.json( - { error: 'Missing required fields: user, amount, source' }, - { status: 400 }, - ); - } - - // Forward request to waitlist API - const response = await fetch( - `${WAITLIST_API_URL}/api/v1/signer/signature`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(body), - }, - ); - - if (!response.ok) { - const errorText = await response.text(); - return NextResponse.json( - { error: errorText || `API error: ${response.status}` }, - { status: response.status }, - ); - } - - const data = await response.json(); - return NextResponse.json(data); - } catch (error) { - console.error('Error in waitlist signature API:', error); - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 }, - ); - } -} diff --git a/libs/burn-waitlist/src/lib/hooks/use-backend-signature.ts b/libs/burn-waitlist/src/lib/hooks/use-backend-signature.ts index dc07efa49..08c637888 100644 --- a/libs/burn-waitlist/src/lib/hooks/use-backend-signature.ts +++ b/libs/burn-waitlist/src/lib/hooks/use-backend-signature.ts @@ -1,7 +1,7 @@ 'use client'; import { useCallback, useState } from 'react'; -import { FundsSource } from '../constants/waitlist-config'; +import { FundsSource, getBackendApiUrl } from '../constants/waitlist-config'; interface BackendSignatureResponse { signature: string; @@ -36,7 +36,8 @@ export function useBackendSignature(): UseBackendSignatureReturn { setError(null); try { - const response = await fetch('/api/waitlist/signature', { + const apiUrl = getBackendApiUrl(); + const response = await fetch(`${apiUrl}/api/v1/signer/signature`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/libs/burn-waitlist/src/lib/hooks/use-waitlist-applications.ts b/libs/burn-waitlist/src/lib/hooks/use-waitlist-applications.ts index 2f4f88a96..5d4b6bfad 100644 --- a/libs/burn-waitlist/src/lib/hooks/use-waitlist-applications.ts +++ b/libs/burn-waitlist/src/lib/hooks/use-waitlist-applications.ts @@ -1,6 +1,7 @@ 'use client'; import { useQuery } from '@tanstack/react-query'; +import { getBackendApiUrl } from '../constants/waitlist-config'; export interface Application { requestId: string; @@ -51,11 +52,17 @@ export function useWaitlistApplications({ params.append('pageSize', pageSize.toString()); const queryString = params.toString(); + const apiUrl = getBackendApiUrl(); const url = queryString - ? `/api/waitlist/applications?${queryString}` - : '/api/waitlist/applications'; + ? `${apiUrl}/api/v1/applications?${queryString}` + : `${apiUrl}/api/v1/applications`; - const response = await fetch(url); + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + }, + }); if (!response.ok) { const errorText = await response.text(); diff --git a/libs/burn-waitlist/src/lib/hooks/use-waitlist-balances.ts b/libs/burn-waitlist/src/lib/hooks/use-waitlist-balances.ts index e4d68e5e5..5ee88c108 100644 --- a/libs/burn-waitlist/src/lib/hooks/use-waitlist-balances.ts +++ b/libs/burn-waitlist/src/lib/hooks/use-waitlist-balances.ts @@ -1,6 +1,7 @@ 'use client'; import { useQuery } from '@tanstack/react-query'; +import { getBackendApiUrl } from '../constants/waitlist-config'; export interface WaitlistBalancesResponse { address: string; @@ -25,7 +26,13 @@ export function useWaitlistBalances(userAddress: string | undefined) { throw new Error('User address is required'); } - const response = await fetch(`/api/waitlist/balances/${userAddress}`); + const apiUrl = getBackendApiUrl(); + const response = await fetch(`${apiUrl}/api/v1/balances/${userAddress}`, { + method: 'GET', + headers: { + Accept: 'application/json', + }, + }); if (!response.ok) { const errorText = await response.text(); @@ -37,6 +44,6 @@ export function useWaitlistBalances(userAddress: string | undefined) { return response.json(); }, enabled: !!userAddress, - staleTime: 30000, // Cache for 30 seconds + staleTime: 0, // Always refetch to get latest data }); } diff --git a/libs/burn-waitlist/src/lib/hooks/use-waitlist-form.ts b/libs/burn-waitlist/src/lib/hooks/use-waitlist-form.ts index e89d262ab..a16cf3938 100644 --- a/libs/burn-waitlist/src/lib/hooks/use-waitlist-form.ts +++ b/libs/burn-waitlist/src/lib/hooks/use-waitlist-form.ts @@ -1,6 +1,6 @@ 'use client'; -import { useState, useCallback, useMemo } from 'react'; +import { useState, useCallback, useMemo, useEffect } from 'react'; import { parseEther, formatEther } from 'viem'; import { FundsSource } from '../constants/waitlist-config'; @@ -41,6 +41,31 @@ export function useWaitlistForm({ errors: {}, }); + // Store formattedAmount in state to preserve exact BigInt value + const [formattedAmount, setFormattedAmount] = useState( + undefined, + ); + + // Update formattedAmount when amount changes + useEffect(() => { + if (!formState.amount || formState.amount === '') { + setFormattedAmount(undefined); + return; + } + + try { + const parsed = parseFloat(formState.amount); + if (isNaN(parsed) || parsed <= 0) { + setFormattedAmount(undefined); + return; + } + const parsedAmount = parseEther(formState.amount); + setFormattedAmount(parsedAmount); + } catch { + setFormattedAmount(undefined); + } + }, [formState.amount]); + const setAmount = useCallback((amount: string) => { setFormState((prev) => ({ ...prev, @@ -50,6 +75,7 @@ export function useWaitlistForm({ amount: undefined, }, })); + // formattedAmount will be updated by useEffect }, []); const setSource = useCallback((source: FundsSource) => { @@ -64,27 +90,14 @@ export function useWaitlistForm({ const handleMaxClick = useCallback(() => { if (availableBalance) { + // Store the exact BigInt value first + setFormattedAmount(availableBalance); + // Then set the string representation for display const formatted = formatEther(availableBalance); setAmount(formatted); } }, [availableBalance, setAmount]); - const formattedAmount = useMemo(() => { - if (!formState.amount || formState.amount === '') { - return undefined; - } - - try { - const parsed = parseFloat(formState.amount); - if (isNaN(parsed) || parsed <= 0) { - return undefined; - } - return parseEther(formState.amount); - } catch { - return undefined; - } - }, [formState.amount]); - const validate = useCallback((): boolean => { const errors: WaitlistFormState['errors'] = {}; diff --git a/libs/burn-waitlist/src/lib/waitlist-page.tsx b/libs/burn-waitlist/src/lib/waitlist-page.tsx index 37927da46..6c2846a84 100644 --- a/libs/burn-waitlist/src/lib/waitlist-page.tsx +++ b/libs/burn-waitlist/src/lib/waitlist-page.tsx @@ -2,7 +2,7 @@ import { useEffect, useState, useCallback, useMemo, useRef } from 'react'; import { useAccount, useBalance, useSwitchChain } from 'wagmi'; -import { formatEther, parseEther } from 'viem'; +import { formatEther } from 'viem'; import { Container } from '@haqq/shell-ui-kit/server'; import { useWaitlistContractState, @@ -234,8 +234,9 @@ export function WaitlistPage({ locale = 'en' }: WaitlistPageProps = {}) { txHash: hash, }; setPendingApplications((prev) => [...prev, optimisticApp]); - // Refetch immediately + // Refetch immediately to update balances and applications refetchApplications(); + refetchBalances(); } } catch (error) { console.error('Failed to create request:', error); @@ -248,6 +249,7 @@ export function WaitlistPage({ locale = 'en' }: WaitlistPageProps = {}) { getSignature, createRequestTx, refetchApplications, + refetchBalances, ]); // Handle cancel request @@ -563,18 +565,7 @@ export function WaitlistPage({ locale = 'en' }: WaitlistPageProps = {}) { setSelectedSource(source); }} onMaxClick={() => { - if (availableBalance) { - // Reserve 0.01 ISLM for fees - const feeReserve = parseEther('0.01'); - const maxAmount = - availableBalance > feeReserve - ? availableBalance - feeReserve - : 0n; - if (maxAmount > 0n) { - const formatted = formatEther(maxAmount); - setAmount(formatted); - } - } + handleMaxClick(); }} onSubmit={handleSubmit} isValid={ From 84b0a99cd0bdd7b5e9840039b6108e96120788fd Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Wed, 7 Jan 2026 17:59:01 +0300 Subject: [PATCH 093/141] refactor: optimize refetching logic in waitlist --- libs/burn-waitlist/src/lib/waitlist-page.tsx | 31 +++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/libs/burn-waitlist/src/lib/waitlist-page.tsx b/libs/burn-waitlist/src/lib/waitlist-page.tsx index 6c2846a84..a406d1fcd 100644 --- a/libs/burn-waitlist/src/lib/waitlist-page.tsx +++ b/libs/burn-waitlist/src/lib/waitlist-page.tsx @@ -324,22 +324,20 @@ export function WaitlistPage({ locale = 'en' }: WaitlistPageProps = {}) { refetchContractState(); refetchBalance(); - // Refetch again after delay to ensure backend has processed the transaction - const timeoutId1 = setTimeout(() => { + // Refetch periodically for the next 10 seconds (every 2 seconds) + const intervalId = setInterval(() => { refetchApplications(); refetchBalances(); }, 2000); - // Final refetch after longer delay to ensure everything is synced - const timeoutId2 = setTimeout(() => { - refetchApplications(); - refetchBalances(); - refetchContractState(); - }, 5000); + // Stop refetching after 10 seconds + const stopTimeoutId = setTimeout(() => { + clearInterval(intervalId); + }, 10000); return () => { - clearTimeout(timeoutId1); - clearTimeout(timeoutId2); + clearInterval(intervalId); + clearTimeout(stopTimeoutId); }; } @@ -376,15 +374,20 @@ export function WaitlistPage({ locale = 'en' }: WaitlistPageProps = {}) { refetchBalances(); refetchContractState(); - // Refetch again after delay to ensure backend has processed - const timeoutId = setTimeout(() => { + // Refetch periodically for the next 10 seconds (every 2 seconds) + const intervalId = setInterval(() => { refetchApplications(); refetchBalances(); - refetchContractState(); }, 2000); + // Stop refetching after 10 seconds + const stopTimeoutId = setTimeout(() => { + clearInterval(intervalId); + }, 10000); + return () => { - clearTimeout(timeoutId); + clearInterval(intervalId); + clearTimeout(stopTimeoutId); }; } }, [ From 7d9ee622c7e76de39e122c4b9f94b4910ac4bfde Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Thu, 8 Jan 2026 15:16:32 +0300 Subject: [PATCH 094/141] refactor: enhance formatting and validation logic in waitlist components --- .../src/lib/components/waitlist-balances.tsx | 6 ++- .../src/lib/hooks/use-waitlist-form.ts | 25 ++++++++++-- libs/burn-waitlist/src/lib/waitlist-page.tsx | 38 +++++++++++++++++-- 3 files changed, 59 insertions(+), 10 deletions(-) diff --git a/libs/burn-waitlist/src/lib/components/waitlist-balances.tsx b/libs/burn-waitlist/src/lib/components/waitlist-balances.tsx index 1dec46124..845eae3f9 100644 --- a/libs/burn-waitlist/src/lib/components/waitlist-balances.tsx +++ b/libs/burn-waitlist/src/lib/components/waitlist-balances.tsx @@ -88,11 +88,13 @@ export function WaitlistBalances({ balances }: WaitlistBalancesProps) { ? `-${parseFloat(balance.value.replace('-', '')).toLocaleString( 'en-US', { - maximumFractionDigits: 6, + minimumFractionDigits: 0, + maximumFractionDigits: 4, }, )}` : parseFloat(balance.value).toLocaleString('en-US', { - maximumFractionDigits: 6, + minimumFractionDigits: 0, + maximumFractionDigits: 4, }); return ( diff --git a/libs/burn-waitlist/src/lib/hooks/use-waitlist-form.ts b/libs/burn-waitlist/src/lib/hooks/use-waitlist-form.ts index a16cf3938..a39b45676 100644 --- a/libs/burn-waitlist/src/lib/hooks/use-waitlist-form.ts +++ b/libs/burn-waitlist/src/lib/hooks/use-waitlist-form.ts @@ -27,6 +27,22 @@ interface UseWaitlistFormReturn { formattedAmount: bigint | undefined; } +/** + * Helper function to clean numeric string by removing thousands separators + * Removes commas, spaces, and other common separators + */ +const cleanNumericString = (value: string): string => { + return value.replace(/[,\s]/g, ''); +}; + +/** + * Helper function to parse float from string with separators + */ +const parseFloatSafe = (value: string): number => { + const cleaned = cleanNumericString(value); + return parseFloat(cleaned); +}; + /** * Hook to manage waitlist participation form state * Note: availableBalance should be recalculated externally when source changes @@ -54,12 +70,13 @@ export function useWaitlistForm({ } try { - const parsed = parseFloat(formState.amount); + const cleanedAmount = cleanNumericString(formState.amount); + const parsed = parseFloatSafe(formState.amount); if (isNaN(parsed) || parsed <= 0) { setFormattedAmount(undefined); return; } - const parsedAmount = parseEther(formState.amount); + const parsedAmount = parseEther(cleanedAmount); setFormattedAmount(parsedAmount); } catch { setFormattedAmount(undefined); @@ -104,7 +121,7 @@ export function useWaitlistForm({ if (!formState.amount || formState.amount === '') { errors.amount = 'Amount is required'; } else { - const parsed = parseFloat(formState.amount); + const parsed = parseFloatSafe(formState.amount); if (isNaN(parsed) || parsed <= 0) { errors.amount = 'Amount must be greater than 0'; } else if (!availableBalance || availableBalance < 0n) { @@ -145,7 +162,7 @@ export function useWaitlistForm({ // Try to parse the amount let parsed: number; try { - parsed = parseFloat(formState.amount); + parsed = parseFloatSafe(formState.amount); if (isNaN(parsed) || parsed <= 0) { return false; } diff --git a/libs/burn-waitlist/src/lib/waitlist-page.tsx b/libs/burn-waitlist/src/lib/waitlist-page.tsx index a406d1fcd..75ee55ea3 100644 --- a/libs/burn-waitlist/src/lib/waitlist-page.tsx +++ b/libs/burn-waitlist/src/lib/waitlist-page.tsx @@ -4,6 +4,16 @@ import { useEffect, useState, useCallback, useMemo, useRef } from 'react'; import { useAccount, useBalance, useSwitchChain } from 'wagmi'; import { formatEther } from 'viem'; import { Container } from '@haqq/shell-ui-kit/server'; + +// Helper function to format ether with 4 decimal places +const formatEtherWithDecimals = (value: bigint): string => { + const formatted = formatEther(value); + const num = parseFloat(formatted); + return num.toLocaleString('en-US', { + minimumFractionDigits: 0, + maximumFractionDigits: 4, + }); +}; import { useWaitlistContractState, useCreateWaitlistRequest, @@ -113,6 +123,8 @@ export function WaitlistPage({ locale = 'en' }: WaitlistPageProps = {}) { // Track if we've already processed the success to avoid infinite loops const hasProcessedSuccess = useRef(false); const hasProcessedCancelSuccess = useRef(); + // Track if we've attempted to switch chain to avoid repeated attempts + const hasAttemptedSwitch = useRef(undefined); // Backend signature const { getSignature, isLoading: isLoadingSignature } = useBackendSignature(); @@ -402,10 +414,28 @@ export function WaitlistPage({ locale = 'en' }: WaitlistPageProps = {}) { const handleSwitchChain = useCallback(async () => { try { await switchChainAsync({ chainId: WAITLIST_DEFAULT_CHAIN_ID }); + // Mark that we've attempted to switch for this chain + if (chain?.id) { + hasAttemptedSwitch.current = chain.id; + } } catch (error) { console.error('Failed to switch chain:', error); } - }, [switchChainAsync]); + }, [switchChainAsync, chain?.id]); + + // Automatically switch to supported chain if current chain is not supported + useEffect(() => { + if ( + isConnected && + chain?.id && + !isCorrectChain && + hasAttemptedSwitch.current !== chain.id + ) { + // Only attempt switch once per chain (track by chain ID) + hasAttemptedSwitch.current = chain.id; + handleSwitchChain(); + } + }, [isConnected, chain?.id, isCorrectChain, handleSwitchChain]); const isSubmitting = isCreating || isConfirmingCreate || isLoadingSignature; const errorMessage = @@ -499,7 +529,7 @@ export function WaitlistPage({ locale = 'en' }: WaitlistPageProps = {}) {
Total Amount
{totalAmount !== undefined - ? `${formatEther(totalAmount)} ISLM` + ? `${formatEtherWithDecimals(totalAmount)} ISLM` : '—'}
@@ -526,7 +556,7 @@ export function WaitlistPage({ locale = 'en' }: WaitlistPageProps = {}) { {/* User Aggregates (5.1) */}
-
+
Your Applications @@ -540,7 +570,7 @@ export function WaitlistPage({ locale = 'en' }: WaitlistPageProps = {}) { Your Total Amount
- {formatEther(userAggregates.totalAmount)} ISLM + {formatEtherWithDecimals(userAggregates.totalAmount)} ISLM
From 902c8c2857b7ee5c2bacdbf8fed6a9a6ed30c2b2 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Thu, 8 Jan 2026 15:48:40 +0300 Subject: [PATCH 095/141] chore: fix build --- libs/bridge/src/lib/bridge-page.tsx | 6 ++---- libs/burn-waitlist/src/lib/waitlist-page.tsx | 8 ++++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/libs/bridge/src/lib/bridge-page.tsx b/libs/bridge/src/lib/bridge-page.tsx index 1c9cde83e..8fecbb874 100644 --- a/libs/bridge/src/lib/bridge-page.tsx +++ b/libs/bridge/src/lib/bridge-page.tsx @@ -38,8 +38,6 @@ import { useWithdrawalRecovery, } from './hooks'; -// SUPPORTED_CHAINS is now imported from @haqq/shell-shared - // SUPPORTED_CHAINS is now imported from @haqq/shell-shared export const useChainProxyAddress = (chainId: number | undefined) => { if (chainId === CHAIN_CONFIG.l1ChainId) { @@ -298,7 +296,7 @@ export function BridgePage() { )} - {isConnected && ( + {isConnected ? ( <> - )} + ) : null}
diff --git a/libs/burn-waitlist/src/lib/waitlist-page.tsx b/libs/burn-waitlist/src/lib/waitlist-page.tsx index 75ee55ea3..659a99af5 100644 --- a/libs/burn-waitlist/src/lib/waitlist-page.tsx +++ b/libs/burn-waitlist/src/lib/waitlist-page.tsx @@ -539,7 +539,7 @@ export function WaitlistPage({ locale = 'en' }: WaitlistPageProps = {}) { {!isConnected && } - {isConnected && ( + {isConnected ? ( <> {/* Second Column: Applications List */} - {isConnected && ( + {isConnected ? (

Your Requests @@ -639,10 +639,10 @@ export function WaitlistPage({ locale = 'en' }: WaitlistPageProps = {}) {

)}
- )} + ) : null}
- )} + ) : null}
From e7f19d7f7cb235ec305c8fe8b9f0524cf77007b2 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Thu, 8 Jan 2026 16:38:19 +0300 Subject: [PATCH 096/141] refactor: improve input handling --- .../src/lib/components/participation-form.tsx | 2 +- libs/ui-kit/src/lib/modal-input.tsx | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/libs/burn-waitlist/src/lib/components/participation-form.tsx b/libs/burn-waitlist/src/lib/components/participation-form.tsx index 91d4547dc..4c3bd2b88 100644 --- a/libs/burn-waitlist/src/lib/components/participation-form.tsx +++ b/libs/burn-waitlist/src/lib/components/participation-form.tsx @@ -64,7 +64,7 @@ export function ParticipationForm({ { // Allow decimal input - pass the string value directly // ModalInput's CurrencyInput handles decimal input correctly diff --git a/libs/ui-kit/src/lib/modal-input.tsx b/libs/ui-kit/src/lib/modal-input.tsx index b1fc586a0..1c833aa6d 100644 --- a/libs/ui-kit/src/lib/modal-input.tsx +++ b/libs/ui-kit/src/lib/modal-input.tsx @@ -28,6 +28,13 @@ export const usePreparedMaskValue = ( ) => { const inputValue = useMemo(() => { if (!value && value !== 0) return undefined; + + // If value is a string, preserve it as-is to maintain decimal point during input + // This is especially important for mobile browsers where input can be more sensitive + if (typeof value === 'string') { + return value; + } + // Hack, because react-text-mask doesn't work correctly with decimals // ex: it converts 0.0709 to 0.070 (not 0.071!) // Additionally, remove trailing zeros and only fix to decimal limit if it has decimals @@ -118,7 +125,7 @@ export function ModalInput({ id, }: { symbol: string; - value: number | undefined; + value: number | string | undefined; onChange: (value: string | undefined) => void; onMaxButtonClick?: () => void; hint?: ReactNode; From d9a1a168c201a80e0aae5fac25a8afd50df5ebc1 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Thu, 8 Jan 2026 16:59:22 +0300 Subject: [PATCH 097/141] feat: adjust max amount calculation to reserve fees in waitlist form --- libs/burn-waitlist/src/lib/hooks/use-waitlist-form.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/libs/burn-waitlist/src/lib/hooks/use-waitlist-form.ts b/libs/burn-waitlist/src/lib/hooks/use-waitlist-form.ts index a39b45676..410db2568 100644 --- a/libs/burn-waitlist/src/lib/hooks/use-waitlist-form.ts +++ b/libs/burn-waitlist/src/lib/hooks/use-waitlist-form.ts @@ -107,10 +107,15 @@ export function useWaitlistForm({ const handleMaxClick = useCallback(() => { if (availableBalance) { + // Reserve 0.2 ISLM for fees + const feeReserve = parseEther('0.2'); + const maxAmount = + availableBalance > feeReserve ? availableBalance - feeReserve : 0n; + // Store the exact BigInt value first - setFormattedAmount(availableBalance); + setFormattedAmount(maxAmount); // Then set the string representation for display - const formatted = formatEther(availableBalance); + const formatted = formatEther(maxAmount); setAmount(formatted); } }, [availableBalance, setAmount]); From 2fbfd9c92919a5b917014ba9230c2e94c8761c52 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Thu, 8 Jan 2026 18:19:48 +0300 Subject: [PATCH 098/141] feat: add warning for negative available balance in waitlist page --- libs/burn-waitlist/src/lib/waitlist-page.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/libs/burn-waitlist/src/lib/waitlist-page.tsx b/libs/burn-waitlist/src/lib/waitlist-page.tsx index 659a99af5..fdc787e46 100644 --- a/libs/burn-waitlist/src/lib/waitlist-page.tsx +++ b/libs/burn-waitlist/src/lib/waitlist-page.tsx @@ -611,6 +611,17 @@ export function WaitlistPage({ locale = 'en' }: WaitlistPageProps = {}) { amountError={formState.errors.amount} /> )} + + {/* Warning for negative available balance */} + {availableBalance !== undefined && availableBalance < 0n && ( +
+
+ Need to fill balance{' '} + {formatEtherWithDecimals(-availableBalance)} ISLM for + request creation +
+
+ )}
{/* Second Column: Applications List */} From 269a7c62fba6fd9ac374eec1b79195db0b5fef6a Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Fri, 9 Jan 2026 15:21:38 +0300 Subject: [PATCH 099/141] refactor: remove auto-switch logic for ucDAO balance in waitlist page --- .../src/lib/components/participation-form.tsx | 2 +- libs/burn-waitlist/src/lib/waitlist-page.tsx | 13 ------------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/libs/burn-waitlist/src/lib/components/participation-form.tsx b/libs/burn-waitlist/src/lib/components/participation-form.tsx index 4c3bd2b88..37e85c4be 100644 --- a/libs/burn-waitlist/src/lib/components/participation-form.tsx +++ b/libs/burn-waitlist/src/lib/components/participation-form.tsx @@ -94,7 +94,7 @@ export function ParticipationForm({
{/* Only show Funds Source selection if ucDAO balance is greater than 0 */} - {balances && BigInt(balances.ucdao) > 0n && ( + {balances && (
); diff --git a/libs/bridge/src/lib/components/bridge-success-message.tsx b/libs/bridge/src/lib/components/bridge-success-message.tsx index 3f23d6ff0..c696c7a7c 100644 --- a/libs/bridge/src/lib/components/bridge-success-message.tsx +++ b/libs/bridge/src/lib/components/bridge-success-message.tsx @@ -1,6 +1,16 @@ 'use client'; -export function BridgeSuccessMessage({ tokenSymbol }: { tokenSymbol: string }) { +export interface BridgeSuccessMessageProps { + tokenSymbol: string; + isL2ToL1?: boolean; +} + +export function BridgeSuccessMessage({ + tokenSymbol, + isL2ToL1 = false, +}: BridgeSuccessMessageProps) { + const confirmationMessage = isL2ToL1 ? 'min 7 days' : '1-3 minutes'; + return (
@@ -8,7 +18,7 @@ export function BridgeSuccessMessage({ tokenSymbol }: { tokenSymbol: string }) {
Your {tokenSymbol} has been successfully bridged. The transaction should - be confirmed within 1-3 minutes. + be confirmed within {confirmationMessage}.
); diff --git a/libs/burn-waitlist/src/lib/components/participation-form.tsx b/libs/burn-waitlist/src/lib/components/participation-form.tsx index 37e85c4be..134483e6b 100644 --- a/libs/burn-waitlist/src/lib/components/participation-form.tsx +++ b/libs/burn-waitlist/src/lib/components/participation-form.tsx @@ -4,9 +4,9 @@ import { useMemo } from 'react'; import { Button } from '@haqq/shell-ui-kit'; import { ModalInput } from '@haqq/shell-ui-kit'; import { FundsSource } from '../constants/waitlist-config'; -import { formatEther } from 'viem'; import { WaitlistBalances } from './waitlist-balances'; import type { WaitlistBalancesResponse } from '../hooks/use-waitlist-balances'; +import { formatEthDecimal } from '@haqq/shell-shared'; export interface ParticipationFormProps { amount: string; @@ -43,18 +43,11 @@ export function ParticipationForm({ } // Handle negative balances if (availableBalance < 0n) { - return `-${formatEther(-availableBalance)}`; + return `-${formatEthDecimal(-availableBalance, 4)}`; } - return formatEther(availableBalance); + return formatEthDecimal(availableBalance, 4); }, [availableBalance]); - const balanceLabel = useMemo(() => { - if (source === FundsSource.OwnBalance) { - return 'Wallet Balance'; - } - return 'Available Balance'; - }, [source]); - return (
@@ -81,11 +74,11 @@ export function ParticipationForm({ {amountError} ) : availableBalance && availableBalance < 0n ? ( - {balanceLabel}: {formattedBalance} ISLM (Insufficient) + Available Balance: {formattedBalance} ISLM (Insufficient) ) : ( - {balanceLabel}: {formattedBalance} ISLM + Available Balance: {formattedBalance} ISLM ) } @@ -126,6 +119,16 @@ export function ParticipationForm({
)} + {/* Warning for negative available balance */} + {availableBalance !== undefined && availableBalance < 0n && ( +
+
+ Need to fill balance {formatEthDecimal(-availableBalance, 4)} ISLM + for request creation +
+
+ )} + {error && (
{error}
diff --git a/libs/burn-waitlist/src/lib/components/requests-list.tsx b/libs/burn-waitlist/src/lib/components/requests-list.tsx index 2525e57d9..6d8610cd9 100644 --- a/libs/burn-waitlist/src/lib/components/requests-list.tsx +++ b/libs/burn-waitlist/src/lib/components/requests-list.tsx @@ -111,7 +111,9 @@ export function RequestsList({
Start undelegate diff --git a/libs/burn-waitlist/src/lib/hooks/use-waitlist-form.ts b/libs/burn-waitlist/src/lib/hooks/use-waitlist-form.ts index 410db2568..2527384f3 100644 --- a/libs/burn-waitlist/src/lib/hooks/use-waitlist-form.ts +++ b/libs/burn-waitlist/src/lib/hooks/use-waitlist-form.ts @@ -107,8 +107,10 @@ export function useWaitlistForm({ const handleMaxClick = useCallback(() => { if (availableBalance) { - // Reserve 0.2 ISLM for fees - const feeReserve = parseEther('0.2'); + // Reserve 0.2 ISLM for fees only if source is OwnBalance + // ucDAO source doesn't need fee reservation + const feeReserve = + formState.source === FundsSource.ucDAO ? 0n : parseEther('0.2'); const maxAmount = availableBalance > feeReserve ? availableBalance - feeReserve : 0n; @@ -118,7 +120,7 @@ export function useWaitlistForm({ const formatted = formatEther(maxAmount); setAmount(formatted); } - }, [availableBalance, setAmount]); + }, [availableBalance, setAmount, formState.source]); const validate = useCallback((): boolean => { const errors: WaitlistFormState['errors'] = {}; diff --git a/libs/burn-waitlist/src/lib/waitlist-page.tsx b/libs/burn-waitlist/src/lib/waitlist-page.tsx index 4531f8423..cec9cfe1b 100644 --- a/libs/burn-waitlist/src/lib/waitlist-page.tsx +++ b/libs/burn-waitlist/src/lib/waitlist-page.tsx @@ -2,18 +2,8 @@ import { useEffect, useState, useCallback, useMemo, useRef } from 'react'; import { useAccount, useBalance, useSwitchChain } from 'wagmi'; -import { formatEther } from 'viem'; import { Container } from '@haqq/shell-ui-kit/server'; -// Helper function to format ether with 4 decimal places -const formatEtherWithDecimals = (value: bigint): string => { - const formatted = formatEther(value); - const num = parseFloat(formatted); - return num.toLocaleString('en-US', { - minimumFractionDigits: 0, - maximumFractionDigits: 4, - }); -}; import { useWaitlistContractState, useCreateWaitlistRequest, @@ -37,6 +27,7 @@ import { FundsSource, WAITLIST_DEFAULT_CHAIN_ID, } from './constants/waitlist-config'; +import { formatEthDecimal } from '@haqq/shell-shared'; export interface WaitlistPageProps { locale?: string; @@ -516,7 +507,7 @@ export function WaitlistPage({ locale = 'en' }: WaitlistPageProps = {}) {
Total Amount
{totalAmount !== undefined - ? `${formatEtherWithDecimals(totalAmount)} ISLM` + ? `${formatEthDecimal(totalAmount, 4)} ISLM` : '—'}
@@ -557,7 +548,7 @@ export function WaitlistPage({ locale = 'en' }: WaitlistPageProps = {}) { Your Total Amount
- {formatEtherWithDecimals(userAggregates.totalAmount)} ISLM + {formatEthDecimal(userAggregates.totalAmount, 4)} ISLM
@@ -598,17 +589,6 @@ export function WaitlistPage({ locale = 'en' }: WaitlistPageProps = {}) { amountError={formState.errors.amount} /> )} - - {/* Warning for negative available balance */} - {availableBalance !== undefined && availableBalance < 0n && ( -
-
- Need to fill balance{' '} - {formatEtherWithDecimals(-availableBalance)} ISLM for - request creation -
-
- )}
{/* Second Column: Applications List */} From 3c5ce9d0f1969238d2e619f29383f2cef8ab9e2f Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Sat, 10 Jan 2026 11:33:35 +0300 Subject: [PATCH 101/141] refactor: optimize cancellation handling and refetching logic in waitlist page --- libs/burn-waitlist/src/lib/waitlist-page.tsx | 61 +++++++++++++------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/libs/burn-waitlist/src/lib/waitlist-page.tsx b/libs/burn-waitlist/src/lib/waitlist-page.tsx index cec9cfe1b..331d056f0 100644 --- a/libs/burn-waitlist/src/lib/waitlist-page.tsx +++ b/libs/burn-waitlist/src/lib/waitlist-page.tsx @@ -247,39 +247,59 @@ export function WaitlistPage({ locale = 'en' }: WaitlistPageProps = {}) { async (requestId: bigint) => { try { setCancellingRequestId(requestId); + + // Mark as cancelling immediately (before transaction is sent) to remove from UI + setPendingApplications((prev) => [ + ...prev, + { + requestId: requestId.toString(), + amount: '0', + author: address || '', + source: 0, + cancelled: true, + valid: false, + ready: false, + isPending: true, + txHash: undefined, + }, + ]); + const hash = await cancelRequestTx(requestId); - // Optimistically remove the application from the list - if (hash && applicationsData) { - setPendingApplications((prev) => [ - ...prev, - { - requestId: requestId.toString(), - amount: '0', - author: address || '', - source: 0, - cancelled: true, - valid: false, - ready: false, - isPending: true, - txHash: hash, - }, - ]); + // Update pending application with hash after transaction is sent + if (hash) { + setPendingApplications((prev) => + prev.map((app) => + app.requestId === requestId.toString() && app.cancelled + ? { ...app, txHash: hash } + : app, + ), + ); } - // Refetch immediately after transaction is sent + // Refetch immediately after transaction is sent (no need to wait for confirmation) refetchApplications(); refetchBalances(); refetchContractState(); - // Refetch again after delay to ensure backend has processed - setTimeout(() => { + // Refetch periodically for the next 10 seconds (every 2 seconds) + const intervalId = setInterval(() => { refetchApplications(); refetchBalances(); - refetchContractState(); }, 2000); + + // Stop refetching after 10 seconds + setTimeout(() => { + clearInterval(intervalId); + }, 10000); } catch (error) { console.error('Failed to cancel request:', error); + // Remove pending cancellation on error + setPendingApplications((prev) => + prev.filter( + (app) => !(app.requestId === requestId.toString() && app.cancelled), + ), + ); } finally { setCancellingRequestId(undefined); } @@ -289,7 +309,6 @@ export function WaitlistPage({ locale = 'en' }: WaitlistPageProps = {}) { refetchApplications, refetchBalances, refetchContractState, - applicationsData, address, ], ); From efc9f81f863314f8f706a506a7cce27b6dec5485 Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Sat, 10 Jan 2026 11:44:45 +0300 Subject: [PATCH 102/141] feat: implement user-friendly error message sanitization in waitlist page --- libs/burn-waitlist/src/lib/waitlist-page.tsx | 71 +++++++++++++++++++- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/libs/burn-waitlist/src/lib/waitlist-page.tsx b/libs/burn-waitlist/src/lib/waitlist-page.tsx index 331d056f0..d1c00ee6d 100644 --- a/libs/burn-waitlist/src/lib/waitlist-page.tsx +++ b/libs/burn-waitlist/src/lib/waitlist-page.tsx @@ -29,6 +29,71 @@ import { } from './constants/waitlist-config'; import { formatEthDecimal } from '@haqq/shell-shared'; +/** + * Sanitizes error messages to show user-friendly messages + */ +function sanitizeErrorMessage( + error: Error | null | undefined, +): string | undefined { + if (!error) { + return undefined; + } + + // Handle error objects without message property + const message = error instanceof Error ? error.message : String(error || ''); + + if (!message || message.trim() === '') { + return undefined; + } + + const messageLower = message.toLowerCase(); + + // User rejection errors + if ( + messageLower.includes('user rejected') || + messageLower.includes('user denied') || + messageLower.includes('denied transaction signature') || + messageLower.includes('rejected the request') || + messageLower.includes('user rejected the request') + ) { + return 'Transaction rejected by user'; + } + + // Network errors + if (messageLower.includes('network') || messageLower.includes('fetch')) { + return 'Network error. Please check your connection and try again'; + } + + // Insufficient funds + if ( + messageLower.includes('insufficient funds') || + messageLower.includes('insufficient balance') + ) { + return 'Insufficient balance'; + } + + // Contract execution reverted + if ( + messageLower.includes('execution reverted') || + messageLower.includes('revert') + ) { + // Try to extract a more meaningful message if available + const revertMatch = message.match(/execution reverted:?\s*(.+?)(?:\n|$)/i); + if (revertMatch && revertMatch[1] && revertMatch[1].trim().length < 100) { + return `Transaction failed: ${revertMatch[1].trim()}`; + } + return 'Transaction failed. Please try again'; + } + + // Transaction timeout or expired + if (messageLower.includes('timeout') || messageLower.includes('expired')) { + return 'Transaction timed out. Please try again'; + } + + // Return original message if no specific pattern matches, but limit length + return message.length > 200 ? `${message.substring(0, 200)}...` : message; +} + export interface WaitlistPageProps { locale?: string; } @@ -435,8 +500,10 @@ export function WaitlistPage({ locale = 'en' }: WaitlistPageProps = {}) { }, [isConnected, chain?.id, isCorrectChain, handleSwitchChain]); const isSubmitting = isCreating || isConfirmingCreate || isLoadingSignature; - const errorMessage = - createError?.message || cancelError?.message || undefined; + const errorMessage = useMemo( + () => sanitizeErrorMessage(createError || cancelError || undefined), + [createError, cancelError], + ); // Merge applications with pending ones, sort by requestId descending (newest first) const mergedApplications = useMemo(() => { From 2124f1d0fa0b2d995743489a9b4265906d8c5c4d Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Mon, 12 Jan 2026 13:17:30 +0300 Subject: [PATCH 103/141] chore: fix refetch applications --- libs/burn-waitlist/src/lib/waitlist-page.tsx | 82 ++++++++++++++++++-- 1 file changed, 77 insertions(+), 5 deletions(-) diff --git a/libs/burn-waitlist/src/lib/waitlist-page.tsx b/libs/burn-waitlist/src/lib/waitlist-page.tsx index d1c00ee6d..2df4c64ab 100644 --- a/libs/burn-waitlist/src/lib/waitlist-page.tsx +++ b/libs/burn-waitlist/src/lib/waitlist-page.tsx @@ -378,17 +378,88 @@ export function WaitlistPage({ locale = 'en' }: WaitlistPageProps = {}) { ], ); + // Automatically remove pending applications when they appear in backend data + // This ensures smooth transition from pending to confirmed state + useEffect(() => { + if (!applicationsData?.applications || pendingApplications.length === 0) { + return; + } + + // For each pending application, check if it appears in backend data + setPendingApplications((prev) => { + // Check if we need to update anything + const hasMatchesToRemove = prev.some((pendingApp) => { + // Only check pending creation apps (not cancelled ones) + if (pendingApp.requestId === 'pending' && !pendingApp.cancelled) { + // Check if we can find a matching application in the backend data + // Match by amount (as string), source, and author + const matchingApp = applicationsData.applications.find((app) => { + // Normalize amounts by comparing as BigInt to handle any string formatting + const pendingAmount = BigInt(pendingApp.amount); + const appAmount = BigInt(app.amount); + const amountsMatch = pendingAmount === appAmount; + + const sourcesMatch = app.source === pendingApp.source; + const authorsMatch = + app.author.toLowerCase() === pendingApp.author.toLowerCase(); + const notCancelled = !app.cancelled; + + return amountsMatch && sourcesMatch && authorsMatch && notCancelled; + }); + + // If found, this pending app should be removed + return !!matchingApp; + } + + return false; + }); + + // Only update if we found matches to remove + if (!hasMatchesToRemove) { + return prev; + } + + // Filter out pending apps that now appear in backend + return prev.filter((pendingApp) => { + // Keep pending apps that are being cancelled + if (pendingApp.cancelled && pendingApp.requestId !== 'pending') { + return true; // Keep cancelled apps until they're removed from backend + } + + // For pending creation apps, check if they appear in backend + if (pendingApp.requestId === 'pending' && !pendingApp.cancelled) { + // Check if we can find a matching application in the backend data + const matchingApp = applicationsData.applications.find((app) => { + // Normalize amounts by comparing as BigInt + const pendingAmount = BigInt(pendingApp.amount); + const appAmount = BigInt(app.amount); + const amountsMatch = pendingAmount === appAmount; + + const sourcesMatch = app.source === pendingApp.source; + const authorsMatch = + app.author.toLowerCase() === pendingApp.author.toLowerCase(); + const notCancelled = !app.cancelled; + + return amountsMatch && sourcesMatch && authorsMatch && notCancelled; + }); + + // If found, remove the pending app (it's now in backend) + // The backend app will be shown instead, maintaining continuity + return !matchingApp; + } + + // Keep other pending apps + return true; + }); + }); + }, [applicationsData?.applications]); + // Refetch after successful creation (transaction confirmed) useEffect(() => { if (isCreateSuccess && createHash && !hasProcessedSuccess.current) { // Mark as processed to prevent re-running hasProcessedSuccess.current = true; - // Remove pending application with this hash - setPendingApplications((prev) => - prev.filter((app) => app.txHash !== createHash), - ); - // Reset form immediately setAmount(''); @@ -399,6 +470,7 @@ export function WaitlistPage({ locale = 'en' }: WaitlistPageProps = {}) { refetchBalance(); // Refetch periodically for the next 10 seconds (every 2 seconds) + // to ensure backend has indexed the new application const intervalId = setInterval(() => { refetchApplications(); refetchBalances(); From a29385e41d1e7002f55d3e32c25991614f3678fc Mon Sep 17 00:00:00 2001 From: garageinc <9-b-rinat@rambler.ru> Date: Mon, 12 Jan 2026 14:25:29 +0300 Subject: [PATCH 104/141] chore: prod waitlist addresses --- libs/burn-waitlist/src/lib/abi/waitlist.ts | 199 ++++++++++++++++-- .../src/lib/components/participation-form.tsx | 23 +- .../src/lib/components/status-messages.tsx | 18 +- .../src/lib/constants/waitlist-config.ts | 23 +- .../src/lib/hooks/use-backend-signature.ts | 4 +- .../lib/hooks/use-waitlist-applications.ts | 13 +- .../src/lib/hooks/use-waitlist-balances.ts | 11 +- libs/burn-waitlist/src/lib/waitlist-page.tsx | 17 +- libs/ui-kit/src/lib/modal-input.tsx | 7 +- 9 files changed, 272 insertions(+), 43 deletions(-) diff --git a/libs/burn-waitlist/src/lib/abi/waitlist.ts b/libs/burn-waitlist/src/lib/abi/waitlist.ts index 279c673d3..bda6a074a 100644 --- a/libs/burn-waitlist/src/lib/abi/waitlist.ts +++ b/libs/burn-waitlist/src/lib/abi/waitlist.ts @@ -1,4 +1,4 @@ -// ABI for Waitlist contract - Updated from deployments/54211/abi.json +// ABI for Waitlist contract - Updated from deployments/11235/abi.json (HAQQ Mainnet) export const WaitlistAbi = [ { inputs: [ @@ -88,11 +88,6 @@ export const WaitlistAbi = [ name: 'InvalidInitialOwner', type: 'error', }, - { - inputs: [], - name: 'InvalidState', - type: 'error', - }, { inputs: [], name: 'OnlyAuthorCanCancel', @@ -154,6 +149,25 @@ export const WaitlistAbi = [ name: 'BackendSignerChanged', type: 'event', }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'previousOwner', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'newOwner', + type: 'address', + }, + ], + name: 'OwnershipTransferStarted', + type: 'event', + }, { anonymous: false, inputs: [ @@ -227,7 +241,7 @@ export const WaitlistAbi = [ type: 'uint256', }, { - indexed: false, + indexed: true, internalType: 'enum Waitlist.FundsSource', name: 'source', type: 'uint8', @@ -311,6 +325,13 @@ export const WaitlistAbi = [ stateMutability: 'view', type: 'function', }, + { + inputs: [], + name: 'acceptOwnership', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, { inputs: [], name: 'backendSigner', @@ -450,11 +471,6 @@ export const WaitlistAbi = [ name: 'cancelled', type: 'bool', }, - { - internalType: 'bytes', - name: 'backendSignature', - type: 'bytes', - }, ], internalType: 'struct Waitlist.Request', name: '', @@ -581,6 +597,19 @@ export const WaitlistAbi = [ stateMutability: 'view', type: 'function', }, + { + inputs: [], + name: 'pendingOwner', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, { inputs: [], name: 'renounceOwnership', @@ -588,6 +617,83 @@ export const WaitlistAbi = [ stateMutability: 'nonpayable', type: 'function', }, + { + inputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + name: 'requestExists', + outputs: [ + { + internalType: 'bool', + name: '', + type: 'bool', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + name: 'requestIdToIndex', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + name: 'requests', + outputs: [ + { + internalType: 'uint256', + name: 'requestId', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + { + internalType: 'address', + name: 'author', + type: 'address', + }, + { + internalType: 'enum Waitlist.FundsSource', + name: 'source', + type: 'uint8', + }, + { + internalType: 'bool', + name: 'cancelled', + type: 'bool', + }, + ], + stateMutability: 'view', + type: 'function', + }, { inputs: [ { @@ -601,6 +707,32 @@ export const WaitlistAbi = [ stateMutability: 'nonpayable', type: 'function', }, + { + inputs: [], + name: 'totalActiveAmount', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'totalActiveCount', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, { inputs: [ { @@ -621,6 +753,49 @@ export const WaitlistAbi = [ stateMutability: 'nonpayable', type: 'function', }, + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + name: 'userNonces', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + name: 'userRequests', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, { inputs: [], name: 'version', diff --git a/libs/burn-waitlist/src/lib/components/participation-form.tsx b/libs/burn-waitlist/src/lib/components/participation-form.tsx index 134483e6b..3cadbb084 100644 --- a/libs/burn-waitlist/src/lib/components/participation-form.tsx +++ b/libs/burn-waitlist/src/lib/components/participation-form.tsx @@ -21,6 +21,7 @@ export interface ParticipationFormProps { isSubmitting: boolean; error?: string; amountError?: string; + disabled?: boolean; } export function ParticipationForm({ @@ -36,6 +37,7 @@ export function ParticipationForm({ isSubmitting, error, amountError, + disabled = false, }: ParticipationFormProps) { const formattedBalance = useMemo(() => { if (!availableBalance) { @@ -82,7 +84,10 @@ export function ParticipationForm({ ) } - isMaxButtonDisabled={!availableBalance || availableBalance <= 0n} + isMaxButtonDisabled={ + !availableBalance || availableBalance <= 0n || disabled + } + disabled={disabled} />
@@ -93,25 +98,31 @@ export function ParticipationForm({ Funds Source
-