@totalreclaw/totalreclaw 1.0.4 → 1.1.0

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.
@@ -0,0 +1,282 @@
1
+ /**
2
+ * Subgraph search path — queries facts via GraphQL hash_in.
3
+ *
4
+ * Used when the managed service is active (TOTALRECLAW_SELF_HOSTED is not
5
+ * "true"). Replaces the HTTP POST to /v1/search with a GraphQL query to
6
+ * the subgraph via the relay server.
7
+ *
8
+ * The relay server proxies GraphQL queries to Graph Studio with its own
9
+ * API key at `${relayUrl}/v1/subgraph`. Clients never need a subgraph endpoint.
10
+ *
11
+ * Query cost optimization:
12
+ * Phase 1: Single query with ALL trapdoors (1 query).
13
+ * Phase 2: If saturated (1000 results), split into small parallel batches
14
+ * so rare trapdoor matches aren't drowned by common ones.
15
+ * Phase 3: Cursor-based pagination for any saturated batch.
16
+ *
17
+ * This minimizes Graph Network query costs (pay-per-query via GRT):
18
+ * - Small datasets (<1000 matches): 1 query total
19
+ * - Medium datasets: 1 + N batch queries
20
+ * - Large datasets: 1 + N batches + pagination queries
21
+ */
22
+
23
+ import { getSubgraphConfig } from './subgraph-store.js';
24
+
25
+ export interface SubgraphSearchFact {
26
+ id: string;
27
+ encryptedBlob: string;
28
+ encryptedEmbedding: string | null;
29
+ decayScore: string;
30
+ timestamp: string;
31
+ isActive: boolean;
32
+ }
33
+
34
+ /** Batch size for Phase 2 split queries. */
35
+ const TRAPDOOR_BATCH_SIZE = parseInt(process.env.TOTALRECLAW_TRAPDOOR_BATCH_SIZE ?? '5', 10);
36
+ /** Graph Studio / Graph Network hard limit on `first` argument. */
37
+ const PAGE_SIZE = parseInt(process.env.TOTALRECLAW_SUBGRAPH_PAGE_SIZE ?? '1000', 10);
38
+
39
+ /**
40
+ * Execute a single GraphQL query against the subgraph endpoint.
41
+ * Returns null on any network or HTTP error (never throws).
42
+ */
43
+ async function gqlQuery<T>(
44
+ endpoint: string,
45
+ query: string,
46
+ variables: Record<string, unknown>,
47
+ authKeyHex?: string,
48
+ ): Promise<T | null> {
49
+ try {
50
+ const headers: Record<string, string> = {
51
+ 'Content-Type': 'application/json',
52
+ 'X-TotalReclaw-Client': 'openclaw-plugin',
53
+ };
54
+ if (authKeyHex) {
55
+ headers['Authorization'] = `Bearer ${authKeyHex}`;
56
+ }
57
+ const response = await fetch(endpoint, {
58
+ method: 'POST',
59
+ headers,
60
+ body: JSON.stringify({ query, variables }),
61
+ });
62
+ if (!response.ok) return null;
63
+ const json = await response.json() as { data?: T };
64
+ return json.data ?? null;
65
+ } catch {
66
+ return null;
67
+ }
68
+ }
69
+
70
+ /** GraphQL query for blind index lookup. */
71
+ const SEARCH_QUERY = `
72
+ query SearchByBlindIndex($trapdoors: [String!]!, $owner: Bytes!, $first: Int!) {
73
+ blindIndexes(
74
+ where: { hash_in: $trapdoors, owner: $owner, fact_: { isActive: true } }
75
+ first: $first
76
+ orderBy: id
77
+ orderDirection: desc
78
+ ) {
79
+ id
80
+ fact {
81
+ id
82
+ encryptedBlob
83
+ encryptedEmbedding
84
+ decayScore
85
+ timestamp
86
+ isActive
87
+ contentFp
88
+ sequenceId
89
+ version
90
+ }
91
+ }
92
+ }
93
+ `;
94
+
95
+ /** Pagination query — cursor-based using id_gt, ascending for deterministic walk. */
96
+ const PAGINATE_QUERY = `
97
+ query PaginateBlindIndex($trapdoors: [String!]!, $owner: Bytes!, $first: Int!, $lastId: String!) {
98
+ blindIndexes(
99
+ where: { hash_in: $trapdoors, owner: $owner, id_gt: $lastId, fact_: { isActive: true } }
100
+ first: $first
101
+ orderBy: id
102
+ orderDirection: asc
103
+ ) {
104
+ id
105
+ fact {
106
+ id
107
+ encryptedBlob
108
+ encryptedEmbedding
109
+ timestamp
110
+ decayScore
111
+ isActive
112
+ contentFp
113
+ sequenceId
114
+ version
115
+ }
116
+ }
117
+ }
118
+ `;
119
+
120
+ interface BlindIndexEntry {
121
+ id: string;
122
+ fact: SubgraphSearchFact;
123
+ }
124
+
125
+ interface SearchResponse {
126
+ blindIndexes?: BlindIndexEntry[];
127
+ }
128
+
129
+ /** Collect facts from blind index entries, deduplicating by fact id. */
130
+ function collectFacts(
131
+ entries: BlindIndexEntry[],
132
+ allResults: Map<string, SubgraphSearchFact>,
133
+ ): void {
134
+ for (const entry of entries) {
135
+ if (entry.fact && entry.fact.isActive !== false && !allResults.has(entry.fact.id)) {
136
+ allResults.set(entry.fact.id, entry.fact);
137
+ }
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Paginate a single trapdoor chunk until exhausted or maxCandidates reached.
143
+ */
144
+ async function paginateChunk(
145
+ subgraphUrl: string,
146
+ chunk: string[],
147
+ owner: string,
148
+ allResults: Map<string, SubgraphSearchFact>,
149
+ maxCandidates: number,
150
+ authKeyHex?: string,
151
+ ): Promise<void> {
152
+ let lastId = '';
153
+ while (allResults.size < maxCandidates) {
154
+ const data = await gqlQuery<SearchResponse>(
155
+ subgraphUrl,
156
+ PAGINATE_QUERY,
157
+ { trapdoors: chunk, owner, first: PAGE_SIZE, lastId },
158
+ authKeyHex,
159
+ );
160
+ const entries = data?.blindIndexes ?? [];
161
+ if (entries.length === 0) break;
162
+ collectFacts(entries, allResults);
163
+ if (entries.length < PAGE_SIZE) break;
164
+ lastId = entries[entries.length - 1].id;
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Search the subgraph for facts matching the given trapdoors.
170
+ *
171
+ * Adaptive strategy to minimize query costs:
172
+ *
173
+ * Phase 1: Single query with ALL trapdoors.
174
+ * - If not saturated (< PAGE_SIZE results): done. 1 query total.
175
+ * - If saturated: common trapdoors may be drowning rare ones. Go to Phase 2.
176
+ *
177
+ * Phase 2: Split trapdoors into small parallel batches (TRAPDOOR_BATCH_SIZE=5).
178
+ * - Each batch independently gets up to PAGE_SIZE results.
179
+ * - Rare trapdoor matches get their own budget.
180
+ *
181
+ * Phase 3: Cursor-based pagination for any saturated batch.
182
+ * - Only for power users with very large datasets.
183
+ */
184
+ export async function searchSubgraph(
185
+ owner: string,
186
+ trapdoors: string[],
187
+ maxCandidates: number,
188
+ authKeyHex?: string,
189
+ ): Promise<SubgraphSearchFact[]> {
190
+ const config = getSubgraphConfig();
191
+ const subgraphUrl = `${config.relayUrl}/v1/subgraph`;
192
+ const allResults = new Map<string, SubgraphSearchFact>();
193
+
194
+ // -----------------------------------------------------------------------
195
+ // Phase 1: Single query with all trapdoors (1 query)
196
+ // -----------------------------------------------------------------------
197
+ const phase1 = await gqlQuery<SearchResponse>(
198
+ subgraphUrl,
199
+ SEARCH_QUERY,
200
+ { trapdoors, owner, first: PAGE_SIZE },
201
+ authKeyHex,
202
+ );
203
+
204
+ const phase1Entries = phase1?.blindIndexes ?? [];
205
+ collectFacts(phase1Entries, allResults);
206
+
207
+ // Not saturated — we got everything in 1 query. Done.
208
+ if (phase1Entries.length < PAGE_SIZE) {
209
+ return Array.from(allResults.values());
210
+ }
211
+
212
+ // -----------------------------------------------------------------------
213
+ // Phase 2: Saturated — split into small batches for better rare-word recall.
214
+ // Common trapdoors were drowning rare ones in the single-query result.
215
+ // -----------------------------------------------------------------------
216
+ const chunks: string[][] = [];
217
+ for (let i = 0; i < trapdoors.length; i += TRAPDOOR_BATCH_SIZE) {
218
+ chunks.push(trapdoors.slice(i, i + TRAPDOOR_BATCH_SIZE));
219
+ }
220
+
221
+ const batchResults = await Promise.all(
222
+ chunks.map(async (chunk) => {
223
+ const data = await gqlQuery<SearchResponse>(
224
+ subgraphUrl,
225
+ SEARCH_QUERY,
226
+ { trapdoors: chunk, owner, first: PAGE_SIZE },
227
+ authKeyHex,
228
+ );
229
+ return { chunk, entries: data?.blindIndexes ?? [] };
230
+ }),
231
+ );
232
+
233
+ const saturatedChunks: string[][] = [];
234
+ for (const { chunk, entries } of batchResults) {
235
+ collectFacts(entries, allResults);
236
+ if (entries.length >= PAGE_SIZE) {
237
+ saturatedChunks.push(chunk);
238
+ }
239
+ }
240
+
241
+ // -----------------------------------------------------------------------
242
+ // Phase 3: Cursor-based pagination for saturated batches (power users).
243
+ // -----------------------------------------------------------------------
244
+ for (const chunk of saturatedChunks) {
245
+ if (allResults.size >= maxCandidates) break;
246
+ await paginateChunk(subgraphUrl, chunk, owner, allResults, maxCandidates, authKeyHex);
247
+ }
248
+
249
+ return Array.from(allResults.values());
250
+ }
251
+
252
+ /**
253
+ * Get fact count from the subgraph for dynamic pool sizing.
254
+ * Uses the globalStates entity for a lightweight single-row lookup
255
+ * instead of fetching and counting individual fact IDs.
256
+ */
257
+ export async function getSubgraphFactCount(owner: string, authKeyHex?: string): Promise<number> {
258
+ const config = getSubgraphConfig();
259
+ const subgraphUrl = `${config.relayUrl}/v1/subgraph`;
260
+
261
+ const query = `
262
+ query FactCount {
263
+ globalStates(first: 1) {
264
+ totalFacts
265
+ }
266
+ }
267
+ `;
268
+
269
+ const data = await gqlQuery<{ globalStates?: Array<{ totalFacts: string }> }>(
270
+ subgraphUrl,
271
+ query,
272
+ {},
273
+ authKeyHex,
274
+ );
275
+
276
+ if (data?.globalStates && data.globalStates.length > 0) {
277
+ const count = parseInt(data.globalStates[0].totalFacts, 10);
278
+ return isNaN(count) ? 0 : count;
279
+ }
280
+
281
+ return 0;
282
+ }
@@ -0,0 +1,346 @@
1
+ /**
2
+ * Subgraph store path — writes facts on-chain via ERC-4337 UserOps.
3
+ *
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
6
+ * flow.
7
+ *
8
+ * Builds UserOps client-side using `permissionless` + `viem` and submits
9
+ * them through the TotalReclaw relay server, which proxies bundler/paymaster
10
+ * JSON-RPC to Pimlico with its own API key. Clients never need a Pimlico key.
11
+ */
12
+
13
+ import { createPublicClient, http, type Hex, type Address, type Chain } from 'viem';
14
+ import { entryPoint07Address } from 'viem/account-abstraction';
15
+ import { mnemonicToAccount } from 'viem/accounts';
16
+ import { gnosis, gnosisChiado } from 'viem/chains';
17
+ import { createSmartAccountClient } from 'permissionless';
18
+ import { toSimpleSmartAccount } from 'permissionless/accounts';
19
+ import { createPimlicoClient } from 'permissionless/clients/pimlico';
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Types
23
+ // ---------------------------------------------------------------------------
24
+
25
+ /** Default EventfulDataEdge contract address on Gnosis mainnet */
26
+ const DEFAULT_DATA_EDGE_ADDRESS = '0xC445af1D4EB9fce4e1E61fE96ea7B8feBF03c5ca';
27
+
28
+ /** Well-known ERC-4337 EntryPoint v0.7 address (same on all chains) */
29
+ const DEFAULT_ENTRYPOINT_ADDRESS = '0x0000000071727De22E5E9d8BAf0edAc6f37da032';
30
+
31
+ export interface SubgraphStoreConfig {
32
+ relayUrl: string; // TotalReclaw relay server URL (proxies bundler + subgraph)
33
+ mnemonic: string; // BIP-39 mnemonic for key derivation
34
+ cachePath: string; // Hot cache file path
35
+ chainId: number; // 100 for Gnosis mainnet, 10200 for Chiado testnet
36
+ dataEdgeAddress: string; // EventfulDataEdge contract address
37
+ entryPointAddress: string; // ERC-4337 EntryPoint v0.7
38
+ authKeyHex?: string; // HKDF auth key for relay server Authorization header
39
+ rpcUrl?: string; // Override chain RPC URL for public client reads
40
+ walletAddress?: string; // Smart Account address for billing (X-Wallet-Address header)
41
+ }
42
+
43
+ export interface FactPayload {
44
+ id: string;
45
+ timestamp: string;
46
+ owner: string; // Smart Account address (hex)
47
+ encryptedBlob: string; // Hex-encoded AES-256-GCM ciphertext
48
+ blindIndices: string[]; // SHA-256 hashes (word + LSH)
49
+ decayScore: number;
50
+ source: string;
51
+ contentFp: string;
52
+ agentId: string;
53
+ encryptedEmbedding?: string;
54
+ }
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Protobuf encoding (unchanged)
58
+ // ---------------------------------------------------------------------------
59
+
60
+ /**
61
+ * Encode a fact payload as a minimal Protobuf wire format.
62
+ *
63
+ * Field numbers match server/proto/totalreclaw.proto:
64
+ * 1: id (string), 2: timestamp (string), 3: owner (string),
65
+ * 4: encrypted_blob (bytes), 5: blind_indices (repeated string),
66
+ * 6: decay_score (double), 7: is_active (bool), 8: version (int32),
67
+ * 9: source (string), 10: content_fp (string), 11: agent_id (string),
68
+ * 12: sequence_id (int64), 13: encrypted_embedding (string)
69
+ */
70
+ export function encodeFactProtobuf(fact: FactPayload): Buffer {
71
+ const parts: Buffer[] = [];
72
+
73
+ // Helper: encode a string field
74
+ const writeString = (fieldNumber: number, value: string) => {
75
+ if (!value) return;
76
+ const data = Buffer.from(value, 'utf-8');
77
+ const key = (fieldNumber << 3) | 2; // wire type 2 = length-delimited
78
+ parts.push(encodeVarint(key));
79
+ parts.push(encodeVarint(data.length));
80
+ parts.push(data);
81
+ };
82
+
83
+ // Helper: encode a bytes field
84
+ const writeBytes = (fieldNumber: number, value: Buffer) => {
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);
141
+ }
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // Chain helpers
145
+ // ---------------------------------------------------------------------------
146
+
147
+ /** Resolve a viem Chain object from chain ID */
148
+ function getChainFromId(chainId: number): Chain {
149
+ switch (chainId) {
150
+ case 100:
151
+ return gnosis;
152
+ case 10200:
153
+ return gnosisChiado;
154
+ default:
155
+ return gnosisChiado;
156
+ }
157
+ }
158
+
159
+ /** Build the relay bundler RPC URL from the relay server URL */
160
+ function getRelayBundlerUrl(relayUrl: string): string {
161
+ return `${relayUrl}/v1/bundler`;
162
+ }
163
+
164
+ // ---------------------------------------------------------------------------
165
+ // On-chain submission (Pimlico UserOps)
166
+ // ---------------------------------------------------------------------------
167
+
168
+ /**
169
+ * Submit a fact on-chain via ERC-4337 UserOp through the relay server.
170
+ *
171
+ * Builds a UserOp client-side using `permissionless` + `viem`:
172
+ * 1. Derives private key from mnemonic (BIP-39 + BIP-44 m/44'/60'/0'/0/0)
173
+ * 2. Creates a SimpleSmartAccount
174
+ * 3. Gets paymaster sponsorship (via relay proxy to Pimlico)
175
+ * 4. Signs and submits the UserOp to relay bundler endpoint
176
+ * 5. Waits for the transaction receipt
177
+ *
178
+ * The relay server proxies all bundler/paymaster JSON-RPC to Pimlico
179
+ * with its own API key. Clients never need a Pimlico API key.
180
+ */
181
+ export async function submitFactOnChain(
182
+ protobufPayload: Buffer,
183
+ config: SubgraphStoreConfig,
184
+ ): Promise<{ txHash: string; userOpHash: string; success: boolean }> {
185
+ if (!config.relayUrl) {
186
+ throw new Error('Relay URL (TOTALRECLAW_SERVER_URL) is required for on-chain submission');
187
+ }
188
+
189
+ if (!config.mnemonic) {
190
+ throw new Error('Mnemonic (TOTALRECLAW_MASTER_PASSWORD) is required for on-chain submission');
191
+ }
192
+
193
+ const chain = getChainFromId(config.chainId);
194
+ const bundlerRpcUrl = getRelayBundlerUrl(config.relayUrl);
195
+ const dataEdgeAddress = config.dataEdgeAddress as Address;
196
+ const entryPointAddr = (config.entryPointAddress || entryPoint07Address) as Address;
197
+
198
+ // Build authenticated transport for relay server proxy
199
+ const headers: Record<string, string> = {
200
+ 'X-TotalReclaw-Client': 'openclaw-plugin',
201
+ };
202
+ if (config.authKeyHex) headers['Authorization'] = `Bearer ${config.authKeyHex}`;
203
+ if (config.walletAddress) headers['X-Wallet-Address'] = config.walletAddress;
204
+
205
+ const authTransport = Object.keys(headers).length > 0
206
+ ? http(bundlerRpcUrl, { fetchOptions: { headers } })
207
+ : http(bundlerRpcUrl);
208
+
209
+ // 1. Derive EOA signer from mnemonic (BIP-44 m/44'/60'/0'/0/0)
210
+ const ownerAccount = mnemonicToAccount(config.mnemonic);
211
+
212
+ // 2. Create a public client for chain reads (using explicit RPC if configured,
213
+ // NOT the bundler proxy which only supports ERC-4337 JSON-RPC methods)
214
+ const publicClient = createPublicClient({
215
+ chain,
216
+ transport: config.rpcUrl ? http(config.rpcUrl) : http(),
217
+ });
218
+
219
+ // 3. Create Pimlico client for bundler + paymaster operations (via relay)
220
+ const pimlicoClient = createPimlicoClient({
221
+ chain,
222
+ transport: authTransport,
223
+ entryPoint: {
224
+ address: entryPointAddr,
225
+ version: '0.7',
226
+ },
227
+ });
228
+
229
+ // 4. Create a SimpleSmartAccount (auto-generates initCode if undeployed)
230
+ const smartAccount = await toSimpleSmartAccount({
231
+ client: publicClient,
232
+ owner: ownerAccount,
233
+ entryPoint: {
234
+ address: entryPointAddr,
235
+ version: '0.7',
236
+ },
237
+ });
238
+
239
+ // 5. Create smart account client wired to relay bundler + paymaster
240
+ const smartAccountClient = createSmartAccountClient({
241
+ account: smartAccount,
242
+ chain,
243
+ bundlerTransport: authTransport,
244
+ // Paymaster sponsorship proxied through relay to Pimlico
245
+ paymaster: pimlicoClient,
246
+ userOperation: {
247
+ estimateFeesPerGas: async () => {
248
+ return (await pimlicoClient.getUserOperationGasPrice()).fast;
249
+ },
250
+ },
251
+ });
252
+
253
+ // 6. Send the transaction: Smart Account execute(dataEdgeAddress, 0, protobufPayload)
254
+ // The DataEdge contract has a fallback() that emits Log(bytes), so the calldata
255
+ // IS the protobuf payload directly (no function selector needed).
256
+ // permissionless encodes the execute() call internally from to/value/data.
257
+ const calldata = `0x${protobufPayload.toString('hex')}` as Hex;
258
+
259
+ // Use sendUserOperation to get the userOpHash, then wait for receipt
260
+ const userOpHash = await smartAccountClient.sendUserOperation({
261
+ calls: [
262
+ {
263
+ to: dataEdgeAddress,
264
+ value: 0n,
265
+ data: calldata,
266
+ },
267
+ ],
268
+ });
269
+
270
+ // 7. Wait for the UserOp to be included in a transaction
271
+ const receipt = await pimlicoClient.waitForUserOperationReceipt({
272
+ hash: userOpHash,
273
+ });
274
+
275
+ return {
276
+ txHash: receipt.receipt.transactionHash,
277
+ userOpHash,
278
+ success: receipt.success,
279
+ };
280
+ }
281
+
282
+ // ---------------------------------------------------------------------------
283
+ // Configuration
284
+ // ---------------------------------------------------------------------------
285
+
286
+ /**
287
+ * Check if subgraph mode is enabled (i.e. using the managed service).
288
+ *
289
+ * Returns true when TOTALRECLAW_SELF_HOSTED is NOT set to "true".
290
+ * The managed service (subgraph mode) is the default.
291
+ */
292
+ export function isSubgraphMode(): boolean {
293
+ return process.env.TOTALRECLAW_SELF_HOSTED !== 'true';
294
+ }
295
+
296
+ /**
297
+ * Get subgraph configuration from environment variables.
298
+ *
299
+ * After the relay refactor, clients only need:
300
+ * - TOTALRECLAW_MASTER_PASSWORD -- BIP-39 mnemonic
301
+ * - TOTALRECLAW_SERVER_URL -- relay server URL (default: https://api.totalreclaw.xyz)
302
+ * - TOTALRECLAW_SELF_HOSTED -- set "true" to use self-hosted server (default: managed service)
303
+ * - TOTALRECLAW_CHAIN_ID -- optional, defaults to 100 (Gnosis mainnet)
304
+ *
305
+ * Removed from client-side config (now server-side only):
306
+ * - PIMLICO_API_KEY
307
+ * - TOTALRECLAW_SUBGRAPH_ENDPOINT
308
+ */
309
+ /**
310
+ * Derive the Smart Account address from a BIP-39 mnemonic.
311
+ * This is the on-chain owner identity used in the subgraph.
312
+ */
313
+ export async function deriveSmartAccountAddress(mnemonic: string, chainId?: number): Promise<string> {
314
+ const chain: Chain = (chainId ?? 100) === 100 ? gnosis : gnosisChiado;
315
+ const ownerAccount = mnemonicToAccount(mnemonic);
316
+ const entryPointAddr = (process.env.TOTALRECLAW_ENTRYPOINT_ADDRESS || DEFAULT_ENTRYPOINT_ADDRESS) as Address;
317
+ const rpcUrl = process.env.TOTALRECLAW_RPC_URL;
318
+
319
+ const publicClient = createPublicClient({
320
+ chain,
321
+ transport: rpcUrl ? http(rpcUrl) : http(),
322
+ });
323
+
324
+ const smartAccount = await toSimpleSmartAccount({
325
+ client: publicClient,
326
+ owner: ownerAccount,
327
+ entryPoint: {
328
+ address: entryPointAddr,
329
+ version: '0.7',
330
+ },
331
+ });
332
+
333
+ return smartAccount.address.toLowerCase();
334
+ }
335
+
336
+ export function getSubgraphConfig(): SubgraphStoreConfig {
337
+ return {
338
+ relayUrl: process.env.TOTALRECLAW_SERVER_URL || 'https://api.totalreclaw.xyz',
339
+ mnemonic: process.env.TOTALRECLAW_MASTER_PASSWORD || '',
340
+ cachePath: process.env.TOTALRECLAW_CACHE_PATH || `${process.env.HOME}/.totalreclaw/cache.enc`,
341
+ chainId: parseInt(process.env.TOTALRECLAW_CHAIN_ID || '100'),
342
+ dataEdgeAddress: process.env.TOTALRECLAW_DATA_EDGE_ADDRESS || DEFAULT_DATA_EDGE_ADDRESS,
343
+ entryPointAddress: process.env.TOTALRECLAW_ENTRYPOINT_ADDRESS || DEFAULT_ENTRYPOINT_ADDRESS,
344
+ rpcUrl: process.env.TOTALRECLAW_RPC_URL || undefined,
345
+ };
346
+ }