diff --git a/packages/walletconnect/src/provider.ts b/packages/walletconnect/src/provider.ts index 9c8d045f9..b9913b246 100644 --- a/packages/walletconnect/src/provider.ts +++ b/packages/walletconnect/src/provider.ts @@ -61,7 +61,7 @@ import { ProviderEvent, ProviderEventArgument, RelayMethod, - ProjectMetaData, + SignClientOptions, ChainInfo } from './types' import { isMobile } from './utils' @@ -80,18 +80,14 @@ import { Sema } from 'async-sema' const REQUESTS_PER_SECOND_LIMIT = 5 -export interface ProviderOptions extends EnableOptionsBase { +export interface ProviderOptions extends EnableOptionsBase, SignClientOptions { // Alephium options networkId: NetworkId // the id of the network, e.g. mainnet, testnet or devnet. addressGroup?: number // either a specific group or undefined to support all groups methods?: RelayMethod[] // all of the methods to be used in relay; no need to configure in most cases // WalletConnect options - projectId?: string - metadata?: ProjectMetaData // metadata used to initialize a sign client - logger?: string // default logger level is Error; no need to configure in most cases client?: SignClient // existing sign client; no need to configure in most cases - relayUrl?: string // the url of the relay server; no need to configure in most cases } export class WalletConnectProvider extends SignerProvider { @@ -240,7 +236,8 @@ export class WalletConnectProvider extends SignerProvider { // ---------- Private ----------------------------------------------- // private getWCStorageKey(prefix: string, version: string, name: string): string { - return prefix + version + '//' + name + const customStoragePrefix = this.providerOpts.customStoragePrefix ? `:${this.providerOpts.customStoragePrefix}` : '' + return prefix + version + customStoragePrefix + '//' + name } private async getSessionTopics(storage: KeyValueStorage): Promise { @@ -334,10 +331,9 @@ export class WalletConnectProvider extends SignerProvider { this.client = this.providerOpts.client || (await SignClient.init({ + ...this.providerOpts, logger: this.providerOpts.logger || LOGGER, - relayUrl: this.providerOpts.relayUrl || RELAY_URL, - projectId: this.providerOpts.projectId, - metadata: this.providerOpts.metadata // fetch metadata automatically if not provided? + relayUrl: this.providerOpts.relayUrl || RELAY_URL })) } diff --git a/packages/walletconnect/src/types.ts b/packages/walletconnect/src/types.ts index b6ab19611..a7e5c300b 100644 --- a/packages/walletconnect/src/types.ts +++ b/packages/walletconnect/src/types.ts @@ -108,3 +108,4 @@ export interface ChainInfo { } export type ProjectMetaData = SignClientTypes.Metadata +export type SignClientOptions = SignClientTypes.Options diff --git a/packages/web3-react/src/components/AlephiumConnect.tsx b/packages/web3-react/src/components/AlephiumConnect.tsx index e2e088235..715684eef 100644 --- a/packages/web3-react/src/components/AlephiumConnect.tsx +++ b/packages/web3-react/src/components/AlephiumConnect.tsx @@ -46,7 +46,7 @@ import { useConnectSettingContext } from '../contexts/alephiumConnect' import { getLastConnectedAccount, removeLastConnectedAccount } from '../utils/storage' -import { ConnectResult, getConnectorById } from '../utils/connector' +import { Connectors, ConnectResult, createDefaultConnectors } from '../utils/connector' import { useInjectedProviders } from '../hooks/useInjectedProviders' export const ConnectSettingProvider: React.FC<{ @@ -121,8 +121,9 @@ export const AlephiumConnectProvider: React.FC<{ network: NetworkId addressGroup?: number keyType?: KeyType + connectors?: Partial children?: React.ReactNode -}> = ({ network, addressGroup, keyType, children }) => { +}> = ({ network, addressGroup, keyType, children, connectors }) => { // Only allow for mounting AlephiumConnectProvider once, so we avoid weird global // state collisions. const context = useContext(AlephiumConnectContext) @@ -134,6 +135,14 @@ export const AlephiumConnectProvider: React.FC<{ const [_addressGroup, setAddressGroup] = useState(addressGroup) const [_keyType, setKeyType] = useState(keyType ?? 'default') const allInjectedProviders = useInjectedProviders() + const defaultConnectors = useMemo(() => createDefaultConnectors(allInjectedProviders), [allInjectedProviders]) + const allConnectors: Connectors = useMemo(() => { + if (connectors === undefined || Object.keys(connectors).length === 0) { + return defaultConnectors + } else { + return { ...defaultConnectors, ...connectors } + } + }, [defaultConnectors, connectors]) useEffect(() => setNetwork(network), [network]) useEffect(() => setAddressGroup(addressGroup), [addressGroup]) @@ -196,15 +205,14 @@ export const AlephiumConnectProvider: React.FC<{ : [lastConnectorId].concat(allConnectorIds.filter((c) => c !== lastConnectorId)) for (const connectorId of sortedConnectorIds) { - const connector = getConnectorById(connectorId) + const connector = allConnectors[`${connectorId}`] if (connector.autoConnect !== undefined) { const result = await connector.autoConnect({ network, addressGroup, keyType, onDisconnected, - onConnected, - allInjectedProviders: connectorId === 'injected' ? allInjectedProviders : undefined + onConnected }) if (result !== undefined) { return @@ -234,7 +242,8 @@ export const AlephiumConnectProvider: React.FC<{ setConnectionStatus, setAccount: updateAccount, signerProvider, - setSignerProvider: updateSignerProvider + setSignerProvider: updateSignerProvider, + connectors: allConnectors } return {children} @@ -313,6 +322,7 @@ type AlephiumWalletProviderProps = { network: NetworkId addressGroup?: number keyType?: KeyType + connectors?: Partial csrModeOnly?: boolean // whether to show the connect button only in CSR mode children?: React.ReactNode } @@ -323,11 +333,12 @@ export const AlephiumWalletProvider = ({ network, addressGroup, keyType, + connectors, csrModeOnly, children }: AlephiumWalletProviderProps) => { return ( - + = ({ connectorId, switchConnectMethod, forceState }) => { const { setOpen } = useConnectSettingContext() const providers = useInjectedProviders() - const [injectedProvider, setInjectedProvider] = useState( - providers.length !== 0 ? providers[0] : undefined + const [injectedProviderId, setInjectedProviderId] = useState( + providers.length !== 0 ? getInjectedProviderId(providers[0]) : undefined ) console.log(`providers size: ${providers.length}`) const { connect } = useConnect() @@ -127,23 +129,23 @@ const ConnectWithInjector: React.FC<{ ) const handleConnect = useCallback( - (injectedProvider) => { - setInjectedProvider(injectedProvider) + (injectedProviderId) => { + setInjectedProviderId(injectedProviderId) setStatus(states.CONNECTING) }, - [setStatus, setInjectedProvider] + [setStatus, setInjectedProviderId] ) const runConnect = useCallback(() => { if (!hasExtensionInstalled || status === states.LISTING) return - connect(injectedProvider).then((address) => { + connect(injectedProviderId).then((address) => { if (!!address) { setStatus(states.CONNECTED) } setOpen(false) }) - }, [hasExtensionInstalled, setOpen, connect, status, injectedProvider]) + }, [hasExtensionInstalled, setOpen, connect, status, injectedProviderId]) const connectTimeoutRef = useRef>() useEffect(() => { @@ -210,13 +212,13 @@ const ConnectWithInjector: React.FC<{ <> {providers.map((provider) => { - const name = getProviderName(provider) + const id = getInjectedProviderId(provider) return ( - handleConnect(provider)}> + handleConnect(id)}> Icon - {name} + {id} ) })} @@ -497,10 +499,3 @@ const ConnectWithInjector: React.FC<{ } export default ConnectWithInjector - -function getProviderName(provider: AlephiumWindowObject): string { - if (provider.icon.includes('onekey')) { - return 'OneKey' - } - return 'Alephium' -} diff --git a/packages/web3-react/src/contexts/alephiumConnect.tsx b/packages/web3-react/src/contexts/alephiumConnect.tsx index 2b8fec7dc..2484a1be8 100644 --- a/packages/web3-react/src/contexts/alephiumConnect.tsx +++ b/packages/web3-react/src/contexts/alephiumConnect.tsx @@ -20,6 +20,7 @@ import React, { createContext, useContext } from 'react' import { Account, KeyType, SignerProvider, NetworkId } from '@alephium/web3' import { Theme, Mode, CustomTheme, ConnectorId } from '../types' import { node } from '@alephium/web3' +import { Connectors } from '../utils/connector' type Error = string | React.ReactNode | null @@ -64,6 +65,7 @@ export type AlephiumConnectContextValue = { setConnectionStatus: (status: ConnectionStatus) => void signerProvider?: SignerProvider setSignerProvider: (signerProvider: SignerProvider | undefined) => void + connectors: Connectors } export const AlephiumConnectContext = createContext(null) diff --git a/packages/web3-react/src/hooks/useConnect.tsx b/packages/web3-react/src/hooks/useConnect.tsx index 835d4c66a..057a59f73 100644 --- a/packages/web3-react/src/hooks/useConnect.tsx +++ b/packages/web3-react/src/hooks/useConnect.tsx @@ -18,12 +18,21 @@ along with the library. If not, see . import { useAlephiumConnectContext, useConnectSettingContext } from '../contexts/alephiumConnect' import { useCallback, useMemo } from 'react' import { removeLastConnectedAccount } from '../utils/storage' -import { ConnectResult, getConnectorById } from '../utils/connector' +import { ConnectResult } from '../utils/connector' +import { InjectedProviderId } from '../types' export function useConnect() { const { connectorId } = useConnectSettingContext() - const { signerProvider, setSignerProvider, setConnectionStatus, setAccount, addressGroup, network, keyType } = - useAlephiumConnectContext() + const { + signerProvider, + setSignerProvider, + setConnectionStatus, + setAccount, + addressGroup, + network, + keyType, + connectors + } = useAlephiumConnectContext() const onDisconnected = useCallback(() => { removeLastConnectedAccount() @@ -50,13 +59,13 @@ export function useConnect() { }, [onDisconnected, onConnected, network, addressGroup, keyType]) const connector = useMemo(() => { - return getConnectorById(connectorId) - }, [connectorId]) + return connectors[`${connectorId}`] + }, [connectorId, connectors]) const connect = useMemo(() => { - return async (injectedProvider?) => { + return async (injectedProviderId?: InjectedProviderId) => { setConnectionStatus('connecting') - return await connector.connect({ ...connectOptions, injectedProvider }) + return await connector.connect({ ...connectOptions, injectedProviderId }) } }, [connector, connectOptions, setConnectionStatus]) diff --git a/packages/web3-react/src/hooks/useInjectedProviders.tsx b/packages/web3-react/src/hooks/useInjectedProviders.tsx index 15b4e36c1..1e4af6304 100644 --- a/packages/web3-react/src/hooks/useInjectedProviders.tsx +++ b/packages/web3-react/src/hooks/useInjectedProviders.tsx @@ -17,7 +17,7 @@ along with the library. If not, see . */ import { useSyncExternalStore } from 'use-sync-external-store/shim' -import { injectedProviderStore } from '../utils/providers' +import { injectedProviderStore } from '../utils/injectedProviders' export const useInjectedProviders = () => useSyncExternalStore( diff --git a/packages/web3-react/src/index.ts b/packages/web3-react/src/index.ts index 3fcad2a66..ad1fd2f7d 100644 --- a/packages/web3-react/src/index.ts +++ b/packages/web3-react/src/index.ts @@ -31,3 +31,4 @@ export { useBalance } from './hooks/useBalance' export { useWallet, Wallet, useWalletConfig, WalletConfig } from './hooks/useWallet' export * from './contexts/alephiumConnect' +export * from './utils/connector' diff --git a/packages/web3-react/src/types.ts b/packages/web3-react/src/types.ts index f65aa7bbc..22b0e3516 100644 --- a/packages/web3-react/src/types.ts +++ b/packages/web3-react/src/types.ts @@ -33,6 +33,7 @@ export type Mode = 'light' | 'dark' | 'auto' export type CustomTheme = any // TODO: define type export const connectorIds = ['injected', 'walletConnect', 'desktopWallet'] as const export type ConnectorId = (typeof connectorIds)[number] +export type InjectedProviderId = 'Alephium' | 'OneKey' export type CustomStyle = { theme?: Theme diff --git a/packages/web3-react/src/utils/connector.ts b/packages/web3-react/src/utils/connector.ts index 2d70cfb33..7c80137b6 100644 --- a/packages/web3-react/src/utils/connector.ts +++ b/packages/web3-react/src/utils/connector.ts @@ -17,11 +17,12 @@ along with the library. If not, see . */ import { Account, NetworkId, SignerProvider, KeyType } from '@alephium/web3' -import { WalletConnectProvider } from '@alephium/walletconnect-provider' +import { WalletConnectProvider, SignClientOptions } from '@alephium/walletconnect-provider' import QRCodeModal from '@alephium/walletconnect-qrcode-modal' import { AlephiumWindowObject, getDefaultAlephiumWallet } from '@alephium/get-extension-wallet' import { setLastConnectedAccount } from './storage' -import { ConnectorId } from '../types' +import { ConnectorId, InjectedProviderId } from '../types' +import { getInjectedProvider } from './injectedProviders' const WALLET_CONNECT_PROJECT_ID = '6e2562e43678dd68a9070a62b6d52207' @@ -39,30 +40,139 @@ export type ConnectOptions = { } export type InjectedConnectOptions = ConnectOptions & { - injectedProvider?: AlephiumWindowObject + injectedProviderId?: InjectedProviderId } -export type InjectedAutoConnectOptions = ConnectOptions & { - allInjectedProviders?: AlephiumWindowObject[] +export type Connector = { + connect: (opts: InjectedConnectOptions | ConnectOptions) => Promise + disconnect: (signerProvider: SignerProvider) => Promise + autoConnect?: (opts: ConnectOptions) => Promise } -export type ConnectFunc = ( - options: ConnectOptions | InjectedConnectOptions | InjectedAutoConnectOptions -) => Promise +export type Connectors = { + injected: Connector + walletConnect: Connector + desktopWallet: Connector +} -export type Connector = { - connect: ConnectFunc - disconnect: (signerProvider: SignerProvider) => Promise - autoConnect?: ConnectFunc +export function createWalletConnectConnector(signClientOptions?: SignClientOptions): Connector { + const connectorId: ConnectorId = 'walletConnect' + return { + connect: async (options: ConnectOptions): Promise => { + const result = await _wcConnect( + (uri) => QRCodeModal.open(uri, () => console.log('qr closed')), + { ...options, signClientOptions }, + connectorId + ) + QRCodeModal.close() + return result + }, + disconnect: wcDisconnect, + autoConnect: async (options: ConnectOptions): Promise => { + return await wcAutoConnect({ ...options, signClientOptions }, connectorId) + } + } +} + +export function createDesktopWalletConnector(signClientOptions?: SignClientOptions): Connector { + const connectorId: ConnectorId = 'desktopWallet' + return { + connect: async (options: ConnectOptions): Promise => { + return await _wcConnect( + (uri) => window.open(`alephium://wc?uri=${uri}`), + { ...options, signClientOptions }, + connectorId + ) + }, + disconnect: wcDisconnect, + autoConnect: async (options: ConnectOptions): Promise => { + return await wcAutoConnect({ ...options, signClientOptions }, connectorId) + } + } +} + +export function createInjectedConnector(providers: AlephiumWindowObject[]): Connector { + return { + connect: async (options: InjectedConnectOptions): Promise => { + try { + const windowAlephium = await getInjectedProvider(providers, options.injectedProviderId) + const enableOptions = { + addressGroup: options.addressGroup, + keyType: options.keyType, + networkId: options.network, + onDisconnected: options.onDisconnected + } + const enabledAccount = await windowAlephium?.enable(enableOptions) + + if (windowAlephium && enabledAccount) { + await options.onConnected({ account: enabledAccount, signerProvider: windowAlephium }) + setLastConnectedAccount('injected', enabledAccount, options.network) + return enabledAccount + } + } catch (error) { + console.error(`Wallet connect error:`, error) + options.onDisconnected() + } + return undefined + }, + disconnect: async (signerProvider: SignerProvider): Promise => { + return await (signerProvider as AlephiumWindowObject).disconnect() + }, + autoConnect: async (options: ConnectOptions): Promise => { + try { + const allProviders = [...providers] + if (allProviders.length === 0) { + const windowAlephium = await getDefaultAlephiumWallet() + if (windowAlephium !== undefined) { + allProviders.push(windowAlephium) + } + } + const enableOptions = { + addressGroup: options.addressGroup, + keyType: options.keyType, + networkId: options.network, + onDisconnected: undefined as any + } + for (const provider of allProviders) { + const enabledAccount = await provider.enableIfConnected(enableOptions as any) + if (enabledAccount) { + await options.onConnected({ account: enabledAccount, signerProvider: provider }) + setLastConnectedAccount('injected', enabledAccount, options.network) + // eslint-disable-next-line + ;(provider as any)['onDisconnected'] = options.onDisconnected + return enabledAccount + } + } + return undefined + } catch (error) { + console.error(`Wallet auto-connect error:`, error) + options.onDisconnected() + } + return undefined + } + } +} + +export function createDefaultConnectors(injectedProviders: AlephiumWindowObject[]): Connectors { + return { + injected: createInjectedConnector(injectedProviders), + walletConnect: createWalletConnectConnector(undefined), + desktopWallet: createDesktopWalletConnector(undefined) + } } // TODO: handle error properly -async function _wcConnect(onDisplayUri: (uri: string) => void, options: ConnectOptions, connectorId: ConnectorId) { +async function _wcConnect( + onDisplayUri: (uri: string) => void, + options: ConnectOptions & { signClientOptions?: SignClientOptions }, + connectorId: 'walletConnect' | 'desktopWallet' +) { const wcProvider = await WalletConnectProvider.init({ projectId: WALLET_CONNECT_PROJECT_ID, networkId: options.network, addressGroup: options.addressGroup, - onDisconnected: options.onDisconnected + onDisconnected: options.onDisconnected, + ...options.signClientOptions }) wcProvider.on('displayUri', onDisplayUri) @@ -83,13 +193,17 @@ async function _wcConnect(onDisplayUri: (uri: string) => void, options: ConnectO return undefined } -const wcAutoConnect = async (options: ConnectOptions, connectorId: ConnectorId): Promise => { +const wcAutoConnect = async ( + options: ConnectOptions & { signClientOptions?: SignClientOptions }, + connectorId: 'walletConnect' | 'desktopWallet' +): Promise => { try { const wcProvider = await WalletConnectProvider.init({ projectId: WALLET_CONNECT_PROJECT_ID, networkId: options.network, addressGroup: options.addressGroup, - onDisconnected: options.onDisconnected + onDisconnected: options.onDisconnected, + ...options.signClientOptions }) wcProvider.on('session_delete', options.onDisconnected) @@ -110,102 +224,6 @@ const wcAutoConnect = async (options: ConnectOptions, connectorId: ConnectorId): return undefined } -const wcConnect = async (options: ConnectOptions): Promise => { - const result = await _wcConnect( - (uri) => QRCodeModal.open(uri, () => console.log('qr closed')), - options, - 'walletConnect' - ) - QRCodeModal.close() - return result -} - -const desktopWalletConnect = async (options: ConnectOptions): Promise => { - return await _wcConnect((uri) => window.open(`alephium://wc?uri=${uri}`), options, 'desktopWallet') -} - const wcDisconnect = async (signerProvider: SignerProvider): Promise => { await (signerProvider as WalletConnectProvider).disconnect() } - -const injectedConnect = async (options: InjectedConnectOptions): Promise => { - try { - const windowAlephium = options.injectedProvider ?? (await getDefaultAlephiumWallet()) - const enableOptions = { - addressGroup: options.addressGroup, - keyType: options.keyType, - networkId: options.network, - onDisconnected: options.onDisconnected - } - const enabledAccount = await windowAlephium?.enable(enableOptions) - - if (windowAlephium && enabledAccount) { - await options.onConnected({ account: enabledAccount, signerProvider: windowAlephium }) - setLastConnectedAccount('injected', enabledAccount, options.network) - return enabledAccount - } - } catch (error) { - console.error(`Wallet connect error:`, error) - options.onDisconnected() - } - return undefined -} - -const injectedDisconnect = async (signerProvider: SignerProvider): Promise => { - return await (signerProvider as AlephiumWindowObject).disconnect() -} - -const injectedAutoConnect = async (options: InjectedAutoConnectOptions): Promise => { - try { - const allProviders = options.allInjectedProviders ?? [] - if (allProviders.length === 0) { - const windowAlephium = await getDefaultAlephiumWallet() - if (windowAlephium !== undefined) { - allProviders.push(windowAlephium) - } - } - const enableOptions = { - addressGroup: options.addressGroup, - keyType: options.keyType, - networkId: options.network, - onDisconnected: undefined as any - } - for (const provider of allProviders) { - const enabledAccount = await provider.enableIfConnected(enableOptions as any) - if (enabledAccount) { - await options.onConnected({ account: enabledAccount, signerProvider: provider }) - setLastConnectedAccount('injected', enabledAccount, options.network) - // eslint-disable-next-line - ;(provider as any)['onDisconnected'] = options.onDisconnected - return enabledAccount - } - } - return undefined - } catch (error) { - console.error(`Wallet auto-connect error:`, error) - options.onDisconnected() - } - return undefined -} - -const connectors: Record = { - injected: { - connect: injectedConnect, - disconnect: injectedDisconnect, - autoConnect: injectedAutoConnect - }, - walletConnect: { - connect: wcConnect, - disconnect: wcDisconnect, - autoConnect: (options) => wcAutoConnect(options, 'walletConnect') - }, - desktopWallet: { - connect: desktopWalletConnect, - disconnect: wcDisconnect, - autoConnect: (options) => wcAutoConnect(options, 'desktopWallet') - } -} - -export function getConnectorById(connectorId: ConnectorId) { - return connectors[`${connectorId}`] -} diff --git a/packages/web3-react/src/utils/providers.ts b/packages/web3-react/src/utils/injectedProviders.ts similarity index 84% rename from packages/web3-react/src/utils/providers.ts rename to packages/web3-react/src/utils/injectedProviders.ts index f447e1d2e..7f8ae76c8 100644 --- a/packages/web3-react/src/utils/providers.ts +++ b/packages/web3-react/src/utils/injectedProviders.ts @@ -19,10 +19,12 @@ along with the library. If not, see . import { alephiumProvider, AlephiumWindowObject, + getDefaultAlephiumWallet, getWalletObject, isWalletObj, providerInitializedEvent } from '@alephium/get-extension-wallet' +import { InjectedProviderId } from '../types' export type InjectedProviderListener = (providers: AlephiumWindowObject[]) => void @@ -99,3 +101,18 @@ function createProviderStore() { } export const injectedProviderStore = createProviderStore() + +export function getInjectedProviderId(provider: AlephiumWindowObject): InjectedProviderId { + if (provider.icon.includes('onekey')) { + return 'OneKey' + } + return 'Alephium' +} + +export async function getInjectedProvider( + providers: AlephiumWindowObject[], + id?: InjectedProviderId +): Promise { + if (id === undefined) return getDefaultAlephiumWallet() + return providers.find((p) => getInjectedProviderId(p) === id) +}