diff --git a/.changeset/kind-windows-sparkle.md b/.changeset/kind-windows-sparkle.md new file mode 100644 index 000000000..ece0fc6c6 --- /dev/null +++ b/.changeset/kind-windows-sparkle.md @@ -0,0 +1,5 @@ +--- +'@reservoir0x/relay-kit-ui': patch +--- + +Allow overriding chain to connector list diff --git a/demo/pages/_app.tsx b/demo/pages/_app.tsx index 43e8eba8f..a7852f2cd 100644 --- a/demo/pages/_app.tsx +++ b/demo/pages/_app.tsx @@ -6,7 +6,6 @@ import React, { ReactNode, FC, useState, useEffect } from 'react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { createConfig, http, WagmiProvider } from 'wagmi' import { Chain, mainnet } from 'wagmi/chains' -import { RelayKitProvider } from '@reservoir0x/relay-kit-ui' import { useRelayChains } from '@reservoir0x/relay-kit-hooks' import { LogLevel, @@ -30,6 +29,7 @@ import { chainIdToAlchemyNetworkMap } from 'utils/chainIdToAlchemyNetworkMap' import { useWalletFilter, WalletFilterProvider } from 'context/walletFilter' import { pipe } from '@dynamic-labs/utils' import { EclipseWalletConnectors } from '@dynamic-labs/eclipse' +import { RelayKitProvider } from '@reservoir0x/relay-kit-ui' type AppWrapperProps = { children: ReactNode diff --git a/packages/ui/src/components/common/CustomAddressModal.tsx b/packages/ui/src/components/common/CustomAddressModal.tsx index f907feeb3..e054409cf 100644 --- a/packages/ui/src/components/common/CustomAddressModal.tsx +++ b/packages/ui/src/components/common/CustomAddressModal.tsx @@ -1,4 +1,4 @@ -import { type FC, useState, useEffect, useMemo } from 'react' +import { type FC, useState, useEffect, useMemo, useContext } from 'react' import { Text, Flex, Button, Input, Pill } from '../primitives/index.js' import { Modal } from '../common/Modal.js' import { type Address } from 'viem' @@ -17,7 +17,7 @@ import type { AdaptedWallet, RelayChain } from '@reservoir0x/relay-sdk' import type { LinkedWallet } from '../../types/index.js' import { truncateAddress } from '../../utils/truncate.js' import { isValidAddress } from '../../utils/address.js' -import { eclipse, eclipseWallets } from '../../utils/solana.js' +import { ProviderOptionsContext } from '../../providers/RelayKitProvider.js' type Props = { open: boolean @@ -50,6 +50,8 @@ export const CustomAddressModal: FC = ({ const connectedAddress = useWalletAddress(wallet, linkedWallets) const [address, setAddress] = useState('') const [input, setInput] = useState('') + const providerOptionsContext = useContext(ProviderOptionsContext) + const connectorKeyOverrides = providerOptionsContext.vmConnectorKeyOverrides const availableWallets = useMemo( () => @@ -58,7 +60,8 @@ export const CustomAddressModal: FC = ({ toChain?.vmType, wallet.address, toChain?.id, - wallet.connector + wallet.connector, + connectorKeyOverrides ) ), [toChain, linkedWallets] diff --git a/packages/ui/src/components/common/MultiWalletDropdown.tsx b/packages/ui/src/components/common/MultiWalletDropdown.tsx index 8bd138e55..464de8619 100644 --- a/packages/ui/src/components/common/MultiWalletDropdown.tsx +++ b/packages/ui/src/components/common/MultiWalletDropdown.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState, type FC } from 'react' +import { useContext, useMemo, useState, type FC } from 'react' import { Dropdown, DropdownMenuItem } from '../primitives/Dropdown.js' import { Box, Button, Flex, Text } from '../primitives/index.js' import type { LinkedWallet } from '../../types/index.js' @@ -10,6 +10,7 @@ import { eclipse, eclipseWallets, solana } from '../../utils/solana.js' import { useENSResolver } from '../../hooks/index.js' import { EventNames } from '../../constants/events.js' import { isValidAddress } from '../../utils/address.js' +import { ProviderOptionsContext } from '../../providers/RelayKitProvider.js' type MultiWalletDropdownProps = { context: 'origin' | 'destination' @@ -33,20 +34,30 @@ export const MultiWalletDropdown: FC = ({ setAddressModalOpen }) => { const [open, setOpen] = useState(false) + const providerOptionsContext = useContext(ProviderOptionsContext) + const connectorKeyOverrides = providerOptionsContext.vmConnectorKeyOverrides const filteredWallets = useMemo(() => { if (!chain) return wallets + + let eclipseConnectorKeys: string[] | undefined = undefined + if (connectorKeyOverrides && connectorKeyOverrides[eclipse.id]) { + eclipseConnectorKeys = connectorKeyOverrides[eclipse.id] + } else if (chain.vmType === 'svm') { + eclipseConnectorKeys = eclipseWallets + } + return wallets.filter((wallet) => { if (wallet.vmType !== chain.vmType) { return false } if ( chain.id === eclipse.id && - !eclipseWallets.includes(wallet.connector.toLowerCase()) + !eclipseConnectorKeys!.includes(wallet.connector.toLowerCase()) ) { return false } else if ( chain.id === solana.id && - eclipseWallets.includes(wallet.connector.toLowerCase()) + eclipseConnectorKeys!.includes(wallet.connector.toLowerCase()) ) { return false } @@ -65,9 +76,16 @@ export const MultiWalletDropdown: FC = ({ chain?.vmType, selectedWalletAddress, chain?.id, - selectedWallet?.connector + selectedWallet?.connector, + connectorKeyOverrides ), - [selectedWalletAddress, selectedWallet, chain?.vmType, chain?.id] + [ + selectedWalletAddress, + selectedWallet, + chain?.vmType, + chain?.id, + connectorKeyOverrides + ] ) const showDropdown = context !== 'origin' || filteredWallets.length > 0 diff --git a/packages/ui/src/components/widgets/SwapWidget/index.tsx b/packages/ui/src/components/widgets/SwapWidget/index.tsx index b825e316e..fe719b926 100644 --- a/packages/ui/src/components/widgets/SwapWidget/index.tsx +++ b/packages/ui/src/components/widgets/SwapWidget/index.tsx @@ -1,5 +1,5 @@ import { Flex, Button, Text, Box } from '../../primitives/index.js' -import { useEffect, useState, type FC } from 'react' +import { useContext, useEffect, useState, type FC } from 'react' import { useMounted, useRelayClient } from '../../../hooks/index.js' import type { Address } from 'viem' import { formatUnits, zeroAddress } from 'viem' @@ -35,6 +35,7 @@ import { ASSETS_RELAY_API } from '@reservoir0x/relay-sdk' import SwapRouteSelector from '../SwapRouteSelector.js' +import { ProviderOptionsContext } from '../../../providers/RelayKitProvider.js' type BaseSwapWidgetProps = { defaultFromToken?: Token @@ -99,6 +100,8 @@ const SwapWidget: FC = ({ onSwapError }) => { const relayClient = useRelayClient() + const providerOptionsContext = useContext(ProviderOptionsContext) + const connectorKeyOverrides = providerOptionsContext.vmConnectorKeyOverrides const [transactionModalOpen, setTransactionModalOpen] = useState(false) const [addressModalOpen, setAddressModalOpen] = useState(false) const isMounted = useMounted() @@ -223,7 +226,8 @@ const SwapWidget: FC = ({ const supportedAddress = findSupportedWallet( fromChain, address, - linkedWallets + linkedWallets, + connectorKeyOverrides ) if (supportedAddress) { onSetPrimaryWallet?.(supportedAddress) @@ -235,7 +239,8 @@ const SwapWidget: FC = ({ address, linkedWallets, onSetPrimaryWallet, - isValidFromAddress + isValidFromAddress, + connectorKeyOverrides ]) const promptSwitchRoute = @@ -411,9 +416,9 @@ const SwapWidget: FC = ({ isSingleChainLocked ? [lockChainId] : fromToken?.chainId !== undefined && - fromToken?.chainId === lockChainId - ? [fromToken?.chainId] - : undefined + fromToken?.chainId === lockChainId + ? [fromToken?.chainId] + : undefined } restrictedTokensList={tokens?.filter( (token) => token.chainId === fromToken?.chainId @@ -757,9 +762,9 @@ const SwapWidget: FC = ({ isSingleChainLocked ? [lockChainId] : toToken?.chainId !== undefined && - toToken?.chainId === lockChainId - ? [toToken?.chainId] - : undefined + toToken?.chainId === lockChainId + ? [toToken?.chainId] + : undefined } restrictedTokensList={tokens?.filter( (token) => token.chainId === toToken?.chainId diff --git a/packages/ui/src/components/widgets/SwapWidgetRenderer.tsx b/packages/ui/src/components/widgets/SwapWidgetRenderer.tsx index 1a7b660fc..f2d28d134 100644 --- a/packages/ui/src/components/widgets/SwapWidgetRenderer.tsx +++ b/packages/ui/src/components/widgets/SwapWidgetRenderer.tsx @@ -144,6 +144,7 @@ const SwapWidgetRenderer: FC = ({ onAnalyticEvent }) => { const providerOptionsContext = useContext(ProviderOptionsContext) + const connectorKeyOverrides = providerOptionsContext.vmConnectorKeyOverrides const relayClient = useRelayClient() const { connector } = useAccount() const [customToAddress, setCustomToAddress] = useState< @@ -207,7 +208,8 @@ const SwapWidgetRenderer: FC = ({ toChain?.id, !customToAddress && _linkedWallet?.address === address ? _linkedWallet?.connector - : undefined + : undefined, + connectorKeyOverrides ) if ( multiWalletSupportEnabled && @@ -218,7 +220,8 @@ const SwapWidgetRenderer: FC = ({ const supportedAddress = findSupportedWallet( toChain, customToAddress, - linkedWallets + linkedWallets, + connectorKeyOverrides ) return supportedAddress @@ -326,13 +329,15 @@ const SwapWidgetRenderer: FC = ({ fromChain?.vmType, address ?? '', fromChain?.id, - linkedWallet?.connector + linkedWallet?.connector, + connectorKeyOverrides ) const fromAddressWithFallback = addressWithFallback( fromChain?.vmType, address, fromChain?.id, - linkedWallet?.connector + linkedWallet?.connector, + connectorKeyOverrides ) const isValidToAddress = isValidAddress(toChain?.vmType, recipient ?? '') diff --git a/packages/ui/src/providers/RelayKitProvider.tsx b/packages/ui/src/providers/RelayKitProvider.tsx index 5b1fb2a20..937356313 100644 --- a/packages/ui/src/providers/RelayKitProvider.tsx +++ b/packages/ui/src/providers/RelayKitProvider.tsx @@ -1,28 +1,41 @@ import { createContext, useMemo } from 'react' import type { FC, ReactNode } from 'react' import { RelayClientProvider } from './RelayClientProvider.js' -import type { RelayClientOptions, paths } from '@reservoir0x/relay-sdk' +import type { ChainVM, RelayClientOptions, paths } from '@reservoir0x/relay-sdk' import type { RelayKitTheme } from '../themes/index.js' import { generateCssVars } from '../utils/theme.js' -export type CoinId = { - [key: string]: string -} -export type CoinGecko = { - proxy?: string - apiKey?: string - coinIds?: CoinId -} - export type AppFees = paths['/quote']['post']['requestBody']['content']['application/json']['appFees'] type RelayKitProviderOptions = { + /** + * The name of the application + */ appName?: string + /** + * An array of fee objects composing of a recipient address and the fee in BPS + */ appFees?: AppFees + /** + * This key is used to fetch token balances, to improve the general UX and suggest relevant tokens + * Can be omitted and the ui will continue to function. Refer to the dune docs on how to get an api key + */ duneApiKey?: string + /** + * Disable the powered by reservoir footer + */ disablePoweredByReservoir?: boolean + /** + * An objecting mapping either a VM type (evm, svm, bvm) or a chain id to a connector key (metamask, backpacksol, etc). + * Connector keys are used for differentiating which wallet maps to which vm/chain. + * Only relevant for eclipse/solana at the moment. + */ + vmConnectorKeyOverrides?: { + [key in number | 'evm' | 'svm' | 'bvm']?: string[] + } } + export interface RelayKitProviderProps { children: ReactNode options: RelayClientOptions & RelayKitProviderOptions @@ -137,7 +150,8 @@ export const RelayKitProvider: FC = function ({ appName: options.appName, appFees: options.appFees, duneApiKey: options.duneApiKey, - disablePoweredByReservoir: options.disablePoweredByReservoir + disablePoweredByReservoir: options.disablePoweredByReservoir, + vmConnectorKeyOverrides: options.vmConnectorKeyOverrides }), [options] ) diff --git a/packages/ui/src/utils/address.ts b/packages/ui/src/utils/address.ts index 46d5236fe..313616b0e 100644 --- a/packages/ui/src/utils/address.ts +++ b/packages/ui/src/utils/address.ts @@ -12,13 +12,22 @@ import { solana } from '../utils/solana.js' import type { LinkedWallet } from '../types/index.js' +import type { RelayKitProviderProps } from '../providers/RelayKitProvider.js' export const isValidAddress = ( vmType?: ChainVM, address?: string, chainId?: number, - connector?: string + connector?: string, + connectorKeyOverrides?: RelayKitProviderProps['options']['vmConnectorKeyOverrides'] ) => { + let eclipseConnectorKeys: string[] | undefined = undefined + if (connectorKeyOverrides && connectorKeyOverrides[eclipse.id]) { + eclipseConnectorKeys = connectorKeyOverrides[eclipse.id] + } else if (vmType === 'svm') { + eclipseConnectorKeys = eclipseWallets + } + if (address) { if (vmType === 'evm' || !vmType) { return isAddress(address) @@ -26,18 +35,18 @@ export const isValidAddress = ( if (chainId && connector) { if ( chainId === eclipse.id && - !eclipseWallets.includes(connector.toLowerCase()) + !eclipseConnectorKeys!.includes(connector.toLowerCase()) ) { return false } if ( chainId === solana.id && - eclipseWallets.includes(connector.toLowerCase()) + eclipseConnectorKeys!.includes(connector.toLowerCase()) ) { return false } } - //tood solana + return isSolanaAddress(address) } else if (vmType === 'bvm') { return isBitcoinAddress(address) @@ -50,9 +59,17 @@ export const addressWithFallback = ( vmType?: ChainVM, address?: string, chainId?: number, - connector?: string + connector?: string, + connectorKeyOverrides?: Parameters['4'] ) => { - return address && isValidAddress(vmType ?? 'evm', address, chainId, connector) + return address && + isValidAddress( + vmType ?? 'evm', + address, + chainId, + connector, + connectorKeyOverrides + ) ? address : getDeadAddress(vmType, chainId) } @@ -60,7 +77,8 @@ export const addressWithFallback = ( export function findSupportedWallet( chain: RelayChain, currentAddress: string | undefined, - linkedWallets: LinkedWallet[] + linkedWallets: LinkedWallet[], + connectorKeyOverrides?: Parameters['4'] ): string | undefined { const currentWallet = linkedWallets.find( (wallet) => wallet.address === currentAddress @@ -72,11 +90,18 @@ export function findSupportedWallet( chain.vmType, currentWallet.address, chain.id, - currentWallet.connector + currentWallet.connector, + connectorKeyOverrides )) ) { const supportedWallet = linkedWallets.find((wallet) => - isValidAddress(chain.vmType, wallet.address, chain.id, wallet.connector) + isValidAddress( + chain.vmType, + wallet.address, + chain.id, + wallet.connector, + connectorKeyOverrides + ) ) return supportedWallet?.address }