@totalreclaw/totalreclaw 3.3.12-rc.7 → 3.3.12

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.
Files changed (79) hide show
  1. package/SKILL.md +33 -27
  2. package/api-client.ts +17 -9
  3. package/batch-gate.ts +42 -0
  4. package/billing-cache.ts +72 -23
  5. package/claims-helper.ts +2 -1
  6. package/config.ts +95 -13
  7. package/consolidation.ts +6 -13
  8. package/contradiction-sync.ts +19 -14
  9. package/credential-provider.ts +184 -0
  10. package/crypto.ts +27 -160
  11. package/dist/api-client.js +17 -9
  12. package/dist/batch-gate.js +40 -0
  13. package/dist/billing-cache.js +54 -24
  14. package/dist/claims-helper.js +2 -1
  15. package/dist/config.js +91 -13
  16. package/dist/consolidation.js +6 -15
  17. package/dist/contradiction-sync.js +15 -15
  18. package/dist/credential-provider.js +145 -0
  19. package/dist/crypto.js +17 -137
  20. package/dist/download-ux.js +11 -7
  21. package/dist/embedder-loader.js +266 -0
  22. package/dist/embedding.js +36 -3
  23. package/dist/entry.js +123 -0
  24. package/dist/fs-helpers.js +75 -379
  25. package/dist/import-adapters/gemini-adapter.js +29 -159
  26. package/dist/index.js +864 -2631
  27. package/dist/memory-runtime.js +459 -0
  28. package/dist/native-memory.js +123 -0
  29. package/dist/onboarding-cli.js +4 -8
  30. package/dist/pair-cli-relay.js +1 -8
  31. package/dist/pair-cli.js +1 -1
  32. package/dist/pair-crypto.js +16 -358
  33. package/dist/pair-http.js +147 -4
  34. package/dist/relay.js +140 -0
  35. package/dist/reranker.js +13 -8
  36. package/dist/semantic-dedup.js +5 -7
  37. package/dist/skill-register.js +97 -0
  38. package/dist/subgraph-search.js +3 -1
  39. package/dist/subgraph-store.js +348 -290
  40. package/dist/tool-gating.js +39 -26
  41. package/dist/tools.js +367 -0
  42. package/dist/tr-cli-export-helper.js +3 -3
  43. package/dist/tr-cli.js +65 -156
  44. package/dist/trajectory-poller.js +155 -9
  45. package/dist/vault-crypto.js +551 -0
  46. package/download-ux.ts +12 -6
  47. package/embedder-loader.ts +293 -1
  48. package/embedding.ts +43 -3
  49. package/entry.ts +132 -0
  50. package/fs-helpers.ts +93 -458
  51. package/import-adapters/gemini-adapter.ts +38 -183
  52. package/index.ts +912 -2917
  53. package/memory-runtime.ts +723 -0
  54. package/native-memory.ts +196 -0
  55. package/onboarding-cli.ts +3 -9
  56. package/openclaw.plugin.json +5 -17
  57. package/package.json +6 -3
  58. package/pair-cli-relay.ts +0 -9
  59. package/pair-cli.ts +1 -1
  60. package/pair-crypto.ts +41 -483
  61. package/pair-http.ts +194 -5
  62. package/postinstall.mjs +138 -0
  63. package/relay.ts +172 -0
  64. package/reranker.ts +13 -8
  65. package/semantic-dedup.ts +5 -6
  66. package/skill-register.ts +146 -0
  67. package/skill.json +1 -1
  68. package/subgraph-search.ts +3 -1
  69. package/subgraph-store.ts +367 -307
  70. package/tool-gating.ts +39 -26
  71. package/tools.ts +499 -0
  72. package/tr-cli-export-helper.ts +3 -3
  73. package/tr-cli.ts +69 -182
  74. package/trajectory-poller.ts +162 -10
  75. package/vault-crypto.ts +705 -0
  76. package/auto-pair-on-load.ts +0 -308
  77. package/dist/auto-pair-on-load.js +0 -197
  78. package/dist/pair-pending-injection.js +0 -125
  79. package/pair-pending-injection.ts +0 -205
@@ -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 POST to /v1/store with an on-chain transaction
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, UserOp hashing, and
9
- * ECDSA signing. Raw fetch() for all JSON-RPC calls to the relay bundler
10
- * and chain RPCs. No viem, no permissionless.
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
@@ -20,51 +21,10 @@ function getWasm() {
20
21
  _wasm = requireWasm('@totalreclaw/core');
21
22
  return _wasm;
22
23
  }
23
- import { CONFIG } from './config.js';
24
+ import { CONFIG, getDataEdgeAddressOverride } from './config.js';
24
25
  import { buildRelayHeaders } from './relay-headers.js';
25
- // ---------------------------------------------------------------------------
26
- // Pimlico 429 retry helper
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
- return 'https://sepolia.base.org';
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
- const resolvedChainId = chainId ?? 84532;
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 response = await fetch(rpcUrl, {
145
- method: 'POST',
109
+ const json = await rpcRequest({
110
+ url: rpcUrl,
146
111
  headers: { 'Content-Type': 'application/json' },
147
- body: JSON.stringify({
148
- jsonrpc: '2.0',
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 (with session cache)
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
- * Session-level cache for account deployment status.
169
- * Once an account is deployed (first successful UserOp), we skip the
170
- * eth_getCode check and omit factory/factoryData for all subsequent calls.
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
- const deployedAccounts = new Set();
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` and `factoryData`
240
- * in the UserOp so the EntryPoint can deploy them during the first transaction.
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
- // Session cache: if we already deployed this account, skip the RPC check
244
- if (deployedAccounts.has(sender.toLowerCase())) {
245
- return { factory: null, factoryData: null };
246
- }
247
- // Check if the Smart Account contract is deployed
248
- const codeResp = await fetch(rpcUrl, {
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
- body: JSON.stringify({
252
- jsonrpc: '2.0', id: 1, method: 'eth_getCode',
253
- params: [sender, 'latest'],
254
- }),
290
+ method: 'eth_getCode',
291
+ params: [sender, 'latest'],
255
292
  });
256
- const codeJson = await codeResp.json();
257
- const isDeployed = codeJson.result && codeJson.result !== '0x' && codeJson.result !== '0x0';
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 raw fetch)
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 raw fetch() to the relay bundler endpoint.
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,18 +356,25 @@ 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
- // 2. Encode calldata (SimpleAccount.execute → DataEdge fallback)
316
- const calldataBytes = getWasm().encodeSingleCall(protobufPayload);
362
+ // 2. Encode calldata (SimpleAccount.execute → DataEdge).
363
+ // Target the resolved DataEdge address (env → relay billing → WASM default,
364
+ // per getSubgraphConfig / #462) so writes land on the SAME contract the
365
+ // relay reads from. The legacy `encodeSingleCall` bakes the PROD DataEdge,
366
+ // which stranded staging writes on prod (#460). Guard: an empty address
367
+ // (config built without getSubgraphConfig) falls back to the legacy encoder
368
+ // — behavior-identical to before, and avoids `encodeSingleCallTo` throwing
369
+ // on a bad address.
370
+ const calldataBytes = config.dataEdgeAddress
371
+ ? getWasm().encodeSingleCallTo(protobufPayload, config.dataEdgeAddress)
372
+ : getWasm().encodeSingleCall(protobufPayload);
317
373
  const callData = `0x${Buffer.from(calldataBytes).toString('hex')}`;
318
374
  // 3. Get gas prices from Pimlico
319
375
  const gasPrices = await rpc('pimlico_getUserOperationGasPrice', []);
320
376
  const fast = gasPrices.fast;
321
377
  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
378
  // 5. Get nonce from EntryPoint via bundler RPC.
325
379
  // Routing through the bundler lets Pimlico account for pending mempool
326
380
  // UserOps, preventing AA25 nonce conflicts on rapid submissions.
@@ -330,85 +384,51 @@ async function submitFactOnChainLocked(protobufPayload, config, eoa, sender) {
330
384
  const senderPadded = sender.slice(2).toLowerCase().padStart(64, '0');
331
385
  const keyPadded = '0'.repeat(64);
332
386
  const nonceCalldata = `0x35567e1a${senderPadded}${keyPadded}`;
333
- let nonce;
334
- try {
335
- const nonceResult = await rpc('eth_call', [{ to: entryPoint, data: nonceCalldata }, 'latest']);
336
- nonce = nonceResult || '0x0';
337
- }
338
- catch {
339
- // Fallback to public RPC if bundler doesn't support eth_call
340
- const nonceResp = await fetch(rpcUrl, {
341
- method: 'POST',
342
- headers: { 'Content-Type': 'application/json' },
343
- body: JSON.stringify({
344
- jsonrpc: '2.0', id: 1, method: 'eth_call',
387
+ // Helper to fetch nonce (with bundler fallback)
388
+ async function fetchNonce() {
389
+ try {
390
+ const nonceResult = await rpc('eth_call', [{ to: entryPoint, data: nonceCalldata }, 'latest']);
391
+ return nonceResult || '0x0';
392
+ }
393
+ catch {
394
+ // Fallback to public RPC if bundler doesn't support eth_call
395
+ const nonceJson = await rpcRequest({
396
+ url: rpcUrl,
397
+ headers: { 'Content-Type': 'application/json' },
398
+ method: 'eth_call',
345
399
  params: [{ to: entryPoint, data: nonceCalldata }, 'latest'],
346
- }),
347
- });
348
- const nonceJson = await nonceResp.json();
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;
400
+ });
401
+ return nonceJson.result || '0x0';
402
+ }
366
403
  }
367
- // 7. Get paymaster sponsorship (fills gas limits + paymaster fields)
368
- const sponsorResult = await rpc('pm_sponsorUserOperation', [unsignedOp, entryPoint]);
369
- Object.assign(unsignedOp, sponsorResult);
370
- // 8. Hash and sign the UserOp via WASM
371
- const opJson = JSON.stringify(unsignedOp);
372
- const hashHex = getWasm().hashUserOp(opJson, entryPoint, BigInt(config.chainId));
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)
404
+ // Track force-deployed senders for AA10 retry (local to this submission attempt)
405
+ const forceDeployed = new Set();
406
+ // Single retry loop: getInitCode → build UserOp → sponsor → sign → send
407
+ // On AA10 "sender already constructed", mark sender as force-deployed and retry.
408
+ // AA10 can occur at pm_sponsorUserOperation (initCode present on deployed sender)
409
+ // or eth_sendUserOperation (same root cause). This loop handles both.
376
410
  let userOpHash;
377
- try {
378
- userOpHash = await rpc('eth_sendUserOperation', [unsignedOp, entryPoint]);
379
- }
380
- catch (err) {
381
- const msg = err?.message || '';
382
- if (/AA25|AA10|invalid account nonce|already being processed/i.test(msg)) {
383
- console.error('AA25/AA10 nonce conflict detected, rebuilding UserOp with fresh nonce...');
384
- // Bust deployment cache so getInitCode re-checks on-chain
385
- deployedAccounts.delete(sender.toLowerCase());
386
- // Wait for previous UserOp to mine before retrying with fresh nonce.
387
- // Public RPC won't reflect the new nonce until the tx is on-chain.
388
- await new Promise(r => setTimeout(r, 15000));
389
- // Re-fetch initCode and nonce
390
- const { factory: retryFactory, factoryData: retryFactoryData } = await getInitCode(sender, eoa.address, rpcUrl);
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';
411
+ let lastErr;
412
+ let attempt = 0;
413
+ const maxAttempts = 2;
414
+ while (attempt < maxAttempts) {
415
+ attempt++;
416
+ try {
417
+ // 4. Check if Smart Account is deployed (needed for factory/factoryData)
418
+ // If force-deployed, skip eth_getCode and return null initCode.
419
+ let factory = null;
420
+ let factoryData = null;
421
+ if (!forceDeployed.has(sender.toLowerCase())) {
422
+ const initCode = await getInitCode(sender, eoa.address, rpcUrl);
423
+ factory = initCode.factory;
424
+ factoryData = initCode.factoryData;
407
425
  }
408
- // Rebuild unsigned UserOp with fresh nonce and initCode
409
- const retryOp = {
426
+ // Fetch fresh nonce for each attempt
427
+ const nonce = await fetchNonce();
428
+ // 6. Build unsigned UserOp (v0.7 fields, camelCase for Rust JSON serde)
429
+ const unsignedOp = {
410
430
  sender,
411
- nonce: retryNonce,
431
+ nonce,
412
432
  callData,
413
433
  callGasLimit: '0x0',
414
434
  verificationGasLimit: '0x0',
@@ -417,23 +437,53 @@ async function submitFactOnChainLocked(protobufPayload, config, eoa, sender) {
417
437
  maxPriorityFeePerGas: fast.maxPriorityFeePerGas,
418
438
  signature: DUMMY_SIGNATURE,
419
439
  };
420
- if (retryFactory) {
421
- retryOp.factory = retryFactory;
422
- retryOp.factoryData = retryFactoryData;
440
+ if (factory) {
441
+ unsignedOp.factory = factory;
442
+ unsignedOp.factoryData = factoryData;
423
443
  }
424
- // Re-sponsor and re-sign
425
- const retrySponsor = await rpc('pm_sponsorUserOperation', [retryOp, entryPoint]);
426
- Object.assign(retryOp, retrySponsor);
427
- const retryOpJson = JSON.stringify(retryOp);
428
- const retryHashHex = getWasm().hashUserOp(retryOpJson, entryPoint, BigInt(config.chainId));
429
- const retrySigHex = getWasm().signUserOp(retryHashHex, eoa.private_key);
430
- retryOp.signature = `0x${retrySigHex}`;
431
- userOpHash = await rpc('eth_sendUserOperation', [retryOp, entryPoint]);
444
+ // 7. Get paymaster sponsorship (fills gas limits + paymaster fields)
445
+ // This is where AA10 "sender already constructed" can occur if initCode
446
+ // is present but the sender is already deployed.
447
+ const sponsorResult = await rpc('pm_sponsorUserOperation', [unsignedOp, entryPoint]);
448
+ Object.assign(unsignedOp, sponsorResult);
449
+ // 8. Hash and sign the UserOp via WASM
450
+ const opJson = JSON.stringify(unsignedOp);
451
+ const hashHex = getWasm().hashUserOp(opJson, entryPoint, BigInt(config.chainId));
452
+ const sigHex = signUserOp(hashHex, eoa.private_key);
453
+ unsignedOp.signature = `0x${sigHex}`;
454
+ // 9. Submit the signed UserOp
455
+ userOpHash = await rpc('eth_sendUserOperation', [unsignedOp, entryPoint]);
456
+ // Success — break out of retry loop
457
+ break;
432
458
  }
433
- else {
459
+ catch (err) {
460
+ const msg = err?.message || '';
461
+ // AA10 "sender already constructed" or AA25 invalid nonce → retry
462
+ if (/AA25|AA10|invalid account nonce|already being processed/i.test(msg)) {
463
+ lastErr = err;
464
+ console.error(`AA25/AA10 detected (attempt ${attempt}/${maxAttempts}), retrying...`);
465
+ // On AA10, force-mark sender as deployed so next retry omits initCode
466
+ if (/AA10/i.test(msg)) {
467
+ forceDeployed.add(sender.toLowerCase());
468
+ console.error('AA10: force-marking sender as deployed, retrying without initCode');
469
+ }
470
+ // Wait for previous UserOp to mine before retrying with fresh nonce.
471
+ // Public RPC won't reflect the new nonce until the tx is on-chain.
472
+ await new Promise(r => setTimeout(r, 15000));
473
+ // Continue to next iteration of retry loop
474
+ continue;
475
+ }
476
+ // Not a retryable error — re-throw
434
477
  throw err;
435
478
  }
436
479
  }
480
+ // Retry budget exhausted with no successful submission — throw the last
481
+ // retryable error instead of falling through to a receipt poll against an
482
+ // undefined userOpHash (which used to burn 120s and surface a misleading
483
+ // 'submission failed (tx=…)'). See #402.
484
+ if (userOpHash == null) {
485
+ throw lastErr ?? new Error('eth_sendUserOperation returned no result');
486
+ }
437
487
  // 10. Wait for receipt (poll up to 120s)
438
488
  let receipt = null;
439
489
  for (let i = 0; i < 60; i++) {
@@ -446,10 +496,10 @@ async function submitFactOnChainLocked(protobufPayload, config, eoa, sender) {
446
496
  catch { /* not mined yet */ }
447
497
  }
448
498
  const success = receipt?.success ?? false;
449
- // Mark account as deployed after first successful submission
450
- if (success) {
451
- deployedAccounts.add(sender.toLowerCase());
452
- }
499
+ // No session-deployment cache to update getInitCode always re-checks
500
+ // eth_getCode on the next submission, so a successful receipt needs no
501
+ // bookkeeping here. (Previous cache removed in the AA10 fix — see note
502
+ // at the top of this section.)
453
503
  return {
454
504
  txHash: receipt?.receipt?.transactionHash || '',
455
505
  userOpHash,
@@ -497,119 +547,72 @@ async function submitFactBatchOnChainLocked(protobufPayloads, config, eoa, sende
497
547
  const headers = buildRelayHeaders(overrides);
498
548
  // Helper for JSON-RPC calls to relay bundler (with 429 retry)
499
549
  async function rpc(method, params) {
500
- return rpcWithRetry(bundlerUrl, headers, method, params);
550
+ return rpcWithRetry({ url: bundlerUrl, headers, method, params });
501
551
  }
502
552
  const entryPoint = config.entryPointAddress || getWasm().getEntryPointAddress();
503
- // Encode batch calldata (SimpleAccount.executeBatch)
504
- // encodeBatchCall expects a JSON array of hex-encoded payload strings
553
+ // Encode batch calldata (SimpleAccount.executeBatch).
554
+ // encodeBatchCall{,To} expect a JSON array of hex-encoded payload strings.
555
+ // Target the resolved DataEdge address (env → relay billing → WASM default,
556
+ // per getSubgraphConfig / #462) so writes land on the SAME contract the
557
+ // relay reads from — the legacy `encodeBatchCall` bakes the PROD DataEdge and
558
+ // stranded staging writes on prod (#460). Guard: empty address → legacy
559
+ // encoder (behavior-identical; avoids `encodeBatchCallTo` throwing).
505
560
  const payloadsHex = protobufPayloads.map(p => p.toString('hex'));
506
- const calldataBytes = getWasm().encodeBatchCall(JSON.stringify(payloadsHex));
561
+ const payloadsJson = JSON.stringify(payloadsHex);
562
+ const calldataBytes = config.dataEdgeAddress
563
+ ? getWasm().encodeBatchCallTo(payloadsJson, config.dataEdgeAddress)
564
+ : getWasm().encodeBatchCall(payloadsJson);
507
565
  const callData = `0x${Buffer.from(calldataBytes).toString('hex')}`;
508
566
  // Get gas prices
509
567
  const gasPrices = await rpc('pimlico_getUserOperationGasPrice', []);
510
568
  const fast = gasPrices.fast;
511
569
  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
570
  // Get nonce via bundler (accounts for pending mempool UserOps) with public RPC fallback
515
571
  const senderPadded = sender.slice(2).toLowerCase().padStart(64, '0');
516
572
  const keyPadded = '0'.repeat(64);
517
573
  const nonceCalldata = `0x35567e1a${senderPadded}${keyPadded}`;
518
- let nonce;
519
- try {
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) {
574
+ // Helper to fetch nonce (with bundler fallback)
575
+ async function fetchNonce() {
555
576
  try {
556
- const gasEstimate = await rpc('eth_estimateUserOperationGas', [unsignedOp, entryPoint]);
557
- if (gasEstimate.callGasLimit)
558
- unsignedOp.callGasLimit = gasEstimate.callGasLimit;
559
- if (gasEstimate.verificationGasLimit)
560
- unsignedOp.verificationGasLimit = gasEstimate.verificationGasLimit;
561
- if (gasEstimate.preVerificationGas)
562
- unsignedOp.preVerificationGas = gasEstimate.preVerificationGas;
577
+ const nonceResult = await rpc('eth_call', [{ to: entryPoint, data: nonceCalldata }, 'latest']);
578
+ return nonceResult || '0x0';
563
579
  }
564
580
  catch {
565
- // If estimation fails, let the paymaster handle it (default behavior)
581
+ const nonceJson = await rpcRequest({
582
+ url: rpcUrl,
583
+ headers: { 'Content-Type': 'application/json' },
584
+ method: 'eth_call',
585
+ params: [{ to: entryPoint, data: nonceCalldata }, 'latest'],
586
+ });
587
+ return nonceJson.result || '0x0';
566
588
  }
567
589
  }
568
- // Paymaster sponsorship (uses gas limits from estimation above for batches)
569
- const sponsorResult = await rpc('pm_sponsorUserOperation', [unsignedOp, entryPoint]);
570
- Object.assign(unsignedOp, sponsorResult);
571
- // Hash and sign via WASM
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)
590
+ // Track force-deployed senders for AA10 retry (local to this submission attempt)
591
+ const forceDeployed = new Set();
592
+ // Single retry loop: getInitCode → build UserOp → estimate → sponsor → sign → send
593
+ // On AA10 "sender already constructed", mark sender as force-deployed and retry.
577
594
  let userOpHash;
578
- try {
579
- userOpHash = await rpc('eth_sendUserOperation', [unsignedOp, entryPoint]);
580
- }
581
- catch (err) {
582
- const msg = err?.message || '';
583
- if (/AA25|AA10|invalid account nonce|already being processed/i.test(msg)) {
584
- console.error('AA25/AA10 nonce conflict detected (batch), rebuilding UserOp with fresh nonce...');
585
- // Bust deployment cache so getInitCode re-checks on-chain
586
- deployedAccounts.delete(sender.toLowerCase());
587
- // Wait for previous UserOp to mine before retrying with fresh nonce.
588
- // Public RPC won't reflect the new nonce until the tx is on-chain.
589
- await new Promise(r => setTimeout(r, 15000));
590
- // Re-fetch initCode and nonce
591
- const { factory: retryFactory, factoryData: retryFactoryData } = await getInitCode(sender, eoa.address, rpcUrl);
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';
595
+ let lastErr;
596
+ let attempt = 0;
597
+ const maxAttempts = 2;
598
+ while (attempt < maxAttempts) {
599
+ attempt++;
600
+ try {
601
+ // Check if Smart Account is deployed (needed for factory/factoryData)
602
+ // If force-deployed, skip eth_getCode and return null initCode.
603
+ let factory = null;
604
+ let factoryData = null;
605
+ if (!forceDeployed.has(sender.toLowerCase())) {
606
+ const initCode = await getInitCode(sender, eoa.address, rpcUrl);
607
+ factory = initCode.factory;
608
+ factoryData = initCode.factoryData;
608
609
  }
609
- // Rebuild unsigned UserOp with fresh nonce and initCode
610
- const retryOp = {
610
+ // Fetch fresh nonce for each attempt
611
+ const nonce = await fetchNonce();
612
+ // Build unsigned UserOp
613
+ const unsignedOp = {
611
614
  sender,
612
- nonce: retryNonce,
615
+ nonce,
613
616
  callData,
614
617
  callGasLimit: '0x0',
615
618
  verificationGasLimit: '0x0',
@@ -618,23 +621,70 @@ async function submitFactBatchOnChainLocked(protobufPayloads, config, eoa, sende
618
621
  maxPriorityFeePerGas: fast.maxPriorityFeePerGas,
619
622
  signature: DUMMY_SIGNATURE,
620
623
  };
621
- if (retryFactory) {
622
- retryOp.factory = retryFactory;
623
- retryOp.factoryData = retryFactoryData;
624
+ if (factory) {
625
+ unsignedOp.factory = factory;
626
+ unsignedOp.factoryData = factoryData;
627
+ }
628
+ // Gas estimation for batch operations — get accurate gas limits from Pimlico
629
+ // before paymaster sponsorship (can't bump after sponsorship as it invalidates
630
+ // the paymaster's signature, causing AA34).
631
+ if (protobufPayloads.length > 1) {
632
+ try {
633
+ const gasEstimate = await rpc('eth_estimateUserOperationGas', [unsignedOp, entryPoint]);
634
+ if (gasEstimate.callGasLimit)
635
+ unsignedOp.callGasLimit = gasEstimate.callGasLimit;
636
+ if (gasEstimate.verificationGasLimit)
637
+ unsignedOp.verificationGasLimit = gasEstimate.verificationGasLimit;
638
+ if (gasEstimate.preVerificationGas)
639
+ unsignedOp.preVerificationGas = gasEstimate.preVerificationGas;
640
+ }
641
+ catch {
642
+ // If estimation fails, let the paymaster handle it (default behavior)
643
+ }
624
644
  }
625
- // Re-sponsor and re-sign
626
- const retrySponsor = await rpc('pm_sponsorUserOperation', [retryOp, entryPoint]);
627
- Object.assign(retryOp, retrySponsor);
628
- const retryOpJson = JSON.stringify(retryOp);
629
- const retryHashHex = getWasm().hashUserOp(retryOpJson, entryPoint, BigInt(config.chainId));
630
- const retrySigHex = getWasm().signUserOp(retryHashHex, eoa.private_key);
631
- retryOp.signature = `0x${retrySigHex}`;
632
- userOpHash = await rpc('eth_sendUserOperation', [retryOp, entryPoint]);
645
+ // Paymaster sponsorship (uses gas limits from estimation above for batches)
646
+ // This is where AA10 "sender already constructed" can occur if initCode
647
+ // is present but the sender is already deployed.
648
+ const sponsorResult = await rpc('pm_sponsorUserOperation', [unsignedOp, entryPoint]);
649
+ Object.assign(unsignedOp, sponsorResult);
650
+ // Hash and sign via WASM
651
+ const opJson = JSON.stringify(unsignedOp);
652
+ const hashHex = getWasm().hashUserOp(opJson, entryPoint, BigInt(config.chainId));
653
+ const sigHex = signUserOp(hashHex, eoa.private_key);
654
+ unsignedOp.signature = `0x${sigHex}`;
655
+ // Submit the signed UserOp
656
+ userOpHash = await rpc('eth_sendUserOperation', [unsignedOp, entryPoint]);
657
+ // Success — break out of retry loop
658
+ break;
633
659
  }
634
- else {
660
+ catch (err) {
661
+ const msg = err?.message || '';
662
+ // AA10 "sender already constructed" or AA25 invalid nonce → retry
663
+ if (/AA25|AA10|invalid account nonce|already being processed/i.test(msg)) {
664
+ lastErr = err;
665
+ console.error(`AA25/AA10 detected (batch, attempt ${attempt}/${maxAttempts}), retrying...`);
666
+ // On AA10, force-mark sender as deployed so next retry omits initCode
667
+ if (/AA10/i.test(msg)) {
668
+ forceDeployed.add(sender.toLowerCase());
669
+ console.error('AA10: force-marking sender as deployed, retrying without initCode');
670
+ }
671
+ // Wait for previous UserOp to mine before retrying with fresh nonce.
672
+ // Public RPC won't reflect the new nonce until the tx is on-chain.
673
+ await new Promise(r => setTimeout(r, 15000));
674
+ // Continue to next iteration of retry loop
675
+ continue;
676
+ }
677
+ // Not a retryable error — re-throw
635
678
  throw err;
636
679
  }
637
680
  }
681
+ // Retry budget exhausted with no successful submission — throw the last
682
+ // retryable error instead of polling for a receipt against an undefined
683
+ // userOpHash (which used to burn 120s and surface a misleading
684
+ // 'submission failed (tx=…)'). See #402.
685
+ if (userOpHash == null) {
686
+ throw lastErr ?? new Error('eth_sendUserOperation returned no result');
687
+ }
638
688
  // Wait for receipt (poll up to 120s)
639
689
  let receipt = null;
640
690
  for (let i = 0; i < 60; i++) {
@@ -647,10 +697,10 @@ async function submitFactBatchOnChainLocked(protobufPayloads, config, eoa, sende
647
697
  catch { /* not mined yet */ }
648
698
  }
649
699
  const batchSuccess = receipt?.success ?? false;
650
- // Mark account as deployed after first successful submission
651
- if (batchSuccess) {
652
- deployedAccounts.add(sender.toLowerCase());
653
- }
700
+ // No session-deployment cache to update getInitCode always re-checks
701
+ // eth_getCode on the next submission, so a successful receipt needs no
702
+ // bookkeeping here. (Previous cache removed in the AA10 fix — see note
703
+ // at the top of the Smart Account deployment-check section.)
654
704
  return {
655
705
  txHash: receipt?.receipt?.transactionHash || '',
656
706
  userOpHash,
@@ -679,7 +729,10 @@ export function isSubgraphMode() {
679
729
  * - TOTALRECLAW_SELF_HOSTED -- set "true" to use self-hosted server (default: managed service)
680
730
  *
681
731
  * Chain ID is no longer configurable via env — it is auto-detected from the
682
- * relay billing response (free = Base Sepolia, Pro = Gnosis mainnet).
732
+ * relay billing response (post-ops-1 both tiers are on Gnosis mainnet, chain
733
+ * 100). The DataEdge contract address is likewise consumed from the relay's
734
+ * authoritative `data_edge_address` (env override still wins, WASM default is
735
+ * the final fallback — see the `dataEdgeAddress` resolution below, #460).
683
736
  */
684
737
  export function getSubgraphConfig() {
685
738
  return {
@@ -688,7 +741,12 @@ export function getSubgraphConfig() {
688
741
  mnemonic: CONFIG.recoveryPhrase,
689
742
  cachePath: CONFIG.cachePath,
690
743
  chainId: CONFIG.chainId,
691
- dataEdgeAddress: CONFIG.dataEdgeAddress || getWasm().getDataEdgeAddress(),
744
+ // Resolution order (#460): explicit operator env override → relay billing
745
+ // `data_edge_address` (verbatim) → WASM-baked default. The relay routes
746
+ // each environment to its own DataEdge (staging is on-chain isolated), so
747
+ // consuming its value keeps writes + reads on the same contract; without
748
+ // the middle term staging writes mined on the prod default → empty recall.
749
+ dataEdgeAddress: CONFIG.dataEdgeAddress || getDataEdgeAddressOverride() || getWasm().getDataEdgeAddress(),
692
750
  entryPointAddress: CONFIG.entryPointAddress || getWasm().getEntryPointAddress(),
693
751
  rpcUrl: CONFIG.rpcUrl || undefined,
694
752
  };