From ef14f70ff340dd59902f629eca0d057413c5952b Mon Sep 17 00:00:00 2001 From: shoom3301 Date: Mon, 13 Jul 2026 14:41:19 +0200 Subject: [PATCH 01/42] fix: switch network through reown --- libs/wallet/src/wagmi/hooks/useSwitchNetwork.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/libs/wallet/src/wagmi/hooks/useSwitchNetwork.ts b/libs/wallet/src/wagmi/hooks/useSwitchNetwork.ts index edbd38ef8ee..a0a1a27f07b 100644 --- a/libs/wallet/src/wagmi/hooks/useSwitchNetwork.ts +++ b/libs/wallet/src/wagmi/hooks/useSwitchNetwork.ts @@ -1,25 +1,34 @@ import { useSetAtom } from 'jotai' import { useCallback } from 'react' -import { useConnection, useSwitchChain } from 'wagmi' +import { useConnection } from 'wagmi' import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { useAppKitNetwork } from '@reown/appkit/react' + import { walletInfoAtom } from '../../api/state' +import { SUPPORTED_REOWN_NETWORKS } from '../../reown/networks' export function useSwitchNetwork(): (chainId: SupportedChainId) => Promise { - const { mutateAsync: switchChain } = useSwitchChain() const { isConnected } = useConnection() const setWalletInfo = useSetAtom(walletInfoAtom) + const { switchNetwork } = useAppKitNetwork() return useCallback( async (chainId: SupportedChainId) => { if (isConnected) { - await switchChain({ chainId }) + const network = SUPPORTED_REOWN_NETWORKS.find(({ id }) => id === chainId) + + if (!network) { + console.error('Unknown network to switch on', chainId) + return + } + await switchNetwork(network) } else { setWalletInfo((prev) => ({ ...prev, chainId })) } }, - [switchChain, isConnected, setWalletInfo], + [switchNetwork, isConnected, setWalletInfo], ) } From 4789ba4e19d71fa97c32842b55bb45cb480c9eed Mon Sep 17 00:00:00 2001 From: shoom3301 Date: Mon, 13 Jul 2026 15:33:29 +0200 Subject: [PATCH 02/42] docs: design for EVM/non-EVM network switching flow Co-Authored-By: Claude Opus 4.8 (1M context) --- ...13-evm-non-evm-network-switching-design.md | 164 ++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-13-evm-non-evm-network-switching-design.md diff --git a/docs/superpowers/specs/2026-07-13-evm-non-evm-network-switching-design.md b/docs/superpowers/specs/2026-07-13-evm-non-evm-network-switching-design.md new file mode 100644 index 00000000000..6562b53ca5a --- /dev/null +++ b/docs/superpowers/specs/2026-07-13-evm-non-evm-network-switching-design.md @@ -0,0 +1,164 @@ +# EVM ↔ non-EVM Network Switching Flow + +**Date:** 2026-07-13 +**Branch:** solana/web-1 +**Status:** Approved + +## Problem + +CoW Swap is gaining non-EVM network support (Solana). EVM and non-EVM networks +use fundamentally different wallets, so a connected wallet cannot simply "switch" +across the family boundary the way it switches between two EVM chains. Today, +selecting a cross-family network in the network picker either silently fails or +leaves the app in an inconsistent state. + +We need an explicit, user-confirmed flow: when the user picks a network that +belongs to a different chain family than the currently connected wallet, we +confirm the intent, disconnect the current wallet, and open the wallet +connection modal on the target network. + +## Goal + +When a user selects a network in the **network selector** that belongs to a +different chain family than the currently connected wallet (EVM ↔ non-EVM): + +1. Show a confirmation popup. +2. On confirm: disconnect the current wallet and open the wallet connection + modal, targeting the selected network. + +Same-family switches (EVM → EVM) keep working exactly as today. + +## Scope + +- **In scope:** the manual network picker path only — `useOnSelectNetwork` + (`apps/cowswap-frontend/src/common/hooks/useOnSelectNetwork.tsx`), which is + what the `NetworkSelector` UI calls. +- **Out of scope / untouched:** + - `useSwitchNetwork` (`libs/wallet/src/wagmi/hooks/useSwitchNetwork.ts`) — the + shared low-level primitive stays as-is. + - The automatic, URL-driven switching path in `useSetupTradeState` + (`apps/cowswap-frontend/src/modules/trade/hooks/setupTradeState/useSetupTradeState.ts`) + — keeps current behavior. Deep-link / programmatic family crossings are not + guarded by this flow. + +## Flow + +``` +user selects targetChain in NetworkSelector + │ + ▼ +is a wallet connected AND family(targetChain) ≠ family(currentChain)? + │ + ┌────┴─────────────────────────────┐ + no yes + │ │ + ▼ ▼ +existing behavior: show ConfirmationModal +switchNetwork(targetChain) "Switching network type… are you sure?" + │ + ┌──────┴───────┐ + Cancel Confirm + │ │ + ▼ ▼ + no-op 1. set target chain in URL + walletInfoAtom + 2. disconnect current wallet + 3. open wallet connection modal +``` + +## Design + +### Chain-family detection + +Add a small helper that answers "do these two chains belong to the same wallet +family?" built on the existing `@cowprotocol/cow-sdk` predicates (`isEvmChain`, +`isSolanaChain`, and later `isBtcChain`). Both-EVM → same family; otherwise same +only if it is the same chain family. This keeps the crossing check a single +readable call and extends cleanly when BTC lands. + +```ts +// conceptually: +function isSameChainFamily(a: SupportedChainId, b: SupportedChainId): boolean { + if (isEvmChain(a) && isEvmChain(b)) return true + // non-EVM families are distinct from EVM and (for now) from each other + return a === b +} +``` + +Placement: colocate with the other chain helpers so it is reusable (e.g. in +`@cowprotocol/cow-sdk` alongside `isEvmChain`, or in a wallet/common-utils +helper). Final location decided during implementation to match existing +conventions. + +### `useOnSelectNetwork` changes + +The hook currently: clears connection error → `await switchNetwork(targetChain)` +→ `setChainIdToUrl(targetChain)` → error handling → close modal. + +New behavior, evaluated at the top of the callback: + +- Compute `isConnected` (a wallet is connected) and + `crossingFamily = !isSameChainFamily(currentChainId, targetChain)`. +- **If `isConnected && crossingFamily`:** + 1. `const confirmed = await triggerConfirmation({ ...generic copy, skipInput: true })`. + 2. If not confirmed → return (no state change, existing wallet stays). + 3. If confirmed: + - `setChainIdToUrl(targetChain)` and set `walletInfoAtom.chainId` to the + target so the app reflects the target network while the user connects. + - `await disconnectWallet()`. + - `openWalletConnectionModal()`. + - Close the network selector modal (respect existing `skipClose`). +- **Otherwise** (same family, or no wallet connected): unchanged — run the + existing `switchNetwork` + `setChainIdToUrl` + error-handling path. + +### Confirmation copy (generic, both-ways) + +- **Title:** "Switching network type" +- **Body:** "You're switching between EVM and non-EVM networks. This requires + connecting a different wallet. Your current wallet will be disconnected. Are + you sure?" +- **Call to action:** "Yes" / cancel is "Cancel". +- `skipInput: true` — plain Yes/Cancel, no type-to-confirm word. + +Wrapped in `` / lingui macros consistent with the surrounding code. + +### Building blocks (all already exist) + +| Concern | Hook / API | +| ------------------ | --------------------------------------------------------------- | +| Confirmation modal | `useConfirmationRequest({ onEnable })` → `Promise` | +| Disconnect | `useDisconnectWallet()` | +| Open connect modal | `useOpenWalletConnectionModal()` (reown `open`) | +| Current chain | `useWalletInfo().chainId` | +| Target persistence | existing `setChainIdToUrl` + `walletInfoAtom.chainId` | + +### Reconnection targeting + +After disconnect we set the target chain in the URL and `walletInfoAtom` so the +app is already on the target network when the connection modal opens. If Reown +AppKit requires an explicit network hint for the connect view to default to the +target chain, add that call during implementation (e.g. an appkit +`switchNetwork(targetNetwork)` before `open`). The observable requirement: +after the user connects a compatible wallet, the app is on the selected network. + +## Decisions + +1. **Confirmation only when a wallet is connected.** With no wallet connected, a + cross-family pick just sets the target chain (today's behavior) — nothing to + disconnect, no reason to prompt. +2. **Canceling the connect modal after disconnect** leaves the app on the target + network with no wallet connected (standard disconnected state). Not a special + case. + +## Testing + +Unit-test the family-crossing decision in `useOnSelectNetwork`: + +- Same-family switch → calls `switchNetwork`; no confirmation shown. +- Cross-family + wallet connected → confirmation shown; on confirm → + `disconnectWallet` and `openWalletConnectionModal` called and target chain + persisted; on cancel → nothing happens (no disconnect, no switch). +- Cross-family + no wallet connected → no confirmation; existing set-target + behavior runs. + +Unit-test `isSameChainFamily`: EVM/EVM → true; EVM/Solana → false; +Solana/Solana → true. From a9ad910a8e47b59224da7c5ae303503e492e16b8 Mon Sep 17 00:00:00 2001 From: shoom3301 Date: Mon, 13 Jul 2026 15:54:54 +0200 Subject: [PATCH 03/42] feat(solana): switch between EVM/non-EVM chains --- .../hooks/useCrossChainFamilySwitch.test.tsx | 129 ++++++++++++++ .../hooks/useCrossChainFamilySwitch.tsx | 78 +++++++++ .../common/hooks/useOnSelectNetwork.test.tsx | 69 ++++++++ .../src/common/hooks/useOnSelectNetwork.tsx | 18 +- .../hooks/useSetWalletConnectionError.ts | 20 --- .../wallet/hooks/useWalletConnectionError.ts | 9 - .../wallet/state/walletConnectionAtom.ts | 7 - ...13-evm-non-evm-network-switching-design.md | 164 ------------------ libs/common-utils/src/index.ts | 1 + .../src/isSameChainFamily.test.ts | 19 ++ libs/common-utils/src/isSameChainFamily.ts | 16 ++ 11 files changed, 322 insertions(+), 208 deletions(-) create mode 100644 apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.test.tsx create mode 100644 apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx create mode 100644 apps/cowswap-frontend/src/common/hooks/useOnSelectNetwork.test.tsx delete mode 100644 apps/cowswap-frontend/src/modules/wallet/hooks/useSetWalletConnectionError.ts delete mode 100644 apps/cowswap-frontend/src/modules/wallet/hooks/useWalletConnectionError.ts delete mode 100644 apps/cowswap-frontend/src/modules/wallet/state/walletConnectionAtom.ts delete mode 100644 docs/superpowers/specs/2026-07-13-evm-non-evm-network-switching-design.md create mode 100644 libs/common-utils/src/isSameChainFamily.test.ts create mode 100644 libs/common-utils/src/isSameChainFamily.ts diff --git a/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.test.tsx b/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.test.tsx new file mode 100644 index 00000000000..34c9947cc39 --- /dev/null +++ b/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.test.tsx @@ -0,0 +1,129 @@ +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { useDisconnectWallet, useOpenWalletConnectionModal, useWalletInfo } from '@cowprotocol/wallet' + +import { act, renderHook } from '@testing-library/react' + +import { useCloseModal } from 'legacy/state/application/hooks' + +import { useConfirmationRequest } from './useConfirmationRequest' +import { useCrossChainFamilySwitch } from './useCrossChainFamilySwitch' +import { useLegacySetChainIdToUrl } from './useLegacySetChainIdToUrl' + +jest.mock('@cowprotocol/wallet') +jest.mock('./useConfirmationRequest') +jest.mock('./useLegacySetChainIdToUrl') +jest.mock('legacy/state/application/hooks') + +const mockedUseWalletInfo = useWalletInfo as jest.MockedFunction +const mockedUseDisconnectWallet = useDisconnectWallet as jest.MockedFunction +const mockedUseOpenWalletConnectionModal = useOpenWalletConnectionModal as jest.MockedFunction< + typeof useOpenWalletConnectionModal +> +const mockedUseConfirmationRequest = useConfirmationRequest as jest.MockedFunction +const mockedUseLegacySetChainIdToUrl = useLegacySetChainIdToUrl as jest.MockedFunction +const mockedUseCloseModal = useCloseModal as jest.MockedFunction + +describe('useCrossChainFamilySwitch', () => { + let disconnectWallet: jest.Mock + let openWalletConnectionModal: jest.Mock + let setChainIdToUrl: jest.Mock + let closeModal: jest.Mock + let triggerConfirmation: jest.Mock + + function setWallet(chainId: SupportedChainId, account: string | undefined): void { + mockedUseWalletInfo.mockReturnValue({ chainId, account } as ReturnType) + } + + beforeEach(() => { + jest.clearAllMocks() + + disconnectWallet = jest.fn().mockResolvedValue(undefined) + openWalletConnectionModal = jest.fn() + setChainIdToUrl = jest.fn() + closeModal = jest.fn() + triggerConfirmation = jest.fn().mockResolvedValue(true) + + mockedUseDisconnectWallet.mockReturnValue(disconnectWallet) + mockedUseOpenWalletConnectionModal.mockReturnValue(openWalletConnectionModal) + mockedUseLegacySetChainIdToUrl.mockReturnValue(setChainIdToUrl) + mockedUseConfirmationRequest.mockReturnValue(triggerConfirmation) + mockedUseCloseModal.mockReturnValue(closeModal) + + setWallet(SupportedChainId.MAINNET, '0xConnected') + }) + + it('returns false for a same-family change without prompting', async () => { + setWallet(SupportedChainId.MAINNET, '0xConnected') + const { result } = renderHook(() => useCrossChainFamilySwitch()) + + let handled = true + await act(async () => { + handled = await result.current(SupportedChainId.ARBITRUM_ONE) + }) + + expect(handled).toBe(false) + expect(triggerConfirmation).not.toHaveBeenCalled() + expect(disconnectWallet).not.toHaveBeenCalled() + }) + + it('returns false for a cross-family change when no wallet is connected', async () => { + setWallet(SupportedChainId.MAINNET, undefined) + const { result } = renderHook(() => useCrossChainFamilySwitch()) + + let handled = true + await act(async () => { + handled = await result.current(SupportedChainId.SOLANA) + }) + + expect(handled).toBe(false) + expect(triggerConfirmation).not.toHaveBeenCalled() + expect(disconnectWallet).not.toHaveBeenCalled() + }) + + it('confirms, disconnects, opens the connect modal and returns true for a cross-family change when connected', async () => { + setWallet(SupportedChainId.MAINNET, '0xConnected') + triggerConfirmation.mockResolvedValue(true) + const { result } = renderHook(() => useCrossChainFamilySwitch()) + + let handled = false + await act(async () => { + handled = await result.current(SupportedChainId.SOLANA) + }) + + expect(handled).toBe(true) + expect(triggerConfirmation).toHaveBeenCalledWith(expect.objectContaining({ skipInput: true })) + expect(setChainIdToUrl).toHaveBeenCalledWith(SupportedChainId.SOLANA) + expect(disconnectWallet).toHaveBeenCalled() + expect(openWalletConnectionModal).toHaveBeenCalled() + expect(closeModal).toHaveBeenCalled() + }) + + it('returns true but does nothing else when the user cancels', async () => { + setWallet(SupportedChainId.MAINNET, '0xConnected') + triggerConfirmation.mockResolvedValue(false) + const { result } = renderHook(() => useCrossChainFamilySwitch()) + + let handled = false + await act(async () => { + handled = await result.current(SupportedChainId.SOLANA) + }) + + expect(handled).toBe(true) + expect(disconnectWallet).not.toHaveBeenCalled() + expect(openWalletConnectionModal).not.toHaveBeenCalled() + expect(closeModal).not.toHaveBeenCalled() + }) + + it('keeps the network selector open when skipClose is passed', async () => { + setWallet(SupportedChainId.MAINNET, '0xConnected') + triggerConfirmation.mockResolvedValue(true) + const { result } = renderHook(() => useCrossChainFamilySwitch()) + + await act(async () => { + await result.current(SupportedChainId.SOLANA, true) + }) + + expect(disconnectWallet).toHaveBeenCalled() + expect(closeModal).not.toHaveBeenCalled() + }) +}) diff --git a/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx b/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx new file mode 100644 index 00000000000..70163ef2026 --- /dev/null +++ b/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx @@ -0,0 +1,78 @@ +import { useCallback } from 'react' + +import { isSameChainFamily } from '@cowprotocol/common-utils' +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { useDisconnectWallet, useOpenWalletConnectionModal, useWalletInfo } from '@cowprotocol/wallet' + +import { t } from '@lingui/core/macro' + +import { useCloseModal } from 'legacy/state/application/hooks' +import { ApplicationModal } from 'legacy/state/application/reducer' + +import { useConfirmationRequest } from './useConfirmationRequest' +import { useLegacySetChainIdToUrl } from './useLegacySetChainIdToUrl' + +/** + * Handles switching to a network that belongs to a different chain family than the + * currently connected wallet (EVM ↔ non-EVM, e.g. Ethereum → Solana). + * + * These families use different wallets, so we can't hot-swap the chain. Instead we + * confirm the user's intent, disconnect the current wallet and open the connection + * modal so they can connect a wallet compatible with the target network. + * + * Returns a function that resolves to `true` when it took over the switch (target + * crosses the family boundary while a wallet is connected — whether the user then + * confirmed or cancelled), and `false` when the caller should perform a regular + * same-family network switch. + */ +export function useCrossChainFamilySwitch(): (chainId: SupportedChainId, skipClose?: boolean) => Promise { + const { chainId: currentChainId, account } = useWalletInfo() + const closeModal = useCloseModal(ApplicationModal.NETWORK_SELECTOR) + const setChainIdToUrl = useLegacySetChainIdToUrl() + const disconnectWallet = useDisconnectWallet() + const openWalletConnectionModal = useOpenWalletConnectionModal() + const triggerConfirmation = useConfirmationRequest({}) + + return useCallback( + async (targetChain: SupportedChainId, skipClose?: boolean) => { + const isWalletConnected = !!account + const crossingChainFamily = !isSameChainFamily(currentChainId, targetChain) + + if (!isWalletConnected || !crossingChainFamily) { + return false + } + + const confirmed = await triggerConfirmation({ + confirmWord: t`confirm`, + title: t`Switching network type`, + description: t`You're switching between EVM and non-EVM networks. This requires connecting a different wallet. Your current wallet will be disconnected. Are you sure?`, + action: t`switch network type`, + callToAction: t`Confirm`, + skipInput: true, + }) + + if (!confirmed) { + return true + } + + setChainIdToUrl(targetChain) + await disconnectWallet() + openWalletConnectionModal() + + if (!skipClose) { + closeModal() + } + + return true + }, + [ + account, + currentChainId, + triggerConfirmation, + disconnectWallet, + openWalletConnectionModal, + setChainIdToUrl, + closeModal, + ], + ) +} diff --git a/apps/cowswap-frontend/src/common/hooks/useOnSelectNetwork.test.tsx b/apps/cowswap-frontend/src/common/hooks/useOnSelectNetwork.test.tsx new file mode 100644 index 00000000000..e2a1563135e --- /dev/null +++ b/apps/cowswap-frontend/src/common/hooks/useOnSelectNetwork.test.tsx @@ -0,0 +1,69 @@ +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { useSwitchNetwork } from '@cowprotocol/wallet' + +import { act, renderHook } from '@testing-library/react' + +import { useCloseModal } from 'legacy/state/application/hooks' + +import { useCrossChainFamilySwitch } from './useCrossChainFamilySwitch' +import { useLegacySetChainIdToUrl } from './useLegacySetChainIdToUrl' +import { useOnSelectNetwork } from './useOnSelectNetwork' + +jest.mock('@cowprotocol/wallet') +jest.mock('@cowprotocol/snackbars', () => ({ useAddSnackbar: () => jest.fn() })) +jest.mock('./useCrossChainFamilySwitch') +jest.mock('./useLegacySetChainIdToUrl') +jest.mock('legacy/state/application/hooks') + +const mockedUseSwitchNetwork = useSwitchNetwork as jest.MockedFunction +const mockedUseCrossChainFamilySwitch = useCrossChainFamilySwitch as jest.MockedFunction< + typeof useCrossChainFamilySwitch +> +const mockedUseLegacySetChainIdToUrl = useLegacySetChainIdToUrl as jest.MockedFunction +const mockedUseCloseModal = useCloseModal as jest.MockedFunction + +describe('useOnSelectNetwork', () => { + let switchNetwork: jest.Mock + let setChainIdToUrl: jest.Mock + let handleCrossChainFamilySwitch: jest.Mock + + beforeEach(() => { + jest.clearAllMocks() + + switchNetwork = jest.fn().mockResolvedValue(undefined) + setChainIdToUrl = jest.fn() + // By default the switch does not cross a chain family, so the regular path runs. + handleCrossChainFamilySwitch = jest.fn().mockResolvedValue(false) + + mockedUseSwitchNetwork.mockReturnValue(switchNetwork) + mockedUseCrossChainFamilySwitch.mockReturnValue(handleCrossChainFamilySwitch) + mockedUseLegacySetChainIdToUrl.mockReturnValue(setChainIdToUrl) + mockedUseCloseModal.mockReturnValue(jest.fn()) + }) + + it('performs a regular network switch when the change stays within the same chain family', async () => { + handleCrossChainFamilySwitch.mockResolvedValue(false) + const { result } = renderHook(() => useOnSelectNetwork()) + + await act(async () => { + await result.current(SupportedChainId.ARBITRUM_ONE) + }) + + expect(handleCrossChainFamilySwitch).toHaveBeenCalledWith(SupportedChainId.ARBITRUM_ONE, undefined) + expect(switchNetwork).toHaveBeenCalledWith(SupportedChainId.ARBITRUM_ONE) + expect(setChainIdToUrl).toHaveBeenCalledWith(SupportedChainId.ARBITRUM_ONE) + }) + + it('delegates to the cross-family flow and short-circuits when it handles the switch', async () => { + handleCrossChainFamilySwitch.mockResolvedValue(true) + const { result } = renderHook(() => useOnSelectNetwork()) + + await act(async () => { + await result.current(SupportedChainId.SOLANA) + }) + + expect(handleCrossChainFamilySwitch).toHaveBeenCalledWith(SupportedChainId.SOLANA, undefined) + expect(switchNetwork).not.toHaveBeenCalled() + expect(setChainIdToUrl).not.toHaveBeenCalled() + }) +}) diff --git a/apps/cowswap-frontend/src/common/hooks/useOnSelectNetwork.tsx b/apps/cowswap-frontend/src/common/hooks/useOnSelectNetwork.tsx index 8783513f2a5..f459276d54f 100644 --- a/apps/cowswap-frontend/src/common/hooks/useOnSelectNetwork.tsx +++ b/apps/cowswap-frontend/src/common/hooks/useOnSelectNetwork.tsx @@ -1,4 +1,4 @@ -/* eslint-disable @typescript-eslint/no-restricted-imports */ // TODO: Don't use 'modules' import +// TODO: Don't use 'modules' import import { useCallback } from 'react' import { getChainInfo } from '@cowprotocol/common-const' @@ -12,21 +12,25 @@ import { Trans } from '@lingui/react/macro' import { useCloseModal } from 'legacy/state/application/hooks' import { ApplicationModal } from 'legacy/state/application/reducer' -import { useSetWalletConnectionError } from 'modules/wallet/hooks/useSetWalletConnectionError' - +import { useCrossChainFamilySwitch } from './useCrossChainFamilySwitch' import { useLegacySetChainIdToUrl } from './useLegacySetChainIdToUrl' export function useOnSelectNetwork(): (chainId: SupportedChainId, skipClose?: boolean) => Promise { const addSnackbar = useAddSnackbar() const closeModal = useCloseModal(ApplicationModal.NETWORK_SELECTOR) const setChainIdToUrl = useLegacySetChainIdToUrl() - const setWalletConnectionError = useSetWalletConnectionError() const switchNetwork = useSwitchNetwork() + const handleCrossChainFamilySwitch = useCrossChainFamilySwitch() return useCallback( async (targetChain: SupportedChainId, skipClose?: boolean) => { + // Switching between EVM and non-EVM networks requires a different wallet and is handled + // separately (confirm + disconnect + reconnect) instead of a regular network switch. + if (await handleCrossChainFamilySwitch(targetChain, skipClose)) { + return + } + try { - setWalletConnectionError(undefined) await switchNetwork(targetChain) setChainIdToUrl(targetChain) @@ -52,14 +56,12 @@ export function useOnSelectNetwork(): (chainId: SupportedChainId, skipClose?: bo ), }) - - setWalletConnectionError(error.message) } if (!skipClose) { closeModal() } }, - [switchNetwork, setWalletConnectionError, addSnackbar, closeModal, setChainIdToUrl], + [handleCrossChainFamilySwitch, switchNetwork, addSnackbar, closeModal, setChainIdToUrl], ) } diff --git a/apps/cowswap-frontend/src/modules/wallet/hooks/useSetWalletConnectionError.ts b/apps/cowswap-frontend/src/modules/wallet/hooks/useSetWalletConnectionError.ts deleted file mode 100644 index caabc7552c5..00000000000 --- a/apps/cowswap-frontend/src/modules/wallet/hooks/useSetWalletConnectionError.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { useSetAtom } from 'jotai' -import { useCallback } from 'react' - -import { walletConnectionAtom } from '../state/walletConnectionAtom' - -// TODO: Add proper return type annotation -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function useSetWalletConnectionError() { - const setWalletConnectionState = useSetAtom(walletConnectionAtom) - - return useCallback( - (error: string | undefined) => { - setWalletConnectionState((state) => ({ - ...state, - connectionError: error, - })) - }, - [setWalletConnectionState], - ) -} diff --git a/apps/cowswap-frontend/src/modules/wallet/hooks/useWalletConnectionError.ts b/apps/cowswap-frontend/src/modules/wallet/hooks/useWalletConnectionError.ts deleted file mode 100644 index cc5730cdc99..00000000000 --- a/apps/cowswap-frontend/src/modules/wallet/hooks/useWalletConnectionError.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { useAtomValue } from 'jotai' - -import { walletConnectionAtom } from '../state/walletConnectionAtom' - -// TODO: Add proper return type annotation -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function useWalletConnectionError() { - return useAtomValue(walletConnectionAtom).connectionError -} diff --git a/apps/cowswap-frontend/src/modules/wallet/state/walletConnectionAtom.ts b/apps/cowswap-frontend/src/modules/wallet/state/walletConnectionAtom.ts deleted file mode 100644 index 2f51e010a67..00000000000 --- a/apps/cowswap-frontend/src/modules/wallet/state/walletConnectionAtom.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { atom } from 'jotai' - -export interface WalletConnectionState { - connectionError?: string -} - -export const walletConnectionAtom = atom({}) diff --git a/docs/superpowers/specs/2026-07-13-evm-non-evm-network-switching-design.md b/docs/superpowers/specs/2026-07-13-evm-non-evm-network-switching-design.md deleted file mode 100644 index 6562b53ca5a..00000000000 --- a/docs/superpowers/specs/2026-07-13-evm-non-evm-network-switching-design.md +++ /dev/null @@ -1,164 +0,0 @@ -# EVM ↔ non-EVM Network Switching Flow - -**Date:** 2026-07-13 -**Branch:** solana/web-1 -**Status:** Approved - -## Problem - -CoW Swap is gaining non-EVM network support (Solana). EVM and non-EVM networks -use fundamentally different wallets, so a connected wallet cannot simply "switch" -across the family boundary the way it switches between two EVM chains. Today, -selecting a cross-family network in the network picker either silently fails or -leaves the app in an inconsistent state. - -We need an explicit, user-confirmed flow: when the user picks a network that -belongs to a different chain family than the currently connected wallet, we -confirm the intent, disconnect the current wallet, and open the wallet -connection modal on the target network. - -## Goal - -When a user selects a network in the **network selector** that belongs to a -different chain family than the currently connected wallet (EVM ↔ non-EVM): - -1. Show a confirmation popup. -2. On confirm: disconnect the current wallet and open the wallet connection - modal, targeting the selected network. - -Same-family switches (EVM → EVM) keep working exactly as today. - -## Scope - -- **In scope:** the manual network picker path only — `useOnSelectNetwork` - (`apps/cowswap-frontend/src/common/hooks/useOnSelectNetwork.tsx`), which is - what the `NetworkSelector` UI calls. -- **Out of scope / untouched:** - - `useSwitchNetwork` (`libs/wallet/src/wagmi/hooks/useSwitchNetwork.ts`) — the - shared low-level primitive stays as-is. - - The automatic, URL-driven switching path in `useSetupTradeState` - (`apps/cowswap-frontend/src/modules/trade/hooks/setupTradeState/useSetupTradeState.ts`) - — keeps current behavior. Deep-link / programmatic family crossings are not - guarded by this flow. - -## Flow - -``` -user selects targetChain in NetworkSelector - │ - ▼ -is a wallet connected AND family(targetChain) ≠ family(currentChain)? - │ - ┌────┴─────────────────────────────┐ - no yes - │ │ - ▼ ▼ -existing behavior: show ConfirmationModal -switchNetwork(targetChain) "Switching network type… are you sure?" - │ - ┌──────┴───────┐ - Cancel Confirm - │ │ - ▼ ▼ - no-op 1. set target chain in URL + walletInfoAtom - 2. disconnect current wallet - 3. open wallet connection modal -``` - -## Design - -### Chain-family detection - -Add a small helper that answers "do these two chains belong to the same wallet -family?" built on the existing `@cowprotocol/cow-sdk` predicates (`isEvmChain`, -`isSolanaChain`, and later `isBtcChain`). Both-EVM → same family; otherwise same -only if it is the same chain family. This keeps the crossing check a single -readable call and extends cleanly when BTC lands. - -```ts -// conceptually: -function isSameChainFamily(a: SupportedChainId, b: SupportedChainId): boolean { - if (isEvmChain(a) && isEvmChain(b)) return true - // non-EVM families are distinct from EVM and (for now) from each other - return a === b -} -``` - -Placement: colocate with the other chain helpers so it is reusable (e.g. in -`@cowprotocol/cow-sdk` alongside `isEvmChain`, or in a wallet/common-utils -helper). Final location decided during implementation to match existing -conventions. - -### `useOnSelectNetwork` changes - -The hook currently: clears connection error → `await switchNetwork(targetChain)` -→ `setChainIdToUrl(targetChain)` → error handling → close modal. - -New behavior, evaluated at the top of the callback: - -- Compute `isConnected` (a wallet is connected) and - `crossingFamily = !isSameChainFamily(currentChainId, targetChain)`. -- **If `isConnected && crossingFamily`:** - 1. `const confirmed = await triggerConfirmation({ ...generic copy, skipInput: true })`. - 2. If not confirmed → return (no state change, existing wallet stays). - 3. If confirmed: - - `setChainIdToUrl(targetChain)` and set `walletInfoAtom.chainId` to the - target so the app reflects the target network while the user connects. - - `await disconnectWallet()`. - - `openWalletConnectionModal()`. - - Close the network selector modal (respect existing `skipClose`). -- **Otherwise** (same family, or no wallet connected): unchanged — run the - existing `switchNetwork` + `setChainIdToUrl` + error-handling path. - -### Confirmation copy (generic, both-ways) - -- **Title:** "Switching network type" -- **Body:** "You're switching between EVM and non-EVM networks. This requires - connecting a different wallet. Your current wallet will be disconnected. Are - you sure?" -- **Call to action:** "Yes" / cancel is "Cancel". -- `skipInput: true` — plain Yes/Cancel, no type-to-confirm word. - -Wrapped in `` / lingui macros consistent with the surrounding code. - -### Building blocks (all already exist) - -| Concern | Hook / API | -| ------------------ | --------------------------------------------------------------- | -| Confirmation modal | `useConfirmationRequest({ onEnable })` → `Promise` | -| Disconnect | `useDisconnectWallet()` | -| Open connect modal | `useOpenWalletConnectionModal()` (reown `open`) | -| Current chain | `useWalletInfo().chainId` | -| Target persistence | existing `setChainIdToUrl` + `walletInfoAtom.chainId` | - -### Reconnection targeting - -After disconnect we set the target chain in the URL and `walletInfoAtom` so the -app is already on the target network when the connection modal opens. If Reown -AppKit requires an explicit network hint for the connect view to default to the -target chain, add that call during implementation (e.g. an appkit -`switchNetwork(targetNetwork)` before `open`). The observable requirement: -after the user connects a compatible wallet, the app is on the selected network. - -## Decisions - -1. **Confirmation only when a wallet is connected.** With no wallet connected, a - cross-family pick just sets the target chain (today's behavior) — nothing to - disconnect, no reason to prompt. -2. **Canceling the connect modal after disconnect** leaves the app on the target - network with no wallet connected (standard disconnected state). Not a special - case. - -## Testing - -Unit-test the family-crossing decision in `useOnSelectNetwork`: - -- Same-family switch → calls `switchNetwork`; no confirmation shown. -- Cross-family + wallet connected → confirmation shown; on confirm → - `disconnectWallet` and `openWalletConnectionModal` called and target chain - persisted; on cancel → nothing happens (no disconnect, no switch). -- Cross-family + no wallet connected → no confirmation; existing set-target - behavior runs. - -Unit-test `isSameChainFamily`: EVM/EVM → true; EVM/Solana → false; -Solana/Solana → true. diff --git a/libs/common-utils/src/index.ts b/libs/common-utils/src/index.ts index cb9c5151302..082dfdd8ac0 100644 --- a/libs/common-utils/src/index.ts +++ b/libs/common-utils/src/index.ts @@ -47,6 +47,7 @@ export * from './isEnoughAmount' export * from './isFractionFalsy' export * from './isIframe' export * from './isInjectedWidget' +export * from './isSameChainFamily' export * from './isSellOrder' export * from './isSupportedChainId' export * from './isValidTokenListSource' diff --git a/libs/common-utils/src/isSameChainFamily.test.ts b/libs/common-utils/src/isSameChainFamily.test.ts new file mode 100644 index 00000000000..ffcf136873a --- /dev/null +++ b/libs/common-utils/src/isSameChainFamily.test.ts @@ -0,0 +1,19 @@ +import { SupportedChainId } from '@cowprotocol/cow-sdk' + +import { isSameChainFamily } from './isSameChainFamily' + +describe('isSameChainFamily', () => { + it('returns true for two EVM chains', () => { + expect(isSameChainFamily(SupportedChainId.MAINNET, SupportedChainId.ARBITRUM_ONE)).toBe(true) + }) + + it('returns true for the same chain', () => { + expect(isSameChainFamily(SupportedChainId.SOLANA, SupportedChainId.SOLANA)).toBe(true) + expect(isSameChainFamily(SupportedChainId.MAINNET, SupportedChainId.MAINNET)).toBe(true) + }) + + it('returns false when crossing between EVM and non-EVM', () => { + expect(isSameChainFamily(SupportedChainId.MAINNET, SupportedChainId.SOLANA)).toBe(false) + expect(isSameChainFamily(SupportedChainId.SOLANA, SupportedChainId.MAINNET)).toBe(false) + }) +}) diff --git a/libs/common-utils/src/isSameChainFamily.ts b/libs/common-utils/src/isSameChainFamily.ts new file mode 100644 index 00000000000..d35f16d78f3 --- /dev/null +++ b/libs/common-utils/src/isSameChainFamily.ts @@ -0,0 +1,16 @@ +import { isEvmChain, SupportedChainId } from '@cowprotocol/cow-sdk' + +/** + * Whether two chains belong to the same wallet family (EVM ↔ EVM, or the exact same non-EVM chain). + * + * EVM and non-EVM networks use different wallets, so switching across the family + * boundary requires disconnecting and connecting a compatible wallet rather than + * a plain network switch. + */ +export function isSameChainFamily(a: SupportedChainId, b: SupportedChainId): boolean { + if (isEvmChain(a) && isEvmChain(b)) return true + + // Non-EVM families are distinct from EVM and from each other (e.g. Solana vs Bitcoin), + // so they only match when it is literally the same chain. + return a === b +} From fdbc00b807751a3e5b4554c84db0acf1dda76d1c Mon Sep 17 00:00:00 2001 From: "cowswap-release-sync[bot]" <274575433+cowswap-release-sync[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 13:56:30 +0000 Subject: [PATCH 04/42] chore(i18n): extract i18n strings [automatic] --- apps/cowswap-frontend/src/locales/en-US.po | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/apps/cowswap-frontend/src/locales/en-US.po b/apps/cowswap-frontend/src/locales/en-US.po index c740d59cfbf..aebbc050b19 100644 --- a/apps/cowswap-frontend/src/locales/en-US.po +++ b/apps/cowswap-frontend/src/locales/en-US.po @@ -159,6 +159,10 @@ msgstr "Order {value} filled" msgid "You must give the CoW Protocol smart contracts permission to use your {tokenSymbol}. If you approve the default amount, you will only have to do this once per token." msgstr "You must give the CoW Protocol smart contracts permission to use your {tokenSymbol}. If you approve the default amount, you will only have to do this once per token." +#: apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx +msgid "Switching network type" +msgstr "Switching network type" + #: apps/cowswap-frontend/src/modules/ethFlow/pure/EthFlowStepper/steps/Step3.tsx msgid "Receiving {nativeTokenSymbol} Refund..." msgstr "Receiving {nativeTokenSymbol} Refund..." @@ -919,6 +923,10 @@ msgstr "CoW forum" msgid "Signing..." msgstr "Signing..." +#: apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx +msgid "switch network type" +msgstr "switch network type" + #: apps/cowswap-frontend/src/modules/affiliate/components/ReferralCodeModal/controller.helpers.tsx #~ msgid "Wallet ineligible. Try another code" #~ msgstr "Wallet ineligible. Try another code" @@ -931,6 +939,10 @@ msgstr "Signing..." #~ msgid "You don't have any {orderTabState} orders at the moment." #~ msgstr "You don't have any {orderTabState} orders at the moment." +#: apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx +msgid "You're switching between EVM and non-EVM networks. This requires connecting a different wallet. Your current wallet will be disconnected. Are you sure?" +msgstr "You're switching between EVM and non-EVM networks. This requires connecting a different wallet. Your current wallet will be disconnected. Are you sure?" + #: apps/cowswap-frontend/src/modules/limitOrders/pure/Settings/LimitOrdersSettings.pure.tsx msgid "Limit Price Position" msgstr "Limit Price Position" @@ -1253,6 +1265,7 @@ msgid "pools available to get yield on your assets!" msgstr "pools available to get yield on your assets!" #: apps/cowswap-frontend/src/common/containers/ConfirmationModal/index.tsx +#: apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx #: apps/cowswap-frontend/src/common/pure/ConfirmationModal/ConfirmationModal.tsx #: apps/cowswap-frontend/src/modules/erc20Approve/containers/ChangeApproveAmountModal/ChangeApproveAmountModalPure.tsx #: apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersConfirmModal/index.tsx @@ -8036,6 +8049,7 @@ msgstr "Receive (incl. fees)" #: apps/cowswap-frontend/src/common/containers/ConfirmationModal/index.tsx #: apps/cowswap-frontend/src/common/hooks/useConfirmPriceImpactWithoutFee.ts +#: apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx #: apps/cowswap-frontend/src/common/pure/ConfirmationModal/ConfirmationModal.cosmos.tsx msgid "confirm" msgstr "confirm" From dde49f11b0233248cb18c0aee9824bce2c48bb90 Mon Sep 17 00:00:00 2001 From: shoom3301 Date: Mon, 13 Jul 2026 16:30:07 +0200 Subject: [PATCH 05/42] fix: support non-EVM network switching from URL --- .../hooks/setupTradeState/useSetupTradeState.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/trade/hooks/setupTradeState/useSetupTradeState.ts b/apps/cowswap-frontend/src/modules/trade/hooks/setupTradeState/useSetupTradeState.ts index 28c744f5729..693ab81acae 100644 --- a/apps/cowswap-frontend/src/modules/trade/hooks/setupTradeState/useSetupTradeState.ts +++ b/apps/cowswap-frontend/src/modules/trade/hooks/setupTradeState/useSetupTradeState.ts @@ -7,6 +7,8 @@ import { getRawCurrentChainIdFromUrl, isRejectRequestProviderError } from '@cowp import { SupportedChainId } from '@cowprotocol/cow-sdk' import { useSwitchNetwork, useWalletInfo } from '@cowprotocol/wallet' +import { useOnSelectNetwork } from 'common/hooks/useOnSelectNetwork' + import { useResetStateWithSymbolDuplication } from './useResetStateWithSymbolDuplication' import { useSetupTradeStateFromUrl } from './useSetupTradeStateFromUrl' import { useTradeStateFromUrl } from './useTradeStateFromUrl' @@ -35,6 +37,7 @@ export function useSetupTradeState(enableSellEqBuy = false): void { const { data: walletClient } = useWalletClient() const tradeNavigate = useTradeNavigate() const switchNetwork = useSwitchNetwork() + const onSelectNetwork = useOnSelectNetwork() const tradeStateFromUrl = useTradeStateFromUrl() const { state, updateState } = useTradeState() const tradeTypeInfo = useTradeTypeInfoFromUrl() @@ -56,9 +59,13 @@ export function useSetupTradeState(enableSellEqBuy = false): void { const isLimitOrderTrade = tradeTypeInfo?.tradeType === TradeType.LIMIT_ORDER const switchNetworkInWallet = useCallback( - async (targetChainId: SupportedChainId, currentProviderChainId: SupportedChainId | null) => { + async (targetChainId: SupportedChainId, currentProviderChainId: SupportedChainId | null, selectNetwork = false) => { try { - await switchNetwork(targetChainId) + if (selectNetwork) { + await onSelectNetwork(targetChainId) + } else { + await switchNetwork(targetChainId) + } } catch (error) { // We are ignoring Gnosis safe context error // Because it's a normal situation when we are not in Gnosis safe App @@ -77,7 +84,7 @@ export function useSetupTradeState(enableSellEqBuy = false): void { // Clean up rememberedUrlStateRef when network switching is finished rememberedUrlStateRef.current = null }, - [switchNetwork, tradeNavigate], + [switchNetwork, onSelectNetwork, tradeNavigate], ) const navigateAndSwitchNetwork = useCallback( @@ -244,7 +251,7 @@ export function useSetupTradeState(enableSellEqBuy = false): void { if (!providerChainId || providerChainId === currentChainId || !isUrlChainIdChanged) return const targetChainId = urlChainId ?? rememberedUrlStateRef.current?.chainId ?? currentChainId - switchNetworkInWallet(targetChainId, providerChainId) + switchNetworkInWallet(targetChainId, providerChainId, true) console.debug('[TRADE STATE]', 'Set chainId to provider', { walletClient, urlChainId }) // Triggering only when chainId in URL is changes, provider is changed or rememberedUrlState is changed From c97af95d8744dca655c29f241a8afab9bfe6ed06 Mon Sep 17 00:00:00 2001 From: shoom3301 Date: Mon, 13 Jul 2026 19:07:37 +0200 Subject: [PATCH 06/42] fix: handle wallet disconnect error --- .../hooks/useCrossChainFamilySwitch.test.tsx | 17 +++++++++++++++++ .../common/hooks/useCrossChainFamilySwitch.tsx | 10 +++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.test.tsx b/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.test.tsx index 34c9947cc39..9318f1a5646 100644 --- a/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.test.tsx +++ b/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.test.tsx @@ -114,6 +114,23 @@ describe('useCrossChainFamilySwitch', () => { expect(closeModal).not.toHaveBeenCalled() }) + it('reports via snackbar and leaves the URL unchanged when the disconnect fails', async () => { + setWallet(SupportedChainId.MAINNET, '0xConnected') + triggerConfirmation.mockResolvedValue(true) + disconnectWallet.mockRejectedValue(new Error('disconnect failed')) + const { result } = renderHook(() => useCrossChainFamilySwitch()) + + let handled = false + await act(async () => { + handled = await result.current(SupportedChainId.SOLANA) + }) + + expect(handled).toBe(true) + expect(setChainIdToUrl).not.toHaveBeenCalled() + expect(openWalletConnectionModal).not.toHaveBeenCalled() + expect(closeModal).not.toHaveBeenCalled() + }) + it('keeps the network selector open when skipClose is passed', async () => { setWallet(SupportedChainId.MAINNET, '0xConnected') triggerConfirmation.mockResolvedValue(true) diff --git a/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx b/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx index 70163ef2026..43cdf00021e 100644 --- a/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx +++ b/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx @@ -55,8 +55,16 @@ export function useCrossChainFamilySwitch(): (chainId: SupportedChainId, skipClo return true } + try { + // Only change the URL after the wallet is actually disconnected, so URL and wallet + // stay in sync if the disconnect fails. + await disconnectWallet() + } catch (error) { + console.error('Failed to disconnect wallet while switching network type', error) + return true + } + setChainIdToUrl(targetChain) - await disconnectWallet() openWalletConnectionModal() if (!skipClose) { From 11e5ce34f7ef89cbbadd8ac85a7d0cb052a7155a Mon Sep 17 00:00:00 2001 From: shoom3301 Date: Tue, 14 Jul 2026 10:42:27 +0200 Subject: [PATCH 07/42] chore: rename test --- .../src/common/hooks/useCrossChainFamilySwitch.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.test.tsx b/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.test.tsx index 9318f1a5646..ae4a3c870fb 100644 --- a/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.test.tsx +++ b/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.test.tsx @@ -114,7 +114,7 @@ describe('useCrossChainFamilySwitch', () => { expect(closeModal).not.toHaveBeenCalled() }) - it('reports via snackbar and leaves the URL unchanged when the disconnect fails', async () => { + it('leaves the URL unchanged when the disconnect fails', async () => { setWallet(SupportedChainId.MAINNET, '0xConnected') triggerConfirmation.mockResolvedValue(true) disconnectWallet.mockRejectedValue(new Error('disconnect failed')) From 2a8d3ee0650b7b5b7ae951b6c9ade7ccce8fd110 Mon Sep 17 00:00:00 2001 From: shoom3301 Date: Tue, 14 Jul 2026 13:15:57 +0200 Subject: [PATCH 08/42] feat(solana): load token balances --- .../hooks/useTokensBalancesCombined.ts | 6 +- .../updater/BalancesCombinedUpdater.tsx | 4 +- .../tokensList/pure/TokenListItem/index.tsx | 6 +- libs/balances-and-allowances/package.json | 4 + .../usePersistBalancesViaWebCalls.test.tsx | 6 + .../hooks/usePersistBalancesViaWebCalls.ts | 12 +- ...ePersistSolanaBalancesViaWebCalls.test.tsx | 179 +++ .../usePersistSolanaBalancesViaWebCalls.ts | 145 +++ .../src/services/fetchSolanaTokenBalances.ts | 54 + pnpm-lock.yaml | 1152 ++++++++++++++--- 10 files changed, 1399 insertions(+), 169 deletions(-) create mode 100644 libs/balances-and-allowances/src/hooks/usePersistSolanaBalancesViaWebCalls.test.tsx create mode 100644 libs/balances-and-allowances/src/hooks/usePersistSolanaBalancesViaWebCalls.ts create mode 100644 libs/balances-and-allowances/src/services/fetchSolanaTokenBalances.ts diff --git a/apps/cowswap-frontend/src/modules/combinedBalances/hooks/useTokensBalancesCombined.ts b/apps/cowswap-frontend/src/modules/combinedBalances/hooks/useTokensBalancesCombined.ts index 457b2ed3d3b..51a765942fe 100644 --- a/apps/cowswap-frontend/src/modules/combinedBalances/hooks/useTokensBalancesCombined.ts +++ b/apps/cowswap-frontend/src/modules/combinedBalances/hooks/useTokensBalancesCombined.ts @@ -1,9 +1,9 @@ import { useAtomValue } from 'jotai' +import type { BalancesState } from '@cowprotocol/balances-and-allowances' + import { balancesCombinedAtom } from '../state/balanceCombinedAtom' -// TODO: Add proper return type annotation -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function useTokensBalancesCombined() { +export function useTokensBalancesCombined(): BalancesState { return useAtomValue(balancesCombinedAtom) } diff --git a/apps/cowswap-frontend/src/modules/combinedBalances/updater/BalancesCombinedUpdater.tsx b/apps/cowswap-frontend/src/modules/combinedBalances/updater/BalancesCombinedUpdater.tsx index 8752d73dbb7..5538d60fa36 100644 --- a/apps/cowswap-frontend/src/modules/combinedBalances/updater/BalancesCombinedUpdater.tsx +++ b/apps/cowswap-frontend/src/modules/combinedBalances/updater/BalancesCombinedUpdater.tsx @@ -12,9 +12,7 @@ import { useIsHooksTradeType } from 'modules/trade' import { balancesCombinedAtom } from '../state/balanceCombinedAtom' -// TODO: Add proper return type annotation -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function BalancesCombinedUpdater() { +export function BalancesCombinedUpdater(): null { const { account, chainId } = useWalletInfo() const setBalancesCombined = useSetAtom(balancesCombinedAtom) const preHooksBalancesDiff = usePreHookBalanceDiff() diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/TokenListItem/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/pure/TokenListItem/index.tsx index 377c4740a51..36fba75b459 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/TokenListItem/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/TokenListItem/index.tsx @@ -2,7 +2,7 @@ import { MouseEventHandler, ReactNode } from 'react' import { TokenWithLogo } from '@cowprotocol/common-const' import { getCurrencyAddress } from '@cowprotocol/common-utils' -import { areAddressesEqual, getAddressKey, getTokenId, isEvmChain } from '@cowprotocol/cow-sdk' +import { areAddressesEqual, getAddressKey, getTokenId } from '@cowprotocol/cow-sdk' import { Currency, CurrencyAmount } from '@cowprotocol/currency' import { TokenListTags } from '@cowprotocol/tokens' import { FiatAmount, HoverTooltip, LoadingRows, LoadingRowSmall, TokenAmount } from '@cowprotocol/ui' @@ -111,9 +111,7 @@ export function TokenListItem(props: TokenListItemProps): ReactNode { // Balances are only fetched for EVM chains (wagmi + BFF are EVM-only). Bridge-only // destinations like Solana or Bitcoin land here too — for them, skip the balance column // entirely instead of rendering an indefinite loading skeleton. - // todo remove it when FE supports Solana rpc connection - const canShowBalances = isEvmChain(token.chainId) - const shouldShowBalances = isWalletConnected && canShowBalances + const shouldShowBalances = isWalletConnected const shouldFormatBalances = shouldShowBalances && hasIntersected const balanceAmount = shouldFormatBalances && balance !== undefined ? CurrencyAmount.fromRawAmount(token, balance.toString()) : undefined diff --git a/libs/balances-and-allowances/package.json b/libs/balances-and-allowances/package.json index bdfb120784b..f3ecc1fd0f2 100644 --- a/libs/balances-and-allowances/package.json +++ b/libs/balances-and-allowances/package.json @@ -35,6 +35,10 @@ "@cowprotocol/wallet": "workspace:*", "@cowprotocol/wallet-provider": "workspace:*", "@cowprotocol/currency": "workspace:*", + "@reown/appkit-adapter-solana": "1.8.19", + "@solana/spl-token": "0.4.14", + "@solana/web3.js": "1.98.4", + "@tanstack/react-query": "5.90.20", "jotai": "2.16.2", "ms.macro": "2.0.0", "react": "19.1.2", diff --git a/libs/balances-and-allowances/src/hooks/usePersistBalancesViaWebCalls.test.tsx b/libs/balances-and-allowances/src/hooks/usePersistBalancesViaWebCalls.test.tsx index 8cc0d1521d0..4328ee03d8d 100644 --- a/libs/balances-and-allowances/src/hooks/usePersistBalancesViaWebCalls.test.tsx +++ b/libs/balances-and-allowances/src/hooks/usePersistBalancesViaWebCalls.test.tsx @@ -17,6 +17,12 @@ jest.mock('wagmi', () => ({ useReadContracts: jest.fn(), })) +// The Solana path has its own dedicated test; stub it here so this suite stays focused on +// EVM wagmi gating and avoids pulling in the reown/web3/react-query runtime. +jest.mock('./usePersistSolanaBalancesViaWebCalls', () => ({ + usePersistSolanaBalancesViaWebCalls: jest.fn(), +})) + const mockBalancesUpdate: PersistentStateByChain> = mapSupportedNetworks({}) const wrapper = ({ children }: { children: ReactNode }): ReactNode => { diff --git a/libs/balances-and-allowances/src/hooks/usePersistBalancesViaWebCalls.ts b/libs/balances-and-allowances/src/hooks/usePersistBalancesViaWebCalls.ts index 32376cb2929..b4c6f79542a 100644 --- a/libs/balances-and-allowances/src/hooks/usePersistBalancesViaWebCalls.ts +++ b/libs/balances-and-allowances/src/hooks/usePersistBalancesViaWebCalls.ts @@ -8,6 +8,7 @@ import { getIsNativeToken } from '@cowprotocol/common-utils' import { isEvmChain, SupportedChainId } from '@cowprotocol/cow-sdk' import { useIsBlockNumberRelevant } from './useIsBlockNumberRelevant' +import { usePersistSolanaBalancesViaWebCalls } from './usePersistSolanaBalancesViaWebCalls' import { balancesAtom, BalancesState, balancesUpdateAtom } from '../state/balancesAtom' @@ -49,6 +50,9 @@ export function usePersistBalancesViaWebCalls(params: PersistBalancesAndAllowanc // wagmi + viem only support evm chains const isEvm = isEvmChain(chainId) + // Non-EVM chains (e.g. Solana) load balances via their own web calls + usePersistSolanaBalancesViaWebCalls(params) + const { data: balances, isLoading: isBalancesLoading, @@ -79,21 +83,21 @@ export function usePersistBalancesViaWebCalls(params: PersistBalancesAndAllowanc // Set balances loading state useEffect(() => { - if (!setLoadingState) return + if (!setLoadingState || !isEvm) return setBalances((state) => ({ ...state, isLoading: isBalancesLoading, chainId })) - }, [setBalances, isBalancesLoading, setLoadingState, chainId]) + }, [setBalances, isBalancesLoading, setLoadingState, isEvm, chainId]) // Set balances error state for full balances fetches only useEffect(() => { - if (!setLoadingState) return + if (!setLoadingState || !isEvm) return if (!error) return const message = error instanceof Error ? error.message : String(error) setBalances((state) => ({ ...state, error: message, isLoading: false })) - }, [setBalances, error, setLoadingState]) + }, [setBalances, error, setLoadingState, isEvm]) // Set balances to the store useEffect(() => { diff --git a/libs/balances-and-allowances/src/hooks/usePersistSolanaBalancesViaWebCalls.test.tsx b/libs/balances-and-allowances/src/hooks/usePersistSolanaBalancesViaWebCalls.test.tsx new file mode 100644 index 00000000000..a69b5e9c731 --- /dev/null +++ b/libs/balances-and-allowances/src/hooks/usePersistSolanaBalancesViaWebCalls.test.tsx @@ -0,0 +1,179 @@ +import { Provider, useAtomValue } from 'jotai' +import { useHydrateAtoms } from 'jotai/utils' +import React, { ReactNode } from 'react' + +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' + +import { getAddressKey, SupportedChainId, mapSupportedNetworks, solana } from '@cowprotocol/cow-sdk' +import { PersistentStateByChain } from '@cowprotocol/types' + +import { PublicKey } from '@solana/web3.js' +import { renderHook, waitFor } from '@testing-library/react' + +import { PersistBalancesAndAllowancesParams } from './usePersistBalancesViaWebCalls' +import { usePersistSolanaBalancesViaWebCalls } from './usePersistSolanaBalancesViaWebCalls' + +import { balancesAtom, BalancesState, balancesUpdateAtom } from '../state/balancesAtom' + +// Valid base58 addresses so `new PublicKey(...)` inside the hook does not throw. +const ACCOUNT = 'So11111111111111111111111111111111111111112' +const MINT_A = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' +const MINT_B = 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB' +const NATIVE_MINT = solana.nativeCurrency.address + +let mockConnection: MockConnection | undefined + +jest.mock('@reown/appkit-adapter-solana/react', () => ({ + useAppKitConnection: () => ({ connection: mockConnection }), +})) + +// The ATA-derivation math is not what we are testing; make it deterministic and echo the mint back so +// the mocked RPC/`unpackAccount` can look accounts up by their ATA key. We only assert what lands in the atom. +jest.mock('@solana/spl-token', () => ({ + TOKEN_PROGRAM_ID: 'TOKEN_PROGRAM_ID', + ASSOCIATED_TOKEN_PROGRAM_ID: 'ASSOCIATED_TOKEN_PROGRAM_ID', + getAssociatedTokenAddressSync: (mint: { toBase58(): string }) => ({ toBase58: () => `ata:${mint.toBase58()}` }), + unpackAccount: (ata: { toBase58(): string }) => ({ amount: mockAmountByAta[ata.toBase58()] }), +})) + +interface MockConnection { + rpcEndpoint: string + getMultipleAccountsInfo: jest.Mock +} + +// ATA base58 -> token amount, read by the mocked `unpackAccount`. +let mockAmountByAta: Record +// ATA base58 -> account info; an absent entry means "no account exists" (a zero balance). +let mockInfoByAta: Record + +function createConnection(): MockConnection { + return { + rpcEndpoint: 'https://solana.example/rpc', + getMultipleAccountsInfo: jest.fn((batch: Array<{ toBase58(): string }>) => + Promise.resolve(batch.map((ata) => mockInfoByAta[ata.toBase58()] ?? null)), + ), + } +} + +const mockBalancesUpdate: PersistentStateByChain> = mapSupportedNetworks({}) + +function makeParams(overrides: Partial = {}): PersistBalancesAndAllowancesParams { + return { + account: ACCOUNT, + chainId: SupportedChainId.SOLANA, + tokenAddresses: [MINT_A, MINT_B], + setLoadingState: true, + ...overrides, + } +} + +function renderWithBalances(params: PersistBalancesAndAllowancesParams): { result: { current: BalancesState } } { + return renderHook( + () => { + usePersistSolanaBalancesViaWebCalls(params) + return useAtomValue(balancesAtom) + }, + { wrapper }, + ) +} + +function wrapper({ children }: { children: ReactNode }): ReactNode { + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }) + + const HydrateAtoms = ({ children }: { children: ReactNode }): ReactNode => { + useHydrateAtoms([ + [ + balancesAtom, + { + isLoading: false, + chainId: SupportedChainId.SOLANA, + values: {}, + fromCache: false, + hasFirstLoad: false, + error: null, + } as BalancesState, + ], + [balancesUpdateAtom, mockBalancesUpdate], + ]) + return <>{children} + } + + return ( + + + {children} + + + ) +} + +describe('usePersistSolanaBalancesViaWebCalls', () => { + beforeEach(() => { + mockAmountByAta = { [`ata:${MINT_A}`]: 100n, [`ata:${MINT_B}`]: 250n } + mockInfoByAta = { [`ata:${MINT_A}`]: { present: true }, [`ata:${MINT_B}`]: { present: true } } + mockConnection = createConnection() + }) + + it('reads SPL balances via a single batched RPC call and persists them to the atom', async () => { + const { result } = renderWithBalances(makeParams()) + + await waitFor(() => expect(result.current.hasFirstLoad).toBe(true)) + + expect(mockConnection?.getMultipleAccountsInfo).toHaveBeenCalledTimes(1) + expect(result.current.values[getAddressKey(MINT_A)]).toBe(100n) + expect(result.current.values[getAddressKey(MINT_B)]).toBe(250n) + }) + + it('clears the loading state once balances are loaded', async () => { + const { result } = renderWithBalances(makeParams()) + + await waitFor(() => expect(result.current.hasFirstLoad).toBe(true)) + + expect(result.current.isLoading).toBe(false) + }) + + it('treats a missing token account as a zero balance rather than an error', async () => { + delete mockInfoByAta[`ata:${MINT_B}`] + + const { result } = renderWithBalances(makeParams()) + + await waitFor(() => expect(result.current.hasFirstLoad).toBe(true)) + + expect(result.current.values[getAddressKey(MINT_A)]).toBe(100n) + expect(result.current.values[getAddressKey(MINT_B)]).toBe(0n) + expect(result.current.error).toBeNull() + }) + + it('batches ATAs into chunks of 100 so large token lists do not exceed the RPC limit', async () => { + // 250 mints -> ceil(250 / 100) = 3 `getMultipleAccountsInfo` calls, none over the 100-account limit. + const mints = Array.from({ length: 250 }, (_, i) => + new PublicKey(Uint8Array.from({ length: 32 }, (_, j) => (i + j + 1) % 256)).toBase58(), + ) + mints.forEach((mint) => { + mockAmountByAta[`ata:${mint}`] = 7n + mockInfoByAta[`ata:${mint}`] = { present: true } + }) + + const { result } = renderWithBalances(makeParams({ tokenAddresses: mints })) + + await waitFor(() => expect(result.current.hasFirstLoad).toBe(true)) + + expect(mockConnection?.getMultipleAccountsInfo).toHaveBeenCalledTimes(3) + mockConnection?.getMultipleAccountsInfo.mock.calls.forEach(([batch]) => { + expect(batch.length).toBeLessThanOrEqual(100) + }) + expect(Object.keys(result.current.values)).toHaveLength(250) + expect(result.current.values[getAddressKey(mints[0])]).toBe(7n) + expect(result.current.values[getAddressKey(mints[249])]).toBe(7n) + }) + + it('excludes the native SOL address from the SPL batch (it has no ATA)', async () => { + renderWithBalances(makeParams({ tokenAddresses: [NATIVE_MINT, MINT_A] })) + + await waitFor(() => expect(mockConnection?.getMultipleAccountsInfo).toHaveBeenCalled()) + + const [atas] = mockConnection!.getMultipleAccountsInfo.mock.calls[0] + expect(atas).toHaveLength(1) + expect(atas[0].toBase58()).toBe(`ata:${MINT_A}`) + }) +}) diff --git a/libs/balances-and-allowances/src/hooks/usePersistSolanaBalancesViaWebCalls.ts b/libs/balances-and-allowances/src/hooks/usePersistSolanaBalancesViaWebCalls.ts new file mode 100644 index 00000000000..0e7f5bd3230 --- /dev/null +++ b/libs/balances-and-allowances/src/hooks/usePersistSolanaBalancesViaWebCalls.ts @@ -0,0 +1,145 @@ +import { useSetAtom } from 'jotai' +import { useEffect, useMemo } from 'react' + +import { useQuery } from '@tanstack/react-query' + +import { getIsNativeToken } from '@cowprotocol/common-utils' +import { getAddressKey, isSolanaChain } from '@cowprotocol/cow-sdk' + +import { useAppKitConnection } from '@reown/appkit-adapter-solana/react' +import { Connection } from '@solana/web3.js' + +import { useIsBlockNumberRelevant } from './useIsBlockNumberRelevant' +import { PersistBalancesAndAllowancesParams } from './usePersistBalancesViaWebCalls' + +import { fetchSolanaTokenBalances } from '../services/fetchSolanaTokenBalances' +import { balancesAtom, BalancesState, balancesUpdateAtom } from '../state/balancesAtom' + +interface SolanaQueryConfig { + enabled: boolean + refetchInterval: number | false | undefined +} + +/** + * Solana counterpart to {@link usePersistBalancesViaWebCalls}. Loads SPL-token balances for + * `tokenAddresses` via the reown Solana adapter's `Connection` and persists them into `balancesAtom` + * in the same shape the EVM path uses, so downstream consumers stay chain-agnostic. + */ +export function usePersistSolanaBalancesViaWebCalls(params: PersistBalancesAndAllowancesParams): void { + const { account, chainId, tokenAddresses, setLoadingState, onBalancesLoaded, refreshTrigger } = params + + const setBalances = useSetAtom(balancesAtom) + const setBalancesUpdate = useSetAtom(balancesUpdateAtom) + + const { connection } = useAppKitConnection() + + const isSolana = isSolanaChain(chainId) + + // Native SOL is handled elsewhere and has no ATA, so deriving one would throw — keep only SPL mints. + const tokenMints = useMemo( + () => tokenAddresses.filter((address) => !getIsNativeToken(chainId, address)), + [tokenAddresses, chainId], + ) + + const { enabled, refetchInterval } = getSolanaQueryConfig(params, isSolana, connection, tokenMints.length) + + const queryKey = useMemo( + // `rpcEndpoint` keys the cache to the active network so a chain switch does not surface stale balances. + // `refreshTrigger` forces an immediate refetch (e.g. after an order is filled), mirroring the EVM `scopeKey`. + () => ['solanaTokenBalances', chainId, account, connection?.rpcEndpoint, refreshTrigger, tokenMints] as const, + [chainId, account, connection?.rpcEndpoint, refreshTrigger, tokenMints], + ) + + const { + data: balances, + isLoading: isBalancesLoading, + error, + dataUpdatedAt, + } = useQuery({ + queryKey, + queryFn: () => fetchSolanaTokenBalances(connection!, account!, tokenMints), + enabled, + refetchInterval: refetchInterval || undefined, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + }) + + // Skip results from outdated fetches if there is a result from a newer one + const isNewData = useIsBlockNumberRelevant(chainId, dataUpdatedAt) + + // Set balances loading state + useEffect(() => { + if (!setLoadingState || !isSolana) return + + setBalances((state) => ({ ...state, isLoading: isBalancesLoading, chainId })) + }, [setBalances, isBalancesLoading, setLoadingState, isSolana, chainId]) + + // Set balances error state for full balances fetches only + useEffect(() => { + if (!setLoadingState || !isSolana) return + + if (!error) return + + const message = error instanceof Error ? error.message : String(error) + + setBalances((state) => ({ ...state, error: message, isLoading: false })) + }, [setBalances, error, setLoadingState, isSolana]) + + // Set balances to the store + useEffect(() => { + if (!isSolana || !account || !balances || !isNewData) return + + const balancesState = balances.reduce((acc, { mint, balance }) => { + acc[getAddressKey(mint)] = balance + return acc + }, {}) + + onBalancesLoaded?.(true) + + setBalances((state) => { + return { + ...state, + chainId, + fromCache: false, + hasFirstLoad: true, + error: null, + values: { ...state.values, ...balancesState }, + ...(setLoadingState ? { isLoading: false } : {}), + } + }) + + if (setLoadingState) { + setBalancesUpdate((state) => ({ + ...state, + [chainId]: { + ...state[chainId], + [account.toLowerCase()]: Date.now(), + }, + })) + } + }, [ + isSolana, + chainId, + account, + balances, + isNewData, + setBalances, + setLoadingState, + onBalancesLoaded, + setBalancesUpdate, + ]) +} + +function getSolanaQueryConfig( + params: PersistBalancesAndAllowancesParams, + isSolana: boolean, + connection: Connection | undefined, + tokenMintsCount: number, +): SolanaQueryConfig { + const { account, balancesQueryConfig, query: queryOptions } = params + + const refetchInterval = balancesQueryConfig?.refetchInterval ?? queryOptions?.refetchInterval + const enabled = isSolana && !!account && !!connection && tokenMintsCount > 0 && !balancesQueryConfig?.isPaused?.() + + return { enabled, refetchInterval } +} diff --git a/libs/balances-and-allowances/src/services/fetchSolanaTokenBalances.ts b/libs/balances-and-allowances/src/services/fetchSolanaTokenBalances.ts new file mode 100644 index 00000000000..bbdc23e1d6d --- /dev/null +++ b/libs/balances-and-allowances/src/services/fetchSolanaTokenBalances.ts @@ -0,0 +1,54 @@ +import { + ASSOCIATED_TOKEN_PROGRAM_ID, + getAssociatedTokenAddressSync, + TOKEN_PROGRAM_ID, + unpackAccount, +} from '@solana/spl-token' +import { Connection, PublicKey } from '@solana/web3.js' + +interface SolanaTokenBalance { + mint: string + balance: bigint +} + +// Solana's `getMultipleAccounts` RPC rejects requests for more than 100 accounts, so ATAs must be +// read in batches — a single request for a full token list (hundreds of tokens) fails outright. +const MAX_ACCOUNTS_PER_REQUEST = 100 + +/** + * Reads SPL-token balances for `tokenMints` owned by `ownerAddress`. + * + * Balances live on the owner's Associated Token Account (ATA), not on the mint, so we derive the + * ATA for each mint and batch-read them via `getMultipleAccountsInfo` in chunks of + * {@link MAX_ACCOUNTS_PER_REQUEST}. A missing account means the owner never held that token, which + * is a zero balance rather than an error. + */ +export async function fetchSolanaTokenBalances( + connection: Connection, + ownerAddress: string, + tokenMints: string[], +): Promise { + const owner = new PublicKey(ownerAddress) + + const atas = tokenMints.map((mint) => + getAssociatedTokenAddressSync(new PublicKey(mint), owner, false, TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID), + ) + + const batches: (typeof atas)[] = [] + for (let i = 0; i < atas.length; i += MAX_ACCOUNTS_PER_REQUEST) { + batches.push(atas.slice(i, i + MAX_ACCOUNTS_PER_REQUEST)) + } + + const infosPerBatch = await Promise.all(batches.map((batch) => connection.getMultipleAccountsInfo(batch))) + const infos = infosPerBatch.flat() + + return infos.map((info, index) => { + if (!info) { + return { mint: tokenMints[index], balance: 0n } + } + + const account = unpackAccount(atas[index], info, TOKEN_PROGRAM_ID) + + return { mint: tokenMints[index], balance: account.amount } + }) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0dec49b5fa4..57d47097022 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1174,7 +1174,7 @@ importers: version: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) wagmi: specifier: 3.6.9 - version: 3.6.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) + version: 3.6.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(react@19.1.2)(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) devDependencies: '@types/react': specifier: 19.1.3 @@ -1292,7 +1292,7 @@ importers: version: 5.90.20(react@19.1.2) '@wagmi/connectors': specifier: 8.0.9 - version: 8.0.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@wagmi/core@3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)))(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) + version: 8.0.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@wagmi/core@3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)))(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) csstype: specifier: 3.1.3 version: 3.1.3 @@ -1435,6 +1435,18 @@ importers: '@cowprotocol/wallet-provider': specifier: workspace:* version: link:../wallet-provider + '@reown/appkit-adapter-solana': + specifier: 1.8.19 + version: 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(zod@3.25.76) + '@solana/spl-token': + specifier: 0.4.14 + version: 0.4.14(@solana/web3.js@1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6))(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@solana/web3.js': + specifier: 1.98.4 + version: 1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@tanstack/react-query': + specifier: 5.90.20 + version: 5.90.20(react@19.1.2) jotai: specifier: 2.16.2 version: 2.16.2(@babel/core@7.28.4)(@babel/template@7.28.6)(@types/react@19.1.3)(react@19.1.2) @@ -1544,7 +1556,7 @@ importers: version: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) wagmi: specifier: 3.6.9 - version: 3.6.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) + version: 3.6.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(react@19.1.2)(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) devDependencies: '@testing-library/react': specifier: 16.3.0 @@ -1635,7 +1647,7 @@ importers: version: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) wagmi: specifier: 3.6.9 - version: 3.6.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) + version: 3.6.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(react@19.1.2)(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) devDependencies: '@types/ms': specifier: 2.1.0 @@ -1727,7 +1739,7 @@ importers: version: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) wagmi: specifier: 3.6.9 - version: 3.6.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) + version: 3.6.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(react@19.1.2)(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) devDependencies: '@types/react': specifier: 19.1.3 @@ -1813,7 +1825,7 @@ importers: version: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) wagmi: specifier: 3.6.9 - version: 3.6.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) + version: 3.6.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(react@19.1.2)(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) devDependencies: '@types/ms.macro': specifier: 2.0.0 @@ -1923,7 +1935,7 @@ importers: version: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) wagmi: specifier: 3.6.9 - version: 3.6.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) + version: 3.6.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(react@19.1.2)(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) devDependencies: '@testing-library/react': specifier: 16.3.0 @@ -2072,7 +2084,7 @@ importers: dependencies: '@coinbase/wallet-sdk': specifier: 4.3.7 - version: 4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + version: 4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@cowprotocol/assets': specifier: workspace:* version: link:../assets @@ -2117,46 +2129,46 @@ importers: version: 2.0.0 '@metamask/sdk': specifier: 0.31.4 - version: 0.31.4(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.6) + version: 0.31.4(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) '@reown/appkit': specifier: 1.8.19 - version: 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(zod@3.25.76) + version: 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-adapter-solana': specifier: 1.8.19 - version: 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(zod@3.25.76) + version: 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-adapter-wagmi': specifier: 1.8.19 - version: 1.8.19(patch_hash=b669cb20e36d425602b690643d4147fd1756aa3d94db205f5e4823777a277b1c)(725f042c52896b22a29c385487b96d7a) + version: 1.8.19(patch_hash=b669cb20e36d425602b690643d4147fd1756aa3d94db205f5e4823777a277b1c)(b5eb2d51373dca5abfc3587eabff26c7) '@reown/appkit-common': specifier: 1.8.19 - version: 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + version: 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-controllers': specifier: 1.8.19 - version: 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + version: 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@safe-global/api-kit': specifier: 4.2.0 - version: 4.2.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + version: 4.2.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@safe-global/safe-apps-react-sdk': specifier: 4.7.2 - version: 4.7.2(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + version: 4.7.2(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@safe-global/safe-apps-sdk': specifier: 9.1.0 - version: 9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + version: 9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@safe-global/types-kit': specifier: 3.0.0 version: 3.0.0(typescript@5.9.3)(zod@3.25.76) '@solana/web3.js': specifier: 1.98.4 - version: 1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6) + version: 1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10) '@tanstack/react-query': specifier: 5.90.20 version: 5.90.20(react@19.1.2) '@wagmi/connectors': specifier: 8.0.9 - version: 8.0.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@wagmi/core@3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)))(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) + version: 8.0.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@wagmi/core@3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) '@wagmi/core': specifier: 3.4.8 - version: 3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) + version: 3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) eventemitter3: specifier: 4.0.7 version: 4.0.7 @@ -2180,10 +2192,10 @@ importers: version: 2.3.3(react@19.1.2) viem: specifier: 2.48.8 - version: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + version: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) wagmi: specifier: 3.6.9 - version: 3.6.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) + version: 3.6.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) devDependencies: '@testing-library/react': specifier: 16.3.0 @@ -2208,7 +2220,7 @@ importers: version: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) wagmi: specifier: 3.6.9 - version: 3.6.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) + version: 3.6.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(react@19.1.2)(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) devDependencies: '@types/react': specifier: 19.1.3 @@ -18677,6 +18689,30 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@base-org/account@2.4.0(@types/react@19.1.3)(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@coinbase/cdp-sdk': 1.47.0(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@noble/hashes': 1.4.0 + clsx: 1.2.1 + eventemitter3: 5.0.1 + idb-keyval: 6.2.1 + ox: 0.6.9(typescript@5.9.3)(zod@3.25.76) + preact: 10.24.2 + viem: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + zustand: 5.0.3(@types/react@19.1.3)(immer@10.0.2)(react@19.1.2)(use-sync-external-store@1.5.0(react@19.1.2)) + transitivePeerDependencies: + - '@types/react' + - bufferutil + - debug + - fastestsmallesttextencoderdecoder + - immer + - react + - typescript + - use-sync-external-store + - utf-8-validate + - zod + optional: true + '@base-org/account@2.4.0(@types/react@19.1.3)(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(zod@3.25.76)': dependencies: '@coinbase/cdp-sdk': 1.47.0(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6) @@ -18921,6 +18957,27 @@ snapshots: human-id: 4.1.3 prettier: 2.8.8 + '@coinbase/cdp-sdk@1.47.0(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)': + dependencies: + '@solana-program/system': 0.10.0(@solana/kit@5.5.1(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)) + '@solana-program/token': 0.9.0(@solana/kit@5.5.1(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)) + '@solana/kit': 5.5.1(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + abitype: 1.0.6(typescript@5.9.3)(zod@3.25.76) + axios: 1.13.6 + axios-retry: 4.5.0(axios@1.13.6) + jose: 6.2.2 + md5: 2.3.0 + uncrypto: 0.1.3 + viem: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + zod: 3.25.76 + transitivePeerDependencies: + - bufferutil + - debug + - fastestsmallesttextencoderdecoder + - typescript + - utf-8-validate + optional: true + '@coinbase/cdp-sdk@1.47.0(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6)': dependencies: '@solana-program/system': 0.10.0(@solana/kit@5.5.1(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6)) @@ -18942,6 +18999,19 @@ snapshots: - utf-8-validate optional: true + '@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@noble/hashes': 1.8.0 + clsx: 1.2.1 + eventemitter3: 5.0.4 + preact: 10.28.2 + viem: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - zod + '@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': dependencies: '@noble/hashes': 1.8.0 @@ -21105,7 +21175,7 @@ snapshots: '@metamask/safe-event-emitter@3.1.2': {} - '@metamask/sdk-communication-layer@0.31.0(cross-fetch@4.0.0(encoding@0.1.13))(eciesjs@0.4.12)(eventemitter2@6.4.9)(readable-stream@3.6.2)(socket.io-client@4.8.1(bufferutil@4.0.8)(utf-8-validate@6.0.6))': + '@metamask/sdk-communication-layer@0.31.0(cross-fetch@4.0.0(encoding@0.1.13))(eciesjs@0.4.12)(eventemitter2@6.4.9)(readable-stream@3.6.2)(socket.io-client@4.8.1(bufferutil@4.0.8)(utf-8-validate@5.0.10))': dependencies: bufferutil: 4.0.8 cross-fetch: 4.0.0(encoding@0.1.13) @@ -21114,7 +21184,7 @@ snapshots: eciesjs: 0.4.12 eventemitter2: 6.4.9 readable-stream: 3.6.2 - socket.io-client: 4.8.1(bufferutil@4.0.8)(utf-8-validate@6.0.6) + socket.io-client: 4.8.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) utf-8-validate: 5.0.10 uuid: 8.3.2 transitivePeerDependencies: @@ -21124,12 +21194,12 @@ snapshots: dependencies: '@paulmillr/qr': 0.2.1 - '@metamask/sdk@0.31.4(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.6)': + '@metamask/sdk@0.31.4(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)': dependencies: '@babel/runtime': 7.27.0 '@metamask/onboarding': 1.0.1 '@metamask/providers': 16.1.0 - '@metamask/sdk-communication-layer': 0.31.0(cross-fetch@4.0.0(encoding@0.1.13))(eciesjs@0.4.12)(eventemitter2@6.4.9)(readable-stream@3.6.2)(socket.io-client@4.8.1(bufferutil@4.0.8)(utf-8-validate@6.0.6)) + '@metamask/sdk-communication-layer': 0.31.0(cross-fetch@4.0.0(encoding@0.1.13))(eciesjs@0.4.12)(eventemitter2@6.4.9)(readable-stream@3.6.2)(socket.io-client@4.8.1(bufferutil@4.0.8)(utf-8-validate@5.0.10)) '@metamask/sdk-install-modal-web': 0.31.2 '@paulmillr/qr': 0.2.1 bowser: 2.11.0 @@ -21141,7 +21211,7 @@ snapshots: obj-multiplex: 1.0.0 pump: 3.0.0 readable-stream: 3.6.2 - socket.io-client: 4.8.1(bufferutil@4.0.8)(utf-8-validate@6.0.6) + socket.io-client: 4.8.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) tslib: 2.8.1 util: 0.12.5 uuid: 8.3.2 @@ -22749,24 +22819,24 @@ snapshots: react: 19.1.2 react-redux: 8.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(redux@4.2.1) - '@reown/appkit-adapter-solana@1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(zod@3.25.76)': + '@reown/appkit-adapter-solana@1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-common': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-controllers': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@reown/appkit': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-common': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-polyfills': 1.8.19 - '@reown/appkit-utils': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(valtio@2.1.7(@types/react@19.1.3)(react@19.1.2))(zod@3.25.76) - '@reown/appkit-wallet': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6) - '@solana/spl-token': 0.4.14(@solana/web3.js@1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6))(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6) - '@solana/wallet-adapter-base': 0.9.27(@solana/web3.js@1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)) + '@reown/appkit-utils': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.1.3)(react@19.1.2))(zod@3.25.76) + '@reown/appkit-wallet': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@solana/spl-token': 0.4.14(@solana/web3.js@1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@solana/wallet-adapter-base': 0.9.27(@solana/web3.js@1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)) '@solana/wallet-standard-features': 1.3.0 '@solana/wallet-standard-util': 1.1.2 - '@solana/web3.js': 1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@solana/web3.js': 1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10) '@wallet-standard/app': 1.1.0 '@wallet-standard/base': 1.1.0 '@wallet-standard/features': 1.1.0 '@walletconnect/types': 2.23.7 - '@walletconnect/universal-provider': 2.23.7(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@walletconnect/universal-provider': 2.23.7(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) valtio: 2.1.7(@types/react@19.1.3)(react@19.1.2) optionalDependencies: borsh: 0.7.0 @@ -22797,22 +22867,28 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-adapter-wagmi@1.8.19(patch_hash=b669cb20e36d425602b690643d4147fd1756aa3d94db205f5e4823777a277b1c)(725f042c52896b22a29c385487b96d7a)': + '@reown/appkit-adapter-solana@1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(zod@3.25.76)': dependencies: '@reown/appkit': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(zod@3.25.76) '@reown/appkit-common': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) '@reown/appkit-controllers': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) '@reown/appkit-polyfills': 1.8.19 - '@reown/appkit-scaffold-ui': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(valtio@2.1.7(@types/react@19.1.3)(react@19.1.2))(zod@3.25.76) '@reown/appkit-utils': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(valtio@2.1.7(@types/react@19.1.3)(react@19.1.2))(zod@3.25.76) '@reown/appkit-wallet': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6) - '@wagmi/core': 3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) + '@solana/spl-token': 0.4.14(@solana/web3.js@1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6))(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@solana/wallet-adapter-base': 0.9.27(@solana/web3.js@1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)) + '@solana/wallet-standard-features': 1.3.0 + '@solana/wallet-standard-util': 1.1.2 + '@solana/web3.js': 1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@wallet-standard/app': 1.1.0 + '@wallet-standard/base': 1.1.0 + '@wallet-standard/features': 1.1.0 + '@walletconnect/types': 2.23.7 '@walletconnect/universal-provider': 2.23.7(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) valtio: 2.1.7(@types/react@19.1.3)(react@19.1.2) - viem: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - wagmi: 3.6.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) optionalDependencies: - '@wagmi/connectors': 8.0.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@wagmi/core@3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)))(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) + borsh: 0.7.0 + bs58: 6.0.0 transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -22820,26 +22896,18 @@ snapshots: - '@azure/identity' - '@azure/keyvault-secrets' - '@azure/storage-blob' - - '@base-org/account' - '@capacitor/preferences' - - '@coinbase/wallet-sdk' - - '@metamask/connect-evm' - '@netlify/blobs' - '@planetscale/database' - '@react-native-async-storage/async-storage' - - '@safe-global/safe-apps-provider' - - '@safe-global/safe-apps-sdk' - '@types/react' - '@upstash/redis' - '@vercel/kv' - - '@walletconnect/ethereum-provider' - - accounts - bufferutil - debug - encoding - fastestsmallesttextencoderdecoder - immer - - porto - react - supports-color - typescript @@ -22847,24 +22915,22 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-common@1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': - dependencies: - big.js: 6.2.2 - dayjs: 1.11.13 - viem: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - transitivePeerDependencies: - - bufferutil - - typescript - - utf-8-validate - - zod - - '@reown/appkit-controllers@1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': + '@reown/appkit-adapter-wagmi@1.8.19(patch_hash=b669cb20e36d425602b690643d4147fd1756aa3d94db205f5e4823777a277b1c)(725f042c52896b22a29c385487b96d7a)': dependencies: + '@reown/appkit': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(zod@3.25.76) '@reown/appkit-common': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@reown/appkit-controllers': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@reown/appkit-polyfills': 1.8.19 + '@reown/appkit-scaffold-ui': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(valtio@2.1.7(@types/react@19.1.3)(react@19.1.2))(zod@3.25.76) + '@reown/appkit-utils': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(valtio@2.1.7(@types/react@19.1.3)(react@19.1.2))(zod@3.25.76) '@reown/appkit-wallet': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@wagmi/core': 3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) '@walletconnect/universal-provider': 2.23.7(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) valtio: 2.1.7(@types/react@19.1.3)(react@19.1.2) viem: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + wagmi: 3.6.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) + optionalDependencies: + '@wagmi/connectors': 8.0.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@wagmi/core@3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)))(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -22872,29 +22938,49 @@ snapshots: - '@azure/identity' - '@azure/keyvault-secrets' - '@azure/storage-blob' + - '@base-org/account' - '@capacitor/preferences' + - '@coinbase/wallet-sdk' + - '@metamask/connect-evm' - '@netlify/blobs' - '@planetscale/database' - '@react-native-async-storage/async-storage' + - '@safe-global/safe-apps-provider' + - '@safe-global/safe-apps-sdk' - '@types/react' - '@upstash/redis' - '@vercel/kv' + - '@walletconnect/ethereum-provider' + - accounts - bufferutil + - debug - encoding + - fastestsmallesttextencoderdecoder + - immer + - porto - react - supports-color - typescript + - use-sync-external-store - utf-8-validate - zod - '@reown/appkit-pay@1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(zod@3.25.76)': + '@reown/appkit-adapter-wagmi@1.8.19(patch_hash=b669cb20e36d425602b690643d4147fd1756aa3d94db205f5e4823777a277b1c)(b5eb2d51373dca5abfc3587eabff26c7)': dependencies: - '@reown/appkit-common': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-controllers': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-ui': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-utils': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(valtio@2.1.7(@types/react@19.1.3)(react@19.1.2))(zod@3.25.76) - lit: 3.3.0 + '@reown/appkit': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-common': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-polyfills': 1.8.19 + '@reown/appkit-scaffold-ui': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.1.3)(react@19.1.2))(zod@3.25.76) + '@reown/appkit-utils': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.1.3)(react@19.1.2))(zod@3.25.76) + '@reown/appkit-wallet': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@wagmi/core': 3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@walletconnect/universal-provider': 2.23.7(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) valtio: 2.1.7(@types/react@19.1.3)(react@19.1.2) + viem: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + wagmi: 3.6.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + optionalDependencies: + '@wagmi/connectors': 8.0.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@wagmi/core@3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -22902,18 +22988,26 @@ snapshots: - '@azure/identity' - '@azure/keyvault-secrets' - '@azure/storage-blob' + - '@base-org/account' - '@capacitor/preferences' + - '@coinbase/wallet-sdk' + - '@metamask/connect-evm' - '@netlify/blobs' - '@planetscale/database' - '@react-native-async-storage/async-storage' + - '@safe-global/safe-apps-provider' + - '@safe-global/safe-apps-sdk' - '@types/react' - '@upstash/redis' - '@vercel/kv' + - '@walletconnect/ethereum-provider' + - accounts - bufferutil - debug - encoding - fastestsmallesttextencoderdecoder - immer + - porto - react - supports-color - typescript @@ -22921,54 +23015,35 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-polyfills@1.8.19': + '@reown/appkit-common@1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - buffer: 6.0.3 + big.js: 6.2.2 + dayjs: 1.11.13 + viem: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - zod - '@reown/appkit-scaffold-ui@1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(valtio@2.1.7(@types/react@19.1.3)(react@19.1.2))(zod@3.25.76)': + '@reown/appkit-common@1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-controllers': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-pay': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-ui': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-utils': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(valtio@2.1.7(@types/react@19.1.3)(react@19.1.2))(zod@3.25.76) - '@reown/appkit-wallet': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6) - lit: 3.3.0 + big.js: 6.2.2 + dayjs: 1.11.13 + viem: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@capacitor/preferences' - - '@netlify/blobs' - - '@planetscale/database' - - '@react-native-async-storage/async-storage' - - '@types/react' - - '@upstash/redis' - - '@vercel/kv' - bufferutil - - debug - - encoding - - fastestsmallesttextencoderdecoder - - immer - - react - - supports-color - typescript - - use-sync-external-store - utf-8-validate - - valtio - zod - '@reown/appkit-ui@1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': + '@reown/appkit-controllers@1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@phosphor-icons/webcomponents': 2.1.5 - '@reown/appkit-common': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-controllers': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-wallet': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6) - lit: 3.3.0 - qrcode: 1.5.3 + '@reown/appkit-common': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-wallet': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@walletconnect/universal-provider': 2.23.7(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + valtio: 2.1.7(@types/react@19.1.3)(react@19.1.2) + viem: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -22991,23 +23066,13 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-utils@1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(valtio@2.1.7(@types/react@19.1.3)(react@19.1.2))(zod@3.25.76)': + '@reown/appkit-controllers@1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': dependencies: '@reown/appkit-common': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-controllers': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-polyfills': 1.8.19 '@reown/appkit-wallet': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6) - '@solana/web3.js': 1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6) - '@wallet-standard/wallet': 1.1.0 - '@walletconnect/logger': 3.0.2 '@walletconnect/universal-provider': 2.23.7(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - bs58: 6.0.0 valtio: 2.1.7(@types/react@19.1.3)(react@19.1.2) viem: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - optionalDependencies: - '@base-org/account': 2.4.0(@types/react@19.1.3)(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(zod@3.25.76) - '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -23023,45 +23088,385 @@ snapshots: - '@upstash/redis' - '@vercel/kv' - bufferutil - - debug - encoding - - fastestsmallesttextencoderdecoder - - immer - react - supports-color - typescript - - use-sync-external-store - utf-8-validate - zod - '@reown/appkit-wallet@1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)': - dependencies: - '@reown/appkit-common': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-polyfills': 1.8.19 - '@walletconnect/logger': 3.0.2 - zod: 3.25.76 - transitivePeerDependencies: - - bufferutil - - typescript - - utf-8-validate - - '@reown/appkit@1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(zod@3.25.76)': + '@reown/appkit-pay@1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-controllers': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-pay': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-polyfills': 1.8.19 - '@reown/appkit-scaffold-ui': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(valtio@2.1.7(@types/react@19.1.3)(react@19.1.2))(zod@3.25.76) - '@reown/appkit-ui': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-utils': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(valtio@2.1.7(@types/react@19.1.3)(react@19.1.2))(zod@3.25.76) - '@reown/appkit-wallet': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6) - '@walletconnect/universal-provider': 2.23.7(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - bs58: 6.0.0 - semver: 7.8.0 + '@reown/appkit-common': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-ui': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.1.3)(react@19.1.2))(zod@3.25.76) + lit: 3.3.0 valtio: 2.1.7(@types/react@19.1.3)(react@19.1.2) - viem: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - optionalDependencies: - '@lit/react': 1.0.8(@types/react@19.1.3) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/kv' + - bufferutil + - debug + - encoding + - fastestsmallesttextencoderdecoder + - immer + - react + - supports-color + - typescript + - use-sync-external-store + - utf-8-validate + - zod + + '@reown/appkit-pay@1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(zod@3.25.76)': + dependencies: + '@reown/appkit-common': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@reown/appkit-controllers': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@reown/appkit-ui': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@reown/appkit-utils': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(valtio@2.1.7(@types/react@19.1.3)(react@19.1.2))(zod@3.25.76) + lit: 3.3.0 + valtio: 2.1.7(@types/react@19.1.3)(react@19.1.2) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/kv' + - bufferutil + - debug + - encoding + - fastestsmallesttextencoderdecoder + - immer + - react + - supports-color + - typescript + - use-sync-external-store + - utf-8-validate + - zod + + '@reown/appkit-polyfills@1.8.19': + dependencies: + buffer: 6.0.3 + + '@reown/appkit-scaffold-ui@1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.1.3)(react@19.1.2))(zod@3.25.76)': + dependencies: + '@reown/appkit-common': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-pay': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-ui': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.1.3)(react@19.1.2))(zod@3.25.76) + '@reown/appkit-wallet': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10) + lit: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/kv' + - bufferutil + - debug + - encoding + - fastestsmallesttextencoderdecoder + - immer + - react + - supports-color + - typescript + - use-sync-external-store + - utf-8-validate + - valtio + - zod + + '@reown/appkit-scaffold-ui@1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(valtio@2.1.7(@types/react@19.1.3)(react@19.1.2))(zod@3.25.76)': + dependencies: + '@reown/appkit-common': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@reown/appkit-controllers': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@reown/appkit-pay': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(zod@3.25.76) + '@reown/appkit-ui': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@reown/appkit-utils': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(valtio@2.1.7(@types/react@19.1.3)(react@19.1.2))(zod@3.25.76) + '@reown/appkit-wallet': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6) + lit: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/kv' + - bufferutil + - debug + - encoding + - fastestsmallesttextencoderdecoder + - immer + - react + - supports-color + - typescript + - use-sync-external-store + - utf-8-validate + - valtio + - zod + + '@reown/appkit-ui@1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@phosphor-icons/webcomponents': 2.1.5 + '@reown/appkit-common': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-wallet': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10) + lit: 3.3.0 + qrcode: 1.5.3 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/kv' + - bufferutil + - encoding + - react + - supports-color + - typescript + - utf-8-validate + - zod + + '@reown/appkit-ui@1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': + dependencies: + '@phosphor-icons/webcomponents': 2.1.5 + '@reown/appkit-common': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@reown/appkit-controllers': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@reown/appkit-wallet': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6) + lit: 3.3.0 + qrcode: 1.5.3 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/kv' + - bufferutil + - encoding + - react + - supports-color + - typescript + - utf-8-validate + - zod + + '@reown/appkit-utils@1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.1.3)(react@19.1.2))(zod@3.25.76)': + dependencies: + '@reown/appkit-common': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-polyfills': 1.8.19 + '@reown/appkit-wallet': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@solana/web3.js': 1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@wallet-standard/wallet': 1.1.0 + '@walletconnect/logger': 3.0.2 + '@walletconnect/universal-provider': 2.23.7(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + bs58: 6.0.0 + valtio: 2.1.7(@types/react@19.1.3)(react@19.1.2) + viem: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + optionalDependencies: + '@base-org/account': 2.4.0(@types/react@19.1.3)(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@5.0.10)(zod@3.25.76) + '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/kv' + - bufferutil + - debug + - encoding + - fastestsmallesttextencoderdecoder + - immer + - react + - supports-color + - typescript + - use-sync-external-store + - utf-8-validate + - zod + + '@reown/appkit-utils@1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(valtio@2.1.7(@types/react@19.1.3)(react@19.1.2))(zod@3.25.76)': + dependencies: + '@reown/appkit-common': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@reown/appkit-controllers': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@reown/appkit-polyfills': 1.8.19 + '@reown/appkit-wallet': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@solana/web3.js': 1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@wallet-standard/wallet': 1.1.0 + '@walletconnect/logger': 3.0.2 + '@walletconnect/universal-provider': 2.23.7(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + bs58: 6.0.0 + valtio: 2.1.7(@types/react@19.1.3)(react@19.1.2) + viem: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + optionalDependencies: + '@base-org/account': 2.4.0(@types/react@19.1.3)(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(zod@3.25.76) + '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/kv' + - bufferutil + - debug + - encoding + - fastestsmallesttextencoderdecoder + - immer + - react + - supports-color + - typescript + - use-sync-external-store + - utf-8-validate + - zod + + '@reown/appkit-wallet@1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)': + dependencies: + '@reown/appkit-common': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-polyfills': 1.8.19 + '@walletconnect/logger': 3.0.2 + zod: 3.25.76 + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + + '@reown/appkit-wallet@1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)': + dependencies: + '@reown/appkit-common': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@reown/appkit-polyfills': 1.8.19 + '@walletconnect/logger': 3.0.2 + zod: 3.25.76 + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + + '@reown/appkit@1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@reown/appkit-common': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-pay': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-polyfills': 1.8.19 + '@reown/appkit-scaffold-ui': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.1.3)(react@19.1.2))(zod@3.25.76) + '@reown/appkit-ui': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.1.3)(react@19.1.2))(zod@3.25.76) + '@reown/appkit-wallet': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@walletconnect/universal-provider': 2.23.7(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + bs58: 6.0.0 + semver: 7.8.0 + valtio: 2.1.7(@types/react@19.1.3)(react@19.1.2) + viem: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + optionalDependencies: + '@lit/react': 1.0.8(@types/react@19.1.3) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/kv' + - bufferutil + - debug + - encoding + - fastestsmallesttextencoderdecoder + - immer + - react + - supports-color + - typescript + - use-sync-external-store + - utf-8-validate + - zod + + '@reown/appkit@1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(zod@3.25.76)': + dependencies: + '@reown/appkit-common': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@reown/appkit-controllers': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@reown/appkit-pay': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(zod@3.25.76) + '@reown/appkit-polyfills': 1.8.19 + '@reown/appkit-scaffold-ui': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(valtio@2.1.7(@types/react@19.1.3)(react@19.1.2))(zod@3.25.76) + '@reown/appkit-ui': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@reown/appkit-utils': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(valtio@2.1.7(@types/react@19.1.3)(react@19.1.2))(zod@3.25.76) + '@reown/appkit-wallet': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@walletconnect/universal-provider': 2.23.7(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + bs58: 6.0.0 + semver: 7.8.0 + valtio: 2.1.7(@types/react@19.1.3)(react@19.1.2) + viem: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + optionalDependencies: + '@lit/react': 1.0.8(@types/react@19.1.3) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -23372,6 +23777,19 @@ snapshots: transitivePeerDependencies: - '@types/node' + '@safe-global/api-kit@4.2.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@safe-global/protocol-kit': 7.2.0(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@safe-global/types-kit': 3.1.0(typescript@5.9.3)(zod@3.25.76) + node-fetch: 2.7.0(encoding@0.1.13) + viem: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - bufferutil + - encoding + - typescript + - utf-8-validate + - zod + '@safe-global/api-kit@4.2.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': dependencies: '@safe-global/protocol-kit': 7.2.0(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) @@ -23404,6 +23822,23 @@ snapshots: - supports-color - utf-8-validate + '@safe-global/protocol-kit@7.2.0(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@safe-global/safe-deployments': 1.37.56 + '@safe-global/safe-modules-deployments': 3.0.4 + '@safe-global/types-kit': 3.1.0(typescript@5.9.3)(zod@3.25.76) + abitype: 1.2.3(typescript@5.9.3)(zod@3.25.76) + semver: 7.8.0 + viem: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + optionalDependencies: + '@noble/curves': 1.9.7 + '@peculiar/asn1-schema': 2.6.0 + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - zod + '@safe-global/protocol-kit@7.2.0(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': dependencies: '@safe-global/safe-deployments': 1.37.56 @@ -23421,6 +23856,18 @@ snapshots: - utf-8-validate - zod + '@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + events: 3.3.0 + transitivePeerDependencies: + - bufferutil + - encoding + - typescript + - utf-8-validate + - zod + optional: true + '@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': dependencies: '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) @@ -23433,9 +23880,9 @@ snapshots: - zod optional: true - '@safe-global/safe-apps-react-sdk@4.7.2(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': + '@safe-global/safe-apps-react-sdk@4.7.2(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) react: 19.1.2 transitivePeerDependencies: - bufferutil @@ -23444,6 +23891,17 @@ snapshots: - utf-8-validate - zod + '@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@safe-global/safe-gateway-typescript-sdk': 3.8.0(encoding@0.1.13) + viem: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - bufferutil + - encoding + - typescript + - utf-8-validate + - zod + '@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': dependencies: '@safe-global/safe-gateway-typescript-sdk': 3.8.0(encoding@0.1.13) @@ -23454,6 +23912,7 @@ snapshots: - typescript - utf-8-validate - zod + optional: true '@safe-global/safe-deployments@1.37.50': dependencies: @@ -23658,11 +24117,21 @@ snapshots: '@socket.io/component-emitter@3.1.2': {} + '@solana-program/system@0.10.0(@solana/kit@5.5.1(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10))': + dependencies: + '@solana/kit': 5.5.1(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + optional: true + '@solana-program/system@0.10.0(@solana/kit@5.5.1(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6))': dependencies: '@solana/kit': 5.5.1(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6) optional: true + '@solana-program/token@0.9.0(@solana/kit@5.5.1(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10))': + dependencies: + '@solana/kit': 5.5.1(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + optional: true + '@solana-program/token@0.9.0(@solana/kit@5.5.1(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6))': dependencies: '@solana/kit': 5.5.1(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6) @@ -23702,6 +24171,18 @@ snapshots: typescript: 5.9.3 optional: true + '@solana/buffer-layout-utils@0.2.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)': + dependencies: + '@solana/buffer-layout': 4.0.1 + '@solana/web3.js': 1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10) + bigint-buffer: 1.1.5 + bignumber.js: 9.1.2 + transitivePeerDependencies: + - bufferutil + - encoding + - typescript + - utf-8-validate + '@solana/buffer-layout-utils@0.2.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)': dependencies: '@solana/buffer-layout': 4.0.1 @@ -23878,6 +24359,38 @@ snapshots: - fastestsmallesttextencoderdecoder optional: true + '@solana/kit@5.5.1(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)': + dependencies: + '@solana/accounts': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 5.5.1(typescript@5.9.3) + '@solana/functional': 5.5.1(typescript@5.9.3) + '@solana/instruction-plans': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/instructions': 5.5.1(typescript@5.9.3) + '@solana/keys': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/offchain-messages': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/plugin-core': 5.5.1(typescript@5.9.3) + '@solana/programs': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-api': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-parsed-types': 5.5.1(typescript@5.9.3) + '@solana/rpc-spec-types': 5.5.1(typescript@5.9.3) + '@solana/rpc-subscriptions': 5.5.1(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/signers': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/sysvars': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-confirmation': 5.5.1(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@solana/transaction-messages': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - fastestsmallesttextencoderdecoder + - utf-8-validate + optional: true + '@solana/kit@5.5.1(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6)': dependencies: '@solana/accounts': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -24027,6 +24540,20 @@ snapshots: - fastestsmallesttextencoderdecoder optional: true + '@solana/rpc-subscriptions-channel-websocket@5.5.1(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)': + dependencies: + '@solana/errors': 5.5.1(typescript@5.9.3) + '@solana/functional': 5.5.1(typescript@5.9.3) + '@solana/rpc-subscriptions-spec': 5.5.1(typescript@5.9.3) + '@solana/subscribable': 5.5.1(typescript@5.9.3) + ws: 8.20.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + optional: true + '@solana/rpc-subscriptions-channel-websocket@5.5.1(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)': dependencies: '@solana/errors': 5.5.1(typescript@5.9.3) @@ -24051,6 +24578,27 @@ snapshots: typescript: 5.9.3 optional: true + '@solana/rpc-subscriptions@5.5.1(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)': + dependencies: + '@solana/errors': 5.5.1(typescript@5.9.3) + '@solana/fast-stable-stringify': 5.5.1(typescript@5.9.3) + '@solana/functional': 5.5.1(typescript@5.9.3) + '@solana/promises': 5.5.1(typescript@5.9.3) + '@solana/rpc-spec-types': 5.5.1(typescript@5.9.3) + '@solana/rpc-subscriptions-api': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-subscriptions-channel-websocket': 5.5.1(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@solana/rpc-subscriptions-spec': 5.5.1(typescript@5.9.3) + '@solana/rpc-transformers': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/subscribable': 5.5.1(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - fastestsmallesttextencoderdecoder + - utf-8-validate + optional: true + '@solana/rpc-subscriptions@5.5.1(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6)': dependencies: '@solana/errors': 5.5.1(typescript@5.9.3) @@ -24143,6 +24691,14 @@ snapshots: - fastestsmallesttextencoderdecoder optional: true + '@solana/spl-token-group@0.0.7(@solana/web3.js@1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/web3.js': 1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - typescript + '@solana/spl-token-group@0.0.7(@solana/web3.js@1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: '@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -24151,6 +24707,14 @@ snapshots: - fastestsmallesttextencoderdecoder - typescript + '@solana/spl-token-metadata@0.1.6(@solana/web3.js@1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/web3.js': 1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - typescript + '@solana/spl-token-metadata@0.1.6(@solana/web3.js@1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: '@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -24159,6 +24723,21 @@ snapshots: - fastestsmallesttextencoderdecoder - typescript + '@solana/spl-token@0.4.14(@solana/web3.js@1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)': + dependencies: + '@solana/buffer-layout': 4.0.1 + '@solana/buffer-layout-utils': 0.2.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@solana/spl-token-group': 0.0.7(@solana/web3.js@1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/spl-token-metadata': 0.1.6(@solana/web3.js@1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/web3.js': 1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10) + buffer: 6.0.3 + transitivePeerDependencies: + - bufferutil + - encoding + - fastestsmallesttextencoderdecoder + - typescript + - utf-8-validate + '@solana/spl-token@0.4.14(@solana/web3.js@1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6))(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6)': dependencies: '@solana/buffer-layout': 4.0.1 @@ -24193,6 +24772,26 @@ snapshots: - fastestsmallesttextencoderdecoder optional: true + '@solana/transaction-confirmation@5.5.1(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)': + dependencies: + '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-strings': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 5.5.1(typescript@5.9.3) + '@solana/keys': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/promises': 5.5.1(typescript@5.9.3) + '@solana/rpc': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-subscriptions': 5.5.1(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-messages': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - fastestsmallesttextencoderdecoder + - utf-8-validate + optional: true + '@solana/transaction-confirmation@5.5.1(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6)': dependencies: '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -24250,6 +24849,14 @@ snapshots: - fastestsmallesttextencoderdecoder optional: true + '@solana/wallet-adapter-base@0.9.27(@solana/web3.js@1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10))': + dependencies: + '@solana/wallet-standard-features': 1.3.0 + '@solana/web3.js': 1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@wallet-standard/base': 1.1.0 + '@wallet-standard/features': 1.1.0 + eventemitter3: 5.0.4 + '@solana/wallet-adapter-base@0.9.27(@solana/web3.js@1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6))': dependencies: '@solana/wallet-standard-features': 1.3.0 @@ -24273,6 +24880,29 @@ snapshots: '@solana/wallet-standard-chains': 1.1.1 '@solana/wallet-standard-features': 1.3.0 + '@solana/web3.js@1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)': + dependencies: + '@babel/runtime': 7.27.0 + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + '@solana/buffer-layout': 4.0.1 + '@solana/codecs-numbers': 2.3.0(typescript@5.9.3) + agentkeepalive: 4.6.0 + bn.js: 5.2.1 + borsh: 0.7.0 + bs58: 4.0.1 + buffer: 6.0.3 + fast-stable-stringify: 1.0.0 + jayson: 4.3.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + node-fetch: 2.7.0(encoding@0.1.13) + rpc-websockets: 9.3.8 + superstruct: 2.0.2 + transitivePeerDependencies: + - bufferutil + - encoding + - typescript + - utf-8-validate + '@solana/web3.js@1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)': dependencies: '@babel/runtime': 7.27.0 @@ -25892,6 +26522,28 @@ snapshots: '@vue/shared@3.5.13': {} + '@wagmi/connectors@8.0.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@wagmi/core@3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': + dependencies: + '@wagmi/core': 3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + viem: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + optionalDependencies: + '@coinbase/wallet-sdk': 4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/ethereum-provider': 2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10) + typescript: 5.9.3 + + '@wagmi/connectors@8.0.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@wagmi/core@3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)))(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))': + dependencies: + '@wagmi/core': 3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) + viem: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + optionalDependencies: + '@coinbase/wallet-sdk': 4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@walletconnect/ethereum-provider': 2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10) + typescript: 5.9.3 + '@wagmi/connectors@8.0.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@wagmi/core@3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)))(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))': dependencies: '@wagmi/core': 3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) @@ -25903,6 +26555,21 @@ snapshots: '@walletconnect/ethereum-provider': 2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10) typescript: 5.9.3 + '@wagmi/core@3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': + dependencies: + eventemitter3: 5.0.1 + mipd: 0.0.7(typescript@5.9.3) + viem: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + zustand: 5.0.0(@types/react@19.1.3)(immer@10.0.2)(react@19.1.2)(use-sync-external-store@1.5.0(react@19.1.2)) + optionalDependencies: + '@tanstack/query-core': 5.90.20 + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/react' + - immer + - react + - use-sync-external-store + '@wagmi/core@3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))': dependencies: eventemitter3: 5.0.1 @@ -25968,6 +26635,44 @@ snapshots: - supports-color - utf-8-validate + '@walletconnect/core@2.23.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-provider': 1.0.14 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 3.0.2 + '@walletconnect/relay-api': 1.0.11 + '@walletconnect/relay-auth': 1.1.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.23.7 + '@walletconnect/utils': 2.23.7(typescript@5.9.3)(zod@3.25.76) + '@walletconnect/window-getters': 1.0.1 + es-toolkit: 1.44.0 + events: 3.3.0 + uint8arrays: 3.1.1 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/kv' + - bufferutil + - supports-color + - typescript + - utf-8-validate + - zod + '@walletconnect/core@2.23.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': dependencies: '@walletconnect/heartbeat': 1.2.2 @@ -26199,6 +26904,36 @@ snapshots: - supports-color - utf-8-validate + '@walletconnect/sign-client@2.23.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@walletconnect/core': 2.23.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/events': 1.0.1 + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/logger': 3.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.23.7 + '@walletconnect/utils': 2.23.7(typescript@5.9.3)(zod@3.25.76) + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/kv' + - bufferutil + - supports-color + - typescript + - utf-8-validate + - zod + '@walletconnect/sign-client@2.23.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': dependencies: '@walletconnect/core': 2.23.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) @@ -26334,6 +27069,40 @@ snapshots: - supports-color - utf-8-validate + '@walletconnect/universal-provider@2.23.7(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@walletconnect/events': 1.0.1 + '@walletconnect/jsonrpc-http-connection': 1.0.8(encoding@0.1.13) + '@walletconnect/jsonrpc-provider': 1.0.14 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 3.0.2 + '@walletconnect/sign-client': 2.23.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/types': 2.23.7 + '@walletconnect/utils': 2.23.7(typescript@5.9.3)(zod@3.25.76) + es-toolkit: 1.44.0 + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/kv' + - bufferutil + - encoding + - supports-color + - typescript + - utf-8-validate + - zod + '@walletconnect/universal-provider@2.23.7(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': dependencies: '@walletconnect/events': 1.0.1 @@ -28833,12 +29602,12 @@ snapshots: dependencies: once: 1.4.0 - engine.io-client@6.6.2(bufferutil@4.0.8)(utf-8-validate@6.0.6): + engine.io-client@6.6.2(bufferutil@4.0.8)(utf-8-validate@5.0.10): dependencies: '@socket.io/component-emitter': 3.1.2 debug: 4.3.7 engine.io-parser: 5.2.3 - ws: 8.17.1(bufferutil@4.0.8)(utf-8-validate@6.0.6) + ws: 8.17.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) xmlhttprequest-ssl: 2.1.2 transitivePeerDependencies: - bufferutil @@ -31046,6 +31815,10 @@ snapshots: isomorphic-timers-promises@1.0.1: {} + isomorphic-ws@4.0.1(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)): + dependencies: + ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10) + isomorphic-ws@4.0.1(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@6.0.6)): dependencies: ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@6.0.6) @@ -31161,6 +31934,24 @@ snapshots: javascript-stringify@2.1.0: {} + jayson@4.3.0(bufferutil@4.0.8)(utf-8-validate@5.0.10): + dependencies: + '@types/connect': 3.4.35 + '@types/node': 12.20.55 + '@types/ws': 7.4.7 + commander: 2.20.3 + delay: 5.0.0 + es6-promisify: 5.0.0 + eyes: 0.1.8 + isomorphic-ws: 4.0.1(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)) + json-stringify-safe: 5.0.1 + stream-json: 1.9.1 + uuid: 8.3.2 + ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + jayson@4.3.0(bufferutil@4.0.8)(utf-8-validate@6.0.6): dependencies: '@types/connect': 3.4.35 @@ -35318,11 +36109,11 @@ snapshots: dot-case: 3.0.4 tslib: 2.8.1 - socket.io-client@4.8.1(bufferutil@4.0.8)(utf-8-validate@6.0.6): + socket.io-client@4.8.1(bufferutil@4.0.8)(utf-8-validate@5.0.10): dependencies: '@socket.io/component-emitter': 3.1.2 debug: 4.3.7 - engine.io-client: 6.6.2(bufferutil@4.0.8)(utf-8-validate@6.0.6) + engine.io-client: 6.6.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) socket.io-parser: 4.2.4 transitivePeerDependencies: - bufferutil @@ -36926,7 +37717,53 @@ snapshots: dependencies: xml-name-validator: 5.0.0 + wagmi@3.6.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)): + dependencies: + '@tanstack/react-query': 5.90.20(react@19.1.2) + '@wagmi/connectors': 8.0.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@wagmi/core@3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@wagmi/core': 3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + react: 19.1.2 + use-sync-external-store: 1.5.0(react@19.1.2) + viem: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@base-org/account' + - '@coinbase/wallet-sdk' + - '@metamask/connect-evm' + - '@safe-global/safe-apps-provider' + - '@safe-global/safe-apps-sdk' + - '@tanstack/query-core' + - '@types/react' + - '@walletconnect/ethereum-provider' + - accounts + - immer + - porto + wagmi@3.6.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)): + dependencies: + '@tanstack/react-query': 5.90.20(react@19.1.2) + '@wagmi/connectors': 8.0.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@wagmi/core@3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)))(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) + '@wagmi/core': 3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) + react: 19.1.2 + use-sync-external-store: 1.5.0(react@19.1.2) + viem: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@base-org/account' + - '@coinbase/wallet-sdk' + - '@metamask/connect-evm' + - '@safe-global/safe-apps-provider' + - '@safe-global/safe-apps-sdk' + - '@tanstack/query-core' + - '@types/react' + - '@walletconnect/ethereum-provider' + - accounts + - immer + - porto + + wagmi@3.6.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(react@19.1.2)(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)): dependencies: '@tanstack/react-query': 5.90.20(react@19.1.2) '@wagmi/connectors': 8.0.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@wagmi/core@3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)))(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) @@ -37645,6 +38482,11 @@ snapshots: bufferutil: 4.0.8 utf-8-validate: 6.0.6 + ws@8.17.1(bufferutil@4.0.8)(utf-8-validate@5.0.10): + optionalDependencies: + bufferutil: 4.0.8 + utf-8-validate: 5.0.10 + ws@8.17.1(bufferutil@4.0.8)(utf-8-validate@6.0.6): optionalDependencies: bufferutil: 4.0.8 From e9e29e2674c51329cc2aa646639534aa9bbc610a Mon Sep 17 00:00:00 2001 From: shoom3301 Date: Tue, 14 Jul 2026 15:03:51 +0200 Subject: [PATCH 09/42] fix: confirm cross-chain switch on currency select --- .../hooks/useCrossChainFamilySwitch.test.tsx | 32 ++++++++--------- .../hooks/useCrossChainFamilySwitch.tsx | 20 ++++++++--- .../src/common/hooks/useOnSelectNetwork.tsx | 6 ++-- .../hooks/useOpenTokenSelectWidget.ts | 34 +++++++++++++++++-- .../src/theme/ThemedGlobalStyle.tsx | 2 +- 5 files changed, 68 insertions(+), 26 deletions(-) diff --git a/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.test.tsx b/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.test.tsx index ae4a3c870fb..0f1c8706026 100644 --- a/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.test.tsx +++ b/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.test.tsx @@ -6,7 +6,7 @@ import { act, renderHook } from '@testing-library/react' import { useCloseModal } from 'legacy/state/application/hooks' import { useConfirmationRequest } from './useConfirmationRequest' -import { useCrossChainFamilySwitch } from './useCrossChainFamilySwitch' +import { CrossChainFamilySwitchState, useCrossChainFamilySwitch } from './useCrossChainFamilySwitch' import { useLegacySetChainIdToUrl } from './useLegacySetChainIdToUrl' jest.mock('@cowprotocol/wallet') @@ -52,45 +52,45 @@ describe('useCrossChainFamilySwitch', () => { setWallet(SupportedChainId.MAINNET, '0xConnected') }) - it('returns false for a same-family change without prompting', async () => { + it('returns NOT_CROSSING_CHAIN for a same-family change without prompting', async () => { setWallet(SupportedChainId.MAINNET, '0xConnected') const { result } = renderHook(() => useCrossChainFamilySwitch()) - let handled = true + let handled: CrossChainFamilySwitchState | undefined await act(async () => { handled = await result.current(SupportedChainId.ARBITRUM_ONE) }) - expect(handled).toBe(false) + expect(handled).toBe(CrossChainFamilySwitchState.NOT_CROSSING_CHAIN) expect(triggerConfirmation).not.toHaveBeenCalled() expect(disconnectWallet).not.toHaveBeenCalled() }) - it('returns false for a cross-family change when no wallet is connected', async () => { + it('returns NOT_CROSSING_CHAIN for a cross-family change when no wallet is connected', async () => { setWallet(SupportedChainId.MAINNET, undefined) const { result } = renderHook(() => useCrossChainFamilySwitch()) - let handled = true + let handled: CrossChainFamilySwitchState | undefined await act(async () => { handled = await result.current(SupportedChainId.SOLANA) }) - expect(handled).toBe(false) + expect(handled).toBe(CrossChainFamilySwitchState.NOT_CROSSING_CHAIN) expect(triggerConfirmation).not.toHaveBeenCalled() expect(disconnectWallet).not.toHaveBeenCalled() }) - it('confirms, disconnects, opens the connect modal and returns true for a cross-family change when connected', async () => { + it('confirms, disconnects, opens the connect modal and returns FINISHED for a cross-family change when connected', async () => { setWallet(SupportedChainId.MAINNET, '0xConnected') triggerConfirmation.mockResolvedValue(true) const { result } = renderHook(() => useCrossChainFamilySwitch()) - let handled = false + let handled: CrossChainFamilySwitchState | undefined await act(async () => { handled = await result.current(SupportedChainId.SOLANA) }) - expect(handled).toBe(true) + expect(handled).toBe(CrossChainFamilySwitchState.FINISHED) expect(triggerConfirmation).toHaveBeenCalledWith(expect.objectContaining({ skipInput: true })) expect(setChainIdToUrl).toHaveBeenCalledWith(SupportedChainId.SOLANA) expect(disconnectWallet).toHaveBeenCalled() @@ -98,34 +98,34 @@ describe('useCrossChainFamilySwitch', () => { expect(closeModal).toHaveBeenCalled() }) - it('returns true but does nothing else when the user cancels', async () => { + it('returns NOT_CONFIRMED but does nothing else when the user cancels', async () => { setWallet(SupportedChainId.MAINNET, '0xConnected') triggerConfirmation.mockResolvedValue(false) const { result } = renderHook(() => useCrossChainFamilySwitch()) - let handled = false + let handled: CrossChainFamilySwitchState | undefined await act(async () => { handled = await result.current(SupportedChainId.SOLANA) }) - expect(handled).toBe(true) + expect(handled).toBe(CrossChainFamilySwitchState.NOT_CONFIRMED) expect(disconnectWallet).not.toHaveBeenCalled() expect(openWalletConnectionModal).not.toHaveBeenCalled() expect(closeModal).not.toHaveBeenCalled() }) - it('leaves the URL unchanged when the disconnect fails', async () => { + it('returns DISCONNECT_FAILED and leaves the URL unchanged when the disconnect fails', async () => { setWallet(SupportedChainId.MAINNET, '0xConnected') triggerConfirmation.mockResolvedValue(true) disconnectWallet.mockRejectedValue(new Error('disconnect failed')) const { result } = renderHook(() => useCrossChainFamilySwitch()) - let handled = false + let handled: CrossChainFamilySwitchState | undefined await act(async () => { handled = await result.current(SupportedChainId.SOLANA) }) - expect(handled).toBe(true) + expect(handled).toBe(CrossChainFamilySwitchState.DISCONNECT_FAILED) expect(setChainIdToUrl).not.toHaveBeenCalled() expect(openWalletConnectionModal).not.toHaveBeenCalled() expect(closeModal).not.toHaveBeenCalled() diff --git a/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx b/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx index 43cdf00021e..918105a1dad 100644 --- a/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx +++ b/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx @@ -12,6 +12,13 @@ import { ApplicationModal } from 'legacy/state/application/reducer' import { useConfirmationRequest } from './useConfirmationRequest' import { useLegacySetChainIdToUrl } from './useLegacySetChainIdToUrl' +export enum CrossChainFamilySwitchState { + NOT_CROSSING_CHAIN, + NOT_CONFIRMED, + DISCONNECT_FAILED, + FINISHED, +} + /** * Handles switching to a network that belongs to a different chain family than the * currently connected wallet (EVM ↔ non-EVM, e.g. Ethereum → Solana). @@ -25,7 +32,10 @@ import { useLegacySetChainIdToUrl } from './useLegacySetChainIdToUrl' * confirmed or cancelled), and `false` when the caller should perform a regular * same-family network switch. */ -export function useCrossChainFamilySwitch(): (chainId: SupportedChainId, skipClose?: boolean) => Promise { +export function useCrossChainFamilySwitch(): ( + chainId: SupportedChainId, + skipClose?: boolean, +) => Promise { const { chainId: currentChainId, account } = useWalletInfo() const closeModal = useCloseModal(ApplicationModal.NETWORK_SELECTOR) const setChainIdToUrl = useLegacySetChainIdToUrl() @@ -39,7 +49,7 @@ export function useCrossChainFamilySwitch(): (chainId: SupportedChainId, skipClo const crossingChainFamily = !isSameChainFamily(currentChainId, targetChain) if (!isWalletConnected || !crossingChainFamily) { - return false + return CrossChainFamilySwitchState.NOT_CROSSING_CHAIN } const confirmed = await triggerConfirmation({ @@ -52,7 +62,7 @@ export function useCrossChainFamilySwitch(): (chainId: SupportedChainId, skipClo }) if (!confirmed) { - return true + return CrossChainFamilySwitchState.NOT_CONFIRMED } try { @@ -61,7 +71,7 @@ export function useCrossChainFamilySwitch(): (chainId: SupportedChainId, skipClo await disconnectWallet() } catch (error) { console.error('Failed to disconnect wallet while switching network type', error) - return true + return CrossChainFamilySwitchState.DISCONNECT_FAILED } setChainIdToUrl(targetChain) @@ -71,7 +81,7 @@ export function useCrossChainFamilySwitch(): (chainId: SupportedChainId, skipClo closeModal() } - return true + return CrossChainFamilySwitchState.FINISHED }, [ account, diff --git a/apps/cowswap-frontend/src/common/hooks/useOnSelectNetwork.tsx b/apps/cowswap-frontend/src/common/hooks/useOnSelectNetwork.tsx index f459276d54f..f8fb5531a2a 100644 --- a/apps/cowswap-frontend/src/common/hooks/useOnSelectNetwork.tsx +++ b/apps/cowswap-frontend/src/common/hooks/useOnSelectNetwork.tsx @@ -12,7 +12,7 @@ import { Trans } from '@lingui/react/macro' import { useCloseModal } from 'legacy/state/application/hooks' import { ApplicationModal } from 'legacy/state/application/reducer' -import { useCrossChainFamilySwitch } from './useCrossChainFamilySwitch' +import { CrossChainFamilySwitchState, useCrossChainFamilySwitch } from './useCrossChainFamilySwitch' import { useLegacySetChainIdToUrl } from './useLegacySetChainIdToUrl' export function useOnSelectNetwork(): (chainId: SupportedChainId, skipClose?: boolean) => Promise { @@ -26,7 +26,9 @@ export function useOnSelectNetwork(): (chainId: SupportedChainId, skipClose?: bo async (targetChain: SupportedChainId, skipClose?: boolean) => { // Switching between EVM and non-EVM networks requires a different wallet and is handled // separately (confirm + disconnect + reconnect) instead of a regular network switch. - if (await handleCrossChainFamilySwitch(targetChain, skipClose)) { + if ( + (await handleCrossChainFamilySwitch(targetChain, skipClose)) !== CrossChainFamilySwitchState.NOT_CROSSING_CHAIN + ) { return } diff --git a/apps/cowswap-frontend/src/modules/tokensList/hooks/useOpenTokenSelectWidget.ts b/apps/cowswap-frontend/src/modules/tokensList/hooks/useOpenTokenSelectWidget.ts index b926c7db087..4f9ef7b97f9 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/hooks/useOpenTokenSelectWidget.ts +++ b/apps/cowswap-frontend/src/modules/tokensList/hooks/useOpenTokenSelectWidget.ts @@ -2,6 +2,7 @@ import { useCallback } from 'react' import { LpToken, TokenWithLogo } from '@cowprotocol/common-const' import { useIsBridgingEnabled } from '@cowprotocol/common-hooks' +import { isEvmChain } from '@cowprotocol/cow-sdk' import { Currency } from '@cowprotocol/currency' import { Nullish } from 'types' @@ -11,6 +12,8 @@ import { Field } from 'legacy/state/types' import { TradeType, useTradeTypeInfo } from 'modules/trade' import { useTradeTypeInfoFromUrl } from 'modules/trade/hooks/useTradeTypeInfoFromUrl' +import { CrossChainFamilySwitchState, useCrossChainFamilySwitch } from 'common/hooks/useCrossChainFamilySwitch' + import { useCloseTokenSelectWidget } from './useCloseTokenSelectWidget' import { useUpdateSelectTokenWidgetState } from './useUpdateSelectTokenWidgetState' @@ -25,6 +28,7 @@ export function useOpenTokenSelectWidget(): ( const isBridgingEnabled = useIsBridgingEnabled() const tradeTypeInfoFromState = useTradeTypeInfo() const tradeTypeInfoFromUrl = useTradeTypeInfoFromUrl() + const crossChainFamilySwitch = useCrossChainFamilySwitch() const tradeTypeInfo = tradeTypeInfoFromState ?? tradeTypeInfoFromUrl const tradeType = tradeTypeInfo?.tradeType // Advanced trades lock the target chain so price guarantees stay valid while the widget is open. @@ -46,13 +50,39 @@ export function useOpenTokenSelectWidget(): ( forceOpen: false, selectedTargetChainId: nextSelectedTargetChainId, tradeType, - onSelectToken: (currency) => { + onSelectToken: async (currency) => { + if (selectedToken) { + const isSelectedTokenEvm = isEvmChain(selectedToken.chainId) + const isNewTokenEvm = isEvmChain(currency.chainId) + const shouldConfirmNetworkSwitch = + (isSelectedTokenEvm && !isNewTokenEvm) || (!isSelectedTokenEvm && isNewTokenEvm) + + const crossChainSwitched = + (await crossChainFamilySwitch(currency.chainId)) === CrossChainFamilySwitchState.FINISHED + + /** + * In case of switching from EVM to non-EVM (and vice versa) + * Ask a confirmation from the user + * Because it requires connecting to another wallet + */ + if (shouldConfirmNetworkSwitch && !crossChainSwitched) { + return + } + } + // Keep selector UX consistent with #6251: always close after a selection, even if a chain switch follows. closeTokenSelectWidget({ overrideForceLock: true }) onSelectToken(currency) }, }) }, - [closeTokenSelectWidget, updateSelectTokenWidget, isBridgingEnabled, shouldLockTargetChain, tradeType], + [ + closeTokenSelectWidget, + updateSelectTokenWidget, + crossChainFamilySwitch, + isBridgingEnabled, + shouldLockTargetChain, + tradeType, + ], ) } diff --git a/apps/cowswap-frontend/src/theme/ThemedGlobalStyle.tsx b/apps/cowswap-frontend/src/theme/ThemedGlobalStyle.tsx index 1c124084b41..b6600dcb462 100644 --- a/apps/cowswap-frontend/src/theme/ThemedGlobalStyle.tsx +++ b/apps/cowswap-frontend/src/theme/ThemedGlobalStyle.tsx @@ -75,7 +75,7 @@ export const ThemedGlobalStyle = createGlobalStyle` // TODO: Can be removed once we control this component [data-reach-dialog-overlay] { - z-index: 10 !important; + z-index: 1000 !important; ${Media.upToMedium()} { top: 0 !important; From 1db89fb2af6ad0115570b16b1b37423d67985d74 Mon Sep 17 00:00:00 2001 From: shoom3301 Date: Tue, 14 Jul 2026 15:08:14 +0200 Subject: [PATCH 10/42] chore: update text --- .../src/common/hooks/useCrossChainFamilySwitch.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx b/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx index 918105a1dad..26fbe665063 100644 --- a/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx +++ b/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx @@ -55,7 +55,7 @@ export function useCrossChainFamilySwitch(): ( const confirmed = await triggerConfirmation({ confirmWord: t`confirm`, title: t`Switching network type`, - description: t`You're switching between EVM and non-EVM networks. This requires connecting a different wallet. Your current wallet will be disconnected. Are you sure?`, + description: t`You're switching between EVM and non-EVM networks. This requires connecting a different wallet. Your current wallet will be disconnected.`, action: t`switch network type`, callToAction: t`Confirm`, skipInput: true, From b1d4de1f299cff8e5f6bc27b51472b31d8c06fdd Mon Sep 17 00:00:00 2001 From: shoom3301 Date: Tue, 14 Jul 2026 18:12:19 +0200 Subject: [PATCH 11/42] chore: rename text --- .../src/common/hooks/useCrossChainFamilySwitch.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx b/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx index 26fbe665063..f856bbe64b6 100644 --- a/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx +++ b/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx @@ -57,7 +57,7 @@ export function useCrossChainFamilySwitch(): ( title: t`Switching network type`, description: t`You're switching between EVM and non-EVM networks. This requires connecting a different wallet. Your current wallet will be disconnected.`, action: t`switch network type`, - callToAction: t`Confirm`, + callToAction: t`Connect wallet`, skipInput: true, }) From 2306273c1516cee6f0ec6809e1bf01755ea89c02 Mon Sep 17 00:00:00 2001 From: shoom3301 Date: Tue, 14 Jul 2026 18:14:53 +0200 Subject: [PATCH 12/42] chore: ff IS_SOLANA_ENABLED in URL --- libs/common-const/src/featureFlags.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/libs/common-const/src/featureFlags.ts b/libs/common-const/src/featureFlags.ts index 06240a431b0..30bdf0a7456 100644 --- a/libs/common-const/src/featureFlags.ts +++ b/libs/common-const/src/featureFlags.ts @@ -2,4 +2,10 @@ export type FeatureFlags = Record export type FeatureFlagValue = boolean | number | undefined -export const IS_SOLANA_ENABLED = typeof localStorage !== 'undefined' && !!localStorage.getItem('IS_SOLANA_ENABLED') +export let IS_SOLANA_ENABLED = typeof localStorage !== 'undefined' && !!localStorage.getItem('IS_SOLANA_ENABLED') + +if (typeof location !== 'undefined') { + const value = new URLSearchParams(location.hash.split('?')[1]).get('IS_SOLANA_ENABLED') + + IS_SOLANA_ENABLED = value === 'true' +} From 0a6d43d7a404acb42fbc45c96703bb6ae5c2a310 Mon Sep 17 00:00:00 2001 From: shoom3301 Date: Tue, 14 Jul 2026 18:16:21 +0200 Subject: [PATCH 13/42] chore: ff IS_SOLANA_ENABLED in URL --- libs/common-const/src/featureFlags.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/libs/common-const/src/featureFlags.ts b/libs/common-const/src/featureFlags.ts index 30bdf0a7456..42f4ca09108 100644 --- a/libs/common-const/src/featureFlags.ts +++ b/libs/common-const/src/featureFlags.ts @@ -2,10 +2,14 @@ export type FeatureFlags = Record export type FeatureFlagValue = boolean | number | undefined -export let IS_SOLANA_ENABLED = typeof localStorage !== 'undefined' && !!localStorage.getItem('IS_SOLANA_ENABLED') - if (typeof location !== 'undefined') { const value = new URLSearchParams(location.hash.split('?')[1]).get('IS_SOLANA_ENABLED') - IS_SOLANA_ENABLED = value === 'true' + if (value === 'true') { + localStorage.setItem('IS_SOLANA_ENABLED', '1') + } else { + localStorage.removeItem('IS_SOLANA_ENABLED') + } } + +export const IS_SOLANA_ENABLED = typeof localStorage !== 'undefined' && !!localStorage.getItem('IS_SOLANA_ENABLED') From 855a75117ad639335976716f507dcbb819383522 Mon Sep 17 00:00:00 2001 From: "cowswap-release-sync[bot]" <274575433+cowswap-release-sync[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:18:06 +0000 Subject: [PATCH 14/42] chore(i18n): extract i18n strings [automatic] --- apps/cowswap-frontend/src/locales/en-US.po | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/cowswap-frontend/src/locales/en-US.po b/apps/cowswap-frontend/src/locales/en-US.po index 09b85f416be..6cda94d3c32 100644 --- a/apps/cowswap-frontend/src/locales/en-US.po +++ b/apps/cowswap-frontend/src/locales/en-US.po @@ -940,8 +940,8 @@ msgstr "switch network type" #~ msgstr "You don't have any {orderTabState} orders at the moment." #: apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx -msgid "You're switching between EVM and non-EVM networks. This requires connecting a different wallet. Your current wallet will be disconnected. Are you sure?" -msgstr "You're switching between EVM and non-EVM networks. This requires connecting a different wallet. Your current wallet will be disconnected. Are you sure?" +#~ msgid "You're switching between EVM and non-EVM networks. This requires connecting a different wallet. Your current wallet will be disconnected. Are you sure?" +#~ msgstr "You're switching between EVM and non-EVM networks. This requires connecting a different wallet. Your current wallet will be disconnected. Are you sure?" #: apps/cowswap-frontend/src/modules/limitOrders/pure/Settings/LimitOrdersSettings.pure.tsx msgid "Limit Price Position" @@ -1049,6 +1049,10 @@ msgstr "Connect your wallet to activate it and start earning rewards when you tr #~ msgid "Share your referral code and earn <0>{partnerRewardAmount} for every <1>{triggerVolumeLabel} in eligible volume within {affiliateTimeCapDays} days.<2/><3/>" #~ msgstr "Share your referral code and earn <0>{partnerRewardAmount} for every <1>{triggerVolumeLabel} in eligible volume within {affiliateTimeCapDays} days.<2/><3/>" +#: apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx +msgid "You're switching between EVM and non-EVM networks. This requires connecting a different wallet. Your current wallet will be disconnected." +msgstr "You're switching between EVM and non-EVM networks. This requires connecting a different wallet. Your current wallet will be disconnected." + #: apps/cowswap-frontend/src/modules/application/containers/App/menuConsts.tsx msgid "CoW Runner" msgstr "CoW Runner" @@ -1265,7 +1269,6 @@ msgid "pools available to get yield on your assets!" msgstr "pools available to get yield on your assets!" #: apps/cowswap-frontend/src/common/containers/ConfirmationModal/index.tsx -#: apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx #: apps/cowswap-frontend/src/common/pure/ConfirmationModal/ConfirmationModal.tsx #: apps/cowswap-frontend/src/modules/erc20Approve/containers/ChangeApproveAmountModal/ChangeApproveAmountModalPure.tsx #: apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersConfirmModal/index.tsx @@ -4338,6 +4341,7 @@ msgstr "Coinbase Wallet" msgid "Only the {limit} most recent orders were searched." msgstr "Only the {limit} most recent orders were searched." +#: apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx #: apps/cowswap-frontend/src/modules/affiliate/containers/AffiliatePartnerOnboard.tsx #: apps/cowswap-frontend/src/modules/affiliate/containers/AffiliateTraderOnboard.tsx #: apps/cowswap-frontend/src/modules/hooksStore/dapps/PermitHookApp/index.tsx From 079e0f081adcc5da438c51e1b8de75c7eae25d44 Mon Sep 17 00:00:00 2001 From: shoom3301 Date: Wed, 15 Jul 2026 12:17:22 +0200 Subject: [PATCH 15/42] chore: update confirm text --- .../containers/ConfirmationModal/index.tsx | 3 ++- .../src/common/hooks/useConfirmationRequest.ts | 5 +++-- .../common/hooks/useCrossChainFamilySwitch.tsx | 13 +++++++++---- .../pure/ConfirmationModal/ConfirmationModal.tsx | 16 ++++++++++++---- .../pure/ConfirmedButton/ConfirmedButton.tsx | 8 +++++--- 5 files changed, 31 insertions(+), 14 deletions(-) diff --git a/apps/cowswap-frontend/src/common/containers/ConfirmationModal/index.tsx b/apps/cowswap-frontend/src/common/containers/ConfirmationModal/index.tsx index edfa93ab4a9..519a0917ede 100644 --- a/apps/cowswap-frontend/src/common/containers/ConfirmationModal/index.tsx +++ b/apps/cowswap-frontend/src/common/containers/ConfirmationModal/index.tsx @@ -8,7 +8,7 @@ import { ConfirmationModal as Pure, ConfirmationModalProps } from 'common/pure/C // TODO: Add proper return type annotation // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function ConfirmationModal({ isOpen, onDismiss }: Pick) { - const { title, callToAction, description, onEnable, warning, confirmWord, action, skipInput } = + const { title, callToAction, description, onEnable, warning, confirmWord, action, skipInput, bottomContent } = useAtomValue(confirmationModalContextAtom) return ( @@ -23,6 +23,7 @@ export function ConfirmationModal({ isOpen, onDismiss }: Pick ) } diff --git a/apps/cowswap-frontend/src/common/hooks/useConfirmationRequest.ts b/apps/cowswap-frontend/src/common/hooks/useConfirmationRequest.ts index 371979de0e4..1ca61e936e5 100644 --- a/apps/cowswap-frontend/src/common/hooks/useConfirmationRequest.ts +++ b/apps/cowswap-frontend/src/common/hooks/useConfirmationRequest.ts @@ -1,6 +1,6 @@ import { atom, useSetAtom } from 'jotai' import { atomWithReset, useResetAtom } from 'jotai/utils' -import { useCallback } from 'react' +import { ReactNode, useCallback } from 'react' import { Command } from '@cowprotocol/types' @@ -20,6 +20,7 @@ interface ConfirmationModalContext { action: string onEnable: Command skipInput?: boolean + bottomContent?: ReactNode triggerConfirmation: ({ title, description, @@ -30,7 +31,7 @@ interface ConfirmationModalContext { } type TriggerConfirmationParams = Pick< ConfirmationModalProps, - 'title' | 'description' | 'callToAction' | 'warning' | 'confirmWord' | 'action' | 'skipInput' + 'title' | 'description' | 'callToAction' | 'warning' | 'confirmWord' | 'action' | 'skipInput' | 'bottomContent' > export const DEFAULT_CONFIRMATION_MODAL_CONTEXT: ConfirmationModalContext = { diff --git a/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx b/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx index f856bbe64b6..c846ef81968 100644 --- a/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx +++ b/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx @@ -1,5 +1,6 @@ import { useCallback } from 'react' +import { CHAIN_INFO } from '@cowprotocol/common-const' import { isSameChainFamily } from '@cowprotocol/common-utils' import { SupportedChainId } from '@cowprotocol/cow-sdk' import { useDisconnectWallet, useOpenWalletConnectionModal, useWalletInfo } from '@cowprotocol/wallet' @@ -44,21 +45,25 @@ export function useCrossChainFamilySwitch(): ( const triggerConfirmation = useConfirmationRequest({}) return useCallback( - async (targetChain: SupportedChainId, skipClose?: boolean) => { + async (targetChainId: SupportedChainId, skipClose?: boolean) => { const isWalletConnected = !!account - const crossingChainFamily = !isSameChainFamily(currentChainId, targetChain) + const crossingChainFamily = !isSameChainFamily(currentChainId, targetChainId) if (!isWalletConnected || !crossingChainFamily) { return CrossChainFamilySwitchState.NOT_CROSSING_CHAIN } + const sourceChainLabel = CHAIN_INFO[currentChainId].label + const targetChainLabel = CHAIN_INFO[targetChainId].label + const confirmed = await triggerConfirmation({ confirmWord: t`confirm`, title: t`Switching network type`, - description: t`You're switching between EVM and non-EVM networks. This requires connecting a different wallet. Your current wallet will be disconnected.`, + description: t`You're switching from ${sourceChainLabel} to ${targetChainLabel}. This requires connecting a different wallet. Your current wallet will be disconnected.`, action: t`switch network type`, callToAction: t`Connect wallet`, skipInput: true, + bottomContent: null, }) if (!confirmed) { @@ -74,7 +79,7 @@ export function useCrossChainFamilySwitch(): ( return CrossChainFamilySwitchState.DISCONNECT_FAILED } - setChainIdToUrl(targetChain) + setChainIdToUrl(targetChainId) openWalletConnectionModal() if (!skipClose) { diff --git a/apps/cowswap-frontend/src/common/pure/ConfirmationModal/ConfirmationModal.tsx b/apps/cowswap-frontend/src/common/pure/ConfirmationModal/ConfirmationModal.tsx index d486a86e9d2..b0aea991000 100644 --- a/apps/cowswap-frontend/src/common/pure/ConfirmationModal/ConfirmationModal.tsx +++ b/apps/cowswap-frontend/src/common/pure/ConfirmationModal/ConfirmationModal.tsx @@ -1,3 +1,5 @@ +import { ReactNode } from 'react' + import { Command } from '@cowprotocol/types' import { Trans } from '@lingui/react/macro' @@ -34,11 +36,10 @@ export interface ConfirmationModalProps { onEnable: Command confirmWord: string action: string + bottomContent?: ReactNode skipInput?: boolean } -// TODO: Add proper return type annotation -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function ConfirmationModal({ isOpen, title, @@ -49,8 +50,9 @@ export function ConfirmationModal({ onEnable, action, confirmWord, + bottomContent, skipInput = false, -}: ConfirmationModalProps) { +}: ConfirmationModalProps): ReactNode { const shouldShowDescription = !!description const shouldShowWarning = !!warning @@ -64,7 +66,13 @@ export function ConfirmationModal({ {warning} )} - + {callToAction ? callToAction : Confirm} diff --git a/apps/cowswap-frontend/src/common/pure/ConfirmedButton/ConfirmedButton.tsx b/apps/cowswap-frontend/src/common/pure/ConfirmedButton/ConfirmedButton.tsx index 979fbf6a661..91d7d289cc7 100644 --- a/apps/cowswap-frontend/src/common/pure/ConfirmedButton/ConfirmedButton.tsx +++ b/apps/cowswap-frontend/src/common/pure/ConfirmedButton/ConfirmedButton.tsx @@ -35,18 +35,18 @@ interface ConfirmedButtonProps { action: string confirmWord: string skipInput?: boolean + bottomContent?: ReactNode } -// TODO: Add proper return type annotation -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function ConfirmedButton({ className, onConfirm, children, action, confirmWord, + bottomContent, skipInput = false, -}: ConfirmedButtonProps) { +}: ConfirmedButtonProps): ReactNode { const [inputValue, setInputValue] = useState('') const onInputChange: ChangeEventHandler = (event) => setInputValue(event.target.value ?? '') const shouldShowInput = !skipInput @@ -70,6 +70,8 @@ export function ConfirmedButton({ Please type the word "{confirmWord}" to {action}. + ) : typeof bottomContent !== undefined ? ( + bottomContent ) : ( Please click confirm to {action}. From 0cdca9ab563eb5a8b511b4cf8e97d762665794a6 Mon Sep 17 00:00:00 2001 From: shoom3301 Date: Wed, 15 Jul 2026 12:22:23 +0200 Subject: [PATCH 16/42] fix: disable bridge from Solana --- .../src/modules/tokensList/hooks/useChainsToSelect.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/cowswap-frontend/src/modules/tokensList/hooks/useChainsToSelect.ts b/apps/cowswap-frontend/src/modules/tokensList/hooks/useChainsToSelect.ts index 13fb4c8ce3a..2b56f54361e 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/hooks/useChainsToSelect.ts +++ b/apps/cowswap-frontend/src/modules/tokensList/hooks/useChainsToSelect.ts @@ -25,6 +25,8 @@ import { sortChainsByDisplayOrder } from '../utils/sortChainsByDisplayOrder' // Re-export for tests and external usage export { createInputChainsState, createOutputChainsState } from '../utils/chainsState' +const DISABLED_SOURCE_CHAINS = new Set([SupportedChainId.SOLANA]) + /** * Returns an array of chains to select in the token selector widget. * The array depends on sell/buy token selection. @@ -67,6 +69,7 @@ export function useChainsToSelect(): ChainsToSelectState | undefined { defaultChainId: selectedTargetChainId, chains: shouldHideNetworkSelector ? [] : sortChainsByDisplayOrder(supportedChains), isLoading: false, + disabledChainIds: DISABLED_SOURCE_CHAINS, } } From e0fd749a96312e82cd7cff36eb97480e9c8cda01 Mon Sep 17 00:00:00 2001 From: shoom3301 Date: Wed, 15 Jul 2026 13:26:53 +0200 Subject: [PATCH 17/42] fix: update text --- .../src/common/hooks/useCrossChainFamilySwitch.tsx | 13 ++++++++++++- .../pure/ConfirmationModal/ConfirmationModal.tsx | 2 +- libs/common-const/src/featureFlags.ts | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx b/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx index c846ef81968..a4ee01b9f7e 100644 --- a/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx +++ b/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx @@ -6,6 +6,7 @@ import { SupportedChainId } from '@cowprotocol/cow-sdk' import { useDisconnectWallet, useOpenWalletConnectionModal, useWalletInfo } from '@cowprotocol/wallet' import { t } from '@lingui/core/macro' +import { Trans } from '@lingui/react/macro' import { useCloseModal } from 'legacy/state/application/hooks' import { ApplicationModal } from 'legacy/state/application/reducer' @@ -59,7 +60,17 @@ export function useCrossChainFamilySwitch(): ( const confirmed = await triggerConfirmation({ confirmWord: t`confirm`, title: t`Switching network type`, - description: t`You're switching from ${sourceChainLabel} to ${targetChainLabel}. This requires connecting a different wallet. Your current wallet will be disconnected.`, + description: ( + + + You're switching from {sourceChainLabel} to {targetChainLabel}. + +
+ This requires connecting a different wallet. +
+ Your current wallet will be disconnected. +
+ ), action: t`switch network type`, callToAction: t`Connect wallet`, skipInput: true, diff --git a/apps/cowswap-frontend/src/common/pure/ConfirmationModal/ConfirmationModal.tsx b/apps/cowswap-frontend/src/common/pure/ConfirmationModal/ConfirmationModal.tsx index b0aea991000..f6f2b6b9de5 100644 --- a/apps/cowswap-frontend/src/common/pure/ConfirmationModal/ConfirmationModal.tsx +++ b/apps/cowswap-frontend/src/common/pure/ConfirmationModal/ConfirmationModal.tsx @@ -29,7 +29,7 @@ const Warning = styled.strong` export interface ConfirmationModalProps { isOpen: boolean title: string - description?: string + description?: ReactNode warning?: string callToAction?: string onDismiss: Command diff --git a/libs/common-const/src/featureFlags.ts b/libs/common-const/src/featureFlags.ts index 42f4ca09108..f0ebcfca475 100644 --- a/libs/common-const/src/featureFlags.ts +++ b/libs/common-const/src/featureFlags.ts @@ -7,7 +7,7 @@ if (typeof location !== 'undefined') { if (value === 'true') { localStorage.setItem('IS_SOLANA_ENABLED', '1') - } else { + } else if (value === 'false') { localStorage.removeItem('IS_SOLANA_ENABLED') } } From 1fce32b05c8f02ff6d6def3b831820a21b1fc545 Mon Sep 17 00:00:00 2001 From: "cowswap-release-sync[bot]" <274575433+cowswap-release-sync[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 11:28:49 +0000 Subject: [PATCH 18/42] chore(i18n): extract i18n strings [automatic] --- apps/cowswap-frontend/src/locales/en-US.po | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/apps/cowswap-frontend/src/locales/en-US.po b/apps/cowswap-frontend/src/locales/en-US.po index 6cda94d3c32..b198f1077b3 100644 --- a/apps/cowswap-frontend/src/locales/en-US.po +++ b/apps/cowswap-frontend/src/locales/en-US.po @@ -1050,8 +1050,8 @@ msgstr "Connect your wallet to activate it and start earning rewards when you tr #~ msgstr "Share your referral code and earn <0>{partnerRewardAmount} for every <1>{triggerVolumeLabel} in eligible volume within {affiliateTimeCapDays} days.<2/><3/>" #: apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx -msgid "You're switching between EVM and non-EVM networks. This requires connecting a different wallet. Your current wallet will be disconnected." -msgstr "You're switching between EVM and non-EVM networks. This requires connecting a different wallet. Your current wallet will be disconnected." +#~ msgid "You're switching between EVM and non-EVM networks. This requires connecting a different wallet. Your current wallet will be disconnected." +#~ msgstr "You're switching between EVM and non-EVM networks. This requires connecting a different wallet. Your current wallet will be disconnected." #: apps/cowswap-frontend/src/modules/application/containers/App/menuConsts.tsx msgid "CoW Runner" @@ -7771,6 +7771,10 @@ msgstr "<0>CoW Swap requires offline signatures, which is currently not supporte msgid "Order expired" msgstr "Order expired" +#: apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx +msgid "Your current wallet will be disconnected." +msgstr "Your current wallet will be disconnected." + #: apps/cowswap-frontend/src/modules/hooksStore/containers/TenderlySimulate/index.tsx msgid "Simulate" msgstr "Simulate" @@ -7835,6 +7839,10 @@ msgstr "Leverage advanced strategies for optimal growth" msgid "View List" msgstr "View List" +#: apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx +msgid "This requires connecting a different wallet." +msgstr "This requires connecting a different wallet." + #: libs/hook-dapp-lib/src/hookDappsRegistry.ts #~ msgid "Add liquidity to any of the CoW AMM pools" #~ msgstr "Add liquidity to any of the CoW AMM pools" @@ -7925,6 +7933,10 @@ msgstr "Distance to market" msgid "Did you know?" msgstr "Did you know?" +#: apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx +msgid "You're switching from {sourceChainLabel} to {targetChainLabel}." +msgstr "You're switching from {sourceChainLabel} to {targetChainLabel}." + #: apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/SmallPartTimeWarning.tsx msgid "Insufficient time between parts" msgstr "Insufficient time between parts" From 72254aa3a037b4e16ad72cf2e3e894265681fb4b Mon Sep 17 00:00:00 2001 From: shoom3301 Date: Wed, 15 Jul 2026 13:50:33 +0200 Subject: [PATCH 19/42] fix: fix network switching --- .../hooks/useCrossChainFamilySwitch.test.tsx | 4 ++-- .../common/hooks/useCrossChainFamilySwitch.tsx | 15 ++++++++++----- .../src/common/hooks/useOnSelectNetwork.tsx | 7 ++++++- .../tokensList/hooks/useOpenTokenSelectWidget.ts | 6 ++++-- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.test.tsx b/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.test.tsx index 0f1c8706026..b30cfd25e1b 100644 --- a/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.test.tsx +++ b/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.test.tsx @@ -66,7 +66,7 @@ describe('useCrossChainFamilySwitch', () => { expect(disconnectWallet).not.toHaveBeenCalled() }) - it('returns NOT_CROSSING_CHAIN for a cross-family change when no wallet is connected', async () => { + it('returns WALLET_NOT_CONNECTED for a cross-family change when no wallet is connected', async () => { setWallet(SupportedChainId.MAINNET, undefined) const { result } = renderHook(() => useCrossChainFamilySwitch()) @@ -75,7 +75,7 @@ describe('useCrossChainFamilySwitch', () => { handled = await result.current(SupportedChainId.SOLANA) }) - expect(handled).toBe(CrossChainFamilySwitchState.NOT_CROSSING_CHAIN) + expect(handled).toBe(CrossChainFamilySwitchState.WALLET_NOT_CONNECTED) expect(triggerConfirmation).not.toHaveBeenCalled() expect(disconnectWallet).not.toHaveBeenCalled() }) diff --git a/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx b/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx index a4ee01b9f7e..5c410bee15d 100644 --- a/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx +++ b/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx @@ -15,10 +15,11 @@ import { useConfirmationRequest } from './useConfirmationRequest' import { useLegacySetChainIdToUrl } from './useLegacySetChainIdToUrl' export enum CrossChainFamilySwitchState { - NOT_CROSSING_CHAIN, - NOT_CONFIRMED, - DISCONNECT_FAILED, - FINISHED, + WALLET_NOT_CONNECTED = 'WALLET_NOT_CONNECTED', + NOT_CROSSING_CHAIN = 'NOT_CROSSING_CHAIN', + NOT_CONFIRMED = 'NOT_CONFIRMED', + DISCONNECT_FAILED = 'DISCONNECT_FAILED', + FINISHED = 'FINISHED', } /** @@ -50,10 +51,14 @@ export function useCrossChainFamilySwitch(): ( const isWalletConnected = !!account const crossingChainFamily = !isSameChainFamily(currentChainId, targetChainId) - if (!isWalletConnected || !crossingChainFamily) { + if (!crossingChainFamily) { return CrossChainFamilySwitchState.NOT_CROSSING_CHAIN } + if (!isWalletConnected) { + return CrossChainFamilySwitchState.WALLET_NOT_CONNECTED + } + const sourceChainLabel = CHAIN_INFO[currentChainId].label const targetChainLabel = CHAIN_INFO[targetChainId].label diff --git a/apps/cowswap-frontend/src/common/hooks/useOnSelectNetwork.tsx b/apps/cowswap-frontend/src/common/hooks/useOnSelectNetwork.tsx index f8fb5531a2a..eb18123b34e 100644 --- a/apps/cowswap-frontend/src/common/hooks/useOnSelectNetwork.tsx +++ b/apps/cowswap-frontend/src/common/hooks/useOnSelectNetwork.tsx @@ -26,8 +26,13 @@ export function useOnSelectNetwork(): (chainId: SupportedChainId, skipClose?: bo async (targetChain: SupportedChainId, skipClose?: boolean) => { // Switching between EVM and non-EVM networks requires a different wallet and is handled // separately (confirm + disconnect + reconnect) instead of a regular network switch. + const switchChainState = await handleCrossChainFamilySwitch(targetChain, skipClose) + if ( - (await handleCrossChainFamilySwitch(targetChain, skipClose)) !== CrossChainFamilySwitchState.NOT_CROSSING_CHAIN + !( + switchChainState === CrossChainFamilySwitchState.FINISHED || + switchChainState === CrossChainFamilySwitchState.WALLET_NOT_CONNECTED + ) ) { return } diff --git a/apps/cowswap-frontend/src/modules/tokensList/hooks/useOpenTokenSelectWidget.ts b/apps/cowswap-frontend/src/modules/tokensList/hooks/useOpenTokenSelectWidget.ts index 4f9ef7b97f9..62fa3b6824e 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/hooks/useOpenTokenSelectWidget.ts +++ b/apps/cowswap-frontend/src/modules/tokensList/hooks/useOpenTokenSelectWidget.ts @@ -51,14 +51,16 @@ export function useOpenTokenSelectWidget(): ( selectedTargetChainId: nextSelectedTargetChainId, tradeType, onSelectToken: async (currency) => { - if (selectedToken) { + if (selectedToken && !isOutputField) { const isSelectedTokenEvm = isEvmChain(selectedToken.chainId) const isNewTokenEvm = isEvmChain(currency.chainId) const shouldConfirmNetworkSwitch = (isSelectedTokenEvm && !isNewTokenEvm) || (!isSelectedTokenEvm && isNewTokenEvm) + const chainSwitchState = await crossChainFamilySwitch(currency.chainId) const crossChainSwitched = - (await crossChainFamilySwitch(currency.chainId)) === CrossChainFamilySwitchState.FINISHED + chainSwitchState !== CrossChainFamilySwitchState.NOT_CROSSING_CHAIN && + chainSwitchState !== CrossChainFamilySwitchState.NOT_CONFIRMED /** * In case of switching from EVM to non-EVM (and vice versa) From f17495fd9c3a7f1c8a21f81872ba72517c654d6a Mon Sep 17 00:00:00 2001 From: shoom3301 Date: Wed, 15 Jul 2026 14:51:06 +0200 Subject: [PATCH 20/42] fix: fix network switching --- .../src/common/hooks/useConfirmationRequest.ts | 2 +- .../src/common/hooks/useOnSelectNetwork.test.tsx | 4 ++-- .../src/common/hooks/useOnSelectNetwork.tsx | 7 +------ 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/apps/cowswap-frontend/src/common/hooks/useConfirmationRequest.ts b/apps/cowswap-frontend/src/common/hooks/useConfirmationRequest.ts index 1ca61e936e5..b3c153c5d1a 100644 --- a/apps/cowswap-frontend/src/common/hooks/useConfirmationRequest.ts +++ b/apps/cowswap-frontend/src/common/hooks/useConfirmationRequest.ts @@ -14,7 +14,7 @@ interface ConfirmationModalContext { activePromise?: Promise title: string callToAction: string - description?: string + description?: ReactNode warning?: string confirmWord: string action: string diff --git a/apps/cowswap-frontend/src/common/hooks/useOnSelectNetwork.test.tsx b/apps/cowswap-frontend/src/common/hooks/useOnSelectNetwork.test.tsx index e2a1563135e..3a13da503e2 100644 --- a/apps/cowswap-frontend/src/common/hooks/useOnSelectNetwork.test.tsx +++ b/apps/cowswap-frontend/src/common/hooks/useOnSelectNetwork.test.tsx @@ -5,7 +5,7 @@ import { act, renderHook } from '@testing-library/react' import { useCloseModal } from 'legacy/state/application/hooks' -import { useCrossChainFamilySwitch } from './useCrossChainFamilySwitch' +import { CrossChainFamilySwitchState, useCrossChainFamilySwitch } from './useCrossChainFamilySwitch' import { useLegacySetChainIdToUrl } from './useLegacySetChainIdToUrl' import { useOnSelectNetwork } from './useOnSelectNetwork' @@ -55,7 +55,7 @@ describe('useOnSelectNetwork', () => { }) it('delegates to the cross-family flow and short-circuits when it handles the switch', async () => { - handleCrossChainFamilySwitch.mockResolvedValue(true) + handleCrossChainFamilySwitch.mockResolvedValue(CrossChainFamilySwitchState.NOT_CONFIRMED) const { result } = renderHook(() => useOnSelectNetwork()) await act(async () => { diff --git a/apps/cowswap-frontend/src/common/hooks/useOnSelectNetwork.tsx b/apps/cowswap-frontend/src/common/hooks/useOnSelectNetwork.tsx index eb18123b34e..f5af5221e14 100644 --- a/apps/cowswap-frontend/src/common/hooks/useOnSelectNetwork.tsx +++ b/apps/cowswap-frontend/src/common/hooks/useOnSelectNetwork.tsx @@ -28,12 +28,7 @@ export function useOnSelectNetwork(): (chainId: SupportedChainId, skipClose?: bo // separately (confirm + disconnect + reconnect) instead of a regular network switch. const switchChainState = await handleCrossChainFamilySwitch(targetChain, skipClose) - if ( - !( - switchChainState === CrossChainFamilySwitchState.FINISHED || - switchChainState === CrossChainFamilySwitchState.WALLET_NOT_CONNECTED - ) - ) { + if (switchChainState === CrossChainFamilySwitchState.NOT_CONFIRMED) { return } From a3e8e29af812aa92b00876753551ca9e6a42f4f8 Mon Sep 17 00:00:00 2001 From: shoom3301 Date: Wed, 15 Jul 2026 15:46:42 +0200 Subject: [PATCH 21/42] feat(solana): support token2022 --- ...ePersistSolanaBalancesViaWebCalls.test.tsx | 55 +++++-- .../usePersistSolanaBalancesViaWebCalls.ts | 33 +++- .../services/fetchSolanaTokenBalances.test.ts | 146 ++++++++++++++++++ .../src/services/fetchSolanaTokenBalances.ts | 62 +++++--- libs/common-const/src/types.test.ts | 35 +++++ libs/common-const/src/types.ts | 25 ++- 6 files changed, 319 insertions(+), 37 deletions(-) create mode 100644 libs/balances-and-allowances/src/services/fetchSolanaTokenBalances.test.ts create mode 100644 libs/common-const/src/types.test.ts diff --git a/libs/balances-and-allowances/src/hooks/usePersistSolanaBalancesViaWebCalls.test.tsx b/libs/balances-and-allowances/src/hooks/usePersistSolanaBalancesViaWebCalls.test.tsx index a69b5e9c731..f708cf993aa 100644 --- a/libs/balances-and-allowances/src/hooks/usePersistSolanaBalancesViaWebCalls.test.tsx +++ b/libs/balances-and-allowances/src/hooks/usePersistSolanaBalancesViaWebCalls.test.tsx @@ -4,6 +4,7 @@ import React, { ReactNode } from 'react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { TOKEN_2022_TAG } from '@cowprotocol/common-const' import { getAddressKey, SupportedChainId, mapSupportedNetworks, solana } from '@cowprotocol/cow-sdk' import { PersistentStateByChain } from '@cowprotocol/types' @@ -21,18 +22,39 @@ const MINT_A = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' const MINT_B = 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB' const NATIVE_MINT = solana.nativeCurrency.address +const CLASSIC_PROGRAM = 'TOKEN_PROGRAM_ID' +const TOKEN_2022_PROGRAM = 'TOKEN_2022_PROGRAM_ID' + +// The ATA key encodes the selected program, so a test can assert which token program a mint was read under. +function ataKey(mint: string, program: string = CLASSIC_PROGRAM): string { + return `ata:${program}:${mint}` +} + let mockConnection: MockConnection | undefined +// getAddressKey(address) -> token metadata, mirroring the token list; its tags drive program selection. +let mockTokensByAddress: Record jest.mock('@reown/appkit-adapter-solana/react', () => ({ useAppKitConnection: () => ({ connection: mockConnection }), })) -// The ATA-derivation math is not what we are testing; make it deterministic and echo the mint back so -// the mocked RPC/`unpackAccount` can look accounts up by their ATA key. We only assert what lands in the atom. +jest.mock('@cowprotocol/tokens', () => ({ + useTokensByAddressMapForChain: () => mockTokensByAddress, +})) + +// The ATA-derivation math is not what we are testing; make it deterministic and echo the mint plus the +// selected program back so the mocked RPC/`unpackAccount` can look accounts up by their ATA key. We only +// assert what lands in the atom and which program each ATA was derived with. jest.mock('@solana/spl-token', () => ({ TOKEN_PROGRAM_ID: 'TOKEN_PROGRAM_ID', + TOKEN_2022_PROGRAM_ID: 'TOKEN_2022_PROGRAM_ID', ASSOCIATED_TOKEN_PROGRAM_ID: 'ASSOCIATED_TOKEN_PROGRAM_ID', - getAssociatedTokenAddressSync: (mint: { toBase58(): string }) => ({ toBase58: () => `ata:${mint.toBase58()}` }), + getAssociatedTokenAddressSync: ( + mint: { toBase58(): string }, + _owner: unknown, + _allowOwnerOffCurve: boolean, + programId: string, + ) => ({ toBase58: () => `ata:${programId}:${mint.toBase58()}` }), unpackAccount: (ata: { toBase58(): string }) => ({ amount: mockAmountByAta[ata.toBase58()] }), })) @@ -109,8 +131,9 @@ function wrapper({ children }: { children: ReactNode }): ReactNode { describe('usePersistSolanaBalancesViaWebCalls', () => { beforeEach(() => { - mockAmountByAta = { [`ata:${MINT_A}`]: 100n, [`ata:${MINT_B}`]: 250n } - mockInfoByAta = { [`ata:${MINT_A}`]: { present: true }, [`ata:${MINT_B}`]: { present: true } } + mockTokensByAddress = {} + mockAmountByAta = { [ataKey(MINT_A)]: 100n, [ataKey(MINT_B)]: 250n } + mockInfoByAta = { [ataKey(MINT_A)]: { present: true }, [ataKey(MINT_B)]: { present: true } } mockConnection = createConnection() }) @@ -133,7 +156,7 @@ describe('usePersistSolanaBalancesViaWebCalls', () => { }) it('treats a missing token account as a zero balance rather than an error', async () => { - delete mockInfoByAta[`ata:${MINT_B}`] + delete mockInfoByAta[ataKey(MINT_B)] const { result } = renderWithBalances(makeParams()) @@ -150,8 +173,8 @@ describe('usePersistSolanaBalancesViaWebCalls', () => { new PublicKey(Uint8Array.from({ length: 32 }, (_, j) => (i + j + 1) % 256)).toBase58(), ) mints.forEach((mint) => { - mockAmountByAta[`ata:${mint}`] = 7n - mockInfoByAta[`ata:${mint}`] = { present: true } + mockAmountByAta[ataKey(mint)] = 7n + mockInfoByAta[ataKey(mint)] = { present: true } }) const { result } = renderWithBalances(makeParams({ tokenAddresses: mints })) @@ -174,6 +197,20 @@ describe('usePersistSolanaBalancesViaWebCalls', () => { const [atas] = mockConnection!.getMultipleAccountsInfo.mock.calls[0] expect(atas).toHaveLength(1) - expect(atas[0].toBase58()).toBe(`ata:${MINT_A}`) + expect(atas[0].toBase58()).toBe(ataKey(MINT_A)) + }) + + it('derives a Token-2022 ATA for mints tagged as Token-2022 in the token list', async () => { + mockTokensByAddress = { [getAddressKey(MINT_A)]: { tags: [TOKEN_2022_TAG] } } + mockAmountByAta = { [ataKey(MINT_A, TOKEN_2022_PROGRAM)]: 999n } + mockInfoByAta = { [ataKey(MINT_A, TOKEN_2022_PROGRAM)]: { present: true } } + + const { result } = renderWithBalances(makeParams({ tokenAddresses: [MINT_A] })) + + await waitFor(() => expect(result.current.hasFirstLoad).toBe(true)) + + const [atas] = mockConnection!.getMultipleAccountsInfo.mock.calls[0] + expect(atas[0].toBase58()).toBe(ataKey(MINT_A, TOKEN_2022_PROGRAM)) + expect(result.current.values[getAddressKey(MINT_A)]).toBe(999n) }) }) diff --git a/libs/balances-and-allowances/src/hooks/usePersistSolanaBalancesViaWebCalls.ts b/libs/balances-and-allowances/src/hooks/usePersistSolanaBalancesViaWebCalls.ts index 0e7f5bd3230..1550c2e88b5 100644 --- a/libs/balances-and-allowances/src/hooks/usePersistSolanaBalancesViaWebCalls.ts +++ b/libs/balances-and-allowances/src/hooks/usePersistSolanaBalancesViaWebCalls.ts @@ -3,8 +3,10 @@ import { useEffect, useMemo } from 'react' import { useQuery } from '@tanstack/react-query' +import { getIsToken2022 } from '@cowprotocol/common-const' import { getIsNativeToken } from '@cowprotocol/common-utils' import { getAddressKey, isSolanaChain } from '@cowprotocol/cow-sdk' +import { useTokensByAddressMapForChain } from '@cowprotocol/tokens' import { useAppKitConnection } from '@reown/appkit-adapter-solana/react' import { Connection } from '@solana/web3.js' @@ -12,7 +14,7 @@ import { Connection } from '@solana/web3.js' import { useIsBlockNumberRelevant } from './useIsBlockNumberRelevant' import { PersistBalancesAndAllowancesParams } from './usePersistBalancesViaWebCalls' -import { fetchSolanaTokenBalances } from '../services/fetchSolanaTokenBalances' +import { fetchSolanaTokenBalances, SolanaTokenMint } from '../services/fetchSolanaTokenBalances' import { balancesAtom, BalancesState, balancesUpdateAtom } from '../state/balancesAtom' interface SolanaQueryConfig { @@ -35,10 +37,11 @@ export function usePersistSolanaBalancesViaWebCalls(params: PersistBalancesAndAl const isSolana = isSolanaChain(chainId) - // Native SOL is handled elsewhere and has no ATA, so deriving one would throw — keep only SPL mints. - const tokenMints = useMemo( - () => tokenAddresses.filter((address) => !getIsNativeToken(chainId, address)), - [tokenAddresses, chainId], + const tokensByAddress = useTokensByAddressMapForChain(chainId) + + const tokenMints = useMemo( + () => buildSolanaTokenMints(tokenAddresses, chainId, tokensByAddress), + [tokenAddresses, chainId, tokensByAddress], ) const { enabled, refetchInterval } = getSolanaQueryConfig(params, isSolana, connection, tokenMints.length) @@ -130,6 +133,26 @@ export function usePersistSolanaBalancesViaWebCalls(params: PersistBalancesAndAl ]) } +// Native SOL is handled elsewhere and has no ATA, so deriving one would throw — keep only SPL mints. +// The list's `isToken2022` flag picks the token program; mints absent from the list default to classic +// SPL, so a wrong ATA simply reads as a zero balance rather than erroring. +function buildSolanaTokenMints( + tokenAddresses: string[], + chainId: PersistBalancesAndAllowancesParams['chainId'], + tokensByAddress: ReturnType, +): SolanaTokenMint[] { + return tokenAddresses + .filter((address) => !getIsNativeToken(chainId, address)) + .map((address) => { + const isToken2022 = getIsToken2022(tokensByAddress[getAddressKey(address)]) + + return { + mint: address, + isToken2022, + } + }) +} + function getSolanaQueryConfig( params: PersistBalancesAndAllowancesParams, isSolana: boolean, diff --git a/libs/balances-and-allowances/src/services/fetchSolanaTokenBalances.test.ts b/libs/balances-and-allowances/src/services/fetchSolanaTokenBalances.test.ts new file mode 100644 index 00000000000..08e6bc71481 --- /dev/null +++ b/libs/balances-and-allowances/src/services/fetchSolanaTokenBalances.test.ts @@ -0,0 +1,146 @@ +/** + * @jest-environment node + */ +import { + ACCOUNT_SIZE, + AccountLayout, + AccountState, + ASSOCIATED_TOKEN_PROGRAM_ID, + getAssociatedTokenAddressSync, + TOKEN_2022_PROGRAM_ID, + TOKEN_PROGRAM_ID, +} from '@solana/spl-token' +import { AccountInfo, Connection, Keypair, PublicKey } from '@solana/web3.js' + +import { fetchSolanaTokenBalances } from './fetchSolanaTokenBalances' + +const OWNER = Keypair.generate().publicKey +const CLASSIC_MINT = Keypair.generate().publicKey +const TOKEN_2022_MINT = Keypair.generate().publicKey + +function ataFor(mint: PublicKey, programId: PublicKey): PublicKey { + return getAssociatedTokenAddressSync(mint, OWNER, false, programId, ASSOCIATED_TOKEN_PROGRAM_ID) +} + +function createConnection(accounts: Map | null>): Connection { + return { + rpcEndpoint: 'mock', + getMultipleAccountsInfo: jest.fn(async (pubkeys: PublicKey[]) => + pubkeys.map((pubkey) => accounts.get(pubkey.toBase58()) ?? null), + ), + } as unknown as Connection +} + +/** + * Builds a real, decodable SPL-token account so the production `unpackAccount` runs for real — + * mocking `unpackAccount` would hide an incorrect program being passed to it. + */ +function encodeTokenAccount( + mint: PublicKey, + owner: PublicKey, + amount: bigint, + programId: PublicKey, +): AccountInfo { + const data = Buffer.alloc(ACCOUNT_SIZE) + AccountLayout.encode( + { + mint, + owner, + amount, + delegateOption: 0, + delegate: PublicKey.default, + state: AccountState.Initialized, + isNativeOption: 0, + isNative: 0n, + delegatedAmount: 0n, + closeAuthorityOption: 0, + closeAuthority: PublicKey.default, + }, + data, + ) + + return { data, owner: programId, lamports: 1, executable: false, rentEpoch: 0 } +} + +describe('fetchSolanaTokenBalances', () => { + it('derives a Token-2022 ATA for mints flagged isToken2022', async () => { + const ata = ataFor(TOKEN_2022_MINT, TOKEN_2022_PROGRAM_ID) + const connection = createConnection( + new Map | null>([ + [ata.toBase58(), encodeTokenAccount(TOKEN_2022_MINT, OWNER, 1234n, TOKEN_2022_PROGRAM_ID)], + ]), + ) + + const result = await fetchSolanaTokenBalances(connection, OWNER.toBase58(), [ + { mint: TOKEN_2022_MINT.toBase58(), isToken2022: true }, + ]) + + expect(result).toEqual([{ mint: TOKEN_2022_MINT.toBase58(), balance: 1234n }]) + }) + + it('derives a classic ATA for mints not flagged isToken2022', async () => { + const ata = ataFor(CLASSIC_MINT, TOKEN_PROGRAM_ID) + const connection = createConnection( + new Map | null>([ + [ata.toBase58(), encodeTokenAccount(CLASSIC_MINT, OWNER, 500n, TOKEN_PROGRAM_ID)], + ]), + ) + + const result = await fetchSolanaTokenBalances(connection, OWNER.toBase58(), [ + { mint: CLASSIC_MINT.toBase58(), isToken2022: false }, + ]) + + expect(result).toEqual([{ mint: CLASSIC_MINT.toBase58(), balance: 500n }]) + }) + + it('resolves classic and Token-2022 mints in the same request', async () => { + const classicAta = ataFor(CLASSIC_MINT, TOKEN_PROGRAM_ID) + const token2022Ata = ataFor(TOKEN_2022_MINT, TOKEN_2022_PROGRAM_ID) + const connection = createConnection( + new Map | null>([ + [classicAta.toBase58(), encodeTokenAccount(CLASSIC_MINT, OWNER, 500n, TOKEN_PROGRAM_ID)], + [token2022Ata.toBase58(), encodeTokenAccount(TOKEN_2022_MINT, OWNER, 1234n, TOKEN_2022_PROGRAM_ID)], + ]), + ) + + const result = await fetchSolanaTokenBalances(connection, OWNER.toBase58(), [ + { mint: CLASSIC_MINT.toBase58(), isToken2022: false }, + { mint: TOKEN_2022_MINT.toBase58(), isToken2022: true }, + ]) + + expect(result).toEqual([ + { mint: CLASSIC_MINT.toBase58(), balance: 500n }, + { mint: TOKEN_2022_MINT.toBase58(), balance: 1234n }, + ]) + }) + + it('keeps balances aligned to input order when an ATA is missing', async () => { + const token2022Ata = ataFor(TOKEN_2022_MINT, TOKEN_2022_PROGRAM_ID) + // The classic mint has no ATA, so it must read as zero without shifting the Token-2022 balance onto it. + const connection = createConnection( + new Map | null>([ + [token2022Ata.toBase58(), encodeTokenAccount(TOKEN_2022_MINT, OWNER, 1234n, TOKEN_2022_PROGRAM_ID)], + ]), + ) + + const result = await fetchSolanaTokenBalances(connection, OWNER.toBase58(), [ + { mint: CLASSIC_MINT.toBase58(), isToken2022: false }, + { mint: TOKEN_2022_MINT.toBase58(), isToken2022: true }, + ]) + + expect(result).toEqual([ + { mint: CLASSIC_MINT.toBase58(), balance: 0n }, + { mint: TOKEN_2022_MINT.toBase58(), balance: 1234n }, + ]) + }) + + it('returns a zero balance when the ATA does not exist', async () => { + const connection = createConnection(new Map | null>()) + + const result = await fetchSolanaTokenBalances(connection, OWNER.toBase58(), [ + { mint: CLASSIC_MINT.toBase58(), isToken2022: false }, + ]) + + expect(result).toEqual([{ mint: CLASSIC_MINT.toBase58(), balance: 0n }]) + }) +}) diff --git a/libs/balances-and-allowances/src/services/fetchSolanaTokenBalances.ts b/libs/balances-and-allowances/src/services/fetchSolanaTokenBalances.ts index bbdc23e1d6d..9228364fe21 100644 --- a/libs/balances-and-allowances/src/services/fetchSolanaTokenBalances.ts +++ b/libs/balances-and-allowances/src/services/fetchSolanaTokenBalances.ts @@ -1,10 +1,18 @@ import { ASSOCIATED_TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync, + TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID, unpackAccount, } from '@solana/spl-token' -import { Connection, PublicKey } from '@solana/web3.js' +import { AccountInfo, Connection, PublicKey } from '@solana/web3.js' + +export interface SolanaTokenMint { + mint: string + // Token-2022 mints use a different program, which changes both the ATA address and its data layout. + // Sourced from the token list's `extensions.isToken2022` flag. + isToken2022: boolean +} interface SolanaTokenBalance { mint: string @@ -16,39 +24,49 @@ interface SolanaTokenBalance { const MAX_ACCOUNTS_PER_REQUEST = 100 /** - * Reads SPL-token balances for `tokenMints` owned by `ownerAddress`. + * Reads SPL-token balances for `tokens` owned by `ownerAddress`, supporting both the classic SPL Token + * program and Token-2022. * - * Balances live on the owner's Associated Token Account (ATA), not on the mint, so we derive the - * ATA for each mint and batch-read them via `getMultipleAccountsInfo` in chunks of - * {@link MAX_ACCOUNTS_PER_REQUEST}. A missing account means the owner never held that token, which - * is a zero balance rather than an error. + * Balances live on the owner's Associated Token Account (ATA), not on the mint. The ATA address and its + * data layout both depend on the mint's token program, which we take from each token's `isToken2022` + * flag rather than an extra RPC round-trip to read the mint. ATAs are batch-read via + * `getMultipleAccountsInfo` in chunks of {@link MAX_ACCOUNTS_PER_REQUEST}. A missing account means the + * owner never held that token, which is a zero balance rather than an error. */ export async function fetchSolanaTokenBalances( connection: Connection, ownerAddress: string, - tokenMints: string[], + tokens: SolanaTokenMint[], ): Promise { const owner = new PublicKey(ownerAddress) - const atas = tokenMints.map((mint) => - getAssociatedTokenAddressSync(new PublicKey(mint), owner, false, TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID), + const programIds = tokens.map(({ isToken2022 }) => (isToken2022 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID)) + const atas = tokens.map(({ mint }, index) => + getAssociatedTokenAddressSync(new PublicKey(mint), owner, false, programIds[index], ASSOCIATED_TOKEN_PROGRAM_ID), ) - const batches: (typeof atas)[] = [] - for (let i = 0; i < atas.length; i += MAX_ACCOUNTS_PER_REQUEST) { - batches.push(atas.slice(i, i + MAX_ACCOUNTS_PER_REQUEST)) - } + const ataInfos = await getMultipleAccountsInfoBatched(connection, atas) - const infosPerBatch = await Promise.all(batches.map((batch) => connection.getMultipleAccountsInfo(batch))) - const infos = infosPerBatch.flat() - - return infos.map((info, index) => { - if (!info) { - return { mint: tokenMints[index], balance: 0n } - } + return ataInfos.map((info, index) => { + const { mint } = tokens[index] + if (!info) return { mint, balance: 0n } - const account = unpackAccount(atas[index], info, TOKEN_PROGRAM_ID) + const account = unpackAccount(atas[index], info, programIds[index]) - return { mint: tokenMints[index], balance: account.amount } + return { mint, balance: account.amount } }) } + +async function getMultipleAccountsInfoBatched( + connection: Connection, + addresses: PublicKey[], +): Promise<(AccountInfo | null)[]> { + const batches: PublicKey[][] = [] + for (let i = 0; i < addresses.length; i += MAX_ACCOUNTS_PER_REQUEST) { + batches.push(addresses.slice(i, i + MAX_ACCOUNTS_PER_REQUEST)) + } + + const infosPerBatch = await Promise.all(batches.map((batch) => connection.getMultipleAccountsInfo(batch))) + + return infosPerBatch.flat() +} diff --git a/libs/common-const/src/types.test.ts b/libs/common-const/src/types.test.ts new file mode 100644 index 00000000000..38a578b66a7 --- /dev/null +++ b/libs/common-const/src/types.test.ts @@ -0,0 +1,35 @@ +import { TokenInfo } from '@cowprotocol/types' + +import { getIsToken2022, TOKEN_2022_TAG, TokenWithLogo } from './types' + +const CHAIN_ID = 1000000001 +const ADDRESS = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' + +function tokenInfo(overrides: Partial = {}): TokenInfo { + return { chainId: CHAIN_ID, address: ADDRESS, name: 'Token', decimals: 6, symbol: 'TKN', ...overrides } +} + +describe('TokenWithLogo.fromToken Token-2022 tagging', () => { + it('adds the Token-2022 tag from raw list extensions', () => { + const rawListToken = { ...tokenInfo(), extensions: { isToken2022: true } } as TokenInfo + + const token = TokenWithLogo.fromToken(rawListToken) + + expect(token.tags).toContain(TOKEN_2022_TAG) + expect(getIsToken2022(token)).toBe(true) + }) + + it('preserves an existing Token-2022 tag without duplicating it', () => { + const token = TokenWithLogo.fromToken(tokenInfo({ tags: [TOKEN_2022_TAG] })) + + expect(token.tags.filter((tag) => tag === TOKEN_2022_TAG)).toHaveLength(1) + expect(getIsToken2022(token)).toBe(true) + }) + + it('adds no Token-2022 tag when the extension is absent', () => { + const token = TokenWithLogo.fromToken(tokenInfo()) + + expect(token.tags).not.toContain(TOKEN_2022_TAG) + expect(getIsToken2022(token)).toBe(false) + }) +}) diff --git a/libs/common-const/src/types.ts b/libs/common-const/src/types.ts index dc761ed2a99..2760ec93f56 100644 --- a/libs/common-const/src/types.ts +++ b/libs/common-const/src/types.ts @@ -3,6 +3,11 @@ import { LpTokenProvider, TokenInfo } from '@cowprotocol/types' const emptyTokens = [] as string[] +// Solana Token-2022 mints are flagged in the token list via `extensions.isToken2022`. We surface that as +// a tag so it rides the existing `tags` pipeline. It is intentionally absent from the UI tag registry +// (`tokenListTags`), so it is not rendered as a chip. +export const TOKEN_2022_TAG = 'token-2022' + export class TokenWithLogo extends Token { static fromToken(token: Token | TokenInfo, logoURI?: string): TokenWithLogo { if (!token || token.chainId === undefined || !token.address) { @@ -16,7 +21,7 @@ export class TokenWithLogo extends Token { token.decimals, token.symbol, token.name, - ('tags' in token && token.tags) || [], + resolveTags(token), ) } @@ -60,3 +65,21 @@ export class LpToken extends TokenWithLogo { super(undefined, chainId, address, decimals, symbol, name) } } + +export function getIsToken2022(token: { tags?: string[] } | undefined): boolean { + return Boolean(token?.tags?.includes(TOKEN_2022_TAG)) +} + +// The token list flags Token-2022 mints under `extensions.isToken2022`; lift that to TOKEN_2022_TAG +// (deduped) so it survives both the parsed (`parseTokenInfo`) and raw-list (`buildTokensByAddress`) +// construction paths, and round-trips when an already-tagged token is re-converted. +function resolveTags(token: Token | TokenInfo): string[] { + const tags = ('tags' in token && token.tags) || [] + const hasToken2022Extension = Boolean((token as { extensions?: { isToken2022?: boolean } }).extensions?.isToken2022) + + if (hasToken2022Extension && !tags.includes(TOKEN_2022_TAG)) { + return [...tags, TOKEN_2022_TAG] + } + + return tags +} From 8144ef2223920ca4cd1f2aa536c64f019a2f69b1 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Wed, 15 Jul 2026 16:15:27 +0200 Subject: [PATCH 22/42] fix: capture network switch error --- .../src/common/hooks/useOnSelectNetwork.tsx | 5 +++-- libs/wallet/src/wagmi/hooks/useSwitchNetwork.ts | 8 +++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/cowswap-frontend/src/common/hooks/useOnSelectNetwork.tsx b/apps/cowswap-frontend/src/common/hooks/useOnSelectNetwork.tsx index f5af5221e14..0ac4665226b 100644 --- a/apps/cowswap-frontend/src/common/hooks/useOnSelectNetwork.tsx +++ b/apps/cowswap-frontend/src/common/hooks/useOnSelectNetwork.tsx @@ -38,8 +38,9 @@ export function useOnSelectNetwork(): (chainId: SupportedChainId, skipClose?: bo setChainIdToUrl(targetChain) // TODO: Replace any with proper type definitions // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (error: any) { - console.error('Failed to switch networks', error) + } catch (_error: any) { + console.error('Failed to switch networks', _error) + const error = _error.originalError ?? _error const causeIsRejection = !error.cause || isRejectRequestProviderError(error.cause) if (isRejectRequestProviderError(error) && causeIsRejection) { diff --git a/libs/wallet/src/wagmi/hooks/useSwitchNetwork.ts b/libs/wallet/src/wagmi/hooks/useSwitchNetwork.ts index a0a1a27f07b..2c7e4b4d92e 100644 --- a/libs/wallet/src/wagmi/hooks/useSwitchNetwork.ts +++ b/libs/wallet/src/wagmi/hooks/useSwitchNetwork.ts @@ -5,15 +5,13 @@ import { useConnection } from 'wagmi' import { SupportedChainId } from '@cowprotocol/cow-sdk' -import { useAppKitNetwork } from '@reown/appkit/react' - import { walletInfoAtom } from '../../api/state' import { SUPPORTED_REOWN_NETWORKS } from '../../reown/networks' +import { reownAppKit } from '../config' export function useSwitchNetwork(): (chainId: SupportedChainId) => Promise { const { isConnected } = useConnection() const setWalletInfo = useSetAtom(walletInfoAtom) - const { switchNetwork } = useAppKitNetwork() return useCallback( async (chainId: SupportedChainId) => { @@ -24,11 +22,11 @@ export function useSwitchNetwork(): (chainId: SupportedChainId) => Promise console.error('Unknown network to switch on', chainId) return } - await switchNetwork(network) + await reownAppKit.switchNetwork(network, { throwOnFailure: true }) } else { setWalletInfo((prev) => ({ ...prev, chainId })) } }, - [switchNetwork, isConnected, setWalletInfo], + [isConnected, setWalletInfo], ) } From e7c453a7f6a87e915143fc9df6e916143305c560 Mon Sep 17 00:00:00 2001 From: shoom3301 Date: Wed, 15 Jul 2026 16:27:01 +0200 Subject: [PATCH 23/42] fix: fix account.toLowerCase --- ...ePersistSolanaBalancesViaWebCalls.test.tsx | 16 ++++ .../usePersistSolanaBalancesViaWebCalls.ts | 6 +- .../hooks/useSwrConfigWithPauseForNetwork.ts | 4 +- .../updaters/BalancesCacheUpdater.test.tsx | 80 +++++++++++++++++++ .../src/updaters/BalancesCacheUpdater.tsx | 8 +- 5 files changed, 105 insertions(+), 9 deletions(-) create mode 100644 libs/balances-and-allowances/src/updaters/BalancesCacheUpdater.test.tsx diff --git a/libs/balances-and-allowances/src/hooks/usePersistSolanaBalancesViaWebCalls.test.tsx b/libs/balances-and-allowances/src/hooks/usePersistSolanaBalancesViaWebCalls.test.tsx index f708cf993aa..89f88d76114 100644 --- a/libs/balances-and-allowances/src/hooks/usePersistSolanaBalancesViaWebCalls.test.tsx +++ b/libs/balances-and-allowances/src/hooks/usePersistSolanaBalancesViaWebCalls.test.tsx @@ -200,6 +200,22 @@ describe('usePersistSolanaBalancesViaWebCalls', () => { expect(atas[0].toBase58()).toBe(ataKey(MINT_A)) }) + it('keys the update timestamp by the case-sensitive Solana account, not a lowercased alias', async () => { + const { result } = renderHook( + () => { + usePersistSolanaBalancesViaWebCalls(makeParams()) + return useAtomValue(balancesUpdateAtom) + }, + { wrapper }, + ) + + await waitFor(() => expect(result.current[SupportedChainId.SOLANA]?.[getAddressKey(ACCOUNT)]).toBeDefined()) + + // getAddressKey preserves case for Solana pubkeys; a lowercased key would alias distinct owners. + expect(ACCOUNT).not.toBe(ACCOUNT.toLowerCase()) + expect(result.current[SupportedChainId.SOLANA]?.[ACCOUNT.toLowerCase()]).toBeUndefined() + }) + it('derives a Token-2022 ATA for mints tagged as Token-2022 in the token list', async () => { mockTokensByAddress = { [getAddressKey(MINT_A)]: { tags: [TOKEN_2022_TAG] } } mockAmountByAta = { [ataKey(MINT_A, TOKEN_2022_PROGRAM)]: 999n } diff --git a/libs/balances-and-allowances/src/hooks/usePersistSolanaBalancesViaWebCalls.ts b/libs/balances-and-allowances/src/hooks/usePersistSolanaBalancesViaWebCalls.ts index 1550c2e88b5..734a092a385 100644 --- a/libs/balances-and-allowances/src/hooks/usePersistSolanaBalancesViaWebCalls.ts +++ b/libs/balances-and-allowances/src/hooks/usePersistSolanaBalancesViaWebCalls.ts @@ -1,7 +1,7 @@ import { useSetAtom } from 'jotai' import { useEffect, useMemo } from 'react' -import { useQuery } from '@tanstack/react-query' +import { skipToken, useQuery } from '@tanstack/react-query' import { getIsToken2022 } from '@cowprotocol/common-const' import { getIsNativeToken } from '@cowprotocol/common-utils' @@ -60,7 +60,7 @@ export function usePersistSolanaBalancesViaWebCalls(params: PersistBalancesAndAl dataUpdatedAt, } = useQuery({ queryKey, - queryFn: () => fetchSolanaTokenBalances(connection!, account!, tokenMints), + queryFn: connection && account ? () => fetchSolanaTokenBalances(connection, account, tokenMints) : skipToken, enabled, refetchInterval: refetchInterval || undefined, refetchOnWindowFocus: false, @@ -116,7 +116,7 @@ export function usePersistSolanaBalancesViaWebCalls(params: PersistBalancesAndAl ...state, [chainId]: { ...state[chainId], - [account.toLowerCase()]: Date.now(), + [getAddressKey(account)]: Date.now(), }, })) } diff --git a/libs/balances-and-allowances/src/hooks/useSwrConfigWithPauseForNetwork.ts b/libs/balances-and-allowances/src/hooks/useSwrConfigWithPauseForNetwork.ts index c67a27e2c20..2ae19dd32b3 100644 --- a/libs/balances-and-allowances/src/hooks/useSwrConfigWithPauseForNetwork.ts +++ b/libs/balances-and-allowances/src/hooks/useSwrConfigWithPauseForNetwork.ts @@ -1,7 +1,7 @@ import { useAtomValue } from 'jotai' import { useEffect, useMemo, useRef } from 'react' -import type { SupportedChainId } from '@cowprotocol/cow-sdk' +import { getAddressKey, type SupportedChainId } from '@cowprotocol/cow-sdk' import ms from 'ms.macro' @@ -25,7 +25,7 @@ export function useSwrConfigWithPauseForNetwork( const balancesUpdate = useAtomValue(balancesUpdateAtom) const balancesChainId = balances.chainId - const lastUpdateTimestamp = account ? balancesUpdate[chainId]?.[account.toLowerCase()] : undefined + const lastUpdateTimestamp = account ? balancesUpdate[chainId]?.[getAddressKey(account)] : undefined const lastUpdateTimestampRef = useRef(lastUpdateTimestamp) diff --git a/libs/balances-and-allowances/src/updaters/BalancesCacheUpdater.test.tsx b/libs/balances-and-allowances/src/updaters/BalancesCacheUpdater.test.tsx new file mode 100644 index 00000000000..af883b5c8ef --- /dev/null +++ b/libs/balances-and-allowances/src/updaters/BalancesCacheUpdater.test.tsx @@ -0,0 +1,80 @@ +import { Provider, useAtomValue } from 'jotai' +import { useHydrateAtoms } from 'jotai/utils' +import React, { ReactNode } from 'react' + +import { getAddressKey, mapSupportedNetworks, SupportedChainId } from '@cowprotocol/cow-sdk' + +import { renderHook, waitFor } from '@testing-library/react' + +import { BalancesCacheUpdater } from './BalancesCacheUpdater' + +import { balancesAtom, balancesCacheAtom, BalancesState, DEFAULT_BALANCES_STATE } from '../state/balancesAtom' + +// Mixed-case Solana pubkey: lowercasing it would alias a different owner and corrupt the cache bucket. +const SOLANA_ACCOUNT = 'So11111111111111111111111111111111111111112' +const MINT = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' + +type BalancesCache = ReturnType> + +let hydrateBalances: BalancesState +let hydrateCache: BalancesCache + +function HydrateAtoms({ children }: { children: ReactNode }): ReactNode { + useHydrateAtoms([ + [balancesAtom, hydrateBalances], + [balancesCacheAtom, hydrateCache], + ]) + return <>{children} +} + +function wrapper({ children }: { children: ReactNode }): ReactNode { + return ( + + {children} + + ) +} + +describe('BalancesCacheUpdater (Solana account keys)', () => { + it('persists the cache under the case-sensitive account key, not a lowercased alias', async () => { + hydrateBalances = { + ...DEFAULT_BALANCES_STATE, + chainId: SupportedChainId.SOLANA, + values: { [getAddressKey(MINT)]: 100n }, + } + hydrateCache = mapSupportedNetworks({}) + + const { result } = renderHook( + () => { + BalancesCacheUpdater({ chainId: SupportedChainId.SOLANA, account: SOLANA_ACCOUNT, excludedTokens: new Set() }) + return useAtomValue(balancesCacheAtom) + }, + { wrapper }, + ) + + await waitFor(() => expect(result.current[SupportedChainId.SOLANA]?.[getAddressKey(SOLANA_ACCOUNT)]).toBeDefined()) + + expect(SOLANA_ACCOUNT).not.toBe(SOLANA_ACCOUNT.toLowerCase()) + expect(result.current[SupportedChainId.SOLANA]?.[SOLANA_ACCOUNT.toLowerCase()]).toBeUndefined() + expect(result.current[SupportedChainId.SOLANA]?.[getAddressKey(SOLANA_ACCOUNT)]?.[getAddressKey(MINT)]).toBe('100') + }) + + it('restores balances from a cache stored under the case-sensitive account key', async () => { + hydrateBalances = { ...DEFAULT_BALANCES_STATE } + hydrateCache = mapSupportedNetworks({}) + hydrateCache[SupportedChainId.SOLANA] = { + [getAddressKey(SOLANA_ACCOUNT)]: { [getAddressKey(MINT)]: '250' }, + } + + const { result } = renderHook( + () => { + BalancesCacheUpdater({ chainId: SupportedChainId.SOLANA, account: SOLANA_ACCOUNT, excludedTokens: new Set() }) + return useAtomValue(balancesAtom) + }, + { wrapper }, + ) + + await waitFor(() => expect(result.current.values[getAddressKey(MINT)]).toBe(250n)) + expect(result.current.fromCache).toBe(true) + }) +}) diff --git a/libs/balances-and-allowances/src/updaters/BalancesCacheUpdater.tsx b/libs/balances-and-allowances/src/updaters/BalancesCacheUpdater.tsx index 67c8fed0b4f..f60b23e091d 100644 --- a/libs/balances-and-allowances/src/updaters/BalancesCacheUpdater.tsx +++ b/libs/balances-and-allowances/src/updaters/BalancesCacheUpdater.tsx @@ -1,7 +1,7 @@ import { useAtom } from 'jotai/index' import { useEffect, useLayoutEffect, useRef } from 'react' -import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { getAddressKey, SupportedChainId } from '@cowprotocol/cow-sdk' import { balancesAtom, balancesCacheAtom } from '../state/balancesAtom' @@ -36,7 +36,7 @@ export function BalancesCacheUpdater({ chainId, account, excludedTokens }: Balan {} as Record, ) - const currentCache = state[chainId]?.[account.toLowerCase()] || {} + const currentCache = state[chainId]?.[getAddressKey(account)] || {} // Remove zero balances from the current cache const updatedCache = Object.keys(currentCache).reduce( (acc, tokenAddress) => { @@ -53,7 +53,7 @@ export function BalancesCacheUpdater({ chainId, account, excludedTokens }: Balan ...state, [chainId]: { ...state[chainId], - [account.toLowerCase()]: { + [getAddressKey(account)]: { ...updatedCache, ...balancesToCache, }, @@ -67,7 +67,7 @@ export function BalancesCacheUpdater({ chainId, account, excludedTokens }: Balan if (!account) return if (lastChainCacheUpdateRef.current === chainId) return - const cache = balancesCache[chainId]?.[account.toLowerCase()] + const cache = balancesCache[chainId]?.[getAddressKey(account)] if (!cache) return From a4c85c9b4aa92652de8cec5f43cf0f366e585491 Mon Sep 17 00:00:00 2001 From: shoom3301 Date: Wed, 15 Jul 2026 16:34:32 +0200 Subject: [PATCH 24/42] fix: isolates malformed mint --- .../services/fetchSolanaTokenBalances.test.ts | 22 ++++++++++ .../src/services/fetchSolanaTokenBalances.ts | 43 ++++++++++++++----- 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/libs/balances-and-allowances/src/services/fetchSolanaTokenBalances.test.ts b/libs/balances-and-allowances/src/services/fetchSolanaTokenBalances.test.ts index 08e6bc71481..1f928c3a543 100644 --- a/libs/balances-and-allowances/src/services/fetchSolanaTokenBalances.test.ts +++ b/libs/balances-and-allowances/src/services/fetchSolanaTokenBalances.test.ts @@ -134,6 +134,28 @@ describe('fetchSolanaTokenBalances', () => { ]) }) + it('isolates a malformed mint so valid balances still load', async () => { + // Passes the token-list base58 length/charset check but decodes to 33 bytes, so `new PublicKey` + // throws. It must not prevent the valid mint's balance from loading. + const BAD_MINT = 'z'.repeat(44) + const classicAta = ataFor(CLASSIC_MINT, TOKEN_PROGRAM_ID) + const connection = createConnection( + new Map | null>([ + [classicAta.toBase58(), encodeTokenAccount(CLASSIC_MINT, OWNER, 500n, TOKEN_PROGRAM_ID)], + ]), + ) + + const result = await fetchSolanaTokenBalances(connection, OWNER.toBase58(), [ + { mint: BAD_MINT, isToken2022: false }, + { mint: CLASSIC_MINT.toBase58(), isToken2022: false }, + ]) + + expect(result).toEqual([ + { mint: BAD_MINT, balance: 0n }, + { mint: CLASSIC_MINT.toBase58(), balance: 500n }, + ]) + }) + it('returns a zero balance when the ATA does not exist', async () => { const connection = createConnection(new Map | null>()) diff --git a/libs/balances-and-allowances/src/services/fetchSolanaTokenBalances.ts b/libs/balances-and-allowances/src/services/fetchSolanaTokenBalances.ts index 9228364fe21..f0c58021200 100644 --- a/libs/balances-and-allowances/src/services/fetchSolanaTokenBalances.ts +++ b/libs/balances-and-allowances/src/services/fetchSolanaTokenBalances.ts @@ -40,21 +40,44 @@ export async function fetchSolanaTokenBalances( ): Promise { const owner = new PublicKey(ownerAddress) - const programIds = tokens.map(({ isToken2022 }) => (isToken2022 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID)) - const atas = tokens.map(({ mint }, index) => - getAssociatedTokenAddressSync(new PublicKey(mint), owner, false, programIds[index], ASSOCIATED_TOKEN_PROGRAM_ID), - ) + // Every token defaults to a zero balance, keeping the result aligned to `tokens` order regardless + // of which ATAs resolve or exist. + const balances: SolanaTokenBalance[] = tokens.map(({ mint }) => ({ mint, balance: 0n })) - const ataInfos = await getMultipleAccountsInfoBatched(connection, atas) + // Derive each ATA up front, isolating any mint that fails to parse. A malformed mint can pass the + // token list's base58 length/charset check yet still not decode to a 32-byte public key, so + // `new PublicKey(mint)` would throw. Building the ATAs in a single `map` would let one bad mint + // reject balances for the entire list — instead we drop it and leave its zero balance in place. + const resolvable: { index: number; ata: PublicKey; programId: PublicKey }[] = [] + tokens.forEach(({ mint, isToken2022 }, index) => { + const programId = isToken2022 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID + try { + const ata = getAssociatedTokenAddressSync( + new PublicKey(mint), + owner, + false, + programId, + ASSOCIATED_TOKEN_PROGRAM_ID, + ) + resolvable.push({ index, ata, programId }) + } catch { + // Malformed mint: leave the default zero balance so the rest of the list still loads. + } + }) - return ataInfos.map((info, index) => { - const { mint } = tokens[index] - if (!info) return { mint, balance: 0n } + const ataInfos = await getMultipleAccountsInfoBatched( + connection, + resolvable.map(({ ata }) => ata), + ) - const account = unpackAccount(atas[index], info, programIds[index]) + ataInfos.forEach((info, i) => { + if (!info) return - return { mint, balance: account.amount } + const { index, ata, programId } = resolvable[i] + balances[index].balance = unpackAccount(ata, info, programId).amount }) + + return balances } async function getMultipleAccountsInfoBatched( From e624ae64e5ae8ed067ff795dd63b86ce0a41200c Mon Sep 17 00:00:00 2001 From: shoom3301 Date: Wed, 15 Jul 2026 16:50:48 +0200 Subject: [PATCH 25/42] chore: add solana default RPC --- .../src/modules/permit/hooks/usePermitInfo.ts | 4 +++- libs/common-const/src/networks.ts | 23 +++++++++++-------- libs/wallet/src/wagmi/config.ts | 4 ++-- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/permit/hooks/usePermitInfo.ts b/apps/cowswap-frontend/src/modules/permit/hooks/usePermitInfo.ts index 351e8cfe5a3..a8a855576f0 100644 --- a/apps/cowswap-frontend/src/modules/permit/hooks/usePermitInfo.ts +++ b/apps/cowswap-frontend/src/modules/permit/hooks/usePermitInfo.ts @@ -4,7 +4,7 @@ import { useEffect, useMemo } from 'react' import { useConfig, usePublicClient } from 'wagmi' import { getIsNativeToken, getWrappedToken, COW_PROTOCOL_VAULT_RELAYER_ADDRESS } from '@cowprotocol/common-utils' -import { getAddressKey, mapSupportedNetworks, SupportedChainId } from '@cowprotocol/cow-sdk' +import { getAddressKey, isNonEvmChain, mapSupportedNetworks, SupportedChainId } from '@cowprotocol/cow-sdk' import { Currency } from '@cowprotocol/currency' import { DEFAULT_MIN_GAS_LIMIT, getTokenPermitInfo, PermitInfo } from '@cowprotocol/permit-utils' import { useWalletInfo } from '@cowprotocol/wallet' @@ -73,9 +73,11 @@ export function usePermitInfo( const spender = customSpender || COW_PROTOCOL_VAULT_RELAYER_ADDRESS[chainId] + // eslint-disable-next-line complexity useEffect(() => { if ( !chainId || + isNonEvmChain(chainId) || !isPermitEnabled || !lowerCaseAddress || !config || diff --git a/libs/common-const/src/networks.ts b/libs/common-const/src/networks.ts index f9053ca84a9..76219f6385b 100644 --- a/libs/common-const/src/networks.ts +++ b/libs/common-const/src/networks.ts @@ -1,8 +1,8 @@ -import { EvmChains, HttpsString } from '@cowprotocol/cow-sdk' +import { EvmChains, HttpsString, TargetChainId, NonEvmChains } from '@cowprotocol/cow-sdk' const INFURA_KEY = process.env['REACT_APP_INFURA_KEY'] || '2af29cd5ac554ae3b8d991afe1ba4b7d' // Default rate-limited infura key (should be overridden, not reliable to use) -const RPC_URL_ENVS: Record = { +const RPC_URL_ENVS: Record = { [EvmChains.MAINNET]: (process.env['REACT_APP_NETWORK_URL_1'] as HttpsString) || undefined, [EvmChains.BNB]: (process.env['REACT_APP_NETWORK_URL_56'] as HttpsString) || undefined, [EvmChains.GNOSIS_CHAIN]: (process.env['REACT_APP_NETWORK_URL_100'] as HttpsString) || undefined, @@ -14,13 +14,12 @@ const RPC_URL_ENVS: Record = { [EvmChains.INK]: (process.env['REACT_APP_NETWORK_URL_57073'] as HttpsString) || undefined, [EvmChains.LINEA]: (process.env['REACT_APP_NETWORK_URL_59144'] as HttpsString) || undefined, [EvmChains.SEPOLIA]: (process.env['REACT_APP_NETWORK_URL_11155111'] as HttpsString) || undefined, - // OPTIMISM is bridge-only (not in `SupportedChainId`). Carried here so RPC_URLS satisfies - // `Record` ahead of the Solana-aware SDK migration; CoW Protocol does not - // sell from Optimism today. [EvmChains.OPTIMISM]: (process.env['REACT_APP_NETWORK_URL_10'] as HttpsString) || undefined, + [NonEvmChains.SOLANA]: (process.env['REACT_APP_NETWORK_URL_1000000001'] as HttpsString) || undefined, + [NonEvmChains.BITCOIN]: (process.env['REACT_APP_NETWORK_URL_1000000000'] as HttpsString) || undefined, } -const DEFAULT_RPC_URL: Record = { +const DEFAULT_RPC_URL: Record = { [EvmChains.MAINNET]: { url: `https://mainnet.infura.io/v3/${INFURA_KEY}`, usesInfura: true }, [EvmChains.BNB]: { url: `https://bsc-mainnet.infura.io/v3/${INFURA_KEY}`, usesInfura: true }, [EvmChains.GNOSIS_CHAIN]: { url: `https://rpc.gnosis.gateway.fm`, usesInfura: false }, @@ -33,6 +32,8 @@ const DEFAULT_RPC_URL: Record = (Object.keys(RPC_URL_ENVS) as unknown as EvmChains[]).reduce( +export const RPC_URLS: Record = ( + Object.keys(RPC_URL_ENVS) as unknown as EvmChains[] +).reduce( (acc, chainId) => { - acc[Number(chainId) as EvmChains] = getRpcUrl(Number(chainId) as EvmChains) + acc[Number(chainId) as TargetChainId] = getRpcUrl(Number(chainId) as TargetChainId) return acc }, - {} as Record, + {} as Record, ) -function getRpcUrl(chainId: EvmChains): HttpsString { +function getRpcUrl(chainId: TargetChainId): HttpsString { const envKey = `REACT_APP_NETWORK_URL_${chainId}` const rpcUrl = RPC_URL_ENVS[chainId] diff --git a/libs/wallet/src/wagmi/config.ts b/libs/wallet/src/wagmi/config.ts index 501df38f053..d13b440a346 100644 --- a/libs/wallet/src/wagmi/config.ts +++ b/libs/wallet/src/wagmi/config.ts @@ -3,7 +3,7 @@ import { type Transport } from 'wagmi' import { IS_SOLANA_ENABLED, RPC_URLS } from '@cowprotocol/common-const' import { isInjectedWidget, isMobile } from '@cowprotocol/common-utils' -import { EvmChains } from '@cowprotocol/cow-sdk' +import { EvmChains, TargetChainId } from '@cowprotocol/cow-sdk' import { createAppKit } from '@reown/appkit/react' import { SolanaAdapter } from '@reown/appkit-adapter-solana' @@ -38,7 +38,7 @@ const wagmiTransports = SUPPORTED_REOWN_NETWORKS.reduce( /** CAIP-shaped RPCs for AppKit UI / network metadata (pairs with `wagmiTransports`). */ const customRpcUrls: Record> = {} for (const chain of SUPPORTED_REOWN_NETWORKS) { - const url = RPC_URLS[chain.id as EvmChains] + const url = RPC_URLS[chain.id as TargetChainId] if (url) { customRpcUrls[`eip155:${chain.id}`] = [{ url }] } From 8d440d700036a533a0702d6db4873523b0047016 Mon Sep 17 00:00:00 2001 From: shoom3301 Date: Wed, 15 Jul 2026 17:36:39 +0200 Subject: [PATCH 26/42] fix(solana): fix recent wallet reconnect --- libs/wallet/src/wagmi/config.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libs/wallet/src/wagmi/config.ts b/libs/wallet/src/wagmi/config.ts index d13b440a346..9569dfe5002 100644 --- a/libs/wallet/src/wagmi/config.ts +++ b/libs/wallet/src/wagmi/config.ts @@ -80,7 +80,11 @@ OptionsController.setOptions({ ...OptionsController.state, enableInjected: false const isSafeApp = getIsSafeAppIframe() const isWidget = isInjectedWidget() const hasRecentConnector = - typeof localStorage !== 'undefined' && Boolean(localStorage.getItem(`${wagmiStorage.key}.recentConnectorId`)) + typeof localStorage !== 'undefined' && + Boolean( + localStorage.getItem('@appkit/eip155:connected_connector_id') || + localStorage.getItem('@appkit/solana:connected_connector_id'), + ) const reownAppKit = createAppKit({ adapters: IS_SOLANA_ENABLED ? [wagmiAdapter, solanaAdapter] : [wagmiAdapter], From 27bff7d206be22d20ec6a3eb4bef96c70c7df92d Mon Sep 17 00:00:00 2001 From: shoom3301 Date: Wed, 15 Jul 2026 17:52:39 +0200 Subject: [PATCH 27/42] fix: fix solana dedupe --- apps/cowswap-frontend/vite.config.mts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/cowswap-frontend/vite.config.mts b/apps/cowswap-frontend/vite.config.mts index 5a2b42c46d1..f8c67d67e99 100644 --- a/apps/cowswap-frontend/vite.config.mts +++ b/apps/cowswap-frontend/vite.config.mts @@ -1,5 +1,6 @@ /// import { lingui } from '@lingui/vite-plugin' + import { sentryVitePlugin } from '@sentry/vite-plugin' import react from '@vitejs/plugin-react-swc' import { bundleStats } from 'rollup-plugin-bundle-stats' @@ -289,7 +290,14 @@ export default defineConfig(({ mode, isPreview }) => { // in @reown/appkit-adapter-solana (#7709), pnpm resolves the appkit family to two // peer-instances; without deduping the controllers package, code in libs/wallet reads // an empty ConnectorController while the deduped appkit/adapter-wagmi populate the other. - dedupe: ['react-router', '@reown/appkit', '@reown/appkit-adapter-wagmi', '@reown/appkit-controllers', 'wagmi'], + dedupe: [ + 'react-router', + '@reown/appkit', + '@reown/appkit-adapter-wagmi', + '@reown/appkit-adapter-solana', + '@reown/appkit-controllers', + 'wagmi', + ], }, build: { From 979400fc3d57c8e640c8ecb905bf0f3c81b79f99 Mon Sep 17 00:00:00 2001 From: shoom3301 Date: Wed, 15 Jul 2026 18:05:43 +0200 Subject: [PATCH 28/42] fix: fix build --- apps/cowswap-frontend/package.json | 1 + pnpm-lock.yaml | 138 +++++++++++++++-------------- 2 files changed, 71 insertions(+), 68 deletions(-) diff --git a/apps/cowswap-frontend/package.json b/apps/cowswap-frontend/package.json index 5d528fb410f..a896203817a 100644 --- a/apps/cowswap-frontend/package.json +++ b/apps/cowswap-frontend/package.json @@ -71,6 +71,7 @@ "@reach/menu-button": "0.18.0", "@react-spring/web": "9.7.3", "@reduxjs/toolkit": "1.9.5", + "@reown/appkit-adapter-solana": "1.8.19", "@reown/appkit-adapter-wagmi": "1.8.19", "@reown/appkit-controllers": "1.8.19", "@reown/appkit": "1.8.19", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1254282a85d..ef180aa2615 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -535,7 +535,7 @@ importers: dependencies: '@1inch/permit-signed-approvals-utils': specifier: 1.5.2 - version: 1.5.2(bufferutil@4.0.8)(utf-8-validate@6.0.6) + version: 1.5.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@cowprotocol/analytics': specifier: workspace:* version: link:../../libs/analytics @@ -607,7 +607,7 @@ importers: version: 2.2.2(ajv@8.17.1)(cross-fetch@4.0.0(encoding@0.1.13))(encoding@0.1.13)(multiformats@14.0.0) '@cowprotocol/sdk-viem-adapter': specifier: 0.3.24 - version: 0.3.24(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) + version: 0.3.24(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) '@cowprotocol/snackbars': specifier: workspace:* version: link:../../libs/snackbars @@ -658,16 +658,19 @@ importers: version: 1.9.5(react-redux@8.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(redux@4.2.1))(react@19.1.2) '@reown/appkit': specifier: 1.8.19 - version: 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(zod@3.25.76) + version: 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-adapter-solana': + specifier: 1.8.19 + version: 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-adapter-wagmi': specifier: 1.8.19 - version: 1.8.19(patch_hash=b669cb20e36d425602b690643d4147fd1756aa3d94db205f5e4823777a277b1c)(725f042c52896b22a29c385487b96d7a) + version: 1.8.19(patch_hash=b669cb20e36d425602b690643d4147fd1756aa3d94db205f5e4823777a277b1c)(b5eb2d51373dca5abfc3587eabff26c7) '@reown/appkit-controllers': specifier: 1.8.19 - version: 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + version: 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@safe-global/api-kit': specifier: 4.2.0 - version: 4.2.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + version: 4.2.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@safe-global/types-kit': specifier: 3.0.0 version: 3.0.0(typescript@5.9.3)(zod@3.25.76) @@ -697,7 +700,7 @@ importers: version: 10.3.1(react@19.1.2) '@wagmi/core': specifier: 3.4.8 - version: 3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) + version: 3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) bignumber.js: specifier: 9.1.2 version: 9.1.2 @@ -835,10 +838,10 @@ importers: version: 1.2.4(react@19.1.2) viem: specifier: 2.48.8 - version: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + version: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) wagmi: specifier: 3.6.9 - version: 3.6.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) + version: 3.6.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) workbox-core: specifier: 6.6.1 version: 6.6.1 @@ -860,7 +863,7 @@ importers: devDependencies: '@storybook/react-vite': specifier: 10.3.6 - version: 10.3.6(esbuild@0.27.2)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(rollup@4.52.4)(storybook@10.3.6(@testing-library/dom@10.4.1)(bufferutil@4.0.8)(prettier@3.6.2)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(utf-8-validate@6.0.6))(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.88.0)(sass@1.88.0)(terser@5.46.0)(yaml@2.8.1))(webpack@5.102.1(@swc/core@1.15.26(@swc/helpers@0.5.17))(esbuild@0.27.2)) + version: 10.3.6(esbuild@0.27.2)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(rollup@4.52.4)(storybook@10.3.6(@testing-library/dom@10.4.1)(bufferutil@4.0.8)(prettier@3.6.2)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(utf-8-validate@5.0.10))(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.88.0)(sass@1.88.0)(terser@5.46.0)(yaml@2.8.1))(webpack@5.102.1(@swc/core@1.15.26(@swc/helpers@0.5.17))(esbuild@0.27.2)) '@testing-library/react': specifier: 16.3.0 version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) @@ -902,7 +905,7 @@ importers: version: 0.8.0(@emotion/react@11.14.0(@types/react@19.1.3)(react@19.1.2))(@types/react@19.1.3)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(redux@4.2.1) react-cosmos: specifier: 7.0.0 - version: 7.0.0(@types/express@4.17.21)(bufferutil@4.0.8)(utf-8-validate@6.0.6) + version: 7.0.0(@types/express@4.17.21)(bufferutil@4.0.8)(utf-8-validate@5.0.10) rollup-plugin-bundle-stats: specifier: 4.21.9 version: 4.21.9(core-js@3.48.0)(rollup@4.52.4)(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.88.0)(sass@1.88.0)(terser@5.46.0)(yaml@2.8.1)) @@ -1241,7 +1244,7 @@ importers: dependencies: '@coinbase/wallet-sdk': specifier: 4.3.7 - version: 4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + version: 4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@cowprotocol/analytics': specifier: workspace:* version: link:../../libs/analytics @@ -1280,19 +1283,19 @@ importers: version: 5.17.1(@emotion/react@11.14.0(@types/react@19.1.3)(react@19.1.2))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.3)(react@19.1.2))(@types/react@19.1.3)(react@19.1.2))(@types/react@19.1.3)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) '@reown/appkit': specifier: 1.8.19 - version: 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(zod@3.25.76) + version: 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-adapter-wagmi': specifier: 1.8.19 - version: 1.8.19(patch_hash=b669cb20e36d425602b690643d4147fd1756aa3d94db205f5e4823777a277b1c)(725f042c52896b22a29c385487b96d7a) + version: 1.8.19(patch_hash=b669cb20e36d425602b690643d4147fd1756aa3d94db205f5e4823777a277b1c)(b5eb2d51373dca5abfc3587eabff26c7) '@reown/appkit-controllers': specifier: 1.8.19 - version: 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + version: 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@tanstack/react-query': specifier: 5.90.20 version: 5.90.20(react@19.1.2) '@wagmi/connectors': specifier: 8.0.9 - version: 8.0.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@wagmi/core@3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)))(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) + version: 8.0.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@wagmi/core@3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) csstype: specifier: 3.1.3 version: 3.1.3 @@ -1322,10 +1325,10 @@ importers: version: 15.5.0(react@19.1.2) viem: specifier: 2.48.8 - version: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + version: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) wagmi: specifier: 3.6.9 - version: 3.6.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) + version: 3.6.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) widget-react-v0.13.0: specifier: npm:@cowprotocol/widget-react@0.13.0 version: '@cowprotocol/widget-react@0.13.0' @@ -17739,6 +17742,13 @@ packages: snapshots: + '@1inch/permit-signed-approvals-utils@1.5.2(bufferutil@4.0.8)(utf-8-validate@5.0.10)': + dependencies: + ethers: 6.16.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@1inch/permit-signed-approvals-utils@1.5.2(bufferutil@4.0.8)(utf-8-validate@6.0.6)': dependencies: ethers: 6.16.0(bufferutil@4.0.8)(utf-8-validate@6.0.6) @@ -19027,6 +19037,7 @@ snapshots: - typescript - utf-8-validate - zod + optional: true '@commitlint/cli@20.1.0(@types/node@24.7.1)(typescript@5.9.3)': dependencies: @@ -22918,56 +22929,6 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-adapter-wagmi@1.8.19(patch_hash=b669cb20e36d425602b690643d4147fd1756aa3d94db205f5e4823777a277b1c)(725f042c52896b22a29c385487b96d7a)': - dependencies: - '@reown/appkit': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-common': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-controllers': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-polyfills': 1.8.19 - '@reown/appkit-scaffold-ui': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(valtio@2.1.7(@types/react@19.1.3)(react@19.1.2))(zod@3.25.76) - '@reown/appkit-utils': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(valtio@2.1.7(@types/react@19.1.3)(react@19.1.2))(zod@3.25.76) - '@reown/appkit-wallet': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6) - '@wagmi/core': 3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) - '@walletconnect/universal-provider': 2.23.7(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - valtio: 2.1.7(@types/react@19.1.3)(react@19.1.2) - viem: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - wagmi: 3.6.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) - optionalDependencies: - '@wagmi/connectors': 8.0.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@wagmi/core@3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)))(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) - transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@base-org/account' - - '@capacitor/preferences' - - '@coinbase/wallet-sdk' - - '@metamask/connect-evm' - - '@netlify/blobs' - - '@planetscale/database' - - '@react-native-async-storage/async-storage' - - '@safe-global/safe-apps-provider' - - '@safe-global/safe-apps-sdk' - - '@types/react' - - '@upstash/redis' - - '@vercel/kv' - - '@walletconnect/ethereum-provider' - - accounts - - bufferutil - - debug - - encoding - - fastestsmallesttextencoderdecoder - - immer - - porto - - react - - supports-color - - typescript - - use-sync-external-store - - utf-8-validate - - zod - '@reown/appkit-adapter-wagmi@1.8.19(patch_hash=b669cb20e36d425602b690643d4147fd1756aa3d94db205f5e4823777a277b1c)(b5eb2d51373dca5abfc3587eabff26c7)': dependencies: '@reown/appkit': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@5.0.10)(zod@3.25.76) @@ -30237,6 +30198,19 @@ snapshots: ethereum-cryptography: 0.1.3 rlp: 2.2.7 + ethers@6.16.0(bufferutil@4.0.8)(utf-8-validate@5.0.10): + dependencies: + '@adraffy/ens-normalize': 1.10.1 + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.2 + '@types/node': 22.7.5 + aes-js: 4.0.0-beta.5 + tslib: 2.7.0 + ws: 8.17.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + ethers@6.16.0(bufferutil@4.0.8)(utf-8-validate@6.0.6): dependencies: '@adraffy/ens-normalize': 1.10.1 @@ -34990,6 +34964,29 @@ snapshots: lodash-es: 4.17.21 react-cosmos-core: 7.0.0 + react-cosmos@7.0.0(@types/express@4.17.21)(bufferutil@4.0.8)(utf-8-validate@5.0.10): + dependencies: + '@skidding/launch-editor': 2.2.3 + chokidar: 3.6.0 + express: 4.21.2 + glob: 10.4.5 + http-proxy-middleware: 2.0.9(@types/express@4.17.21) + lodash-es: 4.17.21 + micromatch: 4.0.8 + open: 10.1.1 + pem: 1.14.8 + react-cosmos-core: 7.0.0 + react-cosmos-renderer: 7.0.0 + react-cosmos-ui: 7.0.0 + ws: 8.18.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/express' + - bufferutil + - debug + - supports-color + - utf-8-validate + react-cosmos@7.0.0(@types/express@4.17.21)(bufferutil@4.0.8)(utf-8-validate@6.0.6): dependencies: '@skidding/launch-editor': 2.2.3 @@ -38500,6 +38497,11 @@ snapshots: bufferutil: 4.0.8 utf-8-validate: 5.0.10 + ws@8.18.1(bufferutil@4.0.8)(utf-8-validate@5.0.10): + optionalDependencies: + bufferutil: 4.0.8 + utf-8-validate: 5.0.10 + ws@8.18.1(bufferutil@4.0.8)(utf-8-validate@6.0.6): optionalDependencies: bufferutil: 4.0.8 From 72dd7d88f4acb21c024d642074fa490761a27570 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Thu, 16 Jul 2026 14:28:23 +0200 Subject: [PATCH 29/42] chore: remove DISABLED_SOURCE_CHAINS --- .../src/modules/tokensList/hooks/useChainsToSelect.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/tokensList/hooks/useChainsToSelect.ts b/apps/cowswap-frontend/src/modules/tokensList/hooks/useChainsToSelect.ts index 2b56f54361e..13fb4c8ce3a 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/hooks/useChainsToSelect.ts +++ b/apps/cowswap-frontend/src/modules/tokensList/hooks/useChainsToSelect.ts @@ -25,8 +25,6 @@ import { sortChainsByDisplayOrder } from '../utils/sortChainsByDisplayOrder' // Re-export for tests and external usage export { createInputChainsState, createOutputChainsState } from '../utils/chainsState' -const DISABLED_SOURCE_CHAINS = new Set([SupportedChainId.SOLANA]) - /** * Returns an array of chains to select in the token selector widget. * The array depends on sell/buy token selection. @@ -69,7 +67,6 @@ export function useChainsToSelect(): ChainsToSelectState | undefined { defaultChainId: selectedTargetChainId, chains: shouldHideNetworkSelector ? [] : sortChainsByDisplayOrder(supportedChains), isLoading: false, - disabledChainIds: DISABLED_SOURCE_CHAINS, } } From cbd9c2ed5e4aa8cde429323cd62a2f68a4b0dfb7 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Thu, 16 Jul 2026 14:43:59 +0200 Subject: [PATCH 30/42] chore: fix solana bridge --- .../hooks/useChainsToSelect.test.ts | 32 +++++++++++++++++++ .../modules/tokensList/utils/chainsState.ts | 6 +++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/apps/cowswap-frontend/src/modules/tokensList/hooks/useChainsToSelect.test.ts b/apps/cowswap-frontend/src/modules/tokensList/hooks/useChainsToSelect.test.ts index f9830e19db4..e549f4f7d55 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/hooks/useChainsToSelect.test.ts +++ b/apps/cowswap-frontend/src/modules/tokensList/hooks/useChainsToSelect.test.ts @@ -182,6 +182,38 @@ describe('useChainsToSelect state builders', () => { expect(state.defaultChainId).toBe(SupportedChainId.LINEA) }) + it('disables all chains except source when source is Solana (bridge-only destination, no bridging out)', () => { + const supportedChains = [ + createChainInfoForTests(SupportedChainId.MAINNET), + createChainInfoForTests(SupportedChainId.BASE), + createChainInfoForTests(SupportedChainId.SOLANA), + ] + // Solana IS a bridge-supported network (you can bridge *to* it), unlike Sepolia + const bridgeChains = [ + createChainInfoForTests(SupportedChainId.MAINNET), + createChainInfoForTests(SupportedChainId.BASE), + createChainInfoForTests(SupportedChainId.SOLANA), + ] + + const state = createOutputChainsState({ + selectedTargetChainId: SupportedChainId.BASE, + chainId: SupportedChainId.SOLANA, // Source is Solana + currentChainInfo: createChainInfoForTests(SupportedChainId.SOLANA), + bridgeSupportedNetworks: bridgeChains, + supportedChains, + isLoading: false, + routesAvailability: DEFAULT_ROUTES_AVAILABILITY, + }) + + // Even though Solana is in the bridge networks, it can't be a bridge *source*, + // so every other chain is disabled and only Solana remains selectable. + expect(state.disabledChainIds?.has(SupportedChainId.SOLANA)).toBeFalsy() + expect(state.disabledChainIds?.has(SupportedChainId.MAINNET)).toBe(true) + expect(state.disabledChainIds?.has(SupportedChainId.BASE)).toBe(true) + // Default falls back to the source since the selected target is disabled + expect(state.defaultChainId).toBe(SupportedChainId.SOLANA) + }) + it('disables all chains except source when routes are unavailable from the source', () => { const supportedChains = [ createChainInfoForTests(SupportedChainId.MAINNET), diff --git a/apps/cowswap-frontend/src/modules/tokensList/utils/chainsState.ts b/apps/cowswap-frontend/src/modules/tokensList/utils/chainsState.ts index 98fd446570c..488bacd13e7 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/utils/chainsState.ts +++ b/apps/cowswap-frontend/src/modules/tokensList/utils/chainsState.ts @@ -4,6 +4,10 @@ import { sortChainsByDisplayOrder } from './sortChainsByDisplayOrder' import { ChainsToSelectState } from '../types' +// Solana is a bridge-only destination: you can bridge *to* it, but not *from* it (no source support yet). +// Treat it as a non-bridgeable source so the destination selector is single-chain, matching Sepolia's behaviour. +const SOURCE_CHAINS_BRIDGE_DISABLED = new Set([SupportedChainId.SOLANA]) + export interface CreateOutputChainsOptions { selectedTargetChainId: SupportedChainId | number chainId: SupportedChainId @@ -64,7 +68,7 @@ export function createOutputChainsState({ const orderedChains = sortChainsByDisplayOrder(chainsWithCurrent) const destinationIds = new Set(filterDestinationChains(bridgeSupportedNetworks)?.map((c) => c.id) ?? []) - const sourceSupported = destinationIds.has(chainId) + const sourceSupported = !SOURCE_CHAINS_BRIDGE_DISABLED.has(chainId) && destinationIds.has(chainId) const baseDisabledChainIds = computeDisabledChainIds( orderedChains, From e35c0b0f989d17136e28380411596507821a1ff9 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Thu, 16 Jul 2026 16:17:28 +0200 Subject: [PATCH 31/42] docs: add Solana limit orders prototype design spec Co-Authored-By: Claude Fable 5 --- .../2026-07-16-solana-limit-orders-design.md | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-16-solana-limit-orders-design.md diff --git a/docs/superpowers/specs/2026-07-16-solana-limit-orders-design.md b/docs/superpowers/specs/2026-07-16-solana-limit-orders-design.md new file mode 100644 index 00000000000..fedc4fb12ae --- /dev/null +++ b/docs/superpowers/specs/2026-07-16-solana-limit-orders-design.md @@ -0,0 +1,131 @@ +# Solana order creation in Limit Orders — prototype design + +Date: 2026-07-16 +Branch: `solana/web-3` +Status: approved + +## Goal + +A working prototype that creates a CoW Protocol order on Solana mainnet from the Limit Orders UI. Limit Orders is used (not Swap) because the backend cannot quote Solana pairs yet — the price is entered manually. Success = the `CreateOrder` transaction lands on-chain and the user gets a Solscan link. + +## Context + +- Settlement program (mainnet): `moosEjJg5mbGRPRU7Vg4AaHZLvbbgknevWR9J1bNgME` + (source: https://github.com/cowprotocol/solana-programs) +- Example create-order tx: `4hy8scaTfLNyJiAPbF47k4YWyWmE2CFfvLj6zkTUibpknEcNfjWWDyCT185qDbRYYLMtdDzGkVtuNXJEE2oXE6JG` +- On this branch, Solana already has: Reown AppKit `SolanaAdapter` wallet connection (behind the + `IS_SOLANA_ENABLED` localStorage flag), `SupportedChainId.SOLANA` in `useWalletInfo()`, + RPC via `useAppKitConnection()`, SPL/Token-2022 balances. There is **no** Solana + transaction-signing code anywhere yet. +- Limit orders placement pipeline: `useTradeFlowContext` (EVM-only context assembly) → + `useHandleOrderPlacement` (dispatch) → `tradeFlow` / `safeBundleFlow` → + `tradingSdk.postLimitOrder`. + +## Decisions (agreed with user) + +| Topic | Decision | +| --- | --- | +| Integration approach | Third placement strategy (`solanaOrderFlow`) next to `tradeFlow`/`safeBundleFlow` | +| Cluster | Mainnet only | +| Native SOL | Not supported — SPL tokens only (no wrap flow) | +| Order params | `valid_to` from the existing deadline setting, `partially_fillable` from the existing settings toggle, `app_data` = 32 zero bytes | +| Post-create UX | Success toast/snackbar with tx signature → Solscan link + order UID; no orders-table integration | +| Recipient | Always the owner (no custom recipient) | + +## On-chain interface (from cowprotocol/solana-programs) + +`CreateOrder` instruction: + +- Data: `[discriminator = 2, ...150 intent bytes]` (151 bytes total). +- Accounts: `[owner (signer, readonly), created_by (signer, writable), order_pda (writable), system_program (readonly)]`. We use `created_by = owner`. +- Intent encoding, 150 bytes, amounts/timestamps little-endian: + `owner(32) ‖ buy_token_account(32) ‖ sell_token_account(32) ‖ sell_amount(u64) ‖ buy_amount(u64) ‖ valid_to(u32) ‖ kind(u8: 0=sell, 1=buy) ‖ partially_fillable(u8) ‖ app_data(32)`. + Token accounts are SPL token accounts (ATAs), **not** mints. The sell token account must be + owned by the intent owner. +- `uid = sha256(intent bytes)`. +- Order PDA: `findProgramAddress(["settlement", uid, "order"], programId)`. +- Settlement state PDA (the SPL delegate): `findProgramAddress(["settlement"], programId)`. + +## 1. Architecture & data flow + +New code lives inside `apps/cowswap-frontend/src/modules/limitOrders`: + +- `services/solanaOrderFlow/` + - `index.ts` — `solanaOrderFlow(ctx)`: builds the tx, sends it via the Reown Solana + provider, awaits confirmation, returns `{ signature, orderUid }`. + - `buildCreateOrderTx.ts` — pure function from resolved params to a `Transaction` + (testable without a wallet): intent encoding, UID hashing, PDA derivation, + instruction construction. + - `const.ts` — program ID, seeds (`"settlement"`, `"order"`), discriminator `2`, + intent size `150`. +- `hooks/useSolanaTradeFlowContext.ts` — Solana counterpart of `useTradeFlowContext` + (which hard-requires a wagmi `walletClient` and returns `null` on Solana). Gathers: + owner pubkey (`useWalletInfo`), `connection` (`useAppKitConnection`), Solana + `walletProvider` (`useAppKitProvider('solana')`), sell/buy token (base58 address, + decimals, `extensions.isToken2022`), sell/buy amounts as `bigint` from + `useLimitOrdersDerivedState`, `validTo` from the deadline setting, + `partiallyFillable` from the limit-orders settings. +- Dispatch in `hooks/useHandleOrderPlacement.ts`: `isSolanaChain(chainId)` → + `solanaOrderFlow(solanaCtx)`, checked before the existing `isSafeBundle` / + `tradeFlow` branches. EVM paths are untouched. + +No new npm packages: `@solana/web3.js@1.98.4` and `@solana/spl-token@0.4.14` are already +in the workspace; SHA-256 comes from `@noble/hashes` (already a transitive dependency; +synchronous, which PDA derivation requires). + +## 2. Transaction building + +One legacy `Transaction`, fee payer = owner, `recentBlockhash` from the connection, +three instructions in order: + +1. **Create buy ATA, idempotent** — `createAssociatedTokenAccountIdempotentInstruction` + for the buy mint, owned by the owner. Ensures the order is settleable. +2. **SPL `approve`** — delegate `sell_amount` on the owner's sell ATA to the settlement + state PDA. Token program chosen per token: `TOKEN_2022_PROGRAM_ID` when the token has + `extensions.isToken2022`, else `TOKEN_PROGRAM_ID`. +3. **`CreateOrder`** — as per the on-chain interface above. + +The buy amount comes from the form, where it is derived from the manually entered rate +(existing `limitRateAtom` machinery — no quote involved). Sending uses the Reown Solana +provider's `sendTransaction(tx, connection)`; confirmation awaited at `confirmed` +commitment. + +**Known limitation (accepted):** SPL token accounts have a single delegate, so a second +order's `approve` overwrites the first order's remaining delegated allowance. Fine for a +prototype; noted for the production design. + +## 3. UI gating & validation + +- Entry path: user enables `IS_SOLANA_ENABLED`, picks Solana in the network selector + (already listed behind the flag), opens Limit orders. Token selection and balances + already work on this branch. +- `modules/tradeFormValidation`: add a Solana path — the form is ready when a Solana + wallet is connected, both amounts are set, and an active rate exists. Quote, approval + (EVM allowance), and permit checks are skipped for Solana. +- EVM-only side effects get `isSolanaChain` early-return guards so they no-op on Solana: + quote fetching (`QuoteObserverUpdater`), initial-price fetching + (`InitialPriceUpdater` — the price is typed manually), permit logic. +- Post-create UX: success snackbar/toast with the tx signature linking to Solscan and the + order UID. No orders-table/history integration. + +## 4. Error handling & testing + +- Wallet rejection → existing "user rejected" handling; send/RPC failures → error toast + with the underlying message. No retries, no priority fees. +- Unit tests for `buildCreateOrderTx` using regression vectors from the Rust repo: + - The sample intent (`owner=[0x11;32]`, `buy=[0x22;32]`, `sell=[0x33;32]`, + `sell_amount=0x0123456789abcdef`, `buy_amount=0xfedcba9876543210`, + `valid_to=0xdeadbeef`, kind=Buy, partially_fillable=true, `app_data=[0x44;32]`) + must encode to the byte string pinned in the Rust test + (`interface/src/data/intent.rs::encoding_regression`) and hash to UID + `7ce7c6a74671090771fa33851387444064aca759ce55b80708723076722f5e00`. + - PDA derivation checked against the known example transaction. + - Instruction data/account layout assertions (discriminator, account order, flags). +- Manual e2e: create a small USDC↔WSOL order on mainnet and verify the tx on Solscan + against the example transaction. + +## Out of scope + +Order history/display, order cancellation, native SOL wrapping, custom recipient, +quotes/market price, real appData hash, versioned transactions/priority fees, devnet +support. From 2562044fc3066dc1a8b267a605d9e19615a81efe Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Thu, 16 Jul 2026 17:12:31 +0200 Subject: [PATCH 32/42] docs: add Solana limit orders implementation plan Co-Authored-By: Claude Fable 5 --- .../plans/2026-07-16-solana-limit-orders.md | 1097 +++++++++++++++++ .../2026-07-16-solana-limit-orders-design.md | 17 +- 2 files changed, 1108 insertions(+), 6 deletions(-) create mode 100644 docs/superpowers/plans/2026-07-16-solana-limit-orders.md diff --git a/docs/superpowers/plans/2026-07-16-solana-limit-orders.md b/docs/superpowers/plans/2026-07-16-solana-limit-orders.md new file mode 100644 index 00000000000..f472d6d813a --- /dev/null +++ b/docs/superpowers/plans/2026-07-16-solana-limit-orders.md @@ -0,0 +1,1097 @@ +# Solana Order Creation in Limit Orders — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** A working prototype that creates a CoW Protocol order on Solana mainnet from the Limit Orders UI, with a manually entered price (no backend quote). + +**Architecture:** A third order-placement strategy inside `modules/limitOrders`: a pure transaction builder (intent encoding → SHA-256 UID → PDA derivation → 3 instructions), a `solanaOrderFlow` service that sends the transaction via the Reown AppKit Solana provider, and a dedicated `SolanaTradeButtons` container rendered by the existing `TradeButtons` when the wallet is on Solana. EVM flows are untouched except two guarded one-liners. + +**Tech Stack:** React + jotai (existing app), `@solana/web3.js@1.98.4`, `@solana/spl-token@0.4.14`, `@noble/hashes@1.4.0` (sync SHA-256), Reown AppKit Solana adapter (already integrated), Jest 30 via Nx. + +**Spec:** `docs/superpowers/specs/2026-07-16-solana-limit-orders-design.md` — read it first. + +## Global Constraints + +- Settlement program ID (mainnet): `moosEjJg5mbGRPRU7Vg4AaHZLvbbgknevWR9J1bNgME`. Mainnet only. +- Intent wire format (150 bytes, little-endian numbers): `owner(32) ‖ buy_token_account(32) ‖ sell_token_account(32) ‖ sell_amount(u64) ‖ buy_amount(u64) ‖ valid_to(u32) ‖ kind(u8: 0=sell,1=buy) ‖ partially_fillable(u8) ‖ app_data(32)`. `CreateOrder` instruction data = `[2, ...intent]` (151 bytes). +- PDA seeds: state PDA `["settlement"]`, order PDA `["settlement", sha256(intent), "order"]`. +- Only these new dependencies, in `apps/cowswap-frontend/package.json`: `@solana/web3.js@1.98.4`, `@solana/spl-token@0.4.14`, `@noble/hashes@1.4.0`. Package manager is **pnpm**. +- Test runner is Jest 30 through Nx: `pnpm exec nx test cowswap-frontend --testPathPatterns=` (note: `--testPathPatterns`, plural — Jest 30 renamed the flag). +- Other Nx targets: `pnpm exec nx typecheck cowswap-frontend`, `pnpm exec nx lint cowswap-frontend`, `pnpm exec nx serve cowswap-frontend`. +- User-facing strings use Lingui macros (`` from `@lingui/react/macro`), matching the surrounding code. +- Prototype scope: SPL tokens only (no native-SOL wrapping), recipient = owner, `app_data` = 32 zero bytes, no orders-table integration, no cancellation. +- Do not modify EVM behavior. The only shared files touched are `TradeButtons` (limit orders) and `useQuoteParams`, both with `isSolanaChain` guards. +- Commit style: conventional commits, e.g. `feat(solana): …` (matches recent history). + +## File Structure + +| File | Responsibility | +| --- | --- | +| `apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/const.ts` | Program ID, seeds, discriminator, sizes, Solscan URL | +| `.../solanaOrderFlow/orderIntent.ts` | Intent type, 150-byte encoding, UID hashing, PDA derivation (pure) | +| `.../solanaOrderFlow/orderIntent.test.ts` | Regression vectors from the Rust repo + mainnet example tx | +| `.../solanaOrderFlow/buildCreateOrderInstructions.ts` | ATA derivation + the 3 instructions (pure) | +| `.../solanaOrderFlow/buildCreateOrderInstructions.test.ts` | Reconstructs the mainnet example tx | +| `.../solanaOrderFlow/types.ts` | `SolanaOrderFlowContext`, `SolanaOrderFlowResult` | +| `.../solanaOrderFlow/index.ts` | `solanaOrderFlow()`: validTo calc, build tx, send, confirm | +| `.../solanaOrderFlow/solanaOrderFlow.test.ts` | Flow test with mocked connection/provider | +| `apps/cowswap-frontend/src/modules/limitOrders/hooks/useSolanaOrderFlowContext.ts` | Assembles the context from wallet/derived state/settings | +| `apps/cowswap-frontend/src/modules/limitOrders/containers/SolanaTradeButtons/index.tsx` | Validation ladder + place-order button + snackbars | +| `apps/cowswap-frontend/src/modules/limitOrders/containers/TradeButtons/index.tsx` (modify) | Render `SolanaTradeButtons` when on Solana | +| `apps/cowswap-frontend/src/modules/tradeQuote/hooks/useQuoteParams.ts` (modify) | Skip quote polling for Solana | +| `apps/cowswap-frontend/package.json` (modify) | New dependencies | + +## Verified test vectors (do not re-derive — these are confirmed against mainnet) + +From mainnet tx `4hy8scaTfLNyJiAPbF47k4YWyWmE2CFfvLj6zkTUibpknEcNfjWWDyCT185qDbRYYLMtdDzGkVtuNXJEE2oXE6JG` (decoded from the raw instruction data via RPC): + +- Owner / fee payer: `54o2XBzBTkP7tmQSLu3Um9oDvLdNVrbMyQxqiYVKALLN` +- Sell token account (owner's WSOL ATA): `cEDc7aAMaCqBX546QWCVxnvfMLUUV3JETQ6qnpeLUaY` +- Buy token account (owner's USDC ATA): `E9xwK5SDXSJLW1A4WRyVT1FzVpt8gREGMVibVW9A8xX5` +- Intent fields: `sell_amount=0`, `buy_amount=10000000`, `valid_to=1783575524`, `kind=1` (buy), `partially_fillable=0`, `app_data=32×0x00` +- Order UID: `f41a85a660c71b6fac30d024d29df733b8b101f931e30fbf8c37f5a0f2d42b2f` +- Order PDA: `AmtUsUoFuGtRpxeQnQEFR83xyeqTrPH8Z4y9twso26Lv` +- Approve delegate (= settlement state PDA): `3PYmNPBdoFBGqtAeopGMS5YvnQnfxh8J9sNS3jjzKhb8` + +From the Rust repo (`interface/src/data/intent.rs`, tests `encoding_regression` / `uid_digest_regression`): the sample intent (fields repeated in Task 2's test code) encodes to a pinned byte string and hashes to UID `7ce7c6a74671090771fa33851387444064aca759ce55b80708723076722f5e00`. + +--- + +### Task 1: Add Solana dependencies to cowswap-frontend + +**Files:** +- Modify: `apps/cowswap-frontend/package.json` + +**Interfaces:** +- Consumes: nothing +- Produces: `@solana/web3.js`, `@solana/spl-token`, `@noble/hashes` importable from `apps/cowswap-frontend` sources (Tasks 2–4 import them) + +- [ ] **Step 1: Add dependencies** + +In `apps/cowswap-frontend/package.json`, add to the `dependencies` object (keep alphabetical order; `@reown/appkit` etc. are already there — `@solana/web3.js` and `@solana/spl-token` versions match the ones already used by `libs/wallet` and `libs/balances-and-allowances`): + +```json +"@noble/hashes": "1.4.0", +"@solana/spl-token": "0.4.14", +"@solana/web3.js": "1.98.4", +``` + +- [ ] **Step 2: Install** + +Run: `pnpm install` +Expected: lockfile updates (or is already satisfied — these exact versions are in `pnpm-lock.yaml` as transitive deps), exit code 0. + +- [ ] **Step 3: Commit** + +```bash +git add apps/cowswap-frontend/package.json pnpm-lock.yaml +git commit -m "chore(solana): add web3.js, spl-token and noble/hashes to cowswap-frontend" +``` + +--- + +### Task 2: Order intent encoding, UID and PDA derivation + +**Files:** +- Create: `apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/const.ts` +- Create: `apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/orderIntent.ts` +- Test: `apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/orderIntent.test.ts` + +**Interfaces:** +- Consumes: `@solana/web3.js` (`PublicKey`), `@noble/hashes/sha256`, `OrderKind` from `@cowprotocol/cow-sdk` (string enum: `OrderKind.SELL = 'sell'`, `OrderKind.BUY = 'buy'`) +- Produces (used by Task 3): + - `interface SolanaOrderIntent { owner: PublicKey; buyTokenAccount: PublicKey; sellTokenAccount: PublicKey; sellAmount: bigint; buyAmount: bigint; validTo: number; kind: OrderKind; partiallyFillable: boolean; appData: Uint8Array }` + - `encodeOrderIntent(intent: SolanaOrderIntent): Uint8Array` (150 bytes) + - `computeOrderUid(intentBytes: Uint8Array): Uint8Array` (32 bytes) + - `findStatePda(): PublicKey` + - `findOrderPda(orderUid: Uint8Array): PublicKey` + - Constants: `SOLANA_SETTLEMENT_PROGRAM_ID: PublicKey`, `CREATE_ORDER_DISCRIMINATOR = 2`, `ORDER_INTENT_SIZE = 150`, `SOLANA_APP_DATA` (32 zero bytes), `SOLSCAN_TX_URL` + +- [ ] **Step 1: Write the constants file** + +`apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/const.ts`: + +```ts +import { PublicKey } from '@solana/web3.js' + +// CoW Protocol settlement program on Solana mainnet +// https://github.com/cowprotocol/solana-programs +export const SOLANA_SETTLEMENT_PROGRAM_ID = new PublicKey('moosEjJg5mbGRPRU7Vg4AaHZLvbbgknevWR9J1bNgME') + +// PDA seed scheme: every PDA starts with SETTLEMENT_SEED; order PDAs append the order UID and ORDER_SEED +export const SETTLEMENT_SEED = new TextEncoder().encode('settlement') +export const ORDER_SEED = new TextEncoder().encode('order') + +export const CREATE_ORDER_DISCRIMINATOR = 2 +export const ORDER_INTENT_SIZE = 150 + +// Opaque 32 bytes; the settlement program does not interpret them. Zeroed for the prototype. +export const SOLANA_APP_DATA = new Uint8Array(32) + +export const SOLSCAN_TX_URL = 'https://solscan.io/tx/' +``` + +- [ ] **Step 2: Write the failing test** + +`apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/orderIntent.test.ts`: + +```ts +import { OrderKind } from '@cowprotocol/cow-sdk' + +import { PublicKey } from '@solana/web3.js' + +import { computeOrderUid, encodeOrderIntent, findOrderPda, findStatePda, SolanaOrderIntent } from './orderIntent' + +function toHex(bytes: Uint8Array): string { + return Array.from(bytes) + .map((b) => b.toString(16).padStart(2, '0')) + .join('') +} + +// Mirrors `sample_intent(OrderKind::Buy, true)` from cowprotocol/solana-programs +// interface/src/data/intent.rs (tests `encoding_regression` and `uid_digest_regression`) +const RUST_SAMPLE_INTENT: SolanaOrderIntent = { + owner: new PublicKey(new Uint8Array(32).fill(0x11)), + buyTokenAccount: new PublicKey(new Uint8Array(32).fill(0x22)), + sellTokenAccount: new PublicKey(new Uint8Array(32).fill(0x33)), + sellAmount: 0x0123456789abcdefn, + buyAmount: 0xfedcba9876543210n, + validTo: 0xdeadbeef, + kind: OrderKind.BUY, + partiallyFillable: true, + appData: new Uint8Array(32).fill(0x44), +} + +const RUST_SAMPLE_ENCODING = + '11'.repeat(32) + // owner + '22'.repeat(32) + // buy_token_account + '33'.repeat(32) + // sell_token_account + 'efcdab8967452301' + // sell_amount LE + '1032547698badcfe' + // buy_amount LE + 'efbeadde' + // valid_to LE + '01' + // kind: buy + '01' + // partially_fillable: true + '44'.repeat(32) // app_data + +const RUST_SAMPLE_UID = '7ce7c6a74671090771fa33851387444064aca759ce55b80708723076722f5e00' + +// Decoded from the raw CreateOrder instruction of mainnet tx +// 4hy8scaTfLNyJiAPbF47k4YWyWmE2CFfvLj6zkTUibpknEcNfjWWDyCT185qDbRYYLMtdDzGkVtuNXJEE2oXE6JG +const MAINNET_INTENT: SolanaOrderIntent = { + owner: new PublicKey('54o2XBzBTkP7tmQSLu3Um9oDvLdNVrbMyQxqiYVKALLN'), + buyTokenAccount: new PublicKey('E9xwK5SDXSJLW1A4WRyVT1FzVpt8gREGMVibVW9A8xX5'), + sellTokenAccount: new PublicKey('cEDc7aAMaCqBX546QWCVxnvfMLUUV3JETQ6qnpeLUaY'), + sellAmount: 0n, + buyAmount: 10_000_000n, + validTo: 1783575524, + kind: OrderKind.BUY, + partiallyFillable: false, + appData: new Uint8Array(32), +} +const MAINNET_UID = 'f41a85a660c71b6fac30d024d29df733b8b101f931e30fbf8c37f5a0f2d42b2f' + +describe('encodeOrderIntent', () => { + it('produces 150 bytes', () => { + expect(encodeOrderIntent(RUST_SAMPLE_INTENT)).toHaveLength(150) + }) + + it('matches the Rust encoding_regression vector', () => { + expect(toHex(encodeOrderIntent(RUST_SAMPLE_INTENT))).toBe(RUST_SAMPLE_ENCODING) + }) + + it('encodes a sell fill-or-kill order with zero flag bytes', () => { + const encoded = encodeOrderIntent({ ...RUST_SAMPLE_INTENT, kind: OrderKind.SELL, partiallyFillable: false }) + expect(encoded[116]).toBe(0) + expect(encoded[117]).toBe(0) + }) +}) + +describe('computeOrderUid', () => { + it('matches the Rust uid_digest_regression vector', () => { + expect(toHex(computeOrderUid(encodeOrderIntent(RUST_SAMPLE_INTENT)))).toBe(RUST_SAMPLE_UID) + }) + + it('matches the mainnet example order UID', () => { + expect(toHex(computeOrderUid(encodeOrderIntent(MAINNET_INTENT)))).toBe(MAINNET_UID) + }) +}) + +describe('PDA derivation', () => { + it('derives the settlement state PDA seen as the approve delegate on mainnet', () => { + expect(findStatePda().toBase58()).toBe('3PYmNPBdoFBGqtAeopGMS5YvnQnfxh8J9sNS3jjzKhb8') + }) + + it('derives the order PDA of the mainnet example order', () => { + const uid = computeOrderUid(encodeOrderIntent(MAINNET_INTENT)) + expect(findOrderPda(uid).toBase58()).toBe('AmtUsUoFuGtRpxeQnQEFR83xyeqTrPH8Z4y9twso26Lv') + }) +}) +``` + +- [ ] **Step 3: Run test to verify it fails** + +Run: `pnpm exec nx test cowswap-frontend --testPathPatterns=solanaOrderFlow` +Expected: FAIL — cannot find module `./orderIntent`. + +- [ ] **Step 4: Write the implementation** + +`apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/orderIntent.ts`: + +```ts +import { OrderKind } from '@cowprotocol/cow-sdk' + +import { sha256 } from '@noble/hashes/sha256' +import { PublicKey } from '@solana/web3.js' + +import { ORDER_INTENT_SIZE, ORDER_SEED, SETTLEMENT_SEED, SOLANA_SETTLEMENT_PROGRAM_ID } from './const' + +export interface SolanaOrderIntent { + owner: PublicKey + /** SPL token account (not mint) receiving the buy-side proceeds */ + buyTokenAccount: PublicKey + /** SPL token account (not mint) the sell funds are pulled from; must be owned by `owner` */ + sellTokenAccount: PublicKey + sellAmount: bigint + buyAmount: bigint + /** Unix timestamp (seconds) after which the order expires */ + validTo: number + kind: OrderKind + partiallyFillable: boolean + /** Opaque 32 bytes */ + appData: Uint8Array +} + +/** + * Canonical 150-byte encoding, the wire format and the UID preimage. + * Layout: owner(32) ‖ buy(32) ‖ sell(32) ‖ sellAmount(u64 LE) ‖ buyAmount(u64 LE) + * ‖ validTo(u32 LE) ‖ kind(u8) ‖ partiallyFillable(u8) ‖ appData(32) + */ +export function encodeOrderIntent(intent: SolanaOrderIntent): Uint8Array { + const bytes = new Uint8Array(ORDER_INTENT_SIZE) + const view = new DataView(bytes.buffer) + + bytes.set(intent.owner.toBytes(), 0) + bytes.set(intent.buyTokenAccount.toBytes(), 32) + bytes.set(intent.sellTokenAccount.toBytes(), 64) + view.setBigUint64(96, intent.sellAmount, true) + view.setBigUint64(104, intent.buyAmount, true) + view.setUint32(112, intent.validTo, true) + bytes[116] = intent.kind === OrderKind.BUY ? 1 : 0 + bytes[117] = intent.partiallyFillable ? 1 : 0 + bytes.set(intent.appData, 118) + + return bytes +} + +/** Order UID = SHA-256 of the canonical intent bytes; also the middle seed of the order PDA */ +export function computeOrderUid(intentBytes: Uint8Array): Uint8Array { + return sha256(intentBytes) +} + +/** Settlement state PDA: the SPL delegate that pulls sell funds at execution time */ +export function findStatePda(): PublicKey { + return PublicKey.findProgramAddressSync([SETTLEMENT_SEED], SOLANA_SETTLEMENT_PROGRAM_ID)[0] +} + +export function findOrderPda(orderUid: Uint8Array): PublicKey { + return PublicKey.findProgramAddressSync([SETTLEMENT_SEED, orderUid, ORDER_SEED], SOLANA_SETTLEMENT_PROGRAM_ID)[0] +} +``` + +- [ ] **Step 5: Run test to verify it passes** + +Run: `pnpm exec nx test cowswap-frontend --testPathPatterns=solanaOrderFlow` +Expected: PASS (7 tests). If the UID tests pass but PDA tests fail, the seeds are wrong; if encoding fails at offset 96+, check little-endian writes. + +- [ ] **Step 6: Commit** + +```bash +git add apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow +git commit -m "feat(solana): order intent encoding, UID and PDA derivation" +``` + +--- + +### Task 3: Build the create-order instructions + +**Files:** +- Create: `apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/buildCreateOrderInstructions.ts` +- Test: `apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/buildCreateOrderInstructions.test.ts` + +**Interfaces:** +- Consumes: Task 2's `encodeOrderIntent`, `computeOrderUid`, `findOrderPda`, `findStatePda`, constants +- Produces (used by Task 4): + - `interface SolanaTokenParams { address: string; isToken2022: boolean }` (address = base58 mint) + - `interface BuildCreateOrderParams { account: string; sellToken: SolanaTokenParams; buyToken: SolanaTokenParams; sellAmount: bigint; buyAmount: bigint; validTo: number; kind: OrderKind; partiallyFillable: boolean }` + - `buildCreateOrderInstructions(params: BuildCreateOrderParams): { instructions: TransactionInstruction[]; orderUid: Uint8Array; orderPda: PublicKey; sellTokenAccount: PublicKey; buyTokenAccount: PublicKey }` + +- [ ] **Step 1: Write the failing test** + +`apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/buildCreateOrderInstructions.test.ts`: + +```ts +import { OrderKind } from '@cowprotocol/cow-sdk' + +import { TOKEN_PROGRAM_ID } from '@solana/spl-token' +import { SystemProgram } from '@solana/web3.js' + +import { buildCreateOrderInstructions } from './buildCreateOrderInstructions' +import { SOLANA_SETTLEMENT_PROGRAM_ID } from './const' + +// Reconstructs the order of mainnet tx +// 4hy8scaTfLNyJiAPbF47k4YWyWmE2CFfvLj6zkTUibpknEcNfjWWDyCT185qDbRYYLMtdDzGkVtuNXJEE2oXE6JG +// (buy 10 USDC paying with WSOL, fill-or-kill) +const PARAMS = { + account: '54o2XBzBTkP7tmQSLu3Um9oDvLdNVrbMyQxqiYVKALLN', + sellToken: { address: 'So11111111111111111111111111111111111111112', isToken2022: false }, // WSOL + buyToken: { address: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', isToken2022: false }, // USDC + sellAmount: 0n, + buyAmount: 10_000_000n, + validTo: 1783575524, + kind: OrderKind.BUY, + partiallyFillable: false, +} + +describe('buildCreateOrderInstructions', () => { + it('derives the same accounts and instructions as the mainnet example tx', () => { + const { instructions, orderPda, sellTokenAccount, buyTokenAccount } = buildCreateOrderInstructions(PARAMS) + + // ATAs of the owner, as seen on-chain + expect(sellTokenAccount.toBase58()).toBe('cEDc7aAMaCqBX546QWCVxnvfMLUUV3JETQ6qnpeLUaY') + expect(buyTokenAccount.toBase58()).toBe('E9xwK5SDXSJLW1A4WRyVT1FzVpt8gREGMVibVW9A8xX5') + expect(orderPda.toBase58()).toBe('AmtUsUoFuGtRpxeQnQEFR83xyeqTrPH8Z4y9twso26Lv') + + expect(instructions).toHaveLength(3) + const [createBuyAta, approve, createOrder] = instructions + + // buy ATA idempotent creation: account 1 is the ATA being created + expect(createBuyAta.keys[1].pubkey.equals(buyTokenAccount)).toBe(true) + + // approve: keys are [source token account, delegate, owner]; delegate is the settlement state PDA + expect(approve.programId.equals(TOKEN_PROGRAM_ID)).toBe(true) + expect(approve.keys[0].pubkey.equals(sellTokenAccount)).toBe(true) + expect(approve.keys[1].pubkey.toBase58()).toBe('3PYmNPBdoFBGqtAeopGMS5YvnQnfxh8J9sNS3jjzKhb8') + + // createOrder data: [discriminator=2, ...150 intent bytes] + expect(createOrder.programId.equals(SOLANA_SETTLEMENT_PROGRAM_ID)).toBe(true) + expect(createOrder.data).toHaveLength(151) + expect(createOrder.data[0]).toBe(2) + + // createOrder accounts: owner (signer, ro), created_by = owner (signer, writable), + // order PDA (writable), system program (ro) + expect(createOrder.keys).toHaveLength(4) + expect(createOrder.keys[0].pubkey.toBase58()).toBe(PARAMS.account) + expect(createOrder.keys[0].isSigner).toBe(true) + expect(createOrder.keys[0].isWritable).toBe(false) + expect(createOrder.keys[1].pubkey.toBase58()).toBe(PARAMS.account) + expect(createOrder.keys[1].isSigner).toBe(true) + expect(createOrder.keys[1].isWritable).toBe(true) + expect(createOrder.keys[2].pubkey.equals(orderPda)).toBe(true) + expect(createOrder.keys[2].isSigner).toBe(false) + expect(createOrder.keys[2].isWritable).toBe(true) + expect(createOrder.keys[3].pubkey.equals(SystemProgram.programId)).toBe(true) + expect(createOrder.keys[3].isSigner).toBe(false) + expect(createOrder.keys[3].isWritable).toBe(false) + }) +}) +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `pnpm exec nx test cowswap-frontend --testPathPatterns=buildCreateOrderInstructions` +Expected: FAIL — cannot find module `./buildCreateOrderInstructions`. + +- [ ] **Step 3: Write the implementation** + +`apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/buildCreateOrderInstructions.ts`: + +```ts +import { OrderKind } from '@cowprotocol/cow-sdk' + +import { + createApproveInstruction, + createAssociatedTokenAccountIdempotentInstruction, + getAssociatedTokenAddressSync, + TOKEN_2022_PROGRAM_ID, + TOKEN_PROGRAM_ID, +} from '@solana/spl-token' +import { PublicKey, SystemProgram, TransactionInstruction } from '@solana/web3.js' + +import { CREATE_ORDER_DISCRIMINATOR, SOLANA_APP_DATA, SOLANA_SETTLEMENT_PROGRAM_ID } from './const' +import { computeOrderUid, encodeOrderIntent, findOrderPda, findStatePda } from './orderIntent' + +export interface SolanaTokenParams { + /** base58 mint address */ + address: string + /** Token-2022 mints live under a different token program (see TOKEN_2022_TAG in the token lists) */ + isToken2022: boolean +} + +export interface BuildCreateOrderParams { + /** base58 wallet address: order owner, rent payer and fee payer */ + account: string + sellToken: SolanaTokenParams + buyToken: SolanaTokenParams + sellAmount: bigint + buyAmount: bigint + /** Unix timestamp (seconds) */ + validTo: number + kind: OrderKind + partiallyFillable: boolean +} + +export interface CreateOrderInstructions { + instructions: TransactionInstruction[] + orderUid: Uint8Array + orderPda: PublicKey + sellTokenAccount: PublicKey + buyTokenAccount: PublicKey +} + +export function buildCreateOrderInstructions(params: BuildCreateOrderParams): CreateOrderInstructions { + const owner = new PublicKey(params.account) + const sellMint = new PublicKey(params.sellToken.address) + const buyMint = new PublicKey(params.buyToken.address) + const sellTokenProgram = params.sellToken.isToken2022 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID + const buyTokenProgram = params.buyToken.isToken2022 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID + + const sellTokenAccount = getAssociatedTokenAddressSync(sellMint, owner, false, sellTokenProgram) + const buyTokenAccount = getAssociatedTokenAddressSync(buyMint, owner, false, buyTokenProgram) + + // The buy-side token account must exist for the order to be settleable + const createBuyAta = createAssociatedTokenAccountIdempotentInstruction( + owner, + buyTokenAccount, + owner, + buyMint, + buyTokenProgram, + ) + + // The settlement state PDA pulls the sell funds via SPL delegation at execution time. + // NOTE (accepted prototype limitation): SPL token accounts have a single delegate, + // so a second order on the same sell token overwrites the previous delegated amount. + const approve = createApproveInstruction( + sellTokenAccount, + findStatePda(), + owner, + params.sellAmount, + [], + sellTokenProgram, + ) + + const intentBytes = encodeOrderIntent({ + owner, + buyTokenAccount, + sellTokenAccount, + sellAmount: params.sellAmount, + buyAmount: params.buyAmount, + validTo: params.validTo, + kind: params.kind, + partiallyFillable: params.partiallyFillable, + appData: SOLANA_APP_DATA, + }) + const orderUid = computeOrderUid(intentBytes) + const orderPda = findOrderPda(orderUid) + + const data = Buffer.alloc(1 + intentBytes.length) + data[0] = CREATE_ORDER_DISCRIMINATOR + data.set(intentBytes, 1) + + const createOrder = new TransactionInstruction({ + programId: SOLANA_SETTLEMENT_PROGRAM_ID, + keys: [ + // owner: authenticates the order + { pubkey: owner, isSigner: true, isWritable: false }, + // created_by: funds the order PDA's rent (same as owner here) + { pubkey: owner, isSigner: true, isWritable: true }, + { pubkey: orderPda, isSigner: false, isWritable: true }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + ], + data, + }) + + return { + instructions: [createBuyAta, approve, createOrder], + orderUid, + orderPda, + sellTokenAccount, + buyTokenAccount, + } +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `pnpm exec nx test cowswap-frontend --testPathPatterns=buildCreateOrderInstructions` +Expected: PASS. (If only the `buyTokenAccount` assertion fails, verify with `getAssociatedTokenAddressSync` inputs — the mainnet example's buy account is the owner's canonical USDC ATA.) + +- [ ] **Step 5: Commit** + +```bash +git add apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow +git commit -m "feat(solana): create-order instruction building" +``` + +--- + +### Task 4: The solanaOrderFlow service + +**Files:** +- Create: `apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/types.ts` +- Create: `apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/index.ts` +- Test: `apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/solanaOrderFlow.test.ts` + +**Interfaces:** +- Consumes: Task 3's `buildCreateOrderInstructions`, `BuildCreateOrderParams` +- Produces (used by Tasks 5–6): + - `interface SolanaOrderFlowContext extends Omit { connection: Connection; walletProvider: SolanaProvider; customDeadlineTimestamp: number | null; deadlineMilliseconds: number }` + - `interface SolanaOrderFlowResult { signature: string; orderUid: string; orderPda: string }` + - `solanaOrderFlow(ctx: SolanaOrderFlowContext): Promise` + +- [ ] **Step 1: Write the types** + +`apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/types.ts`: + +```ts +import type { Provider as SolanaProvider } from '@reown/appkit-adapter-solana/react' +import type { Connection } from '@solana/web3.js' + +import type { BuildCreateOrderParams } from './buildCreateOrderInstructions' + +// validTo is intentionally not part of the context: like the EVM flow, it is +// computed just before sending so the deadline is relative to the send time. +export interface SolanaOrderFlowContext extends Omit { + connection: Connection + walletProvider: SolanaProvider + /** Limit-orders settings: fixed deadline (unix seconds) when the user picked a custom date */ + customDeadlineTimestamp: number | null + /** Limit-orders settings: relative deadline duration */ + deadlineMilliseconds: number +} + +export interface SolanaOrderFlowResult { + signature: string + /** hex-encoded 32-byte order UID */ + orderUid: string + /** base58 order PDA address */ + orderPda: string +} +``` + +Note: if `Provider` is not exported from `@reown/appkit-adapter-solana/react`, import it from `@reown/appkit-adapter-solana` instead (both are documented Reown entry points). + +- [ ] **Step 2: Write the failing test** + +`apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/solanaOrderFlow.test.ts`: + +```ts +import { OrderKind } from '@cowprotocol/cow-sdk' + +import type { Provider as SolanaProvider } from '@reown/appkit-adapter-solana/react' +import type { Connection, Transaction } from '@solana/web3.js' + +import { solanaOrderFlow } from './index' +import type { SolanaOrderFlowContext } from './types' + +const OWNER = '54o2XBzBTkP7tmQSLu3Um9oDvLdNVrbMyQxqiYVKALLN' +// Any well-formed base58 32-byte value works as a fake blockhash +const BLOCKHASH = 'EkSnNWid2cvwEVnVx9aBqawnmiCNiDgp3gUdkDPTKN1N' + +function buildContext(): SolanaOrderFlowContext & { + connection: { getLatestBlockhash: jest.Mock; confirmTransaction: jest.Mock } + walletProvider: { sendTransaction: jest.Mock } +} { + const connection = { + getLatestBlockhash: jest.fn().mockResolvedValue({ blockhash: BLOCKHASH, lastValidBlockHeight: 100 }), + confirmTransaction: jest.fn().mockResolvedValue({ value: { err: null } }), + } + const walletProvider = { + sendTransaction: jest.fn().mockResolvedValue('mockSignature'), + } + + return { + account: OWNER, + sellToken: { address: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', isToken2022: false }, + buyToken: { address: 'So11111111111111111111111111111111111111112', isToken2022: false }, + sellAmount: 1_000_000n, + buyAmount: 5_000_000n, + kind: OrderKind.SELL, + partiallyFillable: true, + customDeadlineTimestamp: null, + deadlineMilliseconds: 3_600_000, + connection: connection as unknown as Connection, + walletProvider: walletProvider as unknown as SolanaProvider, + } as never +} + +describe('solanaOrderFlow', () => { + it('sends a 3-instruction transaction and returns signature, UID and PDA', async () => { + const ctx = buildContext() + + const result = await solanaOrderFlow(ctx) + + expect(result.signature).toBe('mockSignature') + expect(result.orderUid).toMatch(/^[0-9a-f]{64}$/) + expect(result.orderPda).toBeTruthy() + + expect(ctx.walletProvider.sendTransaction).toHaveBeenCalledTimes(1) + const [tx] = ctx.walletProvider.sendTransaction.mock.calls[0] as [Transaction] + expect(tx.instructions).toHaveLength(3) + expect(tx.feePayer?.toBase58()).toBe(OWNER) + expect(tx.recentBlockhash).toBe(BLOCKHASH) + + expect(ctx.connection.confirmTransaction).toHaveBeenCalledWith( + { signature: 'mockSignature', blockhash: BLOCKHASH, lastValidBlockHeight: 100 }, + 'confirmed', + ) + }) + + it('uses the custom deadline timestamp as validTo when set', async () => { + const ctx = buildContext() + ctx.customDeadlineTimestamp = 1893456000 + + await solanaOrderFlow(ctx) + + const [tx] = ctx.walletProvider.sendTransaction.mock.calls[0] as [Transaction] + const createOrderData = tx.instructions[2].data + // valid_to is a u32 LE at intent offset 112, i.e. data offset 113 (after the discriminator) + expect(createOrderData.readUInt32LE(113)).toBe(1893456000) + }) + + it('throws when on-chain confirmation reports an error', async () => { + const ctx = buildContext() + ctx.connection.confirmTransaction.mockResolvedValue({ value: { err: { InstructionError: [2, 'Custom'] } } }) + + await expect(solanaOrderFlow(ctx)).rejects.toThrow('Solana transaction failed') + }) +}) +``` + +- [ ] **Step 3: Run test to verify it fails** + +Run: `pnpm exec nx test cowswap-frontend --testPathPatterns=solanaOrderFlow.test` +Expected: FAIL — `solanaOrderFlow` is not exported from `./index`. + +- [ ] **Step 4: Write the implementation** + +`apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/index.ts`: + +```ts +import { PublicKey, Transaction } from '@solana/web3.js' + +import { buildCreateOrderInstructions } from './buildCreateOrderInstructions' +import type { SolanaOrderFlowContext, SolanaOrderFlowResult } from './types' + +export type { SolanaOrderFlowContext, SolanaOrderFlowResult } from './types' + +export async function solanaOrderFlow(ctx: SolanaOrderFlowContext): Promise { + const { connection, walletProvider, customDeadlineTimestamp, deadlineMilliseconds, ...orderParams } = ctx + + // Deadline is relative to the send time, mirroring the EVM flow where + // validTo is calculated just before signing + const validTo = customDeadlineTimestamp ?? Math.floor((Date.now() + deadlineMilliseconds) / 1000) + + const { instructions, orderUid, orderPda } = buildCreateOrderInstructions({ ...orderParams, validTo }) + + const transaction = new Transaction().add(...instructions) + transaction.feePayer = new PublicKey(ctx.account) + + const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash() + transaction.recentBlockhash = blockhash + + const signature = await walletProvider.sendTransaction(transaction, connection) + + const confirmation = await connection.confirmTransaction({ signature, blockhash, lastValidBlockHeight }, 'confirmed') + + if (confirmation.value.err) { + throw new Error(`Solana transaction failed: ${JSON.stringify(confirmation.value.err)}`) + } + + return { + signature, + orderUid: uint8ArrayToHex(orderUid), + orderPda: orderPda.toBase58(), + } +} + +function uint8ArrayToHex(bytes: Uint8Array): string { + return Array.from(bytes) + .map((b) => b.toString(16).padStart(2, '0')) + .join('') +} +``` + +- [ ] **Step 5: Run all solanaOrderFlow tests** + +Run: `pnpm exec nx test cowswap-frontend --testPathPatterns=solanaOrderFlow` +Expected: PASS (all three test files). + +- [ ] **Step 6: Commit** + +```bash +git add apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow +git commit -m "feat(solana): solanaOrderFlow service sending the create-order tx" +``` + +--- + +### Task 5: Context hook — useSolanaOrderFlowContext + +**Files:** +- Create: `apps/cowswap-frontend/src/modules/limitOrders/hooks/useSolanaOrderFlowContext.ts` + +**Interfaces:** +- Consumes: `useWalletInfo` (`@cowprotocol/wallet`; on Solana, `account` is the base58 address and `chainId === SupportedChainId.SOLANA`), `useAppKitConnection` (`@reown/appkit-adapter-solana/react`), `useAppKitProvider` (`@reown/appkit/react`), `useLimitOrdersDerivedState`, `limitOrdersSettingsAtom`, `getIsToken2022` (`@cowprotocol/common-const`), `getCurrencyAddress` (`@cowprotocol/common-utils`), `isSolanaChain` (`@cowprotocol/cow-sdk`), Task 4's `SolanaOrderFlowContext` +- Produces (used by Task 6): `useSolanaOrderFlowContext(): SolanaOrderFlowContext | null` — null while not on Solana or the form/wallet is incomplete + +No unit test for this hook (pure glue over already-tested pieces; AppKit hooks would need heavy mocking). It is exercised by the Task 8 smoke test. + +- [ ] **Step 1: Write the hook** + +`apps/cowswap-frontend/src/modules/limitOrders/hooks/useSolanaOrderFlowContext.ts`: + +```ts +import { useAtomValue } from 'jotai' + +import { getIsToken2022 } from '@cowprotocol/common-const' +import { getCurrencyAddress } from '@cowprotocol/common-utils' +import { isSolanaChain } from '@cowprotocol/cow-sdk' +import { useWalletInfo } from '@cowprotocol/wallet' + +import type { Provider as SolanaProvider } from '@reown/appkit-adapter-solana/react' +import { useAppKitConnection } from '@reown/appkit-adapter-solana/react' +import { useAppKitProvider } from '@reown/appkit/react' + +import { SolanaOrderFlowContext } from 'modules/limitOrders/services/solanaOrderFlow' +import { limitOrdersSettingsAtom } from 'modules/limitOrders/state/limitOrdersSettingsAtom' + +import { useSafeMemo } from 'common/hooks/useSafeMemo' + +import { useLimitOrdersDerivedState } from './useLimitOrdersDerivedState' + +export function useSolanaOrderFlowContext(): SolanaOrderFlowContext | null { + const { chainId, account } = useWalletInfo() + const { connection } = useAppKitConnection() + const { walletProvider } = useAppKitProvider('solana') + const { customDeadlineTimestamp, deadlineMilliseconds, partialFillsEnabled } = useAtomValue(limitOrdersSettingsAtom) + const { inputCurrency, outputCurrency, inputCurrencyAmount, outputCurrencyAmount, orderKind } = + useLimitOrdersDerivedState() + + return useSafeMemo(() => { + if (!isSolanaChain(chainId) || !account || !connection || !walletProvider) return null + if (!inputCurrency || !outputCurrency || !inputCurrencyAmount || !outputCurrencyAmount) return null + + return { + account, + connection, + walletProvider, + sellToken: { + address: getCurrencyAddress(inputCurrency), + isToken2022: getIsToken2022(inputCurrency as { tags?: string[] }), + }, + buyToken: { + address: getCurrencyAddress(outputCurrency), + isToken2022: getIsToken2022(outputCurrency as { tags?: string[] }), + }, + sellAmount: BigInt(inputCurrencyAmount.quotient.toString()), + buyAmount: BigInt(outputCurrencyAmount.quotient.toString()), + kind: orderKind, + partiallyFillable: partialFillsEnabled, + customDeadlineTimestamp, + deadlineMilliseconds, + } + }, [ + chainId, + account, + connection, + walletProvider, + inputCurrency, + outputCurrency, + inputCurrencyAmount, + outputCurrencyAmount, + orderKind, + partialFillsEnabled, + customDeadlineTimestamp, + deadlineMilliseconds, + ]) +} +``` + +- [ ] **Step 2: Typecheck** + +Run: `pnpm exec nx typecheck cowswap-frontend` +Expected: exit 0. If `useAppKitConnection`'s `connection` is typed as possibly `undefined`, the null guard already covers it. If `getIsToken2022` complains about the cast, keep the cast — `TokenWithLogo` carries `tags: string[]` at runtime. + +- [ ] **Step 3: Commit** + +```bash +git add apps/cowswap-frontend/src/modules/limitOrders/hooks/useSolanaOrderFlowContext.ts +git commit -m "feat(solana): limit orders Solana flow context hook" +``` + +--- + +### Task 6: SolanaTradeButtons container + TradeButtons wiring + +**Files:** +- Create: `apps/cowswap-frontend/src/modules/limitOrders/containers/SolanaTradeButtons/index.tsx` +- Modify: `apps/cowswap-frontend/src/modules/limitOrders/containers/TradeButtons/index.tsx` + +**Interfaces:** +- Consumes: Task 5's `useSolanaOrderFlowContext`, Task 4's `solanaOrderFlow`, `TradeFormBlankButton` (exported from `modules/tradeFormValidation`; props: `{ id?, children, disabled?, loading?, onClick? }`), `useAddSnackbar` (`@cowprotocol/snackbars`; takes `{ id: string; icon?: 'success' | 'alert'; content: ReactNode }`), `ExternalLink` (`@cowprotocol/ui`), `isRejectRequestProviderError` + `isFractionFalsy` (`@cowprotocol/common-utils`), `getSwapErrorMessage` (`common/utils/getSwapErrorMessage`), `limitRateAtom` +- Produces: `SolanaTradeButtons(): ReactNode`, rendered by `TradeButtons` when `isSolanaChain(chainId)` + +- [ ] **Step 1: Write the container** + +`apps/cowswap-frontend/src/modules/limitOrders/containers/SolanaTradeButtons/index.tsx`: + +```tsx +import { useAtomValue } from 'jotai' +import { ReactNode, useCallback, useState } from 'react' + +import { isFractionFalsy, isRejectRequestProviderError } from '@cowprotocol/common-utils' +import { useAddSnackbar } from '@cowprotocol/snackbars' +import { ExternalLink } from '@cowprotocol/ui' +import { useWalletInfo } from '@cowprotocol/wallet' + +import { Trans } from '@lingui/react/macro' + +import { useLimitOrdersDerivedState } from 'modules/limitOrders/hooks/useLimitOrdersDerivedState' +import { useSolanaOrderFlowContext } from 'modules/limitOrders/hooks/useSolanaOrderFlowContext' +import { solanaOrderFlow } from 'modules/limitOrders/services/solanaOrderFlow' +import { SOLSCAN_TX_URL } from 'modules/limitOrders/services/solanaOrderFlow/const' +import { limitRateAtom } from 'modules/limitOrders/state/limitRateAtom' +import { TradeFormBlankButton } from 'modules/tradeFormValidation' + +import { getSwapErrorMessage } from 'common/utils/getSwapErrorMessage' + +/** + * Prototype placement flow for Solana limit orders: no quote, no confirm modal. + * The button sends the create-order transaction directly and reports the + * result in a snackbar with a Solscan link. + */ +// eslint-disable-next-line max-lines-per-function +export function SolanaTradeButtons(): ReactNode { + const { account } = useWalletInfo() + const solanaContext = useSolanaOrderFlowContext() + const { inputCurrency, outputCurrency, inputCurrencyAmount, outputCurrencyAmount, inputCurrencyBalance } = + useLimitOrdersDerivedState() + const { activeRate } = useAtomValue(limitRateAtom) + const addSnackbar = useAddSnackbar() + const [isPending, setIsPending] = useState(false) + + const placeOrder = useCallback(async () => { + if (!solanaContext) return + + setIsPending(true) + try { + const { signature, orderUid } = await solanaOrderFlow(solanaContext) + + addSnackbar({ + id: `solana-order-${signature}`, + icon: 'success', + content: ( + + Solana limit order created (UID {orderUid.slice(0, 8)}…){' '} + + View on Solscan + + + ), + }) + } catch (error) { + if (!isRejectRequestProviderError(error)) { + addSnackbar({ + id: 'solana-order-error', + icon: 'alert', + content: {getSwapErrorMessage(error)}, + }) + } + } finally { + setIsPending(false) + } + }, [solanaContext, addSnackbar]) + + if (!account) { + return ( + + Connect wallet + + ) + } + + if (!inputCurrency || !outputCurrency) { + return ( + + Select a token + + ) + } + + if (isFractionFalsy(inputCurrencyAmount) || isFractionFalsy(outputCurrencyAmount)) { + return ( + + Enter an amount + + ) + } + + if (!activeRate) { + return ( + + Enter a price + + ) + } + + // The sell token account must exist and hold the funds for the order to be settleable + if (!inputCurrencyBalance || (inputCurrencyAmount && inputCurrencyBalance.lessThan(inputCurrencyAmount))) { + return ( + + Insufficient balance + + ) + } + + return ( + + Place limit order + + ) +} +``` + +- [ ] **Step 2: Wire into TradeButtons** + +Modify `apps/cowswap-frontend/src/modules/limitOrders/containers/TradeButtons/index.tsx`. Add the imports: + +```tsx +import { isSolanaChain } from '@cowprotocol/cow-sdk' +import { useWalletInfo } from '@cowprotocol/wallet' +``` + +and (with the relative imports at the bottom of the import block): + +```tsx +import { SolanaTradeButtons } from '../SolanaTradeButtons' +``` + +Inside the component, add the hook next to the other hook calls (after `const { i18n, t } = useLingui()`): + +```tsx + const { chainId } = useWalletInfo() +``` + +Then, after the `const tradeFormButtonContext = useTradeFormButtonContext(...)` line and its neighboring hook calls, but **before** `if (!tradeFormButtonContext) return null`, add: + +```tsx + // Solana limit orders use a dedicated on-chain placement flow (prototype); + // the shared validation/quote/approve pipeline is EVM-only + if (isSolanaChain(chainId)) { + return + } +``` + +(All hooks must still be called unconditionally above this early return.) + +- [ ] **Step 3: Typecheck + lint** + +Run: `pnpm exec nx typecheck cowswap-frontend && pnpm exec nx lint cowswap-frontend` +Expected: both exit 0 (pre-existing warnings are fine). + +- [ ] **Step 4: Commit** + +```bash +git add apps/cowswap-frontend/src/modules/limitOrders/containers +git commit -m "feat(solana): Solana trade buttons for limit orders" +``` + +--- + +### Task 7: Skip quote polling on Solana + +**Files:** +- Modify: `apps/cowswap-frontend/src/modules/tradeQuote/hooks/useQuoteParams.ts` + +**Interfaces:** +- Consumes: `isSolanaChain` from `@cowprotocol/cow-sdk` +- Produces: `useQuoteParams` returns `undefined` for Solana sell tokens, so no quote request is ever fired (the backend cannot quote Solana pairs; the QuoteErrors/QuoteLoading validations never apply because the Solana button ladder bypasses them) + +- [ ] **Step 1: Add the guard** + +In `apps/cowswap-frontend/src/modules/tradeQuote/hooks/useQuoteParams.ts`, extend the existing cow-sdk import (line 6): + +```ts +import { getGlobalAdapter, isSolanaChain, OrderKind } from '@cowprotocol/cow-sdk' +``` + +Inside the `useSafeMemo` callback, after the line `if (!inputCurrency || !outputCurrency || !orderKind) return`, add: + +```ts + // No backend quote exists for Solana pairs; the limit-orders prototype uses a manually entered price + if (isSolanaChain(inputCurrency.chainId)) return +``` + +(`inputCurrency` is already in the memo's dependency list — no dep changes needed.) + +- [ ] **Step 2: Verify nothing broke** + +Run: `pnpm exec nx test cowswap-frontend --testPathPatterns=tradeQuote` +Expected: PASS (existing tests unaffected — they use EVM chain ids). + +- [ ] **Step 3: Commit** + +```bash +git add apps/cowswap-frontend/src/modules/tradeQuote/hooks/useQuoteParams.ts +git commit -m "feat(solana): skip quote polling for solana sell tokens" +``` + +--- + +### Task 8: Full verification + mainnet smoke test + +**Files:** none (verification only) + +- [ ] **Step 1: Run the full local gates** + +```bash +pnpm exec nx typecheck cowswap-frontend +pnpm exec nx lint cowswap-frontend +pnpm exec nx test cowswap-frontend +``` + +Expected: all exit 0. Fix anything that fails before proceeding. + +- [ ] **Step 2: Manual smoke test on mainnet (REQUIRED — this is the acceptance test)** + +Prerequisites: a Solana wallet (e.g. Phantom) holding a little SOL for fees/rent and a small SPL balance (e.g. ≥ 0.1 USDC — mint `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`). + +1. `pnpm exec nx serve cowswap-frontend` +2. Open `http://localhost:3000/#/1/limit?IS_SOLANA_ENABLED=true` — the hash query param persists the `IS_SOLANA_ENABLED` localStorage flag (see `libs/common-const/src/featureFlags.ts`). +3. Connect the Solana wallet through the wallet modal; switch the network selector to Solana. +4. In the Limit form: sell token USDC, buy token WSOL (`So11111111111111111111111111111111111111112`), sell amount `0.1`, and type a limit price. +5. Expect the button to read **Place limit order** (walk the ladder first if you want: disconnected → "Connect wallet", empty amount → "Enter an amount", amount > balance → "Insufficient balance"). +6. Click it, approve the transaction in the wallet. +7. Expect a success snackbar: "Solana limit order created (UID …) View on Solscan". +8. Open the Solscan link. Verify the tx succeeded and contains: `createIdempotent` (buy ATA), `approve` with delegate `3PYmNPBdoFBGqtAeopGMS5YvnQnfxh8J9sNS3jjzKhb8` and amount = sell amount, and an instruction to program `moosEjJg5mbGRPRU7Vg4AaHZLvbbgknevWR9J1bNgME` — same shape as the reference tx `4hy8scaTfLNyJiAPbF47k4YWyWmE2CFfvLj6zkTUibpknEcNfjWWDyCT185qDbRYYLMtdDzGkVtuNXJEE2oXE6JG`. +9. Also verify the EVM path still works: switch back to an EVM network, confirm the Limit form still quotes and the "Review limit order" button appears. + +- [ ] **Step 3: Final commit (if smoke test required fixes)** + +```bash +git add -A && git commit -m "fix(solana): smoke test fixes for solana limit order placement" +``` + +--- + +## Known accepted limitations (do not "fix" these) + +- One live approve per sell token: a second order overwrites the previous delegated allowance. +- Orders don't appear in the orders table; the snackbar + Solscan link is the entire post-create UX. +- `app_data` is zeroed; recipient is always the owner; native SOL cannot be sold. +- No priority fees / versioned transactions; `confirmed` commitment is enough for the prototype. diff --git a/docs/superpowers/specs/2026-07-16-solana-limit-orders-design.md b/docs/superpowers/specs/2026-07-16-solana-limit-orders-design.md index fedc4fb12ae..b9a9260be4d 100644 --- a/docs/superpowers/specs/2026-07-16-solana-limit-orders-design.md +++ b/docs/superpowers/specs/2026-07-16-solana-limit-orders-design.md @@ -99,12 +99,17 @@ prototype; noted for the production design. - Entry path: user enables `IS_SOLANA_ENABLED`, picks Solana in the network selector (already listed behind the flag), opens Limit orders. Token selection and balances already work on this branch. -- `modules/tradeFormValidation`: add a Solana path — the form is ready when a Solana - wallet is connected, both amounts are set, and an active rate exists. Quote, approval - (EVM allowance), and permit checks are skipped for Solana. -- EVM-only side effects get `isSolanaChain` early-return guards so they no-op on Solana: - quote fetching (`QuoteObserverUpdater`), initial-price fetching - (`InitialPriceUpdater` — the price is typed manually), permit logic. +- Form readiness on Solana is implemented in a dedicated `SolanaTradeButtons` container + inside `modules/limitOrders` (rendered by the existing `TradeButtons` when + `isSolanaChain(chainId)`), instead of adding a Solana path to the shared + `modules/tradeFormValidation` — that module's validation context is fed by EVM-only + hooks shared with swap/twap, so bypassing it wholesale is safer for a prototype. The + Solana button ladder: wallet connected → tokens selected → amounts set → sufficient + balance → place order. Quote, approval (EVM allowance), and permit checks never apply. +- Quote polling is skipped for Solana with an `isSolanaChain` guard in + `modules/tradeQuote/hooks/useQuoteParams.ts` (no backend quote exists). Initial-price + and market-rate updaters use USD price feeds and fail soft (null) on Solana — no + guards needed; the price is typed manually. - Post-create UX: success snackbar/toast with the tx signature linking to Solscan and the order UID. No orders-table/history integration. From 6bcb8135fd93afdcb269e7ba7b328bbc8002600f Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Thu, 16 Jul 2026 17:25:50 +0200 Subject: [PATCH 33/42] chore(solana): add web3.js, spl-token and noble/hashes to cowswap-frontend Co-Authored-By: Claude Fable 5 --- apps/cowswap-frontend/package.json | 3 + pnpm-lock.yaml | 168 +++++++++++++++-------------- 2 files changed, 92 insertions(+), 79 deletions(-) diff --git a/apps/cowswap-frontend/package.json b/apps/cowswap-frontend/package.json index a896203817a..02a07a9cebe 100644 --- a/apps/cowswap-frontend/package.json +++ b/apps/cowswap-frontend/package.json @@ -67,6 +67,7 @@ "@lingui/macro": "5.5.1", "@lingui/react": "5.5.1", "@marsidev/react-turnstile": "1.4.2", + "@noble/hashes": "1.4.0", "@reach/dialog": "0.18.0", "@reach/menu-button": "0.18.0", "@react-spring/web": "9.7.3", @@ -80,6 +81,8 @@ "@sentry/browser": "7.80.0", "@sentry/react": "7.80.0", "@sentry/types": "7.80.0", + "@solana/spl-token": "0.4.14", + "@solana/web3.js": "1.98.4", "@tanstack/query-core": "5.90.20", "@tanstack/react-query": "5.90.20", "@tanstack/react-virtual": "3.13.12", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef180aa2615..ebcc89fe7c8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -535,7 +535,7 @@ importers: dependencies: '@1inch/permit-signed-approvals-utils': specifier: 1.5.2 - version: 1.5.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) + version: 1.5.2(bufferutil@4.0.8)(utf-8-validate@6.0.6) '@cowprotocol/analytics': specifier: workspace:* version: link:../../libs/analytics @@ -607,7 +607,7 @@ importers: version: 2.2.2(ajv@8.17.1)(cross-fetch@4.0.0(encoding@0.1.13))(encoding@0.1.13)(multiformats@14.0.0) '@cowprotocol/sdk-viem-adapter': specifier: 0.3.24 - version: 0.3.24(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + version: 0.3.24(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) '@cowprotocol/snackbars': specifier: workspace:* version: link:../../libs/snackbars @@ -644,6 +644,9 @@ importers: '@marsidev/react-turnstile': specifier: 1.4.2 version: 1.4.2(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@noble/hashes': + specifier: 1.4.0 + version: 1.4.0 '@reach/dialog': specifier: 0.18.0 version: 0.18.0(@types/react@19.1.3)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) @@ -658,19 +661,19 @@ importers: version: 1.9.5(react-redux@8.1.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(redux@4.2.1))(react@19.1.2) '@reown/appkit': specifier: 1.8.19 - version: 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@5.0.10)(zod@3.25.76) + version: 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(zod@3.25.76) '@reown/appkit-adapter-solana': specifier: 1.8.19 - version: 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@5.0.10)(zod@3.25.76) + version: 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(zod@3.25.76) '@reown/appkit-adapter-wagmi': specifier: 1.8.19 - version: 1.8.19(patch_hash=b669cb20e36d425602b690643d4147fd1756aa3d94db205f5e4823777a277b1c)(b5eb2d51373dca5abfc3587eabff26c7) + version: 1.8.19(patch_hash=b669cb20e36d425602b690643d4147fd1756aa3d94db205f5e4823777a277b1c)(725f042c52896b22a29c385487b96d7a) '@reown/appkit-controllers': specifier: 1.8.19 - version: 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + version: 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) '@safe-global/api-kit': specifier: 4.2.0 - version: 4.2.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + version: 4.2.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) '@safe-global/types-kit': specifier: 3.0.0 version: 3.0.0(typescript@5.9.3)(zod@3.25.76) @@ -683,6 +686,12 @@ importers: '@sentry/types': specifier: 7.80.0 version: 7.80.0 + '@solana/spl-token': + specifier: 0.4.14 + version: 0.4.14(@solana/web3.js@1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6))(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@solana/web3.js': + specifier: 1.98.4 + version: 1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6) '@tanstack/query-core': specifier: 5.90.20 version: 5.90.20 @@ -700,7 +709,7 @@ importers: version: 10.3.1(react@19.1.2) '@wagmi/core': specifier: 3.4.8 - version: 3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + version: 3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) bignumber.js: specifier: 9.1.2 version: 9.1.2 @@ -838,10 +847,10 @@ importers: version: 1.2.4(react@19.1.2) viem: specifier: 2.48.8 - version: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + version: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) wagmi: specifier: 3.6.9 - version: 3.6.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + version: 3.6.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) workbox-core: specifier: 6.6.1 version: 6.6.1 @@ -863,7 +872,7 @@ importers: devDependencies: '@storybook/react-vite': specifier: 10.3.6 - version: 10.3.6(esbuild@0.27.2)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(rollup@4.52.4)(storybook@10.3.6(@testing-library/dom@10.4.1)(bufferutil@4.0.8)(prettier@3.6.2)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(utf-8-validate@5.0.10))(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.88.0)(sass@1.88.0)(terser@5.46.0)(yaml@2.8.1))(webpack@5.102.1(@swc/core@1.15.26(@swc/helpers@0.5.17))(esbuild@0.27.2)) + version: 10.3.6(esbuild@0.27.2)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(rollup@4.52.4)(storybook@10.3.6(@testing-library/dom@10.4.1)(bufferutil@4.0.8)(prettier@3.6.2)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(utf-8-validate@6.0.6))(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.88.0)(sass@1.88.0)(terser@5.46.0)(yaml@2.8.1))(webpack@5.102.1(@swc/core@1.15.26(@swc/helpers@0.5.17))(esbuild@0.27.2)) '@testing-library/react': specifier: 16.3.0 version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) @@ -905,7 +914,7 @@ importers: version: 0.8.0(@emotion/react@11.14.0(@types/react@19.1.3)(react@19.1.2))(@types/react@19.1.3)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(redux@4.2.1) react-cosmos: specifier: 7.0.0 - version: 7.0.0(@types/express@4.17.21)(bufferutil@4.0.8)(utf-8-validate@5.0.10) + version: 7.0.0(@types/express@4.17.21)(bufferutil@4.0.8)(utf-8-validate@6.0.6) rollup-plugin-bundle-stats: specifier: 4.21.9 version: 4.21.9(core-js@3.48.0)(rollup@4.52.4)(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.88.0)(sass@1.88.0)(terser@5.46.0)(yaml@2.8.1)) @@ -1244,7 +1253,7 @@ importers: dependencies: '@coinbase/wallet-sdk': specifier: 4.3.7 - version: 4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + version: 4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) '@cowprotocol/analytics': specifier: workspace:* version: link:../../libs/analytics @@ -1283,19 +1292,19 @@ importers: version: 5.17.1(@emotion/react@11.14.0(@types/react@19.1.3)(react@19.1.2))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.3)(react@19.1.2))(@types/react@19.1.3)(react@19.1.2))(@types/react@19.1.3)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) '@reown/appkit': specifier: 1.8.19 - version: 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@5.0.10)(zod@3.25.76) + version: 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(zod@3.25.76) '@reown/appkit-adapter-wagmi': specifier: 1.8.19 - version: 1.8.19(patch_hash=b669cb20e36d425602b690643d4147fd1756aa3d94db205f5e4823777a277b1c)(b5eb2d51373dca5abfc3587eabff26c7) + version: 1.8.19(patch_hash=b669cb20e36d425602b690643d4147fd1756aa3d94db205f5e4823777a277b1c)(725f042c52896b22a29c385487b96d7a) '@reown/appkit-controllers': specifier: 1.8.19 - version: 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + version: 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) '@tanstack/react-query': specifier: 5.90.20 version: 5.90.20(react@19.1.2) '@wagmi/connectors': specifier: 8.0.9 - version: 8.0.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@wagmi/core@3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + version: 8.0.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@wagmi/core@3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)))(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) csstype: specifier: 3.1.3 version: 3.1.3 @@ -1325,10 +1334,10 @@ importers: version: 15.5.0(react@19.1.2) viem: specifier: 2.48.8 - version: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + version: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) wagmi: specifier: 3.6.9 - version: 3.6.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + version: 3.6.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) widget-react-v0.13.0: specifier: npm:@cowprotocol/widget-react@0.13.0 version: '@cowprotocol/widget-react@0.13.0' @@ -1443,13 +1452,13 @@ importers: version: link:../wallet-provider '@reown/appkit-adapter-solana': specifier: 1.8.19 - version: 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(zod@3.25.76) + version: 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@5.0.10)(zod@3.25.76) '@solana/spl-token': specifier: 0.4.14 - version: 0.4.14(@solana/web3.js@1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6))(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6) + version: 0.4.14(@solana/web3.js@1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) '@solana/web3.js': specifier: 1.98.4 - version: 1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6) + version: 1.98.4(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10) '@tanstack/react-query': specifier: 5.90.20 version: 5.90.20(react@19.1.2) @@ -1467,10 +1476,10 @@ importers: version: 2.3.3(react@19.1.2) viem: specifier: 2.48.8 - version: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + version: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) wagmi: specifier: 3.6.9 - version: 3.6.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) + version: 3.6.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) devDependencies: '@testing-library/react': specifier: 16.3.0 @@ -17742,13 +17751,6 @@ packages: snapshots: - '@1inch/permit-signed-approvals-utils@1.5.2(bufferutil@4.0.8)(utf-8-validate@5.0.10)': - dependencies: - ethers: 6.16.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) - transitivePeerDependencies: - - bufferutil - - utf-8-validate - '@1inch/permit-signed-approvals-utils@1.5.2(bufferutil@4.0.8)(utf-8-validate@6.0.6)': dependencies: ethers: 6.16.0(bufferutil@4.0.8)(utf-8-validate@6.0.6) @@ -19014,7 +19016,7 @@ snapshots: '@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@noble/hashes': 1.8.0 + '@noble/hashes': 1.4.0 clsx: 1.2.1 eventemitter3: 5.0.4 preact: 10.28.2 @@ -19027,7 +19029,7 @@ snapshots: '@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': dependencies: - '@noble/hashes': 1.8.0 + '@noble/hashes': 1.4.0 clsx: 1.2.1 eventemitter3: 5.0.4 preact: 10.28.2 @@ -19037,7 +19039,6 @@ snapshots: - typescript - utf-8-validate - zod - optional: true '@commitlint/cli@20.1.0(@types/node@24.7.1)(typescript@5.9.3)': dependencies: @@ -21240,7 +21241,7 @@ snapshots: '@metamask/utils@8.4.0': dependencies: '@ethereumjs/tx': 4.2.0 - '@noble/hashes': 1.8.0 + '@noble/hashes': 1.4.0 '@scure/base': 1.2.6 '@types/debug': 4.1.12 debug: 4.4.3(supports-color@8.1.1) @@ -21255,7 +21256,7 @@ snapshots: dependencies: '@ethereumjs/tx': 4.2.0 '@metamask/superstruct': 3.2.1 - '@noble/hashes': 1.8.0 + '@noble/hashes': 1.4.0 '@scure/base': 1.2.6 '@types/debug': 4.1.12 debug: 4.4.3(supports-color@8.1.1) @@ -22929,6 +22930,56 @@ snapshots: - utf-8-validate - zod + '@reown/appkit-adapter-wagmi@1.8.19(patch_hash=b669cb20e36d425602b690643d4147fd1756aa3d94db205f5e4823777a277b1c)(725f042c52896b22a29c385487b96d7a)': + dependencies: + '@reown/appkit': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(zod@3.25.76) + '@reown/appkit-common': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@reown/appkit-controllers': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@reown/appkit-polyfills': 1.8.19 + '@reown/appkit-scaffold-ui': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(valtio@2.1.7(@types/react@19.1.3)(react@19.1.2))(zod@3.25.76) + '@reown/appkit-utils': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(valtio@2.1.7(@types/react@19.1.3)(react@19.1.2))(zod@3.25.76) + '@reown/appkit-wallet': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@wagmi/core': 3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) + '@walletconnect/universal-provider': 2.23.7(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + valtio: 2.1.7(@types/react@19.1.3)(react@19.1.2) + viem: 2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + wagmi: 3.6.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) + optionalDependencies: + '@wagmi/connectors': 8.0.9(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(@wagmi/core@3.4.8(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)))(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(typescript@5.9.3)(viem@2.48.8(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@base-org/account' + - '@capacitor/preferences' + - '@coinbase/wallet-sdk' + - '@metamask/connect-evm' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@safe-global/safe-apps-provider' + - '@safe-global/safe-apps-sdk' + - '@types/react' + - '@upstash/redis' + - '@vercel/kv' + - '@walletconnect/ethereum-provider' + - accounts + - bufferutil + - debug + - encoding + - fastestsmallesttextencoderdecoder + - immer + - porto + - react + - supports-color + - typescript + - use-sync-external-store + - utf-8-validate + - zod + '@reown/appkit-adapter-wagmi@1.8.19(patch_hash=b669cb20e36d425602b690643d4147fd1756aa3d94db205f5e4823777a277b1c)(b5eb2d51373dca5abfc3587eabff26c7)': dependencies: '@reown/appkit': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@5.0.10)(zod@3.25.76) @@ -24848,7 +24899,7 @@ snapshots: dependencies: '@babel/runtime': 7.27.0 '@noble/curves': 1.9.7 - '@noble/hashes': 1.8.0 + '@noble/hashes': 1.4.0 '@solana/buffer-layout': 4.0.1 '@solana/codecs-numbers': 2.3.0(typescript@5.9.3) agentkeepalive: 4.6.0 @@ -24871,7 +24922,7 @@ snapshots: dependencies: '@babel/runtime': 7.27.0 '@noble/curves': 1.9.7 - '@noble/hashes': 1.8.0 + '@noble/hashes': 1.4.0 '@solana/buffer-layout': 4.0.1 '@solana/codecs-numbers': 2.3.0(typescript@5.9.3) agentkeepalive: 4.6.0 @@ -30198,19 +30249,6 @@ snapshots: ethereum-cryptography: 0.1.3 rlp: 2.2.7 - ethers@6.16.0(bufferutil@4.0.8)(utf-8-validate@5.0.10): - dependencies: - '@adraffy/ens-normalize': 1.10.1 - '@noble/curves': 1.2.0 - '@noble/hashes': 1.3.2 - '@types/node': 22.7.5 - aes-js: 4.0.0-beta.5 - tslib: 2.7.0 - ws: 8.17.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) - transitivePeerDependencies: - - bufferutil - - utf-8-validate - ethers@6.16.0(bufferutil@4.0.8)(utf-8-validate@6.0.6): dependencies: '@adraffy/ens-normalize': 1.10.1 @@ -34964,29 +35002,6 @@ snapshots: lodash-es: 4.17.21 react-cosmos-core: 7.0.0 - react-cosmos@7.0.0(@types/express@4.17.21)(bufferutil@4.0.8)(utf-8-validate@5.0.10): - dependencies: - '@skidding/launch-editor': 2.2.3 - chokidar: 3.6.0 - express: 4.21.2 - glob: 10.4.5 - http-proxy-middleware: 2.0.9(@types/express@4.17.21) - lodash-es: 4.17.21 - micromatch: 4.0.8 - open: 10.1.1 - pem: 1.14.8 - react-cosmos-core: 7.0.0 - react-cosmos-renderer: 7.0.0 - react-cosmos-ui: 7.0.0 - ws: 8.18.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/express' - - bufferutil - - debug - - supports-color - - utf-8-validate - react-cosmos@7.0.0(@types/express@4.17.21)(bufferutil@4.0.8)(utf-8-validate@6.0.6): dependencies: '@skidding/launch-editor': 2.2.3 @@ -38497,11 +38512,6 @@ snapshots: bufferutil: 4.0.8 utf-8-validate: 5.0.10 - ws@8.18.1(bufferutil@4.0.8)(utf-8-validate@5.0.10): - optionalDependencies: - bufferutil: 4.0.8 - utf-8-validate: 5.0.10 - ws@8.18.1(bufferutil@4.0.8)(utf-8-validate@6.0.6): optionalDependencies: bufferutil: 4.0.8 From 85606d2161c311a744affc1b47c7a920b914fc4b Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Thu, 16 Jul 2026 17:33:30 +0200 Subject: [PATCH 34/42] feat(solana): order intent encoding, UID and PDA derivation Co-Authored-By: Claude Fable 5 --- .../services/solanaOrderFlow/const.ts | 17 ++++ .../solanaOrderFlow/orderIntent.test.ts | 97 +++++++++++++++++++ .../services/solanaOrderFlow/orderIntent.ts | 58 +++++++++++ 3 files changed, 172 insertions(+) create mode 100644 apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/const.ts create mode 100644 apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/orderIntent.test.ts create mode 100644 apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/orderIntent.ts diff --git a/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/const.ts b/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/const.ts new file mode 100644 index 00000000000..140e129d182 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/const.ts @@ -0,0 +1,17 @@ +import { PublicKey } from '@solana/web3.js' + +// CoW Protocol settlement program on Solana mainnet +// https://github.com/cowprotocol/solana-programs +export const SOLANA_SETTLEMENT_PROGRAM_ID = new PublicKey('moosEjJg5mbGRPRU7Vg4AaHZLvbbgknevWR9J1bNgME') + +// PDA seed scheme: every PDA starts with SETTLEMENT_SEED; order PDAs append the order UID and ORDER_SEED +export const SETTLEMENT_SEED = new TextEncoder().encode('settlement') +export const ORDER_SEED = new TextEncoder().encode('order') + +export const CREATE_ORDER_DISCRIMINATOR = 2 +export const ORDER_INTENT_SIZE = 150 + +// Opaque 32 bytes; the settlement program does not interpret them. Zeroed for the prototype. +export const SOLANA_APP_DATA = new Uint8Array(32) + +export const SOLSCAN_TX_URL = 'https://solscan.io/tx/' diff --git a/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/orderIntent.test.ts b/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/orderIntent.test.ts new file mode 100644 index 00000000000..c7f95b64bd5 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/orderIntent.test.ts @@ -0,0 +1,97 @@ +/** + * @jest-environment node + * + * web3.js's ed25519 PDA-derivation math (findProgramAddressSync) is unreliable under jsdom; + * the Solana balance tests in libs/balances-and-allowances use the node environment for the + * same reason. + */ +import { OrderKind } from '@cowprotocol/cow-sdk' + +import { PublicKey } from '@solana/web3.js' + +import { computeOrderUid, encodeOrderIntent, findOrderPda, findStatePda, SolanaOrderIntent } from './orderIntent' + +function toHex(bytes: Uint8Array): string { + return Array.from(bytes) + .map((b) => b.toString(16).padStart(2, '0')) + .join('') +} + +// Mirrors `sample_intent(OrderKind::Buy, true)` from cowprotocol/solana-programs +// interface/src/data/intent.rs (tests `encoding_regression` and `uid_digest_regression`) +const RUST_SAMPLE_INTENT: SolanaOrderIntent = { + owner: new PublicKey(new Uint8Array(32).fill(0x11)), + buyTokenAccount: new PublicKey(new Uint8Array(32).fill(0x22)), + sellTokenAccount: new PublicKey(new Uint8Array(32).fill(0x33)), + sellAmount: 0x0123456789abcdefn, + buyAmount: 0xfedcba9876543210n, + validTo: 0xdeadbeef, + kind: OrderKind.BUY, + partiallyFillable: true, + appData: new Uint8Array(32).fill(0x44), +} + +const RUST_SAMPLE_ENCODING = + '11'.repeat(32) + // owner + '22'.repeat(32) + // buy_token_account + '33'.repeat(32) + // sell_token_account + 'efcdab8967452301' + // sell_amount LE + '1032547698badcfe' + // buy_amount LE + 'efbeadde' + // valid_to LE + '01' + // kind: buy + '01' + // partially_fillable: true + '44'.repeat(32) // app_data + +const RUST_SAMPLE_UID = '7ce7c6a74671090771fa33851387444064aca759ce55b80708723076722f5e00' + +// Decoded from the raw CreateOrder instruction of mainnet tx +// 4hy8scaTfLNyJiAPbF47k4YWyWmE2CFfvLj6zkTUibpknEcNfjWWDyCT185qDbRYYLMtdDzGkVtuNXJEE2oXE6JG +const MAINNET_INTENT: SolanaOrderIntent = { + owner: new PublicKey('54o2XBzBTkP7tmQSLu3Um9oDvLdNVrbMyQxqiYVKALLN'), + buyTokenAccount: new PublicKey('E9xwK5SDXSJLW1A4WRyVT1FzVpt8gREGMVibVW9A8xX5'), + sellTokenAccount: new PublicKey('cEDc7aAMaCqBX546QWCVxnvfMLUUV3JETQ6qnpeLUaY'), + sellAmount: 0n, + buyAmount: 10_000_000n, + validTo: 1783575524, + kind: OrderKind.BUY, + partiallyFillable: false, + appData: new Uint8Array(32), +} +const MAINNET_UID = 'f41a85a660c71b6fac30d024d29df733b8b101f931e30fbf8c37f5a0f2d42b2f' + +describe('encodeOrderIntent', () => { + it('produces 150 bytes', () => { + expect(encodeOrderIntent(RUST_SAMPLE_INTENT)).toHaveLength(150) + }) + + it('matches the Rust encoding_regression vector', () => { + expect(toHex(encodeOrderIntent(RUST_SAMPLE_INTENT))).toBe(RUST_SAMPLE_ENCODING) + }) + + it('encodes a sell fill-or-kill order with zero flag bytes', () => { + const encoded = encodeOrderIntent({ ...RUST_SAMPLE_INTENT, kind: OrderKind.SELL, partiallyFillable: false }) + expect(encoded[116]).toBe(0) + expect(encoded[117]).toBe(0) + }) +}) + +describe('computeOrderUid', () => { + it('matches the Rust uid_digest_regression vector', () => { + expect(toHex(computeOrderUid(encodeOrderIntent(RUST_SAMPLE_INTENT)))).toBe(RUST_SAMPLE_UID) + }) + + it('matches the mainnet example order UID', () => { + expect(toHex(computeOrderUid(encodeOrderIntent(MAINNET_INTENT)))).toBe(MAINNET_UID) + }) +}) + +describe('PDA derivation', () => { + it('derives the settlement state PDA seen as the approve delegate on mainnet', () => { + expect(findStatePda().toBase58()).toBe('3PYmNPBdoFBGqtAeopGMS5YvnQnfxh8J9sNS3jjzKhb8') + }) + + it('derives the order PDA of the mainnet example order', () => { + const uid = computeOrderUid(encodeOrderIntent(MAINNET_INTENT)) + expect(findOrderPda(uid).toBase58()).toBe('AmtUsUoFuGtRpxeQnQEFR83xyeqTrPH8Z4y9twso26Lv') + }) +}) diff --git a/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/orderIntent.ts b/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/orderIntent.ts new file mode 100644 index 00000000000..eea2d63e271 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/orderIntent.ts @@ -0,0 +1,58 @@ +import { OrderKind } from '@cowprotocol/cow-sdk' + +import { sha256 } from '@noble/hashes/sha256' +import { PublicKey } from '@solana/web3.js' + +import { ORDER_INTENT_SIZE, ORDER_SEED, SETTLEMENT_SEED, SOLANA_SETTLEMENT_PROGRAM_ID } from './const' + +export interface SolanaOrderIntent { + owner: PublicKey + /** SPL token account (not mint) receiving the buy-side proceeds */ + buyTokenAccount: PublicKey + /** SPL token account (not mint) the sell funds are pulled from; must be owned by `owner` */ + sellTokenAccount: PublicKey + sellAmount: bigint + buyAmount: bigint + /** Unix timestamp (seconds) after which the order expires */ + validTo: number + kind: OrderKind + partiallyFillable: boolean + /** Opaque 32 bytes */ + appData: Uint8Array +} + +/** Order UID = SHA-256 of the canonical intent bytes; also the middle seed of the order PDA */ +export function computeOrderUid(intentBytes: Uint8Array): Uint8Array { + return sha256(intentBytes) +} + +/** + * Canonical 150-byte encoding, the wire format and the UID preimage. + * Layout: owner(32) ‖ buy(32) ‖ sell(32) ‖ sellAmount(u64 LE) ‖ buyAmount(u64 LE) + * ‖ validTo(u32 LE) ‖ kind(u8) ‖ partiallyFillable(u8) ‖ appData(32) + */ +export function encodeOrderIntent(intent: SolanaOrderIntent): Uint8Array { + const bytes = new Uint8Array(ORDER_INTENT_SIZE) + const view = new DataView(bytes.buffer) + + bytes.set(intent.owner.toBytes(), 0) + bytes.set(intent.buyTokenAccount.toBytes(), 32) + bytes.set(intent.sellTokenAccount.toBytes(), 64) + view.setBigUint64(96, intent.sellAmount, true) + view.setBigUint64(104, intent.buyAmount, true) + view.setUint32(112, intent.validTo, true) + bytes[116] = intent.kind === OrderKind.BUY ? 1 : 0 + bytes[117] = intent.partiallyFillable ? 1 : 0 + bytes.set(intent.appData, 118) + + return bytes +} + +export function findOrderPda(orderUid: Uint8Array): PublicKey { + return PublicKey.findProgramAddressSync([SETTLEMENT_SEED, orderUid, ORDER_SEED], SOLANA_SETTLEMENT_PROGRAM_ID)[0] +} + +/** Settlement state PDA: the SPL delegate that pulls sell funds at execution time */ +export function findStatePda(): PublicKey { + return PublicKey.findProgramAddressSync([SETTLEMENT_SEED], SOLANA_SETTLEMENT_PROGRAM_ID)[0] +} From 35e1cc232ba732355bf9f213d51c6a59ee662ff8 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Thu, 16 Jul 2026 17:34:36 +0200 Subject: [PATCH 35/42] feat(solana): create-order instruction building Co-Authored-By: Claude Fable 5 --- .../buildCreateOrderInstructions.test.ts | 70 +++++++++++ .../buildCreateOrderInstructions.ts | 112 ++++++++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/buildCreateOrderInstructions.test.ts create mode 100644 apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/buildCreateOrderInstructions.ts diff --git a/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/buildCreateOrderInstructions.test.ts b/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/buildCreateOrderInstructions.test.ts new file mode 100644 index 00000000000..6c1c2199dc2 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/buildCreateOrderInstructions.test.ts @@ -0,0 +1,70 @@ +/** + * @jest-environment node + * + * web3.js/spl-token address derivation is unreliable under jsdom; the Solana balance tests in + * libs/balances-and-allowances use the node environment for the same reason. + */ +import { OrderKind } from '@cowprotocol/cow-sdk' + +import { TOKEN_PROGRAM_ID } from '@solana/spl-token' +import { SystemProgram } from '@solana/web3.js' + +import { buildCreateOrderInstructions } from './buildCreateOrderInstructions' +import { SOLANA_SETTLEMENT_PROGRAM_ID } from './const' + +// Reconstructs the order of mainnet tx +// 4hy8scaTfLNyJiAPbF47k4YWyWmE2CFfvLj6zkTUibpknEcNfjWWDyCT185qDbRYYLMtdDzGkVtuNXJEE2oXE6JG +// (buy 10 USDC paying with WSOL, fill-or-kill) +const PARAMS = { + account: '54o2XBzBTkP7tmQSLu3Um9oDvLdNVrbMyQxqiYVKALLN', + sellToken: { address: 'So11111111111111111111111111111111111111112', isToken2022: false }, // WSOL + buyToken: { address: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', isToken2022: false }, // USDC + sellAmount: 0n, + buyAmount: 10_000_000n, + validTo: 1783575524, + kind: OrderKind.BUY, + partiallyFillable: false, +} + +describe('buildCreateOrderInstructions', () => { + it('derives the same accounts and instructions as the mainnet example tx', () => { + const { instructions, orderPda, sellTokenAccount, buyTokenAccount } = buildCreateOrderInstructions(PARAMS) + + // ATAs of the owner, as seen on-chain + expect(sellTokenAccount.toBase58()).toBe('cEDc7aAMaCqBX546QWCVxnvfMLUUV3JETQ6qnpeLUaY') + expect(buyTokenAccount.toBase58()).toBe('E9xwK5SDXSJLW1A4WRyVT1FzVpt8gREGMVibVW9A8xX5') + expect(orderPda.toBase58()).toBe('AmtUsUoFuGtRpxeQnQEFR83xyeqTrPH8Z4y9twso26Lv') + + expect(instructions).toHaveLength(3) + const [createBuyAta, approve, createOrder] = instructions + + // buy ATA idempotent creation: account 1 is the ATA being created + expect(createBuyAta.keys[1].pubkey.equals(buyTokenAccount)).toBe(true) + + // approve: keys are [source token account, delegate, owner]; delegate is the settlement state PDA + expect(approve.programId.equals(TOKEN_PROGRAM_ID)).toBe(true) + expect(approve.keys[0].pubkey.equals(sellTokenAccount)).toBe(true) + expect(approve.keys[1].pubkey.toBase58()).toBe('3PYmNPBdoFBGqtAeopGMS5YvnQnfxh8J9sNS3jjzKhb8') + + // createOrder data: [discriminator=2, ...150 intent bytes] + expect(createOrder.programId.equals(SOLANA_SETTLEMENT_PROGRAM_ID)).toBe(true) + expect(createOrder.data).toHaveLength(151) + expect(createOrder.data[0]).toBe(2) + + // createOrder accounts: owner (signer, ro), created_by = owner (signer, writable), + // order PDA (writable), system program (ro) + expect(createOrder.keys).toHaveLength(4) + expect(createOrder.keys[0].pubkey.toBase58()).toBe(PARAMS.account) + expect(createOrder.keys[0].isSigner).toBe(true) + expect(createOrder.keys[0].isWritable).toBe(false) + expect(createOrder.keys[1].pubkey.toBase58()).toBe(PARAMS.account) + expect(createOrder.keys[1].isSigner).toBe(true) + expect(createOrder.keys[1].isWritable).toBe(true) + expect(createOrder.keys[2].pubkey.equals(orderPda)).toBe(true) + expect(createOrder.keys[2].isSigner).toBe(false) + expect(createOrder.keys[2].isWritable).toBe(true) + expect(createOrder.keys[3].pubkey.equals(SystemProgram.programId)).toBe(true) + expect(createOrder.keys[3].isSigner).toBe(false) + expect(createOrder.keys[3].isWritable).toBe(false) + }) +}) diff --git a/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/buildCreateOrderInstructions.ts b/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/buildCreateOrderInstructions.ts new file mode 100644 index 00000000000..530cf953f4a --- /dev/null +++ b/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/buildCreateOrderInstructions.ts @@ -0,0 +1,112 @@ +import { OrderKind } from '@cowprotocol/cow-sdk' + +import { + createApproveInstruction, + createAssociatedTokenAccountIdempotentInstruction, + getAssociatedTokenAddressSync, + TOKEN_2022_PROGRAM_ID, + TOKEN_PROGRAM_ID, +} from '@solana/spl-token' +import { PublicKey, SystemProgram, TransactionInstruction } from '@solana/web3.js' + +import { CREATE_ORDER_DISCRIMINATOR, SOLANA_APP_DATA, SOLANA_SETTLEMENT_PROGRAM_ID } from './const' +import { computeOrderUid, encodeOrderIntent, findOrderPda, findStatePda } from './orderIntent' + +export interface BuildCreateOrderParams { + /** base58 wallet address: order owner, rent payer and fee payer */ + account: string + sellToken: SolanaTokenParams + buyToken: SolanaTokenParams + sellAmount: bigint + buyAmount: bigint + /** Unix timestamp (seconds) */ + validTo: number + kind: OrderKind + partiallyFillable: boolean +} + +export interface CreateOrderInstructions { + instructions: TransactionInstruction[] + orderUid: Uint8Array + orderPda: PublicKey + sellTokenAccount: PublicKey + buyTokenAccount: PublicKey +} + +export interface SolanaTokenParams { + /** base58 mint address */ + address: string + /** Token-2022 mints live under a different token program (see TOKEN_2022_TAG in the token lists) */ + isToken2022: boolean +} + +export function buildCreateOrderInstructions(params: BuildCreateOrderParams): CreateOrderInstructions { + const owner = new PublicKey(params.account) + const sellMint = new PublicKey(params.sellToken.address) + const buyMint = new PublicKey(params.buyToken.address) + const sellTokenProgram = params.sellToken.isToken2022 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID + const buyTokenProgram = params.buyToken.isToken2022 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID + + const sellTokenAccount = getAssociatedTokenAddressSync(sellMint, owner, false, sellTokenProgram) + const buyTokenAccount = getAssociatedTokenAddressSync(buyMint, owner, false, buyTokenProgram) + + // The buy-side token account must exist for the order to be settleable + const createBuyAta = createAssociatedTokenAccountIdempotentInstruction( + owner, + buyTokenAccount, + owner, + buyMint, + buyTokenProgram, + ) + + // The settlement state PDA pulls the sell funds via SPL delegation at execution time. + // NOTE (accepted prototype limitation): SPL token accounts have a single delegate, + // so a second order on the same sell token overwrites the previous delegated amount. + const approve = createApproveInstruction( + sellTokenAccount, + findStatePda(), + owner, + params.sellAmount, + [], + sellTokenProgram, + ) + + const intentBytes = encodeOrderIntent({ + owner, + buyTokenAccount, + sellTokenAccount, + sellAmount: params.sellAmount, + buyAmount: params.buyAmount, + validTo: params.validTo, + kind: params.kind, + partiallyFillable: params.partiallyFillable, + appData: SOLANA_APP_DATA, + }) + const orderUid = computeOrderUid(intentBytes) + const orderPda = findOrderPda(orderUid) + + const data = Buffer.alloc(1 + intentBytes.length) + data[0] = CREATE_ORDER_DISCRIMINATOR + data.set(intentBytes, 1) + + const createOrder = new TransactionInstruction({ + programId: SOLANA_SETTLEMENT_PROGRAM_ID, + keys: [ + // owner: authenticates the order + { pubkey: owner, isSigner: true, isWritable: false }, + // created_by: funds the order PDA's rent (same as owner here) + { pubkey: owner, isSigner: true, isWritable: true }, + { pubkey: orderPda, isSigner: false, isWritable: true }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + ], + data, + }) + + return { + instructions: [createBuyAta, approve, createOrder], + orderUid, + orderPda, + sellTokenAccount, + buyTokenAccount, + } +} From 0e3d6026f049e60ecbbaed79aa34ca00af75e2db Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Thu, 16 Jul 2026 17:39:07 +0200 Subject: [PATCH 36/42] feat(solana): solanaOrderFlow service sending the create-order tx Co-Authored-By: Claude Fable 5 --- .../services/solanaOrderFlow/index.ts | 43 ++++++++++ .../solanaOrderFlow/solanaOrderFlow.test.ts | 86 +++++++++++++++++++ .../services/solanaOrderFlow/types.ts | 22 +++++ 3 files changed, 151 insertions(+) create mode 100644 apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/index.ts create mode 100644 apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/solanaOrderFlow.test.ts create mode 100644 apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/types.ts diff --git a/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/index.ts b/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/index.ts new file mode 100644 index 00000000000..ba357ad87f3 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/index.ts @@ -0,0 +1,43 @@ +import { PublicKey, Transaction } from '@solana/web3.js' + +import { buildCreateOrderInstructions } from './buildCreateOrderInstructions' + +import type { SolanaOrderFlowContext, SolanaOrderFlowResult } from './types' + +export type { SolanaOrderFlowContext, SolanaOrderFlowResult } from './types' + +export async function solanaOrderFlow(ctx: SolanaOrderFlowContext): Promise { + const { connection, walletProvider, customDeadlineTimestamp, deadlineMilliseconds, ...orderParams } = ctx + + // Deadline is relative to the send time, mirroring the EVM flow where + // validTo is calculated just before signing + const validTo = customDeadlineTimestamp ?? Math.floor((Date.now() + deadlineMilliseconds) / 1000) + + const { instructions, orderUid, orderPda } = buildCreateOrderInstructions({ ...orderParams, validTo }) + + const transaction = new Transaction().add(...instructions) + transaction.feePayer = new PublicKey(ctx.account) + + const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash() + transaction.recentBlockhash = blockhash + + const signature = await walletProvider.sendTransaction(transaction, connection) + + const confirmation = await connection.confirmTransaction({ signature, blockhash, lastValidBlockHeight }, 'confirmed') + + if (confirmation.value.err) { + throw new Error(`Solana transaction failed: ${JSON.stringify(confirmation.value.err)}`) + } + + return { + signature, + orderUid: uint8ArrayToHex(orderUid), + orderPda: orderPda.toBase58(), + } +} + +function uint8ArrayToHex(bytes: Uint8Array): string { + return Array.from(bytes) + .map((b) => b.toString(16).padStart(2, '0')) + .join('') +} diff --git a/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/solanaOrderFlow.test.ts b/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/solanaOrderFlow.test.ts new file mode 100644 index 00000000000..c34c8dc9d50 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/solanaOrderFlow.test.ts @@ -0,0 +1,86 @@ +/** + * @jest-environment node + * + * web3.js address/PDA derivation is unreliable under jsdom; the Solana balance tests in + * libs/balances-and-allowances use the node environment for the same reason. + */ +import { OrderKind } from '@cowprotocol/cow-sdk' + +import { solanaOrderFlow } from './index' + +import type { SolanaOrderFlowContext } from './types' +import type { Provider as SolanaProvider } from '@reown/appkit-adapter-solana/react' +import type { Connection, Transaction } from '@solana/web3.js' + +const OWNER = '54o2XBzBTkP7tmQSLu3Um9oDvLdNVrbMyQxqiYVKALLN' +// Any well-formed base58 32-byte value works as a fake blockhash +const BLOCKHASH = 'EkSnNWid2cvwEVnVx9aBqawnmiCNiDgp3gUdkDPTKN1N' + +function buildContext(): SolanaOrderFlowContext & { + connection: { getLatestBlockhash: jest.Mock; confirmTransaction: jest.Mock } + walletProvider: { sendTransaction: jest.Mock } +} { + const connection = { + getLatestBlockhash: jest.fn().mockResolvedValue({ blockhash: BLOCKHASH, lastValidBlockHeight: 100 }), + confirmTransaction: jest.fn().mockResolvedValue({ value: { err: null } }), + } + const walletProvider = { + sendTransaction: jest.fn().mockResolvedValue('mockSignature'), + } + + return { + account: OWNER, + sellToken: { address: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', isToken2022: false }, + buyToken: { address: 'So11111111111111111111111111111111111111112', isToken2022: false }, + sellAmount: 1_000_000n, + buyAmount: 5_000_000n, + kind: OrderKind.SELL, + partiallyFillable: true, + customDeadlineTimestamp: null, + deadlineMilliseconds: 3_600_000, + connection: connection as unknown as Connection, + walletProvider: walletProvider as unknown as SolanaProvider, + } as never +} + +describe('solanaOrderFlow', () => { + it('sends a 3-instruction transaction and returns signature, UID and PDA', async () => { + const ctx = buildContext() + + const result = await solanaOrderFlow(ctx) + + expect(result.signature).toBe('mockSignature') + expect(result.orderUid).toMatch(/^[0-9a-f]{64}$/) + expect(result.orderPda).toBeTruthy() + + expect(ctx.walletProvider.sendTransaction).toHaveBeenCalledTimes(1) + const [tx] = ctx.walletProvider.sendTransaction.mock.calls[0] as [Transaction] + expect(tx.instructions).toHaveLength(3) + expect(tx.feePayer?.toBase58()).toBe(OWNER) + expect(tx.recentBlockhash).toBe(BLOCKHASH) + + expect(ctx.connection.confirmTransaction).toHaveBeenCalledWith( + { signature: 'mockSignature', blockhash: BLOCKHASH, lastValidBlockHeight: 100 }, + 'confirmed', + ) + }) + + it('uses the custom deadline timestamp as validTo when set', async () => { + const ctx = buildContext() + ctx.customDeadlineTimestamp = 1893456000 + + await solanaOrderFlow(ctx) + + const [tx] = ctx.walletProvider.sendTransaction.mock.calls[0] as [Transaction] + const createOrderData = tx.instructions[2].data + // valid_to is a u32 LE at intent offset 112, i.e. data offset 113 (after the discriminator) + expect(createOrderData.readUInt32LE(113)).toBe(1893456000) + }) + + it('throws when on-chain confirmation reports an error', async () => { + const ctx = buildContext() + ctx.connection.confirmTransaction.mockResolvedValue({ value: { err: { InstructionError: [2, 'Custom'] } } }) + + await expect(solanaOrderFlow(ctx)).rejects.toThrow('Solana transaction failed') + }) +}) diff --git a/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/types.ts b/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/types.ts new file mode 100644 index 00000000000..61e59a04d70 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/types.ts @@ -0,0 +1,22 @@ +import type { BuildCreateOrderParams } from './buildCreateOrderInstructions' +import type { Provider as SolanaProvider } from '@reown/appkit-adapter-solana/react' +import type { Connection } from '@solana/web3.js' + +// validTo is intentionally not part of the context: like the EVM flow, it is +// computed just before sending so the deadline is relative to the send time. +export interface SolanaOrderFlowContext extends Omit { + connection: Connection + walletProvider: SolanaProvider + /** Limit-orders settings: fixed deadline (unix seconds) when the user picked a custom date */ + customDeadlineTimestamp: number | null + /** Limit-orders settings: relative deadline duration */ + deadlineMilliseconds: number +} + +export interface SolanaOrderFlowResult { + signature: string + /** hex-encoded 32-byte order UID */ + orderUid: string + /** base58 order PDA address */ + orderPda: string +} From 96c2f778c9ef85be0c7e85ae1ab7cf459a51955d Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Thu, 16 Jul 2026 17:40:34 +0200 Subject: [PATCH 37/42] feat(solana): limit orders Solana flow context hook Co-Authored-By: Claude Fable 5 --- .../hooks/useSolanaOrderFlowContext.ts | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 apps/cowswap-frontend/src/modules/limitOrders/hooks/useSolanaOrderFlowContext.ts diff --git a/apps/cowswap-frontend/src/modules/limitOrders/hooks/useSolanaOrderFlowContext.ts b/apps/cowswap-frontend/src/modules/limitOrders/hooks/useSolanaOrderFlowContext.ts new file mode 100644 index 00000000000..df3197cae3d --- /dev/null +++ b/apps/cowswap-frontend/src/modules/limitOrders/hooks/useSolanaOrderFlowContext.ts @@ -0,0 +1,65 @@ +import { useAtomValue } from 'jotai' + +import { getIsToken2022 } from '@cowprotocol/common-const' +import { getCurrencyAddress } from '@cowprotocol/common-utils' +import { isSolanaChain } from '@cowprotocol/cow-sdk' +import { useWalletInfo } from '@cowprotocol/wallet' + +import { useAppKitProvider } from '@reown/appkit/react' +import { useAppKitConnection } from '@reown/appkit-adapter-solana/react' + +import { SolanaOrderFlowContext } from 'modules/limitOrders/services/solanaOrderFlow' +import { limitOrdersSettingsAtom } from 'modules/limitOrders/state/limitOrdersSettingsAtom' + +import { useSafeMemo } from 'common/hooks/useSafeMemo' + +import { useLimitOrdersDerivedState } from './useLimitOrdersDerivedState' + +import type { Provider as SolanaProvider } from '@reown/appkit-adapter-solana/react' + +export function useSolanaOrderFlowContext(): SolanaOrderFlowContext | null { + const { chainId, account } = useWalletInfo() + const { connection } = useAppKitConnection() + const { walletProvider } = useAppKitProvider('solana') + const { customDeadlineTimestamp, deadlineMilliseconds, partialFillsEnabled } = useAtomValue(limitOrdersSettingsAtom) + const { inputCurrency, outputCurrency, inputCurrencyAmount, outputCurrencyAmount, orderKind } = + useLimitOrdersDerivedState() + + return useSafeMemo(() => { + if (!isSolanaChain(chainId) || !account || !connection || !walletProvider) return null + if (!inputCurrency || !outputCurrency || !inputCurrencyAmount || !outputCurrencyAmount) return null + + return { + account, + connection, + walletProvider, + sellToken: { + address: getCurrencyAddress(inputCurrency), + isToken2022: getIsToken2022(inputCurrency as { tags?: string[] }), + }, + buyToken: { + address: getCurrencyAddress(outputCurrency), + isToken2022: getIsToken2022(outputCurrency as { tags?: string[] }), + }, + sellAmount: BigInt(inputCurrencyAmount.quotient.toString()), + buyAmount: BigInt(outputCurrencyAmount.quotient.toString()), + kind: orderKind, + partiallyFillable: partialFillsEnabled, + customDeadlineTimestamp, + deadlineMilliseconds, + } + }, [ + chainId, + account, + connection, + walletProvider, + inputCurrency, + outputCurrency, + inputCurrencyAmount, + outputCurrencyAmount, + orderKind, + partialFillsEnabled, + customDeadlineTimestamp, + deadlineMilliseconds, + ]) +} From 87c29bca214e447db9c85f98a07c2ea3c3f87e10 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Thu, 16 Jul 2026 17:51:27 +0200 Subject: [PATCH 38/42] feat(solana): add Solana trade buttons for limit orders Co-Authored-By: Claude Fable 5 --- .../containers/SolanaTradeButtons/index.tsx | 118 ++++++++++++++++++ .../containers/TradeButtons/index.tsx | 11 ++ 2 files changed, 129 insertions(+) create mode 100644 apps/cowswap-frontend/src/modules/limitOrders/containers/SolanaTradeButtons/index.tsx diff --git a/apps/cowswap-frontend/src/modules/limitOrders/containers/SolanaTradeButtons/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/containers/SolanaTradeButtons/index.tsx new file mode 100644 index 00000000000..11fdb9177c9 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/limitOrders/containers/SolanaTradeButtons/index.tsx @@ -0,0 +1,118 @@ +import { useAtomValue } from 'jotai' +import { ReactNode, useCallback, useState } from 'react' + +import { isFractionFalsy, isRejectRequestProviderError } from '@cowprotocol/common-utils' +import { useAddSnackbar } from '@cowprotocol/snackbars' +import { ExternalLink } from '@cowprotocol/ui' +import { useWalletInfo } from '@cowprotocol/wallet' + +import { Trans } from '@lingui/react/macro' + +import { useLimitOrdersDerivedState } from 'modules/limitOrders/hooks/useLimitOrdersDerivedState' +import { useSolanaOrderFlowContext } from 'modules/limitOrders/hooks/useSolanaOrderFlowContext' +import { solanaOrderFlow } from 'modules/limitOrders/services/solanaOrderFlow' +import { SOLSCAN_TX_URL } from 'modules/limitOrders/services/solanaOrderFlow/const' +import { limitRateAtom } from 'modules/limitOrders/state/limitRateAtom' +import { TradeFormBlankButton } from 'modules/tradeFormValidation' + +import { getSwapErrorMessage } from 'common/utils/getSwapErrorMessage' + +/** + * Prototype placement flow for Solana limit orders: no quote, no confirm modal. + * The button sends the create-order transaction directly and reports the + * result in a snackbar with a Solscan link. + */ + +export function SolanaTradeButtons(): ReactNode { + const { account } = useWalletInfo() + const solanaContext = useSolanaOrderFlowContext() + const { inputCurrency, outputCurrency, inputCurrencyAmount, outputCurrencyAmount, inputCurrencyBalance } = + useLimitOrdersDerivedState() + const { activeRate } = useAtomValue(limitRateAtom) + const addSnackbar = useAddSnackbar() + const [isPending, setIsPending] = useState(false) + + const placeOrder = useCallback(async () => { + if (!solanaContext) return + + setIsPending(true) + try { + const { signature, orderUid } = await solanaOrderFlow(solanaContext) + + addSnackbar({ + id: `solana-order-${signature}`, + icon: 'success', + content: ( + + Solana limit order created (UID {orderUid.slice(0, 8)}…){' '} + + View on Solscan + + + ), + }) + } catch (error) { + if (!isRejectRequestProviderError(error)) { + addSnackbar({ + id: 'solana-order-error', + icon: 'alert', + content: {getSwapErrorMessage(error)}, + }) + } + } finally { + setIsPending(false) + } + }, [solanaContext, addSnackbar]) + + if (!account) { + return ( + + Connect wallet + + ) + } + + if (!inputCurrency || !outputCurrency) { + return ( + + Select a token + + ) + } + + if (isFractionFalsy(inputCurrencyAmount) || isFractionFalsy(outputCurrencyAmount)) { + return ( + + Enter an amount + + ) + } + + if (!activeRate) { + return ( + + Enter a price + + ) + } + + // The sell token account must exist and hold the funds for the order to be settleable + if (!inputCurrencyBalance || (inputCurrencyAmount && inputCurrencyBalance.lessThan(inputCurrencyAmount))) { + return ( + + Insufficient balance + + ) + } + + return ( + + Place limit order + + ) +} diff --git a/apps/cowswap-frontend/src/modules/limitOrders/containers/TradeButtons/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/containers/TradeButtons/index.tsx index fb0afe1b79a..7874dbcad6c 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/containers/TradeButtons/index.tsx +++ b/apps/cowswap-frontend/src/modules/limitOrders/containers/TradeButtons/index.tsx @@ -2,6 +2,9 @@ import React, { isValidElement } from 'react' import { MessageDescriptor } from '@lingui/core' +import { isSolanaChain } from '@cowprotocol/cow-sdk' +import { useWalletInfo } from '@cowprotocol/wallet' + import { useLingui } from '@lingui/react/macro' import { useLimitOrdersWarningsAccepted } from 'modules/limitOrders/hooks/useLimitOrdersWarningsAccepted' @@ -17,6 +20,7 @@ import { TradeFormValidation } from 'modules/tradeFormValidation/types' import { limitOrdersTradeButtonsMap } from './limitOrdersTradeButtonsMap' import { useLimitOrdersFormState } from '../../hooks/useLimitOrdersFormState' +import { SolanaTradeButtons } from '../SolanaTradeButtons' const PRIMARY_VALIDATION_OVERRIDEN_BY_LOCAL_VALIDATION: TradeFormValidation[] = [ TradeFormValidation.ApproveAndSwapInBundle, @@ -31,6 +35,7 @@ interface TradeButtonsProps { // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function TradeButtons({ isTradeContextReady }: TradeButtonsProps) { const { i18n, t } = useLingui() + const { chainId } = useWalletInfo() const CONFIRM_TEXT = t`Review limit order` const localFormValidation = useLimitOrdersFormState() const primaryFormValidation = useGetTradeFormValidation() @@ -42,6 +47,12 @@ export function TradeButtons({ isTradeContextReady }: TradeButtonsProps) { const isDisabled = !warningsAccepted || !isTradeContextReady + // Solana limit orders use a dedicated on-chain placement flow (prototype); + // the shared validation/quote/approve pipeline is EVM-only + if (isSolanaChain(chainId)) { + return + } + if (!tradeFormButtonContext) return null // Display local form validation errors only when there are no primary errors From c39f53673e6f2f1eb06212c251ce0429c7d5663b Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Thu, 16 Jul 2026 17:52:13 +0200 Subject: [PATCH 39/42] feat(solana): skip quote polling for solana sell tokens Co-Authored-By: Claude Fable 5 --- .../src/modules/tradeQuote/hooks/useQuoteParams.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useQuoteParams.ts b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useQuoteParams.ts index 4d4058ec8ed..3ec479a7ea7 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useQuoteParams.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useQuoteParams.ts @@ -3,7 +3,7 @@ import { useEffect, useRef } from 'react' import { DEFAULT_APP_CODE } from '@cowprotocol/common-const' import { useDebounce } from '@cowprotocol/common-hooks' import { COW_PROTOCOL_ETH_FLOW_ADDRESS, getCurrencyAddress } from '@cowprotocol/common-utils' -import { getGlobalAdapter, OrderKind } from '@cowprotocol/cow-sdk' +import { getGlobalAdapter, isSolanaChain, OrderKind } from '@cowprotocol/cow-sdk' import { Currency } from '@cowprotocol/currency' import { QuoteBridgeRequest } from '@cowprotocol/sdk-bridging' import { useWalletInfo } from '@cowprotocol/wallet' @@ -75,6 +75,9 @@ export function useQuoteParams(amount: Nullish, partiallyFillable = fals if (isWrapOrUnwrap || isProviderNetworkUnsupported || isProviderNetworkDeprecated) return if (!inputCurrency || !outputCurrency || !orderKind) return + // No backend quote exists for Solana pairs; the limit-orders prototype uses a manually entered price + if (isSolanaChain(inputCurrency.chainId)) return + if (!amount) { return { quoteParams: undefined, inputCurrency, appData: appDataDoc } } From c2d37c044cfd1752ab05d13cc7331dafa950680f Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Thu, 16 Jul 2026 18:04:55 +0200 Subject: [PATCH 40/42] fix(solana): tolerate block-height-exceeded on order confirmation The tx usually lands at the edge of the blockhash validity window because the wallet-signing prompt ages the blockhash before broadcast. Re-check the signature status on a confirmation timeout instead of reporting a false failure. Co-Authored-By: Claude Fable 5 --- .../services/solanaOrderFlow/index.ts | 39 +++++++++++++++++-- .../solanaOrderFlow/solanaOrderFlow.test.ts | 23 ++++++++++- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/index.ts b/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/index.ts index ba357ad87f3..8e28d24e44a 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/index.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/index.ts @@ -1,4 +1,4 @@ -import { PublicKey, Transaction } from '@solana/web3.js' +import { Connection, PublicKey, Transaction, TransactionError } from '@solana/web3.js' import { buildCreateOrderInstructions } from './buildCreateOrderInstructions' @@ -23,10 +23,10 @@ export async function solanaOrderFlow(ctx: SolanaOrderFlowContext): Promise { + try { + const { value } = await connection.confirmTransaction({ signature, blockhash, lastValidBlockHeight }, 'confirmed') + return value.err + } catch (error) { + const { value } = await connection.getSignatureStatus(signature, { searchTransactionHistory: true }) + const landed = value?.confirmationStatus === 'confirmed' || value?.confirmationStatus === 'finalized' + + if (landed) { + return value?.err ?? null + } + + throw error + } +} + function uint8ArrayToHex(bytes: Uint8Array): string { return Array.from(bytes) .map((b) => b.toString(16).padStart(2, '0')) diff --git a/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/solanaOrderFlow.test.ts b/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/solanaOrderFlow.test.ts index c34c8dc9d50..b94e02f66cd 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/solanaOrderFlow.test.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/solanaOrderFlow.test.ts @@ -17,12 +17,13 @@ const OWNER = '54o2XBzBTkP7tmQSLu3Um9oDvLdNVrbMyQxqiYVKALLN' const BLOCKHASH = 'EkSnNWid2cvwEVnVx9aBqawnmiCNiDgp3gUdkDPTKN1N' function buildContext(): SolanaOrderFlowContext & { - connection: { getLatestBlockhash: jest.Mock; confirmTransaction: jest.Mock } + connection: { getLatestBlockhash: jest.Mock; confirmTransaction: jest.Mock; getSignatureStatus: jest.Mock } walletProvider: { sendTransaction: jest.Mock } } { const connection = { getLatestBlockhash: jest.fn().mockResolvedValue({ blockhash: BLOCKHASH, lastValidBlockHeight: 100 }), confirmTransaction: jest.fn().mockResolvedValue({ value: { err: null } }), + getSignatureStatus: jest.fn().mockResolvedValue({ value: { err: null, confirmationStatus: 'confirmed' } }), } const walletProvider = { sendTransaction: jest.fn().mockResolvedValue('mockSignature'), @@ -83,4 +84,24 @@ describe('solanaOrderFlow', () => { await expect(solanaOrderFlow(ctx)).rejects.toThrow('Solana transaction failed') }) + + it('treats a block-height-exceeded timeout as success when the tx actually landed', async () => { + const ctx = buildContext() + // confirmTransaction throws the expiry error even though the tx landed at the edge of the window + ctx.connection.confirmTransaction.mockRejectedValue(new Error('block height exceeded')) + ctx.connection.getSignatureStatus.mockResolvedValue({ value: { err: null, confirmationStatus: 'finalized' } }) + + const result = await solanaOrderFlow(ctx) + + expect(result.signature).toBe('mockSignature') + expect(ctx.connection.getSignatureStatus).toHaveBeenCalledWith('mockSignature', { searchTransactionHistory: true }) + }) + + it('rethrows the timeout when the tx never landed', async () => { + const ctx = buildContext() + ctx.connection.confirmTransaction.mockRejectedValue(new Error('block height exceeded')) + ctx.connection.getSignatureStatus.mockResolvedValue({ value: null }) + + await expect(solanaOrderFlow(ctx)).rejects.toThrow('block height exceeded') + }) }) From 94dd26da651dd3d8b08edf239d241ddc58550384 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Thu, 16 Jul 2026 18:12:29 +0200 Subject: [PATCH 41/42] fix(solana): confirm order by polling signature status confirmTransaction's blockhash strategy throws a false block-height-exceeded error when the tx lands at the edge of the validity window. Poll getSignatureStatus until the tx lands instead, so a successful order is reported as success. Co-Authored-By: Claude Fable 5 --- .../services/solanaOrderFlow/index.ts | 53 +++++++++------- .../solanaOrderFlow/solanaOrderFlow.test.ts | 62 ++++++++++++------- 2 files changed, 67 insertions(+), 48 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/index.ts b/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/index.ts index 8e28d24e44a..4238bb70091 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/index.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/index.ts @@ -6,6 +6,9 @@ import type { SolanaOrderFlowContext, SolanaOrderFlowResult } from './types' export type { SolanaOrderFlowContext, SolanaOrderFlowResult } from './types' +const CONFIRMATION_TIMEOUT_MS = 60_000 +const CONFIRMATION_POLL_INTERVAL_MS = 2_000 + export async function solanaOrderFlow(ctx: SolanaOrderFlowContext): Promise { const { connection, walletProvider, customDeadlineTimestamp, deadlineMilliseconds, ...orderParams } = ctx @@ -18,12 +21,12 @@ export async function solanaOrderFlow(ctx: SolanaOrderFlowContext): Promise { - try { - const { value } = await connection.confirmTransaction({ signature, blockhash, lastValidBlockHeight }, 'confirmed') - return value.err - } catch (error) { +async function confirmOrder(connection: Connection, signature: string): Promise { + const deadline = Date.now() + CONFIRMATION_TIMEOUT_MS + + do { const { value } = await connection.getSignatureStatus(signature, { searchTransactionHistory: true }) - const landed = value?.confirmationStatus === 'confirmed' || value?.confirmationStatus === 'finalized' - if (landed) { - return value?.err ?? null + if (value) { + if (value.err) return value.err + if (value.confirmationStatus === 'confirmed' || value.confirmationStatus === 'finalized') return null } - throw error - } + await sleep(CONFIRMATION_POLL_INTERVAL_MS) + } while (Date.now() < deadline) + + throw new Error(`Solana transaction ${signature} was not confirmed within ${CONFIRMATION_TIMEOUT_MS / 1000}s`) +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)) } function uint8ArrayToHex(bytes: Uint8Array): string { diff --git a/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/solanaOrderFlow.test.ts b/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/solanaOrderFlow.test.ts index b94e02f66cd..b6bf2fd2527 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/solanaOrderFlow.test.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/solanaOrderFlow.test.ts @@ -17,12 +17,11 @@ const OWNER = '54o2XBzBTkP7tmQSLu3Um9oDvLdNVrbMyQxqiYVKALLN' const BLOCKHASH = 'EkSnNWid2cvwEVnVx9aBqawnmiCNiDgp3gUdkDPTKN1N' function buildContext(): SolanaOrderFlowContext & { - connection: { getLatestBlockhash: jest.Mock; confirmTransaction: jest.Mock; getSignatureStatus: jest.Mock } + connection: { getLatestBlockhash: jest.Mock; getSignatureStatus: jest.Mock } walletProvider: { sendTransaction: jest.Mock } } { const connection = { getLatestBlockhash: jest.fn().mockResolvedValue({ blockhash: BLOCKHASH, lastValidBlockHeight: 100 }), - confirmTransaction: jest.fn().mockResolvedValue({ value: { err: null } }), getSignatureStatus: jest.fn().mockResolvedValue({ value: { err: null, confirmationStatus: 'confirmed' } }), } const walletProvider = { @@ -60,10 +59,8 @@ describe('solanaOrderFlow', () => { expect(tx.feePayer?.toBase58()).toBe(OWNER) expect(tx.recentBlockhash).toBe(BLOCKHASH) - expect(ctx.connection.confirmTransaction).toHaveBeenCalledWith( - { signature: 'mockSignature', blockhash: BLOCKHASH, lastValidBlockHeight: 100 }, - 'confirmed', - ) + // Confirmation is done by polling the signature status, not confirmTransaction's blockhash strategy + expect(ctx.connection.getSignatureStatus).toHaveBeenCalledWith('mockSignature', { searchTransactionHistory: true }) }) it('uses the custom deadline timestamp as validTo when set', async () => { @@ -78,30 +75,47 @@ describe('solanaOrderFlow', () => { expect(createOrderData.readUInt32LE(113)).toBe(1893456000) }) - it('throws when on-chain confirmation reports an error', async () => { + it('throws when the signature status reports an on-chain error', async () => { const ctx = buildContext() - ctx.connection.confirmTransaction.mockResolvedValue({ value: { err: { InstructionError: [2, 'Custom'] } } }) + ctx.connection.getSignatureStatus.mockResolvedValue({ + value: { err: { InstructionError: [2, 'Custom'] }, confirmationStatus: 'confirmed' }, + }) await expect(solanaOrderFlow(ctx)).rejects.toThrow('Solana transaction failed') }) - it('treats a block-height-exceeded timeout as success when the tx actually landed', async () => { - const ctx = buildContext() - // confirmTransaction throws the expiry error even though the tx landed at the edge of the window - ctx.connection.confirmTransaction.mockRejectedValue(new Error('block height exceeded')) - ctx.connection.getSignatureStatus.mockResolvedValue({ value: { err: null, confirmationStatus: 'finalized' } }) - - const result = await solanaOrderFlow(ctx) - - expect(result.signature).toBe('mockSignature') - expect(ctx.connection.getSignatureStatus).toHaveBeenCalledWith('mockSignature', { searchTransactionHistory: true }) + it('keeps polling until the tx reaches a confirmed status', async () => { + jest.useFakeTimers() + try { + const ctx = buildContext() + ctx.connection.getSignatureStatus + // still processing on the first poll (mirrors a tx that lands a moment later) + .mockResolvedValueOnce({ value: { err: null, confirmationStatus: 'processed' } }) + .mockResolvedValueOnce({ value: { err: null, confirmationStatus: 'confirmed' } }) + + const promise = solanaOrderFlow(ctx) + await jest.advanceTimersByTimeAsync(2_000) + const result = await promise + + expect(result.signature).toBe('mockSignature') + expect(ctx.connection.getSignatureStatus).toHaveBeenCalledTimes(2) + } finally { + jest.useRealTimers() + } }) - it('rethrows the timeout when the tx never landed', async () => { - const ctx = buildContext() - ctx.connection.confirmTransaction.mockRejectedValue(new Error('block height exceeded')) - ctx.connection.getSignatureStatus.mockResolvedValue({ value: null }) - - await expect(solanaOrderFlow(ctx)).rejects.toThrow('block height exceeded') + it('throws when the tx is not confirmed within the timeout', async () => { + jest.useFakeTimers() + try { + const ctx = buildContext() + ctx.connection.getSignatureStatus.mockResolvedValue({ value: null }) + + const promise = solanaOrderFlow(ctx) + const expectation = expect(promise).rejects.toThrow('was not confirmed within') + await jest.advanceTimersByTimeAsync(61_000) + await expectation + } finally { + jest.useRealTimers() + } }) }) From 37055885e06e556df505c176b567f870cf82be9f Mon Sep 17 00:00:00 2001 From: "cowswap-release-sync[bot]" <274575433+cowswap-release-sync[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2026 16:16:23 +0000 Subject: [PATCH 42/42] chore(i18n): extract i18n strings [automatic] --- apps/cowswap-frontend/src/locales/en-US.po | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/apps/cowswap-frontend/src/locales/en-US.po b/apps/cowswap-frontend/src/locales/en-US.po index b198f1077b3..7a825132242 100644 --- a/apps/cowswap-frontend/src/locales/en-US.po +++ b/apps/cowswap-frontend/src/locales/en-US.po @@ -1462,6 +1462,7 @@ msgid "This app/hook can only be used as a <0>{hookType}-hook" msgstr "This app/hook can only be used as a <0>{hookType}-hook" #: apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersConfirmModal/index.tsx +#: apps/cowswap-frontend/src/modules/limitOrders/containers/SolanaTradeButtons/index.tsx msgid "Place limit order" msgstr "Place limit order" @@ -3840,6 +3841,7 @@ msgstr "Import token" #~ msgid "Interface Settings" #~ msgstr "Interface Settings" +#: apps/cowswap-frontend/src/modules/limitOrders/containers/SolanaTradeButtons/index.tsx #: apps/cowswap-frontend/src/modules/limitOrders/containers/TradeButtons/limitOrdersTradeButtonsMap.tsx msgid "Enter a price" msgstr "Enter a price" @@ -4059,6 +4061,7 @@ msgstr "You are not eligible for this airdrop" #: apps/cowswap-frontend/src/common/pure/CurrencySelectButton/index.tsx #: apps/cowswap-frontend/src/common/pure/CurrencySelectButton/index.tsx +#: apps/cowswap-frontend/src/modules/limitOrders/containers/SolanaTradeButtons/index.tsx #: apps/cowswap-frontend/src/modules/tradeFormValidation/pure/TradeFormButtons/tradeButtonsMap.tsx msgid "Select a token" msgstr "Select a token" @@ -4345,6 +4348,7 @@ msgstr "Only the {limit} most recent orders were searched." #: apps/cowswap-frontend/src/modules/affiliate/containers/AffiliatePartnerOnboard.tsx #: apps/cowswap-frontend/src/modules/affiliate/containers/AffiliateTraderOnboard.tsx #: apps/cowswap-frontend/src/modules/hooksStore/dapps/PermitHookApp/index.tsx +#: apps/cowswap-frontend/src/modules/limitOrders/containers/SolanaTradeButtons/index.tsx #: apps/cowswap-frontend/src/modules/wallet/containers/ConnectWalletModal/index.tsx #: apps/cowswap-frontend/src/modules/wallet/containers/WalletStatusButton/WalletStatusButton.container.tsx msgid "Connect wallet" @@ -4991,6 +4995,10 @@ msgstr "This percentage only applies to dips; if prices are better than this per msgid "Your wallet is already linked to a referral code." msgstr "Your wallet is already linked to a referral code." +#: apps/cowswap-frontend/src/modules/limitOrders/containers/SolanaTradeButtons/index.tsx +msgid "View on Solscan" +msgstr "View on Solscan" + #: apps/cowswap-frontend/src/modules/tradeFormValidation/pure/TradeFormButtons/tradeButtonsMap.tsx msgid "Confirm recipient to swap" msgstr "Confirm recipient to swap" @@ -6115,6 +6123,7 @@ msgstr "You receive exactly" msgid "Checking" msgstr "Checking" +#: apps/cowswap-frontend/src/modules/limitOrders/containers/SolanaTradeButtons/index.tsx #: apps/cowswap-frontend/src/modules/tradeFormValidation/pure/TradeFormButtons/tradeButtonsMap.tsx msgid "Enter an amount" msgstr "Enter an amount" @@ -7029,6 +7038,10 @@ msgstr "Your order expired" msgid "Add custom hook" msgstr "Add custom hook" +#: apps/cowswap-frontend/src/modules/limitOrders/containers/SolanaTradeButtons/index.tsx +msgid "Solana limit order created" +msgstr "Solana limit order created" + #: apps/cowswap-frontend/src/legacy/components/Tokens/TokensTableRow.tsx msgid "Approve {symbol}" msgstr "Approve {symbol}" @@ -8177,6 +8190,7 @@ msgstr "partially" msgid "View on Bridge Explorer ↗" msgstr "View on Bridge Explorer ↗" +#: apps/cowswap-frontend/src/modules/limitOrders/containers/SolanaTradeButtons/index.tsx #: apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure.tsx #: apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTable/Row/WarningEstimatedPrice/OrderRowWarningEstimatedPrice.pure.tsx msgid "Insufficient balance"