web3ql-client 1.2.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.
Files changed (86) hide show
  1. package/README.md +66 -0
  2. package/contracts/PublicKeyRegistry.sol +87 -0
  3. package/dist/src/access.d.ts +176 -0
  4. package/dist/src/access.d.ts.map +1 -0
  5. package/dist/src/access.js +283 -0
  6. package/dist/src/access.js.map +1 -0
  7. package/dist/src/batch.d.ts +107 -0
  8. package/dist/src/batch.d.ts.map +1 -0
  9. package/dist/src/batch.js +188 -0
  10. package/dist/src/batch.js.map +1 -0
  11. package/dist/src/cli.d.ts +40 -0
  12. package/dist/src/cli.d.ts.map +1 -0
  13. package/dist/src/cli.js +361 -0
  14. package/dist/src/cli.js.map +1 -0
  15. package/dist/src/constraints.d.ts +126 -0
  16. package/dist/src/constraints.d.ts.map +1 -0
  17. package/dist/src/constraints.js +192 -0
  18. package/dist/src/constraints.js.map +1 -0
  19. package/dist/src/crypto.d.ts +118 -0
  20. package/dist/src/crypto.d.ts.map +1 -0
  21. package/dist/src/crypto.js +192 -0
  22. package/dist/src/crypto.js.map +1 -0
  23. package/dist/src/factory-client.d.ts +106 -0
  24. package/dist/src/factory-client.d.ts.map +1 -0
  25. package/dist/src/factory-client.js +202 -0
  26. package/dist/src/factory-client.js.map +1 -0
  27. package/dist/src/index-cache.d.ts +156 -0
  28. package/dist/src/index-cache.d.ts.map +1 -0
  29. package/dist/src/index-cache.js +265 -0
  30. package/dist/src/index-cache.js.map +1 -0
  31. package/dist/src/index.d.ts +60 -0
  32. package/dist/src/index.d.ts.map +1 -0
  33. package/dist/src/index.js +60 -0
  34. package/dist/src/index.js.map +1 -0
  35. package/dist/src/migrations.d.ts +114 -0
  36. package/dist/src/migrations.d.ts.map +1 -0
  37. package/dist/src/migrations.js +173 -0
  38. package/dist/src/migrations.js.map +1 -0
  39. package/dist/src/model.d.ts +198 -0
  40. package/dist/src/model.d.ts.map +1 -0
  41. package/dist/src/model.js +379 -0
  42. package/dist/src/model.js.map +1 -0
  43. package/dist/src/query.d.ts +155 -0
  44. package/dist/src/query.d.ts.map +1 -0
  45. package/dist/src/query.js +386 -0
  46. package/dist/src/query.js.map +1 -0
  47. package/dist/src/registry.d.ts +45 -0
  48. package/dist/src/registry.d.ts.map +1 -0
  49. package/dist/src/registry.js +80 -0
  50. package/dist/src/registry.js.map +1 -0
  51. package/dist/src/schema-manager.d.ts +109 -0
  52. package/dist/src/schema-manager.d.ts.map +1 -0
  53. package/dist/src/schema-manager.js +259 -0
  54. package/dist/src/schema-manager.js.map +1 -0
  55. package/dist/src/table-client.d.ts +156 -0
  56. package/dist/src/table-client.d.ts.map +1 -0
  57. package/dist/src/table-client.js +292 -0
  58. package/dist/src/table-client.js.map +1 -0
  59. package/dist/src/typed-table.d.ts +159 -0
  60. package/dist/src/typed-table.d.ts.map +1 -0
  61. package/dist/src/typed-table.js +246 -0
  62. package/dist/src/typed-table.js.map +1 -0
  63. package/dist/src/types.d.ts +48 -0
  64. package/dist/src/types.d.ts.map +1 -0
  65. package/dist/src/types.js +222 -0
  66. package/dist/src/types.js.map +1 -0
  67. package/keyManager.js +337 -0
  68. package/package.json +38 -0
  69. package/src/access.ts +421 -0
  70. package/src/batch.ts +259 -0
  71. package/src/cli.ts +349 -0
  72. package/src/constraints.ts +283 -0
  73. package/src/crypto.ts +239 -0
  74. package/src/factory-client.ts +237 -0
  75. package/src/index-cache.ts +351 -0
  76. package/src/index.ts +171 -0
  77. package/src/migrations.ts +215 -0
  78. package/src/model.ts +538 -0
  79. package/src/query.ts +508 -0
  80. package/src/registry.ts +100 -0
  81. package/src/schema-manager.ts +301 -0
  82. package/src/table-client.ts +393 -0
  83. package/src/typed-table.ts +340 -0
  84. package/src/types.ts +284 -0
  85. package/tsconfig.json +22 -0
  86. package/walletUtils.js +204 -0
package/src/model.ts ADDED
@@ -0,0 +1,538 @@
1
+ /**
2
+ * @file model.ts
3
+ * @notice Web3QL ORM — Prisma-style Model class that unifies:
4
+ *
5
+ * • Encrypted record CRUD (via TypedTableClient under the hood)
6
+ * • On-chain COUNTER fields — automatically merged into every find result
7
+ * • Relation writes — `relatedCreate()` pays with native CELO or any ERC-20
8
+ * and atomically writes the source record + increments target counters
9
+ *
10
+ * Usage:
11
+ * ─────────────────────────────────────────────────────────────
12
+ * // 1. Define your models
13
+ * const projects = new Model<Project>('projects', projectsTableAddr, signer, keypair, {
14
+ * counterFields: ['vote_total', 'vote_count', 'tip_total', 'tip_count'],
15
+ * schema: projectSchema,
16
+ * });
17
+ *
18
+ * const votes = new Model<Vote>('votes', votesTableAddr, signer, keypair);
19
+ *
20
+ * // 2. Standard CRUD
21
+ * await projects.create(1n, { id: 1n, name: 'Web3QL' });
22
+ *
23
+ * // 3. findUnique — counter fields merged in automatically
24
+ * const p = await projects.findUnique(1n);
25
+ * // p = { id: 1n, name: 'Web3QL', vote_total: 420n, vote_count: 3n, ... }
26
+ *
27
+ * // 4. Relation write — pay 2 CELO, atomically save vote + update project counters
28
+ * await votes.relatedCreate({
29
+ * wire : wireAddress,
30
+ * id : 10n,
31
+ * data : { project_id: 1n, voter: myAddr, amount: 2n * 10n**18n },
32
+ * targetId : 1n,
33
+ * amount : 2n * 10n**18n, // native CELO
34
+ * });
35
+ *
36
+ * // 5. ERC-20 vote: approve first, then:
37
+ * await votes.relatedCreate({
38
+ * wire : wireAddress,
39
+ * id : 11n,
40
+ * data : { project_id: 1n, voter: myAddr, amount: 5_000000n },
41
+ * targetId : 1n,
42
+ * amount : 5_000000n, // 5 cUSD (6 decimals)
43
+ * token : CUSD_ADDRESS, // ERC-20 token — pre-approve wire contract first
44
+ * });
45
+ * ─────────────────────────────────────────────────────────────
46
+ */
47
+
48
+ import { ethers } from 'ethers';
49
+ import { EncryptedTableClient, Role } from './table-client.js';
50
+ import { TypedTableClient } from './typed-table.js';
51
+ import type {
52
+ FindManyOptions,
53
+ RecordWithId,
54
+ WhereTuple,
55
+ SchemaDefinition,
56
+ } from './typed-table.js';
57
+ import type { EncryptionKeypair } from './crypto.js';
58
+ import type { PublicKeyRegistryClient } from './registry.js';
59
+ export { Role };
60
+
61
+ // ─────────────────────────────────────────────────────────────
62
+ // Minimal ABIs
63
+ // ─────────────────────────────────────────────────────────────
64
+
65
+ const COUNTER_ABI = [
66
+ 'function counterValue(bytes32 targetKey, bytes32 field) external view returns (uint256)',
67
+ ] as const;
68
+
69
+ const WIRE_ABI = [
70
+ 'function relatedWrite(bytes32 sourceKey, bytes ciphertext, bytes encryptedKey, bytes32 targetKey, address token, uint256 amount) payable external',
71
+ 'function relatedWriteWithPermit(bytes32 sourceKey, bytes ciphertext, bytes encryptedKey, bytes32 targetKey, address token, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external',
72
+ 'function withdrawProjectFunds(bytes32 targetKey, address token, address to) external',
73
+ 'function withdrawAllProjectFunds(bytes32 targetKey, address to) external',
74
+ 'function projectBalances(bytes32 targetKey) external view returns (address[] tokens, uint256[] balances)',
75
+ 'function getAllowedTokens() external view returns (address[])',
76
+ ] as const;
77
+
78
+ const ERC20_ABI = [
79
+ 'function allowance(address owner, address spender) external view returns (uint256)',
80
+ 'function approve(address spender, uint256 amount) external returns (bool)',
81
+ 'function nonces(address owner) external view returns (uint256)',
82
+ 'function name() external view returns (string)',
83
+ 'function version() external view returns (string)',
84
+ 'function DOMAIN_SEPARATOR() external view returns (bytes32)',
85
+ ] as const;
86
+
87
+ // ─────────────────────────────────────────────────────────────
88
+ // Types
89
+ // ─────────────────────────────────────────────────────────────
90
+
91
+ export interface ModelOptions {
92
+ /**
93
+ * Names of COUNTER fields on this table.
94
+ * Counter values are stored in the on-chain `counters` mapping — NOT
95
+ * inside the encrypted ciphertext. They are automatically fetched
96
+ * and merged into every `findUnique` / `findMany` result.
97
+ */
98
+ counterFields?: string[];
99
+ /** Optional schema for validation, type coercion, NOT NULL, DEFAULT. */
100
+ schema? : SchemaDefinition;
101
+ }
102
+
103
+ /** Options for `relatedCreate()` */
104
+ export interface RelatedCreateOptions<T> {
105
+ /** Address of the deployed Web3QLRelationWire contract. */
106
+ wire : string;
107
+ /** Primary key of the NEW record being written to THIS table. */
108
+ id : bigint;
109
+ /** Payload to encrypt and store (COUNTER fields are excluded automatically). */
110
+ data : T;
111
+ /** Primary key of the TARGET record to increment counters on. */
112
+ targetId : bigint;
113
+ /** Name of the target table (needed for key derivation). */
114
+ targetTable : string;
115
+ /**
116
+ * Payment amount.
117
+ * - Native wire (token undefined / address(0)): amount in wei (msg.value)
118
+ * - ERC-20 wire (token set): amount in token's native units
119
+ */
120
+ amount? : bigint;
121
+ /**
122
+ * ERC-20 token address. Omit (or pass `undefined`) for native CELO.
123
+ * The wire contract must be pre-approved: call `model.approveWire(wire, token, amount)` once.
124
+ */
125
+ token? : string;
126
+ }
127
+
128
+ // ─────────────────────────────────────────────────────────────
129
+ // Model
130
+ // ─────────────────────────────────────────────────────────────
131
+
132
+ /**
133
+ * `Model<T>` — the Web3QL ORM entry point.
134
+ *
135
+ * T is the shape of the ENCRYPTED fields only (not counter fields).
136
+ * Counter fields are added automatically to every returned object.
137
+ */
138
+ export class Model<T extends Record<string, unknown>> {
139
+ readonly tableName : string;
140
+ readonly tableAddress : string;
141
+
142
+ private inner : TypedTableClient<T>;
143
+ private rawClient : EncryptedTableClient;
144
+ private counterFields : string[];
145
+ private counterFieldHashes: Map<string, string>; // name → keccak256
146
+ private signer : ethers.Signer;
147
+ private provider : ethers.Provider;
148
+
149
+ constructor(
150
+ tableName : string,
151
+ tableAddress : string,
152
+ signer : ethers.Signer,
153
+ keypair : EncryptionKeypair,
154
+ options? : ModelOptions,
155
+ ) {
156
+ this.tableName = tableName;
157
+ this.tableAddress = tableAddress;
158
+ this.signer = signer;
159
+ this.provider = signer.provider!;
160
+
161
+ const cf = options?.counterFields ?? [];
162
+ this.counterFields = cf;
163
+ this.counterFieldHashes = new Map(
164
+ cf.map((name) => [name, ethers.keccak256(ethers.toUtf8Bytes(name))])
165
+ );
166
+
167
+ this.rawClient = new EncryptedTableClient(tableAddress, signer, keypair);
168
+ this.inner = new TypedTableClient<T>(tableName, this.rawClient, options?.schema);
169
+ }
170
+
171
+ // ─────────────────────────────────────────────────────────────
172
+ // Key derivation
173
+ // ─────────────────────────────────────────────────────────────
174
+
175
+ /** Derive the on-chain bytes32 record key for a given primary key. */
176
+ key(id: bigint): string {
177
+ return this.rawClient.deriveKey(this.tableName, id);
178
+ }
179
+
180
+ // ─────────────────────────────────────────────────────────────
181
+ // Counter reads (public — no auth needed)
182
+ // ─────────────────────────────────────────────────────────────
183
+
184
+ /** Read a single named counter for the record at `id`. */
185
+ async counter(id: bigint, field: string): Promise<bigint> {
186
+ const hash = this.counterFieldHashes.get(field)
187
+ ?? ethers.keccak256(ethers.toUtf8Bytes(field));
188
+ const contract = new ethers.Contract(this.tableAddress, COUNTER_ABI, this.provider);
189
+ const raw = await (contract as any)['counterValue'](this.key(id), hash);
190
+ return BigInt(raw);
191
+ }
192
+
193
+ /**
194
+ * Read ALL registered counter fields for the record at `id`.
195
+ * Returns a plain object mapping field name → bigint value.
196
+ */
197
+ async counters(id: bigint): Promise<Record<string, bigint>> {
198
+ if (this.counterFields.length === 0) return {};
199
+ const contract = new ethers.Contract(this.tableAddress, COUNTER_ABI, this.provider);
200
+ const key = this.key(id);
201
+ const values = await Promise.all(
202
+ this.counterFields.map(async (name) => {
203
+ const hash = this.counterFieldHashes.get(name)!;
204
+ const raw = await (contract as any)['counterValue'](key, hash);
205
+ return [name, BigInt(raw)] as [string, bigint];
206
+ })
207
+ );
208
+ return Object.fromEntries(values);
209
+ }
210
+
211
+ // ─────────────────────────────────────────────────────────────
212
+ // CRUD — counter fields merged into results automatically
213
+ // ─────────────────────────────────────────────────────────────
214
+
215
+ /**
216
+ * Encrypt and store a new record.
217
+ * Pass only encrypted fields — counter fields are managed by the chain.
218
+ */
219
+ async create(id: bigint, data: T): Promise<ethers.TransactionReceipt> {
220
+ return this.inner.create(id, data);
221
+ }
222
+
223
+ /**
224
+ * Read and decrypt a record, with all counter values merged in.
225
+ * Returns `null` if the record does not exist.
226
+ */
227
+ async findUnique(id: bigint): Promise<(T & Record<string, bigint>) | null> {
228
+ const [row, counterValues] = await Promise.all([
229
+ this.inner.findUnique(id),
230
+ this.counters(id),
231
+ ]);
232
+ if (row === null) return null;
233
+ return { ...row, ...counterValues } as T & Record<string, bigint>;
234
+ }
235
+
236
+ /**
237
+ * List and decrypt all records owned by `ownerAddress`.
238
+ * Counter values are merged into every record.
239
+ */
240
+ async findMany(
241
+ ownerAddress : string,
242
+ options? : FindManyOptions,
243
+ ): Promise<RecordWithId<T & Record<string, bigint>>[]> {
244
+ const rows = await this.inner.findMany(ownerAddress, options);
245
+ if (this.counterFields.length === 0) {
246
+ return rows as RecordWithId<T & Record<string, bigint>>[];
247
+ }
248
+
249
+ // Fetch counters for each record in parallel
250
+ const withCounters = await Promise.all(
251
+ rows.map(async (row) => {
252
+ // We need to re-derive id from recordKey — we can't, but we can derive
253
+ // key from recordKey directly: the recordKey IS the bytes32 key
254
+ const counterValues = await this._countersFromBytes32Key(row.recordKey);
255
+ return {
256
+ ...row,
257
+ data: { ...row.data, ...counterValues } as T & Record<string, bigint>,
258
+ };
259
+ })
260
+ );
261
+ return withCounters;
262
+ }
263
+
264
+ /** Get all records (no chain limit). Counter values merged. */
265
+ async findAll(ownerAddress: string): Promise<RecordWithId<T & Record<string, bigint>>[]> {
266
+ const total = Number(await this.inner.count(ownerAddress));
267
+ return this.findMany(ownerAddress, { chainOffset: 0n, chainLimit: BigInt(total) });
268
+ }
269
+
270
+ /**
271
+ * Patch an existing record — fetches current, merges patch, re-encrypts.
272
+ * Counter fields in patch are silently ignored (they live on-chain, not in ciphertext).
273
+ */
274
+ async update(id: bigint, patch: Partial<T>): Promise<ethers.TransactionReceipt> {
275
+ // Strip counter fields from patch — they can't be written via update
276
+ const safePatch = { ...patch };
277
+ for (const cf of this.counterFields) delete (safePatch as Record<string, unknown>)[cf];
278
+ return this.inner.update(id, safePatch);
279
+ }
280
+
281
+ /** Delete a record (soft-delete — scrubs all encrypted key copies). */
282
+ async delete(id: bigint): Promise<ethers.TransactionReceipt> {
283
+ return this.inner.remove(id);
284
+ }
285
+
286
+ /** True if a live record exists for `id`. */
287
+ async exists(id: bigint): Promise<boolean> {
288
+ return this.inner.exists(id);
289
+ }
290
+
291
+ /** Total record count (including deleted) for `ownerAddress`. */
292
+ async count(ownerAddress: string): Promise<bigint> {
293
+ return this.inner.count(ownerAddress);
294
+ }
295
+
296
+ // ─────────────────────────────────────────────────────────────
297
+ // Access control (pass-through to raw client)
298
+ // ─────────────────────────────────────────────────────────────
299
+
300
+ async share(
301
+ id : bigint,
302
+ recipient : string,
303
+ role : Role,
304
+ registry : PublicKeyRegistryClient,
305
+ ): Promise<ethers.TransactionReceipt> {
306
+ return this.rawClient.share(this.key(id), recipient, role, registry);
307
+ }
308
+
309
+ async revoke(id: bigint, user: string): Promise<ethers.TransactionReceipt> {
310
+ return this.rawClient.revoke(this.key(id), user);
311
+ }
312
+
313
+ // ─────────────────────────────────────────────────────────────
314
+ // Relation write (the key feature)
315
+ // ─────────────────────────────────────────────────────────────
316
+
317
+ /**
318
+ * Write a record to THIS table via a RelationWire, atomically incrementing
319
+ * counter fields on the TARGET table in the same transaction.
320
+ *
321
+ * For native CELO: pass `amount` in wei, do NOT pass `token`.
322
+ * For ERC-20: pass `token` address + `amount` in token units.
323
+ * You must have approved the wire contract beforehand —
324
+ * call `model.approveWire(wire, token, amount)` once.
325
+ */
326
+ async relatedCreate(opts: RelatedCreateOptions<T>): Promise<ethers.TransactionReceipt> {
327
+ const { wire, id, data, targetId, targetTable, amount = 0n, token } = opts;
328
+ const isErc20 = !!token && token !== ethers.ZeroAddress;
329
+ const isNative = !isErc20;
330
+
331
+ const { ciphertextBytes, encryptedKeyBytes } = await this._encryptPayload(id, data);
332
+
333
+ const sourceKey = this.key(id);
334
+ const targetKey = ethers.keccak256(
335
+ ethers.solidityPacked(['string', 'uint256'], [targetTable, targetId])
336
+ );
337
+
338
+ const wireContract = new ethers.Contract(wire, WIRE_ABI, this.signer);
339
+
340
+ if (isNative) {
341
+ const tx = await (wireContract as any)['relatedWrite'](
342
+ sourceKey, ciphertextBytes, encryptedKeyBytes, targetKey,
343
+ ethers.ZeroAddress, 0n,
344
+ { value: amount },
345
+ );
346
+ return tx.wait();
347
+ }
348
+
349
+ // ── ERC-20: try permit (gasless approve), fall back to standard approve ──
350
+ const signerAddr = await this.signer.getAddress();
351
+
352
+ let usedPermit = false;
353
+ try {
354
+ const sig = await this._signPermit(token!, wire, amount, signerAddr);
355
+ const tx = await (wireContract as any)['relatedWriteWithPermit'](
356
+ sourceKey, ciphertextBytes, encryptedKeyBytes, targetKey,
357
+ token!, amount,
358
+ sig.deadline, sig.v, sig.r, sig.s,
359
+ );
360
+ const receipt = await tx.wait();
361
+ usedPermit = true;
362
+ return receipt;
363
+ } catch {
364
+ // Token doesn't support EIP-2612 permit, or wallet can't sign typed data — fall through
365
+ }
366
+
367
+ if (!usedPermit) {
368
+ // Standard path: check allowance, approve if needed, then relatedWrite
369
+ const tokenContract = new ethers.Contract(token!, ERC20_ABI, this.signer);
370
+ const allowance = BigInt(await (tokenContract as any)['allowance'](signerAddr, wire));
371
+ if (allowance < amount) {
372
+ const approveTx = await (tokenContract as any)['approve'](wire, ethers.MaxUint256);
373
+ await approveTx.wait();
374
+ }
375
+ const tx = await (wireContract as any)['relatedWrite'](
376
+ sourceKey, ciphertextBytes, encryptedKeyBytes, targetKey,
377
+ token!, amount,
378
+ );
379
+ return tx.wait();
380
+ }
381
+
382
+ // unreachable but satisfies TypeScript
383
+ throw new Error('relatedCreate: unexpected state');
384
+ }
385
+
386
+ /**
387
+ * Approve a RelationWire to spend your ERC-20 tokens.
388
+ * Call this once before using `relatedCreate` with an ERC-20 wire.
389
+ * You can approve `MaxUint256` for unlimited allowance.
390
+ */
391
+ async approveWire(
392
+ wire : string,
393
+ token : string,
394
+ amount : bigint = ethers.MaxUint256,
395
+ ): Promise<ethers.TransactionReceipt> {
396
+ const tokenContract = new ethers.Contract(token, ERC20_ABI, this.signer);
397
+ const tx = await (tokenContract as any)['approve'](wire, amount);
398
+ return tx.wait();
399
+ }
400
+
401
+ /**
402
+ * Withdraw a single token's accumulated payments for a project record.
403
+ * Caller must be the record owner of `id` on this table.
404
+ *
405
+ * @param wire Address of the RelationWire contract
406
+ * @param id Primary key of THIS table's record (the project)
407
+ * @param token Token to withdraw (ethers.ZeroAddress = native CELO)
408
+ * @param to Recipient address (defaults to signer)
409
+ */
410
+ async withdrawFunds(
411
+ wire : string,
412
+ id : bigint,
413
+ token : string = ethers.ZeroAddress,
414
+ to? : string,
415
+ ): Promise<ethers.TransactionReceipt> {
416
+ const recipient = to ?? await this.signer.getAddress();
417
+ const wireContract = new ethers.Contract(wire, WIRE_ABI, this.signer);
418
+ const tx = await (wireContract as any)['withdrawProjectFunds'](
419
+ this.key(id), token, recipient
420
+ );
421
+ return tx.wait();
422
+ }
423
+
424
+ /**
425
+ * Withdraw ALL token balances for a project in one transaction.
426
+ * Caller must be the record owner of `id` on this table.
427
+ *
428
+ * @param wire Address of the RelationWire contract
429
+ * @param id Primary key of the project record
430
+ * @param to Recipient address (defaults to signer)
431
+ */
432
+ async withdrawAllFunds(
433
+ wire : string,
434
+ id : bigint,
435
+ to? : string,
436
+ ): Promise<ethers.TransactionReceipt> {
437
+ const recipient = to ?? await this.signer.getAddress();
438
+ const wireContract = new ethers.Contract(wire, WIRE_ABI, this.signer);
439
+ const tx = await (wireContract as any)['withdrawAllProjectFunds'](
440
+ this.key(id), recipient
441
+ );
442
+ return tx.wait();
443
+ }
444
+
445
+ /**
446
+ * Check pending balances for a project across all tokens accepted by the wire.
447
+ * Returns a plain object: { tokenAddress: bigintBalance, ... }
448
+ *
449
+ * @param wire Address of the RelationWire contract
450
+ * @param id Primary key of the project record
451
+ */
452
+ async projectBalances(
453
+ wire : string,
454
+ id : bigint,
455
+ ): Promise<Record<string, bigint>> {
456
+ const wireContract = new ethers.Contract(wire, WIRE_ABI, this.provider);
457
+ const { tokens, balances } = await (wireContract as any)['projectBalances'](this.key(id));
458
+ const result: Record<string, bigint> = {};
459
+ for (let i = 0; i < tokens.length; i++) {
460
+ result[tokens[i]] = BigInt(balances[i]);
461
+ }
462
+ return result;
463
+ }
464
+
465
+ // ─────────────────────────────────────────────────────────────
466
+ // Private helpers
467
+ // ─────────────────────────────────────────────────────────────
468
+
469
+ /**
470
+ * Build and sign an EIP-2612 permit signature.
471
+ * Works for cUSD, cEUR, cREAL, USDC and any token that implements ERC-2612.
472
+ * Throws if the token doesn't expose `nonces()` or `DOMAIN_SEPARATOR()`.
473
+ */
474
+ private async _signPermit(
475
+ token : string,
476
+ spender : string,
477
+ value : bigint,
478
+ owner : string,
479
+ ttl : number = 20 * 60, // 20 minutes
480
+ ): Promise<{ deadline: bigint; v: number; r: string; s: string }> {
481
+ const tokenContract = new ethers.Contract(token, ERC20_ABI, this.provider);
482
+
483
+ const [nonce, name, deadline] = await Promise.all([
484
+ (tokenContract as any)['nonces'](owner).then(BigInt),
485
+ (tokenContract as any)['name'](),
486
+ Promise.resolve(BigInt(Math.floor(Date.now() / 1000) + ttl)),
487
+ ]);
488
+
489
+ // Try ERC-2612 version(); many tokens omit it and default to "1"
490
+ let version = '1';
491
+ try { version = await (tokenContract as any)['version'](); } catch { /* default "1" */ }
492
+
493
+ const network = await this.provider.getNetwork();
494
+ const domain = { name, version, chainId: Number(network.chainId), verifyingContract: token };
495
+ const types = {
496
+ Permit: [
497
+ { name: 'owner', type: 'address' },
498
+ { name: 'spender', type: 'address' },
499
+ { name: 'value', type: 'uint256' },
500
+ { name: 'nonce', type: 'uint256' },
501
+ { name: 'deadline', type: 'uint256' },
502
+ ],
503
+ };
504
+ const message = { owner, spender, value, nonce, deadline };
505
+
506
+ const sig = await (this.signer as ethers.Signer & {
507
+ signTypedData(domain: object, types: object, value: object): Promise<string>;
508
+ }).signTypedData(domain, types, message);
509
+
510
+ const { v, r, s } = ethers.Signature.from(sig);
511
+ return { deadline, v, r, s };
512
+ }
513
+ private async _countersFromBytes32Key(recordKey: string): Promise<Record<string, bigint>> {
514
+ if (this.counterFields.length === 0) return {};
515
+ const contract = new ethers.Contract(this.tableAddress, COUNTER_ABI, this.provider);
516
+ const values = await Promise.all(
517
+ this.counterFields.map(async (name) => {
518
+ const hash = this.counterFieldHashes.get(name)!;
519
+ const raw = await (contract as any)['counterValue'](recordKey, hash);
520
+ return [name, BigInt(raw)] as [string, bigint];
521
+ })
522
+ );
523
+ return Object.fromEntries(values);
524
+ }
525
+
526
+ /**
527
+ * Encrypt a payload for self, returning raw bytes for the wire contract.
528
+ */
529
+ private async _encryptPayload(
530
+ _id : bigint,
531
+ data : T,
532
+ ): Promise<{ ciphertextBytes: Uint8Array; encryptedKeyBytes: Uint8Array }> {
533
+ const { ciphertext, encryptedKey } = await this.rawClient.encryptForSelf(
534
+ JSON.stringify(data)
535
+ );
536
+ return { ciphertextBytes: ciphertext, encryptedKeyBytes: encryptedKey };
537
+ }
538
+ }