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/query.ts ADDED
@@ -0,0 +1,508 @@
1
+ /**
2
+ * @file query.ts
3
+ * @notice Web3QL v1.1 — client-side query engine.
4
+ *
5
+ * After decrypting records from the chain, this module provides:
6
+ * • WHERE filtering — eq, ne, gt, gte, lt, lte, in, notIn, like, between, isNull, isNotNull
7
+ * • ORDER BY — multi-column, ASC/DESC
8
+ * • LIMIT / OFFSET — pagination over decrypted records
9
+ * • SELECT projection — pick only specific fields
10
+ * • DISTINCT — deduplicate on a column value
11
+ * • COUNT / SUM / AVG / MIN / MAX / GROUP BY aggregations
12
+ *
13
+ * All operations run in-process on the decrypted plaintext — the chain
14
+ * sees only ciphertext. For large tables, use the relay-maintained index
15
+ * endpoint (v1.2) to avoid decrypting every record.
16
+ *
17
+ * Usage:
18
+ * ─────────────────────────────────────────────────────────────
19
+ * const results = query(records)
20
+ * .where('age', 'gt', 18n)
21
+ * .where('name', 'like', 'Ali%')
22
+ * .orderBy('age', 'asc')
23
+ * .limit(10)
24
+ * .select(['name', 'age'])
25
+ * .execute();
26
+ *
27
+ * const stats = query(records)
28
+ * .where('active', 'eq', true)
29
+ * .aggregate({ count: '*', avg: 'score', max: 'score' });
30
+ * ─────────────────────────────────────────────────────────────
31
+ */
32
+
33
+ // ─────────────────────────────────────────────────────────────
34
+ // Types
35
+ // ─────────────────────────────────────────────────────────────
36
+
37
+ export type Row = Record<string, unknown>;
38
+
39
+ export type WhereOperator =
40
+ | 'eq' | 'ne'
41
+ | 'gt' | 'gte' | 'lt' | 'lte'
42
+ | 'in' | 'notIn'
43
+ | 'like' | 'ilike'
44
+ | 'between'
45
+ | 'isNull' | 'isNotNull';
46
+
47
+ export interface WhereClause {
48
+ field : string;
49
+ op : WhereOperator;
50
+ /** Single value for eq/ne/gt/gte/lt/lte/like/ilike/isNull/isNotNull */
51
+ value? : unknown;
52
+ /** Array for `in` and `notIn` */
53
+ values? : unknown[];
54
+ /** Two-element tuple for `between` */
55
+ range? : [unknown, unknown];
56
+ }
57
+
58
+ export type SortDirection = 'asc' | 'desc';
59
+
60
+ export interface OrderByClause {
61
+ field : string;
62
+ direction: SortDirection;
63
+ }
64
+
65
+ export interface AggregateOptions {
66
+ count?: '*' | string;
67
+ sum? : string;
68
+ avg? : string;
69
+ min? : string;
70
+ max? : string;
71
+ groupBy?: string;
72
+ /** Time-bucketing: group timestamps by 'minute'|'hour'|'day'|'week'|'month'|'year' */
73
+ timeBucket?: { field: string; unit: TimeBucketUnit };
74
+ /** HAVING — filter on aggregated values (applied after aggregation) */
75
+ having?: HavingClause[];
76
+ }
77
+
78
+ export type TimeBucketUnit = 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year';
79
+
80
+ export interface HavingClause {
81
+ /** 'count'|'sum'|'avg'|'min'|'max' */
82
+ aggregate: 'count' | 'sum' | 'avg';
83
+ op : 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte';
84
+ value : number;
85
+ }
86
+
87
+ export interface AggregateResult {
88
+ group?: unknown;
89
+ count?: number;
90
+ sum? : number;
91
+ avg? : number;
92
+ min? : unknown;
93
+ max? : unknown;
94
+ }
95
+
96
+ // ─────────────────────────────────────────────────────────────
97
+ // JOIN types
98
+ // ─────────────────────────────────────────────────────────────
99
+
100
+ export type JoinType = 'inner' | 'left' | 'right';
101
+
102
+ export interface JoinClause {
103
+ type : JoinType;
104
+ right : Row[];
105
+ on : { left: string; right: string };
106
+ /** Prefix added to right-side fields to avoid collision. Default: 'j_' */
107
+ prefix? : string;
108
+ }
109
+
110
+ // ─────────────────────────────────────────────────────────────
111
+ // Time-bucket helper
112
+ // ─────────────────────────────────────────────────────────────
113
+
114
+ function truncateTimestamp(ms: number, unit: TimeBucketUnit): number {
115
+ const d = new Date(ms);
116
+ switch (unit) {
117
+ case 'minute':
118
+ d.setUTCSeconds(0, 0);
119
+ break;
120
+ case 'hour':
121
+ d.setUTCMinutes(0, 0, 0);
122
+ break;
123
+ case 'day':
124
+ d.setUTCHours(0, 0, 0, 0);
125
+ break;
126
+ case 'week': {
127
+ const dow = d.getUTCDay(); // 0=Sun
128
+ d.setUTCDate(d.getUTCDate() - dow);
129
+ d.setUTCHours(0, 0, 0, 0);
130
+ break;
131
+ }
132
+ case 'month':
133
+ d.setUTCDate(1);
134
+ d.setUTCHours(0, 0, 0, 0);
135
+ break;
136
+ case 'year':
137
+ d.setUTCMonth(0, 1);
138
+ d.setUTCHours(0, 0, 0, 0);
139
+ break;
140
+ }
141
+ return d.getTime();
142
+ }
143
+
144
+ // ─────────────────────────────────────────────────────────────
145
+ // HAVING filter
146
+ // ─────────────────────────────────────────────────────────────
147
+
148
+ function applyHaving(result: AggregateResult, having: HavingClause[]): boolean {
149
+ for (const h of having) {
150
+ const val = result[h.aggregate] ?? 0;
151
+ switch (h.op) {
152
+ case 'eq': if (val !== h.value) return false; break;
153
+ case 'ne': if (val === h.value) return false; break;
154
+ case 'gt': if (val <= h.value) return false; break;
155
+ case 'gte': if (val < h.value) return false; break;
156
+ case 'lt': if (val >= h.value) return false; break;
157
+ case 'lte': if (val > h.value) return false; break;
158
+ }
159
+ }
160
+ return true;
161
+ }
162
+
163
+ // ─────────────────────────────────────────────────────────────
164
+ // Predicate evaluator
165
+ // ─────────────────────────────────────────────────────────────
166
+
167
+ function likeToRegex(pattern: string, caseInsensitive: boolean): RegExp {
168
+ const escaped = pattern
169
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
170
+ .replace(/%/g, '.*')
171
+ .replace(/_/g, '.');
172
+ return new RegExp(`^${escaped}$`, caseInsensitive ? 'i' : '');
173
+ }
174
+
175
+ function compare(a: unknown, b: unknown): number {
176
+ if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime();
177
+ if (typeof a === 'bigint' && typeof b === 'bigint') return a < b ? -1 : a > b ? 1 : 0;
178
+ if (typeof a === 'number' && typeof b === 'number') return a - b;
179
+ return String(a).localeCompare(String(b));
180
+ }
181
+
182
+ function applyWhere(row: Row, clause: WhereClause): boolean {
183
+ const rawVal = row[clause.field];
184
+
185
+ switch (clause.op) {
186
+ case 'eq': return rawVal === clause.value;
187
+ case 'ne': return rawVal !== clause.value;
188
+ case 'gt': return compare(rawVal, clause.value) > 0;
189
+ case 'gte': return compare(rawVal, clause.value) >= 0;
190
+ case 'lt': return compare(rawVal, clause.value) < 0;
191
+ case 'lte': return compare(rawVal, clause.value) <= 0;
192
+ case 'in': return (clause.values ?? []).includes(rawVal);
193
+ case 'notIn': return !(clause.values ?? []).includes(rawVal);
194
+ case 'like': return likeToRegex(String(clause.value), false).test(String(rawVal));
195
+ case 'ilike': return likeToRegex(String(clause.value), true).test(String(rawVal));
196
+ case 'between': {
197
+ const [lo, hi] = clause.range!;
198
+ return compare(rawVal, lo) >= 0 && compare(rawVal, hi) <= 0;
199
+ }
200
+ case 'isNull': return rawVal === null || rawVal === undefined;
201
+ case 'isNotNull': return rawVal !== null && rawVal !== undefined;
202
+ default: return true;
203
+ }
204
+ }
205
+
206
+ // ─────────────────────────────────────────────────────────────
207
+ // QueryBuilder
208
+ // ─────────────────────────────────────────────────────────────
209
+
210
+ export class QueryBuilder<T extends Row> {
211
+ private _rows : T[];
212
+ private _wheres : WhereClause[] = [];
213
+ private _orders : OrderByClause[] = [];
214
+ private _limitN? : number;
215
+ private _offsetN?: number;
216
+ private _fields? : string[];
217
+ private _distinct?: string;
218
+ private _joins : JoinClause[] = [];
219
+
220
+ constructor(rows: T[]) {
221
+ this._rows = rows;
222
+ }
223
+
224
+ // ── Filtering ───────────────────────────────────────────────
225
+
226
+ /**
227
+ * Add a WHERE condition. Multiple calls are ANDed together.
228
+ *
229
+ * @example
230
+ * .where('age', 'gt', 18n)
231
+ * .where('status', 'in', undefined, ['active', 'pending'])
232
+ * .where('score', 'between', undefined, undefined, [0, 100])
233
+ * .where('deletedAt', 'isNull')
234
+ */
235
+ where(field: string, op: 'isNull' | 'isNotNull') : this;
236
+ where(field: string, op: 'in' | 'notIn', values: unknown[]) : this;
237
+ where(field: string, op: 'between', range: [unknown, unknown]) : this;
238
+ where(field: string, op: WhereOperator, value?: unknown) : this;
239
+ where(
240
+ field : string,
241
+ op : WhereOperator,
242
+ valueOrArr?: unknown,
243
+ _unused2?: unknown,
244
+ ): this {
245
+ if (op === 'isNull' || op === 'isNotNull') {
246
+ this._wheres.push({ field, op });
247
+ } else if (op === 'in' || op === 'notIn') {
248
+ this._wheres.push({ field, op, values: valueOrArr as unknown[] });
249
+ } else if (op === 'between') {
250
+ this._wheres.push({ field, op, range: valueOrArr as [unknown, unknown] });
251
+ } else {
252
+ this._wheres.push({ field, op, value: valueOrArr });
253
+ }
254
+ return this;
255
+ }
256
+
257
+ // ── Sorting ─────────────────────────────────────────────────
258
+
259
+ /** Add an ORDER BY clause. Multiple calls are applied in sequence. */
260
+ orderBy(field: string, direction: SortDirection = 'asc'): this {
261
+ this._orders.push({ field, direction });
262
+ return this;
263
+ }
264
+
265
+ // ── Pagination ──────────────────────────────────────────────
266
+
267
+ limit(n: number): this { this._limitN = n; return this; }
268
+ offset(n: number): this { this._offsetN = n; return this; }
269
+
270
+ // ── Projection ──────────────────────────────────────────────
271
+
272
+ /** Return only the specified fields from each row. */
273
+ select(fields: string[]): this { this._fields = fields; return this; }
274
+
275
+ /** Deduplicate rows where `field` has the same value. */
276
+ distinct(field: string): this { this._distinct = field; return this; }
277
+
278
+ // ── JOIN ────────────────────────────────────────────────────
279
+
280
+ /**
281
+ * Join this table with a `right` array on matching key columns.
282
+ *
283
+ * @example
284
+ * query(orders)
285
+ * .join('inner', users, { left: 'userId', right: 'id' })
286
+ * .execute()
287
+ * // Each matched row = order fields + user fields prefixed with 'j_'
288
+ *
289
+ * query(orders)
290
+ * .join('left', users, { left: 'userId', right: 'id' }, 'user_')
291
+ * .select(['id', 'user_name', 'amount'])
292
+ * .execute()
293
+ */
294
+ join(
295
+ type : JoinType,
296
+ right : Row[],
297
+ on : { left: string; right: string },
298
+ prefix: string = 'j_',
299
+ ): this {
300
+ this._joins.push({ type, right, on, prefix });
301
+ return this;
302
+ }
303
+
304
+ // ── Terminal: execute ───────────────────────────────────────
305
+
306
+ execute(): Partial<T>[] {
307
+ // 0. Apply JOINs
308
+ let rows: Row[] = [...this._rows];
309
+ for (const j of this._joins) {
310
+ rows = applyJoin(rows, j);
311
+ }
312
+
313
+ // 1. Filter
314
+ rows = this._wheres.length
315
+ ? rows.filter((r) => this._wheres.every((w) => applyWhere(r, w)))
316
+ : rows;
317
+
318
+ // 2. Distinct
319
+ if (this._distinct) {
320
+ const seen = new Set<unknown>();
321
+ const field = this._distinct;
322
+ rows = rows.filter((r) => {
323
+ const v = r[field];
324
+ if (seen.has(v)) return false;
325
+ seen.add(v);
326
+ return true;
327
+ });
328
+ }
329
+
330
+ // 3. Sort
331
+ if (this._orders.length) {
332
+ rows.sort((a, b) => {
333
+ for (const ord of this._orders) {
334
+ const cmp = compare(a[ord.field], b[ord.field]);
335
+ if (cmp !== 0) return ord.direction === 'asc' ? cmp : -cmp;
336
+ }
337
+ return 0;
338
+ });
339
+ }
340
+
341
+ // 4. Offset + Limit
342
+ const start = this._offsetN ?? 0;
343
+ const end = this._limitN != null ? start + this._limitN : undefined;
344
+ rows = rows.slice(start, end);
345
+
346
+ // 5. Projection
347
+ if (this._fields) {
348
+ const fields = this._fields;
349
+ return rows.map((r) => {
350
+ const out: Row = {};
351
+ for (const f of fields) out[f] = r[f];
352
+ return out as Partial<T>;
353
+ });
354
+ }
355
+
356
+ return rows as Partial<T>[];
357
+ }
358
+
359
+ // ── Terminal: aggregate ─────────────────────────────────────
360
+
361
+ /**
362
+ * Run aggregation functions over filtered (but not sorted/limited) rows.
363
+ *
364
+ * @example
365
+ * query(rows).where('active', 'eq', true).aggregate({ count: '*', avg: 'score' })
366
+ * // => [{ count: 42, avg: 78.5 }]
367
+ *
368
+ * query(rows).aggregate({ count: '*', groupBy: 'status' })
369
+ * // => [{ group: 'active', count: 30 }, { group: 'inactive', count: 12 }]
370
+ */
371
+ aggregate(opts: AggregateOptions): AggregateResult[] {
372
+ // Apply JOINs first
373
+ let allRows: Row[] = [...this._rows];
374
+ for (const j of this._joins) allRows = applyJoin(allRows, j);
375
+
376
+ const filtered: Row[] = this._wheres.length
377
+ ? allRows.filter((r) => this._wheres.every((w) => applyWhere(r, w)))
378
+ : allRows;
379
+
380
+ // Determine grouping key
381
+ const getGroupKey = (row: Row): unknown => {
382
+ if (opts.timeBucket) {
383
+ const rawMs = Number(row[opts.timeBucket.field]);
384
+ return truncateTimestamp(rawMs, opts.timeBucket.unit);
385
+ }
386
+ if (opts.groupBy) return row[opts.groupBy];
387
+ return '__all__';
388
+ };
389
+
390
+ const groups = new Map<unknown, Row[]>();
391
+ for (const row of filtered) {
392
+ const gv = getGroupKey(row);
393
+ const bucket = groups.get(gv);
394
+ if (bucket) bucket.push(row);
395
+ else groups.set(gv, [row]);
396
+ }
397
+
398
+ const noGroupBy = !opts.groupBy && !opts.timeBucket;
399
+ if (noGroupBy) {
400
+ return [this._aggregateGroup('__all__', filtered, opts, noGroupBy)];
401
+ }
402
+
403
+ const results = Array.from(groups.entries()).map(([groupVal, rows]) =>
404
+ this._aggregateGroup(groupVal, rows, opts, false),
405
+ );
406
+
407
+ // Apply HAVING filter
408
+ if (opts.having?.length) {
409
+ return results.filter((r) => applyHaving(r, opts.having!));
410
+ }
411
+ return results;
412
+ }
413
+
414
+ private _aggregateGroup(
415
+ groupVal: unknown,
416
+ rows : Row[],
417
+ opts : AggregateOptions,
418
+ omitGroup: boolean,
419
+ ): AggregateResult {
420
+ const result: AggregateResult = {};
421
+ if (!omitGroup) result.group = groupVal;
422
+
423
+ if (opts.count != null) {
424
+ result.count = rows.length;
425
+ }
426
+ if (opts.sum) {
427
+ result.sum = rows.reduce((acc, r) => acc + Number(r[opts.sum!] ?? 0), 0);
428
+ }
429
+ if (opts.avg) {
430
+ result.avg = rows.length
431
+ ? rows.reduce((acc, r) => acc + Number(r[opts.avg!] ?? 0), 0) / rows.length
432
+ : 0;
433
+ }
434
+ if (opts.min) {
435
+ const vals = rows.map((r) => r[opts.min!]).filter((v) => v != null);
436
+ result.min = vals.reduce<unknown>((a, b) => (compare(a, b) <= 0 ? a : b), vals[0]);
437
+ }
438
+ if (opts.max) {
439
+ const vals = rows.map((r) => r[opts.max!]).filter((v) => v != null);
440
+ result.max = vals.reduce<unknown>((a, b) => (compare(a, b) >= 0 ? a : b), vals[0]);
441
+ }
442
+ return result;
443
+ }
444
+ }
445
+
446
+ // ─────────────────────────────────────────────────────────────
447
+ // JOIN implementation
448
+ // ─────────────────────────────────────────────────────────────
449
+
450
+ function applyJoin(left: Row[], j: JoinClause): Row[] {
451
+ const prefix = j.prefix ?? 'j_';
452
+ const rightIndex = new Map<unknown, Row[]>();
453
+ for (const r of j.right) {
454
+ const k = r[j.on.right];
455
+ const bucket = rightIndex.get(k);
456
+ if (bucket) bucket.push(r);
457
+ else rightIndex.set(k, [r]);
458
+ }
459
+
460
+ const result: Row[] = [];
461
+
462
+ for (const l of left) {
463
+ const k = l[j.on.left];
464
+ const matches = rightIndex.get(k);
465
+
466
+ if (matches && matches.length > 0) {
467
+ // INNER or LEFT: emit for each match
468
+ for (const r of matches) {
469
+ const merged: Row = { ...l };
470
+ for (const [rk, rv] of Object.entries(r)) {
471
+ merged[`${prefix}${rk}`] = rv;
472
+ }
473
+ result.push(merged);
474
+ }
475
+ } else {
476
+ // LEFT JOIN: emit left row with nulled right fields
477
+ if (j.type === 'left') {
478
+ result.push({ ...l });
479
+ }
480
+ // INNER JOIN: no match → skip
481
+ }
482
+ }
483
+
484
+ // RIGHT JOIN: also emit right rows with no match on left
485
+ if (j.type === 'right') {
486
+ const leftKeys = new Set(left.map((l) => l[j.on.left]));
487
+ for (const r of j.right) {
488
+ if (!leftKeys.has(r[j.on.right])) {
489
+ const merged: Row = {};
490
+ for (const [rk, rv] of Object.entries(r)) {
491
+ merged[`${prefix}${rk}`] = rv;
492
+ }
493
+ result.push(merged);
494
+ }
495
+ }
496
+ }
497
+
498
+ return result;
499
+ }
500
+
501
+ // ─────────────────────────────────────────────────────────────
502
+ // Factory helper
503
+ // ─────────────────────────────────────────────────────────────
504
+
505
+ /** Fluent entry point: `query(records).where(...).orderBy(...).execute()` */
506
+ export function query<T extends Row>(rows: T[]): QueryBuilder<T> {
507
+ return new QueryBuilder(rows);
508
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * @file registry.ts
3
+ * @notice Client for the on-chain PublicKeyRegistry contract.
4
+ *
5
+ * Why is a registry needed?
6
+ * ─────────────────────────────────────────────────────────────
7
+ * When the owner wants to share a record with Alice, they need
8
+ * Alice's X25519 public key to encrypt the symmetric key for her.
9
+ * The registry is a simple on-chain mapping (address → bytes32)
10
+ * where each user registers their own encryption public key once,
11
+ * paying only ~40k gas. After that anyone can look it up.
12
+ *
13
+ * Security note:
14
+ * The public key stored here is the X25519 key DERIVED from the
15
+ * Ethereum private key (sha256(ethPrivKey) → nacl keypair).
16
+ * It does NOT expose the Ethereum private key in any way.
17
+ */
18
+
19
+ import { ethers } from 'ethers';
20
+ import { publicKeyToHex, hexToPublicKey } from './crypto.js';
21
+ import type { EncryptionKeypair } from './crypto.js';
22
+
23
+ // ─────────────────────────────────────────────────────────────
24
+ // ABI — only what we need
25
+ // ─────────────────────────────────────────────────────────────
26
+
27
+ const REGISTRY_ABI = [
28
+ 'function register(bytes32 pubKey) external',
29
+ 'function getKey(address user) external view returns (bytes32)',
30
+ 'function hasKey(address user) external view returns (bool)',
31
+ 'event KeyRegistered(address indexed user, bytes32 publicKey)',
32
+ ] as const;
33
+
34
+ // ─────────────────────────────────────────────────────────────
35
+ // Client
36
+ // ─────────────────────────────────────────────────────────────
37
+
38
+ export class PublicKeyRegistryClient {
39
+ private contract: ethers.Contract;
40
+
41
+ constructor(
42
+ registryAddress : string,
43
+ signerOrProvider : ethers.Signer | ethers.Provider,
44
+ ) {
45
+ this.contract = new ethers.Contract(
46
+ registryAddress,
47
+ REGISTRY_ABI,
48
+ signerOrProvider,
49
+ );
50
+ }
51
+
52
+ /**
53
+ * Register the caller's encryption public key on-chain.
54
+ * Call this once per wallet — ~40k gas on Celo (~$0.001).
55
+ *
56
+ * @param keypair Your EncryptionKeypair from deriveKeypairFromWallet(signer).
57
+ */
58
+ async register(keypair: EncryptionKeypair): Promise<ethers.TransactionReceipt> {
59
+ const hex = publicKeyToHex(keypair.publicKey);
60
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
61
+ const tx = await (this.contract as any).register(hex) as { wait(): Promise<ethers.TransactionReceipt> };
62
+ return tx.wait();
63
+ }
64
+
65
+ /**
66
+ * Check whether an address has registered a public key.
67
+ */
68
+ async hasKey(address: string): Promise<boolean> {
69
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
70
+ return (this.contract as any).hasKey(address) as Promise<boolean>;
71
+ }
72
+
73
+ /**
74
+ * Get the X25519 public key for an address.
75
+ * Throws if the address has not registered.
76
+ */
77
+ async getPublicKey(address: string): Promise<Uint8Array> {
78
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
79
+ const hex = await (this.contract as any).getKey(address) as string;
80
+ return hexToPublicKey(hex);
81
+ }
82
+
83
+ /**
84
+ * Get public keys for multiple addresses in a single batch of calls.
85
+ * Returns `null` for any address that has not registered.
86
+ */
87
+ async getPublicKeys(
88
+ addresses: string[],
89
+ ): Promise<(Uint8Array | null)[]> {
90
+ return Promise.all(
91
+ addresses.map(async (addr) => {
92
+ try {
93
+ return await this.getPublicKey(addr);
94
+ } catch {
95
+ return null;
96
+ }
97
+ }),
98
+ );
99
+ }
100
+ }