@sig-net/midnight 0.0.1 → 0.0.2

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 (56) hide show
  1. package/dist/index.d.ts +2 -4
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +2 -4
  4. package/dist/index.js.map +1 -1
  5. package/dist/managed/compiler/contract-info.json +57 -0
  6. package/dist/managed/compiler/contract-manifest.json +8 -8
  7. package/dist/managed/contract/index.d.ts +13 -0
  8. package/dist/managed/contract/index.js +138 -73
  9. package/dist/managed/contract/index.js.map +1 -1
  10. package/dist/signature-requests-state-reader.d.ts +5 -5
  11. package/dist/signature-requests-state-reader.js +5 -5
  12. package/dist/signet-contract-state-reader.d.ts +88 -2
  13. package/dist/signet-contract-state-reader.d.ts.map +1 -1
  14. package/dist/signet-contract-state-reader.js +106 -3
  15. package/dist/signet-contract-state-reader.js.map +1 -1
  16. package/dist/signet-request-feed.d.ts +40 -21
  17. package/dist/signet-request-feed.d.ts.map +1 -1
  18. package/dist/signet-request-feed.js +79 -34
  19. package/dist/signet-request-feed.js.map +1 -1
  20. package/dist/signet-request-resolver.d.ts +11 -11
  21. package/dist/signet-request-resolver.d.ts.map +1 -1
  22. package/dist/signet-request-resolver.js +22 -22
  23. package/dist/signet-request-resolver.js.map +1 -1
  24. package/dist/signet-requests.d.ts +18 -4
  25. package/dist/signet-requests.d.ts.map +1 -1
  26. package/dist/signet-requests.js +29 -6
  27. package/dist/signet-requests.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/Signet.compact +75 -99
  30. package/src/circuits.compact +5 -2
  31. package/src/index.ts +2 -4
  32. package/src/managed/compiler/contract-info.json +57 -0
  33. package/src/managed/compiler/contract-manifest.json +8 -8
  34. package/src/managed/contract/index.d.ts +13 -0
  35. package/src/managed/contract/index.js +138 -73
  36. package/src/managed/contract/index.js.map +1 -1
  37. package/src/signature-requests-state-reader.ts +5 -5
  38. package/src/signet-contract-state-reader.ts +200 -4
  39. package/src/signet-request-feed.ts +98 -48
  40. package/src/signet-request-resolver.ts +28 -28
  41. package/src/signet-requests.ts +31 -6
  42. package/dist/signet-event-observer.d.ts +0 -102
  43. package/dist/signet-event-observer.d.ts.map +0 -1
  44. package/dist/signet-event-observer.js +0 -127
  45. package/dist/signet-event-observer.js.map +0 -1
  46. package/dist/signet-events.d.ts +0 -175
  47. package/dist/signet-events.d.ts.map +0 -1
  48. package/dist/signet-events.js +0 -210
  49. package/dist/signet-events.js.map +0 -1
  50. package/dist/signet-response-feed.d.ts +0 -161
  51. package/dist/signet-response-feed.d.ts.map +0 -1
  52. package/dist/signet-response-feed.js +0 -234
  53. package/dist/signet-response-feed.js.map +0 -1
  54. package/src/signet-event-observer.ts +0 -186
  55. package/src/signet-events.ts +0 -337
  56. package/src/signet-response-feed.ts +0 -325
@@ -6,8 +6,9 @@
6
6
  // layout convention (see "Signet Contract Ledger Layout" in Signet.compact):
7
7
  // signature response counter index (field 0), signature response log
8
8
  // (field 1), respond-bidirectional index (field 2), sealed MPC key hash
9
- // (field 3). The generic state-tree walk (RawContractState, signetFieldNode)
10
- // and the shared base descriptors live in signature-state-reading.ts.
9
+ // (field 3), notification registry (field 4). The generic state-tree walk
10
+ // (RawContractState, signetFieldNode) and the shared base descriptors live in
11
+ // signature-state-reading.ts.
11
12
 
12
13
  import {
13
14
  CompactTypeBytes,
@@ -19,6 +20,7 @@ import {
19
20
  } from "@midnight-ntwrk/compact-runtime";
20
21
 
21
22
  import {
23
+ bytesToHex,
22
24
  requestIdHex,
23
25
  type RequestId,
24
26
  type RequestIdHex,
@@ -43,6 +45,9 @@ export const RESPOND_BIDIRECTIONAL_INDEX_FIELD = 2;
43
45
  /** Signet contract layout: the sealed MPC attestation key hash is ledger field 3. */
44
46
  export const MPC_PUB_KEY_HASH_FIELD = 3;
45
47
 
48
+ /** Signet contract layout: the request notification registry is ledger field 4. */
49
+ export const SIGN_BIDIRECTIONAL_NOTIFICATION_INDEX_FIELD = 4;
50
+
46
51
  // ---- Type descriptors (must mirror Signet.compact field-for-field) ----
47
52
  // As in the requests reader, fromValue consumes the aligned value
48
53
  // sequentially, so field order and width here must match the Compact structs
@@ -232,6 +237,150 @@ export const respondBidirectionalType: CompactType<RespondBidirectional> = {
232
237
  },
233
238
  };
234
239
 
240
+ /** Descriptor for the notification's packed V1 payload (`Bytes<65>`). */
241
+ const bytes65 = new CompactTypeBytes(65);
242
+
243
+ /**
244
+ * Raw twin of the Compact `SignBidirectionalNotification` struct as stored
245
+ * on-ledger: the version tag plus the still-packed 65-byte payload. Decode
246
+ * the payload with {@link decodeSignBidirectionalNotification}.
247
+ */
248
+ export interface SignBidirectionalNotificationRecord {
249
+ /** Payload layout tag (Compact `Uint<8>`); 1 = the V1 layout. */
250
+ version: bigint;
251
+ /** The packed payload bytes, exactly as the registering circuit built them. */
252
+ payload: Uint8Array;
253
+ }
254
+
255
+ /**
256
+ * Hand-composed descriptor for {@link SignBidirectionalNotificationRecord}.
257
+ * Field order (version, payload) must match the Compact struct.
258
+ */
259
+ export const signBidirectionalNotificationType: CompactType<SignBidirectionalNotificationRecord> =
260
+ {
261
+ /** @returns Compound alignment of the struct's fields in declaration order. */
262
+ alignment() {
263
+ return u8.alignment().concat(bytes65.alignment());
264
+ },
265
+ /**
266
+ * Decode one notification record from an aligned value, consuming it
267
+ * field by field.
268
+ *
269
+ * @param value - Mutable aligned value cursor; pass a copy.
270
+ * @returns The decoded record.
271
+ */
272
+ fromValue(value) {
273
+ return {
274
+ version: u8.fromValue(value),
275
+ payload: bytes65.fromValue(value),
276
+ };
277
+ },
278
+ /**
279
+ * Encode a notification record into its aligned on-ledger representation.
280
+ *
281
+ * @param record - The record to encode.
282
+ * @returns The aligned value, fields concatenated in declaration order.
283
+ */
284
+ toValue(record) {
285
+ return u8.toValue(record.version).concat(bytes65.toValue(record.payload));
286
+ },
287
+ };
288
+
289
+ /** Offset of the V1 `callerAddress` in the packed payload (`Bytes<32>` at the front). */
290
+ const NOTIFICATION_CALLER_ADDRESS_OFFSET = 0;
291
+
292
+ /** Offset of the V1 `requestId` (after the 32 callerAddress bytes). */
293
+ const NOTIFICATION_REQUEST_ID_OFFSET = 32;
294
+
295
+ /** Offset of the V1 `requestsIndexField` (after callerAddress + requestId). */
296
+ const NOTIFICATION_REQUESTS_INDEX_FIELD_OFFSET = 64;
297
+
298
+ /** The only payload interpretation {@link decodeSignBidirectionalNotification} understands today. */
299
+ const SUPPORTED_NOTIFICATION_VERSION = 1n;
300
+
301
+ /**
302
+ * A decoded V1 notification from the signet contract's registry: the flat
303
+ * pointer a client registered to tell the MPC a request was stored — and
304
+ * WHERE to read the authenticated copy. Never trusted on its own: the
305
+ * resolver authenticates by reading the request back from
306
+ * {@link callerAddress}'s own ledger (see signet-request-resolver.ts).
307
+ */
308
+ export interface SignBidirectionalNotification {
309
+ /** Payload layout tag; this decoder only produces version 1. */
310
+ version: number;
311
+ /**
312
+ * Address of the contract whose request index holds the request, rendered
313
+ * as lowercase hex, no `0x` prefix — directly usable as a
314
+ * `queryContractState` argument. The MPC reads the request from THIS
315
+ * contract's authenticated state; the field itself confers no authority.
316
+ */
317
+ callerAddress: string;
318
+ /** Which request in {@link callerAddress}'s index this notification is about. */
319
+ requestId: RequestIdHex;
320
+ /**
321
+ * Ledger field position of the `Map<RequestId, SignBidirectionalRequest>`
322
+ * request index in {@link callerAddress} (the signet layout convention puts
323
+ * it at field 0, but the notification carries it so the reader never assumes).
324
+ */
325
+ requestsIndexField: number;
326
+ }
327
+
328
+ /**
329
+ * Unpack a stored {@link SignBidirectionalNotificationRecord}'s payload by
330
+ * the fixed V1 offsets — the decode twin of the compiled
331
+ * `constructSignBidirectionalNotificationV1` circuit (byte plumbing only; the
332
+ * pack↔decode lockstep is pinned by the state-reader unit test that round-trips
333
+ * through the real circuit).
334
+ *
335
+ * Fails closed on an unrecognised `version`: a future payload layout adds a
336
+ * branch here rather than silently misinterpreting bytes under the V1 offsets.
337
+ *
338
+ * @param record - The raw on-ledger record.
339
+ * @returns The decoded flat notification.
340
+ * @throws Error if the record's `version` is not one this decoder understands.
341
+ */
342
+ export function decodeSignBidirectionalNotification(
343
+ record: SignBidirectionalNotificationRecord,
344
+ ): SignBidirectionalNotification {
345
+ if (record.version !== SUPPORTED_NOTIFICATION_VERSION) {
346
+ throw new Error(
347
+ `SignBidirectionalNotification version ${record.version} is not supported ` +
348
+ `(this decoder understands version ${SUPPORTED_NOTIFICATION_VERSION})`,
349
+ );
350
+ }
351
+ const callerAddress = bytesToHex(
352
+ record.payload.slice(
353
+ NOTIFICATION_CALLER_ADDRESS_OFFSET,
354
+ NOTIFICATION_REQUEST_ID_OFFSET,
355
+ ),
356
+ );
357
+ const requestId = requestIdHex(
358
+ record.payload.slice(
359
+ NOTIFICATION_REQUEST_ID_OFFSET,
360
+ NOTIFICATION_REQUESTS_INDEX_FIELD_OFFSET,
361
+ ),
362
+ );
363
+ const requestsIndexField =
364
+ record.payload[NOTIFICATION_REQUESTS_INDEX_FIELD_OFFSET];
365
+ return {
366
+ version: Number(record.version),
367
+ callerAddress,
368
+ requestId,
369
+ requestsIndexField,
370
+ };
371
+ }
372
+
373
+ /**
374
+ * Plain-JS notification registry parsed out of the ledger: hex request id
375
+ * (the map KEY under which the notification was registered) to the raw
376
+ * stored record. Decode each record with
377
+ * {@link decodeSignBidirectionalNotification}.
378
+ */
379
+ export type SignBidirectionalNotificationIndex = Map<
380
+ RequestIdHex,
381
+ SignBidirectionalNotificationRecord
382
+ >;
383
+
235
384
  /**
236
385
  * Plain-JS signature response counter index parsed out of the ledger: hex
237
386
  * request id (see {@link requestIdHex}) to the number of responses posted for
@@ -271,11 +420,50 @@ export function signetResponseIndexKey(
271
420
  return `${id}:${count}`;
272
421
  }
273
422
 
423
+ /**
424
+ * Read ONLY the notification registry (ledger field
425
+ * {@link SIGN_BIDIRECTIONAL_NOTIFICATION_INDEX_FIELD}) out of raw signet
426
+ * contract state — the poll-loop primitive of {@link SignetRequestFeed},
427
+ * which cycles frequently and has no use for the response fields.
428
+ *
429
+ * @param raw - Raw contract state, e.g. `queryContractState(address).data`
430
+ * from the indexer or `ctx.currentQueryContext.state` from the simulator.
431
+ * @returns The registry, keyed by the hex request id each record was
432
+ * registered under.
433
+ * @throws Error if the field is missing or is not a Map.
434
+ */
435
+ export function readSignBidirectionalNotificationIndexFromState(
436
+ raw: RawContractState,
437
+ ): SignBidirectionalNotificationIndex {
438
+ const notificationMap = signetFieldNode(
439
+ raw,
440
+ SIGN_BIDIRECTIONAL_NOTIFICATION_INDEX_FIELD,
441
+ ).asMap();
442
+ if (notificationMap === undefined) {
443
+ throw new Error(
444
+ `Ledger field ${SIGN_BIDIRECTIONAL_NOTIFICATION_INDEX_FIELD} is not a Map`,
445
+ );
446
+ }
447
+ const index: SignBidirectionalNotificationIndex = new Map();
448
+ for (const key of notificationMap.keys()) {
449
+ // fromValue consumes its input, so hand each descriptor a copy.
450
+ const requestId = requestIdType.fromValue([...key.value]);
451
+ const cell = notificationMap.get(key)?.asCell();
452
+ if (cell === undefined) continue;
453
+ index.set(
454
+ requestIdHex(requestId),
455
+ signBidirectionalNotificationType.fromValue([...cell.value]),
456
+ );
457
+ }
458
+ return index;
459
+ }
460
+
274
461
  /**
275
462
  * The decoded ledger fields of the signet contract: the signature response
276
463
  * counter (field 0) and log (field 1), the respond-bidirectional index
277
- * (field 2), and the sealed MPC attestation key hash (field 3). Together
278
- * they give a client everything the MPC ever delivers for a request.
464
+ * (field 2), the sealed MPC attestation key hash (field 3), and the request
465
+ * notification registry (field 4). Together they give a poller everything
466
+ * the contract ever records about a request.
279
467
  */
280
468
  export interface SignetContractLedger {
281
469
  /**
@@ -299,6 +487,12 @@ export interface SignetContractLedger {
299
487
  * {@link MPC_PUB_KEY_HASH_FIELD}) posts are verified against.
300
488
  */
301
489
  mpcPubKeyHash: Uint8Array;
490
+ /**
491
+ * The request notification registry (ledger field
492
+ * {@link SIGN_BIDIRECTIONAL_NOTIFICATION_INDEX_FIELD}), keyed by hex
493
+ * request id.
494
+ */
495
+ signBidirectionalNotificationIndex: SignBidirectionalNotificationIndex;
302
496
  }
303
497
 
304
498
  /**
@@ -387,5 +581,7 @@ export function readSignetContractLedgerFromState(
387
581
  signatureResponseIndex,
388
582
  respondBidirectionalIndex,
389
583
  mpcPubKeyHash,
584
+ signBidirectionalNotificationIndex:
585
+ readSignBidirectionalNotificationIndexFromState(raw),
390
586
  };
391
587
  }
@@ -1,9 +1,10 @@
1
- // The MPC's single entry point for discovering signature requests by event
2
- // observation: watch the central signet contract's notifications, resolve each
3
- // to an AUTHENTICATED request read from the named caller's own ledger, and
4
- // yield only the ones that pass the membership check. Replaces polling a
5
- // configured list of requester contracts — the feed discovers requesters
6
- // dynamically, keyed off whose authenticated state it actually read.
1
+ // The MPC's single entry point for discovering signature requests: poll the
2
+ // central signet contract's notification registry (ledger field 4), resolve
3
+ // each notification to an AUTHENTICATED request read from the named caller's
4
+ // own ledger, and yield only the ones that pass the membership check. The
5
+ // registry is append-only with no on-ledger cursor, so the feed's in-memory
6
+ // `yielded` set is the diff cursor: every poll enumerates the registry and
7
+ // serves what it has not served before.
7
8
  //
8
9
  // The requester allow-list is an OPTIONAL policy filter here (drop requests
9
10
  // from contracts you choose not to serve), never a security requirement and
@@ -12,60 +13,81 @@
12
13
  // knowledge-base/caller-attribution.md).
13
14
 
14
15
  import {
15
- SignetEventObserver,
16
- sleepUnlessAborted,
17
- type SignetEventSource,
18
- } from "./signet-event-observer.ts";
19
- import {
20
- signBidirectionalEventCodec,
21
- stripHexPrefix,
22
- type SignBidirectionalEvent,
23
- } from "./signet-events.ts";
16
+ decodeSignBidirectionalNotification,
17
+ readSignBidirectionalNotificationIndexFromState,
18
+ } from "./signet-contract-state-reader.ts";
24
19
  import {
25
20
  SignetRequestResolver,
26
21
  type ResolvedSignetRequest,
27
22
  } from "./signet-request-resolver.ts";
28
23
  import type { SignetPublicStateSource } from "./signet-request-response-reader.ts";
29
- import type { RequestIdHex } from "./signet-requests.ts";
24
+ import { stripHexPrefix, type RequestIdHex } from "./signet-requests.ts";
30
25
 
31
26
  /** Default gap between poll cycles of {@link SignetRequestFeed.requests}. */
32
27
  export const DEFAULT_FEED_POLL_INTERVAL_MS = 3000;
33
28
 
34
29
  /** Everything a {@link SignetRequestFeed} needs. */
35
30
  export interface SignetRequestFeedConfig {
36
- /** Address of the central signet contract whose events to watch. */
31
+ /** Address of the central signet contract whose notification registry to poll. */
37
32
  readonly signetContractAddress: string;
38
33
  /**
39
- * Source of BOTH events and raw contract state — a full
40
- * `indexerPublicDataProvider` satisfies both halves.
34
+ * Source of raw contract state for BOTH the signet contract's registry and
35
+ * the requester ledgers the resolver authenticates against — a full
36
+ * `indexerPublicDataProvider` is assignable.
41
37
  */
42
- readonly source: SignetEventSource & SignetPublicStateSource;
38
+ readonly source: SignetPublicStateSource;
43
39
  /**
44
40
  * Optional policy allow-list of requester contract addresses to serve
45
41
  * (matched case- and `0x`-prefix-insensitively). Omit to serve every
46
42
  * requester the resolver can authenticate. NOT a security control.
47
43
  */
48
44
  readonly allowContracts?: Iterable<string>;
49
- /** Durable resume floor for the underlying observer (see {@link SignetEventObserverConfig.fromEventId}). */
50
- readonly fromEventId?: number;
51
45
  /** Poll cadence for {@link SignetRequestFeed.requests}; default {@link DEFAULT_FEED_POLL_INTERVAL_MS}. */
52
46
  readonly pollIntervalMs?: number;
53
47
  }
54
48
 
49
+ /**
50
+ * Resolve after `ms`, or immediately once `signal` aborts. Used to space out
51
+ * polls without wedging a shutdown.
52
+ *
53
+ * @param ms - Milliseconds to wait.
54
+ * @param signal - Abort to resolve early.
55
+ * @returns A promise that settles after the delay or the abort.
56
+ */
57
+ export function sleepUnlessAborted(
58
+ ms: number,
59
+ signal?: AbortSignal,
60
+ ): Promise<void> {
61
+ return new Promise((resolve) => {
62
+ if (signal?.aborted) return resolve();
63
+ const timer = setTimeout(() => {
64
+ signal?.removeEventListener("abort", onAbort);
65
+ resolve();
66
+ }, ms);
67
+ const onAbort = () => {
68
+ clearTimeout(timer);
69
+ resolve();
70
+ };
71
+ signal?.addEventListener("abort", onAbort, { once: true });
72
+ });
73
+ }
74
+
55
75
  /** Canonical form for comparing contract addresses: no `0x`, lowercase. */
56
76
  function normalizeAddress(address: string): string {
57
77
  return stripHexPrefix(address).toLowerCase();
58
78
  }
59
79
 
60
80
  /**
61
- * The event-driven request feed. Composes a {@link SignetEventObserver}
62
- * (discovery) with a {@link SignetRequestResolver} (authentication), yielding
63
- * only notifications that resolve to a real, member request. Dedupes by request
64
- * id across its lifetime, so a redelivered notification is not re-yielded; call
81
+ * The registry-polling request feed. Reads the signet contract's notification
82
+ * registry (discovery) and hands each notification to a
83
+ * {@link SignetRequestResolver} (authentication), yielding only notifications
84
+ * that resolve to a real, member request. Dedupes by request id across its
85
+ * lifetime, so a re-registered notification is not re-yielded; call
65
86
  * {@link forget} to re-arm a request whose downstream processing failed.
66
87
  */
67
88
  export class SignetRequestFeed {
68
- private readonly observer: SignetEventObserver<SignBidirectionalEvent>;
89
+ private readonly signetContractAddress: string;
90
+ private readonly source: SignetPublicStateSource;
69
91
  private readonly resolver: SignetRequestResolver;
70
92
  private readonly allowContracts?: Set<string>;
71
93
  private readonly pollIntervalMs: number;
@@ -75,17 +97,12 @@ export class SignetRequestFeed {
75
97
  private readonly yielded = new Set<RequestIdHex>();
76
98
 
77
99
  /**
78
- * @param config - The signet contract, combined event/state source, optional
79
- * policy allow-list, and resume floor.
100
+ * @param config - The signet contract, state source, and optional policy
101
+ * allow-list.
80
102
  */
81
103
  constructor(config: SignetRequestFeedConfig) {
82
- this.observer = new SignetEventObserver({
83
- signetContractAddress: config.signetContractAddress,
84
- source: config.source,
85
- codec: signBidirectionalEventCodec,
86
- fromEventId: config.fromEventId,
87
- pollIntervalMs: config.pollIntervalMs,
88
- });
104
+ this.signetContractAddress = config.signetContractAddress;
105
+ this.source = config.source;
89
106
  this.resolver = new SignetRequestResolver({ source: config.source });
90
107
  this.allowContracts = config.allowContracts
91
108
  ? new Set(Array.from(config.allowContracts, normalizeAddress))
@@ -103,23 +120,56 @@ export class SignetRequestFeed {
103
120
  }
104
121
 
105
122
  /**
106
- * One-shot: every currently-visible notification that is admitted by the
107
- * allow-list, not already yielded, and resolves to an authenticated member
108
- * request. Forged / not-yet-indexed / wrong-field notifications resolve to
109
- * nothing and are dropped WITHOUT being marked yielded, so a genuine
110
- * request whose ledger write has not indexed yet is retried next cycle.
123
+ * One-shot: every currently-registered notification that decodes, is
124
+ * admitted by the allow-list, not already yielded, and resolves to an
125
+ * authenticated member request. Registry enumeration has no meaningful
126
+ * on-ledger order, so entries are processed in ascending request-id-hex
127
+ * order — a stable, deterministic sequence. Forged / not-yet-indexed /
128
+ * wrong-field notifications resolve to nothing and are dropped WITHOUT
129
+ * being marked yielded, so a genuine request whose ledger write has not
130
+ * indexed yet is retried next cycle; undecodable or unsupported-version
131
+ * records are likewise skipped (and logged) without being marked.
111
132
  *
112
- * @returns The newly-authenticated requests this cycle, in event id order.
133
+ * Dedupe and resolution key off the notification PAYLOAD's request id (the
134
+ * canonical one — the registry map key is caller-supplied and could
135
+ * disagree; the resolver's membership check authenticates the payload's).
136
+ *
137
+ * @returns The newly-authenticated requests this cycle.
138
+ * @throws Error if the signet contract has no readable state at the
139
+ * configured address (wrong address or not yet deployed/indexed).
113
140
  */
114
141
  async poll(): Promise<ResolvedSignetRequest[]> {
115
- const events = await this.observer.currentEvents();
142
+ const state = await this.source.queryContractState(
143
+ this.signetContractAddress,
144
+ );
145
+ if (!state?.data) {
146
+ throw new Error(
147
+ `No contract state at signet contract ${this.signetContractAddress} ` +
148
+ `— wrong address, or not yet deployed/indexed`,
149
+ );
150
+ }
151
+ const registry = readSignBidirectionalNotificationIndexFromState(
152
+ state.data,
153
+ );
116
154
  const out: ResolvedSignetRequest[] = [];
117
- for (const event of events) {
118
- if (this.yielded.has(event.requestId)) continue;
119
- if (!this.allowed(event.callerAddress)) continue;
120
- const resolved = await this.resolver.resolve(event);
155
+ for (const [registryKey, record] of [...registry.entries()].sort(
156
+ ([a], [b]) => (a < b ? -1 : a > b ? 1 : 0),
157
+ )) {
158
+ let notification;
159
+ try {
160
+ notification = decodeSignBidirectionalNotification(record);
161
+ } catch (error) {
162
+ console.warn(
163
+ `SignetRequestFeed: skipping undecodable notification ` +
164
+ `registered under ${registryKey}: ${String(error)}`,
165
+ );
166
+ continue;
167
+ }
168
+ if (this.yielded.has(notification.requestId)) continue;
169
+ if (!this.allowed(notification.callerAddress)) continue;
170
+ const resolved = await this.resolver.resolve(notification);
121
171
  if (resolved === undefined) continue;
122
- this.yielded.add(event.requestId);
172
+ this.yielded.add(notification.requestId);
123
173
  out.push(resolved);
124
174
  }
125
175
  return out;
@@ -1,19 +1,19 @@
1
- // Turn a SignBidirectionalEvent notification into an AUTHENTICATED request
2
- // record. This is the security core of the event flow (see
3
- // knowledge-base/caller-attribution.md and the invariants in this task's
4
- // brief): the event says only WHERE to look; the request that gets signed is
5
- // read from the named caller's own authenticated ledger, and the event's
6
- // `requestId` must be a member of that contract's request index. An attacker
7
- // cannot write into a contract it does not control, so a forged event can at
8
- // most re-point at a legitimate request that already exists — never inject one.
1
+ // Turn a SignBidirectionalNotification into an AUTHENTICATED request record.
2
+ // This is the security core of the discovery flow (see
3
+ // knowledge-base/caller-attribution.md): the notification says only WHERE to
4
+ // look; the request that gets signed is read from the named caller's own
5
+ // authenticated ledger, and the notification's `requestId` must be a member
6
+ // of that contract's request index. An attacker cannot write into a contract
7
+ // it does not control, so a forged notification can at most re-point at a
8
+ // legitimate request that already exists — never inject one.
9
9
  //
10
10
  // Key derivation stays f(callerAddress, path), keyed off the contract whose
11
- // authenticated state was actually read — exactly today's guarantee, now
12
- // reached via an event ping instead of per-contract polling.
11
+ // authenticated state was actually read — the notification registry is a
12
+ // pointer board, never an authority.
13
13
 
14
14
  import { lookupSignetRequestAt } from "./signature-requests-state-reader.ts";
15
15
  import type { RawContractState } from "./signature-state-reading.ts";
16
- import type { SignBidirectionalEvent } from "./signet-events.ts";
16
+ import type { SignBidirectionalNotification } from "./signet-contract-state-reader.ts";
17
17
  import type { SignetPublicStateSource } from "./signet-request-response-reader.ts";
18
18
  import type {
19
19
  RequestIdHex,
@@ -21,15 +21,15 @@ import type {
21
21
  } from "./signet-requests.ts";
22
22
 
23
23
  /**
24
- * A {@link SignBidirectionalEvent} resolved to the authenticated request it
25
- * refers to: the request record read from `callerAddress`'s own ledger, with
26
- * `requestId` confirmed to be a member of that contract's index.
24
+ * A {@link SignBidirectionalNotification} resolved to the authenticated
25
+ * request it refers to: the request record read from `callerAddress`'s own
26
+ * ledger, with `requestId` confirmed to be a member of that contract's index.
27
27
  */
28
28
  export interface ResolvedSignetRequest {
29
29
  /**
30
30
  * The contract whose authenticated state the request was read from — the
31
31
  * epsilon-derivation predecessor. Key derivation keys off THIS, never off a
32
- * field taken from the event on faith.
32
+ * field taken from the notification on faith.
33
33
  */
34
34
  callerAddress: string;
35
35
  /** The request id, confirmed to be a member of `callerAddress`'s index. */
@@ -45,7 +45,7 @@ export interface SignetRequestResolverConfig {
45
45
  }
46
46
 
47
47
  /**
48
- * Resolves {@link SignBidirectionalEvent} notifications to authenticated
48
+ * Resolves {@link SignBidirectionalNotification}s to authenticated
49
49
  * {@link ResolvedSignetRequest}s by reading the named caller's ledger and
50
50
  * enforcing the membership check. Construct once and reuse: resolved records
51
51
  * are immutable (their ledger key is their hash), so they are cached by request
@@ -75,22 +75,22 @@ export class SignetRequestResolver {
75
75
  * Resolve a notification to its authenticated request, or `undefined` when it
76
76
  * cannot be trusted: the caller contract has no state, or `requestId` is not
77
77
  * a member of the index at `requestsIndexField` (forged, stale, wrong field,
78
- * or not yet indexed — poll again). Never throws on an untrusted event; a bad
79
- * notification is dropped, not surfaced as an error.
78
+ * or not yet indexed — poll again). Never throws on an untrusted
79
+ * notification; a bad one is dropped, not surfaced as an error.
80
80
  *
81
- * @param event - The decoded event notification.
82
- * @returns The authenticated request, or `undefined` to drop the event.
81
+ * @param notification - The decoded notification.
82
+ * @returns The authenticated request, or `undefined` to drop the notification.
83
83
  */
84
84
  async resolve(
85
- event: SignBidirectionalEvent,
85
+ notification: SignBidirectionalNotification,
86
86
  ): Promise<ResolvedSignetRequest | undefined> {
87
- const cached = this.resolvedCache.get(event.requestId);
87
+ const cached = this.resolvedCache.get(notification.requestId);
88
88
  if (cached !== undefined) {
89
89
  return cached;
90
90
  }
91
91
  let state: { data: RawContractState } | null;
92
92
  try {
93
- state = await this.source.queryContractState(event.callerAddress);
93
+ state = await this.source.queryContractState(notification.callerAddress);
94
94
  } catch {
95
95
  return undefined; // caller address not a contract / transient read error
96
96
  }
@@ -99,18 +99,18 @@ export class SignetRequestResolver {
99
99
  }
100
100
  const request = lookupSignetRequestAt(
101
101
  state.data,
102
- event.requestsIndexField,
103
- event.requestId,
102
+ notification.requestsIndexField,
103
+ notification.requestId,
104
104
  );
105
105
  if (request === undefined) {
106
106
  return undefined; // membership check failed — drop the notification
107
107
  }
108
108
  const resolved: ResolvedSignetRequest = {
109
- callerAddress: event.callerAddress,
110
- requestId: event.requestId,
109
+ callerAddress: notification.callerAddress,
110
+ requestId: notification.requestId,
111
111
  request,
112
112
  };
113
- this.resolvedCache.set(event.requestId, resolved);
113
+ this.resolvedCache.set(notification.requestId, resolved);
114
114
  return resolved;
115
115
  }
116
116
  }
@@ -461,15 +461,15 @@ function decodeAccessList(
461
461
  * below both go through it, so the transaction a client broadcasts is
462
462
  * provably the one the MPC put its signature over.
463
463
  *
464
- * @param event - The on-ledger request record.
464
+ * @param request - The on-ledger request record.
465
465
  * @returns The unsigned ethers transaction (`unsignedHash` is the digest the
466
466
  * MPC signs).
467
467
  * @throws Error if a calldata word carries an unknown kind.
468
468
  */
469
469
  export function signBidirectionalRequestToUnsignedEVMTransaction(
470
- event: SignBidirectionalRequest,
470
+ request: SignBidirectionalRequest,
471
471
  ): Transaction {
472
- const { txParams } = event;
472
+ const { txParams } = request;
473
473
  return Transaction.from({
474
474
  type: 2,
475
475
  chainId: txParams.chainId,
@@ -581,7 +581,7 @@ export function signatureToSignatureResponse(
581
581
  * requester's derived address — the response log is unauthenticated, so
582
582
  * verify first with `verifySignatureResponse`.
583
583
  *
584
- * @param event - The on-ledger request record.
584
+ * @param request - The on-ledger request record.
585
585
  * @param response - The posted signature record answering it.
586
586
  * @returns The signed ethers transaction; `serialized` is the raw payload for
587
587
  * `eth_sendRawTransaction`, `hash` its on-chain hash, `from` the recovered
@@ -591,10 +591,10 @@ export function signatureToSignatureResponse(
591
591
  * is not a decodable signature.
592
592
  */
593
593
  export function signBidirectionalRequestToSignedEVMTransaction(
594
- event: SignBidirectionalRequest,
594
+ request: SignBidirectionalRequest,
595
595
  response: SignatureResponse,
596
596
  ): Transaction {
597
- const transaction = signBidirectionalRequestToUnsignedEVMTransaction(event);
597
+ const transaction = signBidirectionalRequestToUnsignedEVMTransaction(request);
598
598
  transaction.signature = signatureResponseToSignature(response);
599
599
  return transaction;
600
600
  }
@@ -655,6 +655,31 @@ export function bytesToHex(bytes: Uint8Array): string {
655
655
  return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
656
656
  }
657
657
 
658
+ /**
659
+ * Strip an optional `0x`/`0X` prefix from a hex string.
660
+ *
661
+ * @param hex - A hex string, with or without a `0x` prefix.
662
+ * @returns The bare hex digits.
663
+ */
664
+ export function stripHexPrefix(hex: string): string {
665
+ return hex.startsWith("0x") || hex.startsWith("0X") ? hex.slice(2) : hex;
666
+ }
667
+
668
+ /**
669
+ * Decode a hex string into bytes — the inverse of {@link bytesToHex}.
670
+ *
671
+ * @param hex - Hex digits, with or without a `0x` prefix.
672
+ * @returns The decoded bytes.
673
+ */
674
+ export function hexToBytes(hex: string): Uint8Array {
675
+ const digits = stripHexPrefix(hex);
676
+ const out = new Uint8Array(digits.length >> 1);
677
+ for (let i = 0; i < out.length; i++) {
678
+ out[i] = Number.parseInt(digits.slice(2 * i, 2 * i + 2), 16);
679
+ }
680
+ return out;
681
+ }
682
+
658
683
  /**
659
684
  * Render a request id in its canonical TS form (see
660
685
  * {@link RequestIdHex}) — also usable as a JS `Map` key, which raw