@totalreclaw/totalreclaw 3.3.12-rc.2 → 3.3.12-rc.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/SKILL.md +34 -249
- package/api-client.ts +17 -9
- package/batch-gate.ts +42 -0
- package/billing-cache.ts +25 -20
- package/claims-helper.ts +7 -1
- package/config.ts +54 -12
- package/consolidation.ts +6 -13
- package/contradiction-sync.ts +19 -14
- package/credential-provider.ts +184 -0
- package/crypto.ts +27 -160
- package/dist/api-client.js +17 -9
- package/dist/batch-gate.js +40 -0
- package/dist/billing-cache.js +19 -21
- package/dist/claims-helper.js +7 -1
- package/dist/config.js +54 -12
- package/dist/consolidation.js +6 -15
- package/dist/contradiction-sync.js +15 -15
- package/dist/credential-provider.js +145 -0
- package/dist/crypto.js +17 -137
- package/dist/download-ux.js +11 -7
- package/dist/embedder-loader.js +266 -0
- package/dist/embedding.js +36 -3
- package/dist/entry.js +123 -0
- package/dist/extractor.js +134 -0
- package/dist/fs-helpers.js +116 -241
- package/dist/import-adapters/chatgpt-adapter.js +14 -0
- package/dist/import-adapters/claude-adapter.js +14 -0
- package/dist/import-adapters/gemini-adapter.js +43 -159
- package/dist/import-adapters/mcp-memory-adapter.js +14 -0
- package/dist/import-state-manager.js +100 -0
- package/dist/index.js +1130 -2520
- package/dist/llm-client.js +69 -1
- package/dist/memory-runtime.js +459 -0
- package/dist/native-memory.js +123 -0
- package/dist/onboarding-cli.js +3 -2
- package/dist/pair-cli.js +1 -1
- package/dist/pair-crypto.js +16 -358
- package/dist/pair-http.js +147 -4
- package/dist/relay.js +140 -0
- package/dist/reranker.js +13 -8
- package/dist/semantic-dedup.js +5 -7
- package/dist/skill-register.js +97 -0
- package/dist/subgraph-search.js +3 -1
- package/dist/subgraph-store.js +315 -282
- package/dist/tool-gating.js +39 -26
- package/dist/tools.js +367 -0
- package/dist/tr-cli-export-helper.js +103 -0
- package/dist/tr-cli.js +220 -127
- package/dist/trajectory-poller.js +155 -9
- package/dist/vault-crypto.js +551 -0
- package/download-ux.ts +12 -6
- package/embedder-loader.ts +293 -1
- package/embedding.ts +43 -3
- package/entry.ts +132 -0
- package/extractor.ts +167 -0
- package/fs-helpers.ts +166 -292
- package/import-adapters/chatgpt-adapter.ts +18 -0
- package/import-adapters/claude-adapter.ts +18 -0
- package/import-adapters/gemini-adapter.ts +56 -183
- package/import-adapters/mcp-memory-adapter.ts +18 -0
- package/import-adapters/types.ts +1 -1
- package/import-state-manager.ts +139 -0
- package/index.ts +1432 -3002
- package/llm-client.ts +74 -1
- package/memory-runtime.ts +723 -0
- package/native-memory.ts +196 -0
- package/onboarding-cli.ts +3 -2
- package/openclaw.plugin.json +5 -17
- package/package.json +7 -4
- package/pair-cli.ts +1 -1
- package/pair-crypto.ts +41 -483
- package/pair-http.ts +194 -5
- package/postinstall.mjs +138 -0
- package/relay.ts +172 -0
- package/reranker.ts +13 -8
- package/semantic-dedup.ts +5 -6
- package/skill-register.ts +146 -0
- package/skill.json +1 -1
- package/subgraph-search.ts +3 -1
- package/subgraph-store.ts +334 -299
- package/tool-gating.ts +39 -26
- package/tools.ts +499 -0
- package/tr-cli-export-helper.ts +138 -0
- package/tr-cli.ts +263 -133
- package/trajectory-poller.ts +162 -10
- package/vault-crypto.ts +705 -0
package/dist/subgraph-store.js
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
* Subgraph store path — writes facts on-chain via ERC-4337 UserOps.
|
|
3
3
|
*
|
|
4
4
|
* Used when the managed service is active (TOTALRECLAW_SELF_HOSTED is not
|
|
5
|
-
* "true"). Replaces the HTTP
|
|
5
|
+
* "true"). Replaces the HTTP request to /v1/store with an on-chain transaction
|
|
6
6
|
* flow.
|
|
7
7
|
*
|
|
8
|
-
* Uses @totalreclaw/core WASM for calldata encoding
|
|
9
|
-
* ECDSA
|
|
10
|
-
* and chain RPCs
|
|
8
|
+
* Uses @totalreclaw/core WASM for calldata encoding and UserOp hashing;
|
|
9
|
+
* `signUserOp` (ECDSA) lives in `vault-crypto.ts`. All JSON-RPC calls to
|
|
10
|
+
* the relay bundler and chain RPCs go through `relay.ts` (the plugin's
|
|
11
|
+
* single network site). No viem, no permissionless.
|
|
11
12
|
*/
|
|
12
13
|
// Lazy-load WASM via createRequire — the shipped bundle is ESM-only and
|
|
13
14
|
// the bare `require` global is undefined there (issue #124). Same pattern
|
|
@@ -22,49 +23,8 @@ function getWasm() {
|
|
|
22
23
|
}
|
|
23
24
|
import { CONFIG } from './config.js';
|
|
24
25
|
import { buildRelayHeaders } from './relay-headers.js';
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
/**
|
|
29
|
-
* Wrap a fetch-based JSON-RPC call with exponential backoff for HTTP 429
|
|
30
|
-
* (rate limit) responses from Pimlico. Max 5 retries with 5s base delay,
|
|
31
|
-
* doubling each attempt, capped at 60s, plus random jitter (0-1000ms).
|
|
32
|
-
* Total retry window: ~135s (5+10+20+40+60 plus jitter).
|
|
33
|
-
* All other HTTP errors throw immediately.
|
|
34
|
-
*/
|
|
35
|
-
async function rpcWithRetry(url, headers, method, params) {
|
|
36
|
-
const maxRetries = 5;
|
|
37
|
-
const baseDelay = 5000; // 5 seconds
|
|
38
|
-
const maxDelay = 60_000; // 60 seconds cap
|
|
39
|
-
const body = JSON.stringify({ jsonrpc: '2.0', id: 1, method, params });
|
|
40
|
-
for (let attempt = 1; attempt <= maxRetries + 1; attempt++) {
|
|
41
|
-
const resp = await fetch(url, { method: 'POST', headers, body });
|
|
42
|
-
if (resp.ok) {
|
|
43
|
-
const json = await resp.json();
|
|
44
|
-
if (json.error) {
|
|
45
|
-
// Check if the RPC-level error message indicates a rate limit
|
|
46
|
-
if (attempt <= maxRetries && /429|rate limit/i.test(json.error.message)) {
|
|
47
|
-
const delay = Math.min(Math.pow(2, attempt - 1) * baseDelay, maxDelay) + Math.floor(Math.random() * 1000);
|
|
48
|
-
console.error(`Pimlico rate limited, retrying in ${delay}ms (attempt ${attempt}/${maxRetries})...`);
|
|
49
|
-
await new Promise(r => setTimeout(r, delay));
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
throw new Error(`RPC ${method}: ${json.error.message}`);
|
|
53
|
-
}
|
|
54
|
-
return json.result;
|
|
55
|
-
}
|
|
56
|
-
// HTTP-level 429 — retry with backoff
|
|
57
|
-
if (resp.status === 429 && attempt <= maxRetries) {
|
|
58
|
-
const delay = Math.min(Math.pow(2, attempt - 1) * baseDelay, maxDelay) + Math.floor(Math.random() * 1000);
|
|
59
|
-
console.error(`Pimlico rate limited, retrying in ${delay}ms (attempt ${attempt}/${maxRetries})...`);
|
|
60
|
-
await new Promise(r => setTimeout(r, delay));
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
throw new Error(`Relay returned HTTP ${resp.status} for ${method}`);
|
|
64
|
-
}
|
|
65
|
-
// Should not be reached, but satisfies TypeScript
|
|
66
|
-
throw new Error(`RPC ${method}: max retries exceeded`);
|
|
67
|
-
}
|
|
26
|
+
import { rpcRequest, rpcWithRetry } from './relay.js';
|
|
27
|
+
import { signUserOp } from './vault-crypto.js';
|
|
68
28
|
/** Legacy protobuf wrapper schema version (v0/v1-binary inner blob). */
|
|
69
29
|
export const PROTOBUF_VERSION_LEGACY = 3;
|
|
70
30
|
/** Memory Taxonomy v1 protobuf wrapper schema version. */
|
|
@@ -116,9 +76,12 @@ function getDefaultRpcUrl(chainId) {
|
|
|
116
76
|
case 100:
|
|
117
77
|
return 'https://rpc.gnosischain.com';
|
|
118
78
|
case 84532:
|
|
79
|
+
// Retained for the legacy Base Sepolia chain id, but after ops-1 nothing
|
|
80
|
+
// should resolve here — the relay's authoritative chain_id is 100 (#402).
|
|
119
81
|
return 'https://sepolia.base.org';
|
|
120
82
|
default:
|
|
121
|
-
|
|
83
|
+
// Unknown chain id → Gnosis mainnet (after ops-1 default), NOT Base Sepolia.
|
|
84
|
+
return 'https://rpc.gnosischain.com';
|
|
122
85
|
}
|
|
123
86
|
}
|
|
124
87
|
// ---------------------------------------------------------------------------
|
|
@@ -132,7 +95,9 @@ function getDefaultRpcUrl(chainId) {
|
|
|
132
95
|
*/
|
|
133
96
|
export async function deriveSmartAccountAddress(mnemonic, chainId) {
|
|
134
97
|
const eoa = getWasm().deriveEoa(mnemonic);
|
|
135
|
-
|
|
98
|
+
// Default to Gnosis mainnet (100) after ops-1 — the SA address is CREATE2 and
|
|
99
|
+
// byte-equal across chains, but the RPC we query must be the live one (#402).
|
|
100
|
+
const resolvedChainId = chainId ?? 100;
|
|
136
101
|
// SimpleAccountFactory.getAddress(address owner, uint256 salt) — view function
|
|
137
102
|
// Selector: 0x8cb84e18 = keccak256("getAddress(address,uint256)")[0:4]
|
|
138
103
|
const factoryAddress = getWasm().getSimpleAccountFactory();
|
|
@@ -141,17 +106,12 @@ export async function deriveSmartAccountAddress(mnemonic, chainId) {
|
|
|
141
106
|
const selector = '8cb84e18';
|
|
142
107
|
const calldata = `0x${selector}${ownerPadded}${saltPadded}`;
|
|
143
108
|
const rpcUrl = CONFIG.rpcUrl || getDefaultRpcUrl(resolvedChainId);
|
|
144
|
-
const
|
|
145
|
-
|
|
109
|
+
const json = await rpcRequest({
|
|
110
|
+
url: rpcUrl,
|
|
146
111
|
headers: { 'Content-Type': 'application/json' },
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
id: 1,
|
|
150
|
-
method: 'eth_call',
|
|
151
|
-
params: [{ to: factoryAddress, data: calldata }, 'latest'],
|
|
152
|
-
}),
|
|
112
|
+
method: 'eth_call',
|
|
113
|
+
params: [{ to: factoryAddress, data: calldata }, 'latest'],
|
|
153
114
|
});
|
|
154
|
-
const json = await response.json();
|
|
155
115
|
if (json.error) {
|
|
156
116
|
throw new Error(`Failed to resolve Smart Account address: ${json.error.message}`);
|
|
157
117
|
}
|
|
@@ -162,16 +122,61 @@ export async function deriveSmartAccountAddress(mnemonic, chainId) {
|
|
|
162
122
|
return `0x${json.result.slice(-40)}`.toLowerCase();
|
|
163
123
|
}
|
|
164
124
|
// ---------------------------------------------------------------------------
|
|
165
|
-
// Smart Account deployment check
|
|
125
|
+
// Smart Account deployment check
|
|
166
126
|
// ---------------------------------------------------------------------------
|
|
127
|
+
//
|
|
128
|
+
// NOTE on the removed session cache (2026-06-28, AA10 fix):
|
|
129
|
+
//
|
|
130
|
+
// A module-level `Set<string>` of "already deployed" accounts used to skip
|
|
131
|
+
// the `eth_getCode` RPC after the first successful submission. That cache
|
|
132
|
+
// was the single largest source of AA10 "sender already constructed"
|
|
133
|
+
// failures in production:
|
|
134
|
+
//
|
|
135
|
+
// - A process restart emptied the cache → the next submission relied on
|
|
136
|
+
// ONE `eth_getCode` read that could return stale `0x` from a lagging
|
|
137
|
+
// RPC node (the deploy tx was mined but the node hadn't caught up) →
|
|
138
|
+
// initCode was re-added → the EntryPoint rejected with AA10.
|
|
139
|
+
// - A receipt poll that timed out (success undetected within the 120s
|
|
140
|
+
// window) left the cache unpopulated → the next submission hit the
|
|
141
|
+
// same stale-`eth_getCode` path → AA10.
|
|
142
|
+
// - The AA25 retry path (`deployedAccounts.delete(...)`) existed ONLY to
|
|
143
|
+
// paper over the cache; with no cache there is nothing to invalidate.
|
|
144
|
+
//
|
|
145
|
+
// Fix: `getInitCode` calls `eth_getCode` on EVERY invocation. The
|
|
146
|
+
// per-sender submission mutex (`withSenderLock`) already serializes
|
|
147
|
+
// submissions per account, so the extra RPC cannot introduce a nonce
|
|
148
|
+
// race. The cost is one extra RPC read per submission — negligible
|
|
149
|
+
// against a relay round-trip — and the behavior is correct by
|
|
150
|
+
// construction: the plugin asserts on-chain state at submit time rather
|
|
151
|
+
// than trusting an in-memory guess that can be invalidated by anything
|
|
152
|
+
// the plugin doesn't observe (relayer retries, parallel clients, node
|
|
153
|
+
// reorgs, process restarts).
|
|
167
154
|
/**
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
* This prevents AA10 "duplicate deployment" errors when multiple facts
|
|
172
|
-
* are stored in rapid succession for a first-time user.
|
|
155
|
+
* Test-only RPC probe counter. Incremented each time `getInitCode` issues
|
|
156
|
+
* an `eth_getCode` read. The lifecycle test uses this to assert the cache
|
|
157
|
+
* is truly gone (every call hits the wire). Not part of the public API.
|
|
173
158
|
*/
|
|
174
|
-
|
|
159
|
+
let _ethGetCodeProbeCount = 0;
|
|
160
|
+
/** Test-only seam: drive the private `getInitCode` logic. */
|
|
161
|
+
export async function __getInitCodeForTests(sender, eoaAddress, rpcUrl) {
|
|
162
|
+
return getInitCode(sender, eoaAddress, rpcUrl);
|
|
163
|
+
}
|
|
164
|
+
/** Test-only seam: read the RPC probe counter. */
|
|
165
|
+
export function __getRpcProbeCountForTests() {
|
|
166
|
+
return _ethGetCodeProbeCount;
|
|
167
|
+
}
|
|
168
|
+
/** Test-only seam: reset the RPC probe counter. */
|
|
169
|
+
export function __resetRpcProbeCountForTests() {
|
|
170
|
+
_ethGetCodeProbeCount = 0;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Test-only seam: previously reset the deployment cache. The cache was
|
|
174
|
+
* removed in the AA10 fix; this function is retained as a no-op so the
|
|
175
|
+
* lifecycle test (and any future test that imports it) doesn't break.
|
|
176
|
+
*/
|
|
177
|
+
export function __resetDeployedAccountsForTests() {
|
|
178
|
+
/* no-op: session cache removed; getInitCode always re-checks eth_getCode */
|
|
179
|
+
}
|
|
175
180
|
// ---------------------------------------------------------------------------
|
|
176
181
|
// Per-account submission mutex — 3.3.1-rc.3 AA25 serialization
|
|
177
182
|
// ---------------------------------------------------------------------------
|
|
@@ -233,30 +238,72 @@ async function withSenderLock(sender, fn) {
|
|
|
233
238
|
export function __resetSenderLocksForTests() {
|
|
234
239
|
_senderSubmissionLocks.clear();
|
|
235
240
|
}
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
// Test-only WASM mock seams (AA10 submit-path retry test)
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
/**
|
|
245
|
+
* Test-only seam: inject a mock WASM module.
|
|
246
|
+
*
|
|
247
|
+
* The AA10 submit-path retry test needs to drive the real submitFactBatchOnChain
|
|
248
|
+
* while controlling WASM behavior (deriveEoa, encodeBatchCall, hashUserOp,
|
|
249
|
+
* signUserOp, getEntryPointAddress). This seam swaps the module-level _wasm
|
|
250
|
+
* reference so the test can provide stubs.
|
|
251
|
+
*
|
|
252
|
+
* MUST be followed by __clearWasmForTests() in teardown.
|
|
253
|
+
*/
|
|
254
|
+
export function __setWasmForTests(mock) {
|
|
255
|
+
_wasm = mock;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Test-only seam: restore the real WASM module.
|
|
259
|
+
*
|
|
260
|
+
* Clears a test-injected mock WASM and resets the module to null so the next
|
|
261
|
+
* getWasm() call reloads the real @totalreclaw/core.
|
|
262
|
+
*/
|
|
263
|
+
export function __clearWasmForTests() {
|
|
264
|
+
_wasm = null;
|
|
265
|
+
}
|
|
236
266
|
/**
|
|
237
267
|
* Check if a Smart Account is deployed and return factory/factoryData if not.
|
|
238
268
|
*
|
|
239
|
-
* For ERC-4337 v0.7, undeployed accounts need `factory`
|
|
240
|
-
* in the UserOp so the EntryPoint
|
|
269
|
+
* For ERC-4337 v0.7, undeployed (counterfactual) accounts need `factory`
|
|
270
|
+
* and `factoryData` in the UserOp so the EntryPoint deploys the SA + runs
|
|
271
|
+
* signature validation in one transaction.
|
|
272
|
+
*
|
|
273
|
+
* Re-checks `eth_getCode` on EVERY call — no session cache. The previous
|
|
274
|
+
* in-memory cache was the source of AA10 "sender already constructed"
|
|
275
|
+
* errors: it could be stale (process restart, missed receipt, lagging RPC
|
|
276
|
+
* node) and re-add initCode to a UserOp whose sender was already
|
|
277
|
+
* constructed. Each submission now pays one extra RPC read to assert the
|
|
278
|
+
* on-chain deployment state at submit time, which is the only source of
|
|
279
|
+
* truth the EntryPoint will enforce. See the note above on the removed
|
|
280
|
+
* cache for the full failure taxonomy.
|
|
241
281
|
*/
|
|
242
282
|
async function getInitCode(sender, eoaAddress, rpcUrl) {
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
method: 'POST',
|
|
283
|
+
// Check if the Smart Account contract is deployed. Always re-read — never
|
|
284
|
+
// cache. The per-sender submission mutex serializes calls for the same
|
|
285
|
+
// account, so this cannot race a nonce fetch.
|
|
286
|
+
_ethGetCodeProbeCount++;
|
|
287
|
+
const codeJson = await rpcRequest({
|
|
288
|
+
url: rpcUrl,
|
|
250
289
|
headers: { 'Content-Type': 'application/json' },
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
params: [sender, 'latest'],
|
|
254
|
-
}),
|
|
290
|
+
method: 'eth_getCode',
|
|
291
|
+
params: [sender, 'latest'],
|
|
255
292
|
});
|
|
256
|
-
|
|
257
|
-
|
|
293
|
+
// A JSON-RPC error envelope (e.g. a rate-limited public RPC) or a
|
|
294
|
+
// non-string result is NOT evidence the account is undeployed. Treat it as
|
|
295
|
+
// a hard failure and throw — otherwise we would attach initCode to a
|
|
296
|
+
// possibly-deployed sender, guaranteeing AA10 "sender already constructed"
|
|
297
|
+
// at the paymaster. The thrown error propagates to the submit retry-loop
|
|
298
|
+
// catch; it does not match the AA25/AA10 regex, so it fails fast with an
|
|
299
|
+
// accurate message rather than poisoning a UserOp. Only a literal '0x' /
|
|
300
|
+
// '0x0' result means "not deployed" (#402).
|
|
301
|
+
const codeResult = codeJson.result;
|
|
302
|
+
if (codeJson.error || typeof codeResult !== 'string') {
|
|
303
|
+
throw new Error('eth_getCode failed: ' + (codeJson.error?.message || 'no result'));
|
|
304
|
+
}
|
|
305
|
+
const isDeployed = codeResult !== '0x' && codeResult !== '0x0';
|
|
258
306
|
if (isDeployed) {
|
|
259
|
-
deployedAccounts.add(sender.toLowerCase());
|
|
260
307
|
return { factory: null, factoryData: null };
|
|
261
308
|
}
|
|
262
309
|
// Account not deployed — build factory + factoryData for first-time deployment.
|
|
@@ -270,7 +317,7 @@ async function getInitCode(sender, eoaAddress, rpcUrl) {
|
|
|
270
317
|
return { factory, factoryData };
|
|
271
318
|
}
|
|
272
319
|
// ---------------------------------------------------------------------------
|
|
273
|
-
// On-chain submission (ERC-4337 UserOps via
|
|
320
|
+
// On-chain submission (ERC-4337 UserOps via relay.ts network site)
|
|
274
321
|
// ---------------------------------------------------------------------------
|
|
275
322
|
/**
|
|
276
323
|
* Submit a fact on-chain via ERC-4337 UserOp through the relay server.
|
|
@@ -281,7 +328,7 @@ async function getInitCode(sender, eoaAddress, rpcUrl) {
|
|
|
281
328
|
* 3. UserOp hashing (ERC-4337 v0.7)
|
|
282
329
|
* 4. ECDSA signing (EIP-191 prefixed)
|
|
283
330
|
*
|
|
284
|
-
* All JSON-RPC calls go through
|
|
331
|
+
* All JSON-RPC calls go through `relay.ts` to the relay bundler endpoint.
|
|
285
332
|
*/
|
|
286
333
|
export async function submitFactOnChain(protobufPayload, config) {
|
|
287
334
|
if (!config.relayUrl) {
|
|
@@ -309,7 +356,7 @@ async function submitFactOnChainLocked(protobufPayload, config, eoa, sender) {
|
|
|
309
356
|
const headers = buildRelayHeaders(overrides);
|
|
310
357
|
// Helper for JSON-RPC calls to relay bundler (with 429 retry)
|
|
311
358
|
async function rpc(method, params) {
|
|
312
|
-
return rpcWithRetry(bundlerUrl, headers, method, params);
|
|
359
|
+
return rpcWithRetry({ url: bundlerUrl, headers, method, params });
|
|
313
360
|
}
|
|
314
361
|
const entryPoint = config.entryPointAddress || getWasm().getEntryPointAddress();
|
|
315
362
|
// 2. Encode calldata (SimpleAccount.execute → DataEdge fallback)
|
|
@@ -319,8 +366,6 @@ async function submitFactOnChainLocked(protobufPayload, config, eoa, sender) {
|
|
|
319
366
|
const gasPrices = await rpc('pimlico_getUserOperationGasPrice', []);
|
|
320
367
|
const fast = gasPrices.fast;
|
|
321
368
|
const rpcUrl = config.rpcUrl || CONFIG.rpcUrl || getDefaultRpcUrl(config.chainId);
|
|
322
|
-
// 4. Check if Smart Account is deployed (needed for factory/factoryData)
|
|
323
|
-
const { factory, factoryData } = await getInitCode(sender, eoa.address, rpcUrl);
|
|
324
369
|
// 5. Get nonce from EntryPoint via bundler RPC.
|
|
325
370
|
// Routing through the bundler lets Pimlico account for pending mempool
|
|
326
371
|
// UserOps, preventing AA25 nonce conflicts on rapid submissions.
|
|
@@ -330,85 +375,51 @@ async function submitFactOnChainLocked(protobufPayload, config, eoa, sender) {
|
|
|
330
375
|
const senderPadded = sender.slice(2).toLowerCase().padStart(64, '0');
|
|
331
376
|
const keyPadded = '0'.repeat(64);
|
|
332
377
|
const nonceCalldata = `0x35567e1a${senderPadded}${keyPadded}`;
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
378
|
+
// Helper to fetch nonce (with bundler fallback)
|
|
379
|
+
async function fetchNonce() {
|
|
380
|
+
try {
|
|
381
|
+
const nonceResult = await rpc('eth_call', [{ to: entryPoint, data: nonceCalldata }, 'latest']);
|
|
382
|
+
return nonceResult || '0x0';
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
// Fallback to public RPC if bundler doesn't support eth_call
|
|
386
|
+
const nonceJson = await rpcRequest({
|
|
387
|
+
url: rpcUrl,
|
|
388
|
+
headers: { 'Content-Type': 'application/json' },
|
|
389
|
+
method: 'eth_call',
|
|
345
390
|
params: [{ to: entryPoint, data: nonceCalldata }, 'latest'],
|
|
346
|
-
})
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
nonce = nonceJson.result || '0x0';
|
|
350
|
-
}
|
|
351
|
-
// 6. Build unsigned UserOp (v0.7 fields, camelCase for Rust JSON serde)
|
|
352
|
-
const unsignedOp = {
|
|
353
|
-
sender,
|
|
354
|
-
nonce,
|
|
355
|
-
callData,
|
|
356
|
-
callGasLimit: '0x0',
|
|
357
|
-
verificationGasLimit: '0x0',
|
|
358
|
-
preVerificationGas: '0x0',
|
|
359
|
-
maxFeePerGas: fast.maxFeePerGas,
|
|
360
|
-
maxPriorityFeePerGas: fast.maxPriorityFeePerGas,
|
|
361
|
-
signature: DUMMY_SIGNATURE,
|
|
362
|
-
};
|
|
363
|
-
if (factory) {
|
|
364
|
-
unsignedOp.factory = factory;
|
|
365
|
-
unsignedOp.factoryData = factoryData;
|
|
391
|
+
});
|
|
392
|
+
return nonceJson.result || '0x0';
|
|
393
|
+
}
|
|
366
394
|
}
|
|
367
|
-
//
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
//
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const sigHex = getWasm().signUserOp(hashHex, eoa.private_key);
|
|
374
|
-
unsignedOp.signature = `0x${sigHex}`;
|
|
375
|
-
// 9. Submit the signed UserOp (with AA25 nonce conflict retry)
|
|
395
|
+
// Track force-deployed senders for AA10 retry (local to this submission attempt)
|
|
396
|
+
const forceDeployed = new Set();
|
|
397
|
+
// Single retry loop: getInitCode → build UserOp → sponsor → sign → send
|
|
398
|
+
// On AA10 "sender already constructed", mark sender as force-deployed and retry.
|
|
399
|
+
// AA10 can occur at pm_sponsorUserOperation (initCode present on deployed sender)
|
|
400
|
+
// or eth_sendUserOperation (same root cause). This loop handles both.
|
|
376
401
|
let userOpHash;
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
//
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
let retryNonce;
|
|
392
|
-
try {
|
|
393
|
-
const retryNonceResult = await rpc('eth_call', [{ to: entryPoint, data: nonceCalldata }, 'latest']);
|
|
394
|
-
retryNonce = retryNonceResult || '0x0';
|
|
395
|
-
}
|
|
396
|
-
catch {
|
|
397
|
-
const retryNonceResp = await fetch(rpcUrl, {
|
|
398
|
-
method: 'POST',
|
|
399
|
-
headers: { 'Content-Type': 'application/json' },
|
|
400
|
-
body: JSON.stringify({
|
|
401
|
-
jsonrpc: '2.0', id: 1, method: 'eth_call',
|
|
402
|
-
params: [{ to: entryPoint, data: nonceCalldata }, 'latest'],
|
|
403
|
-
}),
|
|
404
|
-
});
|
|
405
|
-
const retryNonceJson = await retryNonceResp.json();
|
|
406
|
-
retryNonce = retryNonceJson.result || '0x0';
|
|
402
|
+
let lastErr;
|
|
403
|
+
let attempt = 0;
|
|
404
|
+
const maxAttempts = 2;
|
|
405
|
+
while (attempt < maxAttempts) {
|
|
406
|
+
attempt++;
|
|
407
|
+
try {
|
|
408
|
+
// 4. Check if Smart Account is deployed (needed for factory/factoryData)
|
|
409
|
+
// If force-deployed, skip eth_getCode and return null initCode.
|
|
410
|
+
let factory = null;
|
|
411
|
+
let factoryData = null;
|
|
412
|
+
if (!forceDeployed.has(sender.toLowerCase())) {
|
|
413
|
+
const initCode = await getInitCode(sender, eoa.address, rpcUrl);
|
|
414
|
+
factory = initCode.factory;
|
|
415
|
+
factoryData = initCode.factoryData;
|
|
407
416
|
}
|
|
408
|
-
//
|
|
409
|
-
const
|
|
417
|
+
// Fetch fresh nonce for each attempt
|
|
418
|
+
const nonce = await fetchNonce();
|
|
419
|
+
// 6. Build unsigned UserOp (v0.7 fields, camelCase for Rust JSON serde)
|
|
420
|
+
const unsignedOp = {
|
|
410
421
|
sender,
|
|
411
|
-
nonce
|
|
422
|
+
nonce,
|
|
412
423
|
callData,
|
|
413
424
|
callGasLimit: '0x0',
|
|
414
425
|
verificationGasLimit: '0x0',
|
|
@@ -417,23 +428,53 @@ async function submitFactOnChainLocked(protobufPayload, config, eoa, sender) {
|
|
|
417
428
|
maxPriorityFeePerGas: fast.maxPriorityFeePerGas,
|
|
418
429
|
signature: DUMMY_SIGNATURE,
|
|
419
430
|
};
|
|
420
|
-
if (
|
|
421
|
-
|
|
422
|
-
|
|
431
|
+
if (factory) {
|
|
432
|
+
unsignedOp.factory = factory;
|
|
433
|
+
unsignedOp.factoryData = factoryData;
|
|
423
434
|
}
|
|
424
|
-
//
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
435
|
+
// 7. Get paymaster sponsorship (fills gas limits + paymaster fields)
|
|
436
|
+
// This is where AA10 "sender already constructed" can occur if initCode
|
|
437
|
+
// is present but the sender is already deployed.
|
|
438
|
+
const sponsorResult = await rpc('pm_sponsorUserOperation', [unsignedOp, entryPoint]);
|
|
439
|
+
Object.assign(unsignedOp, sponsorResult);
|
|
440
|
+
// 8. Hash and sign the UserOp via WASM
|
|
441
|
+
const opJson = JSON.stringify(unsignedOp);
|
|
442
|
+
const hashHex = getWasm().hashUserOp(opJson, entryPoint, BigInt(config.chainId));
|
|
443
|
+
const sigHex = signUserOp(hashHex, eoa.private_key);
|
|
444
|
+
unsignedOp.signature = `0x${sigHex}`;
|
|
445
|
+
// 9. Submit the signed UserOp
|
|
446
|
+
userOpHash = await rpc('eth_sendUserOperation', [unsignedOp, entryPoint]);
|
|
447
|
+
// Success — break out of retry loop
|
|
448
|
+
break;
|
|
432
449
|
}
|
|
433
|
-
|
|
450
|
+
catch (err) {
|
|
451
|
+
const msg = err?.message || '';
|
|
452
|
+
// AA10 "sender already constructed" or AA25 invalid nonce → retry
|
|
453
|
+
if (/AA25|AA10|invalid account nonce|already being processed/i.test(msg)) {
|
|
454
|
+
lastErr = err;
|
|
455
|
+
console.error(`AA25/AA10 detected (attempt ${attempt}/${maxAttempts}), retrying...`);
|
|
456
|
+
// On AA10, force-mark sender as deployed so next retry omits initCode
|
|
457
|
+
if (/AA10/i.test(msg)) {
|
|
458
|
+
forceDeployed.add(sender.toLowerCase());
|
|
459
|
+
console.error('AA10: force-marking sender as deployed, retrying without initCode');
|
|
460
|
+
}
|
|
461
|
+
// Wait for previous UserOp to mine before retrying with fresh nonce.
|
|
462
|
+
// Public RPC won't reflect the new nonce until the tx is on-chain.
|
|
463
|
+
await new Promise(r => setTimeout(r, 15000));
|
|
464
|
+
// Continue to next iteration of retry loop
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
// Not a retryable error — re-throw
|
|
434
468
|
throw err;
|
|
435
469
|
}
|
|
436
470
|
}
|
|
471
|
+
// Retry budget exhausted with no successful submission — throw the last
|
|
472
|
+
// retryable error instead of falling through to a receipt poll against an
|
|
473
|
+
// undefined userOpHash (which used to burn 120s and surface a misleading
|
|
474
|
+
// 'submission failed (tx=…)'). See #402.
|
|
475
|
+
if (userOpHash == null) {
|
|
476
|
+
throw lastErr ?? new Error('eth_sendUserOperation returned no result');
|
|
477
|
+
}
|
|
437
478
|
// 10. Wait for receipt (poll up to 120s)
|
|
438
479
|
let receipt = null;
|
|
439
480
|
for (let i = 0; i < 60; i++) {
|
|
@@ -446,10 +487,10 @@ async function submitFactOnChainLocked(protobufPayload, config, eoa, sender) {
|
|
|
446
487
|
catch { /* not mined yet */ }
|
|
447
488
|
}
|
|
448
489
|
const success = receipt?.success ?? false;
|
|
449
|
-
//
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
490
|
+
// No session-deployment cache to update — getInitCode always re-checks
|
|
491
|
+
// eth_getCode on the next submission, so a successful receipt needs no
|
|
492
|
+
// bookkeeping here. (Previous cache removed in the AA10 fix — see note
|
|
493
|
+
// at the top of this section.)
|
|
453
494
|
return {
|
|
454
495
|
txHash: receipt?.receipt?.transactionHash || '',
|
|
455
496
|
userOpHash,
|
|
@@ -497,7 +538,7 @@ async function submitFactBatchOnChainLocked(protobufPayloads, config, eoa, sende
|
|
|
497
538
|
const headers = buildRelayHeaders(overrides);
|
|
498
539
|
// Helper for JSON-RPC calls to relay bundler (with 429 retry)
|
|
499
540
|
async function rpc(method, params) {
|
|
500
|
-
return rpcWithRetry(bundlerUrl, headers, method, params);
|
|
541
|
+
return rpcWithRetry({ url: bundlerUrl, headers, method, params });
|
|
501
542
|
}
|
|
502
543
|
const entryPoint = config.entryPointAddress || getWasm().getEntryPointAddress();
|
|
503
544
|
// Encode batch calldata (SimpleAccount.executeBatch)
|
|
@@ -509,107 +550,52 @@ async function submitFactBatchOnChainLocked(protobufPayloads, config, eoa, sende
|
|
|
509
550
|
const gasPrices = await rpc('pimlico_getUserOperationGasPrice', []);
|
|
510
551
|
const fast = gasPrices.fast;
|
|
511
552
|
const rpcUrl = config.rpcUrl || CONFIG.rpcUrl || getDefaultRpcUrl(config.chainId);
|
|
512
|
-
// Check if Smart Account is deployed (needed for factory/factoryData)
|
|
513
|
-
const { factory, factoryData } = await getInitCode(sender, eoa.address, rpcUrl);
|
|
514
553
|
// Get nonce via bundler (accounts for pending mempool UserOps) with public RPC fallback
|
|
515
554
|
const senderPadded = sender.slice(2).toLowerCase().padStart(64, '0');
|
|
516
555
|
const keyPadded = '0'.repeat(64);
|
|
517
556
|
const nonceCalldata = `0x35567e1a${senderPadded}${keyPadded}`;
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
const nonceResult = await rpc('eth_call', [{ to: entryPoint, data: nonceCalldata }, 'latest']);
|
|
521
|
-
nonce = nonceResult || '0x0';
|
|
522
|
-
}
|
|
523
|
-
catch {
|
|
524
|
-
const nonceResp = await fetch(rpcUrl, {
|
|
525
|
-
method: 'POST',
|
|
526
|
-
headers: { 'Content-Type': 'application/json' },
|
|
527
|
-
body: JSON.stringify({
|
|
528
|
-
jsonrpc: '2.0', id: 1, method: 'eth_call',
|
|
529
|
-
params: [{ to: entryPoint, data: nonceCalldata }, 'latest'],
|
|
530
|
-
}),
|
|
531
|
-
});
|
|
532
|
-
const nonceJson = await nonceResp.json();
|
|
533
|
-
nonce = nonceJson.result || '0x0';
|
|
534
|
-
}
|
|
535
|
-
// Build unsigned UserOp
|
|
536
|
-
const unsignedOp = {
|
|
537
|
-
sender,
|
|
538
|
-
nonce,
|
|
539
|
-
callData,
|
|
540
|
-
callGasLimit: '0x0',
|
|
541
|
-
verificationGasLimit: '0x0',
|
|
542
|
-
preVerificationGas: '0x0',
|
|
543
|
-
maxFeePerGas: fast.maxFeePerGas,
|
|
544
|
-
maxPriorityFeePerGas: fast.maxPriorityFeePerGas,
|
|
545
|
-
signature: DUMMY_SIGNATURE,
|
|
546
|
-
};
|
|
547
|
-
if (factory) {
|
|
548
|
-
unsignedOp.factory = factory;
|
|
549
|
-
unsignedOp.factoryData = factoryData;
|
|
550
|
-
}
|
|
551
|
-
// Gas estimation for batch operations — get accurate gas limits from Pimlico
|
|
552
|
-
// before paymaster sponsorship (can't bump after sponsorship as it invalidates
|
|
553
|
-
// the paymaster's signature, causing AA34).
|
|
554
|
-
if (protobufPayloads.length > 1) {
|
|
557
|
+
// Helper to fetch nonce (with bundler fallback)
|
|
558
|
+
async function fetchNonce() {
|
|
555
559
|
try {
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
unsignedOp.callGasLimit = gasEstimate.callGasLimit;
|
|
559
|
-
if (gasEstimate.verificationGasLimit)
|
|
560
|
-
unsignedOp.verificationGasLimit = gasEstimate.verificationGasLimit;
|
|
561
|
-
if (gasEstimate.preVerificationGas)
|
|
562
|
-
unsignedOp.preVerificationGas = gasEstimate.preVerificationGas;
|
|
560
|
+
const nonceResult = await rpc('eth_call', [{ to: entryPoint, data: nonceCalldata }, 'latest']);
|
|
561
|
+
return nonceResult || '0x0';
|
|
563
562
|
}
|
|
564
563
|
catch {
|
|
565
|
-
|
|
564
|
+
const nonceJson = await rpcRequest({
|
|
565
|
+
url: rpcUrl,
|
|
566
|
+
headers: { 'Content-Type': 'application/json' },
|
|
567
|
+
method: 'eth_call',
|
|
568
|
+
params: [{ to: entryPoint, data: nonceCalldata }, 'latest'],
|
|
569
|
+
});
|
|
570
|
+
return nonceJson.result || '0x0';
|
|
566
571
|
}
|
|
567
572
|
}
|
|
568
|
-
//
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
//
|
|
572
|
-
const opJson = JSON.stringify(unsignedOp);
|
|
573
|
-
const hashHex = getWasm().hashUserOp(opJson, entryPoint, BigInt(config.chainId));
|
|
574
|
-
const sigHex = getWasm().signUserOp(hashHex, eoa.private_key);
|
|
575
|
-
unsignedOp.signature = `0x${sigHex}`;
|
|
576
|
-
// Submit (with AA25 nonce conflict retry)
|
|
573
|
+
// Track force-deployed senders for AA10 retry (local to this submission attempt)
|
|
574
|
+
const forceDeployed = new Set();
|
|
575
|
+
// Single retry loop: getInitCode → build UserOp → estimate → sponsor → sign → send
|
|
576
|
+
// On AA10 "sender already constructed", mark sender as force-deployed and retry.
|
|
577
577
|
let userOpHash;
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
//
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
let retryNonce;
|
|
593
|
-
try {
|
|
594
|
-
const retryNonceResult = await rpc('eth_call', [{ to: entryPoint, data: nonceCalldata }, 'latest']);
|
|
595
|
-
retryNonce = retryNonceResult || '0x0';
|
|
596
|
-
}
|
|
597
|
-
catch {
|
|
598
|
-
const retryNonceResp = await fetch(rpcUrl, {
|
|
599
|
-
method: 'POST',
|
|
600
|
-
headers: { 'Content-Type': 'application/json' },
|
|
601
|
-
body: JSON.stringify({
|
|
602
|
-
jsonrpc: '2.0', id: 1, method: 'eth_call',
|
|
603
|
-
params: [{ to: entryPoint, data: nonceCalldata }, 'latest'],
|
|
604
|
-
}),
|
|
605
|
-
});
|
|
606
|
-
const retryNonceJson = await retryNonceResp.json();
|
|
607
|
-
retryNonce = retryNonceJson.result || '0x0';
|
|
578
|
+
let lastErr;
|
|
579
|
+
let attempt = 0;
|
|
580
|
+
const maxAttempts = 2;
|
|
581
|
+
while (attempt < maxAttempts) {
|
|
582
|
+
attempt++;
|
|
583
|
+
try {
|
|
584
|
+
// Check if Smart Account is deployed (needed for factory/factoryData)
|
|
585
|
+
// If force-deployed, skip eth_getCode and return null initCode.
|
|
586
|
+
let factory = null;
|
|
587
|
+
let factoryData = null;
|
|
588
|
+
if (!forceDeployed.has(sender.toLowerCase())) {
|
|
589
|
+
const initCode = await getInitCode(sender, eoa.address, rpcUrl);
|
|
590
|
+
factory = initCode.factory;
|
|
591
|
+
factoryData = initCode.factoryData;
|
|
608
592
|
}
|
|
609
|
-
//
|
|
610
|
-
const
|
|
593
|
+
// Fetch fresh nonce for each attempt
|
|
594
|
+
const nonce = await fetchNonce();
|
|
595
|
+
// Build unsigned UserOp
|
|
596
|
+
const unsignedOp = {
|
|
611
597
|
sender,
|
|
612
|
-
nonce
|
|
598
|
+
nonce,
|
|
613
599
|
callData,
|
|
614
600
|
callGasLimit: '0x0',
|
|
615
601
|
verificationGasLimit: '0x0',
|
|
@@ -618,23 +604,70 @@ async function submitFactBatchOnChainLocked(protobufPayloads, config, eoa, sende
|
|
|
618
604
|
maxPriorityFeePerGas: fast.maxPriorityFeePerGas,
|
|
619
605
|
signature: DUMMY_SIGNATURE,
|
|
620
606
|
};
|
|
621
|
-
if (
|
|
622
|
-
|
|
623
|
-
|
|
607
|
+
if (factory) {
|
|
608
|
+
unsignedOp.factory = factory;
|
|
609
|
+
unsignedOp.factoryData = factoryData;
|
|
610
|
+
}
|
|
611
|
+
// Gas estimation for batch operations — get accurate gas limits from Pimlico
|
|
612
|
+
// before paymaster sponsorship (can't bump after sponsorship as it invalidates
|
|
613
|
+
// the paymaster's signature, causing AA34).
|
|
614
|
+
if (protobufPayloads.length > 1) {
|
|
615
|
+
try {
|
|
616
|
+
const gasEstimate = await rpc('eth_estimateUserOperationGas', [unsignedOp, entryPoint]);
|
|
617
|
+
if (gasEstimate.callGasLimit)
|
|
618
|
+
unsignedOp.callGasLimit = gasEstimate.callGasLimit;
|
|
619
|
+
if (gasEstimate.verificationGasLimit)
|
|
620
|
+
unsignedOp.verificationGasLimit = gasEstimate.verificationGasLimit;
|
|
621
|
+
if (gasEstimate.preVerificationGas)
|
|
622
|
+
unsignedOp.preVerificationGas = gasEstimate.preVerificationGas;
|
|
623
|
+
}
|
|
624
|
+
catch {
|
|
625
|
+
// If estimation fails, let the paymaster handle it (default behavior)
|
|
626
|
+
}
|
|
624
627
|
}
|
|
625
|
-
//
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
628
|
+
// Paymaster sponsorship (uses gas limits from estimation above for batches)
|
|
629
|
+
// This is where AA10 "sender already constructed" can occur if initCode
|
|
630
|
+
// is present but the sender is already deployed.
|
|
631
|
+
const sponsorResult = await rpc('pm_sponsorUserOperation', [unsignedOp, entryPoint]);
|
|
632
|
+
Object.assign(unsignedOp, sponsorResult);
|
|
633
|
+
// Hash and sign via WASM
|
|
634
|
+
const opJson = JSON.stringify(unsignedOp);
|
|
635
|
+
const hashHex = getWasm().hashUserOp(opJson, entryPoint, BigInt(config.chainId));
|
|
636
|
+
const sigHex = signUserOp(hashHex, eoa.private_key);
|
|
637
|
+
unsignedOp.signature = `0x${sigHex}`;
|
|
638
|
+
// Submit the signed UserOp
|
|
639
|
+
userOpHash = await rpc('eth_sendUserOperation', [unsignedOp, entryPoint]);
|
|
640
|
+
// Success — break out of retry loop
|
|
641
|
+
break;
|
|
633
642
|
}
|
|
634
|
-
|
|
643
|
+
catch (err) {
|
|
644
|
+
const msg = err?.message || '';
|
|
645
|
+
// AA10 "sender already constructed" or AA25 invalid nonce → retry
|
|
646
|
+
if (/AA25|AA10|invalid account nonce|already being processed/i.test(msg)) {
|
|
647
|
+
lastErr = err;
|
|
648
|
+
console.error(`AA25/AA10 detected (batch, attempt ${attempt}/${maxAttempts}), retrying...`);
|
|
649
|
+
// On AA10, force-mark sender as deployed so next retry omits initCode
|
|
650
|
+
if (/AA10/i.test(msg)) {
|
|
651
|
+
forceDeployed.add(sender.toLowerCase());
|
|
652
|
+
console.error('AA10: force-marking sender as deployed, retrying without initCode');
|
|
653
|
+
}
|
|
654
|
+
// Wait for previous UserOp to mine before retrying with fresh nonce.
|
|
655
|
+
// Public RPC won't reflect the new nonce until the tx is on-chain.
|
|
656
|
+
await new Promise(r => setTimeout(r, 15000));
|
|
657
|
+
// Continue to next iteration of retry loop
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
// Not a retryable error — re-throw
|
|
635
661
|
throw err;
|
|
636
662
|
}
|
|
637
663
|
}
|
|
664
|
+
// Retry budget exhausted with no successful submission — throw the last
|
|
665
|
+
// retryable error instead of polling for a receipt against an undefined
|
|
666
|
+
// userOpHash (which used to burn 120s and surface a misleading
|
|
667
|
+
// 'submission failed (tx=…)'). See #402.
|
|
668
|
+
if (userOpHash == null) {
|
|
669
|
+
throw lastErr ?? new Error('eth_sendUserOperation returned no result');
|
|
670
|
+
}
|
|
638
671
|
// Wait for receipt (poll up to 120s)
|
|
639
672
|
let receipt = null;
|
|
640
673
|
for (let i = 0; i < 60; i++) {
|
|
@@ -647,10 +680,10 @@ async function submitFactBatchOnChainLocked(protobufPayloads, config, eoa, sende
|
|
|
647
680
|
catch { /* not mined yet */ }
|
|
648
681
|
}
|
|
649
682
|
const batchSuccess = receipt?.success ?? false;
|
|
650
|
-
//
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
683
|
+
// No session-deployment cache to update — getInitCode always re-checks
|
|
684
|
+
// eth_getCode on the next submission, so a successful receipt needs no
|
|
685
|
+
// bookkeeping here. (Previous cache removed in the AA10 fix — see note
|
|
686
|
+
// at the top of the Smart Account deployment-check section.)
|
|
654
687
|
return {
|
|
655
688
|
txHash: receipt?.receipt?.transactionHash || '',
|
|
656
689
|
userOpHash,
|