@totalreclaw/totalreclaw 1.6.0 → 3.0.6
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/CLAWHUB.md +134 -0
- package/README.md +407 -64
- package/SKILL.md +1032 -0
- package/api-client.ts +5 -5
- package/claims-helper.ts +686 -0
- package/config.ts +211 -0
- package/consolidation.ts +141 -33
- package/contradiction-sync.ts +1389 -0
- package/crypto.ts +63 -261
- package/digest-sync.ts +516 -0
- package/embedding.ts +69 -46
- package/extractor.ts +1307 -84
- package/hot-cache-wrapper.ts +1 -1
- package/import-adapters/gemini-adapter.ts +243 -0
- package/import-adapters/index.ts +3 -0
- package/import-adapters/types.ts +1 -1
- package/index.ts +1887 -323
- package/llm-client.ts +106 -53
- package/lsh.ts +21 -210
- package/package.json +20 -7
- package/pin.ts +502 -0
- package/reranker.ts +96 -124
- package/skill.json +213 -0
- package/subgraph-search.ts +112 -5
- package/subgraph-store.ts +559 -275
- package/consolidation.test.ts +0 -356
- package/extractor-dedup.test.ts +0 -168
- package/import-adapters/import-adapters.test.ts +0 -1123
- package/lsh.test.ts +0 -463
- package/pocv2-e2e-test.ts +0 -917
- package/porter-stemmer.d.ts +0 -4
- package/reranker.test.ts +0 -594
- package/semantic-dedup.test.ts +0 -392
- package/setup.sh +0 -19
- package/store-dedup-wiring.test.ts +0 -186
package/subgraph-store.ts
CHANGED
|
@@ -5,34 +5,83 @@
|
|
|
5
5
|
* "true"). Replaces the HTTP POST to /v1/store with an on-chain transaction
|
|
6
6
|
* flow.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
import {
|
|
13
|
+
// Lazy-load WASM to avoid crash when npm install hasn't finished yet.
|
|
14
|
+
let _wasm: typeof import('@totalreclaw/core') | null = null;
|
|
15
|
+
function getWasm() {
|
|
16
|
+
if (!_wasm) _wasm = require('@totalreclaw/core');
|
|
17
|
+
return _wasm;
|
|
18
|
+
}
|
|
19
|
+
import { CONFIG } from './config.js';
|
|
20
20
|
|
|
21
21
|
// ---------------------------------------------------------------------------
|
|
22
|
-
//
|
|
22
|
+
// Pimlico 429 retry helper
|
|
23
23
|
// ---------------------------------------------------------------------------
|
|
24
24
|
|
|
25
|
-
/**
|
|
26
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Wrap a fetch-based JSON-RPC call with exponential backoff for HTTP 429
|
|
27
|
+
* (rate limit) responses from Pimlico. Max 5 retries with 5s base delay,
|
|
28
|
+
* doubling each attempt, capped at 60s, plus random jitter (0-1000ms).
|
|
29
|
+
* Total retry window: ~135s (5+10+20+40+60 plus jitter).
|
|
30
|
+
* All other HTTP errors throw immediately.
|
|
31
|
+
*/
|
|
32
|
+
async function rpcWithRetry(
|
|
33
|
+
url: string,
|
|
34
|
+
headers: Record<string, string>,
|
|
35
|
+
method: string,
|
|
36
|
+
params: unknown[],
|
|
37
|
+
): Promise<any> {
|
|
38
|
+
const maxRetries = 5;
|
|
39
|
+
const baseDelay = 5000; // 5 seconds
|
|
40
|
+
const maxDelay = 60_000; // 60 seconds cap
|
|
41
|
+
const body = JSON.stringify({ jsonrpc: '2.0', id: 1, method, params });
|
|
42
|
+
|
|
43
|
+
for (let attempt = 1; attempt <= maxRetries + 1; attempt++) {
|
|
44
|
+
const resp = await fetch(url, { method: 'POST', headers, body });
|
|
45
|
+
|
|
46
|
+
if (resp.ok) {
|
|
47
|
+
const json = await resp.json() as { result?: any; error?: { message: string } };
|
|
48
|
+
if (json.error) {
|
|
49
|
+
// Check if the RPC-level error message indicates a rate limit
|
|
50
|
+
if (attempt <= maxRetries && /429|rate limit/i.test(json.error.message)) {
|
|
51
|
+
const delay = Math.min(Math.pow(2, attempt - 1) * baseDelay, maxDelay) + Math.floor(Math.random() * 1000);
|
|
52
|
+
console.error(`Pimlico rate limited, retrying in ${delay}ms (attempt ${attempt}/${maxRetries})...`);
|
|
53
|
+
await new Promise(r => setTimeout(r, delay));
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
throw new Error(`RPC ${method}: ${json.error.message}`);
|
|
57
|
+
}
|
|
58
|
+
return json.result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// HTTP-level 429 — retry with backoff
|
|
62
|
+
if (resp.status === 429 && attempt <= maxRetries) {
|
|
63
|
+
const delay = Math.min(Math.pow(2, attempt - 1) * baseDelay, maxDelay) + Math.floor(Math.random() * 1000);
|
|
64
|
+
console.error(`Pimlico rate limited, retrying in ${delay}ms (attempt ${attempt}/${maxRetries})...`);
|
|
65
|
+
await new Promise(r => setTimeout(r, delay));
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
throw new Error(`Relay returned HTTP ${resp.status} for ${method}`);
|
|
70
|
+
}
|
|
27
71
|
|
|
28
|
-
|
|
29
|
-
|
|
72
|
+
// Should not be reached, but satisfies TypeScript
|
|
73
|
+
throw new Error(`RPC ${method}: max retries exceeded`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Types
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
30
79
|
|
|
31
80
|
export interface SubgraphStoreConfig {
|
|
32
81
|
relayUrl: string; // TotalReclaw relay server URL (proxies bundler + subgraph)
|
|
33
82
|
mnemonic: string; // BIP-39 mnemonic for key derivation
|
|
34
83
|
cachePath: string; // Hot cache file path
|
|
35
|
-
chainId: number; // 100 for Gnosis mainnet,
|
|
84
|
+
chainId: number; // 100 for Gnosis mainnet, 84532 for Base Sepolia
|
|
36
85
|
dataEdgeAddress: string; // EventfulDataEdge contract address
|
|
37
86
|
entryPointAddress: string; // ERC-4337 EntryPoint v0.7
|
|
38
87
|
authKeyHex?: string; // HKDF auth key for relay server Authorization header
|
|
@@ -44,141 +93,203 @@ export interface FactPayload {
|
|
|
44
93
|
id: string;
|
|
45
94
|
timestamp: string;
|
|
46
95
|
owner: string; // Smart Account address (hex)
|
|
47
|
-
encryptedBlob: string; // Hex-encoded
|
|
96
|
+
encryptedBlob: string; // Hex-encoded XChaCha20-Poly1305 ciphertext
|
|
48
97
|
blindIndices: string[]; // SHA-256 hashes (word + LSH)
|
|
49
98
|
decayScore: number;
|
|
50
99
|
source: string;
|
|
51
100
|
contentFp: string;
|
|
52
101
|
agentId: string;
|
|
53
102
|
encryptedEmbedding?: string;
|
|
103
|
+
/**
|
|
104
|
+
* Outer protobuf schema version. Plugin v3.0.0 writes Memory Taxonomy v1
|
|
105
|
+
* JSON inner blobs, so this defaults to `PROTOBUF_VERSION_V4` (4). Omitting
|
|
106
|
+
* the field (or passing 0) yields the legacy `DEFAULT_PROTOBUF_VERSION`
|
|
107
|
+
* (3), which is retained so tombstone rows stay wire-compatible with
|
|
108
|
+
* pre-v3 readers if ever needed.
|
|
109
|
+
*/
|
|
110
|
+
version?: number;
|
|
54
111
|
}
|
|
55
112
|
|
|
113
|
+
/** Legacy protobuf wrapper schema version (v0/v1-binary inner blob). */
|
|
114
|
+
export const PROTOBUF_VERSION_LEGACY = 3;
|
|
115
|
+
|
|
116
|
+
/** Memory Taxonomy v1 protobuf wrapper schema version. */
|
|
117
|
+
export const PROTOBUF_VERSION_V4 = 4;
|
|
118
|
+
|
|
119
|
+
// Stub 65-byte signature for gas estimation (pm_sponsorUserOperation).
|
|
120
|
+
// Must be a structurally valid ECDSA signature (r,s,v) so that ecrecover does
|
|
121
|
+
// NOT revert inside SimpleAccount._validateSignature. All-zeros causes
|
|
122
|
+
// OpenZeppelin ECDSA.recover() to revert with ECDSAInvalidSignature() (0xf645eedf),
|
|
123
|
+
// which the EntryPoint surfaces as AA23.
|
|
124
|
+
// This matches the stub used by permissionless/viem — ecrecover returns a
|
|
125
|
+
// non-owner address, so validateUserOp returns SIG_VALIDATION_FAILED (1)
|
|
126
|
+
// instead of reverting, which is what bundlers expect during simulation.
|
|
127
|
+
const DUMMY_SIGNATURE =
|
|
128
|
+
'0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c';
|
|
129
|
+
|
|
56
130
|
// ---------------------------------------------------------------------------
|
|
57
|
-
// Protobuf encoding (
|
|
131
|
+
// Protobuf encoding (WASM)
|
|
58
132
|
// ---------------------------------------------------------------------------
|
|
59
133
|
|
|
60
134
|
/**
|
|
61
|
-
* Encode a fact payload as a minimal Protobuf wire format.
|
|
135
|
+
* Encode a fact payload as a minimal Protobuf wire format via WASM core.
|
|
62
136
|
*
|
|
63
|
-
* Field numbers match server/proto/totalreclaw.proto
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
137
|
+
* Field numbers match server/proto/totalreclaw.proto.
|
|
138
|
+
*
|
|
139
|
+
* As of plugin v3.0.0 the outer protobuf `version` field is written as 4
|
|
140
|
+
* when the caller passes `version: PROTOBUF_VERSION_V4`. Omitting the field
|
|
141
|
+
* preserves legacy v3 semantics (e.g. for tombstone tombstone rows that
|
|
142
|
+
* should round-trip through pre-v3 readers).
|
|
69
143
|
*/
|
|
70
144
|
export function encodeFactProtobuf(fact: FactPayload): Buffer {
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const key = (fieldNumber << 3) | 2;
|
|
86
|
-
parts.push(encodeVarint(key));
|
|
87
|
-
parts.push(encodeVarint(value.length));
|
|
88
|
-
parts.push(value);
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
// Helper: encode a double field (wire type 1 = 64-bit)
|
|
92
|
-
const writeDouble = (fieldNumber: number, value: number) => {
|
|
93
|
-
const key = (fieldNumber << 3) | 1;
|
|
94
|
-
parts.push(encodeVarint(key));
|
|
95
|
-
const buf = Buffer.alloc(8);
|
|
96
|
-
buf.writeDoubleLE(value);
|
|
97
|
-
parts.push(buf);
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
// Helper: encode a varint field (wire type 0)
|
|
101
|
-
const writeVarintField = (fieldNumber: number, value: number) => {
|
|
102
|
-
const key = (fieldNumber << 3) | 0;
|
|
103
|
-
parts.push(encodeVarint(key));
|
|
104
|
-
parts.push(encodeVarint(value));
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
// Encode fields
|
|
108
|
-
writeString(1, fact.id);
|
|
109
|
-
writeString(2, fact.timestamp);
|
|
110
|
-
writeString(3, fact.owner);
|
|
111
|
-
writeBytes(4, Buffer.from(fact.encryptedBlob, 'hex'));
|
|
112
|
-
|
|
113
|
-
for (const index of fact.blindIndices) {
|
|
114
|
-
writeString(5, index);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
writeDouble(6, fact.decayScore);
|
|
118
|
-
writeVarintField(7, 1); // is_active = true
|
|
119
|
-
writeVarintField(8, 2); // version = 2
|
|
120
|
-
writeString(9, fact.source);
|
|
121
|
-
writeString(10, fact.contentFp);
|
|
122
|
-
writeString(11, fact.agentId);
|
|
123
|
-
// Field 12 (sequence_id) is assigned by the subgraph mapping, not the client
|
|
124
|
-
if (fact.encryptedEmbedding) {
|
|
125
|
-
writeString(13, fact.encryptedEmbedding);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return Buffer.concat(parts);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** Encode an integer as a Protobuf varint */
|
|
132
|
-
export function encodeVarint(value: number): Buffer {
|
|
133
|
-
const bytes: number[] = [];
|
|
134
|
-
let v = value >>> 0; // unsigned
|
|
135
|
-
while (v > 0x7f) {
|
|
136
|
-
bytes.push((v & 0x7f) | 0x80);
|
|
137
|
-
v >>>= 7;
|
|
138
|
-
}
|
|
139
|
-
bytes.push(v & 0x7f);
|
|
140
|
-
return Buffer.from(bytes);
|
|
145
|
+
const json = JSON.stringify({
|
|
146
|
+
id: fact.id,
|
|
147
|
+
timestamp: fact.timestamp,
|
|
148
|
+
owner: fact.owner,
|
|
149
|
+
encrypted_blob_hex: fact.encryptedBlob,
|
|
150
|
+
blind_indices: fact.blindIndices,
|
|
151
|
+
decay_score: fact.decayScore,
|
|
152
|
+
source: fact.source,
|
|
153
|
+
content_fp: fact.contentFp,
|
|
154
|
+
agent_id: fact.agentId,
|
|
155
|
+
encrypted_embedding: fact.encryptedEmbedding || null,
|
|
156
|
+
version: fact.version ?? PROTOBUF_VERSION_LEGACY,
|
|
157
|
+
});
|
|
158
|
+
return Buffer.from(getWasm().encodeFactProtobuf(json));
|
|
141
159
|
}
|
|
142
160
|
|
|
143
161
|
// ---------------------------------------------------------------------------
|
|
144
162
|
// Chain helpers
|
|
145
163
|
// ---------------------------------------------------------------------------
|
|
146
164
|
|
|
147
|
-
/**
|
|
148
|
-
function
|
|
165
|
+
/** Get the default public RPC URL for a chain ID */
|
|
166
|
+
function getDefaultRpcUrl(chainId: number): string {
|
|
149
167
|
switch (chainId) {
|
|
150
168
|
case 100:
|
|
151
|
-
return
|
|
152
|
-
case 10200:
|
|
153
|
-
return gnosisChiado;
|
|
169
|
+
return 'https://rpc.gnosischain.com';
|
|
154
170
|
case 84532:
|
|
155
|
-
return
|
|
171
|
+
return 'https://sepolia.base.org';
|
|
156
172
|
default:
|
|
157
|
-
return
|
|
173
|
+
return 'https://sepolia.base.org';
|
|
158
174
|
}
|
|
159
175
|
}
|
|
160
176
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// Smart Account address derivation
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Derive the Smart Account address from a BIP-39 mnemonic.
|
|
183
|
+
*
|
|
184
|
+
* Uses the SimpleAccountFactory's getAddress(owner, salt=0) view function
|
|
185
|
+
* via a raw eth_call to the chain RPC. The address is deterministic (CREATE2).
|
|
186
|
+
*/
|
|
187
|
+
export async function deriveSmartAccountAddress(mnemonic: string, chainId?: number): Promise<string> {
|
|
188
|
+
const eoa = getWasm().deriveEoa(mnemonic) as { private_key: string; address: string };
|
|
189
|
+
const resolvedChainId = chainId ?? 84532;
|
|
190
|
+
|
|
191
|
+
// SimpleAccountFactory.getAddress(address owner, uint256 salt) — view function
|
|
192
|
+
// Selector: 0x8cb84e18 = keccak256("getAddress(address,uint256)")[0:4]
|
|
193
|
+
const factoryAddress = getWasm().getSimpleAccountFactory();
|
|
194
|
+
const ownerPadded = eoa.address.slice(2).toLowerCase().padStart(64, '0');
|
|
195
|
+
const saltPadded = '0'.repeat(64);
|
|
196
|
+
const selector = '8cb84e18';
|
|
197
|
+
const calldata = `0x${selector}${ownerPadded}${saltPadded}`;
|
|
198
|
+
|
|
199
|
+
const rpcUrl = CONFIG.rpcUrl || getDefaultRpcUrl(resolvedChainId);
|
|
200
|
+
const response = await fetch(rpcUrl, {
|
|
201
|
+
method: 'POST',
|
|
202
|
+
headers: { 'Content-Type': 'application/json' },
|
|
203
|
+
body: JSON.stringify({
|
|
204
|
+
jsonrpc: '2.0',
|
|
205
|
+
id: 1,
|
|
206
|
+
method: 'eth_call',
|
|
207
|
+
params: [{ to: factoryAddress, data: calldata }, 'latest'],
|
|
208
|
+
}),
|
|
209
|
+
});
|
|
210
|
+
const json = await response.json() as { result?: string; error?: { message: string } };
|
|
211
|
+
if (json.error) {
|
|
212
|
+
throw new Error(`Failed to resolve Smart Account address: ${json.error.message}`);
|
|
213
|
+
}
|
|
214
|
+
if (!json.result || json.result === '0x') {
|
|
215
|
+
throw new Error('Failed to resolve Smart Account address: empty result');
|
|
216
|
+
}
|
|
217
|
+
// Result is a 32-byte ABI-encoded address — take last 20 bytes
|
|
218
|
+
return `0x${json.result.slice(-40)}`.toLowerCase();
|
|
164
219
|
}
|
|
165
220
|
|
|
166
221
|
// ---------------------------------------------------------------------------
|
|
167
|
-
//
|
|
222
|
+
// Smart Account deployment check (with session cache)
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Session-level cache for account deployment status.
|
|
227
|
+
* Once an account is deployed (first successful UserOp), we skip the
|
|
228
|
+
* eth_getCode check and omit factory/factoryData for all subsequent calls.
|
|
229
|
+
* This prevents AA10 "duplicate deployment" errors when multiple facts
|
|
230
|
+
* are stored in rapid succession for a first-time user.
|
|
231
|
+
*/
|
|
232
|
+
const deployedAccounts = new Set<string>();
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Check if a Smart Account is deployed and return factory/factoryData if not.
|
|
236
|
+
*
|
|
237
|
+
* For ERC-4337 v0.7, undeployed accounts need `factory` and `factoryData`
|
|
238
|
+
* in the UserOp so the EntryPoint can deploy them during the first transaction.
|
|
239
|
+
*/
|
|
240
|
+
async function getInitCode(
|
|
241
|
+
sender: string,
|
|
242
|
+
eoaAddress: string,
|
|
243
|
+
rpcUrl: string,
|
|
244
|
+
): Promise<{ factory: string | null; factoryData: string | null }> {
|
|
245
|
+
// Session cache: if we already deployed this account, skip the RPC check
|
|
246
|
+
if (deployedAccounts.has(sender.toLowerCase())) {
|
|
247
|
+
return { factory: null, factoryData: null };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Check if the Smart Account contract is deployed
|
|
251
|
+
const codeResp = await fetch(rpcUrl, {
|
|
252
|
+
method: 'POST',
|
|
253
|
+
headers: { 'Content-Type': 'application/json' },
|
|
254
|
+
body: JSON.stringify({
|
|
255
|
+
jsonrpc: '2.0', id: 1, method: 'eth_getCode',
|
|
256
|
+
params: [sender, 'latest'],
|
|
257
|
+
}),
|
|
258
|
+
});
|
|
259
|
+
const codeJson = await codeResp.json() as { result?: string };
|
|
260
|
+
const isDeployed = codeJson.result && codeJson.result !== '0x' && codeJson.result !== '0x0';
|
|
261
|
+
|
|
262
|
+
if (isDeployed) {
|
|
263
|
+
deployedAccounts.add(sender.toLowerCase());
|
|
264
|
+
return { factory: null, factoryData: null };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Account not deployed — build factory + factoryData for first-time deployment.
|
|
268
|
+
// createAccount(address owner, uint256 salt) — state-changing function
|
|
269
|
+
// Selector: 0x5fbfb9cf = keccak256("createAccount(address,uint256)")[0:4]
|
|
270
|
+
const factory = getWasm().getSimpleAccountFactory();
|
|
271
|
+
const ownerPadded = eoaAddress.slice(2).toLowerCase().padStart(64, '0');
|
|
272
|
+
const saltPadded = '0'.repeat(64);
|
|
273
|
+
const selector = '5fbfb9cf';
|
|
274
|
+
const factoryData = `0x${selector}${ownerPadded}${saltPadded}`;
|
|
275
|
+
|
|
276
|
+
return { factory, factoryData };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
// On-chain submission (ERC-4337 UserOps via raw fetch)
|
|
168
281
|
// ---------------------------------------------------------------------------
|
|
169
282
|
|
|
170
283
|
/**
|
|
171
284
|
* Submit a fact on-chain via ERC-4337 UserOp through the relay server.
|
|
172
285
|
*
|
|
173
|
-
*
|
|
174
|
-
* 1.
|
|
175
|
-
* 2.
|
|
176
|
-
* 3.
|
|
177
|
-
* 4.
|
|
178
|
-
* 5. Waits for the transaction receipt
|
|
286
|
+
* Uses @totalreclaw/core WASM for:
|
|
287
|
+
* 1. EOA derivation from mnemonic (BIP-39 + BIP-44)
|
|
288
|
+
* 2. Calldata encoding (SimpleAccount.execute)
|
|
289
|
+
* 3. UserOp hashing (ERC-4337 v0.7)
|
|
290
|
+
* 4. ECDSA signing (EIP-191 prefixed)
|
|
179
291
|
*
|
|
180
|
-
*
|
|
181
|
-
* with its own API key. Clients never need a Pimlico API key.
|
|
292
|
+
* All JSON-RPC calls go through raw fetch() to the relay bundler endpoint.
|
|
182
293
|
*/
|
|
183
294
|
export async function submitFactOnChain(
|
|
184
295
|
protobufPayload: Buffer,
|
|
@@ -192,92 +303,178 @@ export async function submitFactOnChain(
|
|
|
192
303
|
throw new Error('Mnemonic (TOTALRECLAW_RECOVERY_PHRASE) is required for on-chain submission');
|
|
193
304
|
}
|
|
194
305
|
|
|
195
|
-
const
|
|
196
|
-
const bundlerRpcUrl = getRelayBundlerUrl(config.relayUrl);
|
|
197
|
-
const dataEdgeAddress = config.dataEdgeAddress as Address;
|
|
198
|
-
const entryPointAddr = (config.entryPointAddress || entryPoint07Address) as Address;
|
|
199
|
-
|
|
200
|
-
// Build authenticated transport for relay server proxy
|
|
306
|
+
const bundlerUrl = `${config.relayUrl}/v1/bundler`;
|
|
201
307
|
const headers: Record<string, string> = {
|
|
308
|
+
'Content-Type': 'application/json',
|
|
202
309
|
'X-TotalReclaw-Client': 'openclaw-plugin',
|
|
203
310
|
};
|
|
204
311
|
if (config.authKeyHex) headers['Authorization'] = `Bearer ${config.authKeyHex}`;
|
|
205
312
|
if (config.walletAddress) headers['X-Wallet-Address'] = config.walletAddress;
|
|
206
313
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
// 1. Derive EOA signer from mnemonic (BIP-44 m/44'/60'/0'/0/0)
|
|
212
|
-
const ownerAccount = mnemonicToAccount(config.mnemonic);
|
|
314
|
+
// Helper for JSON-RPC calls to relay bundler (with 429 retry)
|
|
315
|
+
async function rpc(method: string, params: unknown[]): Promise<any> {
|
|
316
|
+
return rpcWithRetry(bundlerUrl, headers, method, params);
|
|
317
|
+
}
|
|
213
318
|
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
319
|
+
// 1. Derive EOA from mnemonic
|
|
320
|
+
const eoa = getWasm().deriveEoa(config.mnemonic) as { private_key: string; address: string };
|
|
321
|
+
const sender = config.walletAddress || await deriveSmartAccountAddress(config.mnemonic, config.chainId);
|
|
322
|
+
const entryPoint = config.entryPointAddress || getWasm().getEntryPointAddress();
|
|
323
|
+
|
|
324
|
+
// 2. Encode calldata (SimpleAccount.execute → DataEdge fallback)
|
|
325
|
+
const calldataBytes = getWasm().encodeSingleCall(protobufPayload);
|
|
326
|
+
const callData = `0x${Buffer.from(calldataBytes).toString('hex')}`;
|
|
327
|
+
|
|
328
|
+
// 3. Get gas prices from Pimlico
|
|
329
|
+
const gasPrices = await rpc('pimlico_getUserOperationGasPrice', []);
|
|
330
|
+
const fast = gasPrices.fast;
|
|
331
|
+
|
|
332
|
+
const rpcUrl = config.rpcUrl || CONFIG.rpcUrl || getDefaultRpcUrl(config.chainId);
|
|
333
|
+
|
|
334
|
+
// 4. Check if Smart Account is deployed (needed for factory/factoryData)
|
|
335
|
+
const { factory, factoryData } = await getInitCode(sender, eoa.address, rpcUrl);
|
|
336
|
+
|
|
337
|
+
// 5. Get nonce from EntryPoint via bundler RPC.
|
|
338
|
+
// Routing through the bundler lets Pimlico account for pending mempool
|
|
339
|
+
// UserOps, preventing AA25 nonce conflicts on rapid submissions.
|
|
340
|
+
// Requires relay allowlist to include eth_call (added in relay v1.x).
|
|
341
|
+
// Fallback: if bundler rejects eth_call (403/method_not_allowed), use public RPC.
|
|
342
|
+
// getNonce(address sender, uint192 key) — selector 0x35567e1a
|
|
343
|
+
const senderPadded = sender.slice(2).toLowerCase().padStart(64, '0');
|
|
344
|
+
const keyPadded = '0'.repeat(64);
|
|
345
|
+
const nonceCalldata = `0x35567e1a${senderPadded}${keyPadded}`;
|
|
346
|
+
|
|
347
|
+
let nonce: string;
|
|
348
|
+
try {
|
|
349
|
+
const nonceResult = await rpc('eth_call', [{ to: entryPoint, data: nonceCalldata }, 'latest']);
|
|
350
|
+
nonce = nonceResult || '0x0';
|
|
351
|
+
} catch {
|
|
352
|
+
// Fallback to public RPC if bundler doesn't support eth_call
|
|
353
|
+
const nonceResp = await fetch(rpcUrl, {
|
|
354
|
+
method: 'POST',
|
|
355
|
+
headers: { 'Content-Type': 'application/json' },
|
|
356
|
+
body: JSON.stringify({
|
|
357
|
+
jsonrpc: '2.0', id: 1, method: 'eth_call',
|
|
358
|
+
params: [{ to: entryPoint, data: nonceCalldata }, 'latest'],
|
|
359
|
+
}),
|
|
360
|
+
});
|
|
361
|
+
const nonceJson = await nonceResp.json() as { result?: string };
|
|
362
|
+
nonce = nonceJson.result || '0x0';
|
|
363
|
+
}
|
|
220
364
|
|
|
221
|
-
//
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
365
|
+
// 6. Build unsigned UserOp (v0.7 fields, camelCase for Rust JSON serde)
|
|
366
|
+
const unsignedOp: Record<string, any> = {
|
|
367
|
+
sender,
|
|
368
|
+
nonce,
|
|
369
|
+
callData,
|
|
370
|
+
callGasLimit: '0x0',
|
|
371
|
+
verificationGasLimit: '0x0',
|
|
372
|
+
preVerificationGas: '0x0',
|
|
373
|
+
maxFeePerGas: fast.maxFeePerGas,
|
|
374
|
+
maxPriorityFeePerGas: fast.maxPriorityFeePerGas,
|
|
375
|
+
signature: DUMMY_SIGNATURE,
|
|
376
|
+
};
|
|
377
|
+
if (factory) {
|
|
378
|
+
unsignedOp.factory = factory;
|
|
379
|
+
unsignedOp.factoryData = factoryData;
|
|
380
|
+
}
|
|
230
381
|
|
|
231
|
-
//
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
382
|
+
// 7. Get paymaster sponsorship (fills gas limits + paymaster fields)
|
|
383
|
+
const sponsorResult = await rpc('pm_sponsorUserOperation', [unsignedOp, entryPoint]);
|
|
384
|
+
Object.assign(unsignedOp, sponsorResult);
|
|
385
|
+
|
|
386
|
+
// 8. Hash and sign the UserOp via WASM
|
|
387
|
+
const opJson = JSON.stringify(unsignedOp);
|
|
388
|
+
const hashHex = getWasm().hashUserOp(opJson, entryPoint, BigInt(config.chainId));
|
|
389
|
+
const sigHex = getWasm().signUserOp(hashHex, eoa.private_key);
|
|
390
|
+
unsignedOp.signature = `0x${sigHex}`;
|
|
391
|
+
|
|
392
|
+
// 9. Submit the signed UserOp (with AA25 nonce conflict retry)
|
|
393
|
+
let userOpHash: string;
|
|
394
|
+
try {
|
|
395
|
+
userOpHash = await rpc('eth_sendUserOperation', [unsignedOp, entryPoint]);
|
|
396
|
+
} catch (err: any) {
|
|
397
|
+
const msg = err?.message || '';
|
|
398
|
+
if (/AA25|AA10|invalid account nonce|already being processed/i.test(msg)) {
|
|
399
|
+
console.error('AA25/AA10 nonce conflict detected, rebuilding UserOp with fresh nonce...');
|
|
400
|
+
// Bust deployment cache so getInitCode re-checks on-chain
|
|
401
|
+
deployedAccounts.delete(sender.toLowerCase());
|
|
402
|
+
|
|
403
|
+
// Wait for previous UserOp to mine before retrying with fresh nonce.
|
|
404
|
+
// Public RPC won't reflect the new nonce until the tx is on-chain.
|
|
405
|
+
await new Promise(r => setTimeout(r, 15000));
|
|
406
|
+
|
|
407
|
+
// Re-fetch initCode and nonce
|
|
408
|
+
const { factory: retryFactory, factoryData: retryFactoryData } = await getInitCode(sender, eoa.address, rpcUrl);
|
|
409
|
+
let retryNonce: string;
|
|
410
|
+
try {
|
|
411
|
+
const retryNonceResult = await rpc('eth_call', [{ to: entryPoint, data: nonceCalldata }, 'latest']);
|
|
412
|
+
retryNonce = retryNonceResult || '0x0';
|
|
413
|
+
} catch {
|
|
414
|
+
const retryNonceResp = await fetch(rpcUrl, {
|
|
415
|
+
method: 'POST',
|
|
416
|
+
headers: { 'Content-Type': 'application/json' },
|
|
417
|
+
body: JSON.stringify({
|
|
418
|
+
jsonrpc: '2.0', id: 1, method: 'eth_call',
|
|
419
|
+
params: [{ to: entryPoint, data: nonceCalldata }, 'latest'],
|
|
420
|
+
}),
|
|
421
|
+
});
|
|
422
|
+
const retryNonceJson = await retryNonceResp.json() as { result?: string };
|
|
423
|
+
retryNonce = retryNonceJson.result || '0x0';
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Rebuild unsigned UserOp with fresh nonce and initCode
|
|
427
|
+
const retryOp: Record<string, any> = {
|
|
428
|
+
sender,
|
|
429
|
+
nonce: retryNonce,
|
|
430
|
+
callData,
|
|
431
|
+
callGasLimit: '0x0',
|
|
432
|
+
verificationGasLimit: '0x0',
|
|
433
|
+
preVerificationGas: '0x0',
|
|
434
|
+
maxFeePerGas: fast.maxFeePerGas,
|
|
435
|
+
maxPriorityFeePerGas: fast.maxPriorityFeePerGas,
|
|
436
|
+
signature: DUMMY_SIGNATURE,
|
|
437
|
+
};
|
|
438
|
+
if (retryFactory) {
|
|
439
|
+
retryOp.factory = retryFactory;
|
|
440
|
+
retryOp.factoryData = retryFactoryData;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Re-sponsor and re-sign
|
|
444
|
+
const retrySponsor = await rpc('pm_sponsorUserOperation', [retryOp, entryPoint]);
|
|
445
|
+
Object.assign(retryOp, retrySponsor);
|
|
446
|
+
const retryOpJson = JSON.stringify(retryOp);
|
|
447
|
+
const retryHashHex = getWasm().hashUserOp(retryOpJson, entryPoint, BigInt(config.chainId));
|
|
448
|
+
const retrySigHex = getWasm().signUserOp(retryHashHex, eoa.private_key);
|
|
449
|
+
retryOp.signature = `0x${retrySigHex}`;
|
|
450
|
+
|
|
451
|
+
userOpHash = await rpc('eth_sendUserOperation', [retryOp, entryPoint]);
|
|
452
|
+
} else {
|
|
453
|
+
throw err;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
240
456
|
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
return (await pimlicoClient.getUserOperationGasPrice()).fast;
|
|
251
|
-
},
|
|
252
|
-
},
|
|
253
|
-
});
|
|
457
|
+
// 10. Wait for receipt (poll up to 120s)
|
|
458
|
+
let receipt = null;
|
|
459
|
+
for (let i = 0; i < 60; i++) {
|
|
460
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
461
|
+
try {
|
|
462
|
+
receipt = await rpc('eth_getUserOperationReceipt', [userOpHash]);
|
|
463
|
+
if (receipt) break;
|
|
464
|
+
} catch { /* not mined yet */ }
|
|
465
|
+
}
|
|
254
466
|
|
|
255
|
-
|
|
256
|
-
// The DataEdge contract has a fallback() that emits Log(bytes), so the calldata
|
|
257
|
-
// IS the protobuf payload directly (no function selector needed).
|
|
258
|
-
// permissionless encodes the execute() call internally from to/value/data.
|
|
259
|
-
const calldata = `0x${protobufPayload.toString('hex')}` as Hex;
|
|
260
|
-
|
|
261
|
-
// Use sendUserOperation to get the userOpHash, then wait for receipt
|
|
262
|
-
const userOpHash = await smartAccountClient.sendUserOperation({
|
|
263
|
-
calls: [
|
|
264
|
-
{
|
|
265
|
-
to: dataEdgeAddress,
|
|
266
|
-
value: 0n,
|
|
267
|
-
data: calldata,
|
|
268
|
-
},
|
|
269
|
-
],
|
|
270
|
-
});
|
|
467
|
+
const success = receipt?.success ?? false;
|
|
271
468
|
|
|
272
|
-
//
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
469
|
+
// Mark account as deployed after first successful submission
|
|
470
|
+
if (success) {
|
|
471
|
+
deployedAccounts.add(sender.toLowerCase());
|
|
472
|
+
}
|
|
276
473
|
|
|
277
474
|
return {
|
|
278
|
-
txHash: receipt
|
|
475
|
+
txHash: receipt?.receipt?.transactionHash || '',
|
|
279
476
|
userOpHash,
|
|
280
|
-
success
|
|
477
|
+
success,
|
|
281
478
|
};
|
|
282
479
|
}
|
|
283
480
|
|
|
@@ -311,71 +508,187 @@ export async function submitFactBatchOnChain(
|
|
|
311
508
|
throw new Error('Mnemonic (TOTALRECLAW_RECOVERY_PHRASE) is required for on-chain submission');
|
|
312
509
|
}
|
|
313
510
|
|
|
314
|
-
const
|
|
315
|
-
const bundlerRpcUrl = getRelayBundlerUrl(config.relayUrl);
|
|
316
|
-
const dataEdgeAddress = config.dataEdgeAddress as Address;
|
|
317
|
-
const entryPointAddr = (config.entryPointAddress || entryPoint07Address) as Address;
|
|
318
|
-
|
|
511
|
+
const bundlerUrl = `${config.relayUrl}/v1/bundler`;
|
|
319
512
|
const headers: Record<string, string> = {
|
|
513
|
+
'Content-Type': 'application/json',
|
|
320
514
|
'X-TotalReclaw-Client': 'openclaw-plugin',
|
|
321
515
|
};
|
|
322
516
|
if (config.authKeyHex) headers['Authorization'] = `Bearer ${config.authKeyHex}`;
|
|
323
517
|
if (config.walletAddress) headers['X-Wallet-Address'] = config.walletAddress;
|
|
324
518
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
519
|
+
// Helper for JSON-RPC calls to relay bundler (with 429 retry)
|
|
520
|
+
async function rpc(method: string, params: unknown[]): Promise<any> {
|
|
521
|
+
return rpcWithRetry(bundlerUrl, headers, method, params);
|
|
522
|
+
}
|
|
328
523
|
|
|
329
|
-
const
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
524
|
+
const eoa = getWasm().deriveEoa(config.mnemonic) as { private_key: string; address: string };
|
|
525
|
+
const sender = config.walletAddress || await deriveSmartAccountAddress(config.mnemonic, config.chainId);
|
|
526
|
+
const entryPoint = config.entryPointAddress || getWasm().getEntryPointAddress();
|
|
527
|
+
|
|
528
|
+
// Encode batch calldata (SimpleAccount.executeBatch)
|
|
529
|
+
// encodeBatchCall expects a JSON array of hex-encoded payload strings
|
|
530
|
+
const payloadsHex = protobufPayloads.map(p => p.toString('hex'));
|
|
531
|
+
const calldataBytes = getWasm().encodeBatchCall(JSON.stringify(payloadsHex));
|
|
532
|
+
const callData = `0x${Buffer.from(calldataBytes).toString('hex')}`;
|
|
533
|
+
|
|
534
|
+
// Get gas prices
|
|
535
|
+
const gasPrices = await rpc('pimlico_getUserOperationGasPrice', []);
|
|
536
|
+
const fast = gasPrices.fast;
|
|
537
|
+
|
|
538
|
+
const rpcUrl = config.rpcUrl || CONFIG.rpcUrl || getDefaultRpcUrl(config.chainId);
|
|
539
|
+
|
|
540
|
+
// Check if Smart Account is deployed (needed for factory/factoryData)
|
|
541
|
+
const { factory, factoryData } = await getInitCode(sender, eoa.address, rpcUrl);
|
|
542
|
+
|
|
543
|
+
// Get nonce via bundler (accounts for pending mempool UserOps) with public RPC fallback
|
|
544
|
+
const senderPadded = sender.slice(2).toLowerCase().padStart(64, '0');
|
|
545
|
+
const keyPadded = '0'.repeat(64);
|
|
546
|
+
const nonceCalldata = `0x35567e1a${senderPadded}${keyPadded}`;
|
|
547
|
+
|
|
548
|
+
let nonce: string;
|
|
549
|
+
try {
|
|
550
|
+
const nonceResult = await rpc('eth_call', [{ to: entryPoint, data: nonceCalldata }, 'latest']);
|
|
551
|
+
nonce = nonceResult || '0x0';
|
|
552
|
+
} catch {
|
|
553
|
+
const nonceResp = await fetch(rpcUrl, {
|
|
554
|
+
method: 'POST',
|
|
555
|
+
headers: { 'Content-Type': 'application/json' },
|
|
556
|
+
body: JSON.stringify({
|
|
557
|
+
jsonrpc: '2.0', id: 1, method: 'eth_call',
|
|
558
|
+
params: [{ to: entryPoint, data: nonceCalldata }, 'latest'],
|
|
559
|
+
}),
|
|
560
|
+
});
|
|
561
|
+
const nonceJson = await nonceResp.json() as { result?: string };
|
|
562
|
+
nonce = nonceJson.result || '0x0';
|
|
563
|
+
}
|
|
334
564
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
565
|
+
// Build unsigned UserOp
|
|
566
|
+
const unsignedOp: Record<string, any> = {
|
|
567
|
+
sender,
|
|
568
|
+
nonce,
|
|
569
|
+
callData,
|
|
570
|
+
callGasLimit: '0x0',
|
|
571
|
+
verificationGasLimit: '0x0',
|
|
572
|
+
preVerificationGas: '0x0',
|
|
573
|
+
maxFeePerGas: fast.maxFeePerGas,
|
|
574
|
+
maxPriorityFeePerGas: fast.maxPriorityFeePerGas,
|
|
575
|
+
signature: DUMMY_SIGNATURE,
|
|
576
|
+
};
|
|
577
|
+
if (factory) {
|
|
578
|
+
unsignedOp.factory = factory;
|
|
579
|
+
unsignedOp.factoryData = factoryData;
|
|
580
|
+
}
|
|
343
581
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
582
|
+
// Gas estimation for batch operations — get accurate gas limits from Pimlico
|
|
583
|
+
// before paymaster sponsorship (can't bump after sponsorship as it invalidates
|
|
584
|
+
// the paymaster's signature, causing AA34).
|
|
585
|
+
if (protobufPayloads.length > 1) {
|
|
586
|
+
try {
|
|
587
|
+
const gasEstimate = await rpc('eth_estimateUserOperationGas', [unsignedOp, entryPoint]);
|
|
588
|
+
if (gasEstimate.callGasLimit) unsignedOp.callGasLimit = gasEstimate.callGasLimit;
|
|
589
|
+
if (gasEstimate.verificationGasLimit) unsignedOp.verificationGasLimit = gasEstimate.verificationGasLimit;
|
|
590
|
+
if (gasEstimate.preVerificationGas) unsignedOp.preVerificationGas = gasEstimate.preVerificationGas;
|
|
591
|
+
} catch {
|
|
592
|
+
// If estimation fails, let the paymaster handle it (default behavior)
|
|
593
|
+
}
|
|
594
|
+
}
|
|
352
595
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
596
|
+
// Paymaster sponsorship (uses gas limits from estimation above for batches)
|
|
597
|
+
const sponsorResult = await rpc('pm_sponsorUserOperation', [unsignedOp, entryPoint]);
|
|
598
|
+
Object.assign(unsignedOp, sponsorResult);
|
|
599
|
+
|
|
600
|
+
// Hash and sign via WASM
|
|
601
|
+
const opJson = JSON.stringify(unsignedOp);
|
|
602
|
+
const hashHex = getWasm().hashUserOp(opJson, entryPoint, BigInt(config.chainId));
|
|
603
|
+
const sigHex = getWasm().signUserOp(hashHex, eoa.private_key);
|
|
604
|
+
unsignedOp.signature = `0x${sigHex}`;
|
|
605
|
+
|
|
606
|
+
// Submit (with AA25 nonce conflict retry)
|
|
607
|
+
let userOpHash: string;
|
|
608
|
+
try {
|
|
609
|
+
userOpHash = await rpc('eth_sendUserOperation', [unsignedOp, entryPoint]);
|
|
610
|
+
} catch (err: any) {
|
|
611
|
+
const msg = err?.message || '';
|
|
612
|
+
if (/AA25|AA10|invalid account nonce|already being processed/i.test(msg)) {
|
|
613
|
+
console.error('AA25/AA10 nonce conflict detected (batch), rebuilding UserOp with fresh nonce...');
|
|
614
|
+
// Bust deployment cache so getInitCode re-checks on-chain
|
|
615
|
+
deployedAccounts.delete(sender.toLowerCase());
|
|
616
|
+
|
|
617
|
+
// Wait for previous UserOp to mine before retrying with fresh nonce.
|
|
618
|
+
// Public RPC won't reflect the new nonce until the tx is on-chain.
|
|
619
|
+
await new Promise(r => setTimeout(r, 15000));
|
|
620
|
+
|
|
621
|
+
// Re-fetch initCode and nonce
|
|
622
|
+
const { factory: retryFactory, factoryData: retryFactoryData } = await getInitCode(sender, eoa.address, rpcUrl);
|
|
623
|
+
let retryNonce: string;
|
|
624
|
+
try {
|
|
625
|
+
const retryNonceResult = await rpc('eth_call', [{ to: entryPoint, data: nonceCalldata }, 'latest']);
|
|
626
|
+
retryNonce = retryNonceResult || '0x0';
|
|
627
|
+
} catch {
|
|
628
|
+
const retryNonceResp = await fetch(rpcUrl, {
|
|
629
|
+
method: 'POST',
|
|
630
|
+
headers: { 'Content-Type': 'application/json' },
|
|
631
|
+
body: JSON.stringify({
|
|
632
|
+
jsonrpc: '2.0', id: 1, method: 'eth_call',
|
|
633
|
+
params: [{ to: entryPoint, data: nonceCalldata }, 'latest'],
|
|
634
|
+
}),
|
|
635
|
+
});
|
|
636
|
+
const retryNonceJson = await retryNonceResp.json() as { result?: string };
|
|
637
|
+
retryNonce = retryNonceJson.result || '0x0';
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Rebuild unsigned UserOp with fresh nonce and initCode
|
|
641
|
+
const retryOp: Record<string, any> = {
|
|
642
|
+
sender,
|
|
643
|
+
nonce: retryNonce,
|
|
644
|
+
callData,
|
|
645
|
+
callGasLimit: '0x0',
|
|
646
|
+
verificationGasLimit: '0x0',
|
|
647
|
+
preVerificationGas: '0x0',
|
|
648
|
+
maxFeePerGas: fast.maxFeePerGas,
|
|
649
|
+
maxPriorityFeePerGas: fast.maxPriorityFeePerGas,
|
|
650
|
+
signature: DUMMY_SIGNATURE,
|
|
651
|
+
};
|
|
652
|
+
if (retryFactory) {
|
|
653
|
+
retryOp.factory = retryFactory;
|
|
654
|
+
retryOp.factoryData = retryFactoryData;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Re-sponsor and re-sign
|
|
658
|
+
const retrySponsor = await rpc('pm_sponsorUserOperation', [retryOp, entryPoint]);
|
|
659
|
+
Object.assign(retryOp, retrySponsor);
|
|
660
|
+
const retryOpJson = JSON.stringify(retryOp);
|
|
661
|
+
const retryHashHex = getWasm().hashUserOp(retryOpJson, entryPoint, BigInt(config.chainId));
|
|
662
|
+
const retrySigHex = getWasm().signUserOp(retryHashHex, eoa.private_key);
|
|
663
|
+
retryOp.signature = `0x${retrySigHex}`;
|
|
664
|
+
|
|
665
|
+
userOpHash = await rpc('eth_sendUserOperation', [retryOp, entryPoint]);
|
|
666
|
+
} else {
|
|
667
|
+
throw err;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Wait for receipt (poll up to 120s)
|
|
672
|
+
let receipt = null;
|
|
673
|
+
for (let i = 0; i < 60; i++) {
|
|
674
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
675
|
+
try {
|
|
676
|
+
receipt = await rpc('eth_getUserOperationReceipt', [userOpHash]);
|
|
677
|
+
if (receipt) break;
|
|
678
|
+
} catch { /* not mined yet */ }
|
|
679
|
+
}
|
|
364
680
|
|
|
365
|
-
|
|
366
|
-
const calls = protobufPayloads.map(payload => ({
|
|
367
|
-
to: dataEdgeAddress,
|
|
368
|
-
value: 0n,
|
|
369
|
-
data: `0x${payload.toString('hex')}` as Hex,
|
|
370
|
-
}));
|
|
681
|
+
const batchSuccess = receipt?.success ?? false;
|
|
371
682
|
|
|
372
|
-
|
|
373
|
-
|
|
683
|
+
// Mark account as deployed after first successful submission
|
|
684
|
+
if (batchSuccess) {
|
|
685
|
+
deployedAccounts.add(sender.toLowerCase());
|
|
686
|
+
}
|
|
374
687
|
|
|
375
688
|
return {
|
|
376
|
-
txHash: receipt
|
|
689
|
+
txHash: receipt?.receipt?.transactionHash || '',
|
|
377
690
|
userOpHash,
|
|
378
|
-
success:
|
|
691
|
+
success: batchSuccess,
|
|
379
692
|
batchSize: protobufPayloads.length,
|
|
380
693
|
};
|
|
381
694
|
}
|
|
@@ -391,57 +704,28 @@ export async function submitFactBatchOnChain(
|
|
|
391
704
|
* The managed service (subgraph mode) is the default.
|
|
392
705
|
*/
|
|
393
706
|
export function isSubgraphMode(): boolean {
|
|
394
|
-
return
|
|
707
|
+
return !CONFIG.selfHosted;
|
|
395
708
|
}
|
|
396
709
|
|
|
397
710
|
/**
|
|
398
711
|
* Get subgraph configuration from environment variables.
|
|
399
712
|
*
|
|
400
|
-
* After the
|
|
713
|
+
* After the v1 env var cleanup, clients only need:
|
|
401
714
|
* - TOTALRECLAW_RECOVERY_PHRASE -- BIP-39 mnemonic
|
|
402
715
|
* - TOTALRECLAW_SERVER_URL -- relay server URL (default: https://api.totalreclaw.xyz)
|
|
403
716
|
* - TOTALRECLAW_SELF_HOSTED -- set "true" to use self-hosted server (default: managed service)
|
|
404
|
-
* - TOTALRECLAW_CHAIN_ID -- optional, defaults to 100 (Gnosis mainnet)
|
|
405
717
|
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
* - TOTALRECLAW_SUBGRAPH_ENDPOINT
|
|
409
|
-
*/
|
|
410
|
-
/**
|
|
411
|
-
* Derive the Smart Account address from a BIP-39 mnemonic.
|
|
412
|
-
* This is the on-chain owner identity used in the subgraph.
|
|
718
|
+
* Chain ID is no longer configurable via env — it is auto-detected from the
|
|
719
|
+
* relay billing response (free = Base Sepolia, Pro = Gnosis mainnet).
|
|
413
720
|
*/
|
|
414
|
-
export async function deriveSmartAccountAddress(mnemonic: string, chainId?: number): Promise<string> {
|
|
415
|
-
const chain: Chain = getChainFromId(chainId ?? 100);
|
|
416
|
-
const ownerAccount = mnemonicToAccount(mnemonic);
|
|
417
|
-
const entryPointAddr = (process.env.TOTALRECLAW_ENTRYPOINT_ADDRESS || DEFAULT_ENTRYPOINT_ADDRESS) as Address;
|
|
418
|
-
const rpcUrl = process.env.TOTALRECLAW_RPC_URL;
|
|
419
|
-
|
|
420
|
-
const publicClient = createPublicClient({
|
|
421
|
-
chain,
|
|
422
|
-
transport: rpcUrl ? http(rpcUrl) : http(),
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
const smartAccount = await toSimpleSmartAccount({
|
|
426
|
-
client: publicClient,
|
|
427
|
-
owner: ownerAccount,
|
|
428
|
-
entryPoint: {
|
|
429
|
-
address: entryPointAddr,
|
|
430
|
-
version: '0.7',
|
|
431
|
-
},
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
return smartAccount.address.toLowerCase();
|
|
435
|
-
}
|
|
436
|
-
|
|
437
721
|
export function getSubgraphConfig(): SubgraphStoreConfig {
|
|
438
722
|
return {
|
|
439
|
-
relayUrl:
|
|
440
|
-
mnemonic:
|
|
441
|
-
cachePath:
|
|
442
|
-
chainId:
|
|
443
|
-
dataEdgeAddress:
|
|
444
|
-
entryPointAddress:
|
|
445
|
-
rpcUrl:
|
|
723
|
+
relayUrl: CONFIG.serverUrl || 'https://api.totalreclaw.xyz',
|
|
724
|
+
mnemonic: CONFIG.recoveryPhrase,
|
|
725
|
+
cachePath: CONFIG.cachePath,
|
|
726
|
+
chainId: CONFIG.chainId,
|
|
727
|
+
dataEdgeAddress: CONFIG.dataEdgeAddress || getWasm().getDataEdgeAddress(),
|
|
728
|
+
entryPointAddress: CONFIG.entryPointAddress || getWasm().getEntryPointAddress(),
|
|
729
|
+
rpcUrl: CONFIG.rpcUrl || undefined,
|
|
446
730
|
};
|
|
447
731
|
}
|