diff --git a/apps/cowswap-frontend/package.json b/apps/cowswap-frontend/package.json index 5d528fb410f..02a07a9cebe 100644 --- a/apps/cowswap-frontend/package.json +++ b/apps/cowswap-frontend/package.json @@ -67,10 +67,12 @@ "@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", "@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", @@ -79,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/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..b3c153c5d1a 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' @@ -14,12 +14,13 @@ interface ConfirmationModalContext { activePromise?: Promise title: string callToAction: string - description?: string + description?: ReactNode warning?: string confirmWord: string 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.test.tsx b/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.test.tsx new file mode 100644 index 00000000000..b30cfd25e1b --- /dev/null +++ b/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.test.tsx @@ -0,0 +1,146 @@ +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 { CrossChainFamilySwitchState, 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 NOT_CROSSING_CHAIN for a same-family change without prompting', async () => { + setWallet(SupportedChainId.MAINNET, '0xConnected') + const { result } = renderHook(() => useCrossChainFamilySwitch()) + + let handled: CrossChainFamilySwitchState | undefined + await act(async () => { + handled = await result.current(SupportedChainId.ARBITRUM_ONE) + }) + + expect(handled).toBe(CrossChainFamilySwitchState.NOT_CROSSING_CHAIN) + expect(triggerConfirmation).not.toHaveBeenCalled() + expect(disconnectWallet).not.toHaveBeenCalled() + }) + + it('returns WALLET_NOT_CONNECTED for a cross-family change when no wallet is connected', async () => { + setWallet(SupportedChainId.MAINNET, undefined) + const { result } = renderHook(() => useCrossChainFamilySwitch()) + + let handled: CrossChainFamilySwitchState | undefined + await act(async () => { + handled = await result.current(SupportedChainId.SOLANA) + }) + + expect(handled).toBe(CrossChainFamilySwitchState.WALLET_NOT_CONNECTED) + expect(triggerConfirmation).not.toHaveBeenCalled() + expect(disconnectWallet).not.toHaveBeenCalled() + }) + + 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: CrossChainFamilySwitchState | undefined + await act(async () => { + handled = await result.current(SupportedChainId.SOLANA) + }) + + expect(handled).toBe(CrossChainFamilySwitchState.FINISHED) + expect(triggerConfirmation).toHaveBeenCalledWith(expect.objectContaining({ skipInput: true })) + expect(setChainIdToUrl).toHaveBeenCalledWith(SupportedChainId.SOLANA) + expect(disconnectWallet).toHaveBeenCalled() + expect(openWalletConnectionModal).toHaveBeenCalled() + expect(closeModal).toHaveBeenCalled() + }) + + 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: CrossChainFamilySwitchState | undefined + await act(async () => { + handled = await result.current(SupportedChainId.SOLANA) + }) + + expect(handled).toBe(CrossChainFamilySwitchState.NOT_CONFIRMED) + expect(disconnectWallet).not.toHaveBeenCalled() + expect(openWalletConnectionModal).not.toHaveBeenCalled() + expect(closeModal).not.toHaveBeenCalled() + }) + + 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: CrossChainFamilySwitchState | undefined + await act(async () => { + handled = await result.current(SupportedChainId.SOLANA) + }) + + expect(handled).toBe(CrossChainFamilySwitchState.DISCONNECT_FAILED) + 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) + 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..5c410bee15d --- /dev/null +++ b/apps/cowswap-frontend/src/common/hooks/useCrossChainFamilySwitch.tsx @@ -0,0 +1,117 @@ +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' + +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' + +import { useConfirmationRequest } from './useConfirmationRequest' +import { useLegacySetChainIdToUrl } from './useLegacySetChainIdToUrl' + +export enum CrossChainFamilySwitchState { + WALLET_NOT_CONNECTED = 'WALLET_NOT_CONNECTED', + NOT_CROSSING_CHAIN = 'NOT_CROSSING_CHAIN', + NOT_CONFIRMED = 'NOT_CONFIRMED', + DISCONNECT_FAILED = 'DISCONNECT_FAILED', + FINISHED = '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). + * + * 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 (targetChainId: SupportedChainId, skipClose?: boolean) => { + const isWalletConnected = !!account + const crossingChainFamily = !isSameChainFamily(currentChainId, targetChainId) + + 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 + + const confirmed = await triggerConfirmation({ + confirmWord: t`confirm`, + title: t`Switching network type`, + 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, + bottomContent: null, + }) + + if (!confirmed) { + return CrossChainFamilySwitchState.NOT_CONFIRMED + } + + 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 CrossChainFamilySwitchState.DISCONNECT_FAILED + } + + setChainIdToUrl(targetChainId) + openWalletConnectionModal() + + if (!skipClose) { + closeModal() + } + + return CrossChainFamilySwitchState.FINISHED + }, + [ + 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..3a13da503e2 --- /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 { CrossChainFamilySwitchState, 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(CrossChainFamilySwitchState.NOT_CONFIRMED) + 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..0ac4665226b 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,28 +12,35 @@ 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 { CrossChainFamilySwitchState, 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. + const switchChainState = await handleCrossChainFamilySwitch(targetChain, skipClose) + + if (switchChainState === CrossChainFamilySwitchState.NOT_CONFIRMED) { + return + } + try { - setWalletConnectionError(undefined) await switchNetwork(targetChain) 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) { @@ -52,14 +59,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/common/pure/ConfirmationModal/ConfirmationModal.tsx b/apps/cowswap-frontend/src/common/pure/ConfirmationModal/ConfirmationModal.tsx index d486a86e9d2..f6f2b6b9de5 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' @@ -27,18 +29,17 @@ const Warning = styled.strong` export interface ConfirmationModalProps { isOpen: boolean title: string - description?: string + description?: ReactNode warning?: string callToAction?: string onDismiss: Command 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}. diff --git a/apps/cowswap-frontend/src/locales/en-US.po b/apps/cowswap-frontend/src/locales/en-US.po index b2bd90eba4a..7a825132242 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" @@ -1037,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" @@ -1446,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" @@ -3824,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" @@ -4043,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" @@ -4325,9 +4344,11 @@ 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 +#: 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" @@ -4974,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" @@ -6098,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" @@ -7012,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}" @@ -7754,6 +7784,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" @@ -7818,6 +7852,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" @@ -7908,6 +7946,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" @@ -8044,6 +8086,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" @@ -8147,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" 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/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 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, + ]) +} 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, + } +} 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/index.ts b/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/index.ts new file mode 100644 index 00000000000..4238bb70091 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/index.ts @@ -0,0 +1,79 @@ +import { Connection, PublicKey, Transaction, TransactionError } from '@solana/web3.js' + +import { buildCreateOrderInstructions } from './buildCreateOrderInstructions' + +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 + + // 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 } = await connection.getLatestBlockhash() + transaction.recentBlockhash = blockhash + + const signature = await walletProvider.sendTransaction(transaction, connection) + + const err = await confirmOrder(connection, signature) + + if (err) { + throw new Error(`Solana transaction failed: ${JSON.stringify(err)}`) + } + + return { + signature, + orderUid: uint8ArrayToHex(orderUid), + orderPda: orderPda.toBase58(), + } +} + +/** + * Wait for the transaction to land by polling its signature status. + * + * We deliberately avoid `connection.confirmTransaction`'s blockhash strategy: it throws + * `TransactionExpiredBlockheightExceededError` ("block height exceeded") whenever the blockhash + * validity window passes before it observes confirmation — which happens routinely here because + * the wallet-signing prompt ages the blockhash before the tx even broadcasts, so the tx lands + * right at the edge of the window and is reported as a failure despite succeeding. Polling the + * signature status instead reflects what actually happened on-chain. + * + * Returns the on-chain error (null on success); throws only if the tx never lands within the timeout. + */ +async function confirmOrder(connection: Connection, signature: string): Promise { + const deadline = Date.now() + CONFIRMATION_TIMEOUT_MS + + do { + const { value } = await connection.getSignatureStatus(signature, { searchTransactionHistory: true }) + + if (value) { + if (value.err) return value.err + if (value.confirmationStatus === 'confirmed' || value.confirmationStatus === 'finalized') return null + } + + 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 { + return Array.from(bytes) + .map((b) => b.toString(16).padStart(2, '0')) + .join('') +} 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] +} 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..b6bf2fd2527 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/limitOrders/services/solanaOrderFlow/solanaOrderFlow.test.ts @@ -0,0 +1,121 @@ +/** + * @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; getSignatureStatus: jest.Mock } + walletProvider: { sendTransaction: jest.Mock } +} { + const connection = { + getLatestBlockhash: jest.fn().mockResolvedValue({ blockhash: BLOCKHASH, lastValidBlockHeight: 100 }), + getSignatureStatus: jest.fn().mockResolvedValue({ value: { err: null, confirmationStatus: 'confirmed' } }), + } + 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) + + // 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 () => { + 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 the signature status reports an on-chain error', async () => { + const ctx = buildContext() + ctx.connection.getSignatureStatus.mockResolvedValue({ + value: { err: { InstructionError: [2, 'Custom'] }, confirmationStatus: 'confirmed' }, + }) + + await expect(solanaOrderFlow(ctx)).rejects.toThrow('Solana transaction failed') + }) + + 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('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() + } + }) +}) 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 +} 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/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/hooks/useOpenTokenSelectWidget.ts b/apps/cowswap-frontend/src/modules/tokensList/hooks/useOpenTokenSelectWidget.ts index b926c7db087..62fa3b6824e 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,41 @@ export function useOpenTokenSelectWidget(): ( forceOpen: false, selectedTargetChainId: nextSelectedTargetChainId, tradeType, - onSelectToken: (currency) => { + onSelectToken: async (currency) => { + 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 = + chainSwitchState !== CrossChainFamilySwitchState.NOT_CROSSING_CHAIN && + chainSwitchState !== CrossChainFamilySwitchState.NOT_CONFIRMED + + /** + * 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/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/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, 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 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 } } 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/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; 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: { 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 new file mode 100644 index 00000000000..b9a9260be4d --- /dev/null +++ b/docs/superpowers/specs/2026-07-16-solana-limit-orders-design.md @@ -0,0 +1,136 @@ +# 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. +- 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. + +## 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. 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..89f88d76114 --- /dev/null +++ b/libs/balances-and-allowances/src/hooks/usePersistSolanaBalancesViaWebCalls.test.tsx @@ -0,0 +1,232 @@ +import { Provider, useAtomValue } from 'jotai' +import { useHydrateAtoms } from 'jotai/utils' +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' + +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 + +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 }), +})) + +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 }, + _owner: unknown, + _allowOwnerOffCurve: boolean, + programId: string, + ) => ({ toBase58: () => `ata:${programId}:${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(() => { + mockTokensByAddress = {} + mockAmountByAta = { [ataKey(MINT_A)]: 100n, [ataKey(MINT_B)]: 250n } + mockInfoByAta = { [ataKey(MINT_A)]: { present: true }, [ataKey(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[ataKey(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[ataKey(mint)] = 7n + mockInfoByAta[ataKey(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(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 } + 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 new file mode 100644 index 00000000000..734a092a385 --- /dev/null +++ b/libs/balances-and-allowances/src/hooks/usePersistSolanaBalancesViaWebCalls.ts @@ -0,0 +1,168 @@ +import { useSetAtom } from 'jotai' +import { useEffect, useMemo } from 'react' + +import { skipToken, 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' + +import { useIsBlockNumberRelevant } from './useIsBlockNumberRelevant' +import { PersistBalancesAndAllowancesParams } from './usePersistBalancesViaWebCalls' + +import { fetchSolanaTokenBalances, SolanaTokenMint } 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) + + const tokensByAddress = useTokensByAddressMapForChain(chainId) + + const tokenMints = useMemo( + () => buildSolanaTokenMints(tokenAddresses, chainId, tokensByAddress), + [tokenAddresses, chainId, tokensByAddress], + ) + + 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: connection && account ? () => fetchSolanaTokenBalances(connection, account, tokenMints) : skipToken, + 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], + [getAddressKey(account)]: Date.now(), + }, + })) + } + }, [ + isSolana, + chainId, + account, + balances, + isNewData, + setBalances, + setLoadingState, + onBalancesLoaded, + setBalancesUpdate, + ]) +} + +// 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, + 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/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/services/fetchSolanaTokenBalances.test.ts b/libs/balances-and-allowances/src/services/fetchSolanaTokenBalances.test.ts new file mode 100644 index 00000000000..1f928c3a543 --- /dev/null +++ b/libs/balances-and-allowances/src/services/fetchSolanaTokenBalances.test.ts @@ -0,0 +1,168 @@ +/** + * @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('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>()) + + 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 new file mode 100644 index 00000000000..f0c58021200 --- /dev/null +++ b/libs/balances-and-allowances/src/services/fetchSolanaTokenBalances.ts @@ -0,0 +1,95 @@ +import { + ASSOCIATED_TOKEN_PROGRAM_ID, + getAssociatedTokenAddressSync, + TOKEN_2022_PROGRAM_ID, + TOKEN_PROGRAM_ID, + unpackAccount, +} from '@solana/spl-token' +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 + 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 `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. 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, + tokens: SolanaTokenMint[], +): Promise { + const owner = new PublicKey(ownerAddress) + + // 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 })) + + // 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. + } + }) + + const ataInfos = await getMultipleAccountsInfoBatched( + connection, + resolvable.map(({ ata }) => ata), + ) + + ataInfos.forEach((info, i) => { + if (!info) return + + const { index, ata, programId } = resolvable[i] + balances[index].balance = unpackAccount(ata, info, programId).amount + }) + + return balances +} + +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/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 diff --git a/libs/common-const/src/featureFlags.ts b/libs/common-const/src/featureFlags.ts index 06240a431b0..f0ebcfca475 100644 --- a/libs/common-const/src/featureFlags.ts +++ b/libs/common-const/src/featureFlags.ts @@ -2,4 +2,14 @@ export type FeatureFlags = Record export type FeatureFlagValue = boolean | number | undefined +if (typeof location !== 'undefined') { + const value = new URLSearchParams(location.hash.split('?')[1]).get('IS_SOLANA_ENABLED') + + if (value === 'true') { + localStorage.setItem('IS_SOLANA_ENABLED', '1') + } else if (value === 'false') { + localStorage.removeItem('IS_SOLANA_ENABLED') + } +} + export const IS_SOLANA_ENABLED = typeof localStorage !== 'undefined' && !!localStorage.getItem('IS_SOLANA_ENABLED') 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/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 +} diff --git a/libs/common-utils/src/index.ts b/libs/common-utils/src/index.ts index 3563926a9ec..898ff3e8c3f 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 +} diff --git a/libs/wallet/src/wagmi/config.ts b/libs/wallet/src/wagmi/config.ts index 501df38f053..9569dfe5002 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 }] } @@ -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], diff --git a/libs/wallet/src/wagmi/hooks/useSwitchNetwork.ts b/libs/wallet/src/wagmi/hooks/useSwitchNetwork.ts index edbd38ef8ee..2c7e4b4d92e 100644 --- a/libs/wallet/src/wagmi/hooks/useSwitchNetwork.ts +++ b/libs/wallet/src/wagmi/hooks/useSwitchNetwork.ts @@ -1,25 +1,32 @@ import { useSetAtom } from 'jotai' import { useCallback } from 'react' -import { useConnection, useSwitchChain } from 'wagmi' +import { useConnection } from 'wagmi' import { SupportedChainId } from '@cowprotocol/cow-sdk' import { walletInfoAtom } from '../../api/state' +import { SUPPORTED_REOWN_NETWORKS } from '../../reown/networks' +import { reownAppKit } from '../config' export function useSwitchNetwork(): (chainId: SupportedChainId) => Promise { - const { mutateAsync: switchChain } = useSwitchChain() const { isConnected } = useConnection() const setWalletInfo = useSetAtom(walletInfoAtom) 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 reownAppKit.switchNetwork(network, { throwOnFailure: true }) } else { setWalletInfo((prev) => ({ ...prev, chainId })) } }, - [switchChain, isConnected, setWalletInfo], + [isConnected, setWalletInfo], ) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2678714e570..ebcc89fe7c8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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) @@ -659,6 +662,9 @@ importers: '@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) + '@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) '@reown/appkit-adapter-wagmi': specifier: 1.8.19 version: 1.8.19(patch_hash=b669cb20e36d425602b690643d4147fd1756aa3d94db205f5e4823777a277b1c)(725f042c52896b22a29c385487b96d7a) @@ -680,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 @@ -1174,7 +1186,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 +1304,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 @@ -1438,6 +1450,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@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@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@5.0.10) + '@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) @@ -1452,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 @@ -1547,7 +1571,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 @@ -1638,7 +1662,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 @@ -1730,7 +1754,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 @@ -1816,7 +1840,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 @@ -1926,7 +1950,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 @@ -2075,7 +2099,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 @@ -2120,46 +2144,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 @@ -2183,10 +2207,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 @@ -2211,7 +2235,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 @@ -18680,6 +18704,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) @@ -18924,6 +18972,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)) @@ -18945,9 +19014,22 @@ 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.4.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 + '@noble/hashes': 1.4.0 clsx: 1.2.1 eventemitter3: 5.0.4 preact: 10.28.2 @@ -21108,7 +21190,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) @@ -21117,7 +21199,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: @@ -21127,12 +21209,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 @@ -21144,7 +21226,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 @@ -21159,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) @@ -21174,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) @@ -22752,6 +22834,54 @@ 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@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@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@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@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@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 + bs58: 6.0.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 + - zod + '@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) @@ -22815,7 +22945,57 @@ snapshots: 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)) + '@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) + '@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' @@ -22850,6 +23030,17 @@ snapshots: - utf-8-validate - zod + '@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: + 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-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 @@ -22861,6 +23052,35 @@ snapshots: - 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@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-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' + - '@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-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) @@ -22890,6 +23110,40 @@ snapshots: - 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@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-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) + 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) @@ -22928,6 +23182,42 @@ snapshots: 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) @@ -22964,6 +23254,36 @@ snapshots: - 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 @@ -22994,6 +23314,49 @@ 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@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) @@ -23037,6 +23400,17 @@ snapshots: - 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) @@ -23048,6 +23422,49 @@ snapshots: - 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) @@ -23375,6 +23792,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) @@ -23407,6 +23837,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 @@ -23424,6 +23871,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) @@ -23436,9 +23895,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 @@ -23447,6 +23906,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) @@ -23457,6 +23927,7 @@ snapshots: - typescript - utf-8-validate - zod + optional: true '@safe-global/safe-deployments@1.37.50': dependencies: @@ -23661,11 +24132,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) @@ -23705,6 +24186,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 @@ -23881,6 +24374,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) @@ -24030,6 +24555,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) @@ -24054,6 +24593,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) @@ -24146,6 +24706,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) @@ -24154,6 +24722,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) @@ -24162,6 +24738,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 @@ -24196,6 +24787,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) @@ -24253,6 +24864,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 @@ -24276,11 +24895,34 @@ 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.4.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 '@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 @@ -25895,6 +26537,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)) @@ -25906,6 +26570,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 @@ -25971,6 +26650,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 @@ -26202,6 +26919,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) @@ -26337,6 +27084,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 @@ -28836,12 +29617,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 @@ -31049,6 +31830,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) @@ -31164,6 +31949,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 @@ -35321,11 +36124,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 @@ -36929,7 +37732,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)) @@ -37648,6 +38497,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