@totemsdk/omnia-host 0.9.0 → 0.11.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.
package/dist/lifecycle.js CHANGED
@@ -1,13 +1,25 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.createOmniaHost = createOmniaHost;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const bus_js_1 = require("./observe/bus.js");
9
+ const emitter_js_1 = require("./observe/emitter.js");
10
+ const store_js_1 = require("./observe/store.js");
11
+ const snapshot_js_1 = require("./observe/snapshot.js");
4
12
  const omnia_1 = require("@totemsdk/omnia");
5
13
  const sqlite_store_js_1 = require("./stores/sqlite-store.js");
6
14
  const operations_js_1 = require("./stores/operations.js");
7
15
  const jsonrpc_js_1 = require("./api/jsonrpc.js");
8
16
  const totem_node_adapter_js_1 = require("./chain/totem-node-adapter.js");
9
17
  const routing_provider_js_1 = require("./router/routing-provider.js");
18
+ const p2p_hub_1 = require("@totemsdk/p2p-hub");
10
19
  const methods_js_1 = require("./api/methods.js");
20
+ const reply_cache_js_1 = require("./reply-cache.js");
21
+ const omnia_factory_1 = require("@totemsdk/omnia-factory");
22
+ const omnia_splice_1 = require("@totemsdk/omnia-splice");
11
23
  /**
12
24
  * Phase-1 lifecycle shell. Subsystems will be attached in later phases while
13
25
  * preserving idempotent startup and shutdown for the CLI and embedding users.
@@ -23,9 +35,15 @@ function createOmniaHost(config, dependencies = {}) {
23
35
  : undefined;
24
36
  const injectedOperationStore = dependencies.operationStore;
25
37
  let ownedOperationStore;
38
+ let observationStore = dependencies.observationStore;
39
+ let ownedObservationStore;
40
+ const observationBus = dependencies.observationBus ?? new bus_js_1.ObservationBus();
41
+ let observation;
26
42
  let controlServer = dependencies.controlServer;
27
43
  let chainAdapter = dependencies.chainAdapter;
28
44
  let routingProvider = dependencies.routingProvider ?? new routing_provider_js_1.InProcessRoutingProvider();
45
+ let p2pHubServer;
46
+ let p2pHubUrl;
29
47
  return {
30
48
  config,
31
49
  get swarm() {
@@ -34,6 +52,9 @@ function createOmniaHost(config, dependencies = {}) {
34
52
  get channels() {
35
53
  return channelStore;
36
54
  },
55
+ get p2pHubUrl() {
56
+ return p2pHubUrl;
57
+ },
37
58
  async start() {
38
59
  if (started)
39
60
  return;
@@ -50,13 +71,46 @@ function createOmniaHost(config, dependencies = {}) {
50
71
  ownedOperationStore = new operations_js_1.OperationStore(config.dbPath);
51
72
  operationStore = ownedOperationStore;
52
73
  }
74
+ const observeEnabled = dependencies.observe === true || (dependencies.observe !== false && config.observeDisabled !== true && config.observeDbPath !== undefined);
75
+ if (observeEnabled && !observationStore) {
76
+ const observePath = config.observeDbPath ?? node_path_1.default.join(node_path_1.default.dirname(config.dbPath), 'observe.sqlite');
77
+ try {
78
+ ownedObservationStore = await (0, store_js_1.createSqliteObservationStore)(observePath);
79
+ observationStore = ownedObservationStore;
80
+ }
81
+ catch (err) {
82
+ console.error('[omnia-host] observation store disabled:', err instanceof Error ? err.message : String(err));
83
+ }
84
+ }
85
+ if (observationStore) {
86
+ observation = new emitter_js_1.ObservationEmitter({
87
+ store: observationStore,
88
+ bus: observationBus,
89
+ networkId: config.networkId ?? dependencies.networkId ?? 'omnia',
90
+ sourceNodeId: config.sourceNodeId ?? dependencies.sourceNodeId ?? config.localPartyId ?? 'omnia-host',
91
+ });
92
+ }
53
93
  if (!swarm) {
54
94
  const relay = config.relay
55
95
  ? { mode: 'self-hosted', relayUrl: config.relay }
56
96
  : { mode: 'native' };
97
+ const dhtBootstrap = (process.env.OMNIA_DHT_BOOTSTRAP ?? '')
98
+ .split(',')
99
+ .map((s) => s.trim())
100
+ .filter(Boolean);
101
+ // Serverless native mode: bind/announce an explicit local interface so
102
+ // peers can reach us directly (multi-homed hosts, netns, VPN, etc.).
103
+ const dhtHost = process.env.OMNIA_DHT_HOST?.trim() || undefined;
104
+ const dhtPort = process.env.OMNIA_DHT_PORT
105
+ ? Number(process.env.OMNIA_DHT_PORT)
106
+ : undefined;
57
107
  swarm = await (dependencies.createSwarm ?? omnia_1.createOmniaSwarm)({
58
108
  localPubkey: config.localPubkey,
59
109
  relay,
110
+ ...(dhtBootstrap.length ? { bootstrap: dhtBootstrap } : {}),
111
+ ...(dependencies.transportKeyPair ? { keyPair: dependencies.transportKeyPair } : {}),
112
+ ...(dhtHost ? { host: dhtHost } : {}),
113
+ ...(dhtPort ? { port: dhtPort } : {}),
60
114
  });
61
115
  }
62
116
  if (!chainAdapter) {
@@ -68,6 +122,21 @@ function createOmniaHost(config, dependencies = {}) {
68
122
  ssl: rpc.protocol === 'https:',
69
123
  });
70
124
  }
125
+ if (config.p2pHubPort !== undefined || dependencies.p2pHubNetwork) {
126
+ const native = swarm.nativeSwarm?.();
127
+ const network = dependencies.p2pHubNetwork ?? await (0, p2p_hub_1.createHyperswarmHubNetwork)(native ? { swarm: native } : {});
128
+ p2pHubServer = await (0, p2p_hub_1.createWebSocketHubServer)({
129
+ network,
130
+ host: config.p2pHubHost ?? '127.0.0.1',
131
+ port: config.p2pHubPort ?? 0,
132
+ ...(config.p2pHubToken ? { token: config.p2pHubToken } : {}),
133
+ ...(config.p2pHubOrigins ? { allowedOrigins: config.p2pHubOrigins } : {}),
134
+ ...(config.p2pHubTopics ? { allowedTopicPrefixes: config.p2pHubTopics } : {}),
135
+ });
136
+ const address = await p2pHubServer.listen();
137
+ const host = address.host.includes(':') ? `[${address.host}]` : address.host;
138
+ p2pHubUrl = `ws://${host}:${address.port}`;
139
+ }
71
140
  const localParticipant = dependencies.localParticipant ?? (config.localPartyId && config.localPubkey && config.localAddressIndex !== undefined
72
141
  ? {
73
142
  partyId: config.localPartyId,
@@ -76,11 +145,316 @@ function createOmniaHost(config, dependencies = {}) {
76
145
  settlementAddress: config.localSettlementAddress,
77
146
  }
78
147
  : undefined);
148
+ // Factory managers default to this node's own lease bundle so the local
149
+ // side can propose/sign without extra wiring. A two-host factory still
150
+ // needs peer coordination (FACTORY_PROPOSAL/ACCEPT).
151
+ const factoryBundles = dependencies.factoryBundles ?? (localParticipant && dependencies.signer && dependencies.leaseProvider
152
+ ? {
153
+ [localParticipant.partyId]: {
154
+ signer: dependencies.signer,
155
+ leaseProvider: dependencies.leaseProvider,
156
+ },
157
+ }
158
+ : undefined);
159
+ const factories = dependencies.factories ?? (factoryBundles ? new Map() : undefined);
160
+ const localBundle = localParticipant
161
+ ? factoryBundles?.[localParticipant.partyId]
162
+ : undefined;
163
+ const spliceLeaseProvider = dependencies.spliceLeaseProvider ?? (dependencies.signer && dependencies.leaseProvider
164
+ ? {
165
+ signer: dependencies.signer,
166
+ wotsLease: dependencies.leaseProvider,
167
+ ...(chainAdapter
168
+ ? { broadcast: (txHex) => chainAdapter.broadcastTxPoW(txHex) }
169
+ : {}),
170
+ }
171
+ : undefined);
172
+ const spliceProposals = dependencies.spliceProposals ?? new Map();
173
+ const spliceAcceptances = dependencies.spliceAcceptances ?? new Map();
174
+ const onSpliceMessage = async (peer, msg) => {
175
+ if (!spliceLeaseProvider || !channelStore)
176
+ return;
177
+ if (msg?.type !== 'SPLICE_PROPOSAL' && msg?.type !== 'SPLICE_FINALIZED')
178
+ return;
179
+ const payload = (msg.payload ?? {});
180
+ try {
181
+ if (msg.type === 'SPLICE_PROPOSAL' && payload.proposal) {
182
+ const proposal = payload.proposal;
183
+ let channel = channelStore.get(proposal.channelId);
184
+ if (!channel)
185
+ throw new Error(`Splice channel ${proposal.channelId} not found`);
186
+ const prior = spliceAcceptances.get(proposal.spliceId);
187
+ if (prior) {
188
+ await peer.sendMessage({
189
+ type: 'SPLICE_ACCEPT',
190
+ channelId: msg.channelId,
191
+ nonce: msg.nonce,
192
+ payload: { acceptance: prior },
193
+ });
194
+ return;
195
+ }
196
+ if (channel.status === 'active') {
197
+ channel = await (0, omnia_splice_1.quiesceChannel)(channel, spliceLeaseProvider);
198
+ channelStore.set(channel.channelId, channel);
199
+ }
200
+ const acceptance = await (0, omnia_splice_1.acceptSplice)(channel, proposal, spliceLeaseProvider);
201
+ spliceAcceptances.set(proposal.spliceId, acceptance);
202
+ await peer.sendMessage({
203
+ type: 'SPLICE_ACCEPT',
204
+ channelId: msg.channelId,
205
+ nonce: msg.nonce,
206
+ payload: { acceptance },
207
+ });
208
+ return;
209
+ }
210
+ if (msg.type === 'SPLICE_FINALIZED' && payload.channel) {
211
+ const channel = payload.channel;
212
+ channelStore.set(channel.channelId, channel);
213
+ void observation?.emit({
214
+ domain: 'splice', kind: 'splice.finalized', phase: 'confirmed',
215
+ entity: { type: 'channel', id: channel.channelId },
216
+ amounts: { capacity: channel.totalValue?.toString(), balances: Object.fromEntries(Object.entries(channel.balances ?? {}).map(([k, v]) => [k, v.toString()])) },
217
+ data: { spliceId: payload.spliceId, txId: payload.txId },
218
+ });
219
+ const acceptance = spliceAcceptances.get(payload.spliceId);
220
+ if (acceptance && typeof payload.txId === 'string') {
221
+ await spliceLeaseProvider.wotsLease
222
+ .commitKeyUse(acceptance.acceptorReservationId, payload.txId)
223
+ .catch(() => { });
224
+ }
225
+ }
226
+ }
227
+ catch (err) {
228
+ await peer.sendMessage({
229
+ type: 'ERROR',
230
+ channelId: msg.channelId,
231
+ nonce: msg.nonce,
232
+ payload: { error: err instanceof Error ? err.message : String(err) },
233
+ }).catch(() => { });
234
+ }
235
+ };
236
+ const factoryReplies = new reply_cache_js_1.ReplyCache();
237
+ const vcReplies = new reply_cache_js_1.ReplyCache();
238
+ const onFactoryMessage = async (peer, msg) => {
239
+ if (!factories || !localBundle)
240
+ return;
241
+ if (msg?.type !== 'FACTORY_PROPOSAL' &&
242
+ msg?.type !== 'FACTORY_FUNDING_REQUEST' &&
243
+ msg?.type !== 'FACTORY_CLOSE_PROPOSAL' &&
244
+ msg?.type !== 'FACTORY_CLOSE_FINALIZED')
245
+ return;
246
+ const payload = (msg?.payload ?? {});
247
+ if (process.env.OMNIA_DEBUG) {
248
+ console.error('[factory] recv', msg.type, 'hasFactory', !!payload.factory, 'bundles', !!localBundle);
249
+ }
250
+ const key = factoryReplies.key(msg.type, msg.channelId, msg.nonce);
251
+ const reply = await factoryReplies.run(key, async () => {
252
+ try {
253
+ if (msg.type === 'FACTORY_FUNDING_REQUEST' && typeof payload.digest === 'string') {
254
+ const digest = Uint8Array.from(Buffer.from(payload.digest.replace(/^0x/i, ''), 'hex'));
255
+ const reservation = await localBundle.leaseProvider.reserveKeyUse({
256
+ treeId: 'factory-funding',
257
+ purpose: 'factory-funding',
258
+ payloadHash: payload.digest,
259
+ });
260
+ try {
261
+ const signature = await localBundle.signer.sign(digest, reservation.indices);
262
+ await localBundle.leaseProvider.commitKeyUse(reservation.reservationId, 'factory-funding');
263
+ return {
264
+ type: 'FACTORY_FUNDING_RESPONSE',
265
+ channelId: msg.channelId,
266
+ nonce: msg.nonce,
267
+ payload: { signature: '0x' + Buffer.from(signature).toString('hex') },
268
+ };
269
+ }
270
+ catch (err) {
271
+ await localBundle.leaseProvider.burnReservation(reservation.reservationId, 'factory-funding-failed').catch(() => { });
272
+ throw err;
273
+ }
274
+ }
275
+ if (msg.type === 'FACTORY_PROPOSAL' && payload.factory) {
276
+ const accepted = await (0, omnia_factory_1.acceptFactory)(payload.factory, localBundle);
277
+ factories.set(accepted.factoryId, accepted);
278
+ void observation?.emit({
279
+ domain: 'factory', kind: 'factory.accepted', phase: 'accepted',
280
+ entity: { type: 'factory', id: accepted.factoryId },
281
+ data: { participants: accepted.participants },
282
+ });
283
+ return {
284
+ type: 'FACTORY_ACCEPT',
285
+ channelId: accepted.factoryId,
286
+ nonce: msg.nonce,
287
+ payload: { factory: accepted },
288
+ };
289
+ }
290
+ if (msg.type === 'FACTORY_CLOSE_PROPOSAL' && payload.factoryId) {
291
+ const factory = factories.get(payload.factoryId);
292
+ if (!factory)
293
+ throw new Error(`Factory ${payload.factoryId} not found`);
294
+ const signature = await (0, omnia_factory_1.signFactorySettlement)(factory, payload.draft, localBundle);
295
+ await localBundle.leaseProvider
296
+ .commitKeyUse(signature.reservationId, `factory-settlement-${payload.factoryId}`)
297
+ .catch(() => { });
298
+ return {
299
+ type: 'FACTORY_CLOSE_ACCEPT',
300
+ channelId: payload.factoryId,
301
+ nonce: msg.nonce,
302
+ payload: { signature },
303
+ };
304
+ }
305
+ if (msg.type === 'FACTORY_CLOSE_FINALIZED' && payload.factoryId) {
306
+ const factory = factories.get(payload.factoryId);
307
+ if (factory)
308
+ factories.set(payload.factoryId, { ...factory, status: 'closed' });
309
+ return null;
310
+ }
311
+ return null;
312
+ }
313
+ catch (e) {
314
+ if (process.env.OMNIA_DEBUG)
315
+ console.error('[factory] err', String(e));
316
+ return {
317
+ type: 'ERROR',
318
+ channelId: msg.channelId,
319
+ nonce: msg.nonce,
320
+ payload: { error: String(e) },
321
+ };
322
+ }
323
+ });
324
+ if (reply)
325
+ await peer.sendMessage(reply);
326
+ };
327
+ const channelPeers = new Map();
328
+ const onPeerMessage = (peer, msg) => {
329
+ if (msg?.channelId)
330
+ channelPeers.set(msg.channelId, peer);
331
+ void observation?.emit({
332
+ domain: 'transport', kind: 'transport.message', phase: 'observed',
333
+ entity: { type: 'node', id: config.sourceNodeId ?? config.localPartyId ?? 'omnia-host' },
334
+ evidence: { peerPublicKeyDigest: peer?.publicKeyDigest },
335
+ data: { type: msg?.type, channelId: msg?.channelId, nonce: msg?.nonce },
336
+ });
337
+ };
338
+ const onChannelAccepted = (channel, peer) => {
339
+ void observation?.emit({
340
+ domain: 'channel', kind: 'channel.proposal.accepted', phase: 'accepted',
341
+ entity: { type: 'channel', id: channel.channelId },
342
+ parties: { local: localParticipant?.partyId, remote: channel.parties?.find((p) => p.partyId !== localParticipant?.partyId)?.partyId },
343
+ token: { tokenId: channel.tokenId, scale: channel.tokenScale ?? 0 },
344
+ amounts: { capacity: channel.totalValue?.toString(), balances: Object.fromEntries(Object.entries(channel.balances ?? {}).map(([k, v]) => [k, v.toString()])) },
345
+ transitionHash: (0, emitter_js_1.channelTransitionHash)(channel),
346
+ evidence: { peerPublicKeyDigest: peer?.publicKeyDigest },
347
+ });
348
+ };
349
+ const onStateUpdated = (channel, peer) => {
350
+ void observation?.emit({
351
+ domain: 'channel', kind: 'channel.updated', phase: 'committed',
352
+ entity: { type: 'channel', id: channel.channelId },
353
+ parties: { local: localParticipant?.partyId, remote: channel.parties?.find((p) => p.partyId !== localParticipant?.partyId)?.partyId },
354
+ token: { tokenId: channel.tokenId, scale: channel.tokenScale ?? 0 },
355
+ after: { sequence: channel.currentSequence, balances: Object.fromEntries(Object.entries(channel.balances ?? {}).map(([k, v]) => [k, v.toString()])) },
356
+ transitionHash: (0, emitter_js_1.channelTransitionHash)(channel),
357
+ evidence: { peerPublicKeyDigest: peer?.publicKeyDigest },
358
+ });
359
+ };
360
+ const onSettlementProposed = (payload, peer) => {
361
+ void observation?.emit({
362
+ domain: 'settlement', kind: 'settlement.proposed', phase: 'proposed',
363
+ entity: { type: 'channel', id: payload?.channelId ?? 'unknown' },
364
+ evidence: { peerPublicKeyDigest: peer?.publicKeyDigest },
365
+ data: { payload },
366
+ });
367
+ };
368
+ const onVcMessage = async (peer, msg) => {
369
+ if (!factories || !localBundle)
370
+ return;
371
+ const payload = (msg?.payload ?? {});
372
+ if (msg?.type === 'VC_FINALIZED') {
373
+ if (payload.factory)
374
+ factories.set(payload.factory.factoryId, payload.factory);
375
+ if (payload.kind === 'vc-open' && payload.channel) {
376
+ channelStore?.set(payload.channel.channelId, payload.channel);
377
+ void observation?.emit({
378
+ domain: 'factory', kind: 'factory.vc_opened', phase: 'accepted',
379
+ entity: { type: 'channel', id: payload.channel.channelId },
380
+ data: { factoryId: payload.factory?.factoryId },
381
+ });
382
+ }
383
+ else if (payload.kind === 'vc-close' && payload.channelId) {
384
+ channelStore?.delete(payload.channelId);
385
+ void observation?.emit({
386
+ domain: 'factory', kind: 'factory.vc_closed', phase: 'closed',
387
+ entity: { type: 'channel', id: payload.channelId },
388
+ data: { factoryId: payload.factory?.factoryId },
389
+ });
390
+ }
391
+ return;
392
+ }
393
+ if (msg?.type !== 'VC_PROPOSAL')
394
+ return;
395
+ const key = vcReplies.key(msg.type, msg.channelId, msg.nonce, payload.kind);
396
+ const reply = await vcReplies.run(key, async () => {
397
+ try {
398
+ const factory = factories.get(payload.factoryId);
399
+ if (!factory)
400
+ throw new Error(`Factory ${payload.factoryId} not found`);
401
+ if (payload.kind === 'vc-open') {
402
+ const signature = await (0, omnia_factory_1.signVirtualChannelOpen)(factory, payload.plan, localBundle);
403
+ await localBundle.leaseProvider.commitKeyUse(signature.reservationId, `factory-vc-open-${payload.factoryId}`);
404
+ return {
405
+ type: 'VC_ACCEPT',
406
+ channelId: payload.vcId,
407
+ nonce: msg.nonce,
408
+ payload: { kind: 'vc-open', signature },
409
+ };
410
+ }
411
+ if (payload.kind === 'vc-close') {
412
+ const signature = await (0, omnia_factory_1.signVirtualChannelClose)(factory, payload.plan, localBundle);
413
+ await localBundle.leaseProvider.commitKeyUse(signature.reservationId, `factory-vc-close-${payload.factoryId}`);
414
+ return {
415
+ type: 'VC_ACCEPT',
416
+ channelId: payload.vcId,
417
+ nonce: msg.nonce,
418
+ payload: { kind: 'vc-close', signature },
419
+ };
420
+ }
421
+ return null;
422
+ }
423
+ catch (e) {
424
+ return {
425
+ type: 'ERROR',
426
+ channelId: msg.channelId,
427
+ nonce: msg.nonce,
428
+ payload: { error: String(e) },
429
+ };
430
+ }
431
+ });
432
+ if (reply)
433
+ await peer.sendMessage(reply);
434
+ };
79
435
  unsubscribe = (0, omnia_1.createOmniaIntegration)(swarm, channelStore, {
80
436
  signer: dependencies.signer,
81
437
  leaseProvider: dependencies.leaseProvider,
82
438
  chainProvider: dependencies.chainProvider ?? chainAdapter,
439
+ onFactoryMessage,
440
+ onVcMessage,
441
+ onSpliceMessage,
442
+ onPeerMessage,
443
+ onChannelAccepted,
444
+ onStateUpdated,
445
+ onSettlementProposed,
83
446
  });
447
+ const preparedPay = new Map();
448
+ const preparedSettle = new Map();
449
+ const preparedOpen = new Map();
450
+ const preparedSplice = new Map();
451
+ const preparedFactory = new Map();
452
+ const vtxoPools = new Map();
453
+ const vtxos = new Map();
454
+ const vtxoReceipts = new Map();
455
+ const preparedVtxoReceipts = new Map();
456
+ const pools = new Map();
457
+ const poolPositions = new Map();
84
458
  if (!controlServer) {
85
459
  const readOnly = !dependencies.signer || !dependencies.leaseProvider;
86
460
  if (readOnly) {
@@ -93,20 +467,53 @@ function createOmniaHost(config, dependencies = {}) {
93
467
  isReady: () => started,
94
468
  allowedOrigins: config.allowedOrigins,
95
469
  authToken: config.controlToken,
470
+ observe: observationStore ? {
471
+ store: observationStore,
472
+ bus: observationBus,
473
+ snapshot: async () => (0, snapshot_js_1.createObservationSnapshot)({
474
+ networkId: config.networkId ?? dependencies.networkId ?? 'omnia',
475
+ sourceNodeId: config.sourceNodeId ?? dependencies.sourceNodeId ?? config.localPartyId ?? 'omnia-host',
476
+ streamId: `${config.sourceNodeId ?? dependencies.sourceNodeId ?? config.localPartyId ?? 'omnia-host'}:${config.networkId ?? dependencies.networkId ?? 'omnia'}`,
477
+ cursor: await observationStore.head(),
478
+ channels: channelStore?.values(),
479
+ factories: factories?.values(),
480
+ vtxoPools: vtxoPools.values(),
481
+ vtxos: vtxos.values(),
482
+ pools: pools.values(),
483
+ operations: (dependencies.operationStoreForApi ?? operationStore),
484
+ }),
485
+ } : undefined,
96
486
  methods: dependencies.methods ?? (0, methods_js_1.createHostMethods)({
97
487
  channels: channelStore,
98
488
  routing: routingProvider,
99
489
  operations: dependencies.operationStoreForApi ?? operationStore,
490
+ observe: observation,
100
491
  signer: dependencies.signer,
101
492
  leaseProvider: dependencies.leaseProvider,
102
493
  chainProvider: dependencies.chainProvider ?? chainAdapter,
103
494
  swarm,
495
+ channelPeers,
104
496
  localParticipant,
105
- factories: dependencies.factories,
106
- factoryBundles: dependencies.factoryBundles,
107
- spliceProposals: dependencies.spliceProposals,
108
- spliceAcceptances: dependencies.spliceAcceptances,
109
- spliceLeaseProvider: dependencies.spliceLeaseProvider,
497
+ transportPubkey: dependencies.transportPubkey,
498
+ factories,
499
+ factoryBundles,
500
+ spliceProposals,
501
+ spliceAcceptances,
502
+ spliceLeaseProvider,
503
+ custodyMode: config.custodyMode ?? 'self',
504
+ signerPort: dependencies.signerPort,
505
+ preparedPay,
506
+ preparedSettle,
507
+ preparedOpen,
508
+ preparedSplice,
509
+ preparedFactory,
510
+ vtxoPools,
511
+ vtxos,
512
+ vtxoReceipts,
513
+ preparedVtxoReceipts,
514
+ pools,
515
+ poolPositions,
516
+ coordinator: !dependencies.signer && !config.readOnly,
110
517
  fundingSigner: config.fundingPubkey
111
518
  ? async (digest) => {
112
519
  const sig = await chainAdapter.signData(config.fundingPubkey, '0x' + Buffer.from(digest).toString('hex'));
@@ -116,6 +523,7 @@ function createOmniaHost(config, dependencies = {}) {
116
523
  readOnly,
117
524
  identity: dependencies.identity,
118
525
  manifest: dependencies.manifest,
526
+ p2pHubUrl,
119
527
  refreshRouting: () => routingProvider.rebuild((0, routing_provider_js_1.channelGraphEdges)(channelStore.values())),
120
528
  }),
121
529
  });
@@ -127,6 +535,9 @@ function createOmniaHost(config, dependencies = {}) {
127
535
  catch (error) {
128
536
  unsubscribe?.();
129
537
  unsubscribe = undefined;
538
+ await p2pHubServer?.close();
539
+ p2pHubServer = undefined;
540
+ p2pHubUrl = undefined;
130
541
  chainAdapter?.close();
131
542
  await swarm?.close();
132
543
  if (ownedOperationStore)
@@ -142,6 +553,9 @@ function createOmniaHost(config, dependencies = {}) {
142
553
  return;
143
554
  unsubscribe?.();
144
555
  unsubscribe = undefined;
556
+ await p2pHubServer?.close();
557
+ p2pHubServer = undefined;
558
+ p2pHubUrl = undefined;
145
559
  await controlServer?.close();
146
560
  chainAdapter?.close();
147
561
  await swarm?.close();
@@ -150,6 +564,9 @@ function createOmniaHost(config, dependencies = {}) {
150
564
  else if (injectedOperationStore)
151
565
  await injectedOperationStore.close();
152
566
  ownedOperationStore = undefined;
567
+ if (ownedObservationStore)
568
+ await ownedObservationStore.close();
569
+ ownedObservationStore = undefined;
153
570
  ownedStore?.close();
154
571
  ownedStore = undefined;
155
572
  started = false;
@@ -0,0 +1,13 @@
1
+ import type { OmniaObservation } from "./types.js";
2
+ export type ObservationListener = (observation: OmniaObservation) => void;
3
+ /**
4
+ * In-process live fan-out for SSE consumers. The durable journal remains the
5
+ * source of truth: a subscriber that misses a live event still replays it from
6
+ * `ObservationStore.readSince(cursor)`.
7
+ */
8
+ export declare class ObservationBus {
9
+ private readonly listeners;
10
+ subscribe(listener: ObservationListener): () => void;
11
+ publish(observation: OmniaObservation): void;
12
+ get size(): number;
13
+ }
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ObservationBus = void 0;
4
+ /**
5
+ * In-process live fan-out for SSE consumers. The durable journal remains the
6
+ * source of truth: a subscriber that misses a live event still replays it from
7
+ * `ObservationStore.readSince(cursor)`.
8
+ */
9
+ class ObservationBus {
10
+ listeners = new Set();
11
+ subscribe(listener) {
12
+ this.listeners.add(listener);
13
+ return () => this.listeners.delete(listener);
14
+ }
15
+ publish(observation) {
16
+ for (const listener of this.listeners) {
17
+ try {
18
+ listener(observation);
19
+ }
20
+ catch {
21
+ // A broken consumer must never break the mutation path.
22
+ }
23
+ }
24
+ }
25
+ get size() {
26
+ return this.listeners.size;
27
+ }
28
+ }
29
+ exports.ObservationBus = ObservationBus;
@@ -0,0 +1,61 @@
1
+ import type { OmniaChannel } from "@totemsdk/omnia";
2
+ import type { ObservationBus } from "./bus.js";
3
+ import type { ObservationDomain, ObservationEntity, ObservationPhase, ObservationRecord, ObservationStore, OmniaObservation } from "./types.js";
4
+ export interface ObservationEmitterConfig {
5
+ store: ObservationStore;
6
+ bus: ObservationBus;
7
+ networkId: string;
8
+ sourceNodeId: string;
9
+ streamId?: string;
10
+ /** Mark events from fixture/simulated hosts. */
11
+ simulated?: boolean;
12
+ /** Called on emission failures (defaults to console.error). */
13
+ onError?: (error: unknown) => void;
14
+ }
15
+ export interface EmitObservationInput {
16
+ domain: ObservationDomain;
17
+ kind: string;
18
+ phase: ObservationPhase;
19
+ entity: ObservationEntity;
20
+ occurredAt?: number;
21
+ operationId?: string;
22
+ causationId?: string;
23
+ transitionHash?: string;
24
+ parties?: {
25
+ local?: string;
26
+ remote?: string;
27
+ };
28
+ token?: {
29
+ tokenId: string;
30
+ scale: number;
31
+ };
32
+ before?: Record<string, unknown>;
33
+ after?: Record<string, unknown>;
34
+ amounts?: ObservationRecord["amounts"];
35
+ chain?: ObservationRecord["chain"];
36
+ evidence?: ObservationRecord["evidence"];
37
+ simulated?: boolean;
38
+ data?: Record<string, unknown>;
39
+ }
40
+ /** Stable hash for transition identity (never includes secrets). */
41
+ export declare function hashTransition(parts: Array<string | number | undefined | null>): string;
42
+ /** Hash a channel's signed commitment for cross-node correlation. */
43
+ export declare function channelTransitionHash(channel: OmniaChannel): string;
44
+ export declare function hashString(value: string): string;
45
+ /**
46
+ * Builds, redacts and persists observation events, then fans them out live.
47
+ *
48
+ * `emit()` never throws into a mutation: persistence/export failures are
49
+ * reported out-of-band so a payment can never be blocked by the observer.
50
+ */
51
+ export declare class ObservationEmitter {
52
+ private readonly store;
53
+ private readonly bus;
54
+ private readonly networkId;
55
+ private readonly sourceNodeId;
56
+ private readonly streamId;
57
+ private readonly simulated;
58
+ private readonly onError;
59
+ constructor(config: ObservationEmitterConfig);
60
+ emit(input: EmitObservationInput): Promise<OmniaObservation | undefined>;
61
+ }