Skip to content

Eip 7795 - #734

Merged
Dargon789 merged 5 commits into
0x-v2from
eip-7795
Aug 13, 2026
Merged

Eip 7795#734
Dargon789 merged 5 commits into
0x-v2from
eip-7795

Conversation

@Dargon789

@Dargon789 Dargon789 commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Summary by Sourcery

Add support for EIP-5792-style batched wallet calls with optional on/off-chain preconditions and propagate precondition-aware relay options throughout the account, wallet, relayer, and provider stack.

New Features:

  • Introduce EIP-5792 JSON-RPC methods (wallet_sendCalls, wallet_getCapabilities, and stubs for status methods) on the provider, including atomic batch execution across chains.
  • Add client and signer support for sending batched transactions via wallet_sendCalls, with optional preconditions passed from dapps to the wallet.
  • Define precondition types and encoding utilities in the relayer and API layers, plus a new satisfy RPC for resolving precondition solutions.

Enhancements:

  • Refactor Account, Wallet, and Relayer relay/sendTransaction APIs to accept a single options object for fee quotes, project access keys, preconditions, receipt waiting, and callbacks.
  • Update WalletUserPrompter interfaces to work with batched transactions and preconditions-aware prompts.
  • Streamline wallet deployment and bootstrap flows to use the updated relayer API and improve error handling.

Build:

  • Wire new @0xsequence/api and @0xsequence/relayer dependencies into account, provider, api, and relayer packages, including ethers as a peer/dev dependency where needed.

@bolt-new-by-stackblitz

Copy link
Copy Markdown

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@codesandbox

codesandbox Bot commented Aug 13, 2026

Copy link
Copy Markdown

Review or Edit in CodeSandbox

Open the branch in Web EditorVS CodeInsiders

Open Preview

@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
sequence.js Canceled Canceled Aug 13, 2026 3:53am
wagmi-project Canceled Canceled Aug 13, 2026 3:53am

@snyk-io

snyk-io Bot commented Aug 13, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@sourcery-ai

sourcery-ai Bot commented Aug 13, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements EIP-5792-style multi-call support and preconditions across provider, relayer, wallet, account, and API layers, refactors transaction sending to accept richer options, and wires new RPC and client surfaces for batched calls and capability discovery.

Sequence diagram for wallet_sendCalls multi-call with preconditions (EIP-5792)

sequenceDiagram
  actor User
  participant SequenceSigner
  participant SequenceClient
  participant SequenceProvider as Provider
  participant WalletRequestHandler as RequestHandler
  participant Account
  participant Relayer

  User->>SequenceSigner: sendTransaction(transaction[], { chainId, preconditions })
  SequenceSigner->>SequenceClient: sendTransaction(transaction[], { chainId, preconditions })
  SequenceClient->>SequenceClient: encodePrecondition(preconditions)
  SequenceClient->>SequenceProvider: request(wallet_sendCalls, params, chainId)
  SequenceProvider->>RequestHandler: handle(wallet_sendCalls, request)

  RequestHandler->>RequestHandler: validateTransactionRequest(account.address, call)
  alt prompter exists
    RequestHandler->>RequestHandler: prompter.promptSendTransaction([{ chainId, transactions }], { preconditions })
    RequestHandler-->>SequenceProvider: JSON.stringify(hashes)
  else no prompter
    RequestHandler->>Account: sendTransaction(transactionish, chainId, { preconditions })
    Account->>Relayer: relay(intendedBundle, { quote, preconditions, waitForReceipt })
    Relayer-->>Account: TransactionResponse
    Account-->>RequestHandler: metaTxn hash
    RequestHandler-->>SequenceProvider: JSON.stringify(hashes)
  end

  SequenceProvider-->>SequenceClient: JSON result
  SequenceClient->>SequenceClient: decode JSON
  SequenceClient-->>SequenceSigner: txnHash for chainId
  SequenceSigner-->>User: commons.transaction.TransactionResponse
Loading

File-Level Changes

Change Details Files
Add EIP-5792 wallet_sendCalls flow with validation, batching, optional preconditions, and capability discovery in the provider/request handler stack.
  • Introduce ethers.Interface mainModule for encoding selfExecute batch calls
  • Implement wallet_sendCalls JSON-RPC handler with parameter validation, per-call chain grouping, and atomic batch construction using selfExecute when multiple calls are present
  • Wire wallet_getCallsStatus, wallet_showCallsStatus placeholders, and wallet_getCapabilities to advertise atomicBatch and preconditions support per network
  • Update WalletUserPrompter promptSignTransaction/promptSendTransaction to accept arrays of chain-scoped transactions plus options including preconditions, and adjust eth_sendTransaction/eth_signTransaction paths to use the new signatures
packages/provider/src/transports/wallet-request-handler.ts
packages/network/src/json-rpc/middleware/signing-provider.ts
packages/provider/src/provider.ts
Extend client and signer APIs to send transactions via wallet_sendCalls and propagate preconditions from dapps to the wallet.
  • Refactor SequenceClient.sendTransaction to call wallet_sendCalls instead of eth_sendTransaction, encoding calls and optional preconditions per EIP-5792-style schema
  • Return chainId-keyed results from wallet_sendCalls and pick the current chain’s hash
  • Allow SequenceSigner.sendTransaction options to carry preconditions through to SequenceClient
packages/provider/src/client.ts
packages/provider/src/signer.ts
Introduce a typed Precondition model in the API and relayer layers, plus encoding helpers to translate to RPC wire types.
  • Add Precondition/Solution/Transactions/SolutionPrecondition types and new satisfy RPC method to api.gen.ts and its client wrapper
  • Create api/src/index.ts helpers to represent chain-aware Precondition, validate via isPrecondition, and encode to API Precondition with chainID serialization
  • Add relayer-side Precondition type union and encodePrecondition/isPrecondition for native, ERC20, ERC721, and ERC1155 balance/approval checks
packages/api/src/api.gen.ts
packages/api/src/index.ts
packages/relayer/src/precondition.ts
packages/api/package.json
packages/relayer/src/rpc-relayer/relayer.gen.ts
packages/relayer/src/index.ts
Refactor Account and Wallet transaction sending to accept richer options and route through relayer.relay with an options object, including fee quotes and preconditions, instead of positional arguments.
  • Change Account.doBootstrap to drop feeQuote and pass a single options object to relayer.relay
  • Make Account.sendSignedTransactions take transactions plus an options bag (quote, preconditions, waitForReceipt, status, callback, projectAccessKey) and forward this to relayer.relay
  • Update Account.sendTransaction to use options.status/skipPredecorate, collect bundles, and call sendSignedTransactions with the options object
  • Update Wallet.deploy, Wallet.sendSignedTransaction, and Wallet.sendTransaction to require a relayer, accept options (quote, preconditions, waitForReceipt, projectAccessKey), and pass them through to relayer.relay
  • Update AccountSigner to use the new Account.sendTransaction signature and options.nonceSpace
packages/account/src/account.ts
packages/wallet/src/wallet.ts
packages/account/src/signer.ts
packages/auth/src/session.ts
packages/account/package.json
Adapt relayer implementations to the new relay signature and options, and extend RpcRelayer to support projectAccessKey and waitForReceipt semantics via options.
  • Change Relayer interface relay signature to accept transactions plus options, including projectAccessKey/quote/preconditions/waitForReceipt
  • Update ProviderRelayer, RpcRelayer, and LocalRelayer implementations to match the new signature, logging the options.quote instead of positional quote and using options.waitForReceipt to decide whether to block for receipts
  • Adjust RpcRelayer to pass X-Access-Key from options.projectAccessKey when calling sendMetaTxn and to use transactions.intent fields when constructing responses
  • Update LocalRelayer to ignore fee quotes from options.quote and to encode bundle data from transactions
packages/relayer/src/index.ts
packages/relayer/src/provider-relayer.ts
packages/relayer/src/rpc-relayer/index.ts
packages/relayer/src/local-relayer.ts
Wire new dependencies for API and provider/account packages to support preconditions and API usage.
  • Add @0xsequence/api and @0xsequence/relayer as dependencies where needed and ethers 6 as a peer/dev dependency in the API package
  • Adjust imports across modules to use newly introduced Precondition and encoding utilities from @0xsequence/api and @0xsequence/relayer
packages/account/package.json
packages/provider/package.json
packages/api/package.json
pnpm-lock.yaml

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai 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.

Hey - I've found 7 issues, and left some high level feedback:

  • The new SequenceClient.sendTransaction expects wallet_sendCalls to return a JSON object keyed by chainId, but wallet_sendCalls currently returns a JSON stringified array of hashes, so indexing with [ethers.toQuantity(chainId)] will yield undefined; align the response shape or the client parsing.
  • In SequenceClient.sendTransaction, ethers.resolveAddress(to) is used without await, but it is asynchronous in ethers v6, so this will pass a Promise instead of a string into the call payload; resolve the address before constructing the calls array.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `SequenceClient.sendTransaction` expects `wallet_sendCalls` to return a JSON object keyed by chainId, but `wallet_sendCalls` currently returns a JSON stringified array of hashes, so indexing with `[ethers.toQuantity(chainId)]` will yield `undefined`; align the response shape or the client parsing.
- In `SequenceClient.sendTransaction`, `ethers.resolveAddress(to)` is used without `await`, but it is asynchronous in ethers v6, so this will pass a Promise instead of a string into the call payload; resolve the address before constructing the `calls` array.

## Individual Comments

### Comment 1
<location path="packages/provider/src/transports/wallet-request-handler.ts" line_range="610-611" />
<code_context>
+            throw new Error(`wallet_sendCalls capabilities '${JSON.stringify(capabilities)}' is invalid`)
+          }
+
+          let preconditions: Precondition[] | undefined
+          if (capabilities.preconditions !== undefined) {
+            if (!(capabilities.preconditions instanceof Array) || !capabilities.preconditions.every(isPrecondition)) {
+              throw new Error(`wallet_sendCalls preconditions '${JSON.stringify(capabilities.preconditions)}' is invalid`)
</code_context>
<issue_to_address>
**issue (bug_risk):** Accessing capabilities.preconditions without guarding for undefined will throw a runtime error.

If `capabilities` can be `undefined`, this condition will throw a `TypeError` before the intended validation runs. Please guard `capabilities` first, e.g.:

```ts
if (capabilities && capabilities.preconditions !== undefined) {
  ...
}
```
</issue_to_address>

### Comment 2
<location path="packages/provider/src/transports/wallet-request-handler.ts" line_range="618-620" />
<code_context>
+            preconditions = capabilities.preconditions
+          }
+
+          if (this.prompter) {
+            return JSON.stringify(
+              await this.prompter.promptSendTransaction(
+                calls.map((call: any) => ({ ...call, chainId: call.chainId !== undefined ? Number(call.chainId) : undefined })),
+                { ...request, preconditions }
</code_context>
<issue_to_address>
**issue (bug_risk):** The promptSendTransaction call does not match the updated prompter interface and the response shape used by the client.

`WalletUserPrompter.promptSendTransaction` now takes an array of `{ chainId?: number; transactions: commons.transaction.Transactionish }` plus an `options` object, and returns `string[]`. This code passes each `call` directly and uses `{ ...request, preconditions }` as options, which includes extra fields beyond `{ origin, projectAccessKey, preconditions }`. Also, `SequenceClient.sendTransaction` expects `wallet_sendCalls` to return a JSON object keyed by `chainId`, but both the prompter and non-prompter paths currently return an array. Please (1) wrap each call as `{ chainId: Number(call.chainId), transactions: call }`, (2) pass only the required options fields, and (3) update the return shape to the keyed-by-chainId object the client expects.
</issue_to_address>

### Comment 3
<location path="packages/provider/src/client.ts" line_range="509-510" />
<code_context>
+          {
+            version: '1.0',
+            from: this.getAddress(),
+            calls: transactions.map(({ to, value, data }) => ({
+              to: to ? ethers.resolveAddress(to) : undefined,
+              value: value !== undefined && value !== null ? ethers.toQuantity(value) : undefined,
+              data: data || undefined,
</code_context>
<issue_to_address>
**issue (bug_risk):** ethers.resolveAddress is async and should not be used directly in a synchronous mapping.

`ethers.resolveAddress` returns a Promise, so using it directly in the `map` makes `to` a Promise in the `calls` payload instead of a resolved address, causing the JSON-RPC request to fail. You should either resolve all `to` values before building `calls` (e.g. `await Promise.all(...)` and then map over the resolved addresses) or require `to` to already be a checksummed address and skip `resolveAddress` here.
</issue_to_address>

### Comment 4
<location path="packages/relayer/src/rpc-relayer/index.ts" line_range="216-219" />
<code_context>

-    const data = commons.transaction.encodeBundleExecData(signedTxs)
+    const data = commons.transaction.encodeBundleExecData(transactions)
     const metaTxn = await this.service.sendMetaTxn(
       {
-        call: {
-          walletAddress: signedTxs.intent.wallet,
-          contract: signedTxs.entrypoint,
-          input: data
-        },
+        call: { walletAddress: transactions.intent.wallet, contract: transactions.entrypoint, input: data },
         quote: typecheckedQuote
       },
-      { ...(projectAccessKey ? { 'X-Access-Key': projectAccessKey } : undefined) }
</code_context>
<issue_to_address>
**issue (bug_risk):** Relay options include preconditions, but they are not forwarded to the relayer RPC despite schema support.

The relayer schema (`SendMetaTxnArgs` in `relayer.gen.ts`) exposes `preconditions?: Array<Precondition>`, and `Relayer.relay` accepts `options?.preconditions`, but `RpcRelayer.relay` ignores them and never passes them to `sendMetaTxn`. If preconditions are meant to be enforced server-side, they should be serialized and included in the call (e.g. `preconditions: options?.preconditions?.map(encodePrecondition)`), otherwise clients will assume preconditions are applied when they are actually dropped.
</issue_to_address>

### Comment 5
<location path="packages/api/src/index.ts" line_range="53-55" />
<code_context>
+  )
+}
+
+export function encodePrecondition(precondition: Precondition): proto.Precondition {
+  const { type, precondition: args } = encodeChainPrecondition(precondition)
+  delete args.chainId
+  return { type, chainID: encodeBigNumberish(precondition.chainId), precondition: args }
+}
</code_context>
<issue_to_address>
**suggestion:** encodePrecondition mutates the object returned by encodeChainPrecondition and relies on deleting chainId in-place.

`encodePrecondition` deletes `chainId` from the object returned by `encodeChainPrecondition`, which may be shared elsewhere and lead to subtle side effects. Instead, clone and omit the field explicitly, e.g. `const { chainId, ...rest } = args`, then return `{ type, chainID: encodeBigNumberish(precondition.chainId), precondition: rest }` so the original object remains unchanged and the omission of `chainId` is explicit.

Suggested implementation:

```typescript
export function encodePrecondition(precondition: Precondition): proto.Precondition {
  const { type, precondition: args } = encodeChainPrecondition(precondition)
  const { chainId, ...rest } = args
  return { type, chainID: encodeBigNumberish(precondition.chainId), precondition: rest }
}

```

If `encodePrecondition` appears multiple times due to a bad merge (the snippet you shared shows some duplication), apply the same replacement to each occurrence so that none of them uses `delete args.chainId`. This ensures all callers benefit from the non-mutating behavior.
</issue_to_address>

### Comment 6
<location path="packages/provider/src/client.ts" line_range="493" />
<code_context>
   }

   async sendTransaction(
-    txs: commons.transaction.Transactionish,
+    transactions: commons.transaction.Transactionish,
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting the wallet_sendCalls request construction and response parsing into dedicated helper functions so sendTransaction remains a clear, high-level flow.

You can keep the new behavior while reducing complexity by extracting the protocol-specific request/response handling into small helpers. This keeps `sendTransaction` as a high-level helper and makes the response shape explicit.

For example:

```ts
type SendTransactionOptions = OptionalChainId & { preconditions?: Precondition[] }

function buildWalletSendCallsRequest(
  from: string,
  transactions: ethers.TransactionRequest[],
  chainId: number,
  options?: SendTransactionOptions
) {
  return {
    version: '1.0',
    from,
    calls: transactions.map(({ to, value, data }) => ({
      to: to ? ethers.resolveAddress(to) : undefined,
      value: value != null ? ethers.toQuantity(value) : undefined,
      data: data || undefined,
      chainId: ethers.toQuantity(chainId)
    })),
    capabilities: options?.preconditions
      ? { preconditions: options.preconditions.map(encodePrecondition) }
      : undefined
  }
}

function extractSendCallsHashForChain(rawResult: unknown, chainId: number): string {
  const parsed = typeof rawResult === 'string' ? JSON.parse(rawResult) : rawResult
  const key = ethers.toQuantity(chainId)
  return (parsed as Record<string, string>)[key]
}
```

Then `sendTransaction` becomes:

```ts
async sendTransaction(
  tx: ethers.TransactionRequest[] | ethers.TransactionRequest,
  options?: SendTransactionOptions
): Promise<string> {
  const transactions = Array.isArray(tx) ? tx : [tx]
  const chainId = options?.chainId ?? this.getChainId()

  this.analytics?.track({
    event: 'SEND_TRANSACTION_REQUEST',
    props: { chainId: chainId.toString() }
  })

  const rawResult = await this.request({
    method: 'wallet_sendCalls',
    params: [buildWalletSendCallsRequest(this.getAddress(), transactions, chainId, options)],
    chainId
  })

  return extractSendCallsHashForChain(rawResult, chainId)
}
```

This keeps all functionality (including `wallet_sendCalls` and preconditions), but makes `sendTransaction` read as: normalize inputs → track → build request → send → extract hash, and isolates the protocol-specific details in dedicated helpers.
</issue_to_address>

### Comment 7
<location path="packages/relayer/src/precondition.ts" line_range="26" />
<code_context>
+  )
+}
+
+export function encodePrecondition(precondition: Precondition): proto.Precondition {
+  const { type, precondition: args } = encodeChainPrecondition(precondition)
+  delete args.chainId
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting shared balance-like encoding logic into a dispatcher-based `encodePrecondition` to remove repetition and make future extensions simpler.

A small refactor can reduce repetition and make future additions easier without changing behavior.

You can factor out the repeated “copy + numeric encoding” logic and use a dispatcher map keyed by `type`. For example:

```ts
type AnyBalancePrecondition = {
  address: `0x${string}`
  token?: `0x${string}`
  tokenId?: ethers.BigNumberish
  min?: ethers.BigNumberish
  max?: ethers.BigNumberish
}

function encodeBalanceLikePrecondition<T extends AnyBalancePrecondition>(
  type: Precondition['type'],
  precondition: T
): proto.Precondition {
  return {
    type,
    precondition: {
      ...precondition,
      type: undefined,
      tokenId: encodeBigNumberish(precondition.tokenId),
      min: encodeBigNumberish(precondition.min),
      max: encodeBigNumberish(precondition.max),
    },
  }
}
```

Then the type‑specific encoders become very small:

```ts
const PRECONDITION_ENCODERS: Record<Precondition['type'], (p: any) => proto.Precondition> = {
  'native-balance': p => encodeBalanceLikePrecondition('native-balance', p),
  'erc20-balance':  p => encodeBalanceLikePrecondition('erc20-balance', p),
  'erc20-approval': p => ({
    type: p.type,
    precondition: { ...p, type: undefined, min: encodeBigNumberish(p.min) },
  }),
  'erc721-ownership': p => ({
    type: p.type,
    precondition: {
      ...p,
      type: undefined,
      tokenId: encodeBigNumberish(p.tokenId),
      owned: p.owned !== false,
    },
  }),
  'erc721-approval': p => ({
    type: p.type,
    precondition: { ...p, type: undefined, tokenId: encodeBigNumberish(p.tokenId) },
  }),
  'erc1155-balance': p => encodeBalanceLikePrecondition('erc1155-balance', p),
  'erc1155-approval': p => ({
    type: p.type,
    precondition: {
      ...p,
      type: undefined,
      tokenId: encodeBigNumberish(p.tokenId),
      min: encodeBigNumberish(p.min),
    },
  }),
}

export function encodePrecondition(precondition: Precondition): proto.Precondition {
  const encoder = PRECONDITION_ENCODERS[precondition.type]
  if (!encoder) throw new Error('unreachable')
  return encoder(precondition)
}
```

This keeps the special cases (`owned !== false`, required `min`, etc.) but removes the long `if/else` chain and repeated spread/encoding patterns.

Separately, if `isBigNumberish` / `encodeBigNumberish` already exist in `packages/api/src/index.ts`, consider re-exporting them from a shared module and importing here:

```ts
// common.ts
export { isBigNumberish, encodeBigNumberish } from 'packages/api/src/index'

// in this file
import { isBigNumberish, encodeBigNumberish } from '../common'
```

That centralizes BigNumberish handling and avoids future divergence between copies.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread packages/provider/src/transports/wallet-request-handler.ts
Comment thread packages/provider/src/transports/wallet-request-handler.ts
Comment thread packages/provider/src/client.ts
Comment thread packages/relayer/src/rpc-relayer/index.ts
Comment thread packages/api/src/index.ts
Comment thread packages/provider/src/client.ts
Comment thread packages/relayer/src/precondition.ts
@Dargon789
Dargon789 merged commit f6d7a3d into 0x-v2 Aug 13, 2026
15 of 19 checks passed
@Dargon789
Dargon789 deleted the eip-7795 branch August 13, 2026 07:53
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.

2 participants