diff --git a/src/internal/accountDelegation.ts b/src/internal/accountDelegation.ts index da4d1f7..fa4eb30 100644 --- a/src/internal/accountDelegation.ts +++ b/src/internal/accountDelegation.ts @@ -407,6 +407,7 @@ export async function load( const response = raw.response as AuthenticatorAssertionResponse const address = Bytes.toHex(new Uint8Array(response.userHandle!)) + const credentialId = raw.id // If there are extra keys to authorize (ie. session keys), sign over them. const authorizeKeysResult = await (async () => { @@ -419,7 +420,7 @@ export async function load( const { signature, metadata } = await WebAuthnP256.sign({ challenge: payload, - credentialId: raw.id, + credentialId, rpId, }) @@ -465,7 +466,7 @@ export async function load( if (index === 0) return { expiry: 0n, - id: raw.id, + id: credentialId, publicKey: PublicKey.from(key.publicKey), raw, status: 'unlocked', diff --git a/src/internal/provider.ts b/src/internal/provider.ts index 0e4a17d..ab28b21 100644 --- a/src/internal/provider.ts +++ b/src/internal/provider.ts @@ -3,20 +3,20 @@ import * as Address from 'ox/Address' import * as Hex from 'ox/Hex' import * as Json from 'ox/Json' import * as PersonalMessage from 'ox/PersonalMessage' -import * as Provider_ox from 'ox/Provider' +import * as ox_Provider from 'ox/Provider' import * as PublicKey from 'ox/PublicKey' import * as RpcResponse from 'ox/RpcResponse' -import type * as RpcSchema from 'ox/RpcSchema' import * as TypedData from 'ox/TypedData' +import type { RpcSchema } from 'ox' import type * as Chains from '../Chains.js' import type { Config, Store } from '../Porto.js' import * as AccountDelegation from './accountDelegation.js' -import type * as RpcSchema_internal from './rpcSchema.js' +import type * as Schema from './rpcSchema.js' -export type Provider = Provider_ox.Provider<{ +export type Provider = ox_Provider.Provider<{ includeEvents: true - schema: RpcSchema_internal.Schema + schema: Schema.Schema }> & { /** * Not part of versioned API, proceed with caution. @@ -27,19 +27,6 @@ export type Provider = Provider_ox.Provider<{ } } -export function announce(provider: Provider) { - if (typeof window === 'undefined') return () => {} - return Mipd.announceProvider({ - info: { - icon: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTk1IiBoZWlnaHQ9IjU5NSIgdmlld0JveD0iMCAwIDU5NSA1OTUiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSI1OTUiIGhlaWdodD0iNTk1IiBmaWxsPSIjMTQ1QUM2Ii8+CjxwYXRoIGQ9Ik0zNzMuMzI1IDMwNS44NTJDMzgyLjQ4NyAzMDMuMTA5IDM5Mi4zMyAzMDcuMDA1IDM5Ny4xNjMgMzE1LjI4N0w0NTAuNjAxIDQwNi44NTVDNDU3LjM1NyA0MTguNDMyIDQ0OS4wNDIgNDMzIDQzNS42NzggNDMzSDE2MC4zMjdDMTQ2LjI4NCA0MzMgMTM4LjA5NSA0MTcuMDg3IDE0Ni4yMTkgNDA1LjU4N0wxNzAuNTIxIDM3MS4xODhDMTczLjIwNCAzNjcuMzkxIDE3Ny4wNzYgMzY0LjYwNCAxODEuNTE5IDM2My4yNzRMMzczLjMyNSAzMDUuODUyWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggb3BhY2l0eT0iMC43NSIgZD0iTTI3NC4zOTggMTc2LjcxOUMyNzguMzQzIDE2OS42NiAyODguOTE0IDE3MS4zODMgMjkwLjQzMyAxNzkuMzMzTDMxMi45OTYgMjk3LjQ0MUMzMTQuMTYxIDMwMy41MzkgMzEwLjU2MiAzMDkuNTM5IDMwNC42NDggMzExLjM1NUwxOTcuOSAzNDQuMTUyQzE5MC40NCAzNDYuNDQzIDE4NC4wMSAzMzguNDI5IDE4Ny44MjggMzMxLjU5OUwyNzQuMzk4IDE3Ni43MTlaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBvcGFjaXR5PSIwLjUiIGQ9Ik0zMDEuNjc1IDE2OS4yMTlDMzAwLjU2NiAxNjMuNDUyIDMwOC4zMjggMTYwLjUzNyAzMTEuMjYgMTY1LjYyTDM3OS4wNDggMjgzLjEzM0MzODAuNzUgMjg2LjA4MyAzNzkuMjE4IDI4OS44NTEgMzc1Ljk0NyAyOTAuNzY0TDMzNi42NzcgMzAxLjcxNkMzMzEuODEyIDMwMy4wNzMgMzI2LjgyOSAyOTkuOTc0IDMyNS44NzEgMjk0Ljk5N0wzMDEuNjc1IDE2OS4yMTlaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K', - name: 'Porto', - rdns: 'xyz.ithaca.porto', - uuid: crypto.randomUUID(), - }, - provider: provider as any, - }) -} - export function from< chains extends readonly [Chains.Chain, ...Chains.Chain[]] = readonly [ Chains.Chain, @@ -49,8 +36,8 @@ export function from< const { config, store } = parameters const { announceProvider, headless, keystoreHost } = config - const emitter = Provider_ox.createEmitter() - const provider = Provider_ox.from({ + const emitter = ox_Provider.createEmitter() + const provider = ox_Provider.from({ ...emitter, async request({ method, params }) { const state = store.getState() @@ -58,16 +45,20 @@ export function from< switch (method) { case 'eth_accounts': { if (state.accounts.length === 0) - throw new Provider_ox.DisconnectedError() - return state.accounts.map((account) => account.address) + throw new ox_Provider.DisconnectedError() + return state.accounts.map( + (account) => account.address, + ) satisfies RpcSchema.ExtractReturnType } case 'eth_chainId': { - return Hex.fromNumber(state.chainId) + return Hex.fromNumber( + state.chainId, + ) satisfies RpcSchema.ExtractReturnType } case 'eth_requestAccounts': { - if (!headless) throw new Provider_ox.UnsupportedMethodError() + if (!headless) throw new ox_Provider.UnsupportedMethodError() const { account } = await AccountDelegation.load(state.client, { rpId: keystoreHost, @@ -76,22 +67,25 @@ export function from< store.setState((x) => ({ ...x, accounts: [account] })) emitter.emit('connect', { chainId: Hex.fromNumber(state.chainId) }) - return [account.address] + return [account.address] satisfies RpcSchema.ExtractReturnType< + Schema.Schema, + 'eth_requestAccounts' + > } case 'eth_sendTransaction': { - if (!headless) throw new Provider_ox.UnsupportedMethodError() + if (!headless) throw new ox_Provider.UnsupportedMethodError() if (state.accounts.length === 0) - throw new Provider_ox.DisconnectedError() + throw new ox_Provider.DisconnectedError() const [{ chainId, data = '0x', from, to, value = '0x0' }] = params as RpcSchema.ExtractParams< - RpcSchema_internal.Schema, + Schema.Schema, 'eth_sendTransaction' > if (chainId && Hex.toNumber(chainId) !== state.chainId) - throw new Provider_ox.ChainDisconnectedError() + throw new ox_Provider.ChainDisconnectedError() requireParameter(to, 'to') requireParameter(from, 'from') @@ -99,11 +93,11 @@ export function from< const account = state.accounts.find((account) => Address.isEqual(account.address, from), ) - if (!account) throw new Provider_ox.UnauthorizedError() + if (!account) throw new ox_Provider.UnauthorizedError() const keyIndex = getActiveSessionKeyIndex({ account }) - return await AccountDelegation.execute(state.client, { + return (await AccountDelegation.execute(state.client, { account, calls: [ { @@ -114,23 +108,26 @@ export function from< ], keyIndex, rpId: keystoreHost, - }) + })) satisfies RpcSchema.ExtractReturnType< + Schema.Schema, + 'eth_sendTransaction' + > } case 'eth_signTypedData_v4': { - if (!headless) throw new Provider_ox.UnsupportedMethodError() + if (!headless) throw new ox_Provider.UnsupportedMethodError() if (state.accounts.length === 0) - throw new Provider_ox.DisconnectedError() + throw new ox_Provider.DisconnectedError() const [address, data] = params as RpcSchema.ExtractParams< - RpcSchema_internal.Schema, + Schema.Schema, 'eth_signTypedData_v4' > const account = state.accounts.find((account) => Address.isEqual(account.address, address), ) - if (!account) throw new Provider_ox.UnauthorizedError() + if (!account) throw new ox_Provider.UnauthorizedError() const keyIndex = getActiveSessionKeyIndex({ account }) @@ -141,14 +138,17 @@ export function from< rpId: keystoreHost, }) - return signature + return signature satisfies RpcSchema.ExtractReturnType< + Schema.Schema, + 'eth_signTypedData_v4' + > } case 'experimental_connect': { - if (!headless) throw new Provider_ox.UnsupportedMethodError() + if (!headless) throw new ox_Provider.UnsupportedMethodError() const [{ capabilities }] = (params as RpcSchema.ExtractParams< - RpcSchema_internal.Schema, + Schema.Schema, 'experimental_connect' >) ?? [{}] const { createAccount, grantSession } = capabilities ?? {} @@ -181,14 +181,27 @@ export function from< store.setState((x) => ({ ...x, accounts: [account] })) emitter.emit('connect', { chainId: Hex.fromNumber(state.chainId) }) - return [account.address] + return [ + { + address: account.address, + capabilities: { + sessions: account.keys.map((key) => ({ + expiry: Number(key.expiry), + id: PublicKey.toHex(key.publicKey), + })), + }, + }, + ] satisfies RpcSchema.ExtractReturnType< + Schema.Schema, + 'experimental_connect' + > } case 'experimental_createAccount': { - if (!headless) throw new Provider_ox.UnsupportedMethodError() + if (!headless) throw new ox_Provider.UnsupportedMethodError() const [{ label }] = (params as RpcSchema.ExtractParams< - RpcSchema_internal.Schema, + Schema.Schema, 'experimental_createAccount' >) ?? [{}] @@ -202,7 +215,10 @@ export function from< store.setState((x) => ({ ...x, accounts: [account] })) emitter.emit('connect', { chainId: Hex.fromNumber(state.chainId) }) - return [account.address] + return account.address satisfies RpcSchema.ExtractReturnType< + Schema.Schema, + 'experimental_createAccount' + > } case 'experimental_disconnect': { @@ -211,27 +227,26 @@ export function from< } case 'experimental_grantSession': { - if (!headless) throw new Provider_ox.UnsupportedMethodError() + if (!headless) throw new ox_Provider.UnsupportedMethodError() if (state.accounts.length === 0) - throw new Provider_ox.DisconnectedError() + throw new ox_Provider.DisconnectedError() const [ { address, - expiry = Math.floor(Date.now() / 1000) + 60 * 60, // 1 hour - } = {}, - ] = - (params as RpcSchema.ExtractParams< - RpcSchema_internal.Schema, - 'experimental_grantSession' - >) ?? [] + expiry = Math.floor(Date.now() / 1_000) + 60 * 60, // 1 hour + }, + ] = (params as RpcSchema.ExtractParams< + Schema.Schema, + 'experimental_grantSession' + >) ?? [{}] const account = address ? state.accounts.find((account) => Address.isEqual(account.address, address), ) : state.accounts[0] - if (!account) throw new Provider_ox.UnauthorizedError() + if (!account) throw new ox_Provider.UnauthorizedError() const key = await AccountDelegation.createWebCryptoKey({ expiry: BigInt(expiry), @@ -276,15 +291,18 @@ export function from< return { expiry, id: PublicKey.toHex(key.publicKey), - } + } satisfies RpcSchema.ExtractReturnType< + Schema.Schema, + 'experimental_grantSession' + > } case 'experimental_sessions': { if (state.accounts.length === 0) - throw new Provider_ox.DisconnectedError() + throw new ox_Provider.DisconnectedError() const [{ address }] = (params as RpcSchema.ExtractParams< - RpcSchema_internal.Schema, + Schema.Schema, 'experimental_sessions' >) ?? [{}] @@ -306,23 +324,26 @@ export function from< } case 'porto_ping': { - return 'pong' + return 'pong' satisfies RpcSchema.ExtractReturnType< + Schema.Schema, + 'porto_ping' + > } case 'personal_sign': { - if (!headless) throw new Provider_ox.UnsupportedMethodError() + if (!headless) throw new ox_Provider.UnsupportedMethodError() if (state.accounts.length === 0) - throw new Provider_ox.DisconnectedError() + throw new ox_Provider.DisconnectedError() const [data, address] = params as RpcSchema.ExtractParams< - RpcSchema_internal.Schema, + Schema.Schema, 'personal_sign' > const account = state.accounts.find((account) => Address.isEqual(account.address, address), ) - if (!account) throw new Provider_ox.UnauthorizedError() + if (!account) throw new ox_Provider.UnauthorizedError() const keyIndex = getActiveSessionKeyIndex({ account }) @@ -333,13 +354,16 @@ export function from< rpId: keystoreHost, }) - return signature + return signature satisfies RpcSchema.ExtractReturnType< + Schema.Schema, + 'personal_sign' + > } case 'wallet_getCallsStatus': { const [id] = (params as RpcSchema.ExtractParams< - RpcSchema_internal.Schema, + Schema.Schema, 'wallet_getCallsStatus' >) ?? [] @@ -352,7 +376,10 @@ export function from< return { receipts: [receipt], status: 'CONFIRMED', - } + } satisfies RpcSchema.ExtractReturnType< + Schema.Schema, + 'wallet_getCallsStatus' + > } case 'wallet_getCapabilities': { @@ -368,29 +395,29 @@ export function from< supported: true, }, }, - } + } satisfies RpcSchema.ExtractReturnType< + Schema.Schema, + 'wallet_getCapabilities' + > } case 'wallet_sendCalls': { - if (!headless) throw new Provider_ox.UnsupportedMethodError() + if (!headless) throw new ox_Provider.UnsupportedMethodError() if (state.accounts.length === 0) - throw new Provider_ox.DisconnectedError() + throw new ox_Provider.DisconnectedError() const [{ chainId, calls, from, capabilities }] = - params as RpcSchema.ExtractParams< - RpcSchema_internal.Schema, - 'wallet_sendCalls' - > + params as RpcSchema.ExtractParams if (chainId && Hex.toNumber(chainId) !== state.chainId) - throw new Provider_ox.ChainDisconnectedError() + throw new ox_Provider.ChainDisconnectedError() requireParameter(from, 'from') const account = state.accounts.find((account) => Address.isEqual(account.address, from), ) - if (!account) throw new Provider_ox.UnauthorizedError() + if (!account) throw new ox_Provider.UnauthorizedError() const { enabled = true, id } = capabilities?.session ?? {} @@ -398,19 +425,22 @@ export function from< ? getActiveSessionKeyIndex({ account, id }) : undefined if (typeof keyIndex !== 'number') - throw new Provider_ox.UnauthorizedError() + throw new ox_Provider.UnauthorizedError() - return await AccountDelegation.execute(state.client, { + return (await AccountDelegation.execute(state.client, { account, calls: calls as AccountDelegation.Calls, keyIndex, rpId: keystoreHost, - }) + })) satisfies RpcSchema.ExtractReturnType< + Schema.Schema, + 'wallet_sendCalls' + > } default: { if (method.startsWith('wallet_')) - throw new Provider_ox.UnsupportedMethodError() + throw new ox_Provider.UnsupportedMethodError() return state.client.request({ method, params } as any) } } @@ -464,6 +494,19 @@ export declare namespace from { } } +export function announce(provider: Provider) { + if (typeof window === 'undefined') return () => {} + return Mipd.announceProvider({ + info: { + icon: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTk1IiBoZWlnaHQ9IjU5NSIgdmlld0JveD0iMCAwIDU5NSA1OTUiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSI1OTUiIGhlaWdodD0iNTk1IiBmaWxsPSIjMTQ1QUM2Ii8+CjxwYXRoIGQ9Ik0zNzMuMzI1IDMwNS44NTJDMzgyLjQ4NyAzMDMuMTA5IDM5Mi4zMyAzMDcuMDA1IDM5Ny4xNjMgMzE1LjI4N0w0NTAuNjAxIDQwNi44NTVDNDU3LjM1NyA0MTguNDMyIDQ0OS4wNDIgNDMzIDQzNS42NzggNDMzSDE2MC4zMjdDMTQ2LjI4NCA0MzMgMTM4LjA5NSA0MTcuMDg3IDE0Ni4yMTkgNDA1LjU4N0wxNzAuNTIxIDM3MS4xODhDMTczLjIwNCAzNjcuMzkxIDE3Ny4wNzYgMzY0LjYwNCAxODEuNTE5IDM2My4yNzRMMzczLjMyNSAzMDUuODUyWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggb3BhY2l0eT0iMC43NSIgZD0iTTI3NC4zOTggMTc2LjcxOUMyNzguMzQzIDE2OS42NiAyODguOTE0IDE3MS4zODMgMjkwLjQzMyAxNzkuMzMzTDMxMi45OTYgMjk3LjQ0MUMzMTQuMTYxIDMwMy41MzkgMzEwLjU2MiAzMDkuNTM5IDMwNC42NDggMzExLjM1NUwxOTcuOSAzNDQuMTUyQzE5MC40NCAzNDYuNDQzIDE4NC4wMSAzMzguNDI5IDE4Ny44MjggMzMxLjU5OUwyNzQuMzk4IDE3Ni43MTlaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBvcGFjaXR5PSIwLjUiIGQ9Ik0zMDEuNjc1IDE2OS4yMTlDMzAwLjU2NiAxNjMuNDUyIDMwOC4zMjggMTYwLjUzNyAzMTEuMjYgMTY1LjYyTDM3OS4wNDggMjgzLjEzM0MzODAuNzUgMjg2LjA4MyAzNzkuMjE4IDI4OS44NTEgMzc1Ljk0NyAyOTAuNzY0TDMzNi42NzcgMzAxLjcxNkMzMzEuODEyIDMwMy4wNzMgMzI2LjgyOSAyOTkuOTc0IDMyNS44NzEgMjk0Ljk5N0wzMDEuNjc1IDE2OS4yMTlaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K', + name: 'Porto', + rdns: 'xyz.ithaca.porto', + uuid: crypto.randomUUID(), + }, + provider: provider as any, + }) +} + function requireParameter( param: unknown, details: string, diff --git a/src/internal/wagmi/core.ts b/src/internal/wagmi/core.ts index 31f9f19..fb96f1a 100644 --- a/src/internal/wagmi/core.ts +++ b/src/internal/wagmi/core.ts @@ -1,4 +1,4 @@ -import type * as RpcSchema from 'ox/RpcSchema' +import type { RpcSchema } from 'ox' import { type Address, type Chain, @@ -6,25 +6,40 @@ import { type EIP1193Provider, type Hex, } from 'viem' -import type { BaseErrorType, Config, Connector } from 'wagmi' +import { + ConnectorAlreadyConnectedError, + ProviderNotFoundError, + type BaseErrorType, + type Config, + type Connector, + type CreateConnectorFn, +} from 'wagmi' import { getConnectorClient, disconnect as wagmi_disconnect, + type ConnectReturnType, } from 'wagmi/actions' + import type { CreateAccountParameters, GrantSessionParameters, Schema, } from '../rpcSchema.js' +import type { ChainIdParameter, ConnectorParameter } from './types.js' export async function connect( config: config, parameters: connect.Parameters, -): Promise { - const { createAccount, grantSession } = parameters +): Promise> { + // "Register" connector if not already created + let connector: Connector + if (typeof parameters.connector === 'function') { + connector = config._internal.connectors.setup(parameters.connector) + } else connector = parameters.connector - const connector = parameters.connector ?? config.connectors[0] - if (!connector) throw new Error('Connector is required') + // Check if connector is already connected + if (connector.uid === config.state.current) + throw new ConnectorAlreadyConnectedError() if (parameters.chainId && parameters.chainId !== config.state.chainId) throw new ChainMismatchError({ @@ -37,26 +52,68 @@ export async function connect( currentChainId: config.state.chainId, }) - const provider = (await connector.getProvider()) as EIP1193Provider - return provider.request<{ - Method: 'experimental_connect' - Parameters?: RpcSchema.ExtractParams - ReturnType: RpcSchema.ExtractReturnType - }>({ - method: 'experimental_connect', - params: [{ capabilities: { createAccount, grantSession } }], - }) as unknown as connect.ReturnType + try { + config.setState((x) => ({ ...x, status: 'connecting' })) + connector.emitter.emit('message', { type: 'connecting' }) + + const provider = (await connector.getProvider()) as + | EIP1193Provider + | undefined + if (!provider) throw new ProviderNotFoundError() + + const { createAccount, grantSession } = parameters + const method = 'experimental_connect' + type method = typeof method + await provider.request<{ + Method: method + Parameters?: RpcSchema.ExtractParams + ReturnType: RpcSchema.ExtractReturnType + }>({ + method, + params: [{ capabilities: { createAccount, grantSession } }], + }) + // we already connected, but call `connector.connect` so connector even listeners are set up + const data = await connector.connect({ + chainId: parameters.chainId, + isReconnecting: true, + }) + const accounts = data.accounts as readonly [Address, ...Address[]] + + connector.emitter.off('connect', config._internal.events.connect) + connector.emitter.on('change', config._internal.events.change) + connector.emitter.on('disconnect', config._internal.events.disconnect) + + await config.storage?.setItem('recentConnectorId', connector.id) + config.setState((x) => ({ + ...x, + connections: new Map(x.connections).set(connector.uid, { + accounts, + chainId: data.chainId, + connector, + }), + current: connector.uid, + status: 'connected', + })) + + return { accounts, chainId: data.chainId } + } catch (error) { + config.setState((x) => ({ + ...x, + // Keep existing connector connected in case of error + status: x.current ? 'connected' : 'disconnected', + })) + throw error + } } export declare namespace connect { - type Parameters = { - chainId?: config['chains'][number]['id'] | undefined - connector?: Connector | undefined + type Parameters = ChainIdParameter & { + connector: Connector | CreateConnectorFn createAccount?: boolean | CreateAccountParameters | undefined grantSession?: boolean | GrantSessionParameters | undefined } - type ReturnType = RpcSchema.ExtractReturnType + type ReturnType = ConnectReturnType // TODO: Exhaustive ErrorType type ErrorType = BaseErrorType @@ -65,11 +122,16 @@ export declare namespace connect { export async function createAccount( config: config, parameters: createAccount.Parameters, -): Promise { - const { label } = parameters +): Promise> { + // "Register" connector if not already created + let connector: Connector + if (typeof parameters.connector === 'function') { + connector = config._internal.connectors.setup(parameters.connector) + } else connector = parameters.connector - const connector = parameters.connector ?? config.connectors[0] - if (!connector) throw new Error('Connector is required') + // Check if connector is already connected + if (connector.uid === config.state.current) + throw new ConnectorAlreadyConnectedError() if (parameters.chainId && parameters.chainId !== config.state.chainId) throw new ChainMismatchError({ @@ -82,33 +144,76 @@ export async function createAccount( currentChainId: config.state.chainId, }) - const provider = (await connector.getProvider()) as EIP1193Provider - return provider.request<{ - Method: 'experimental_createAccount' - Parameters?: [createAccount.Parameters] - ReturnType: createAccount.ReturnType - }>({ - method: 'experimental_createAccount', - params: [{ label }], - }) + try { + config.setState((x) => ({ ...x, status: 'connecting' })) + connector.emitter.emit('message', { type: 'connecting' }) + + const provider = (await connector.getProvider()) as + | EIP1193Provider + | undefined + if (!provider) throw new ProviderNotFoundError() + + const { label } = parameters + const method = 'experimental_createAccount' + type method = typeof method + await provider.request<{ + Method: method + Parameters?: RpcSchema.ExtractParams + ReturnType: RpcSchema.ExtractReturnType + }>({ + method, + params: [{ label }], + }) + + // we already connected, but call `connector.connect` so connector even listeners are set up + const data = await connector.connect({ + chainId: parameters.chainId, + isReconnecting: true, + }) + const accounts = data.accounts as readonly [Address, ...Address[]] + + connector.emitter.off('connect', config._internal.events.connect) + connector.emitter.on('change', config._internal.events.change) + connector.emitter.on('disconnect', config._internal.events.disconnect) + + await config.storage?.setItem('recentConnectorId', connector.id) + config.setState((x) => ({ + ...x, + connections: new Map(x.connections).set(connector.uid, { + accounts, + chainId: data.chainId, + connector, + }), + current: connector.uid, + status: 'connected', + })) + + return { accounts, chainId: data.chainId } + } catch (error) { + config.setState((x) => ({ + ...x, + // Keep existing connector connected in case of error + status: x.current ? 'connected' : 'disconnected', + })) + throw error + } } export declare namespace createAccount { - type Parameters = { - chainId?: config['chains'][number]['id'] | undefined - connector?: Connector | undefined + type Parameters = ChainIdParameter & { + connector: Connector | CreateConnectorFn label?: string | undefined } - type ReturnType = [Address] + type ReturnType = ConnectReturnType // TODO: Exhaustive ErrorType type ErrorType = BaseErrorType } -export async function disconnect( - config: config, - parameters: disconnect.Parameters, +export async function disconnect( + config: Config, + parameters: disconnect.Parameters, ): Promise { const connector = (() => { if (parameters.connector) return parameters.connector @@ -117,24 +222,23 @@ export async function disconnect( return connection?.connector })() - await wagmi_disconnect(config, parameters) - const provider = (await connector?.getProvider()) as | EIP1193Provider | undefined + + await wagmi_disconnect(config, parameters) + + const method = 'experimental_disconnect' + type method = typeof method await provider?.request<{ - Method: 'experimental_disconnect' - Parameters?: undefined - ReturnType: disconnect.ReturnType - }>({ - method: 'experimental_disconnect', - }) + Method: method + Parameters: never + ReturnType: never + }>({ method }) } export declare namespace disconnect { - type Parameters<_config extends Config = Config> = { - connector?: Connector | undefined - } + type Parameters = ConnectorParameter // biome-ignore lint/suspicious/noConfusingVoidType: type ReturnType = void @@ -155,23 +259,24 @@ export async function grantSession( connector, }) + const method = 'experimental_grantSession' + type method = typeof method return client.request<{ - Method: 'experimental_grantSession' - Parameters: [grantSession.Parameters] | [] - ReturnType: grantSession.ReturnType + Method: method + Parameters?: RpcSchema.ExtractParams + ReturnType: RpcSchema.ExtractReturnType }>({ - method: 'experimental_grantSession', + method, params: [{ address, expiry }], }) } export declare namespace grantSession { - type Parameters = { - address?: Address | undefined - chainId?: config['chains'][number]['id'] | undefined - connector?: Connector | undefined - expiry?: number | undefined - } + type Parameters = ChainIdParameter & + ConnectorParameter & { + address?: Address | undefined + expiry?: number | undefined + } type ReturnType = { expiry: number @@ -194,24 +299,25 @@ export async function sessions( connector, }) + const method = 'experimental_sessions' + type method = typeof method return client.request<{ - Method: 'experimental_sessions' - Parameters?: [sessions.Parameters] - ReturnType: sessions.ReturnType + Method: method + Parameters?: RpcSchema.ExtractParams + ReturnType: RpcSchema.ExtractReturnType }>({ - method: 'experimental_sessions', + method, params: [{ address }], }) } export declare namespace sessions { - type Parameters = { - address?: Address | undefined - chainId?: config['chains'][number]['id'] | undefined - connector?: Connector | undefined - } + type Parameters = ChainIdParameter & + ConnectorParameter & { + address?: Address | undefined + } - type ReturnType = { + type ReturnType = readonly { expiry: number id: Hex }[] diff --git a/src/internal/wagmi/react.ts b/src/internal/wagmi/react.ts index e384d24..86be8c6 100644 --- a/src/internal/wagmi/react.ts +++ b/src/internal/wagmi/react.ts @@ -30,6 +30,7 @@ import { sessions, } from './core.js' import { sessionsQueryKey } from './query.js' +import type { ConfigParameter } from './types.js' export function useConnect< config extends Config = ResolvedRegister['config'], @@ -42,15 +43,17 @@ export function useConnect< return useMutation({ ...mutation, async mutationFn(variables) { - return connect(config, variables) + return connect(config as Config, variables) }, mutationKey: ['connect'], }) } export declare namespace useConnect { - type Parameters = { - config?: Config | config | undefined + type Parameters< + config extends Config = Config, + context = unknown, + > = ConfigParameter & { mutation?: | UseMutationParameters< connect.ReturnType, @@ -83,15 +86,17 @@ export function useCreateAccount< return useMutation({ ...mutation, async mutationFn(variables) { - return createAccount(config, variables) + return createAccount(config as Config, variables) }, mutationKey: ['createAccount'], }) } export declare namespace useCreateAccount { - type Parameters = { - config?: Config | config | undefined + type Parameters< + config extends Config = Config, + context = unknown, + > = ConfigParameter & { mutation?: | UseMutationParameters< createAccount.ReturnType, @@ -117,8 +122,8 @@ export function useDisconnect< config extends Config = ResolvedRegister['config'], context = unknown, >( - parameters: useDisconnect.Parameters = {}, -): useDisconnect.ReturnType { + parameters: useDisconnect.Parameters = {}, +): useDisconnect.ReturnType { const { mutation } = parameters const config = useConfig(parameters) return useMutation({ @@ -131,25 +136,21 @@ export function useDisconnect< } export declare namespace useDisconnect { - type Parameters = { - config?: Config | config | undefined + type Parameters = ConfigParameter & { mutation?: | UseMutationParameters< disconnect.ReturnType, disconnect.ErrorType, - disconnect.Parameters, + disconnect.Parameters, context > | undefined } - type ReturnType< - config extends Config = Config, - context = unknown, - > = UseMutationResult< + type ReturnType = UseMutationResult< disconnect.ReturnType, disconnect.ErrorType, - disconnect.Parameters, + disconnect.Parameters, context > } @@ -172,8 +173,10 @@ export function useGrantSession< } export declare namespace useGrantSession { - type Parameters = { - config?: Config | config | undefined + type Parameters< + config extends Config = Config, + context = unknown, + > = ConfigParameter & { mutation?: | UseMutationParameters< grantSession.ReturnType, @@ -200,7 +203,7 @@ export function useSessions< selectData = sessions.ReturnType, >( parameters: useSessions.Parameters = {}, -): useSessions.ReturnType { +): useSessions.ReturnType { const { query = {}, ...rest } = parameters const config = useConfig(rest) @@ -264,23 +267,23 @@ export declare namespace useSessions { type Parameters< config extends Config = Config, selectData = sessions.ReturnType, - > = sessions.Parameters & { - config?: Config | config | undefined - query?: - | Omit< - UseQueryParameters< - sessions.ReturnType, - sessions.ErrorType, - selectData, - sessionsQueryKey.Value - >, - 'gcTime' | 'staleTime' - > - | undefined - } + > = sessions.Parameters & + ConfigParameter & { + query?: + | Omit< + UseQueryParameters< + sessions.ReturnType, + sessions.ErrorType, + selectData, + sessionsQueryKey.Value + >, + 'gcTime' | 'staleTime' + > + | undefined + } - type ReturnType< - _config extends Config = Config, - selectData = sessions.ReturnType, - > = UseQueryReturnType + type ReturnType = UseQueryReturnType< + selectData, + sessions.ErrorType + > } diff --git a/src/internal/wagmi/types.ts b/src/internal/wagmi/types.ts new file mode 100644 index 0000000..5e8c80e --- /dev/null +++ b/src/internal/wagmi/types.ts @@ -0,0 +1,21 @@ +import type { Config, Connector } from 'wagmi' + +export type ChainIdParameter< + config extends Config, + chainId extends + | config['chains'][number]['id'] + | undefined = config['chains'][number]['id'], +> = { + chainId?: + | (chainId extends config['chains'][number]['id'] ? chainId : undefined) + | config['chains'][number]['id'] + | undefined +} + +export type ConfigParameter = { + config?: Config | config | undefined +} + +export type ConnectorParameter = { + connector?: Connector | undefined +}