import type { Address } from 'abitype'
import * as Address_ from 'ox/Address'
import * as Hash from 'ox/Hash'
import * as Hex from 'ox/Hex'
import * as RpcResponse from 'ox/RpcResponse'
import {
  KeyAuthorization,
  MultisigConfig,
  MultisigOperation,
  Transaction as ox_Transaction,
  SignatureEnvelope,
  TxEnvelopeTempo,
} from 'ox/tempo'
import { getBlockNumber } from '../actions/public/getBlockNumber.js'
import { createClient } from '../clients/createClient.js'
import { custom } from '../clients/transports/custom.js'
import type { EIP1193RequestOptions } from '../types/eip1193.js'
import { decodeFunctionData } from '../utils/abi/decodeFunctionData.js'
import { isAddressEqual } from '../utils/address/isAddressEqual.js'
import * as Abis from './Abis.js'
import * as Addresses from './Addresses.js'
import { getConfigCommitment } from './actions/multisig.js'
import * as ConfigStore from './multisig/Config.js'
import * as OperationStore from './multisig/Operation.js'
import type * as Store from './Store.js'
import * as Transaction from './Transaction.js'

const submissionTtl = 30_000
const pollingInterval = 100

/**
 * Creates an RPC request handler that coordinates native multisig approvals.
 *
 * @param next - Downstream RPC request handler.
 * @param parameters - Handler parameters.
 * @returns The multisig-aware RPC request handler.
 */
export function handleRequest(
  next: handleRequest.Handler,
  parameters: handleRequest.Parameters,
): handleRequest.Handler {
  if (!parameters.store.compareAndSet)
    throw new RpcResponse.InvalidParamsError({
      message:
        'Multisig coordination requires a store with atomic `compareAndSet`.',
    })
  return async (request, requestOptions_) => {
    const requestOptions = await resolveRequestOptions({
      request,
      requestOptions: requestOptions_,
      store: parameters.store,
    })
    const client = createClient({
      transport: custom({
        request: ({ method, params }, options) =>
          next({ method, params }, { ...requestOptions, ...options }),
      }),
    })

    if (request.method === 'multisig_getConfig') {
      const value = request.params?.[0]
      const address =
        value && typeof value === 'object' && 'address' in value
          ? value.address
          : undefined
      if (
        typeof address !== 'string' ||
        !Address_.validate(address) ||
        Hex.toBigInt(address) === 0n
      )
        throw new RpcResponse.InvalidParamsError({
          message: 'Expected a multisig account address.',
        })
      const blockNumber = await getBlockNumber(client, { cacheTime: 0 })
      const commitment = await getConfigCommitment(client, {
        account: address,
        blockNumber,
      })
      const config = await ConfigStore.read(parameters.store, {
        address,
        commitment,
      })
      if (!config) return null
      return MultisigConfig.toRpc(config)
    }

    if (request.method === 'multisig_getOperation') {
      const hash = request.params?.[0]
      if (typeof hash !== 'string' || !Hash.validate(hash))
        throw new RpcResponse.InvalidParamsError({
          message: 'Expected a multisig operation hash.',
        })
      const operation = await OperationStore.read(parameters.store, hash)
      return operation ? MultisigOperation.toRpc(operation) : null
    }

    if (request.method === 'multisig_approveKeyAuthorization')
      return await approveKeyAuthorization({
        client,
        request,
        store: parameters.store,
      })

    if (
      request.method === 'eth_getTransactionByHash' ||
      request.method === 'eth_getTransactionReceipt'
    ) {
      const hash = request.params?.[0]
      if (typeof hash !== 'string' || !Hash.validate(hash))
        return await next(request, requestOptions)
      const operation = await OperationStore.read(parameters.store, hash)
      if (!operation || operation.type !== 'transaction')
        return await next(request, requestOptions)
      if (request.method === 'eth_getTransactionReceipt') {
        if (operation.status === 'pending') return null
        const transactionHash = await getSubmittedTransactionHash(
          parameters.store,
          operation,
        )
        if (!transactionHash) return null
        const receipt = await next(
          {
            ...request,
            params: [transactionHash],
          },
          requestOptions,
        )
        if (!receipt || typeof receipt !== 'object') return receipt
        const success =
          operation.status === 'submitting'
            ? await completeSubmission(
                parameters.store,
                operation,
                transactionHash,
              )
            : operation
        return {
          ...receipt,
          multisig: MultisigOperation.toRpc(success),
        }
      }
      if (operation.status === 'pending')
        return await toTransaction(client, operation)
      const transactionHash = await getSubmittedTransactionHash(
        parameters.store,
        operation,
      )
      if (!transactionHash) return await toTransaction(client, operation)
      const transaction = await next(
        {
          ...request,
          params: [transactionHash],
        },
        requestOptions,
      )
      if (!transaction || typeof transaction !== 'object')
        return await toTransaction(client, operation)
      const success =
        operation.status === 'submitting'
          ? await completeSubmission(
              parameters.store,
              operation,
              transactionHash,
            )
          : operation
      return {
        ...transaction,
        multisig: MultisigOperation.toRpc(success),
      }
    }

    if (
      request.method !== 'eth_sendRawTransaction' &&
      request.method !== 'eth_sendRawTransactionSync' &&
      request.method !== 'multisig_approveRawTransaction' &&
      request.method !== 'multisig_approveRawTransactionSync'
    )
      return await next(request, requestOptions)

    const standard =
      request.method === 'eth_sendRawTransaction' ||
      request.method === 'eth_sendRawTransactionSync'
    const serialized = request.params?.[0]
    if (!isSerializedTempoTransaction(serialized)) {
      if (standard) return await next(request, requestOptions)
      throw new RpcResponse.InvalidParamsError({
        message: 'Expected a serialized Tempo multisig transaction.',
      })
    }

    return await submit({
      client,
      method: request.method,
      next,
      request,
      requestOptions,
      serialized,
      store: parameters.store,
    })
  }
}

export declare namespace handleRequest {
  /** RPC request handler. */
  export type Handler = (
    request: Request,
    options?: RequestOptions | undefined,
  ) => Promise<unknown>

  /** RPC request passed to a handler. */
  export type Request = {
    /** RPC method name. */
    method: string
    /** RPC method parameters. */
    params?: readonly unknown[] | undefined
  }

  /** Options for one handled request. */
  export type RequestOptions = EIP1193RequestOptions & {
    /** Chain selected by the caller or inferred from the multisig request. */
    chainId?: number | undefined
  }

  /** Parameters for {@link handleRequest}. */
  export type Parameters = {
    /** Store shared by multisig coordinators. */
    store: Store.Atomic
  }

  /** Error type for {@link handleRequest}. */
  export type ErrorType =
    | ConfigStore.InvalidStoreValueError
    | OperationStore.InvalidStoreValueError
    | OperationStore.StoreConflictError
    | RpcResponse.InvalidParamsError
}

/** Collects approvals and submits a transaction after quorum. @internal */
// biome-ignore lint/correctness/noUnusedVariables: _
async function submit(options: submit.Options) {
  const transaction = (() => {
    try {
      return deserialize(options.serialized)
    } catch (error) {
      if (
        options.method !== 'eth_sendRawTransaction' &&
        options.method !== 'eth_sendRawTransactionSync'
      )
        throw error
      return undefined
    }
  })()
  const signature = transaction?.signature
  if (!transaction || signature?.type !== 'multisig') {
    if (
      options.method === 'eth_sendRawTransaction' ||
      options.method === 'eth_sendRawTransactionSync'
    )
      return await options.next(options.request, options.requestOptions)
    throw new RpcResponse.InvalidParamsError({
      message: 'Expected a multisig transaction signature.',
    })
  }

  const { signature: _, ...unsigned } = transaction
  const envelope = TxEnvelopeTempo.from(unsigned as never)
  const serializedUnsigned = serializeUnsigned(
    envelope,
    options.serialized.startsWith(TxEnvelopeTempo.feePayerMagic),
  )
  const blockNumber = await getBlockNumber(options.client, { cacheTime: 0 })
  const config = MultisigConfig.from(signature.config)
  const configCommitment = await validateConfig({
    account: signature.account,
    blockNumber,
    client: options.client,
    config,
  })
  const operationHash = MultisigOperation.getHash({
    account: signature.account,
    config,
    transaction: serializedUnsigned,
    type: 'transaction',
  })
  const incoming = signature.signatures.map((signature) =>
    SignatureEnvelope.serialize(signature),
  )
  if (incoming.length === 0)
    throw new RpcResponse.InvalidParamsError({
      message: 'A multisig approval envelope must include a signature.',
    })
  const now = Date.now()
  const expiredSubmissionIds = new Set<Hex.Hex>()
  const operation = await OperationStore.update(
    options.store,
    operationHash,
    async (existing) => {
      if (existing && existing.type !== 'transaction')
        throw new OperationStore.InvalidStoreValueError()
      if (existing?.status === 'success') return existing
      if (existing?.status === 'submitting' && existing.expiresAt! > now)
        return existing
      if (existing?.status === 'submitting')
        expiredSubmissionIds.add(existing.submissionId!)
      const existingApprovals = existing
        ? await selectApprovals({
            account: signature.account,
            config,
            hash: operationHash,
            approvals: existing.approvals,
          })
        : undefined
      const approvals = await selectApprovals({
        account: signature.account,
        config,
        hash: operationHash,
        approvals: [...(existingApprovals?.approvals ?? []), ...incoming],
      })
      return MultisigOperation.from({
        account: signature.account,
        approvals: approvals.approvals,
        config,
        createdAt: existing?.createdAt ?? now,
        hash: operationHash,
        signatureCount: approvals.signatureCount,
        status: 'pending',
        threshold: approvals.threshold,
        transaction: mergeTransaction(
          existing?.transaction,
          serializedUnsigned,
        ),
        type: 'transaction',
        updatedAt: now,
        weight: approvals.weight,
      })
    },
  )

  for (const submissionId of expiredSubmissionIds) {
    if (
      operation.status === 'submitting' &&
      operation.submissionId?.toLowerCase() === submissionId.toLowerCase()
    )
      continue
    await removeSettledSubmission(options.store, operationHash, submissionId)
  }

  await ConfigStore.write(options.store, {
    address: signature.account,
    commitment: configCommitment,
    config,
  })
  await cacheNextConfigs({
    account: signature.account,
    config,
    store: options.store,
    transaction,
  })

  if (operation.type !== 'transaction')
    throw new OperationStore.InvalidStoreValueError()
  if (operation.status === 'success')
    return await submittedResult(options, operation)
  if (operation.status === 'submitting' && operation.expiresAt! > Date.now())
    return await submittingResult(options, operation)
  if (operation.weight < operation.threshold)
    return pendingResult(options.method, operation)

  const submissionId = Hex.random(32)
  const timeout = options.request.params?.[1]
  const synchronous =
    options.method === 'eth_sendRawTransactionSync' ||
    options.method === 'multisig_approveRawTransactionSync'
  const leaseTtl =
    synchronous &&
    typeof timeout === 'number' &&
    Number.isSafeInteger(timeout) &&
    timeout >= 0
      ? timeout + submissionTtl
      : submissionTtl
  const claim = await OperationStore.update(
    options.store,
    operationHash,
    (current) => {
      if (!current || current.type !== 'transaction')
        throw new OperationStore.InvalidStoreValueError()
      if (current.status === 'success') return current
      if (current.status === 'submitting' && current.expiresAt! > Date.now())
        return current
      if (current.weight < current.threshold) return current
      const now = Date.now()
      return MultisigOperation.from({
        ...current,
        expiresAt: Math.min(now + leaseTtl, Number.MAX_SAFE_INTEGER),
        status: 'submitting',
        submissionId,
        updatedAt: now,
      })
    },
  )
  if (claim.type !== 'transaction')
    throw new OperationStore.InvalidStoreValueError()
  if (claim.status === 'success') return await submittedResult(options, claim)
  if (claim.status === 'pending') return pendingResult(options.method, claim)
  if (claim.submissionId !== submissionId)
    return await submittingResult(options, claim)

  const { final, transactionHash: initialTransactionHash } =
    await (async () => {
      try {
        const blockNumber = await getBlockNumber(options.client, {
          cacheTime: 0,
        })
        await validateConfig({
          account: claim.account,
          blockNumber,
          client: options.client,
          config: claim.config,
        })
        const finalApprovals = await selectApprovals({
          account: claim.account,
          config: claim.config,
          hash: claim.hash,
          approvals: claim.approvals,
        })
        const final = MultisigOperation.serializeTransaction(claim, {
          approvals: finalApprovals.selectedApprovals,
        })
        const transactionHash = TxEnvelopeTempo.hash(
          TxEnvelopeTempo.deserialize(final) as TxEnvelopeTempo.Signed,
        )
        await OperationStore.writeSubmission(
          options.store,
          operationHash,
          submissionId,
          final,
        )
        return { final, transactionHash }
      } catch (error) {
        await releaseSubmission(options.store, operationHash, submissionId)
        throw error
      }
    })()
  let transactionHash = initialTransactionHash

  let result: unknown
  const stopLease = maintainSubmissionLease(
    options.store,
    operationHash,
    submissionId,
    leaseTtl,
  )
  let leaseStopped = false
  const stopLeaseOnce = async () => {
    if (leaseStopped) return
    leaseStopped = true
    await stopLease()
  }
  try {
    try {
      result = await options.next(
        {
          method:
            options.method === 'eth_sendRawTransaction' ||
            options.method === 'multisig_approveRawTransaction'
              ? 'eth_sendRawTransaction'
              : 'eth_sendRawTransactionSync',
          params: [final, ...(options.request.params?.slice(1) ?? [])],
        },
        options.requestOptions,
      )
      const returnedHash = getTransactionHash(result)
      if (returnedHash.toLowerCase() !== transactionHash.toLowerCase())
        transactionHash = returnedHash
    } catch (error) {
      const submittedHash = await OperationStore.readSubmission(
        options.store,
        claim,
        submissionId,
      )
      const transaction = await (async () => {
        if (!submittedHash) return null
        try {
          return await options.next({
            method: 'eth_getTransactionByHash',
            params: [submittedHash],
          })
        } catch {
          return null
        }
      })()
      await stopLeaseOnce()
      if (!submittedHash || !transaction) {
        await releaseSubmission(options.store, operationHash, submissionId)
        throw error
      }
      transactionHash = submittedHash
    }
  } finally {
    await stopLeaseOnce()
  }
  const success = await OperationStore.update(
    options.store,
    operationHash,
    (current) => {
      if (!current || current.type !== 'transaction')
        throw new OperationStore.InvalidStoreValueError()
      if (current.status === 'success') {
        if (
          current.transactionHash?.toLowerCase() !==
          transactionHash.toLowerCase()
        )
          throw new OperationStore.InvalidStoreValueError()
        return current
      }
      if (
        current.status !== 'submitting' ||
        current.submissionId !== submissionId
      )
        throw new OperationStore.InvalidStoreValueError()
      const { expiresAt: _, submissionId: __, ...operation } = current
      return MultisigOperation.from({
        ...operation,
        status: 'success',
        transactionHash,
        updatedAt: Date.now(),
      })
    },
  )
  if (success.type !== 'transaction' || success.status !== 'success')
    throw new OperationStore.InvalidStoreValueError()
  await removeSettledSubmission(options.store, operationHash, submissionId)
  if (
    options.method === 'eth_sendRawTransactionSync' &&
    result &&
    typeof result === 'object'
  )
    return { ...result, multisig: MultisigOperation.toRpc(success) }
  return await submittedResult(options, success)
}

declare namespace submit {
  /** Options for {@link submit}. */
  export type Options = {
    /** Client used to validate configs. */
    client: ReturnType<typeof createClient>
    /** RPC submission method. */
    method:
      | 'eth_sendRawTransaction'
      | 'eth_sendRawTransactionSync'
      | 'multisig_approveRawTransaction'
      | 'multisig_approveRawTransactionSync'
    /** Downstream RPC request handler. */
    next: handleRequest.Handler
    /** Original RPC request. */
    request: handleRequest.Request
    /** Original request overrides. */
    requestOptions?: handleRequest.RequestOptions | undefined
    /** Serialized Tempo transaction. */
    serialized: Hex.Hex
    /** Shared multisig store. */
    store: Store.Atomic
  }
}

/** Collects approvals for a multisig key authorization. @internal */
// biome-ignore lint/correctness/noUnusedVariables: declaration merge
async function approveKeyAuthorization(
  options: approveKeyAuthorization.Options,
) {
  const value = options.request.params?.[0]
  if (!value || typeof value !== 'object')
    throw new RpcResponse.InvalidParamsError({
      message: 'Expected a multisig key authorization approval.',
    })

  const approval = await (async () => {
    if ('keyAuthorization' in value) {
      const authorization = (() => {
        try {
          return KeyAuthorization.fromRpc(
            value.keyAuthorization as KeyAuthorization.Rpc,
          )
        } catch {
          throw new RpcResponse.InvalidParamsError({
            message: 'Invalid multisig key authorization.',
          })
        }
      })()
      const signature = authorization.signature
      if (signature?.type !== 'multisig')
        throw new RpcResponse.InvalidParamsError({
          message: 'Expected a multisig key authorization signature.',
        })
      if (
        !authorization.account ||
        !isAddressEqual(authorization.account, signature.account)
      )
        throw new RpcResponse.InvalidParamsError({
          message:
            'Multisig key authorization account does not match its signature.',
        })
      const { signature: _, ...unsigned } = authorization
      const keyAuthorization = KeyAuthorization.serialize(
        KeyAuthorization.from(unsigned),
      )
      const config = MultisigConfig.from(signature.config)
      const hash = MultisigOperation.getHash({
        account: signature.account,
        config,
        keyAuthorization,
        type: 'keyAuthorization',
      })
      return {
        account: signature.account,
        approvals: signature.signatures.map((signature) =>
          SignatureEnvelope.serialize(signature),
        ),
        config,
        hash,
        keyAuthorization,
      }
    }

    if ('hash' in value && 'signature' in value) {
      if (typeof value.hash !== 'string' || !Hash.validate(value.hash))
        throw new RpcResponse.InvalidParamsError({
          message: 'Expected a multisig operation hash.',
        })
      const operation = await OperationStore.read(options.store, value.hash)
      if (!operation || operation.type !== 'keyAuthorization')
        throw new RpcResponse.InvalidParamsError({
          message: 'Multisig key authorization operation was not found.',
        })
      if (operation.status === 'success') return { operation }
      const signature = (() => {
        try {
          return SignatureEnvelope.serialize(
            SignatureEnvelope.from(value.signature as Hex.Hex),
          )
        } catch {
          throw new RpcResponse.InvalidParamsError({
            message: 'Invalid multisig owner signature.',
          })
        }
      })()
      return {
        account: operation.account,
        approvals: [signature],
        config: operation.config,
        hash: operation.hash,
        keyAuthorization: operation.keyAuthorization,
      }
    }

    throw new RpcResponse.InvalidParamsError({
      message: 'Expected a multisig key authorization approval.',
    })
  })()
  if ('operation' in approval)
    return MultisigOperation.toRpc(approval.operation)
  if (approval.approvals.length === 0)
    throw new RpcResponse.InvalidParamsError({
      message: 'A multisig approval envelope must include a signature.',
    })

  const blockNumber = await getBlockNumber(options.client, { cacheTime: 0 })
  const commitment = await validateConfig({
    account: approval.account,
    blockNumber,
    client: options.client,
    config: approval.config,
  })
  const now = Date.now()
  const operation = await OperationStore.update(
    options.store,
    approval.hash,
    async (existing) => {
      if (existing && existing.type !== 'keyAuthorization')
        throw new OperationStore.InvalidStoreValueError()
      if (existing?.status === 'success') return existing
      if (
        existing &&
        (existing.account.toLowerCase() !== approval.account.toLowerCase() ||
          MultisigConfig.getCommitment(existing.config).toLowerCase() !==
            MultisigConfig.getCommitment(approval.config).toLowerCase() ||
          existing.keyAuthorization.toLowerCase() !==
            approval.keyAuthorization.toLowerCase())
      )
        throw new OperationStore.InvalidStoreValueError()
      const existingApprovals = existing
        ? await selectApprovals({
            account: approval.account,
            approvals: existing.approvals,
            config: approval.config,
            hash: approval.hash,
          })
        : undefined
      const approvals = await selectApprovals({
        account: approval.account,
        approvals: [
          ...(existingApprovals?.approvals ?? []),
          ...approval.approvals,
        ],
        config: approval.config,
        hash: approval.hash,
      })
      if (
        existing &&
        approvals.approvals.length === existing.approvals.length &&
        approvals.approvals.every(
          (approval, index) => approval === existing.approvals[index],
        )
      )
        return existing
      const pending = {
        account: approval.account,
        approvals: approvals.approvals,
        config: approval.config,
        createdAt: existing?.createdAt ?? now,
        hash: approval.hash,
        keyAuthorization: approval.keyAuthorization,
        signatureCount: approvals.signatureCount,
        threshold: approvals.threshold,
        type: 'keyAuthorization',
        updatedAt: now,
        weight: approvals.weight,
      } as const
      if (approvals.weight < approvals.threshold)
        return MultisigOperation.from({ ...pending, status: 'pending' })
      return MultisigOperation.from({
        ...pending,
        keyAuthorization: MultisigOperation.serializeKeyAuthorization(
          approval.keyAuthorization,
          {
            account: approval.account,
            approvals: approvals.selectedApprovals,
            config: approval.config,
          },
        ),
        status: 'success',
      })
    },
  )
  await ConfigStore.write(options.store, {
    address: approval.account,
    commitment,
    config: approval.config,
  })
  if (operation.type !== 'keyAuthorization')
    throw new OperationStore.InvalidStoreValueError()
  return MultisigOperation.toRpc(operation)
}

declare namespace approveKeyAuthorization {
  /** Options for {@link approveKeyAuthorization}. */
  export type Options = {
    /** Client used to validate configs. */
    client: ReturnType<typeof createClient>
    /** Original RPC request. */
    request: handleRequest.Request
    /** Shared multisig store. */
    store: Store.Atomic
  }
}

/** Deserializes a Tempo transaction or throws an RPC parameter error. */
function deserialize(serialized: Hex.Hex) {
  try {
    return Transaction.deserialize(
      serialized as Transaction.TransactionSerializedTempo,
    )
  } catch {
    throw new RpcResponse.InvalidParamsError({
      message: 'Invalid serialized Tempo transaction.',
    })
  }
}

/** Validates a config against an observed chain commitment. */
function assertConfig(options: {
  account: `0x${string}`
  commitment: Hex.Hex
  config: MultisigConfig.Config
}) {
  const { account, commitment, config } = options
  if (
    config.version === 0n &&
    MultisigConfig.getAddress(config, {
      factory: Addresses.nativeMultisigFactory,
    }).toLowerCase() !== account.toLowerCase()
  )
    throw new RpcResponse.InvalidParamsError({
      message: 'Initial multisig config does not match the multisig account.',
    })
  if (config.version === 0n && Hex.toBigInt(commitment) === 0n) return
  if (
    commitment.toLowerCase() !==
    MultisigConfig.getCommitment(config).toLowerCase()
  )
    throw new RpcResponse.InvalidParamsError({
      message: `Multisig config does not match account ${account}.`,
    })
}

/** Reads and validates a config commitment at one block. */
async function validateConfig(options: {
  account: `0x${string}`
  blockNumber: bigint
  client: ReturnType<typeof createClient>
  config: MultisigConfig.Config
}) {
  const { account, blockNumber, client, config } = options
  const commitment = await getConfigCommitment(client, {
    account,
    blockNumber,
  })
  assertConfig({ account, commitment, config })
  return commitment
}

/** Caches candidate configs from valid native update calls. */
// biome-ignore lint/correctness/noUnusedVariables: declaration merge
async function cacheNextConfigs(options: cacheNextConfigs.Options) {
  let currentConfig = options.config
  for (const call of options.transaction.calls) {
    if (
      !call.to ||
      !isAddressEqual(call.to, Addresses.nativeMultisig) ||
      !call.data
    )
      continue
    const decoded = (() => {
      try {
        return decodeFunctionData({ abi: Abis.nativeMultisig, data: call.data })
      } catch {
        return undefined
      }
    })()
    if (decoded?.functionName !== 'updateConfig') continue
    const [current, threshold, owners] = decoded.args
    const suppliedConfig = (() => {
      try {
        return MultisigConfig.from(current)
      } catch {
        return undefined
      }
    })()
    if (
      !suppliedConfig ||
      MultisigConfig.getCommitment(suppliedConfig).toLowerCase() !==
        MultisigConfig.getCommitment(currentConfig).toLowerCase()
    )
      continue
    const config = (() => {
      try {
        return MultisigConfig.from({
          owners,
          salt: suppliedConfig.salt,
          threshold,
          version: suppliedConfig.version + 1n,
        })
      } catch {
        return undefined
      }
    })()
    if (!config) continue
    await ConfigStore.write(options.store, {
      address: options.account,
      commitment: MultisigConfig.getCommitment(config),
      config,
    })
    currentConfig = config
  }
}

declare namespace cacheNextConfigs {
  /** Parameters for {@link cacheNextConfigs}. */
  export type Options = {
    /** Root multisig account. */
    account: Address
    /** Root config used to authorize the transaction. */
    config: MultisigConfig.Config
    /** Shared multisig store. */
    store: Store.Store
    /** Submitted transaction containing potential config updates. */
    transaction: Transaction.TransactionSerializableTempo
  }
}

/** Validates primitive approvals and translates invalid input into an RPC error. */
async function selectApprovals(
  options: MultisigOperation.selectApprovals.Options,
) {
  try {
    return await MultisigOperation.selectApprovals(options)
  } catch (error) {
    if (error instanceof MultisigOperation.InvalidApprovalError)
      throw new RpcResponse.InvalidParamsError({ message: error.shortMessage })
    throw error
  }
}

/** Returns an existing successful operation through the requested send method. */
async function submittedResult(
  options: submit.Options,
  operation: MultisigOperation.TransactionOperation,
) {
  if (
    options.method === 'eth_sendRawTransaction' ||
    options.method === 'multisig_approveRawTransaction'
  )
    return operation.hash
  if (options.method === 'multisig_approveRawTransactionSync')
    return MultisigOperation.toRpc(operation)
  const timeout = options.request.params?.[1]
  const deadline =
    Date.now() +
    (typeof timeout === 'number' && timeout >= 0 ? timeout : submissionTtl)
  while (true) {
    const receipt = await options.next(
      {
        method: 'eth_getTransactionReceipt',
        params: [operation.transactionHash],
      },
      options.requestOptions,
    )
    if (receipt && typeof receipt === 'object')
      return { ...receipt, multisig: MultisigOperation.toRpc(operation) }
    if (Date.now() >= deadline)
      throw new Error('Timed out while waiting for the multisig transaction.')
    await new Promise((resolve) => setTimeout(resolve, pollingInterval))
  }
}

/** Waits for the relay that owns a live submission lease. */
async function submittingResult(
  options: submit.Options,
  operation: MultisigOperation.TransactionOperation,
): Promise<unknown> {
  if (
    options.method === 'eth_sendRawTransaction' ||
    options.method === 'multisig_approveRawTransaction'
  )
    return operation.hash
  const timeout = options.request.params?.[1]
  const deadline =
    Date.now() +
    (typeof timeout === 'number' && timeout >= 0 ? timeout : submissionTtl)
  while (true) {
    const current = await OperationStore.read(options.store, operation.hash)
    if (!current || current.type !== 'transaction')
      throw new OperationStore.InvalidStoreValueError()
    if (current.status === 'success')
      return await submittedResult(options, current)
    if (current.status === 'pending' || current.expiresAt! <= Date.now())
      return await submit(options)
    if (Date.now() >= deadline) return pendingResult(options.method, current)
    await new Promise((resolve) => setTimeout(resolve, pollingInterval))
  }
}

/** Returns a pending result for an operation that has not reached quorum. */
function pendingResult(
  method: submit.Options['method'],
  operation: MultisigOperation.TransactionOperation,
) {
  if (
    method === 'eth_sendRawTransaction' ||
    method === 'multisig_approveRawTransaction'
  )
    return operation.hash
  if (method === 'multisig_approveRawTransactionSync')
    return MultisigOperation.toRpc(operation)
  return {
    blockHash: null,
    blockNumber: null,
    contractAddress: null,
    cumulativeGasUsed: null,
    effectiveGasPrice: null,
    from: operation.account,
    gasUsed: null,
    logs: [],
    logsBloom: null,
    multisig: MultisigOperation.toRpc(operation),
    status: 'pending',
    to: null,
    transactionHash: operation.hash,
    transactionIndex: null,
    type: '0x76',
  } as const
}

/** Marks a transaction as successful after a lookup proves that it was submitted. */
async function completeSubmission(
  store: Store.Atomic,
  operation: MultisigOperation.TransactionOperation,
  transactionHash: Hex.Hex,
) {
  if (operation.status !== 'submitting' || !operation.submissionId)
    throw new OperationStore.InvalidStoreValueError()
  const success = await OperationStore.update(
    store,
    operation.hash,
    (current) => {
      if (!current || current.type !== 'transaction')
        throw new OperationStore.InvalidStoreValueError()
      if (current.status === 'success') {
        if (
          current.transactionHash?.toLowerCase() !==
          transactionHash.toLowerCase()
        )
          throw new OperationStore.InvalidStoreValueError()
        return current
      }
      if (
        current.status !== 'submitting' ||
        current.submissionId !== operation.submissionId
      )
        throw new OperationStore.InvalidStoreValueError()
      const {
        expiresAt: _,
        submissionId: __,
        transactionHash: ___,
        ...value
      } = current
      return MultisigOperation.from({
        ...value,
        status: 'success',
        transactionHash,
        updatedAt: Date.now(),
      })
    },
  )
  await removeSettledSubmission(store, operation.hash, operation.submissionId)
  return success as MultisigOperation.TransactionOperation
}

/** Releases a failed submission lease without discarding collected approvals. */
async function releaseSubmission(
  store: Store.Atomic,
  hash: Hex.Hex,
  submissionId: Hex.Hex,
) {
  await OperationStore.update(store, hash, (current) => {
    if (!current || current.type !== 'transaction')
      throw new OperationStore.InvalidStoreValueError()
    if (
      current.status !== 'submitting' ||
      current.submissionId !== submissionId
    )
      return current
    const { expiresAt: _, submissionId: __, ...operation } = current
    return MultisigOperation.from({
      ...operation,
      status: 'pending',
      updatedAt: Date.now(),
    })
  })
  await OperationStore.removeSubmission(store, hash, submissionId)
}

/** Removes settled submission data without replacing a successful result with a cleanup error. */
async function removeSettledSubmission(
  store: Store.Store,
  hash: Hex.Hex,
  submissionId: Hex.Hex,
) {
  try {
    await OperationStore.removeSubmission(store, hash, submissionId)
  } catch {}
}

/** Preserves or upgrades a fee-payer envelope without removing an existing signature. */
function mergeTransaction(existing: Hex.Hex | undefined, incoming: Hex.Hex) {
  if (!existing) return incoming
  const existingTransaction = TxEnvelopeTempo.deserialize(existing as never)
  const incomingTransaction = TxEnvelopeTempo.deserialize(incoming as never)
  if (!('feePayerSignature' in incomingTransaction)) return existing
  if (incomingTransaction.feePayerSignature !== null) return incoming
  if (!('feePayerSignature' in existingTransaction)) return incoming
  return existing
}

/** Returns a synthetic transaction while the downstream transaction is unavailable. */
async function toTransaction(
  client: ReturnType<typeof createClient>,
  operation: MultisigOperation.TransactionOperation,
) {
  const approvals = await (async () => {
    if (operation.status !== 'pending')
      return await MultisigOperation.selectApprovals({
        account: operation.account,
        approvals: operation.approvals,
        config: operation.config,
        hash: operation.hash,
      })
    const blockNumber = await getBlockNumber(client, { cacheTime: 0 })
    await validateConfig({
      account: operation.account,
      blockNumber,
      client,
      config: operation.config,
    })
    return await selectApprovals({
      account: operation.account,
      approvals: operation.approvals,
      config: operation.config,
      hash: operation.hash,
    })
  })()
  const current =
    operation.status === 'pending'
      ? MultisigOperation.from({
          ...operation,
          approvals: approvals.approvals,
          signatureCount: approvals.signatureCount,
          threshold: approvals.threshold,
          weight: approvals.weight,
        })
      : operation
  const serialized = MultisigOperation.serializeTransaction(current, {
    approvals: approvals.selectedApprovals,
  })
  const transaction = deserialize(serialized)
  return {
    ...ox_Transaction.toRpc(
      {
        ...transaction,
        blockHash: null,
        blockNumber: null,
        from: operation.account,
        hash: operation.hash,
        transactionIndex: null,
      } as never,
      { pending: true },
    ),
    multisig: MultisigOperation.toRpc(current),
  }
}

/** Returns the persisted transaction hash for a submitting or successful operation. */
async function getSubmittedTransactionHash(
  store: Store.Store,
  operation: MultisigOperation.TransactionOperation,
) {
  if (operation.status === 'success') return operation.transactionHash!
  if (operation.status !== 'submitting' || !operation.submissionId)
    throw new OperationStore.InvalidStoreValueError()
  return await OperationStore.readSubmission(
    store,
    operation,
    operation.submissionId,
  )
}

/** Renews a submission lease until the returned cleanup function runs. */
function maintainSubmissionLease(
  store: Store.Atomic,
  hash: Hex.Hex,
  submissionId: Hex.Hex,
  leaseTtl: number,
) {
  let active = true
  let error: unknown
  let renewal = Promise.resolve()
  const timer = setInterval(() => {
    renewal = renewal.then(async () => {
      if (!active || error) return
      try {
        const operation = await OperationStore.update(
          store,
          hash,
          (current) => {
            if (!current || current.type !== 'transaction')
              throw new OperationStore.InvalidStoreValueError()
            if (
              current.status !== 'submitting' ||
              current.submissionId !== submissionId
            )
              return current
            return MultisigOperation.from({
              ...current,
              expiresAt: Math.min(
                Date.now() + leaseTtl,
                Number.MAX_SAFE_INTEGER,
              ),
              updatedAt: Date.now(),
            })
          },
        )
        if (
          operation.type !== 'transaction' ||
          operation.status !== 'submitting' ||
          operation.submissionId !== submissionId
        )
          throw new OperationStore.InvalidStoreValueError()
      } catch (cause) {
        error = cause
      }
    })
  }, submissionTtl / 2)

  return async () => {
    active = false
    clearInterval(timer)
    await renewal
    if (error) throw error
  }
}

/** Canonically serializes an unsigned Tempo transaction. */
function serializeUnsigned(
  envelope: Omit<TxEnvelopeTempo.TxEnvelopeTempo, 'signature'>,
  feePayer: boolean,
) {
  return TxEnvelopeTempo.serialize(
    envelope,
    feePayer
      ? envelope.from
        ? { format: 'feePayer', sender: envelope.from }
        : { format: 'feePayer' }
      : {},
  )
}

/** Extracts a transaction hash from an RPC submission result. */
function getTransactionHash(result: unknown): Hex.Hex {
  if (typeof result === 'string' && Hash.validate(result)) return result
  if (
    result &&
    typeof result === 'object' &&
    'transactionHash' in result &&
    typeof result.transactionHash === 'string' &&
    Hash.validate(result.transactionHash)
  )
    return result.transactionHash
  throw new Error('Expected transaction hash in multisig broadcast result.')
}

/** Resolves and validates the chain used by a handled request. */
async function resolveRequestOptions(options: {
  request: handleRequest.Request
  requestOptions?: handleRequest.RequestOptions | undefined
  store: Store.Store
}): Promise<handleRequest.RequestOptions | undefined> {
  const { request, requestOptions, store } = options
  const chainId_explicit = requestOptions?.chainId
  if (
    chainId_explicit !== undefined &&
    (!Number.isSafeInteger(chainId_explicit) || chainId_explicit <= 0)
  )
    throw new RpcResponse.InvalidParamsError({
      message: 'Expected a valid chain ID.',
    })
  const chainId_request = await resolveRequestChainId(request, store)
  if (
    chainId_explicit !== undefined &&
    chainId_request !== undefined &&
    chainId_explicit !== chainId_request
  )
    throw new RpcResponse.InvalidParamsError({
      message: 'Conflicting chain ids.',
    })
  const chainId = chainId_explicit ?? chainId_request
  if (chainId === undefined) return requestOptions
  return { ...requestOptions, chainId }
}

/** Resolves a chain from request fields, envelopes, or stored operations. */
async function resolveRequestChainId(
  request: handleRequest.Request,
  store: Store.Store,
) {
  const value = request.params?.[0]
  const chainId_body =
    value && typeof value === 'object' && 'chainId' in value
      ? parseChainId(value.chainId)
      : undefined
  const chainId_multisig = await (async () => {
    if (
      request.method === 'eth_sendRawTransaction' ||
      request.method === 'eth_sendRawTransactionSync' ||
      request.method === 'multisig_approveRawTransaction' ||
      request.method === 'multisig_approveRawTransactionSync'
    ) {
      if (!isSerializedTempoTransaction(value)) return undefined
      try {
        return parseChainId(Transaction.deserialize(value).chainId)
      } catch {
        return undefined
      }
    }

    if (
      request.method === 'multisig_approveKeyAuthorization' &&
      value &&
      typeof value === 'object' &&
      'keyAuthorization' in value
    ) {
      try {
        return parseChainId(
          KeyAuthorization.fromRpc(
            value.keyAuthorization as KeyAuthorization.Rpc,
          ).chainId,
        )
      } catch {
        return undefined
      }
    }

    const hash = (() => {
      if (
        request.method === 'eth_getTransactionByHash' ||
        request.method === 'eth_getTransactionReceipt'
      )
        return value
      if (
        request.method === 'multisig_approveKeyAuthorization' &&
        value &&
        typeof value === 'object' &&
        'hash' in value
      )
        return value.hash
      return undefined
    })()
    if (typeof hash !== 'string' || !Hash.validate(hash)) return undefined
    const operation = await OperationStore.read(store, hash)
    if (!operation) return undefined
    if (operation.type === 'transaction')
      return parseChainId(
        Transaction.deserialize(
          operation.transaction as Transaction.TransactionSerializedTempo,
        ).chainId,
      )
    return parseChainId(
      KeyAuthorization.deserialize(operation.keyAuthorization).chainId,
    )
  })()

  if (
    chainId_body !== undefined &&
    chainId_multisig !== undefined &&
    chainId_body !== chainId_multisig
  )
    throw new RpcResponse.InvalidParamsError({
      message: 'Conflicting chain ids.',
    })
  return chainId_body ?? chainId_multisig
}

/** Parses a supported chain ID representation. */
function parseChainId(value: unknown) {
  const chainId = (() => {
    if (typeof value === 'number') return value
    if (typeof value === 'bigint') return Number(value)
    if (typeof value === 'string' && Hex.validate(value)) {
      try {
        return Hex.toNumber(value)
      } catch {
        return undefined
      }
    }
    return undefined
  })()
  if (chainId === undefined || !Number.isSafeInteger(chainId) || chainId <= 0)
    return undefined
  return chainId
}

/** Checks whether a value is a serialized Tempo transaction. */
function isSerializedTempoTransaction(value: unknown): value is Hex.Hex {
  return (
    typeof value === 'string' &&
    (value.startsWith('0x76') || value.startsWith('0x78'))
  )
}
