@did-btcr2/method 0.38.0 → 0.40.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 (240) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/browser.js +24918 -35517
  3. package/dist/browser.mjs +24918 -35517
  4. package/dist/cjs/index.js +403 -6066
  5. package/dist/esm/core/beacon/fee-estimator.js +6 -29
  6. package/dist/esm/core/beacon/fee-estimator.js.map +1 -1
  7. package/dist/esm/core/did-sender-resolver.js +26 -0
  8. package/dist/esm/core/did-sender-resolver.js.map +1 -0
  9. package/dist/esm/index.js +1 -17
  10. package/dist/esm/index.js.map +1 -1
  11. package/dist/types/core/beacon/fee-estimator.d.ts +4 -39
  12. package/dist/types/core/beacon/fee-estimator.d.ts.map +1 -1
  13. package/dist/types/core/did-sender-resolver.d.ts +14 -0
  14. package/dist/types/core/did-sender-resolver.d.ts.map +1 -0
  15. package/dist/types/index.d.ts +1 -16
  16. package/dist/types/index.d.ts.map +1 -1
  17. package/package.json +4 -4
  18. package/src/core/beacon/fee-estimator.ts +8 -51
  19. package/src/core/did-sender-resolver.ts +25 -0
  20. package/src/index.ts +1 -18
  21. package/dist/esm/core/aggregation/beacon-strategy.js +0 -76
  22. package/dist/esm/core/aggregation/beacon-strategy.js.map +0 -1
  23. package/dist/esm/core/aggregation/cohort.js +0 -316
  24. package/dist/esm/core/aggregation/cohort.js.map +0 -1
  25. package/dist/esm/core/aggregation/conditions.js +0 -102
  26. package/dist/esm/core/aggregation/conditions.js.map +0 -1
  27. package/dist/esm/core/aggregation/errors.js +0 -22
  28. package/dist/esm/core/aggregation/errors.js.map +0 -1
  29. package/dist/esm/core/aggregation/fallback-spend.js +0 -132
  30. package/dist/esm/core/aggregation/fallback-spend.js.map +0 -1
  31. package/dist/esm/core/aggregation/logger.js +0 -15
  32. package/dist/esm/core/aggregation/logger.js.map +0 -1
  33. package/dist/esm/core/aggregation/messages/base.js +0 -36
  34. package/dist/esm/core/aggregation/messages/base.js.map +0 -1
  35. package/dist/esm/core/aggregation/messages/bodies.js +0 -149
  36. package/dist/esm/core/aggregation/messages/bodies.js.map +0 -1
  37. package/dist/esm/core/aggregation/messages/constants.js +0 -37
  38. package/dist/esm/core/aggregation/messages/constants.js.map +0 -1
  39. package/dist/esm/core/aggregation/messages/factories.js +0 -139
  40. package/dist/esm/core/aggregation/messages/factories.js.map +0 -1
  41. package/dist/esm/core/aggregation/messages/guards.js +0 -40
  42. package/dist/esm/core/aggregation/messages/guards.js.map +0 -1
  43. package/dist/esm/core/aggregation/messages/index.js +0 -6
  44. package/dist/esm/core/aggregation/messages/index.js.map +0 -1
  45. package/dist/esm/core/aggregation/participant.js +0 -588
  46. package/dist/esm/core/aggregation/participant.js.map +0 -1
  47. package/dist/esm/core/aggregation/phases.js +0 -45
  48. package/dist/esm/core/aggregation/phases.js.map +0 -1
  49. package/dist/esm/core/aggregation/recovery-policy.js +0 -156
  50. package/dist/esm/core/aggregation/recovery-policy.js.map +0 -1
  51. package/dist/esm/core/aggregation/recovery-spend.js +0 -106
  52. package/dist/esm/core/aggregation/recovery-spend.js.map +0 -1
  53. package/dist/esm/core/aggregation/runner/aggregation-runner.js +0 -75
  54. package/dist/esm/core/aggregation/runner/aggregation-runner.js.map +0 -1
  55. package/dist/esm/core/aggregation/runner/events.js +0 -2
  56. package/dist/esm/core/aggregation/runner/events.js.map +0 -1
  57. package/dist/esm/core/aggregation/runner/index.js +0 -6
  58. package/dist/esm/core/aggregation/runner/index.js.map +0 -1
  59. package/dist/esm/core/aggregation/runner/participant-runner.js +0 -407
  60. package/dist/esm/core/aggregation/runner/participant-runner.js.map +0 -1
  61. package/dist/esm/core/aggregation/runner/service-runner.js +0 -699
  62. package/dist/esm/core/aggregation/runner/service-runner.js.map +0 -1
  63. package/dist/esm/core/aggregation/runner/typed-emitter.js +0 -80
  64. package/dist/esm/core/aggregation/runner/typed-emitter.js.map +0 -1
  65. package/dist/esm/core/aggregation/service.js +0 -774
  66. package/dist/esm/core/aggregation/service.js.map +0 -1
  67. package/dist/esm/core/aggregation/signer.js +0 -33
  68. package/dist/esm/core/aggregation/signer.js.map +0 -1
  69. package/dist/esm/core/aggregation/signing-session.js +0 -195
  70. package/dist/esm/core/aggregation/signing-session.js.map +0 -1
  71. package/dist/esm/core/aggregation/transport/didcomm.js +0 -41
  72. package/dist/esm/core/aggregation/transport/didcomm.js.map +0 -1
  73. package/dist/esm/core/aggregation/transport/error.js +0 -12
  74. package/dist/esm/core/aggregation/transport/error.js.map +0 -1
  75. package/dist/esm/core/aggregation/transport/factory.js +0 -29
  76. package/dist/esm/core/aggregation/transport/factory.js.map +0 -1
  77. package/dist/esm/core/aggregation/transport/http/client.js +0 -350
  78. package/dist/esm/core/aggregation/transport/http/client.js.map +0 -1
  79. package/dist/esm/core/aggregation/transport/http/envelope.js +0 -126
  80. package/dist/esm/core/aggregation/transport/http/envelope.js.map +0 -1
  81. package/dist/esm/core/aggregation/transport/http/errors.js +0 -11
  82. package/dist/esm/core/aggregation/transport/http/errors.js.map +0 -1
  83. package/dist/esm/core/aggregation/transport/http/inbox-buffer.js +0 -45
  84. package/dist/esm/core/aggregation/transport/http/inbox-buffer.js.map +0 -1
  85. package/dist/esm/core/aggregation/transport/http/index.js +0 -12
  86. package/dist/esm/core/aggregation/transport/http/index.js.map +0 -1
  87. package/dist/esm/core/aggregation/transport/http/nonce-cache.js +0 -38
  88. package/dist/esm/core/aggregation/transport/http/nonce-cache.js.map +0 -1
  89. package/dist/esm/core/aggregation/transport/http/protocol.js +0 -28
  90. package/dist/esm/core/aggregation/transport/http/protocol.js.map +0 -1
  91. package/dist/esm/core/aggregation/transport/http/rate-limiter.js +0 -45
  92. package/dist/esm/core/aggregation/transport/http/rate-limiter.js.map +0 -1
  93. package/dist/esm/core/aggregation/transport/http/request-auth.js +0 -100
  94. package/dist/esm/core/aggregation/transport/http/request-auth.js.map +0 -1
  95. package/dist/esm/core/aggregation/transport/http/server.js +0 -481
  96. package/dist/esm/core/aggregation/transport/http/server.js.map +0 -1
  97. package/dist/esm/core/aggregation/transport/http/sse-stream.js +0 -110
  98. package/dist/esm/core/aggregation/transport/http/sse-stream.js.map +0 -1
  99. package/dist/esm/core/aggregation/transport/http/sse-writer.js +0 -25
  100. package/dist/esm/core/aggregation/transport/http/sse-writer.js.map +0 -1
  101. package/dist/esm/core/aggregation/transport/in-memory.js +0 -149
  102. package/dist/esm/core/aggregation/transport/in-memory.js.map +0 -1
  103. package/dist/esm/core/aggregation/transport/index.js +0 -8
  104. package/dist/esm/core/aggregation/transport/index.js.map +0 -1
  105. package/dist/esm/core/aggregation/transport/nostr.js +0 -491
  106. package/dist/esm/core/aggregation/transport/nostr.js.map +0 -1
  107. package/dist/esm/core/aggregation/transport/transport.js +0 -2
  108. package/dist/esm/core/aggregation/transport/transport.js.map +0 -1
  109. package/dist/types/core/aggregation/beacon-strategy.d.ts +0 -56
  110. package/dist/types/core/aggregation/beacon-strategy.d.ts.map +0 -1
  111. package/dist/types/core/aggregation/cohort.d.ts +0 -182
  112. package/dist/types/core/aggregation/cohort.d.ts.map +0 -1
  113. package/dist/types/core/aggregation/conditions.d.ts +0 -86
  114. package/dist/types/core/aggregation/conditions.d.ts.map +0 -1
  115. package/dist/types/core/aggregation/errors.d.ts +0 -14
  116. package/dist/types/core/aggregation/errors.d.ts.map +0 -1
  117. package/dist/types/core/aggregation/fallback-spend.d.ts +0 -94
  118. package/dist/types/core/aggregation/fallback-spend.d.ts.map +0 -1
  119. package/dist/types/core/aggregation/logger.d.ts +0 -22
  120. package/dist/types/core/aggregation/logger.d.ts.map +0 -1
  121. package/dist/types/core/aggregation/messages/base.d.ts +0 -58
  122. package/dist/types/core/aggregation/messages/base.d.ts.map +0 -1
  123. package/dist/types/core/aggregation/messages/bodies.d.ts +0 -177
  124. package/dist/types/core/aggregation/messages/bodies.d.ts.map +0 -1
  125. package/dist/types/core/aggregation/messages/constants.d.ts +0 -31
  126. package/dist/types/core/aggregation/messages/constants.d.ts.map +0 -1
  127. package/dist/types/core/aggregation/messages/factories.d.ts +0 -217
  128. package/dist/types/core/aggregation/messages/factories.d.ts.map +0 -1
  129. package/dist/types/core/aggregation/messages/guards.d.ts +0 -11
  130. package/dist/types/core/aggregation/messages/guards.d.ts.map +0 -1
  131. package/dist/types/core/aggregation/messages/index.d.ts +0 -6
  132. package/dist/types/core/aggregation/messages/index.d.ts.map +0 -1
  133. package/dist/types/core/aggregation/participant.d.ts +0 -162
  134. package/dist/types/core/aggregation/participant.d.ts.map +0 -1
  135. package/dist/types/core/aggregation/phases.d.ts +0 -55
  136. package/dist/types/core/aggregation/phases.d.ts.map +0 -1
  137. package/dist/types/core/aggregation/recovery-policy.d.ts +0 -132
  138. package/dist/types/core/aggregation/recovery-policy.d.ts.map +0 -1
  139. package/dist/types/core/aggregation/recovery-spend.d.ts +0 -94
  140. package/dist/types/core/aggregation/recovery-spend.d.ts.map +0 -1
  141. package/dist/types/core/aggregation/runner/aggregation-runner.d.ts +0 -72
  142. package/dist/types/core/aggregation/runner/aggregation-runner.d.ts.map +0 -1
  143. package/dist/types/core/aggregation/runner/events.d.ts +0 -147
  144. package/dist/types/core/aggregation/runner/events.d.ts.map +0 -1
  145. package/dist/types/core/aggregation/runner/index.d.ts +0 -6
  146. package/dist/types/core/aggregation/runner/index.d.ts.map +0 -1
  147. package/dist/types/core/aggregation/runner/participant-runner.d.ts +0 -122
  148. package/dist/types/core/aggregation/runner/participant-runner.d.ts.map +0 -1
  149. package/dist/types/core/aggregation/runner/service-runner.d.ts +0 -228
  150. package/dist/types/core/aggregation/runner/service-runner.d.ts.map +0 -1
  151. package/dist/types/core/aggregation/runner/typed-emitter.d.ts +0 -41
  152. package/dist/types/core/aggregation/runner/typed-emitter.d.ts.map +0 -1
  153. package/dist/types/core/aggregation/service.d.ts +0 -177
  154. package/dist/types/core/aggregation/service.d.ts.map +0 -1
  155. package/dist/types/core/aggregation/signer.d.ts +0 -50
  156. package/dist/types/core/aggregation/signer.d.ts.map +0 -1
  157. package/dist/types/core/aggregation/signing-session.d.ts +0 -81
  158. package/dist/types/core/aggregation/signing-session.d.ts.map +0 -1
  159. package/dist/types/core/aggregation/transport/didcomm.d.ts +0 -23
  160. package/dist/types/core/aggregation/transport/didcomm.d.ts.map +0 -1
  161. package/dist/types/core/aggregation/transport/error.d.ts +0 -8
  162. package/dist/types/core/aggregation/transport/error.d.ts.map +0 -1
  163. package/dist/types/core/aggregation/transport/factory.d.ts +0 -28
  164. package/dist/types/core/aggregation/transport/factory.d.ts.map +0 -1
  165. package/dist/types/core/aggregation/transport/http/client.d.ts +0 -48
  166. package/dist/types/core/aggregation/transport/http/client.d.ts.map +0 -1
  167. package/dist/types/core/aggregation/transport/http/envelope.d.ts +0 -64
  168. package/dist/types/core/aggregation/transport/http/envelope.d.ts.map +0 -1
  169. package/dist/types/core/aggregation/transport/http/errors.d.ts +0 -9
  170. package/dist/types/core/aggregation/transport/http/errors.d.ts.map +0 -1
  171. package/dist/types/core/aggregation/transport/http/inbox-buffer.d.ts +0 -32
  172. package/dist/types/core/aggregation/transport/http/inbox-buffer.d.ts.map +0 -1
  173. package/dist/types/core/aggregation/transport/http/index.d.ts +0 -12
  174. package/dist/types/core/aggregation/transport/http/index.d.ts.map +0 -1
  175. package/dist/types/core/aggregation/transport/http/nonce-cache.d.ts +0 -26
  176. package/dist/types/core/aggregation/transport/http/nonce-cache.d.ts.map +0 -1
  177. package/dist/types/core/aggregation/transport/http/protocol.d.ts +0 -53
  178. package/dist/types/core/aggregation/transport/http/protocol.d.ts.map +0 -1
  179. package/dist/types/core/aggregation/transport/http/rate-limiter.d.ts +0 -41
  180. package/dist/types/core/aggregation/transport/http/rate-limiter.d.ts.map +0 -1
  181. package/dist/types/core/aggregation/transport/http/request-auth.d.ts +0 -50
  182. package/dist/types/core/aggregation/transport/http/request-auth.d.ts.map +0 -1
  183. package/dist/types/core/aggregation/transport/http/server.d.ts +0 -110
  184. package/dist/types/core/aggregation/transport/http/server.d.ts.map +0 -1
  185. package/dist/types/core/aggregation/transport/http/sse-stream.d.ts +0 -34
  186. package/dist/types/core/aggregation/transport/http/sse-stream.d.ts.map +0 -1
  187. package/dist/types/core/aggregation/transport/http/sse-writer.d.ts +0 -12
  188. package/dist/types/core/aggregation/transport/http/sse-writer.d.ts.map +0 -1
  189. package/dist/types/core/aggregation/transport/in-memory.d.ts +0 -64
  190. package/dist/types/core/aggregation/transport/in-memory.d.ts.map +0 -1
  191. package/dist/types/core/aggregation/transport/index.d.ts +0 -8
  192. package/dist/types/core/aggregation/transport/index.d.ts.map +0 -1
  193. package/dist/types/core/aggregation/transport/nostr.d.ts +0 -153
  194. package/dist/types/core/aggregation/transport/nostr.d.ts.map +0 -1
  195. package/dist/types/core/aggregation/transport/transport.d.ts +0 -62
  196. package/dist/types/core/aggregation/transport/transport.d.ts.map +0 -1
  197. package/src/core/aggregation/beacon-strategy.ts +0 -142
  198. package/src/core/aggregation/cohort.ts +0 -415
  199. package/src/core/aggregation/conditions.ts +0 -172
  200. package/src/core/aggregation/errors.ts +0 -25
  201. package/src/core/aggregation/fallback-spend.ts +0 -223
  202. package/src/core/aggregation/logger.ts +0 -33
  203. package/src/core/aggregation/messages/base.ts +0 -80
  204. package/src/core/aggregation/messages/bodies.ts +0 -328
  205. package/src/core/aggregation/messages/constants.ts +0 -40
  206. package/src/core/aggregation/messages/factories.ts +0 -297
  207. package/src/core/aggregation/messages/guards.ts +0 -61
  208. package/src/core/aggregation/messages/index.ts +0 -5
  209. package/src/core/aggregation/participant.ts +0 -779
  210. package/src/core/aggregation/phases.ts +0 -91
  211. package/src/core/aggregation/recovery-policy.ts +0 -237
  212. package/src/core/aggregation/recovery-spend.ts +0 -192
  213. package/src/core/aggregation/runner/aggregation-runner.ts +0 -125
  214. package/src/core/aggregation/runner/events.ts +0 -129
  215. package/src/core/aggregation/runner/index.ts +0 -5
  216. package/src/core/aggregation/runner/participant-runner.ts +0 -488
  217. package/src/core/aggregation/runner/service-runner.ts +0 -888
  218. package/src/core/aggregation/runner/typed-emitter.ts +0 -87
  219. package/src/core/aggregation/service.ts +0 -1000
  220. package/src/core/aggregation/signer.ts +0 -67
  221. package/src/core/aggregation/signing-session.ts +0 -290
  222. package/src/core/aggregation/transport/didcomm.ts +0 -59
  223. package/src/core/aggregation/transport/error.ts +0 -13
  224. package/src/core/aggregation/transport/factory.ts +0 -65
  225. package/src/core/aggregation/transport/http/client.ts +0 -409
  226. package/src/core/aggregation/transport/http/envelope.ts +0 -204
  227. package/src/core/aggregation/transport/http/errors.ts +0 -11
  228. package/src/core/aggregation/transport/http/inbox-buffer.ts +0 -53
  229. package/src/core/aggregation/transport/http/index.ts +0 -11
  230. package/src/core/aggregation/transport/http/nonce-cache.ts +0 -43
  231. package/src/core/aggregation/transport/http/protocol.ts +0 -57
  232. package/src/core/aggregation/transport/http/rate-limiter.ts +0 -75
  233. package/src/core/aggregation/transport/http/request-auth.ts +0 -164
  234. package/src/core/aggregation/transport/http/server.ts +0 -615
  235. package/src/core/aggregation/transport/http/sse-stream.ts +0 -121
  236. package/src/core/aggregation/transport/http/sse-writer.ts +0 -23
  237. package/src/core/aggregation/transport/in-memory.ts +0 -178
  238. package/src/core/aggregation/transport/index.ts +0 -7
  239. package/src/core/aggregation/transport/nostr.ts +0 -576
  240. package/src/core/aggregation/transport/transport.ts +0 -79
@@ -1,888 +0,0 @@
1
- import type { SchnorrKeyPair } from '@did-btcr2/keypair';
2
- import { AggregationServiceError } from '../errors.js';
3
- import type { BaseMessage } from '../messages/base.js';
4
- import {
5
- COHORT_OPT_IN,
6
- FALLBACK_SIGNATURE,
7
- NONCE_CONTRIBUTION,
8
- SIGNATURE_AUTHORIZATION,
9
- SUBMIT_NONINCLUDED,
10
- SUBMIT_UPDATE,
11
- VALIDATION_ACK,
12
- } from '../messages/constants.js';
13
- import { ServiceCohortPhase } from '../phases.js';
14
- import type { ServiceCohortPhaseType } from '../phases.js';
15
- import type {
16
- AggregationResult,
17
- CohortConfig,
18
- PendingOptIn,
19
- SigningTxData} from '../service.js';
20
- import {
21
- AggregationService
22
- } from '../service.js';
23
- import type { Transport } from '../transport/transport.js';
24
- import { DEFAULT_FEE_ESTIMATOR } from '../../beacon/fee-estimator.js';
25
- import type { FeeEstimator } from '../../beacon/fee-estimator.js';
26
- import type { AggregationServiceEvents } from './events.js';
27
- import { TypedEventEmitter } from './typed-emitter.js';
28
-
29
- /** Decision callback: accept or reject a participant's opt-in. */
30
- export type OnOptInReceived = (optIn: PendingOptIn) => Promise<{ accepted: boolean }>;
31
-
32
- /** Decision callback: finalize keygen now, or wait for more participants. */
33
- export type OnReadyToFinalize = (info: {
34
- acceptedCount: number;
35
- minRequired: number;
36
- }) => Promise<{ finalize: boolean }>;
37
-
38
- /** Data callback: provide the Bitcoin transaction data to sign. */
39
- export type OnProvideTxData = (info: {
40
- cohortId: string;
41
- beaconAddress: string;
42
- signalBytes: Uint8Array;
43
- /**
44
- * Fee estimator the runner is configured with (the runner's `feeEstimator`
45
- * option, or a static 5 sat/vB default). Forward it to the beacon transaction
46
- * builder so a dynamic rate injected at the runner is honored, rather than
47
- * hard-coding a rate inside this callback (ADR 045).
48
- */
49
- feeEstimator: FeeEstimator;
50
- }) => Promise<SigningTxData>;
51
-
52
- export interface AggregationServiceRunnerOptions {
53
- /** Underlying transport (NostrTransport, MockTransport, etc.). */
54
- transport: Transport;
55
-
56
- /** This service's identity. */
57
- did: string;
58
- keys: SchnorrKeyPair;
59
-
60
- /**
61
- * Default cohort configuration for the {@link AggregationServiceRunner.run}
62
- * convenience path. Optional: omit it when driving the runner with
63
- * {@link AggregationServiceRunner.advertiseCohort}, which takes a per-cohort
64
- * config and can be called many times on one runner.
65
- */
66
- config?: CohortConfig;
67
-
68
- /**
69
- * Decide whether to accept a participant's opt-in.
70
- * Default: auto-accept all opt-ins.
71
- */
72
- onOptInReceived?: OnOptInReceived;
73
-
74
- /**
75
- * Decide whether to finalize keygen now or wait for more participants.
76
- * Called after each accepted opt-in once minParticipants is reached.
77
- * Default: finalize as soon as minParticipants is reached.
78
- */
79
- onReadyToFinalize?: OnReadyToFinalize;
80
-
81
- /**
82
- * Provide the Bitcoin transaction data to sign.
83
- * REQUIRED - no sensible default.
84
- */
85
- onProvideTxData: OnProvideTxData;
86
-
87
- /**
88
- * Fee estimator passed to {@link OnProvideTxData} so the beacon transaction the
89
- * callback builds is sized at a chosen rate. Inject a dynamic estimator (a mempool
90
- * API or Bitcoin Core `estimatesmartfee`) here as the single standard point for
91
- * fee-rate selection, instead of hard-coding a rate inside the callback (ADR 045).
92
- * Defaults to a static 5 sat/vB estimator.
93
- */
94
- feeEstimator?: FeeEstimator;
95
-
96
- /**
97
- * Maximum canonicalized byte-length of a signed update body. Submissions
98
- * above this cap are rejected and surfaced via the `message-rejected` event.
99
- * Defaults to {@link DEFAULT_MAX_UPDATE_SIZE_BYTES} (256 KiB).
100
- */
101
- maxUpdateSizeBytes?: number;
102
-
103
- /**
104
- * Overall wall-clock budget for each cohort, from advertise to
105
- * signing-complete. On expiry the cohort is dropped, `cohort-failed` is
106
- * emitted, and that cohort's completion rejects with a timeout error. Other
107
- * cohorts on the same runner are unaffected. Leave undefined to disable.
108
- */
109
- cohortTtlMs?: number;
110
-
111
- /**
112
- * Maximum time allowed between phase transitions for a cohort. Protects
113
- * against stalled cohorts (e.g. a participant vanishing mid-protocol). Reset
114
- * automatically on every observed phase change. Applied per cohort. Leave
115
- * undefined to disable.
116
- */
117
- phaseTimeoutMs?: number;
118
-
119
- /**
120
- * Re-publish COHORT_ADVERT on this interval until a cohort's keygen is
121
- * finalized. Works around relays that don't backfill historical events to
122
- * late subscribers - a republish gives late joiners a window to discover the
123
- * advert without protocol changes. The first publish is immediate;
124
- * subsequent publishes fire every `advertRepeatIntervalMs` until that
125
- * cohort's keygen completes, fails, or is stopped. Defaults to
126
- * {@link DEFAULT_ADVERT_REPEAT_INTERVAL_MS} (60 s). Set to 0 to publish
127
- * once and never retry.
128
- */
129
- advertRepeatIntervalMs?: number;
130
-
131
- /**
132
- * When a cohort stalls (phase timeout) while the optimistic n-of-n signing
133
- * round is in flight, fall back to the k-of-n script path instead of failing
134
- * the cohort (graceful liveness, ADR 042). Off by default: enabling it trades a
135
- * cheaper/private key-path spend for a larger script-path spend whenever the
136
- * optimistic round does not complete in time. A stall outside the signing
137
- * phases still fails the cohort. Operators can also drive the fallback
138
- * explicitly via {@link AggregationServiceRunner.triggerFallback}.
139
- */
140
- autoFallbackOnStall?: boolean;
141
- }
142
-
143
- /** Default cadence for re-publishing COHORT_ADVERT until keygen completes: 60 seconds. */
144
- export const DEFAULT_ADVERT_REPEAT_INTERVAL_MS = 60_000;
145
-
146
- /**
147
- * Per-cohort runtime bookkeeping the runner keeps for each advertised cohort.
148
- * One {@link RunContext} per cohortId lives in the runner's `#contexts` map so
149
- * many cohorts run concurrently on a single runner, each with its own
150
- * completion promise, finalize guard, timers, and advert-republish loop. The
151
- * underlying {@link AggregationService} state machine is already keyed by
152
- * cohortId; this struct is the runner-layer counterpart (see ADR 040).
153
- */
154
- interface RunContext {
155
- /** The cohort this context drives. */
156
- cohortId: string;
157
- /** The conditions this cohort was advertised with. */
158
- config: CohortConfig;
159
- /** Resolve this cohort's completion with its aggregation result. */
160
- resolve: (result: AggregationResult) => void;
161
- /** Reject this cohort's completion. */
162
- reject: (err: Error) => void;
163
- /** The promise handed back from {@link AggregationServiceRunner.advertiseCohort}. */
164
- completion: Promise<AggregationResult>;
165
- /**
166
- * Guard against the async race where two concurrent #handleOptIn invocations
167
- * for THIS cohort both pass the `participants.length >= minParticipants`
168
- * check before either mutates the cohort phase. Set synchronously before any
169
- * `await` so subsequent handlers observe it on their next resumption.
170
- */
171
- finalizing: boolean;
172
- /** Once settled (resolved or rejected), late timers/messages must not re-settle. */
173
- settled: boolean;
174
- /**
175
- * The spend path this cohort is committed to once signing reaches a decision:
176
- * `optimistic` (n-of-n key path) or `fallback` (k-of-n script path). A cohort
177
- * spends its single beacon UTXO exactly once, so this latch (set synchronously
178
- * before any await) ensures the optimistic completion and the fallback never
179
- * both finalize and broadcast the same UTXO (the ADR-042 double-spend hazard).
180
- */
181
- committedPath?: 'optimistic' | 'fallback';
182
- cohortTtlTimer?: ReturnType<typeof setTimeout>;
183
- phaseTimer?: ReturnType<typeof setTimeout>;
184
- lastObservedPhase?: string;
185
- /** Stop handle for THIS cohort's repeating COHORT_ADVERT publish loop. */
186
- stopAdvertRepeat?: () => void;
187
- }
188
-
189
- /**
190
- * High-level facade for running an Aggregation Service over a Transport.
191
- *
192
- * Wires the {@link AggregationService} state machine to a {@link Transport},
193
- * encapsulating message handler registration, outgoing message dispatch,
194
- * and decision callback orchestration.
195
- *
196
- * A single runner is a long-lived multiplexer: it advertises and drives many
197
- * cohorts concurrently over one transport. Each advertised cohort owns an
198
- * independent completion promise and fails in isolation - a stalled or failed
199
- * cohort never settles its siblings (see ADR 040). Use
200
- * {@link AggregationServiceRunner.advertiseCohort} for the multi-cohort path;
201
- * {@link AggregationServiceRunner.run} is a thin single-cohort convenience over
202
- * it.
203
- *
204
- * @example
205
- * ```typescript
206
- * const transport = new NostrTransport({ relays: [RELAY] });
207
- * transport.registerActor(serviceDid, serviceKeys);
208
- *
209
- * const runner = new AggregationServiceRunner({
210
- * transport,
211
- * did: serviceDid,
212
- * keys: serviceKeys,
213
- * onProvideTxData: async ({ beaconAddress, signalBytes, feeEstimator }) => {
214
- * // Forward feeEstimator so a dynamic rate injected at the runner is honored.
215
- * return await buildBeaconTransaction(beaconAddress, signalBytes, bitcoin, feeEstimator);
216
- * },
217
- * });
218
- *
219
- * runner.on('keygen-complete', ({ cohortId, beaconAddress }) => console.log(beaconAddress));
220
- * runner.on('signing-complete', ({ cohortId, signature }) => console.log('done', cohortId));
221
- *
222
- * // Multi-cohort: advertise several cohorts; each completion resolves independently.
223
- * const a = runner.advertiseCohort({ minParticipants: 2, network: 'mutinynet', beaconType: 'CASBeacon' });
224
- * const b = runner.advertiseCohort({ minParticipants: 3, network: 'mutinynet', beaconType: 'SMTBeacon' });
225
- * const [ra, rb] = await Promise.all([a.completion, b.completion]);
226
- *
227
- * // Single-cohort convenience (requires `config` in the options):
228
- * // const result = await runner.run();
229
- * ```
230
- *
231
- * For full manual control, drop down to the underlying state machine via
232
- * `runner.session`. The state machine has no transport coupling and exposes
233
- * every protocol decision as an explicit method.
234
- *
235
- * @class AggregationServiceRunner
236
- */
237
- export class AggregationServiceRunner extends TypedEventEmitter<AggregationServiceEvents> {
238
- /** Direct access to the underlying state machine for advanced use. */
239
- readonly session: AggregationService;
240
-
241
- readonly #transport: Transport;
242
- readonly #did: string;
243
- readonly #defaultConfig?: CohortConfig;
244
- readonly #onOptInReceived: OnOptInReceived;
245
- readonly #onReadyToFinalize: OnReadyToFinalize;
246
- readonly #onProvideTxData: OnProvideTxData;
247
- readonly #feeEstimator: FeeEstimator;
248
- readonly #cohortTtlMs?: number;
249
- readonly #phaseTimeoutMs?: number;
250
- readonly #advertRepeatIntervalMs: number;
251
- readonly #autoFallbackOnStall: boolean;
252
-
253
- /** Phases during which a stall can be salvaged by the k-of-n fallback (ADR 042). */
254
- static readonly #SIGNING_PHASES: readonly ServiceCohortPhaseType[] = [
255
- ServiceCohortPhase.SigningStarted,
256
- ServiceCohortPhase.NoncesCollected,
257
- ServiceCohortPhase.AwaitingPartialSigs,
258
- ];
259
-
260
- /** Per-cohort run state, keyed by cohortId. */
261
- readonly #contexts: Map<string, RunContext> = new Map();
262
- #handlersRegistered = false;
263
- #stopped = false;
264
-
265
- constructor(options: AggregationServiceRunnerOptions) {
266
- super();
267
- this.#transport = options.transport;
268
- this.#did = options.did;
269
- this.#defaultConfig = options.config;
270
- this.#onOptInReceived = options.onOptInReceived ?? (async () => ({ accepted: true }));
271
- this.#onReadyToFinalize = options.onReadyToFinalize ?? (async ({ acceptedCount, minRequired }) => ({
272
- finalize : acceptedCount >= minRequired,
273
- }));
274
- this.#onProvideTxData = options.onProvideTxData;
275
- this.#feeEstimator = options.feeEstimator ?? DEFAULT_FEE_ESTIMATOR;
276
- this.#cohortTtlMs = options.cohortTtlMs;
277
- this.#phaseTimeoutMs = options.phaseTimeoutMs;
278
- this.#advertRepeatIntervalMs = options.advertRepeatIntervalMs ?? DEFAULT_ADVERT_REPEAT_INTERVAL_MS;
279
- this.#autoFallbackOnStall = options.autoFallbackOnStall ?? false;
280
-
281
- this.session = new AggregationService({
282
- // The coordinator never signs, so the state machine receives only the
283
- // public half of the operator's keypair (see ADR 038). The full keypair
284
- // remains the operator's transport/communication identity.
285
- did : options.did,
286
- publicKey : options.keys.publicKey,
287
- maxUpdateSizeBytes : options.maxUpdateSizeBytes,
288
- });
289
- }
290
-
291
- /** Resolve the {@link RunContext} an inbound message belongs to, by cohortId. */
292
- #contextFor(msg: BaseMessage): RunContext | undefined {
293
- const cohortId = msg.body?.cohortId;
294
- if(!cohortId) return undefined;
295
- return this.#contexts.get(cohortId);
296
- }
297
-
298
- /**
299
- * Drain any silent rejections the state machine recorded for a cohort during
300
- * the most recent receive() and surface them as `message-rejected` events.
301
- */
302
- #drainRejections(ctx: RunContext): void {
303
- for(const r of this.session.drainRejections(ctx.cohortId)) {
304
- this.emit('message-rejected', { cohortId: ctx.cohortId, ...r });
305
- }
306
- }
307
-
308
- /**
309
- * Advertise a new cohort and begin driving it to completion. Callable many
310
- * times on one runner; each cohort runs concurrently and independently.
311
- *
312
- * @param config Per-cohort conditions + network (see {@link CohortConfig}).
313
- * @returns The new cohort's id and a `completion` promise that resolves with
314
- * that cohort's {@link AggregationResult} (or rejects if it fails/stalls).
315
- * @throws If the runner has been stopped, or the config is invalid
316
- * (fail-fast via `createCohort`).
317
- */
318
- advertiseCohort(config: CohortConfig): { cohortId: string; completion: Promise<AggregationResult> } {
319
- if(this.#stopped) {
320
- throw new AggregationServiceError('Cannot advertise on a stopped runner.', 'RUNNER_STOPPED', {});
321
- }
322
- this.#registerHandlers();
323
- // createCohort validates the conditions and throws on a bad config before
324
- // any context exists - fail-fast, nothing to clean up.
325
- const cohortId = this.session.createCohort(config);
326
-
327
- let resolve!: (result: AggregationResult) => void;
328
- let reject!: (err: Error) => void;
329
- const completion = new Promise<AggregationResult>((res, rej) => { resolve = res; reject = rej; });
330
- const ctx: RunContext = {
331
- cohortId,
332
- config,
333
- resolve,
334
- reject,
335
- completion,
336
- finalizing : false,
337
- settled : false,
338
- };
339
- this.#contexts.set(cohortId, ctx);
340
-
341
- try {
342
- this.#startTimers(ctx);
343
- // Emit cohort-advertised BEFORE the send so the event fires before any downstream cascade.
344
- const advertMsgs = this.session.advertise(cohortId);
345
- this.#onPhaseMaybeChanged(ctx);
346
- this.emit('cohort-advertised', { cohortId });
347
- // Publish the advert. If advertRepeatIntervalMs > 0 we republish on that
348
- // cadence until this cohort's keygen-complete / fail / stop - works around
349
- // relays that don't backfill historical events to late subscribers.
350
- // Otherwise fall back to a single send.
351
- if(this.#advertRepeatIntervalMs > 0) {
352
- this.#startAdvertRepeat(ctx, advertMsgs);
353
- } else {
354
- this.#sendAll(advertMsgs).catch(err => this.#failCohort(ctx, err as Error));
355
- }
356
- } catch(err) {
357
- this.#failCohort(ctx, err as Error);
358
- }
359
-
360
- return { cohortId, completion };
361
- }
362
-
363
- /**
364
- * Run a single cohort to completion using the `config` supplied in the
365
- * runner options. Thin convenience over {@link advertiseCohort} for the
366
- * single-cohort case (and the path {@link AggregationRunner.solo} rides).
367
- *
368
- * @returns {Promise<AggregationResult>} The final result with signature and signed tx.
369
- */
370
- run(): Promise<AggregationResult> {
371
- if(!this.#defaultConfig) {
372
- return Promise.reject(new AggregationServiceError(
373
- 'run() requires `config` in the runner options; use advertiseCohort(config) to drive cohorts explicitly.',
374
- 'MISSING_COHORT_CONFIG', {}
375
- ));
376
- }
377
- try {
378
- return this.advertiseCohort(this.#defaultConfig).completion;
379
- } catch(err) {
380
- return Promise.reject(err as Error);
381
- }
382
- }
383
-
384
- /**
385
- * Wait for every currently-outstanding cohort to settle and return the
386
- * successful results. Dynamic drain: cohorts advertised while this is pending
387
- * are included, and it resolves only once no cohorts remain. Failed cohorts
388
- * are surfaced via `error` / `cohort-failed` events and their rejected
389
- * `completion` promises; they are omitted from the returned array (this
390
- * method does not throw). Bound long-running cohorts with `cohortTtlMs` /
391
- * `phaseTimeoutMs` or this may never resolve.
392
- *
393
- * @returns {Promise<AggregationResult[]>} Results of the cohorts that completed.
394
- */
395
- async runAll(): Promise<AggregationResult[]> {
396
- const collected = new Map<string, AggregationResult>();
397
- // Capture every completion, including a cohort that is advertised and
398
- // finishes entirely within one drain round (so it never appears in a
399
- // snapshot below).
400
- const onComplete = (result: AggregationResult): void => { collected.set(result.cohortId, result); };
401
- this.on('signing-complete', onComplete);
402
- try {
403
- // Block until the live set empties; re-snapshot each round to pick up
404
- // cohorts advertised mid-drain.
405
- while(this.#contexts.size > 0) {
406
- await Promise.allSettled([ ...this.#contexts.values() ].map(c => c.completion));
407
- }
408
- } finally {
409
- this.off('signing-complete', onComplete);
410
- }
411
- return [ ...collected.values() ];
412
- }
413
-
414
- /**
415
- * Begin publishing a cohort's advert immediately and on a repeating interval
416
- * until the cohort's advert loop is stopped. Each advert is broadcast (no
417
- * recipient) via the transport's `publishRepeating` primitive.
418
- */
419
- #startAdvertRepeat(ctx: RunContext, advertMsgs: BaseMessage[]): void {
420
- // COHORT_ADVERT is always a single broadcast message in the current
421
- // protocol, but iterate for generality.
422
- const stops: Array<() => void> = [];
423
- for(const msg of advertMsgs) {
424
- stops.push(this.#transport.publishRepeating(msg, this.#did, this.#advertRepeatIntervalMs));
425
- }
426
- ctx.stopAdvertRepeat = () => {
427
- for(const stop of stops) {
428
- try { stop(); } catch { /* ignore */ }
429
- }
430
- };
431
- }
432
-
433
- /** Stop a cohort's advert republish loop. Idempotent. */
434
- #stopAdvertRepeating(ctx: RunContext): void {
435
- if(!ctx.stopAdvertRepeat) return;
436
- const stop = ctx.stopAdvertRepeat;
437
- ctx.stopAdvertRepeat = undefined;
438
- stop();
439
- }
440
-
441
- /** Schedule a cohort's TTL + phase timeout when it is advertised. */
442
- #startTimers(ctx: RunContext): void {
443
- if(this.#cohortTtlMs !== undefined) {
444
- ctx.cohortTtlTimer = setTimeout(() => {
445
- const reason = `Cohort ${ctx.cohortId} exceeded TTL of ${this.#cohortTtlMs}ms`;
446
- this.emit('cohort-failed', { cohortId: ctx.cohortId, reason });
447
- this.#failCohort(ctx, new Error(reason));
448
- }, this.#cohortTtlMs);
449
- }
450
- this.#resetPhaseTimer(ctx);
451
- }
452
-
453
- /** Reset a cohort's per-phase stall timer. Called when a phase transition is observed. */
454
- #resetPhaseTimer(ctx: RunContext): void {
455
- if(ctx.phaseTimer) clearTimeout(ctx.phaseTimer);
456
- ctx.phaseTimer = undefined;
457
- if(this.#phaseTimeoutMs === undefined) return;
458
- ctx.phaseTimer = setTimeout(() => {
459
- // A stall during the optimistic signing round can be salvaged by the k-of-n
460
- // fallback rather than failing the whole cohort (graceful liveness, ADR
461
- // 042) - but only if enabled and not already committed to a path.
462
- const phase = this.session.getCohortPhase(ctx.cohortId);
463
- const inSigning = phase !== undefined && AggregationServiceRunner.#SIGNING_PHASES.includes(phase);
464
- if(this.#autoFallbackOnStall && inSigning && !ctx.committedPath && !ctx.settled) {
465
- this.triggerFallback(ctx.cohortId).catch(err => this.#failCohort(ctx, err as Error));
466
- return;
467
- }
468
- const reason = `Cohort ${ctx.cohortId} stalled in phase ${ctx.lastObservedPhase ?? '?'} for ${this.#phaseTimeoutMs}ms`;
469
- this.emit('cohort-failed', { cohortId: ctx.cohortId, reason });
470
- this.#failCohort(ctx, new Error(reason));
471
- }, this.#phaseTimeoutMs);
472
- }
473
-
474
- /**
475
- * Abandon the optimistic n-of-n key path for a cohort and collect k-of-n
476
- * fallback (script-path) signatures instead (ADR 042). Idempotent and safe
477
- * against the optimistic completion: it commits the cohort to the fallback
478
- * path synchronously (the `committedPath` latch) before sending anything, so a
479
- * late optimistic signature can no longer complete-and-broadcast a competing
480
- * spend of the same UTXO. No-op if the cohort is unknown, already settled, or
481
- * already committed to a path.
482
- *
483
- * Wired automatically to the phase-stall timer when `autoFallbackOnStall` is
484
- * set; otherwise call it from an operator decision (a UI "fall back now"
485
- * action). Throws only if the underlying state machine rejects the transition
486
- * (e.g. signing has not started).
487
- */
488
- async triggerFallback(cohortId: string): Promise<void> {
489
- const ctx = this.#contexts.get(cohortId);
490
- if(!ctx || ctx.settled || ctx.committedPath) return;
491
- // startFallbackSigning is synchronous and throws if the cohort is not in a
492
- // signing phase (e.g. a premature operator call). Run it FIRST so a rejected
493
- // transition cannot poison the latch: only after it commits the state machine
494
- // to the fallback do we set committedPath. This still happens synchronously
495
- // before any await, so a concurrent optimistic completion observes the latch
496
- // and stands down - but a bad-phase call leaves the optimistic path intact.
497
- const messages = this.session.startFallbackSigning(cohortId);
498
- ctx.committedPath = 'fallback';
499
- this.#stopAdvertRepeating(ctx);
500
- this.#onPhaseMaybeChanged(ctx);
501
- const sessionId = this.session.getSigningSessionId(cohortId) ?? '';
502
- this.emit('fallback-started', { cohortId, sessionId });
503
- await this.#sendAll(messages);
504
- }
505
-
506
- /** Detect a phase change for a cohort since the last observation and reset its phase timer. */
507
- #onPhaseMaybeChanged(ctx: RunContext): void {
508
- const phase = this.session.getCohortPhase(ctx.cohortId);
509
- if(phase !== ctx.lastObservedPhase) {
510
- ctx.lastObservedPhase = phase;
511
- this.#resetPhaseTimer(ctx);
512
- }
513
- }
514
-
515
- /** Clear a cohort's timers. Called on completion, stop, and failure. */
516
- #clearTimers(ctx: RunContext): void {
517
- if(ctx.cohortTtlTimer) clearTimeout(ctx.cohortTtlTimer);
518
- if(ctx.phaseTimer) clearTimeout(ctx.phaseTimer);
519
- ctx.cohortTtlTimer = undefined;
520
- ctx.phaseTimer = undefined;
521
- }
522
-
523
- /**
524
- * Reclaim one cohort's runner-layer bookkeeping: stop its advert loop, clear
525
- * its timers, and drop its {@link RunContext}. Does NOT touch sibling cohorts
526
- * and does NOT detach the shared transport handlers. Leaves the cohort in the
527
- * state machine; whether that cohort's `session` state is also removed is the
528
- * caller's choice (see {@link #completeCohort} vs {@link #failCohort}).
529
- */
530
- #disposeCohort(ctx: RunContext): void {
531
- this.#stopAdvertRepeating(ctx);
532
- this.#clearTimers(ctx);
533
- this.#contexts.delete(ctx.cohortId);
534
- }
535
-
536
- /**
537
- * Settle one cohort successfully. Reclaims the runner context but leaves the
538
- * completed cohort in `session` so callers can read its beaconAddress / cohort
539
- * via `session.getCohort(result.cohortId)`; reclaim it with
540
- * `session.removeCohort(cohortId)` when done. Idempotent via `ctx.settled`.
541
- */
542
- #completeCohort(ctx: RunContext, result: AggregationResult): void {
543
- if(ctx.settled) return;
544
- ctx.settled = true;
545
- this.#disposeCohort(ctx);
546
- this.emit('signing-complete', result);
547
- ctx.resolve(result);
548
- }
549
-
550
- /**
551
- * Fail one cohort. Reclaims its runner context, drops its now-dead state from
552
- * the state machine, and rejects only its completion; siblings keep running
553
- * and the shared transport handlers stay registered. Idempotent via
554
- * `ctx.settled`.
555
- */
556
- #failCohort(ctx: RunContext, err: Error): void {
557
- if(ctx.settled) return;
558
- ctx.settled = true;
559
- this.#disposeCohort(ctx);
560
- this.session.removeCohort(ctx.cohortId);
561
- this.emit('error', err);
562
- ctx.reject(err);
563
- }
564
-
565
- /**
566
- * Stop a single cohort early without affecting the rest of the runner. Drops
567
- * the cohort's state machine state; its `completion` promise rejects with a
568
- * stopped error.
569
- */
570
- stopCohort(cohortId: string): void {
571
- const ctx = this.#contexts.get(cohortId);
572
- if(!ctx || ctx.settled) return;
573
- ctx.settled = true;
574
- this.#disposeCohort(ctx);
575
- this.session.removeCohort(cohortId);
576
- ctx.reject(new AggregationServiceError(`Cohort ${cohortId} stopped.`, 'COHORT_STOPPED', { cohortId }));
577
- }
578
-
579
- /**
580
- * Stop the whole runner. Fails every outstanding cohort, then detaches the
581
- * shared transport handlers so a restart or a new runner doesn't inherit
582
- * stale dispatch. Safe to call repeatedly.
583
- */
584
- stop(): void {
585
- this.#stopped = true;
586
- for(const ctx of [ ...this.#contexts.values() ]) {
587
- if(ctx.settled) continue;
588
- ctx.settled = true;
589
- this.#disposeCohort(ctx);
590
- this.session.removeCohort(ctx.cohortId);
591
- ctx.reject(new AggregationServiceError('Service runner stopped.', 'RUNNER_STOPPED', { cohortId: ctx.cohortId }));
592
- }
593
- this.#contexts.clear();
594
- this.#unregisterHandlers();
595
- }
596
-
597
- /** Message types this runner listens for on the transport. */
598
- static readonly #HANDLED_MESSAGE_TYPES: readonly string[] = [
599
- COHORT_OPT_IN,
600
- SUBMIT_UPDATE,
601
- SUBMIT_NONINCLUDED,
602
- VALIDATION_ACK,
603
- NONCE_CONTRIBUTION,
604
- SIGNATURE_AUTHORIZATION,
605
- FALLBACK_SIGNATURE,
606
- ];
607
-
608
- /**
609
- * Internal: handler registration with the transport. Idempotent. Handlers
610
- * are DID-scoped and cohort-agnostic - one registration serves every cohort
611
- * this runner drives; demux to the right {@link RunContext} happens in each
612
- * handler via the inbound message's cohortId.
613
- */
614
- #registerHandlers(): void {
615
- if(this.#handlersRegistered) return;
616
- this.#handlersRegistered = true;
617
-
618
- this.#transport.registerMessageHandler(this.#did, COHORT_OPT_IN, this.#handleOptIn.bind(this));
619
- this.#transport.registerMessageHandler(this.#did, SUBMIT_UPDATE, this.#handleSubmitUpdate.bind(this));
620
- // A non-inclusion (decline) is an update-phase response handled identically:
621
- // session.receive() routes by type, and the response gate + distribute
622
- // trigger are shared with SUBMIT_UPDATE.
623
- this.#transport.registerMessageHandler(this.#did, SUBMIT_NONINCLUDED, this.#handleSubmitUpdate.bind(this));
624
- this.#transport.registerMessageHandler(this.#did, VALIDATION_ACK, this.#handleValidationAck.bind(this));
625
- this.#transport.registerMessageHandler(this.#did, NONCE_CONTRIBUTION, this.#handleNonceContribution.bind(this));
626
- this.#transport.registerMessageHandler(this.#did, SIGNATURE_AUTHORIZATION, this.#handleSignatureAuthorization.bind(this));
627
- this.#transport.registerMessageHandler(this.#did, FALLBACK_SIGNATURE, this.#handleFallbackSignature.bind(this));
628
- }
629
-
630
- /** Internal: detach from the transport. Safe to call repeatedly. */
631
- #unregisterHandlers(): void {
632
- if(!this.#handlersRegistered) return;
633
- this.#handlersRegistered = false;
634
- for(const type of AggregationServiceRunner.#HANDLED_MESSAGE_TYPES) {
635
- this.#transport.unregisterMessageHandler(this.#did, type);
636
- }
637
- }
638
-
639
- /**
640
- * Internal: message handlers for each protocol step. Each handler:
641
- * 1) resolves the cohort the message belongs to (by cohortId); ignores it if unknown
642
- * 2) feeds the message into the state machine via session.receive()
643
- * 3) emits a high-level event (carrying cohortId) for external observers
644
- * 4) checks if the new state triggers any automatic next steps, and if so:
645
- * a) calls the appropriate decision callback(s)
646
- * b) sends any resulting messages from the state machine
647
- * Errors fail only the owning cohort. A stopped runner ignores messages.
648
- * @param {BaseMessage} msg - The incoming message to handle.
649
- * @returns {Promise<void>} Resolves when handling is complete.
650
- */
651
- async #handleOptIn(msg: BaseMessage): Promise<void> {
652
- if(this.#stopped) return;
653
- const ctx = this.#contextFor(msg);
654
- if(!ctx) return;
655
- try {
656
- this.session.receive(msg);
657
- this.#drainRejections(ctx);
658
- this.#onPhaseMaybeChanged(ctx);
659
-
660
- const optIn = this.session.pendingOptIns(ctx.cohortId).get(msg.from);
661
- if(!optIn) return;
662
- // PendingOptIn already carries cohortId, so this event is cohort-identified.
663
- this.emit('opt-in-received', optIn);
664
-
665
- // Register peer key for encrypted messaging
666
- if(optIn.communicationPk) {
667
- this.#transport.registerPeer(msg.from, optIn.communicationPk);
668
- }
669
-
670
- const decision = await this.#onOptInReceived(optIn);
671
- if(!decision.accepted) return;
672
-
673
- // Don't accept past the advertised maxParticipants: acceptParticipant
674
- // would throw COHORT_FULL and fail the cohort. Silently ignore the surplus
675
- // opt-in (the cohort is full).
676
- const maxParticipants = ctx.config.maxParticipants;
677
- const cohortNow = this.session.getCohort(ctx.cohortId);
678
- if(maxParticipants !== undefined && cohortNow && cohortNow.participants.length >= maxParticipants) {
679
- return;
680
- }
681
-
682
- await this.#sendAll(this.session.acceptParticipant(ctx.cohortId, msg.from));
683
- this.emit('participant-accepted', { cohortId: ctx.cohortId, participantDid: msg.from });
684
-
685
- // Check if it's time to finalize. The per-cohort `finalizing` flag is set
686
- // synchronously before the first await so concurrent opt-in handlers for
687
- // the same cohort observe it and skip - otherwise two handlers could both
688
- // pass the minParticipants check and both call finalizeKeygen, the second
689
- // of which would throw (phase mismatch).
690
- const cohort = this.session.getCohort(ctx.cohortId)!;
691
- if(cohort.participants.length >= ctx.config.minParticipants && !ctx.finalizing) {
692
- ctx.finalizing = true;
693
- const finalizeDecision = await this.#onReadyToFinalize({
694
- acceptedCount : cohort.participants.length,
695
- minRequired : ctx.config.minParticipants,
696
- });
697
- if(!finalizeDecision.finalize) {
698
- // Operator declined - reset the flag so a later opt-in can retry.
699
- ctx.finalizing = false;
700
- return;
701
- }
702
- // finalizeKeygen() computes the beacon address synchronously
703
- // emit BEFORE awaiting sendAll. Otherwise the downstream cascade
704
- // (which can run all the way to signing-complete) would resolve the
705
- // cohort's completion promise before this event fires.
706
- const readyMsgs = this.session.finalizeKeygen(ctx.cohortId);
707
- // Keygen done - stop re-advertising the cohort. New participants
708
- // arriving after this point would be rejected anyway.
709
- this.#stopAdvertRepeating(ctx);
710
- this.emit('keygen-complete', {
711
- cohortId : ctx.cohortId,
712
- beaconAddress : cohort.beaconAddress,
713
- });
714
- await this.#sendAll(readyMsgs);
715
- }
716
- } catch(err) {
717
- this.#failCohort(ctx, err as Error);
718
- }
719
- }
720
-
721
- /**
722
- * Handler for receiving participant updates. When all updates are received, automatically builds
723
- * and distributes the data for validation.
724
- * @param {BaseMessage} msg - The incoming message to handle.
725
- * @returns {Promise<void>} Resolves when handling is complete.
726
- */
727
- async #handleSubmitUpdate(msg: BaseMessage): Promise<void> {
728
- if(this.#stopped) return;
729
- const ctx = this.#contextFor(msg);
730
- if(!ctx) return;
731
- try {
732
- this.session.receive(msg);
733
- this.#drainRejections(ctx);
734
- this.#onPhaseMaybeChanged(ctx);
735
- this.emit('update-received', { cohortId: ctx.cohortId, participantDid: msg.from });
736
-
737
- // When all updates collected, build and distribute
738
- if(this.session.getCohortPhase(ctx.cohortId) === ServiceCohortPhase.UpdatesCollected) {
739
- const distributeMsgs = this.session.buildAndDistribute(ctx.cohortId);
740
- this.emit('data-distributed', { cohortId: ctx.cohortId });
741
- await this.#sendAll(distributeMsgs);
742
- }
743
- } catch(err) {
744
- this.#failCohort(ctx, err as Error);
745
- }
746
- }
747
-
748
- /**
749
- * Handler for receiving validation acknowledgments. When all validations are received,
750
- * automatically requests tx data and starts signing.
751
- * @param {BaseMessage} msg - The incoming message to handle.
752
- * @returns {Promise<void>} Resolves when handling is complete.
753
- */
754
- async #handleValidationAck(msg: BaseMessage): Promise<void> {
755
- if(this.#stopped) return;
756
- const ctx = this.#contextFor(msg);
757
- if(!ctx) return;
758
- try {
759
- this.session.receive(msg);
760
- this.#drainRejections(ctx);
761
- this.#onPhaseMaybeChanged(ctx);
762
- const approved = !!msg.body?.approved;
763
- this.emit('validation-received', { cohortId: ctx.cohortId, participantDid: msg.from, approved });
764
-
765
- const phase = this.session.getCohortPhase(ctx.cohortId);
766
-
767
- // A participant rejection flips the cohort to Failed. Emit a structured
768
- // event so the runner/caller sees the failure instead of the cohort
769
- // silently stalling.
770
- if(phase === ServiceCohortPhase.Failed) {
771
- const reason = `Validation rejected by participant ${msg.from}`;
772
- this.emit('cohort-failed', { cohortId: ctx.cohortId, reason });
773
- this.#failCohort(ctx, new Error(reason));
774
- return;
775
- }
776
-
777
- // When all validations received, request tx data and start signing
778
- if(phase === ServiceCohortPhase.Validated) {
779
- const cohort = this.session.getCohort(ctx.cohortId)!;
780
- const txData = await this.#onProvideTxData({
781
- cohortId : ctx.cohortId,
782
- beaconAddress : cohort.beaconAddress,
783
- signalBytes : cohort.signalBytes!,
784
- feeEstimator : this.#feeEstimator,
785
- });
786
- const authMsgs = this.session.startSigning(ctx.cohortId, txData);
787
- const sessionId = this.session.getSigningSessionId(ctx.cohortId) ?? '';
788
- this.emit('signing-started', { cohortId: ctx.cohortId, sessionId });
789
- await this.#sendAll(authMsgs);
790
- }
791
- } catch(err) {
792
- this.#failCohort(ctx, err as Error);
793
- }
794
- }
795
-
796
- /**
797
- * Handler for receiving nonce contributions. When all nonces are received, sends the aggregated
798
- * nonce back to the cohort.
799
- * @param {BaseMessage} msg - The incoming message to handle.
800
- * @returns {Promise<void>} Resolves when handling is complete.
801
- */
802
- async #handleNonceContribution(msg: BaseMessage): Promise<void> {
803
- if(this.#stopped) return;
804
- const ctx = this.#contextFor(msg);
805
- if(!ctx) return;
806
- try {
807
- this.session.receive(msg);
808
- this.#drainRejections(ctx);
809
- this.#onPhaseMaybeChanged(ctx);
810
- this.emit('nonce-received', { cohortId: ctx.cohortId, participantDid: msg.from });
811
-
812
- // When all nonces collected, send aggregated nonce
813
- if(this.session.getCohortPhase(ctx.cohortId) === ServiceCohortPhase.NoncesCollected) {
814
- await this.#sendAll(this.session.sendAggregatedNonce(ctx.cohortId));
815
- }
816
- } catch(err) {
817
- this.#failCohort(ctx, err as Error);
818
- }
819
- }
820
-
821
- /**
822
- * Handler for receiving signature authorizations. When all partial signatures are received, the
823
- * session automatically completes; the final result is emitted and the cohort's completion
824
- * promise resolves.
825
- * @param {BaseMessage} msg - The incoming message to handle.
826
- * @returns {Promise<void>} Resolves when handling is complete.
827
- */
828
- async #handleSignatureAuthorization(msg: BaseMessage): Promise<void> {
829
- if(this.#stopped) return;
830
- const ctx = this.#contextFor(msg);
831
- if(!ctx) return;
832
- try {
833
- this.session.receive(msg);
834
- this.#drainRejections(ctx);
835
- this.#onPhaseMaybeChanged(ctx);
836
-
837
- // If the cohort already committed to the fallback path, ignore a late
838
- // optimistic completion: only one path may finalize the single beacon UTXO.
839
- if(ctx.committedPath === 'fallback') return;
840
-
841
- // The state machine auto-completes when all partial sigs received.
842
- const result = this.session.getResult(ctx.cohortId);
843
- if(result) {
844
- ctx.committedPath = 'optimistic';
845
- this.#completeCohort(ctx, result);
846
- }
847
- } catch(err) {
848
- this.#failCohort(ctx, err as Error);
849
- }
850
- }
851
-
852
- /**
853
- * Handler for receiving fallback (k-of-n script-path) signatures. The state
854
- * machine assembles and finalizes the fallback spend once k valid signatures
855
- * are in; the result is then emitted and the cohort's completion resolves. The
856
- * cohort is already committed to the fallback path (via {@link triggerFallback}).
857
- * @param {BaseMessage} msg - The incoming FALLBACK_SIGNATURE message.
858
- * @returns {Promise<void>} Resolves when handling is complete.
859
- */
860
- async #handleFallbackSignature(msg: BaseMessage): Promise<void> {
861
- if(this.#stopped) return;
862
- const ctx = this.#contextFor(msg);
863
- if(!ctx) return;
864
- try {
865
- this.session.receive(msg);
866
- this.#drainRejections(ctx);
867
- this.#onPhaseMaybeChanged(ctx);
868
-
869
- const result = this.session.getResult(ctx.cohortId);
870
- if(result) {
871
- this.#completeCohort(ctx, result);
872
- }
873
- } catch(err) {
874
- this.#failCohort(ctx, err as Error);
875
- }
876
- }
877
-
878
- /**
879
- * Internal: helper to send all messages sequentially. Catches and propagates errors.
880
- * @param {BaseMessage[]} msgs - The messages to send.
881
- * @returns {Promise<void>} Resolves when all messages have been sent.
882
- */
883
- async #sendAll(msgs: BaseMessage[]): Promise<void> {
884
- for(const m of msgs) {
885
- await this.#transport.sendMessage(m, this.#did, m.to);
886
- }
887
- }
888
- }