UNPKG

accounts

Version:

Tempo Accounts SDK

2,392 lines 101 kB
import { announceProvider } from 'mipd'
import { Mppx, tempo as mppx_tempo, type ResolveAccountInfo } from 'mppx/client'
import { Address, Hash, Hex, Json, Provider as ox_Provider, RpcResponse } from 'ox'
import { KeyAuthorization } from 'ox/tempo'
import {
  createWalletClient,
  custom,
  hashMessage,
  hashTypedData,
  http,
  parseUnits,
  type Chain,
  type Client as ViemClient,
  type Transport,
} from 'viem'
import type { JsonRpcAccount } from 'viem/accounts'
import {
  prepareTransactionRequest,
  sendTransaction,
  sendTransactionSync as viem_sendTransactionSync,
  signTransaction as viem_signTransaction,
} from 'viem/actions'
import { parseSiweMessage } from 'viem/siwe'
import { Account as TempoAccount, Actions } from 'viem/tempo'
import { tempo, tempoDevnet, tempoModerato } from 'viem/tempo/chains'
import * as z from 'zod/mini'

import * as AccessKey from './AccessKey.js'
import * as Account from './Account.js'
import type * as Adapter from './Adapter.js'
import { dialog } from './adapters/dialog.js'
import * as Client from './Client.js'
import * as AccessKeyTransaction from './internal/AccessKeyTransaction.js'
import * as AddressUtil from './internal/address.js'
import { withDedupe } from './internal/withDedupe.js'
import * as Keystore from './Keystore.js'
import * as Schema from './Schema.js'
import * as Storage from './Storage.js'
import * as Store from './Store.js'
import * as Tokenlist from './Tokenlist.js'
import * as Request from './zod/request.js'
import * as Rpc from './zod/rpc.js'

export type Provider = ox_Provider.Provider<{ schema: Schema.Ox }> &
  ox_Provider.Emitter & {
    /** Configured chains. */
    chains: readonly [Chain, ...Chain[]]
    /** Returns the active root account as a viem account. */
    getAccount(
      options: Omit<Account.find.Options, 'store'> & { signable: true },
    ): TempoAccount.Account
    getAccount(options?: Omit<Account.find.Options, 'store'>): JsonRpcAccount
    /** Returns local or on-chain publication status for an access key. */
    getAccessKeyStatus(
      options?: getAccessKeyStatus.Options | undefined,
    ): Promise<getAccessKeyStatus.ReturnType>
    /** Returns a viem Client for the given (or current) chain ID. */
    getClient(options?: {
      chainId?: number | undefined
      feePayer?: string | undefined
    }): ViemClient<Transport, typeof tempo>
    /** Returns mppx Tempo client parameters backed by this provider. */
    getMppxParameters(options?: getMppxParameters.Options | undefined): MppxParameters
    /** Reactive state store. */
    store: Store.Store
  }

const announced = new Set<string>()

type TransactionParameters = Adapter.ActionRequest<typeof Rpc.eth_sendTransaction.schema>

type WalletConnectCapabilities = NonNullable<
  NonNullable<Rpc.wallet_connect.Decoded['params']>[number]['capabilities']
>
type AuthCapabilityResult = NonNullable<
  Rpc.wallet_connect.Encoded['returns']['accounts'][number]['capabilities']['auth']
>

type MppxParameters = {
  getClient: NonNullable<mppx_tempo.Parameters['getClient']>
  resolveAccount: NonNullable<mppx_tempo.Parameters['resolveAccount']>
}

function isBrowserWebCrypto() {
  return typeof document !== 'undefined' && !!globalThis.crypto?.subtle
}

/**
 * Creates an EIP-1193 provider with a pluggable adapter.
 *
 * @example
 * ```ts
 * import { Provider } from 'accounts'
 *
 * const provider = Provider.create()
 * ```
 */
export function create(options: create.Options = {}): create.ReturnType {
  const {
    adapter = dialog(),
    chains = [tempo, tempoModerato, tempoDevnet],
    maxAccounts,
    persistCredentials,
    relay,
    testnet,
    storage = typeof window !== 'undefined' ? Storage.idb() : Storage.memory(),
  } = options
  const authorizeAccessKey_default = options.accessKey?.authorize ?? options.authorizeAccessKey
  // Filled in below once the adapter instance exists (adapters may supply
  // environment defaults). The object identity is shared with the store's
  // access-key manager and serializer; nothing reads it before the first
  // request.
  const keystores: Keystore.Keystores = {}

  // Build per-chain transports from `relay` (if set), then layer caller-provided
  // `transports` on top so explicit per-chain overrides win.
  const transports = (() => {
    if (!relay && !options.transports) return undefined
    const base = relay
      ? Object.fromEntries(
          chains.map((c) => [c.id, http(`${relay.replace(/\/$/, '')}/${c.id}`)] as const),
        )
      : {}
    return { ...base, ...options.transports } as Record<number, Transport>
  })()

  const feePayerConfig = (() => {
    if (!options.feePayer) return undefined
    if (typeof options.feePayer === 'string')
      return { precedence: 'fee-payer-first' as const, url: options.feePayer }
    return {
      precedence: options.feePayer.precedence ?? ('fee-payer-first' as const),
      url: options.feePayer.url,
    }
  })()

  const defaultChain = testnet
    ? (chains.find((c) => c.testnet) ?? chains[chains.length - 1]!)
    : chains[0]!

  const store = Store.create({
    chainId: defaultChain.id,
    keystores,
    maxAccounts,
    persistCredentials,
    schema: adapter.schema,
    storage,
  })

  const getAccount: Account.Find = (options = {}) => Account.find({ ...options, store }) as never
  // Lazy reference — assigned after the provider is created so the client
  // transport can route provider methods (wallet_connect, etc.) through it.
  let providerRef: ox_Provider.Provider | undefined

  function getClient(
    options: { chainId?: number | undefined; feePayer?: string | false | undefined } = {},
  ) {
    const { chainId, feePayer } = options
    return Client.fromChainId(chainId, {
      chains,
      feePayer: (() => {
        if (feePayer === false) return false
        if (feePayer) return { url: feePayer, precedence: feePayerConfig?.precedence }
        return undefined
      })(),
      store,
      transports,
    })
  }

  const instance = adapter({ getAccount, getClient, storage, store })
  const { actions } = instance

  // App-level keystores override adapter-supplied defaults.
  const keystores_configured = options.accessKey?.keystores ?? instance.accessKey?.keystores
  Object.assign(keystores, keystores_configured ?? Keystore.defaults)

  const emitter = ox_Provider.createEmitter()
  const requestCache = new Map<string, Promise<unknown>>()

  // Emit EIP-1193 events on state changes.
  store.subscribe(
    (state) => state.accounts.map((a) => a.address).join(),
    () =>
      emitter.emit(
        'accountsChanged',
        store.getState().accounts.map((a) => a.address),
      ),
  )
  store.subscribe(
    (state) => state.chainId,
    (chainId) => emitter.emit('chainChanged', Hex.fromNumber(chainId)),
  )
  store.subscribe(
    (state) => state.accounts.length > 0,
    (connected) => {
      if (connected) emitter.emit('connect', { chainId: Hex.fromNumber(store.getState().chainId) })
      else emitter.emit('disconnect', new ox_Provider.DisconnectedError())
    },
  )

  /** Throws `DisconnectedError` if no accounts are connected. */
  function assertConnected() {
    if (store.getState().accounts.length === 0)
      throw new ox_Provider.DisconnectedError({ message: 'No accounts connected.' })
  }

  /** Returns connected account addresses with the active account first. */
  function getAccountAddresses() {
    const { accounts, activeAccount } = store.getState()
    if (accounts.length === 0) return []
    const active = accounts[activeAccount]?.address
    const activeIdx = accounts.findIndex((a) => a.address === active)
    const sorted = [...accounts]
    if (activeIdx >= 0) {
      const [account] = sorted.splice(activeIdx, 1)
      return [account!.address, ...sorted.map((a) => a.address)]
    }
    return sorted.map((a) => a.address)
  }

  function unsupported(action: string): never {
    throw new ox_Provider.UnsupportedMethodError({
      message: `\`${action}\` not supported by adapter.`,
    })
  }

  async function getAdapterAccount(
    options: Adapter.getAccount.Options = {},
  ): Promise<Awaited<Adapter.getAccount.ReturnType>> {
    if (!instance.getAccount) unsupported('getAccount')
    return await instance.getAccount(options)
  }

  function getWalletClient(options: {
    account: Awaited<Adapter.getAccount.ReturnType>['account']
    chainId?: number | undefined
    feePayer?: string | false | undefined
    transport?: Transport | undefined
  }) {
    const client = getClient({ chainId: options.chainId, feePayer: options.feePayer })
    return createWalletClient({
      account: options.account,
      chain: client.chain,
      transport: options.transport ?? custom({ request: client.request }),
    })
  }

  function forwardFeePayer(
    feePayer: TransactionParameters['feePayer'],
    account: Awaited<Adapter.getAccount.ReturnType>['account'],
  ) {
    if (account.type === 'json-rpc' && feePayer !== undefined) return { feePayer }
    if (feePayer) return { feePayer: true as const }
    return {}
  }

  async function prepareRootTransaction(parameters: TransactionParameters) {
    const { feePayer, ...rest } = parameters
    const selected = await getAdapterAccount({ address: parameters.from })
    const client = getWalletClient({
      account: selected.account,
      chainId: parameters.chainId,
      feePayer: feePayer === true ? undefined : feePayer,
      transport: selected.transport,
    })
    const request = {
      ...rest,
      ...forwardFeePayer(feePayer, selected.account),
    }
    if (selected.account.type === 'json-rpc') return { client, request, selected }
    const prepared = await prepareTransactionRequest(client, {
      account: selected.account,
      ...request,
      type: 'tempo',
    } as never)
    return { client, request: prepared, selected }
  }

  async function signTransaction_(parameters: TransactionParameters) {
    {
      const state = store.getState()
      const chainId = parameters.chainId ?? state.chainId
      const client = getClient({
        chainId,
        feePayer: (() => {
          if (parameters.feePayer === false) return false
          if (typeof parameters.feePayer === 'string') return parameters.feePayer
          return undefined
        })(),
      })
      const address = parameters.from ?? state.accounts[state.activeAccount]?.address
      const transaction = address
        ? await AccessKeyTransaction.create({
            address,
            calls: parameters.calls,
            chainId,
            client,
            store,
          })
        : undefined
      if (transaction)
        try {
          const { feePayer, ...rest } = parameters
          const prepared = await transaction.prepare({
            ...rest,
            ...(feePayer ? { feePayer: true as never } : {}),
          })
          return await prepared.sign()
        } catch {}
    }

    const { client, request, selected } = await prepareRootTransaction(parameters)
    if (selected.account.type === 'json-rpc')
      return await viem_signTransaction(client, request as never)
    return await selected.account.signTransaction!(request as never)
  }

  async function sendTransaction_(parameters: TransactionParameters) {
    {
      const state = store.getState()
      const chainId = parameters.chainId ?? state.chainId
      const client = getClient({
        chainId,
        feePayer: (() => {
          if (parameters.feePayer === false) return false
          if (typeof parameters.feePayer === 'string') return parameters.feePayer
          return undefined
        })(),
      })
      const address = parameters.from ?? state.accounts[state.activeAccount]?.address
      const transaction = address
        ? await AccessKeyTransaction.create({
            address,
            calls: parameters.calls,
            chainId,
            client,
            store,
          })
        : undefined
      if (transaction)
        try {
          const { feePayer, ...rest } = parameters
          const prepared = await transaction.prepare({
            ...rest,
            ...(feePayer ? { feePayer: true as never } : {}),
          })
          return await prepared.send()
        } catch {}
    }

    const { feePayer, ...rest } = parameters
    const selected = await getAdapterAccount({ address: parameters.from })
    const client = getWalletClient({
      account: selected.account,
      chainId: parameters.chainId,
      feePayer: feePayer === true ? undefined : feePayer,
      transport: selected.transport,
    })
    return await sendTransaction(client, {
      ...rest,
      ...forwardFeePayer(feePayer, selected.account),
    } as never)
  }

  async function sendTransactionSync_(parameters: TransactionParameters) {
    {
      const state = store.getState()
      const chainId = parameters.chainId ?? state.chainId
      const client = getClient({
        chainId,
        feePayer: (() => {
          if (parameters.feePayer === false) return false
          if (typeof parameters.feePayer === 'string') return parameters.feePayer
          return undefined
        })(),
      })
      const address = parameters.from ?? state.accounts[state.activeAccount]?.address
      const transaction = address
        ? await AccessKeyTransaction.create({
            address,
            calls: parameters.calls,
            chainId,
            client,
            store,
          })
        : undefined
      if (transaction)
        try {
          const { feePayer, ...rest } = parameters
          const prepared = await transaction.prepare({
            ...rest,
            ...(feePayer ? { feePayer: true as never } : {}),
          })
          return await prepared.sendSync()
        } catch {}
    }

    const { feePayer, ...rest } = parameters
    const selected = await getAdapterAccount({ address: parameters.from })
    const client = getWalletClient({
      account: selected.account,
      chainId: parameters.chainId,
      feePayer: feePayer === true ? undefined : feePayer,
      transport: selected.transport,
    })
    const request = {
      ...rest,
      ...forwardFeePayer(feePayer, selected.account),
    }
    if (selected.account.type === 'json-rpc')
      return (await client.request({
        method: 'eth_sendTransactionSync' as never,
        params: [z.encode(Rpc.transactionRequest, request)] as never,
      })) as Rpc.eth_sendTransactionSync.Encoded['returns']
    const receipt = await viem_sendTransactionSync(client, {
      account: selected.account,
      ...request,
    } as never)
    return z.encode(Rpc.receipt, receipt as never) as Rpc.eth_sendTransactionSync.Encoded['returns']
  }

  async function signPersonalMessage(parameters: { address: Address.Address; data: Hex.Hex }) {
    const selected = await getAdapterAccount({ address: parameters.address })
    const client = getWalletClient({ account: selected.account, transport: selected.transport })
    if (selected.account.type === 'json-rpc')
      return await client.signMessage({
        account: selected.account,
        message: { raw: parameters.data },
      })
    return await selected.account.sign!({ hash: hashMessage({ raw: parameters.data }) })
  }

  async function signTypedData(parameters: { address: Address.Address; data: string }) {
    const typedData = JSON.parse(parameters.data) as {
      domain: Record<string, unknown>
      message: Record<string, unknown>
      primaryType: string
      types: Record<string, unknown>
    }
    const selected = await getAdapterAccount({ address: parameters.address })
    const client = getWalletClient({ account: selected.account, transport: selected.transport })
    if (selected.account.type === 'json-rpc')
      return await client.signTypedData({
        account: selected.account,
        ...typedData,
      } as never)
    return await selected.account.sign!({ hash: hashTypedData(typedData as never) })
  }

  async function authorizeAccessKey(parameters: Adapter.authorizeAccessKey.Parameters) {
    const selected = await getAdapterAccount()
    const chainId = parameters.chainId ?? getClient().chain.id
    if (selected.account.type !== 'json-rpc') {
      const keyAuthorization = await store.accessKeys.authorize({
        account: selected.account as Pick<TempoAccount.Account, 'address' | 'sign'>,
        chainId,
        parameters,
      })
      return { keyAuthorization, rootAddress: selected.account.address }
    }

    const prepared = await prepareAuthorizeAccessKey(parameters, Number(chainId))
    const client = getWalletClient({
      account: selected.account,
      chainId: Number(chainId),
      transport: selected.transport,
    })
    const result = (await client.request({
      method: 'wallet_authorizeAccessKey' as never,
      params: [z.encode(Rpc.wallet_authorizeAccessKey.parameters, prepared.parameters)] as never,
    })) as Adapter.authorizeAccessKey.ReturnType
    await savePreparedAccessKey({
      account: result.rootAddress,
      accessKey: prepared,
      keyAuthorization: result.keyAuthorization,
    })
    return result
  }

  async function prepareAuthorizeAccessKey(
    parameters: Adapter.authorizeAccessKey.Parameters,
    chainId: number | undefined,
  ): Promise<{
    key?: { handle: Keystore.Handle; publicKey: Hex.Hex } | undefined
    parameters: Adapter.authorizeAccessKey.Parameters
    privateKey?: Hex.Hex | undefined
  }> {
    const chainId_ = parameters.chainId ?? chainId ?? getClient().chain.id
    if (parameters.privateKey || parameters.address || parameters.publicKey) {
      const prepared = await AccessKey.prepareAuthorization({
        ...parameters,
        chainId: chainId_,
      })
      return {
        parameters: toAuthorizeAccessKeyParameters(parameters, prepared.keyAuthorization),
        ...(prepared.privateKey ? { privateKey: prepared.privateKey } : {}),
      }
    }

    // Resolve the key source: configured keystores (app-level, else the
    // adapter's defaults) win; otherwise the built-in keystore — in browsers
    // only, since elsewhere the wallet generates the key.
    if (!keystores_configured && !isBrowserWebCrypto()) return { parameters }

    const { key, keyAuthorization } = await AccessKey.prepareAuthorization({
      ...parameters,
      chainId: chainId_,
      keystores,
    })
    if (!key)
      throw new RpcResponse.InternalError({
        message: 'Keystore did not produce access-key material.',
      })
    return {
      key,
      parameters: {
        ...toAuthorizeAccessKeyParameters(parameters, keyAuthorization),
        publicKey: key.publicKey,
      },
    }
  }

  function toAuthorizeAccessKeyParameters(
    parameters: Adapter.authorizeAccessKey.Parameters,
    keyAuthorization: Awaited<
      ReturnType<typeof AccessKey.prepareAuthorization>
    >['keyAuthorization'],
  ) {
    const parameters_rpc = { ...parameters }
    delete parameters_rpc.privateKey
    return {
      ...parameters_rpc,
      address: keyAuthorization.address,
      chainId: keyAuthorization.chainId,
      keyType: keyAuthorization.type,
    }
  }

  async function savePreparedAccessKey(options: {
    accessKey: Awaited<ReturnType<typeof prepareAuthorizeAccessKey>> | undefined
    account: Address.Address | undefined
    keyAuthorization: KeyAuthorization.Rpc | undefined
  }) {
    const { accessKey, account, keyAuthorization } = options
    if (!account || !keyAuthorization || !accessKey) return
    const { key, privateKey } = accessKey
    const material = key
      ? { handle: key.handle, publicKey: key.publicKey }
      : privateKey
        ? { privateKey }
        : undefined
    if (!material) return
    store.accessKeys.add({
      account,
      authorization: KeyAuthorization.fromRpc(keyAuthorization),
      ...material,
    })
  }

  async function revokeAccessKey(parameters: {
    address: Address.Address
    accessKeyAddress: Address.Address
  }) {
    const selected = await getAdapterAccount({ address: parameters.address })
    const client = getWalletClient({
      account: selected.account,
      transport: selected.transport,
    })
    if (selected.account.type === 'json-rpc') {
      await client.request({
        method: 'wallet_revokeAccessKey' as never,
        params: [parameters] as never,
      })
    } else {
      try {
        await Actions.accessKey.revokeSync(getClient(), {
          account: selected.account as TempoAccount.Account,
          accessKey: parameters.accessKeyAddress,
        })
      } catch (error) {
        if (!AccessKey.isUnavailableError(error)) throw error
      }
    }
    store.accessKeys.remove({
      accessKey: parameters.accessKeyAddress,
      account: parameters.address,
      chainId: store.getState().chainId,
    })
  }

  async function updateAccessKey(parameters: Adapter.updateAccessKey.Parameters) {
    const selected = await getAdapterAccount({ address: parameters.address })
    const chainId = Number(parameters.chainId ?? getClient().chain.id)
    if (selected.account.type === 'json-rpc') {
      const client = getWalletClient({
        account: selected.account,
        chainId,
        transport: selected.transport,
      })
      await client.request({
        method: 'wallet_updateAccessKey' as never,
        params: [z.encode(Rpc.wallet_updateAccessKey.parameters, parameters)] as never,
      })
      return
    }
    // One transaction; `updateSpendingLimit` writes each token's remaining
    // allowance directly, preserving period configuration.
    const calls = parameters.limits.map((limit) =>
      Actions.accessKey.updateLimit.call({
        accessKey: parameters.accessKeyAddress,
        limit: limit.limit,
        token: limit.token,
      }),
    )
    await viem_sendTransactionSync(getClient({ chainId }), {
      account: selected.account,
      calls,
    })
  }

  async function signTransactionAction(
    parameters: TransactionParameters,
    request: Pick<Rpc.eth_signTransaction.Encoded, 'method' | 'params'>,
  ) {
    if (actions.signTransaction) return await actions.signTransaction(parameters, request)
    if (instance.getAccount) return await signTransaction_(parameters)
    unsupported('signTransaction')
  }

  async function sendTransactionAction(
    parameters: TransactionParameters,
    request: Pick<Rpc.eth_sendTransaction.Encoded, 'method' | 'params'>,
  ) {
    if (actions.sendTransaction) return await actions.sendTransaction(parameters, request)
    if (instance.getAccount) return await sendTransaction_(parameters)
    unsupported('sendTransaction')
  }

  async function sendTransactionSyncAction(
    parameters: TransactionParameters,
    request: Pick<Rpc.eth_sendTransactionSync.Encoded, 'method' | 'params'>,
  ) {
    if (actions.sendTransactionSync) return await actions.sendTransactionSync(parameters, request)
    if (instance.getAccount) return await sendTransactionSync_(parameters)
    unsupported('sendTransactionSync')
  }

  async function signPersonalMessageAction(
    parameters: { address: Address.Address; data: Hex.Hex },
    request: Pick<Rpc.personal_sign.Encoded, 'method' | 'params'>,
  ) {
    if (actions.signPersonalMessage) return await actions.signPersonalMessage(parameters, request)
    if (instance.getAccount) return await signPersonalMessage(parameters)
    unsupported('signPersonalMessage')
  }

  async function signTypedDataAction(
    parameters: { address: Address.Address; data: string },
    request: Pick<Rpc.eth_signTypedData_v4.Encoded, 'method' | 'params'>,
  ) {
    if (actions.signTypedData) return await actions.signTypedData(parameters, request)
    if (instance.getAccount) return await signTypedData(parameters)
    unsupported('signTypedData')
  }

  async function authorizeAccessKeyAction(
    parameters: Adapter.authorizeAccessKey.Parameters,
    request: Pick<Rpc.wallet_authorizeAccessKey.Encoded, 'method' | 'params'>,
  ) {
    if (actions.authorizeAccessKey) return await actions.authorizeAccessKey(parameters, request)
    if (instance.getAccount) return await authorizeAccessKey(parameters)
    unsupported('authorizeAccessKey')
  }

  async function revokeAccessKeyAction(
    parameters: Adapter.revokeAccessKey.Parameters,
    request: Pick<Rpc.wallet_revokeAccessKey.Encoded, 'method' | 'params'>,
  ) {
    if (actions.revokeAccessKey) return await actions.revokeAccessKey(parameters, request)
    if (instance.getAccount) return await revokeAccessKey(parameters)
    unsupported('revokeAccessKey')
  }

  async function updateAccessKeyAction(
    parameters: Adapter.updateAccessKey.Parameters,
    request: Pick<Rpc.wallet_updateAccessKey.Encoded, 'method' | 'params'>,
  ) {
    if (actions.updateAccessKey) return await actions.updateAccessKey(parameters, request)
    if (instance.getAccount) return await updateAccessKey(parameters)
    unsupported('updateAccessKey')
  }

  /** Returns accounts to persist. When `persistAccounts` is set, merges new accounts with existing ones. */
  function resolveAccounts(accounts: readonly Account.Store[]) {
    if (!instance.persistAccounts) return accounts
    const merged = [...accounts]
    for (const a of store.getState().accounts)
      if (!merged.some((m) => AddressUtil.isEqual(m.address, a.address))) merged.push(a)
    return merged
  }

  /** Resolves the `feePayer` field from a transaction request into an absolute URL string or `undefined`. */
  function resolveFeePayer(feePayer: string | boolean | undefined): string | false | undefined {
    if (feePayer === false) return false
    const url = (() => {
      if (typeof feePayer === 'string') return feePayer
      return feePayerConfig?.url
    })()
    if (!url) return undefined
    if (url.startsWith('http://') || url.startsWith('https://')) return url
    if (typeof window !== 'undefined') return new URL(url, window.location.origin).href
    return url
  }

  function stripAuthorizeAccessKey(
    parameters: create.AuthorizeAccessKeyParameters,
  ): Adapter.authorizeAccessKey.Parameters {
    const { reuse: _reuse, ...rest } = parameters
    return rest
  }

  function resolveDefaultAuthorizeAccessKey(): create.AuthorizeAccessKeyParameters | undefined {
    if (typeof authorizeAccessKey_default === 'function') return authorizeAccessKey_default()
    return authorizeAccessKey_default
  }

  async function defaultAuthorizeAccessKeyForConnect(options_: {
    capabilities: WalletConnectCapabilities | undefined
    chainId: number
  }): Promise<Adapter.authorizeAccessKey.Parameters | undefined> {
    const parameters = resolveDefaultAuthorizeAccessKey()
    if (!parameters) return undefined
    const address = reusableConnectAccount(options_.capabilities)
    if (
      address &&
      (await AccessKey.hasReusableAuthorization({
        account: address,
        chainId: Number(parameters.chainId ?? options_.chainId),
        parameters,
        store: { keystores, state: store },
      }))
    )
      return undefined
    return stripAuthorizeAccessKey(parameters)
  }

  function reusableConnectAccount(
    capabilities: WalletConnectCapabilities | undefined,
  ): Address.Address | undefined {
    const state = store.getState()
    if (state.accounts.length === 0) return undefined
    if (capabilities && 'selectAccount' in capabilities && capabilities.selectAccount)
      return undefined
    if (capabilities && 'credentialId' in capabilities && capabilities.credentialId) {
      if (Array.isArray(capabilities.credentialId) && capabilities.credentialId.length !== 1)
        return undefined
      const credentialId = Array.isArray(capabilities.credentialId)
        ? capabilities.credentialId[0]
        : capabilities.credentialId
      const account = state.accounts.find(
        (a) => 'credential' in a && a.credential?.id === credentialId,
      )
      return account?.address
    }
    if (capabilities?.method === 'register' && capabilities.name) {
      const account = state.accounts.find(
        (a) => 'credential' in a && a.label?.toLowerCase() === capabilities.name!.toLowerCase(),
      )
      return account?.address
    }
    return state.accounts[state.activeAccount]?.address ?? state.accounts[0]?.address
  }

  async function authorizeDefaultAccessKeyForTransaction(options_: {
    address: Address.Address
    calls?: readonly AccessKey.Call[] | undefined
    chainId: number
  }): Promise<void> {
    const parameters = resolveDefaultAuthorizeAccessKey()
    if (!parameters) return
    const existing = await store.accessKeys.select({
      account: options_.address,
      ...(options_.calls ? { calls: options_.calls } : {}),
      chainId: Number(parameters.chainId ?? options_.chainId),
    })
    if (existing) return
    if (!AccessKey.canAuthorizeCalls({ parameters, calls: options_.calls })) return
    const decoded = stripAuthorizeAccessKey(parameters)
    await authorizeAccessKeyAction(decoded, {
      method: 'wallet_authorizeAccessKey',
      params: [z.encode(Rpc.wallet_authorizeAccessKey.parameters, decoded)!],
    })
  }

  function assertMppAccountConnected(address: Address.Address) {
    const { accounts } = store.getState()
    if (accounts.length === 0)
      throw new ox_Provider.DisconnectedError({ message: 'No accounts connected.' })
    if (!accounts.some((account) => AddressUtil.isEqual(account.address, address)))
      throw new ox_Provider.UnauthorizedError({ message: `Account "${address}" not found.` })
  }

  async function resolveMppAccount(
    info: ResolveAccountInfo,
    options_: { accessKey?: Address.Address | undefined } = {},
  ) {
    const account = AddressUtil.from(info.account.address)
    if (!account) return undefined
    assertMppAccountConnected(account)

    if (options_.accessKey) {
      const authority =
        info.operation.kind === 'authorizePaymentChannel'
          ? AddressUtil.from(info.operation.authority)
          : undefined
      if (authority && !AddressUtil.isEqual(authority, options_.accessKey))
        throw new ox_Provider.UnauthorizedError({
          message: `Access key "${options_.accessKey}" cannot satisfy channel authority "${authority}".`,
        })
      const query = {
        account,
        accessKey: options_.accessKey,
        chainId: info.chainId,
      } as const
      if (info.operation.kind === 'executeCalls' && !info.operation.calls) {
        const accessKey = await store.accessKeys.get(query)
        const record = store.accessKeys.list(query)[0]
        if (accessKey && record?.scopes)
          throw new ox_Provider.UnauthorizedError({
            message: `Access key "${options_.accessKey}" cannot be selected for executeCalls without transaction call details.`,
          })
        if (accessKey) return accessKey
      }
      const accessKey = await store.accessKeys.get({
        ...query,
        ...(info.operation.kind === 'executeCalls' ? { calls: info.operation.calls } : {}),
      })
      if (!accessKey) {
        throw new ox_Provider.UnauthorizedError({
          message: `Access key "${options_.accessKey}" cannot sign for account "${account}".`,
        })
      }
      return accessKey
    }

    if (info.operation.kind === 'executeCalls')
      return await store.accessKeys.select({
        account,
        ...(info.operation.calls ? { calls: info.operation.calls } : {}),
        chainId: info.chainId,
      })

    const accessKey = AddressUtil.from(info.operation.authority)
    if (!accessKey) return await store.accessKeys.select({ account, chainId: info.chainId })
    if (AddressUtil.isZero(accessKey) || AddressUtil.isEqual(accessKey, account)) return undefined

    return await store.accessKeys.get({
      account,
      accessKey,
      chainId: info.chainId,
    })
  }

  function getMppxParameters(options_: getMppxParameters.Options = {}): MppxParameters {
    const accessKey = (() => {
      if (!options_.accessKey) return undefined
      const accessKey = AddressUtil.from(options_.accessKey)
      if (!accessKey)
        throw new RpcResponse.InvalidParamsError({
          message: `Invalid access key address "${options_.accessKey}".`,
        })
      return accessKey
    })()

    return {
      getClient({ chainId }: { chainId?: number | undefined }) {
        if (chainId !== undefined && !chains.some((chain) => chain.id === chainId))
          throw new ox_Provider.UnsupportedChainIdError({
            message: `Chain ${chainId} not configured.`,
          })
        const client = provider.getClient({ chainId })
        const account = store.getState().accounts[store.getState().activeAccount]
        if (!account) throw new ox_Provider.DisconnectedError({ message: 'No active account.' })
        return Object.assign(Object.create(Object.getPrototypeOf(client)), client, {
          account: {
            address: account.address,
            type: 'json-rpc' as const,
          },
        })
      },
      resolveAccount: (info: ResolveAccountInfo) => resolveMppAccount(info, { accessKey }),
    }
  }

  const provider = Object.assign(
    ox_Provider.from(
      {
        ...(emitter as unknown as ox_Provider.Emitter),
        async request({ method, params }: { method: string; params?: any }) {
          await Store.waitForHydration(store)

          const shouldDedupe = [
            'eth_accounts',
            'eth_chainId',
            'eth_requestAccounts',
            'wallet_connect',
            'wallet_getBalances',
            'wallet_getCapabilities',
          ].includes(method)

          return withDedupe(
            async () => {
              // Validate known methods. Unknown methods fall through to the RPC proxy.
              let request: Request.WithDecoded<typeof Schema.Request>
              try {
                request = Request.validate(Schema.Request, { method, params })
              } catch (e) {
                if (!(e instanceof ox_Provider.UnsupportedMethodError)) throw e
                // Proxy unknown methods to the RPC node.
                return await Client.fromChainId(undefined, { chains, store, transports }).request({
                  method: method as any,
                  params: params as any,
                })
              }

              const result = await (async () => {
                switch (request.method) {
                  case 'eth_accounts':
                    return getAccountAddresses() satisfies Rpc.eth_accounts.Encoded['returns']

                  case 'eth_chainId':
                    return Hex.fromNumber(
                      store.getState().chainId,
                    ) satisfies Rpc.eth_chainId.Encoded['returns']

                  case 'eth_requestAccounts': {
                    const existing = getAccountAddresses()
                    if (existing.length > 0)
                      return existing satisfies Rpc.eth_requestAccounts.Encoded['returns']

                    const { accounts } = await actions.loadAccounts(undefined, {
                      method: 'wallet_connect',
                      params: undefined,
                    })

                    store.setState({ accounts: resolveAccounts(accounts), activeAccount: 0 })

                    return accounts.map(
                      (a) => a.address,
                    ) satisfies Rpc.eth_requestAccounts.Encoded['returns']
                  }

                  case 'eth_sendTransaction': {
                    assertConnected()
                    const [decoded] = request._decoded.params
                    const { to, data, ...rest } = decoded
                    const calls =
                      decoded.calls ?? (to ? [{ to, data, value: decoded.value }] : undefined)
                    const state = store.getState()
                    const chainId = decoded.chainId ?? state.chainId
                    const from = decoded.from ?? state.accounts[state.activeAccount]?.address
                    if (from)
                      await authorizeDefaultAccessKeyForTransaction({
                        address: from,
                        ...(calls ? { calls } : {}),
                        chainId,
                      })
                    return (await sendTransactionAction(
                      {
                        ...rest,
                        chainId,
                        from,
                        ...(calls ? { calls } : {}),
                        feePayer: resolveFeePayer(decoded.feePayer),
                      },
                      request,
                    )) satisfies Rpc.eth_sendTransaction.Encoded['returns']
                  }

                  case 'eth_fillTransaction': {
                    const [decoded] = request._decoded.params
                    const parameters = { ...decoded }
                    const feePayer = resolveFeePayer(parameters.feePayer)
                    const client = getClient({ chainId: parameters.chainId, feePayer })
                    const state = store.getState()
                    const chainId = parameters.chainId ?? state.chainId
                    const address = parameters.from ?? state.accounts[state.activeAccount]?.address

                    // The node sizes intrinsic gas from the signing key's signature
                    // type; absent it, it assumes secp256k1 (the smallest signature)
                    // and underestimates gas for p256/webAuthn signers. The key type
                    // is the *signer's*, not the embedded `keyAuthorization`'s (which
                    // is just a registration payload). The `fill` path below always
                    // signs with the root account, so resolve its key type (a caller's
                    // explicit `keyType` wins) and hand viem's tempo formatter an
                    // account carrying it — viem forwards `keyType` and derives the
                    // webAuthn `keyData` hint. secp256k1 (the node default) flows
                    // through the legacy formatter unchanged. The managed access-key
                    // path carries the access key's type via its own viem account.
                    const signer = ((): JsonRpcAccount | undefined => {
                      if (!address) return undefined
                      const keyType =
                        parameters.keyType ??
                        Account.signatureKeyType(
                          state.accounts.find((a) => AddressUtil.isEqual(a.address, address)),
                        )
                      if (!keyType) return undefined
                      return { address, keyType, type: 'json-rpc' } as JsonRpcAccount
                    })()
                    const fill = (account = signer) => {
                      const fillRequest = {
                        ...parameters,
                        ...(account ? { account, from: account.address } : {}),
                        chainId,
                        ...(feePayer ? { feePayer: true } : {}),
                      }
                      const formatter = client.chain?.formatters?.transactionRequest
                      const formatted = formatter
                        ? formatter.format({ ...fillRequest } as never, 'fillTransaction')
                        : fillRequest
                      return client.request({
                        method: 'eth_fillTransaction',
                        params: [formatted as never],
                      })
                    }

                    // Dapp-provided `keyAuthorization`: the root account signs and
                    // the authorization rides along in `parameters` (so the node
                    // prices its on-chain registration). Skip managed access-key
                    // routing, which would otherwise attach a second authorization.
                    if (parameters.keyAuthorization) return await fill()

                    // Locally-managed access key: the access key signs, and viem
                    // attaches a stored `keyAuthorization` if one is still pending
                    // (i.e. not yet authorized on-chain). When it does, the estimate
                    // must include the gas to authorize the key on-chain — so this
                    // path can't be collapsed into the plain root fill. Falls back to
                    // the root account on failure.
                    if (address) {
                      const calls =
                        parameters.calls ??
                        (parameters.to ? [{ data: parameters.data, to: parameters.to }] : undefined)
                      const transaction = await AccessKeyTransaction.create({
                        address,
                        calls,
                        chainId,
                        client,
                        store,
                      })
                      if (transaction)
                        try {
                          return await transaction.fill({
                            ...parameters,
                            chainId,
                            from: address,
                            ...(feePayer ? { feePayer: true } : {}),
                          })
                        } catch {
                          // Fall through to the root account fill.
                        }
                    }

                    return await fill()
                  }

                  case 'eth_signTransaction': {
                    assertConnected()
                    const [decoded] = request._decoded.params
                    const { to, data, ...rest } = decoded
                    const calls =
                      decoded.calls ?? (to ? [{ to, data, value: decoded.value }] : undefined)
                    const state = store.getState()
                    return (await signTransactionAction(
                      {
                        ...rest,
                        chainId: decoded.chainId ?? state.chainId,
                        from: decoded.from ?? state.accounts[state.activeAccount]?.address,
                        ...(calls ? { calls } : {}),
                        feePayer:
                          decoded.feePayer === true && !feePayerConfig
                            ? true
                            : resolveFeePayer(decoded.feePayer),
                      },
                      request,
                    )) satisfies Rpc.eth_signTransaction.Encoded['returns']
                  }

                  case 'eth_sendTransactionSync': {
                    assertConnected()
                    const [decoded] = request._decoded.params
                    const { to, data, ...rest } = decoded
                    const calls =
                      decoded.calls ?? (to ? [{ to, data, value: decoded.value }] : undefined)
                    const state = store.getState()
                    const chainId = decoded.chainId ?? state.chainId
                    const from = decoded.from ?? state.accounts[state.activeAccount]?.address
                    if (from)
                      await authorizeDefaultAccessKeyForTransaction({
                        address: from,
                        ...(calls ? { calls } : {}),
                        chainId,
                      })
                    return (await sendTransactionSyncAction(
                      {
                        ...rest,
                        chainId,
                        from,
                        ...(calls ? { calls } : {}),
                        feePayer: resolveFeePayer(decoded.feePayer),
                      },
                      request,
                    )) satisfies Rpc.eth_sendTransactionSync.Encoded['returns']
                  }

                  case 'eth_signTypedData_v4': {
                    assertConnected()
                    const [address, data] = request._decoded.params
                    return (await signTypedDataAction(
                      {
                        address,
                        data,
                      },
                      request,
                    )) satisfies Rpc.eth_signTypedData_v4.Encoded['returns']
                  }

                  case 'personal_sign': {
                    assertConnected()
                    const [data, address] = request._decoded.params
                    return (await signPersonalMessageAction(
                      {
                        address,
                        data,
                      },
                      request,
                    )) satisfies Rpc.personal_sign.Encoded['returns']
                  }

                  case 'wallet_sendCalls': {
                    try {
                      assertConnected()
                      const decoded = request._decoded.params?.[0]
                      const { calls = [], capabilities, chainId, from } = decoded ?? {}
                      const sync = capabilities?.sync
                      const feePayer = resolveFeePayer(
                        capabilities?.feePayer ?? (feePayerConfig ? true : undefined),
                      )
                      const state = store.getState()
                      const from_ = from ?? state.accounts[state.activeAccount]?.address
                      if (from_)
                        await authorizeDefaultAccessKeyForTransaction({
                          address: from_,
                          calls,
                          chainId: chainId ?? state.chainId,
                        })
                      const txRequest = {
                        calls,
                        chainId,
                        from: from_,
                        ...(feePayer !== undefined ? { feePayer } : {}),
                      }
                      if (!sync) {
                        const hash = await sendTransactionAction(txRequest, {
                          method: 'eth_sendTransaction',
                          params: [z.encode(Rpc.transactionRequest, txRequest)] as const,
                        })
                        const chainId = Hex.fromNumber(store.getState().chainId)
                        const id = Hex.concat(hash, Hex.padLeft(chainId, 32), sendCallsMagic)
                        return { capabilities: { sync }, id }
                      }
                      const receipt = await sendTransactionSyncAction(txRequest as never, {
                        method: 'eth_sendTransactionSync',
                        params: [z.encode(Rpc.transactionRequest, txRequest)] as const,
                      })
                      const hash = receipt.transactionHash
                      const chainIdHex = Hex.fromNumber(store.getState().chainId)
                      const id = Hex.concat(hash, Hex.padLeft(chainIdHex, 32), sendCallsMagic)
                      return {
                        atomic: true,
                        capabilities: { sync },
                        chainId: chainIdHex,
                        id,
                        receipts: [receipt],
                        status: (receipt as { status: string }).status === '0x1' ? 200 : 500,
                        version: '2.0.0',
                      } satisfies Rpc.wallet_sendCalls.Encoded['returns']
                    } catch (error) {
                      throw withDetails(error)
                    }
                  }

                  case 'wallet_getBalances': {
                    const decoded = request._decoded.params?.[0]
                    const { accounts, activeAccount } = store.getState()
                    const account = decoded?.account ?? accounts[activeAccount]?.address
                    if (!account)
                      throw new ox_Provider.DisconnectedError({
                        message: 'No accounts connected.',
                      })
                    const tokens = decoded?.tokens
                    // TODO: hook up to indexer
                    if (!tokens || tokens.length === 0)
                      throw new RpcResponse.InvalidParamsError({
                        message: '`tokens` is required.',
                      })
                    const client = Client.fromChainId(decoded?.chainId, {
                      chains,
                      store,
                      transports,
                    })
                    return (await Promise.all(
                      tokens.map(async (token) => {
                        const [balance, metadata] = await Promise.all([
                          Actions.token.getBalance(client, { account, token }),
                          Actions.token.getMetadata(client, { token }),
                        ])
                        const value = Number(balance.amount) / 10 ** metadata.decimals
                        const display = new Intl.NumberFormat('en-US', {
                          style: 'currency',
                          currency: 'USD',
                        }).format(value)
                        return {
                          address: token,
                          balance: Hex.fromNumber(balance.amount),
                          decimals: metadata.decimals,
                          display,
                          name: metadata.name,
                          symbol: metadata.symbol,
                        }
                      }),
                    )) satisfies Rpc.wallet_getBalances.Encoded['returns']
                  }

                  case 'wallet_getCallsStatus': {
                    const [id] = request._decoded.params ?? []
                    if (!id) throw new Error('`id` not found')
                    if (!id.endsWith(sendCallsMagic.slice(2))) throw new Error('`id` not supported')
                    Hex.assert(id)
                    const hash = Hex.slice(id, 0, 32)
                    const chainId = Hex.fromNumber(Number(Hex.slice(id, 32, 64)))
                    const client = Client.fromChainId(Number(chainId), {
                      chains,
                      store,
                      transports,
                    })
                    const receipt = await client.request({
                      method: 'eth_getTransactionReceipt',
                      params: [hash],
                    })
                    return {
                      atomic: true,
                      chainId,
                      id,
                      receipts: receipt ? [receipt as never] : [],
                      status: (() => {
                        if (!receipt) return 100 // pending
                        if (receipt.status === '0x1') return 200 // success
                        return 500 // failed
                      })(),
                      version: '2.0.0',
                    } satisfies Rpc.wallet_getCallsStatus.Encoded['returns']
                  }

                  case 'wallet_getCapabilities': {
                    const decoded = request._decoded.params
                    const address = decoded?.[0]
                    const chainIds = decoded?.[1]

                    if (address) {
                      const { accounts } = store.getState()
                      if (!accounts.some((a) => AddressUtil.isEqual(a.address, address)))
                        throw new ox_Provider.UnauthorizedError({
                          message: `Address ${address} is not connected.`,
                        })
                    }

                    const filtered = chainIds
                      ? chains.filter((c) => chainIds.includes(Hex.fromNumber(c.id)))
                      : chains

                    const result: Record<
                      string,
                      {
                        accessKeys: { status: 'supported' }
                        atomic: { status: 'supported' }
                        feePayer?: { status: 'supported' } | undefined
                      }
                    > = {}
                    for (const chain of filtered)
                      result[Hex.fromNumber(chain.id)] = {
                        accessKeys: { status: 'supported' },
                        atomic: { status: 'supported' },
                        ...(feePayerConfig ? { feePayer: { status: 'supported' } } : {}),
                      }
                    return result as Rpc.wallet_getCapabilities.Encoded['returns']
                  }

                  case 'wallet_connect': {
                    const chainId = request._decoded.params?.[0]?.chainId
                    if (chainId) store.setState((x) => ({ ...x, chainId }))

                    const capabilities = request._decoded.params?.[0]?.capabilities
                    const authorizeAccessKey =
                      capabilities?.authorizeAccessKey ??
                      (await defaultAuthorizeAccessKeyForConnect({
                        capabilities,
                        chainId: chainId ?? store.getState().chainId,
                      }))

                    // Server Authentication: pre-resolve `auth` URLs against
                    // this dapp-side Provider's `window.location.origin`. The
                    // wallet host (different origin in dialog mode) cannot
                    // reconstruct the dapp's origin, so forwarding the raw
                    // relative URLs would resolve to the wrong host. We then
                    // fetch the challenge BEFORE the ceremony so we can fold
                    // its message into the existing `personalSign` capability.
                    // Forwarding adapters (dialog) skip orchestration — the
                    // wallet host's Provider runs it instead.
                    const auth_input = capabilities?.auth ?? options.auth
                    const auth_request = auth_input
                      ? absolutizeAuth(
                          auth_input as NonNullable<z.output<typeof Rpc.wallet_connect.auth>>,
                        )
                      : undefined
                    if (auth_request && typeof auth_request === 'object' && !auth_request.challenge)
                      throw new RpcResponse.InvalidParamsError({
                        message:
                          '`auth` capability must include either `url` or an explicit `challenge` endpoint.',
                      })
                    if (auth_request && capabilities?.personalSign)
                      throw new RpcResponse.InvalidParamsError({
                        message:
                          '`auth` and `personalSign` cannot both be set on `wallet_connect`.',
                      })

                    const authChainId = chainId ?? store.getState().chainId ?? 0
                    const accessKey = authorizeAccessKey
                      ? await prepareAuthorizeAccessKey(authorizeAccessKey, chainId)
                      : undefined

                    // Patch the raw request so forwarding adapters carry the
                    // absolutized auth URLs and prepared access-key material
                    // downstream.
                    if (auth_request || accessKey)
                      request = {
                        ...request,
                        params: [
                          {
                            ...request.params?.[0],
                            capabilities: {
                              ...request.params?.[0]?.capabilities,
                              ...(auth_request ? { auth: auth_request } : {}),
                              ...(accessKey
                                ? {
                                    authorizeAccessKey: z.encode(
                                      Rpc.wallet_connect.authorizeAccessKey,
                                      accessKey.parameters,
                                    ),
                                  }
                                : {}),
                            },
                          },
                        ] as never,
                      }

                    const auth =
                      auth_request && !instance.forwardsAuth
                        ? await fetchAuthChallenge(auth_request, authChainId)
                        : undefined

                    const personalSign_request = auth
                      ? { message: auth.message }
                      : capabilities?.personalSign

                    const {
                      accounts,
                      auth: auth_capability,
                      identity,
                      keyAuthorization,
                      personalSign,
                      signature,
                      username,
                    } = await (async () => {
                      if (capabilities?.method === 'register') {
                        // If a stored account already has this label, sign in
                        // with its credential instead of creating a new one.
                        const existing = capabilities.name
                          ? store
                              .getState()
                              .accounts.find(
                                (a) =>
                                  'credential' in a &&
                                  a.label?.toLowerCase() === capabilities.name!.toLowerCase(),
                              )
                          : undefined
                        if (existing && 'credential' in existing)
                          return await actions.loadAccounts(
                            {
                              credentialId: existing.credential?.id,
                              digest: capabilities.digest,
                              authorizeAccessKey: accessKey?.parameters,
                              ...(personalSign_request
                                ? { personalSign: personalSign_request }
                                : {}),
                              ...(capabilities.showDeposit !== undefined
                                ? { showDeposit: capabilities.showDeposit }
                                : {}),
                            },
                            request,
                          )
                        return await actions.createAccount(
                          {
                            digest: capabilities.digest,
                            authorizeAccessKey: accessKey?.parameters,
                            name: capabilities.name ?? 'default',
                            ...(capabilities.showDeposit !== undefined
                              ? { showDeposit: capabilities.showDeposit }
                              : {}),
                            userId: capabilities.userId ?? Hex.random(16),
                            ...(personalSign_request ? { personalSign: personalSign_request } : {}),
                          },
                          request,
                        )
                      }
                      return await actions.loadAccounts(
                        {
                          credentialId: capabilities?.credentialId,
                          digest: capabilities?.digest,
                          authorizeAccessKey: accessKey?.parameters,
                          selectAccount: capabilities?.selectAccount,
                          ...(personalSign_request ? { personalSign: personalSign_request } : {}),
                          ...(capabilities?.showDeposit !== undefined
                            ? { showDeposit: capabilities.showDeposit }
                            : {}),
                        },
                        request,
                      )
                    })()

                    store.setState({
                      accounts: resolveAccounts(accounts),
                      activeAccount: 0,
                      // Persist absolutized auth URLs so a later
                      // `wallet_disconnect` can hit logout even when the
                      // URL was passed per-call. Always overwrite (never
                      // merge) so a connect WITHOUT auth clears stale
                      // state from a prior connect — otherwise a later
                      // disconnect could POST to a logout URL the
                      // current page never opted into.
                      auth:
                        auth_request && typeof auth_request === 'object' ? auth_request : undefined,
                    })

                    const accountAddress = accounts[0]?.address
                    await savePreparedAccessKey({
                      accessKey,
                      account: accountAddress,
                      keyAuthorization,
                    })

                    // Local adapters return no identity claims — there is no
                    // wallet host to mint them. When the request asks for the
                    // email and an issuer is configured, mint the id token
                    // provider-side. Best-effort, mirroring the wallet host: a
                    // failed mint omits the claim, never the connect.
                    const identity_result =
                      identity ??
                      (await (async () => {
                        const email = capabilities?.identity?.email
                        if (!email || !options.identity || instance.forwardsAuth) return undefined
                        if (!accountAddress) return undefined
                        const audience = options.identity.audience ?? globalThis.location?.origin
                        if (!audience) return undefined
                        const nonce =
                          (typeof email === 'object' ? email.nonce : undefined) ??
                          (auth ? parseAuthNonce(auth.message) : undefined)
                        return await mintIdentity({
                          audience,
                          credentials: options.identity.credentials,
                          issuer: options.identity.issuer,
                          nonce,
                          subject: accountAddress,
                        }).catch(() => undefined)
                      })())

                    // Server Authentication verify: POST the signed SIWE message
                    // to the verify endpoint. Skipped when the auth capability
                    // omits `verify` — typical when the wallet host strips it
                    // so the dapp-origin Provider does the verify call (and
                    // receives the session cookie on the dapp's origin).
                    //
                    // The signed message comes from one of two places:
                    // - terminal Provider (wallet host): `auth.message` we just fetched.
                    // - forwarding Provider (dapp): `personalSign.message` echoed back
                    //   by the wallet host's Provider.
                    const verifyUrl =
                      auth_request && typeof auth_request === 'object'
                        ? auth_request.verify
                        : undefined
                    const verifyMessage = auth?.message ?? personalSign?.message
                    if (auth_request && verifyUrl && verifyMessage && signature && accountAddress)
                      validateAuthMessage(auth_request, {
                        chainId: authChainId,
                        message: verifyMessage,
                      })
                    const auth_result =
                      auth_request && verifyUrl && verifyMessage && signature && accountAddress
                        ? await verifyAuthMessage(auth_request, {
                            address: accountAddress,
                            message: verifyMessage,
                            signature,
                            // Forward the verified-email id token so a
                            // `Handler.auth({ identity })` can verify it and fold
                            // the email onto the session in the same round-trip.
                            // The minter reused this SIWE nonce, so the
                            // handler's nonce check passes.
                            ...(identity_result?.idToken
                              ? { idToken: identity_result.idToken }
                              : {}),
                            ...(personalSign?.keyAuthorization
                              ? { keyAuthorization: personalSign.keyAuthorization }
                              : {}),
                          })
                        : undefined

                    return {
                      accounts: accounts.map((a) => ({
                        address: a.address,
                        capabilities:
                          a.address === accountAddress
                            ? {
                                ...(keyAuthorization
                                  ? {
                                      keyAuthorization: {
                                        ...keyAuthorization,
                                        address: keyAuthorization.keyId,
                                      },
                                    }
                                  : {}),
                                ...(signature && (!auth_request || auth_result || !verifyUrl)
                                  ? { signature }
                                  : {}),
                                ...(username !== undefined ? { username } : {}),
                                ...((auth_result ?? auth_capability)
                                  ? { auth: auth_result ?? auth_capability }
                                  : {}),
                                ...(identity_result ? { identity: identity_result } : {}),
                                ...(personalSign
                                  ? {
                                      personalSign: {
                                        message: personalSign.message,
                                        ...(personalSign.keyAuthorization
                                          ? { keyAuthorization: personalSign.keyAuthorization }
                                          : {}),
                                      },
                                    }
                                  : {}),
                              }
                            : {},
                      })),
                    } satisfies Rpc.wallet_connect.Encoded['returns']
                  }

                  case 'wallet_disconnect': {
                    // Best-effort logout. Source of the URL, in order:
                    // 1. Last-connected `auth` URLs persisted in the store
                    //    (handles per-call `auth` passed via wallet_connect).
                    // 2. Provider.create({ auth }) option fallback.
                    // Swallows all errors — disconnect must succeed even
                    // when the session is already gone or the server is
                    // unreachable.
                    const logoutUrl = (() => {
                      const stored = store.getState().auth
                      if (stored?.logout) return stored.logout
                      if (!options.auth) return undefined
                      try {
                        const absolute = absolutizeAuth(
                          options.auth as NonNullable<z.output<typeof Rpc.wallet_connect.auth>>,
                        )
                        return typeof absolute === 'object' ? absolute.logout : undefined
                      } catch {
                        return undefined
                      }
                    })()
                    if (logoutUrl)
                      await fetch(logoutUrl, {
                        method: 'POST',
                        credentials: 'include',
                      }).catch(() => {})
                    await actions.disconnect?.()
                    store.disconnect()
                    return
                  }

                  case 'wallet_authorizeAccessKey': {
                    const decoded = request._decoded.params[0]
                    const result = await authorizeAccessKeyAction(decoded, request)
                    return {
                      keyAuthorization: {
                        ...result.keyAuthorization,
                        address: result.keyAuthorization.keyId,
                      },
                      rootAddress: result.rootAddress,
                    } satisfies Rpc.wallet_authorizeAccessKey.Encoded['returns']
                  }

                  case 'wallet_revokeAccessKey': {
                    assertConnected()
                    const [decoded] = request._decoded.params
                    await revokeAccessKeyAction({ ...decoded }, request)
                    return
                  }

                  case 'wallet_updateAccessKey': {
                    assertConnected()
                    const [decoded] = request._decoded.params
                    await updateAccessKeyAction({ ...decoded }, request)
                    return
                  }

                  case 'wallet_deposit': {
                    if (!actions.deposit)
                      throw new ox_Provider.UnsupportedMethodError({
                        message: '`deposit` not supported by adapter.',
                      })
                    return (await actions.deposit(
                      request._decoded.params?.[0] ?? {},
                      request,
                    )) satisfies Rpc.wallet_deposit.Encoded['returns']
                  }

                  case 'wallet_transfer': {
                    assertConnected()
                    // Default to the editable variant when params are
                    // omitted — Read-only mode requires `amount`,
                    // `to`, and `token`, so an empty call only makes
                    // sense as "open the wallet send UI".
                    const decoded = request._decoded.params?.[0] ?? { editable: true as const }

                    // Editable variant: forward to the wallet host UI.
                    if (decoded.editable === true) {
                      if (!actions.transfer)
                        throw new ox_Provider.UnsupportedMethodError({
                          message: '`transfer` not supported by adapter.',
                        })
                      const parameters = {
                        ...decoded,
                        ...(typeof decoded.feePayer !== 'undefined'
                          ? { feePayer: resolveFeePayer(decoded.feePayer) }
                          : {}),
                      } as Adapter.transfer.Parameters
                      return (await actions.transfer(
                        parameters,
                        request,
                      )) satisfies Rpc.wallet_transfer.Encoded['returns']
                    }

                    // Programmatic variant (default): skip the wallet UI,
                    // build the TIP-20 `transfer` call inline, and route
                    // through `eth_sendTransactionSync` (which uses an
                    // access key when one matches, falling back to the
                    // dialog otherwise).
                    const { amount, feePayer, from, memo, to, token } = decoded
                    const state = store.getState()
                    const chainId = decoded.chainId ?? state.chainId
                    const resolvedFeePayer = resolveFeePayer(feePayer)

                    const client = getClient({
                      chainId,
                      feePayer: typeof resolvedFeePayer === 'string' ? resolvedFeePayer : undefined,
                    })
                    const { address: tokenAddress, decimals } = await (async () => {
                      if (Address.validate(token)) {
                        const metadata = await Actions.token.getMetadata(client, {
                          token,
                        })
                        return { address: token, decimals: metadata.decimals }
                      }
                      const resolved = await Tokenlist.resolveSymbol({
                        chainId: client.chain.id,
                        symbol: token,
                      })
                      if (!resolved)
                        throw new ox_Provider.ProviderRpcError(
                          -32602,
                          `Unknown token symbol "${token}".`,
                        )
                      return { address: resolved.address, decimals: resolved.decimals }
                    })()
                    const amountUnits = parseUnits(amount, decimals)

                    // The signer is the active account (or its access
                    // key). `from` here is the TIP-20 source for
                    // `transferFrom` semantics, so we only forward it
                    // when the caller explicitly set it to a different
                    // address — otherwise `Actions.token.transfer.call`
                    // emits `transferFrom` (different selector) instead
                    // of plain `transfer`, breaking access-key scope
                    // matching.
                    const signerAddress = state.accounts[state.activeAccount]?.address
                    const sourceFrom =
                      from && signerAddress && from.toLowerCase() !== signerAddress.toLowerCase()
                        ? from
                        : undefined
                    const call = Actions.token.transfer.call({
                      amount: amountUnits,
                      ...(sourceFrom ? { from: sourceFrom } : {}),
                      memo: memo ? Hex.fromString(memo) : undefined,
                      to,
                      token: tokenAddress,
                    })

                    const txRequest = {
                      calls: [call],
                      chainId,
                      from: signerAddress,
                      ...(resolvedFeePayer !== undefined ? { feePayer: resolvedFeePayer } : {}),
                    }
                    const receipt = await sendTransactionSyncAction(txRequest, {
                      method: 'eth_sendTransactionSync',
                      params: [z.encode(Rpc.transactionRequest, txRequest)] as const,
                    })
                    return {
                      chainId: Hex.fromNumber(chainId),
                      receipt,
                    } satisfies Rpc.wallet_transfer.Encoded['returns']
                  }

                  case 'wallet_swap': {
                    assertConnected()
                    if (!actions.swap)
                      throw new ox_Provider.UnsupportedMethodError({
                        message: '`swap` not supported by adapter.',
                      })
                    return (await actions.swap(
                      (request._decoded.params?.[0] ?? {}) as Adapter.swap.Parameters,
                      request,
                    )) satisfies Rpc.wallet_swap.Encoded['returns']
                  }

                  case 'wallet_depositZone': {
                    assertConnected()
                    if (!actions.depositZone)
                      throw new ox_Provider.UnsupportedMethodError({
                        message: '`depositZone` not supported by adapter.',
                      })
                    const decoded = request._decoded.params?.[0] ?? {}
                    const parameters = {
                      ...decoded,
                      ...(typeof decoded.feePayer !== 'undefined'
                        ? { feePayer: resolveFeePayer(decoded.feePayer) }
                        : {}),
                    } as Adapter.depositZone.Parameters
                    return (await actions.depositZone(
                      parameters,
                      request,
                    )) satisfies Rpc.wallet_depositZone.Encoded['returns']
                  }

                  case 'wallet_withdrawZone': {
                    assertConnected()
                    if (!actions.withdrawZone)
                      throw new ox_Provider.UnsupportedMethodError({
                        message: '`withdrawZone` not supported by adapter.',
                      })
                    return (await actions.withdrawZone(
                      (request._decoded.params?.[0] ?? {}) as Adapter.withdrawZone.Parameters,
                      request,
                    )) satisfies Rpc.wallet_withdrawZone.Encoded['returns']
                  }

                  case 'wallet_switchEthereumChain': {
                    const { chainId } = request._decoded.params[0]
                    if (!chains.some((c) => c.id === chainId))
                      throw new ox_Provider.UnsupportedChainIdError({
                        message: `Chain ${chainId} not configured.`,
                      })
                    await actions.switchChain?.({ chainId })
                    store.setState({ chainId })
                    return
                  }
                }
              })()

              return result
            },
            {
              cache: requestCache,
              enabled: shouldDedupe,
              id: Json.stringify({ method, params }),
            },
          )
        },
      },
      { schema: Schema.ox },
    ),
    {
      chains,
      getAccount: ((options?: Omit<Account.find.Options, 'store'>) => {
        const account = getAccount(options)
        if (options?.signable) return account
        return { address: account.address, type: 'json-rpc' as const }
      }) as Provider['getAccount'],
      async getAccessKeyStatus(options: getAccessKeyStatus.Options = {}) {
        const state = store.getState()
        const address = options.address ?? state.accounts[state.activeAccount]?.address
        if (!address) return 'missing'
        const chainId = options.chainId ?? state.chainId
        const { accessKey, calls } = options
        return await store.accessKeys.getStatus({
          account: address,
          ...(accessKey ? { accessKey } : {}),
          ...(calls ? { calls } : {}),
          chainId,
          client: provider.getClient({ chainId }),
        })
      },
      getClient(options: { chainId?: number | undefined; feePayer?: string | undefined } = {}) {
        const { chainId, feePayer } = options
        return Client.fromChainId(chainId, {
          chains,
          feePayer,
          provider: providerRef,
          store,
          transports,
        })
      },
      getMppxParameters,
      store,
    },
  )

  if (
    typeof window !== 'undefined' &&
    typeof CustomEvent !== 'undefined' &&
    typeof crypto.randomUUID === 'function'
  ) {
    const rdns =
      adapter.rdns ?? `com.${(adapter.name ?? 'Injected Wallet').toLowerCase().replace(/\s+/g, '')}`

    if (!announced.has(rdns)) {
      announced.add(rdns)
      announceProvider({
        info: {
          icon: adapter.icon ?? defaultIcon,
          name: adapter.name ?? 'Injected Wallet',
          rdns,
          uuid: crypto.randomUUID(),
        },
        provider,
      } as never)
    }
  }

  const mpp = (() => {
    if (options.mpp === false) return undefined
    if (typeof options.mpp === 'object') return options.mpp
    return {}
  })()
  if (mpp) {
    const { mode = 'push', polyfill: polyfill_option, ...methodOptions } = mpp
    // Skip polyfill on runtimes where `globalThis.fetch` is read-only (e.g.
    // Cloudflare Workers). Caller can also explicitly opt out via `mpp.polyfill`.
    const polyfill = polyfill_option ?? isFetchWritable()
    const parameters = provider.getMppxParameters()
    const tempoOptions = {
      ...methodOptions,
      ...parameters,
      mode,
    }
    Mppx.create({
      methods: [mppx_tempo(tempoOptions), mppx_tempo.subscription(parameters)],
      polyfill,
    })
  }

  providerRef = provider

  return provider
}

const defaultIcon =
  'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"><rect width="1" height="1"/></svg>' as const
const sendCallsMagic = Hash.keccak256(Hex.fromString('TEMPO_5792'))

export declare namespace create {
  type Options = {
    /** Access-key configuration: authorization policy and key material. */
    accessKey?:
      | {
          /**
           * Access-key parameters to authorize automatically when no stored
           * key satisfies a request.
           *
           * Applies to `wallet_connect` and transaction sends. Pass an object
           * to use the same parameters for every request, or a function to
           * compute them per request — return `undefined` to skip
           * authorization for that request.
           */
          authorize?: AuthorizeAccessKey | undefined
          /**
           * Keystores backing provider-generated access keys, one per key
           * type. A keystore creates key material and turns persisted
           * records back into signing accounts — see
           * {@link Keystore.Keystore} for the contract.
           *
           * App-level keystores override adapter-supplied defaults.
           * Keystores hold key material; `storage` persists provider state.
           * Access keys created before a keystore was configured keep
           * working.
           *
           * @default Keystore.defaults — `{ p256: Keystore.webCryptoP256() }`
           *
           * @example
           * ```ts
           * import { Keystore, Provider } from 'accounts'
           *
           * const provider = Provider.create({
           *   accessKey: {
           *     keystores: { p256: Keystore.webCryptoP256({ extractable: true }) },
           *   },
           * })
           * ```
           */
          keystores?: Keystore.Keystores | undefined
        }
      | undefined
    /** Adapter to use for account management. @default dialog() */
    adapter?: Adapter.Adapter | undefined
    /**
     * Default Server Authentication configuration for `wallet_connect`.
     *
     * When set, every `wallet_connect` call orchestrates the round-trip
     * against this endpoint unless the caller passes their own
     * `capabilities.auth` (per-call override).
     */
    auth?: z.input<typeof Rpc.wallet_connect.auth> | undefined
    /** @deprecated Use `accessKey.authorize` instead. */
    authorizeAccessKey?: AuthorizeAccessKey | undefined
    /**
     * Supported chains. First chain is the default.
     * @default [tempo, tempoModerato, tempoDevnet]
     */
    chains?: readonly [Chain, ...Chain[]] | undefined
    /** Fee payer configuration. @see {@link Client.fromChainId.Options.feePayer} */
    feePayer?: Client.fromChainId.Options['feePayer']
    /**
     * Identity (verified email) token minting for local adapters. When a
     * `wallet_connect` request asks for `identity.email` and the adapter
     * returns no identity claims (local adapters have no wallet host to mint
     * them), the provider mints the id token from this issuer's `/token`
     * route (`Handler.oidcProvider` shape). The issuer must trust
     * body-supplied subjects, so this is for development deployments.
     */
    identity?:
      | {
          /** Audience (`aud`) bound into minted tokens. @default `location.origin` */
          audience?: string | undefined
          /** Credential mode for token requests. @default "same-origin" */
          credentials?: RequestCredentials | undefined
          /** OIDC issuer whose `/token` route mints the id token. */
          issuer: string
        }
      | undefined
    /** Maximum number of accounts to persist. Oldest accounts are evicted when exceeded (LRU). */
    maxAccounts?: number | undefined
    /**
     * Enable Machine Payment Protocol (mppx) support.
     *
     * Pass an options object to configure, or `false` to disable.
     *
     * @default true
     */
    mpp?: boolean | mpp.Options | undefined
    /** Whether to persist credentials and access keys to storage. When `false`, only account addresses are persisted. @default true */
    persistCredentials?: boolean | undefined
    /**
     * Base URL for a wallet relay endpoint. When set, every chain's transport
     * defaults to `http(`${relay}/${chainId}`)` — a single endpoint that
     * routes by chain ID via the path. Per-chain entries in `transports`
     * override this on a chain-by-chain basis.
     *
     * @example
     * ```ts
     * const provider = Provider.create({ relay: '/relay' })
     * // tempo (33139) → http('/relay/33139')
     * // tempoModerato → http('/relay/<id>')
     * ```
     */
    relay?: string | undefined
    /** Storage adapter for persistence. @default Storage.idb() in browser, Storage.memory() otherwise. */
    storage?: Storage.Storage | undefined
    /**
     * Use testnet.
     * @default false
     */
    testnet?: boolean | undefined
    /**
     * Per-chain transports keyed by chain ID. When omitted, defaults to
     * `http()` for each chain (uses the chain's default RPC URL).
     *
     * @example
     * ```ts
     * import { http } from 'viem'
     * import { tempo, tempoModerato } from 'viem/tempo/chains'
     *
     * const provider = Provider.create({
     *   transports: {
     *     [tempo.id]: http('/relay/' + tempo.id),
     *     [tempoModerato.id]: http('/relay/' + tempoModerato.id),
     *   },
     * })
     * ```
     */
    transports?: Record<number, Transport> | undefined
  }

  /** Access-key parameters to authorize automatically, with SDK-only reuse policy. */
  type AuthorizeAccessKeyParameters = Adapter.authorizeAccessKey.Parameters & {
    /** SDK-only policy for deciding whether a stored local key can be reused. */
    reuse?: AccessKey.ReusePolicy | undefined
  }
  /** Static or per-request access-key authorization parameters. */
  type AuthorizeAccessKey =
    | AuthorizeAccessKeyParameters
    | (() => AuthorizeAccessKeyParameters | undefined)
  type ReturnType = Provider
}

export declare namespace getAccessKeyStatus {
  /** Options for {@link Provider.getAccessKeyStatus}. */
  type Options = {
    /** Root account address. Defaults to the active account. */
    address?: Address.Address | undefined
    /** Specific access key address to query. When omitted, the first locally matching key is used. */
    accessKey?: Address.Address | undefined
    /** Calls to match against access key scopes. */
    calls?: readonly { to?: Address.Address | undefined; data?: Hex.Hex | undefined }[] | undefined
    /** Chain ID the access key must be authorized on. Defaults to the active chain. */
    chainId?: number | undefined
  }

  /** Access-key publication status. */
  type ReturnType = 'missing' | 'pending' | 'published' | 'expired'
}

export declare namespace getMppxParameters {
  /** Options for {@link Provider.getMppxParameters}. */
  type Options = {
    /** Specific access key address to use for mppx signing. */
    accessKey?: Address.Address | undefined
  }
}

export declare namespace mpp {
  /** Options for Machine Payment Protocol (mppx) integration. */
  type Options = Omit<mppx_tempo.Parameters, 'account' | 'getClient' | 'resolveAccount'> & {
    /**
     * Whether to polyfill `globalThis.fetch` with the payment-aware wrapper.
     *
     * Defaults to `true` when `globalThis.fetch` is writable, and `false`
     * otherwise (e.g. Cloudflare Workers, where `globalThis.fetch` is
     * read-only).
     */
    polyfill?: boolean | undefined
  }
}

function withDetails(error: unknown): Error & { details: string } {
  if (error instanceof Error) {
    const details = (error as { details?: unknown }).details
    if (typeof details === 'string') return error as Error & { details: string }
    Object.assign(error, { details: error.message })
    return error as Error & { details: string }
  }
  const next = new Error(String(error))
  Object.assign(next, { details: next.message })
  return next as Error & { details: string }
}

/**
 * Returns `true` if `globalThis.fetch` can be reassigned. Some runtimes
 * (notably Cloudflare Workers) expose a non-writable, non-configurable
 * `fetch` that throws when `Mppx.create({ polyfill: true })` tries to
 * replace it.
 *
 * Tries an actual no-op self-reassignment because some runtimes report a
 * writable descriptor but still throw at assignment time (e.g. Workers
 * dev runner via Durable Objects).
 */
function isFetchWritable(): boolean {
  try {
    const original = globalThis.fetch
    globalThis.fetch = original
    return true
  } catch {
    return false
  }
}

/**
 * Heuristic for whether the current runtime carries cookies on
 * `fetch(..., { credentials: 'include' })`:
 *
 * - **Browser**: `document.cookie` exists → uses the browser cookie jar.
 * - **React Native / Node / CLI**: neither — `credentials: 'include'` is not
 *   enough for the SDK to surface a bearer token to the caller.
 *
 * False negatives are possible (Node with `tough-cookie` shimmed in); the
 * caller can always force token mode via `auth: { returnToken: true }`.
 */
function hasCookieJar(): boolean {
  if (typeof document !== 'undefined' && typeof document.cookie === 'string') return true
  return false
}

/**
 * Resolves a Server Authentication endpoint from the `auth` capability
 * into an absolute URL.
 *
 * - `auth: '/api/auth'`            → `/api/auth/challenge`, `/api/auth` (verify), `/api/auth/logout`
 * - `auth: { url: '/api/auth' }`   → same as above
 * - `auth: { challenge, verify }`  → explicit per-endpoint
 * - Mix: explicit endpoint wins over derivation from `url`.
 *
 * Relative paths (`/api/auth`, `auth/challenge`) are absolutized against
 * `window.location.origin` when available — same shape as `resolveFeePayer`.
 * Already-absolute `http(s)://` URLs pass through verbatim.
 */
function resolveAuthEndpoint(
  auth: NonNullable<z.output<typeof Rpc.wallet_connect.auth>>,
  kind: 'challenge' | 'verify' | 'logout',
): string {
  const path = (() => {
    if (typeof auth === 'string') {
      const base = auth.endsWith('/') ? auth.slice(0, -1) : auth
      return kind === 'verify' ? base : `${base}/${kind}`
    }
    const explicit = auth[kind]
    if (explicit) return explicit
    if (auth.url) {
      const base = auth.url.endsWith('/') ? auth.url.slice(0, -1) : auth.url
      return kind === 'verify' ? base : `${base}/${kind}`
    }
    throw new RpcResponse.InvalidParamsError({
      message: `\`auth\` capability must include either \`url\` or an explicit \`${kind}\` endpoint.`,
    })
  })()
  if (path.startsWith('http://') || path.startsWith('https://')) return path
  if (typeof window !== 'undefined') return new URL(path, window.location.origin).href
  return path
}

/**
 * Pre-resolves the `auth` capability into its absolute object form. Run
 * once at the dapp-side Provider so forwarding adapters (dialog) carry
 * absolute URLs to the wallet host — the wallet's `window.location.origin`
 * belongs to the wallet, not the dapp, and cannot resolve relative paths
 * correctly.
 *
 * Individual endpoints are omitted when the input doesn't supply enough
 * info to derive them. `logout` is optional in the protocol; `verify` can
 * also be omitted by wallet-host re-entry so the dapp-origin Provider runs
 * verification and receives the session cookie.
 */
function absolutizeAuth(
  auth: NonNullable<z.output<typeof Rpc.wallet_connect.auth>>,
): NonNullable<z.output<typeof Rpc.wallet_connect.auth>> {
  // Wallet-host re-entry can strip endpoints (e.g. drop `verify` so the
  // dapp-origin Provider runs verify). Only resolve endpoints the input
  // can derive — pass through everything else as-is.
  const hasUrl = typeof auth === 'string' || Boolean(auth.url)
  const hasChallenge = hasUrl || (typeof auth === 'object' && Boolean(auth.challenge))
  const hasVerify = hasUrl || (typeof auth === 'object' && Boolean(auth.verify))
  const hasLogout = hasUrl || (typeof auth === 'object' && Boolean(auth.logout))
  const resolved = {
    ...(hasChallenge ? { challenge: resolveAuthEndpoint(auth, 'challenge') } : {}),
    ...(hasVerify ? { verify: resolveAuthEndpoint(auth, 'verify') } : {}),
    ...(hasLogout ? { logout: resolveAuthEndpoint(auth, 'logout') } : {}),
    ...(typeof auth === 'object' && auth.resources !== undefined
      ? { resources: auth.resources }
      : {}),
    ...(typeof auth === 'object' && auth.returnToken ? { returnToken: true } : {}),
  }
  assertSameAuthOrigin(resolved)
  return resolved
}

function assertSameAuthOrigin(auth: NonNullable<z.output<typeof Rpc.wallet_connect.auth>>): void {
  if (typeof auth !== 'object') return
  const urls = [auth.challenge, auth.verify, auth.logout].filter(
    (u): u is string => typeof u === 'string',
  )
  const origins = urls.map((url) => {
    try {
      return new URL(url).origin
    } catch {
      throw new RpcResponse.InvalidParamsError({
        message: `\`auth\` endpoint is not a valid URL: ${url}`,
      })
    }
  })
  const first = origins[0]!
  if (origins.some((origin) => origin !== first))
    throw new RpcResponse.InvalidParamsError({
      message: '`auth` endpoints (`challenge`, `verify`, `logout`) must share the same origin.',
    })
}

/**
 * Hint appended to "domain mismatch" / "uri mismatch" errors raised in
 * {@link fetchAuthChallenge}. Most of the time these come from a server
 * sitting behind a TLS-terminating proxy (Cloudflare Tunnel, ngrok, a
 * CDN) that forwards `x-forwarded-proto` / `x-forwarded-host` headers the
 * auth handler isn't honoring by default.
 */
const authOriginHint =
  ' Hint: if the server is behind a reverse proxy or tunnel, set `Handler.auth({ trustProxy: true })` to honor `x-forwarded-*` headers, or pin the public origin with `Handler.auth({ origin: "https://app.example.com" })`.'

function stringsEqual(a: readonly string[], b: readonly string[]): boolean {
  if (a.length !== b.length) return false
  return a.every((value, index) => value === b[index])
}

function validateAuthMessage(
  auth: NonNullable<z.output<typeof Rpc.wallet_connect.auth>>,
  options: {
    chainId: number
    message: string
    url?: string | undefined
  },
): void {
  const { chainId, message } = options
  const url =
    options.url ??
    (typeof auth === 'object' ? auth.challenge! : resolveAuthEndpoint(auth, 'challenge'))
  const resources = typeof auth === 'object' ? auth.resources : undefined
  const parsed = parseSiweMessage(message)
  const expected = new URL(url)

  if (parsed.version !== '1')
    throw new RpcResponse.InvalidParamsError({
      message: `Server Authentication challenge endpoint \`${url}\` returned a non-SIWE-v1 message.`,
    })
  if (!parsed.nonce)
    throw new RpcResponse.InvalidParamsError({
      message: `Server Authentication challenge endpoint \`${url}\` response is missing a \`nonce\`.`,
    })
  if (parsed.domain !== expected.host)
    throw new RpcResponse.InvalidParamsError({
      message: `Server Authentication challenge endpoint \`${url}\` returned a message bound to \`${parsed.domain}\` (expected \`${expected.host}\`).${authOriginHint}`,
    })
  if (parsed.uri !== expected.origin)
    throw new RpcResponse.InvalidParamsError({
      message: `Server Authentication challenge endpoint \`${url}\` returned a message with \`uri\` \`${parsed.uri}\` (expected \`${expected.origin}\`).${authOriginHint}`,
    })
  if (parsed.chainId !== chainId)
    throw new RpcResponse.InvalidParamsError({
      message: `Server Authentication challenge endpoint \`${url}\` returned a message bound to chainId \`${parsed.chainId}\` (expected \`${chainId}\`).`,
    })
  if (resources !== undefined && !stringsEqual(parsed.resources ?? [], resources))
    throw new RpcResponse.InvalidParamsError({
      message: `Server Authentication challenge endpoint \`${url}\` did not echo the requested SIWE resources.`,
    })
}

/**
 * Fetches an auth challenge from the auth endpoint and validates that the
 * server-supplied message is bound to the auth endpoint's origin and the
 * requested chain.
 *
 * Expects an absolutized auth capability (post-`absolutizeAuth`).
 *
 * The signature produced from this challenge is a portable artifact: once
 * the wallet signs, anyone holding the bytes can replay it against any
 * auth verifier that accepts the embedded domain. We therefore refuse to
 * sign a message whose `domain`/`uri` doesn't match the auth endpoint —
 * otherwise a compromised auth provider could trick the wallet into
 * signing "Sign in to attacker.com" and use it to log in as the user
 * elsewhere.
 */
async function fetchAuthChallenge(
  auth: NonNullable<z.output<typeof Rpc.wallet_connect.auth>>,
  chainId: number,
): Promise<{ message: string }> {
  const url = typeof auth === 'object' ? auth.challenge! : resolveAuthEndpoint(auth, 'challenge')
  const resources = typeof auth === 'object' ? auth.resources : undefined
  const res = await fetch(url, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({
      chainId,
      ...(resources !== undefined ? { resources } : {}),
    }),
  })
  if (!res.ok)
    throw new RpcResponse.InvalidParamsError({
      message: `Server Authentication challenge endpoint \`${url}\` returned ${res.status}.`,
    })
  const body = (await res.json().catch(() => ({}))) as { message?: string }
  if (!body.message)
    throw new RpcResponse.InvalidParamsError({
      message: `Server Authentication challenge endpoint \`${url}\` response missing \`message\`.`,
    })

  validateAuthMessage(auth, { chainId, message: body.message, url })

  return { message: body.message }
}

/**
 * Extracts the nonce from an EIP-4361 (SIWE) challenge message so a minted
 * identity token binds to the same single-use value the server checks.
 */
function parseAuthNonce(message: string) {
  return message.match(/^Nonce: (.+)$/m)?.[1]
}

/**
 * Mints a verified-email identity token from an OIDC issuer's `/token` route
 * (`Handler.oidcProvider` shape). Local adapters fulfill the connect ceremony
 * without a wallet host, so the provider mints the token itself when
 * configured (see `create.Options.identity`).
 */
async function mintIdentity(options: {
  audience: string
  credentials?: RequestCredentials | undefined
  issuer: string
  nonce?: string | undefined
  subject: string
}): Promise<{ email: string | null; idToken: string }> {
  const { audience, credentials, issuer, nonce, subject } = options
  const res = await fetch(`${issuer.replace(/\/+$/, '')}/token`, {
    body: JSON.stringify({
      audience,
      ...(nonce ? { nonce } : {}),
      subject,
    }),
    ...(credentials ? { credentials } : {}),
    headers: { 'content-type': 'application/json' },
    method: 'POST',
  })
  if (!res.ok) throw new Error(`Identity issuer \`${issuer}\` returned ${res.status}.`)
  const { idToken } = (await res.json()) as { idToken: string }
  // Display-only claim read; server-side trust comes from JWKS verification.
  const payload = JSON.parse(
    atob(idToken.split('.')[1]!.replace(/-/g, '+').replace(/_/g, '/')),
  ) as { email?: string | undefined }
  return { email: payload.email ?? null, idToken }
}

/**
 * Posts the signed message to the auth `verify` endpoint and returns
 * the SDK-shaped `auth` capability output. `{ token }` remains reserved
 * for token mode; other JSON fields returned by the verify endpoint are
 * preserved for application-specific metadata.
 */
async function verifyAuthMessage(
  auth: NonNullable<z.output<typeof Rpc.wallet_connect.auth>>,
  body: {
    address: Address.Address
    idToken?: string | undefined
    message: string
    signature: Hex.Hex
    keyAuthorization?: Hex.Hex | undefined
  },
): Promise<AuthCapabilityResult> {
  const url = typeof auth === 'object' ? auth.verify! : resolveAuthEndpoint(auth, 'verify')
  // Auto-request the token in environments without a cookie jar (React
  // Native / Node / CLI). Browser lets the cookie do the work; explicit
  // `returnToken: true` always wins.
  const explicitReturnToken = typeof auth === 'object' && auth.returnToken === true
  const returnToken = explicitReturnToken || !hasCookieJar()
  const res = await fetch(url, {
    method: 'POST',
    credentials: 'include',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({
      ...body,
      ...(returnToken ? { returnToken: true } : {}),
    }),
  })
  if (!res.ok)
    throw new RpcResponse.InternalError({
      message: `Server Authentication verify endpoint \`${url}\` returned ${res.status}.`,
    })
  const json = await res.json().catch(() => ({}))
  if (!json || typeof json !== 'object' || Array.isArray(json)) return {}
  return json as AuthCapabilityResult
}