Skip to content

feat(tempo): multisig - #5029

Merged
jxom merged 77 commits into
mainfrom
jxom/multisig-store
Sep 1, 2026
Merged

jxom merged 77 commits into
mainfrom
jxom/multisig-store

Conversation

@jxom

@jxom jxom commented Aug 20, 2026

Copy link
Copy Markdown
Member

Adds persistent multisig operation storage and explicit approval actions to Tempo clients. This keeps partial quorum coordination out of standard transaction sending while preserving direct sends for complete multisig envelopes.

Supersedes #5026.

@changeset-bot

changeset-bot Bot commented Aug 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 7292473

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
viem Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Aug 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
viem Ready Ready Preview Sep 1, 2026 1:45am UTC

Request Review

@jxom
jxom marked this pull request as ready for review August 20, 2026 01:33
@pkg-pr-new

pkg-pr-new Bot commented Aug 20, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/viem@5029

commit: 9f1fda6

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a485f97064

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/tempo/Multisig.ts Outdated
Comment on lines +174 to +178
const result = await options.next({
method:
options.method === 'eth_approveMultisigTransaction'
? 'eth_sendRawTransaction'
: 'eth_sendRawTransactionSync',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reconcile broadcasts whose response is lost

If the downstream node accepts the transaction but the response is lost through a timeout or disconnect, this await throws before the operation is marked successful. The store therefore continues reporting pending even though the transaction may have executed, and later approvals rebroadcast the same envelope and can fail with an already-known or consumed-nonce error. Derive and persist the transaction identity before broadcasting, or reconcile ambiguous failures before leaving the operation pending.

Useful? React with 👍 / 👎.

Comment thread src/tempo/Client.ts Outdated
Comment on lines +200 to +203
request: Multisig.handleRequest(
(request) => value.request(request as never),
parameters,
) as typeof value.request,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve request options through the transport wrapper

For every client configured with multisig, the replacement request function forwards only the request object and discards the optional second argument accepted by EIP1193RequestFn. Consequently abort signals and per-request retryCount or dedupe overrides are ignored even for unrelated RPC methods; for example, cancelling client.request(..., { signal }) no longer reaches the underlying transport. Accept and forward the request options alongside the request.

Useful? React with 👍 / 👎.

Comment thread src/tempo/multisig/Operation.ts Outdated
Comment on lines +181 to +184
export function deserialize(value: string): Operation {
if (value.length > maxStoredValueLength) throw new InvalidStoreValueError()
const operation = Json.parse(value) as Operation
const config = MultisigConfig.from(operation.config)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject unsupported persisted schema versions

When a custom or persistent store contains an operation with a missing or non-1 schemaVersion, this cast accepts it and proceeds to interpret it as the current layout. That defeats the version discriminator and can return or update an incompatible operation instead of raising InvalidStoreValueError; validate schemaVersion before constructing the configuration or transaction.

Useful? React with 👍 / 👎.

Comment thread src/tempo/index.ts
export * as Formatters from './Formatters.js'
export * as Hardfork from './Hardfork.js'
export * as KeyAuthorizationManager from './KeyAuthorizationManager.js'
export * as Multisig from './Multisig.js'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Document the new public Multisig entrypoint

This adds the public Multisig namespace without the required entrypoint TSDoc block, leaving the namespace undocumented in generated API and editor surfaces. Add a concise TSDoc description immediately above the export as required for new public modules.

AGENTS.md reference: AGENTS.md:L89-L93

Useful? React with 👍 / 👎.

Comment thread src/tempo/actions/multisig.ts Outdated
Comment on lines +326 to +327
if (value === null) return null
return transactionOperation(value)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Return key-authorization operations from getOperation

When the configured store or remote endpoint returns a key-authorization operation, this routes it through transactionOperation, which deliberately throws whenever keyAuthorization is present. As a result, getOperation only returns this advertised Operation.Operation variant when callers bypass the RPC path by passing store explicitly; deserialize the generic operation here instead.

Useful? React with 👍 / 👎.

Comment thread src/tempo/Decorator.ts Outdated
Comment on lines +2374 to +2376
approveTransaction: (
parameters: multisigActions.approveTransaction.Parameters<chain, account>,
) => Promise<multisigActions.approveTransaction.ReturnValue>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve request generics on decorated approval methods

These decorated methods instantiate Parameters with its broad default chainOverride and request types instead of declaring method-level generics, unlike the standalone functions and wallet decorators. Calls through client.multisig therefore lose literal request inference and compile-time chain-override checking, so incompatible builder output can type-check and fail only at runtime; make both approval methods generic over the request and chain override.

AGENTS.md reference: AGENTS.md:L68-L72

Useful? React with 👍 / 👎.

@gakonst

gakonst commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

How do you handle transaction hashes not landed on chain yet? Ie what does eth get tx receipt return of anything?

@gakonst

gakonst commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Also should nail the DX / UX of going through the 1 of 1 flow - since that will be very common by default as well as going from 1 of 1 to 1 of 2 when upgrading an agent account to a passkey backed or otherwise account (maybe using prf)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f547f1f990

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/tempo/actions/multisig.ts Outdated
* @param parameters - Transaction request, multisig identity, and owner account.
* @returns The pending or successful multisig operation and prepared request.
*/
export async function approveTransaction<

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add site docs for the new multisig actions

The new approveTransaction, approveTransactionSync, and getOperation public actions have no corresponding pages under site/pages/tempo/actions, entries in the actions index, or sidebar wiring in site/vocs.config.ts. Users browsing the documented Tempo action surface therefore cannot discover or reference these APIs; add the action pages and wire them into the index and sidebar.

AGENTS.md reference: AGENTS.md:L103-L104

Useful? React with 👍 / 👎.

Comment thread src/tempo/Transaction.ts Outdated
keyAuthorization?: KeyAuthorization.Signed<quantity, index> | undefined
multisig?: Address | MultisigConfig.Config<index> | undefined
/** Bootstrap multisig config hint for node-side gas modeling (TIP-1061). Attached automatically when `multisig` is present; the node ignores it for registered senders. */
multisigInit?:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore TSDoc for multisig request fields

This change removes the only descriptions of multisigInit, multisigOwnerStates, multisigSignatureCount, and multisigVersion while leaving them as public properties of TransactionRequestTempo. Generated API/editor documentation will now expose these non-obvious protocol fields without explaining their semantics, so retain TSDoc on each property.

AGENTS.md reference: AGENTS.md:L117-L118

Useful? React with 👍 / 👎.

Comment thread src/tempo/Multisig.ts Outdated
Comment on lines +319 to +323
signatures: nested.flatMap((signature) =>
signature.signatures.map((approval) =>
SignatureEnvelope.serialize(approval),
),
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Discard stale nested approvals after owner rotation

When a nested multisig owner's configuration changes while a parent operation is pending, this flattens every previously stored child signature and verifies it using the child's newly fetched version. The old signatures were made for the prior version and fail verification, and because they remain in existing.approvals, submitting a new valid child approval fails on every retry, permanently wedging that parent operation unless the store is manually altered. Track the child version or replace/reset stale nested approvals when the nested configuration changes.

Useful? React with 👍 / 👎.

Comment thread src/tempo/Client.ts Outdated
Comment on lines +200 to +203
request: Multisig.handleRequest(
(request) => value.request(request as never),
parameters,
) as typeof value.request,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve raw transport response envelopes

When the supplied transport uses http(..., { raw: true }), value.request returns { result, error } envelopes, but the multisig handler treats those envelopes as decoded RPC values and returns plain strings or objects for intercepted methods. Consequently approval submissions fail while decoding getConfig, and direct eth_getMultisigOperation requests violate the raw transport's declared response shape. The wrapper must unwrap downstream raw responses for its internal actions and rewrap intercepted results, or reject raw transports explicitly.

Useful? React with 👍 / 👎.

Comment thread src/tempo/e2e.multisig.test.ts Outdated
Comment on lines +2128 to +2130
serializedTransactions.push(serialized)
if (serializedTransactions.length === 2) release?.()
await thresholdCrossed

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid requiring duplicate broadcasts in the concurrency test

This test releases thresholdCrossed only after recording two eth_sendRawTransactionSync calls, so an implementation that correctly elects a single broadcaster will hang forever rather than demonstrate correct observable behavior. It also asserts the internal request count and payload instead of the resulting operation and chain state; restructure the synchronization around observable outcomes without requiring duplicate downstream calls.

AGENTS.md reference: AGENTS.md:L187-L192

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7f04b9d8d4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/tempo/Multisig.ts Outdated
async (existing) => {
if (existing?.keyAuthorization) throw new Store.InvalidStoreValueError()
if (existing?.status === 'success') return existing
if (existing && existing.weight >= existing.threshold) return existing

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Accept corrected envelopes after quorum

When a sponsored transaction reaches quorum but the downstream broadcast rejects its fee-payer signature, resubmitting an approval with a corrected feePayerSignature cannot recover the operation. The fee-payer signature is outside the sender approval digest, so the corrected envelope resolves to the same operation ID, but this early return retains the previously stored transaction and rebroadcasts its stale sponsorship data on every retry. Allow a quorum-complete pending operation to replace non-sender envelope data after validating that its approval payload is unchanged.

Useful? React with 👍 / 👎.

Comment thread src/tempo/actions/multisig.ts Outdated
Comment on lines +594 to +597
if (typeof account === 'object' && account.source === 'multisig')
return account as TempoAccount.MultisigAccount
if (parameters.multisig)
return TempoAccount.fromMultisig(parameters.multisig)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Honor explicit parent identities for nested owners

When a local multisig account is approving a different parent multisig supplied as an address or raw config, this branch selects the signing child as multisigAccount before considering the explicit parameters.multisig. The subsequent overrides therefore prepare and submit the transaction from the child account rather than the requested parent, potentially executing the calls against the wrong account; explicit multisig identities should take precedence regardless of whether they are addresses, configs, or MultisigAccount objects.

Useful? React with 👍 / 👎.

Comment thread src/tempo/actions/multisig.ts Outdated
Comment on lines +613 to +616
const signature = await signTransaction(client, {
...request,
account,
} as never)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject or correctly sign with JSON-RPC owner accounts

When account is an address, which the public parameter type explicitly permits, this calls the generic signTransaction JSON-RPC path. That path invokes eth_signTransaction and returns a complete transaction envelope from the address, not the primitive signature over the multisig approval digest expected by serializeApproval; the envelope is then treated as an owner signature and approval construction fails. Either restrict approval owners to local accounts or implement a JSON-RPC signing flow that signs the derived multisig digest.

Useful? React with 👍 / 👎.

Comment thread .changeset/calm-mailboxes-coordinate.md Outdated

`viem/tempo`: Added persistent multisig operation coordination and explicit owner approval actions.

```ts

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep the changeset entry to one sentence

The changeset continues after its past-tense summary with a fenced usage example, even though repository changeset entries must consist of a single sentence. Remove the example and retain only the summary sentence.

AGENTS.md reference: AGENTS.md:L224-L227

Useful? React with 👍 / 👎.

@jxom jxom changed the title feat(tempo): add multisig approval store feat(tempo): up multisig Aug 20, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a250139c3d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +289 to +292
const address = (() => {
if (account) return account.address
if (typeof accountValue === 'string') return accountValue as Address
return undefined

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve explicit JSON-RPC account overrides

When account is supplied as a JsonRpcAccount object with currentConfig, this resolver ignores its address, and the destructured override is never restored in the call below. The action consequently uses client.account instead, so clients with a different default account either submit the config update from the wrong sender or fail during multisig preparation. Preserve non-multisig account objects as the explicit override rather than dropping them.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 339187fccf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/tempo/Account.ts
Comment on lines +392 to +403
if (request.owner) {
const owner = parseAccount(request.owner)
if (owner.type !== 'local')
throw new Error(
'A local owner account is required to approve a multisig transaction.',
)
if (owner.source !== 'root' && owner.source !== 'multisig')
throw new Error(
'A Tempo owner account is required to approve a multisig transaction.',
)
const { owner: _, ...ownerRequest } = request
return await owner.signTransaction(ownerRequest as never, options)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject owner signing without multisig simulation

When the public signTransaction action is called with a multisig account and the new owner option, it does not run the chain's preparation hook, so multisigSimulation is normally absent. This branch nevertheless delegates the request to the owner, which produces an ordinary Tempo transaction whose recovered sender is the owner rather than the requested multisig; broadcasting it can execute the calls with the wrong account's authority and balance. Require a prepared multisig simulation context here instead of falling back to ordinary owner signing.

Useful? React with 👍 / 👎.

Comment thread src/tempo/Account.ts
Comment on lines 428 to 431
return SignatureEnvelope.serialize(
await signMultisig(account, {
payload: parentDigest,
states: request.multisigOwnerStates,
version: state?.version,
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow partial nested approvals to reach the coordinator

When a nested multisig owner is itself distributed and the local participant holds less than the child's threshold, this call reaches signMultisig, which throws Local multisig owners do not meet the threshold instead of returning the partial nested envelope. Consequently the coordinator never receives either child's partial approval and cannot merge them into the nested branch, despite the documented coordinated flow in site/pages/tempo/guides/multisig/nested-accounts.mdx:87-89; support emitting partial nested approvals when signing a parent operation.

Useful? React with 👍 / 👎.

})
```

## How it works

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Capitalize the withMultisig heading

Rename this heading to ## How It Works; the new page currently uses sentence case even though all documentation headings must use Title Case.

AGENTS.md reference: AGENTS.md:L248-L258

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e7cd17bbe9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/tempo/Multisig.ts
Comment on lines +263 to +268
const configCommitment = await validateConfig({
account: signature.account,
blockNumber,
client: options.client,
config,
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Return stored successes before revalidating rotated configs

When the successful operation updates its own multisig config, or the config is rotated afterward, retrying the operation after a lost client response revalidates the old signed config against the new onchain commitment here and throws before OperationStore.update can return its existing success record. This makes an already-completed operation non-idempotent, including retries made with its documented operation hash; compute the hash and return a matching stored success before applying current-config validation.

Useful? React with 👍 / 👎.

Comment thread src/tempo/Multisig.ts
Comment on lines +464 to +468
const submittedHash = await OperationStore.readSubmission(
options.store,
claim,
submissionId,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reconcile broadcasts using the transformed transaction hash

When the downstream relay transforms the envelope before broadcasting, such as by adding sponsorship, the normal path explicitly accepts a returned hash different from the locally derived hash at lines 460-462. If that broadcast succeeds but its response is lost, this recovery path queries only the hash of the persisted pre-transform envelope, cannot find the mined transaction, releases the lease, and may repeatedly rebroadcast or leave the operation pending. Recovery needs a relay lookup or persisted identifier that resolves the actual transformed transaction rather than assuming the pre-broadcast hash.

Useful? React with 👍 / 👎.

Comment thread src/tempo/Account.ts
Comment on lines +402 to +403
const { owner: _, ...ownerRequest } = request
return await owner.signTransaction(ownerRequest as never, options)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Strip prior approvals before signing the next owner

When a transaction request supplies both owner and existing signatures, this delegation forwards those signatures into the owner's serializer. The owner therefore hashes an envelope that already contains the earlier multisig approvals, while transactionEnvelope later appends the new approval to the original unsigned operation; the coordinator verifies that approval against the unsigned payload and rejects it. Remove signatures from the request used for owner signing, while retaining them for final envelope assembly.

Useful? React with 👍 / 👎.

const resolvedAccount = (() => {
if (account) return { ...account, config: currentConfig }
if (typeof accountValue === 'object') return accountValue
if (address) return fromMultisig({ address, ...currentConfig })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Rebuild cached version-zero accounts with inferred addresses

When updateConfig receives an address account and resolves its cached initial config, that config has version 0, so this call to fromMultisig({ address, ...currentConfig }) throws A current multisig config must have a version. before the supplied owner can coordinate the account's first rotation. This contradicts the action's cached-config inference path specifically for initial accounts; construct version-zero configs with address: 'infer' and verify the derived address matches, as the coordinated key-authorization path already does.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a22eeba61a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/tempo/Transaction.ts
quantity = bigint,
index = number,
status = 'success' | 'reverted',
status = 'success' | 'reverted' | 'pending',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Model nullable fields on pending receipts

When a distributed multisig has not reached quorum, pendingResult returns null for blockHash, blockNumber, cumulativeGasUsed, effectiveGasPrice, gasUsed, logsBloom, and transactionIndex, but adding 'pending' only to this status parameter leaves those fields non-nullable through viem_TransactionReceipt. TypeScript callers can therefore perform bigint or hash operations on values that are actually null; model pending receipts as a discriminated variant with nullable inclusion fields and add a type test for the narrowing.

AGENTS.md reference: AGENTS.md:L75-L76

Useful? React with 👍 / 👎.

These recipes assume you have [set up a Tempo client](/tempo).

### Combine Owner Key Types
### Approve from Different Key Types

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore the Recipes prerequisite

This guide now moves directly from ## Recipes to the first recipe, removing the required prerequisite line that links readers to Tempo client setup. Restore that line before this heading so the recipe's imported client has the required setup context.

AGENTS.md reference: AGENTS.md:L352-L354

Useful? React with 👍 / 👎.

Comment thread src/tempo/Multisig.ts
Comment on lines +290 to +292
if (existing?.status === 'submitting' && existing.expiresAt! > now)
return existing
const existingApprovals = existing

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove expired submission envelopes before replacing leases

When a coordinator exits after writeSubmission but before broadcasting or cleanup, the lease eventually expires and this path replaces the submitting operation with a new pending operation without removing the artifact keyed by the old submissionId. That identifier is then lost, so every such recovery can permanently leak another final envelope of up to 1 MiB into the shared store; retain the expired ID long enough to remove its submission key before starting a new attempt.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 202a332c04

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/tempo/Account.ts Outdated
Comment on lines +348 to +349
if (value.address !== 'infer' && config!.version === 0n)
throw new Error('A current multisig config must have a version.')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve address-less initial multisig configs

Existing calls such as Account.fromMultisig({ owners: [...] }) now produce a version-zero config with value.address === undefined, enter this branch, and throw A current multisig config must have a version. at runtime; typed consumers also stop compiling because the public Config shape was removed. Since the commit publishes a patch changeset, continue treating an omitted address as inferred, or release and document this as a breaking migration.

AGENTS.md reference: AGENTS.md:L224-L227

Useful? React with 👍 / 👎.

Comment thread src/tempo/index.ts
/** @experimental */
export * as Selectors from './Selectors.js'
export * as Storage from './Storage.js'
export * as Store from './Store.js'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Retain the public Storage namespace as an alias

Replacing this export removes the previously public Storage namespace and deletes its module, so existing import { Storage } from 'viem/tempo' consumers fail to compile after a patch upgrade even though Store exposes essentially the renamed API. Keep a deprecated Storage alias, including defaultStorage, or publish this as a documented breaking migration.

AGENTS.md reference: AGENTS.md:L224-L227

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e2f8c9ec4c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/tempo/chainConfig.ts
Comment on lines +238 to +239
request.nonceKey = Hex.toBigInt(Hex.random(31)) + 1n
request.nonce = 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve an explicitly supplied nonce

When a coordinated multisig caller supplies nonce but leaves nonceKey at its normal default, this branch silently replaces both the default nonce lane and the caller's nonce with a random lane at nonce zero. The resulting operation can execute outside the caller's intended sequence; only allocate the random lane when neither nonce field was explicitly provided.

Useful? React with 👍 / 👎.

Comment thread src/tempo/Client.test.ts
createTransport({
key: 'recording',
name: 'Recording',
request: async () => 'tempo' as never,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Replace the synthetic transport with a real endpoint

This newly added request-options test uses a hand-built transport stub whose request function returns a fixed value, rather than exercising behavior against the configured test chain or a real ephemeral server. Rework the test around a real endpoint so it complies with the repository's explicit prohibition on mocks, stubs, and fake clients.

AGENTS.md reference: AGENTS.md:L146-L149

Useful? React with 👍 / 👎.

Comment thread src/tempo/Transaction.ts Outdated
multisigVersion?: bigint | undefined
multisigSimulation?: MultisigSimulation.Spec | undefined
nonceKey?: 'expiring' | quantity | undefined
owner?: Account | MultisigAccount | Address | undefined

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restrict transaction owners to supported local accounts

The public transaction type permits an address or any Tempo Account as owner, but request preparation unconditionally rejects string owners and account sources other than root or multisig. Calls such as sendTransaction({ account: multisig, owner: owner.address, ... }) therefore compile and then fail before signing; narrow both request and serializable owner fields to RootAccount | MultisigAccount.

AGENTS.md reference: AGENTS.md:L75-L76

Useful? React with 👍 / 👎.

Comment thread src/tempo/Multisig.ts
createdAt: existing?.createdAt ?? now,
hash: operationHash,
signatureCount: approvals.signatureCount,
status: 'pending',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bound persistent pending operations

When this handler is exposed to untrusted RPC callers, an attacker can repeatedly submit one valid approval for unique version-zero 2-of-2 accounts and transactions; each request passes initial-config validation and is permanently persisted with pending status here. Operation keys have no TTL, quota, or removal path (only temporary submission artifacts are removed), so the shared coordinator store can be exhausted without any transaction reaching quorum; enforce retention limits or expire incomplete operations.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9f1fda6fbc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +247 to +248
if ((receipt as TransactionReceipt).status === 'pending')
return { receipt } as never

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Model pending sync results as a discriminated union

When a coordinated multisig has not reached quorum, this branch returns only { receipt }, but authorizeSync.ReturnValue still requires every KeyAuthorized event field. TypeScript therefore lets callers read fields such as account that are actually undefined. The same unchecked pending branch was added across the other *Sync action wrappers, so their public return types should include a pending variant discriminated by receipt.status, with type tests covering both outcomes.

AGENTS.md reference: AGENTS.md:L75-L76

Useful? React with 👍 / 👎.

Comment thread src/tempo/Account.ts
Comment on lines +381 to +384
async sign({ hash }) {
return SignatureEnvelope.serialize(
await signMultisig(account, { payload: hash }),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Document the newly supported multisig sign method

This implementation makes MultisigAccount.sign functional when the account has a sufficient local owner quorum, but the updated site/pages/tempo/accounts/account.fromMultisig.mdx:147-149 still explicitly says that sign is unsupported. Users relying on that public account reference will miss the signing capability used by nested multisig and coordinated authorization flows, so update the page and the source TSDoc to describe its quorum/config requirements.

AGENTS.md reference: AGENTS.md:L116-L116

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9f1fda6fbc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +247 to +248
if ((receipt as TransactionReceipt).status === 'pending')
return { receipt } as never

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Model pending sync results as a union

When a distributed multisig has not reached quorum, this branch returns only { receipt }, but authorizeSync.ReturnValue still requires every KeyAuthorized event field. Typed callers can therefore use fields such as the authorized account as defined values even though they are absent at runtime. Return a discriminated pending variant instead of hiding the different shape behind as never; the same pattern should be corrected in the other modified sync actions.

AGENTS.md reference: AGENTS.md:L74-L76

Useful? React with 👍 / 👎.

@jxom
jxom merged commit dd7d792 into main Sep 1, 2026
33 of 35 checks passed
@jxom
jxom deleted the jxom/multisig-store branch September 1, 2026 01:42
@github-actions github-actions Bot mentioned this pull request Sep 1, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 729247317a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/tempo/Multisig.ts
Comment on lines +915 to +916
const key = signature.account.toLowerCase()
configs.set(key, { account: signature.account, config })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate every config used by repeated nested accounts

When separate branches of a nested approval tree reuse the same multisig account but carry different config versions, this address-only map lets the later branch overwrite the earlier config before onchain validation. MultisigOperation.selectApprovals verifies each branch against its supplied config, so a stale branch can still contribute weight while only the other branch's current config is checked, producing an invalid transaction or a falsely successful key-authorization operation. Key and validate entries by both account and config commitment, or reject conflicting configs for the same account.

Useful? React with 👍 / 👎.

[`Store.memory`](/tempo/utilities/Store.memory) for process-local data or
[`Store.from`](/tempo/utilities/Store.from) to wrap another store.

## Contract

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Structure the Store page as a module landing page

This new Store module landing page jumps directly into ## Contract and later ## See Also, omitting the required ## Overview, minimal usage example, and Cards grid linking its from, memory, session, and defaultStore sub-pages. Rework the page into the prescribed concept-first landing-page structure.

AGENTS.md reference: AGENTS.md:L267-L275

Useful? React with 👍 / 👎.

Comment thread src/tempo/chainConfig.ts
Comment on lines +420 to +422
const localOwner = local?.owners.find((account) =>
isAddressEqual(account.address, owner.owner),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include the explicit nested owner in gas simulation

When a coordinated request uses an address-only parent account and passes a MultisigAccount through owner, the nested account is not present in local.owners, so this lookup models it as one primitive WebAuthn approval instead of a nested quorum. Both initial submissions and operation-hash continuations can therefore receive a gas limit based on a much smaller envelope than the one ultimately serialized, causing an otherwise valid nested approval to run out of gas; include the explicit owner when resolving simulation accounts.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants