@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/LICENSE +202 -21
- package/NOTICE +60 -0
- package/README.md +44 -0
- package/dist/api/jsonrpc.d.ts +12 -0
- package/dist/api/jsonrpc.js +100 -1
- package/dist/api/methods.d.ts +135 -3
- package/dist/api/methods.js +1986 -45
- package/dist/chain/totem-node-adapter.js +1 -1
- package/dist/cli.js +67 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.js +33 -1
- package/dist/identity.js +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +20 -1
- package/dist/lifecycle.d.ts +22 -0
- package/dist/lifecycle.js +422 -5
- package/dist/observe/bus.d.ts +13 -0
- package/dist/observe/bus.js +29 -0
- package/dist/observe/emitter.d.ts +61 -0
- package/dist/observe/emitter.js +133 -0
- package/dist/observe/index.d.ts +5 -0
- package/dist/observe/index.js +21 -0
- package/dist/observe/snapshot.d.ts +38 -0
- package/dist/observe/snapshot.js +101 -0
- package/dist/observe/store.d.ts +31 -0
- package/dist/observe/store.js +82 -0
- package/dist/observe/types.d.ts +106 -0
- package/dist/observe/types.js +14 -0
- package/dist/pairing.d.ts +109 -0
- package/dist/pairing.js +231 -0
- package/dist/reply-cache.d.ts +21 -0
- package/dist/reply-cache.js +62 -0
- package/dist/signer-port.d.ts +27 -0
- package/dist/signer-port.js +79 -0
- package/dist/signing.d.ts +11 -0
- package/dist/signing.js +27 -0
- package/dist/stores/operations.d.ts +3 -2
- package/dist/stores/operations.js +96 -83
- package/dist/stores/sqlite-store.d.ts +8 -5
- package/dist/stores/sqlite-store.js +45 -42
- package/package.json +16 -14
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
+
}
|