@totemsdk/omnia-host 0.11.0 → 0.13.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.
@@ -1697,21 +1697,7 @@ function createHostMethods(context) {
1697
1697
  });
1698
1698
  if (!route)
1699
1699
  return { success: false, error: 'No route found', errorCode: 'ROUTE_NOT_FOUND' };
1700
- return {
1701
- success: true,
1702
- route: {
1703
- ...route,
1704
- totalFees: route.totalFees.toString(),
1705
- hops: route.hops.map((hop) => ({
1706
- ...hop,
1707
- amount: hop.amount.toString(),
1708
- ...('isSwap' in hop && hop.isSwap ? {
1709
- amountIn: hop.amountIn.toString(),
1710
- amountOut: hop.amountOut.toString(),
1711
- } : {}),
1712
- })),
1713
- },
1714
- };
1700
+ return { success: true, route: serializeWireRoute(route) };
1715
1701
  };
1716
1702
  register('totem_omniaGetRoute', getRoute, 'omnia/getRoute');
1717
1703
  const getSwapRate = () => ({ success: true, announcements: [] });
@@ -2529,28 +2515,115 @@ function createHostMethods(context) {
2529
2515
  };
2530
2516
  register('totem_omniaSpliceIn', splice('in'), 'omnia/spliceIn');
2531
2517
  register('totem_omniaSpliceOut', splice('out'), 'omnia/spliceOut');
2518
+ function parseWireRoute(value) {
2519
+ if (!value || typeof value !== 'object')
2520
+ throw new Error('route must be an object');
2521
+ const raw = value;
2522
+ if (!Array.isArray(raw.hops) || raw.hops.length === 0)
2523
+ throw new Error('route.hops must be a non-empty array');
2524
+ const hops = raw.hops.map((entry, index) => {
2525
+ if (!entry || typeof entry !== 'object')
2526
+ throw new Error(`route.hops[${index}] must be an object`);
2527
+ const hop = entry;
2528
+ const base = {
2529
+ channelId: String(hop.channelId ?? ''),
2530
+ from: String(hop.from ?? ''),
2531
+ to: String(hop.to ?? ''),
2532
+ amount: typeof hop.amount === 'bigint' ? hop.amount : BigInt(String(hop.amount ?? '0')),
2533
+ tokenId: String(hop.tokenId ?? ''),
2534
+ ...(typeof hop.htlcId === 'string' ? { htlcId: hop.htlcId } : {}),
2535
+ };
2536
+ if (hop.isSwap === true) {
2537
+ return {
2538
+ ...base,
2539
+ isSwap: true,
2540
+ tokenIn: String(hop.tokenIn ?? ''),
2541
+ tokenOut: String(hop.tokenOut ?? ''),
2542
+ amountIn: typeof hop.amountIn === 'bigint' ? hop.amountIn : BigInt(String(hop.amountIn ?? '0')),
2543
+ amountOut: typeof hop.amountOut === 'bigint' ? hop.amountOut : BigInt(String(hop.amountOut ?? '0')),
2544
+ rate: String(hop.rate ?? ''),
2545
+ inboundChannelId: String(hop.inboundChannelId ?? ''),
2546
+ outboundChannelId: String(hop.outboundChannelId ?? ''),
2547
+ };
2548
+ }
2549
+ return base;
2550
+ });
2551
+ const route = {
2552
+ hops,
2553
+ totalFees: typeof raw.totalFees === 'bigint' ? raw.totalFees : BigInt(String(raw.totalFees ?? '0')),
2554
+ tokenIn: String(raw.tokenIn ?? ''),
2555
+ tokenOut: String(raw.tokenOut ?? ''),
2556
+ estimatedBlocks: Number(raw.estimatedBlocks ?? 0),
2557
+ };
2558
+ if (hops.some((hop) => hop.isSwap)) {
2559
+ route.swapHops = hops.filter((hop) => hop.isSwap);
2560
+ }
2561
+ return route;
2562
+ }
2563
+ function serializeWireRoute(route) {
2564
+ return {
2565
+ tokenIn: route.tokenIn,
2566
+ tokenOut: route.tokenOut,
2567
+ totalFees: route.totalFees.toString(),
2568
+ estimatedBlocks: route.estimatedBlocks,
2569
+ hops: route.hops.map((hop) => {
2570
+ const base = {
2571
+ channelId: hop.channelId,
2572
+ from: hop.from,
2573
+ to: hop.to,
2574
+ amount: hop.amount.toString(),
2575
+ tokenId: hop.tokenId,
2576
+ ...(hop.htlcId ? { htlcId: hop.htlcId } : {}),
2577
+ };
2578
+ if ('isSwap' in hop && hop.isSwap) {
2579
+ return {
2580
+ ...base,
2581
+ isSwap: true,
2582
+ tokenIn: hop.tokenIn,
2583
+ tokenOut: hop.tokenOut,
2584
+ amountIn: hop.amountIn.toString(),
2585
+ amountOut: hop.amountOut.toString(),
2586
+ rate: hop.rate,
2587
+ inboundChannelId: hop.inboundChannelId,
2588
+ outboundChannelId: hop.outboundChannelId,
2589
+ };
2590
+ }
2591
+ return base;
2592
+ }),
2593
+ };
2594
+ }
2532
2595
  const payMultiHop = async (raw) => {
2533
2596
  const params = paramsObject(raw);
2534
2597
  requireMutationContext(context);
2535
- const route = params.route;
2536
- if (!route || typeof route !== 'object' || !Array.isArray(route.hops)) {
2537
- throw new Error('route with hops is required');
2538
- }
2539
- const paymentRequest = params.paymentRequest;
2540
- if (!paymentRequest || typeof paymentRequest !== 'object') {
2541
- throw new Error('paymentRequest is required');
2542
- }
2543
- const request = paymentRequest;
2598
+ const from = context.localParticipant?.publicKeyDigest;
2599
+ if (!from)
2600
+ throw new Error('Local participant is not configured');
2601
+ const amount = requiredBigInt(params, 'amount');
2602
+ const tokenId = requiredString(params, 'tokenId');
2603
+ const targetTokenId = typeof params.targetTokenId === 'string' ? params.targetTokenId : undefined;
2604
+ const parsedRoute = params.route
2605
+ ? parseWireRoute(params.route)
2606
+ : await context.routing.getRoute({
2607
+ from,
2608
+ to: normalizeRouteNode(requiredString(params, 'toPartyId')),
2609
+ amount,
2610
+ tokenId,
2611
+ ...(targetTokenId ? { targetTokenId } : {}),
2612
+ ...(typeof params.maxHops === 'number' ? { maxHops: params.maxHops } : {}),
2613
+ });
2614
+ if (!parsedRoute)
2615
+ throw new Error('No route found');
2616
+ const isCrossToken = parsedRoute.hops.some((hop) => 'isSwap' in hop && hop.isSwap === true);
2544
2617
  const parsedRequest = {
2545
- hashlock: requiredString(request, 'hashlock'),
2546
- amount: requiredBigInt(request, 'amount'),
2547
- tokenId: requiredString(request, 'tokenId'),
2548
- expiryBlock: requiredBigInt(request, 'expiryBlock'),
2549
- preimage: typeof request.preimage === 'string' ? request.preimage : undefined,
2550
- description: typeof request.description === 'string' ? request.description : undefined,
2618
+ hashlock: requiredString(params, 'hashlock'),
2619
+ amount,
2620
+ tokenId,
2621
+ expiryBlock: requiredBigInt(params, 'expiryBlock'),
2622
+ ...(typeof params.preimage === 'string' ? { preimage: params.preimage } : {}),
2623
+ ...(typeof params.description === 'string' ? { description: params.description } : {}),
2551
2624
  };
2552
- const parsedRoute = route;
2553
- const isCrossToken = 'swapHops' in parsedRoute && Array.isArray(parsedRoute.swapHops) && parsedRoute.swapHops.length > 0;
2625
+ if (!parsedRequest.preimage)
2626
+ throw new Error('preimage is required');
2554
2627
  const ops = {
2555
2628
  addHTLC: (channel, htlcParams, leaseProvider) => (0, omnia_1.addHTLC)(channel, htlcParams, leaseProvider, context.signer),
2556
2629
  fulfillHTLC: (channel, htlcId, preimage, leaseProvider) => (0, omnia_1.fulfillHTLC)(channel, htlcId, preimage, leaseProvider, context.signer),
@@ -2588,6 +2661,7 @@ function createHostMethods(context) {
2588
2661
  success: true,
2589
2662
  preimage: result.preimage,
2590
2663
  settledHops: result.settledHops,
2664
+ route: serializeWireRoute(parsedRoute),
2591
2665
  };
2592
2666
  });
2593
2667
  };
@@ -1,5 +1,6 @@
1
1
  import type { OmniaHostConfig } from './config.js';
2
2
  import { ObservationBus } from './observe/bus.js';
3
+ import { SyncSqliteStore } from '@totemsdk/storage/sqlite-sync';
3
4
  import type { ObservationStore } from './observe/types.js';
4
5
  import type { OmniaSwarm, ChannelSigner, WotsLeaseProviderLike, MinimalChainProvider } from '@totemsdk/omnia';
5
6
  import { createOmniaSwarm } from '@totemsdk/omnia';
@@ -55,6 +56,8 @@ export interface OmniaHostDependencies {
55
56
  /** Observation feed: inject a store/bus (tests/embedding) or disable it. */
56
57
  observationStore?: ObservationStore;
57
58
  observationBus?: ObservationBus;
59
+ /** Synchronous storage adapter for durable factories/splices/VTXOs/pools. */
60
+ hostStateStore?: SyncSqliteStore;
58
61
  observe?: boolean;
59
62
  networkId?: string;
60
63
  sourceNodeId?: string;
package/dist/lifecycle.js CHANGED
@@ -9,6 +9,8 @@ const bus_js_1 = require("./observe/bus.js");
9
9
  const emitter_js_1 = require("./observe/emitter.js");
10
10
  const store_js_1 = require("./observe/store.js");
11
11
  const snapshot_js_1 = require("./observe/snapshot.js");
12
+ const sqlite_sync_1 = require("@totemsdk/storage/sqlite-sync");
13
+ const persistent_map_js_1 = require("./stores/persistent-map.js");
12
14
  const omnia_1 = require("@totemsdk/omnia");
13
15
  const sqlite_store_js_1 = require("./stores/sqlite-store.js");
14
16
  const operations_js_1 = require("./stores/operations.js");
@@ -37,6 +39,8 @@ function createOmniaHost(config, dependencies = {}) {
37
39
  let ownedOperationStore;
38
40
  let observationStore = dependencies.observationStore;
39
41
  let ownedObservationStore;
42
+ let hostStateStore = dependencies.hostStateStore;
43
+ let ownedHostStateStore;
40
44
  const observationBus = dependencies.observationBus ?? new bus_js_1.ObservationBus();
41
45
  let observation;
42
46
  let controlServer = dependencies.controlServer;
@@ -90,6 +94,16 @@ function createOmniaHost(config, dependencies = {}) {
90
94
  sourceNodeId: config.sourceNodeId ?? dependencies.sourceNodeId ?? config.localPartyId ?? 'omnia-host',
91
95
  });
92
96
  }
97
+ if (!hostStateStore) {
98
+ try {
99
+ ownedHostStateStore = new sqlite_sync_1.SyncSqliteStore(config.dbPath);
100
+ hostStateStore = ownedHostStateStore;
101
+ }
102
+ catch (err) {
103
+ console.error('[omnia-host] host-state store disabled:', err instanceof Error ? err.message : String(err));
104
+ }
105
+ }
106
+ const hostMap = (namespace, injected) => injected ?? (hostStateStore ? new persistent_map_js_1.PersistentMap(hostStateStore, namespace) : new Map());
93
107
  if (!swarm) {
94
108
  const relay = config.relay
95
109
  ? { mode: 'self-hosted', relayUrl: config.relay }
@@ -156,7 +170,7 @@ function createOmniaHost(config, dependencies = {}) {
156
170
  },
157
171
  }
158
172
  : undefined);
159
- const factories = dependencies.factories ?? (factoryBundles ? new Map() : undefined);
173
+ const factories = dependencies.factories ?? (factoryBundles ? hostMap('omnia:factory:') : undefined);
160
174
  const localBundle = localParticipant
161
175
  ? factoryBundles?.[localParticipant.partyId]
162
176
  : undefined;
@@ -169,8 +183,8 @@ function createOmniaHost(config, dependencies = {}) {
169
183
  : {}),
170
184
  }
171
185
  : undefined);
172
- const spliceProposals = dependencies.spliceProposals ?? new Map();
173
- const spliceAcceptances = dependencies.spliceAcceptances ?? new Map();
186
+ const spliceProposals = hostMap('omnia:splice:proposal:', dependencies.spliceProposals);
187
+ const spliceAcceptances = hostMap('omnia:splice:acceptance:', dependencies.spliceAcceptances);
174
188
  const onSpliceMessage = async (peer, msg) => {
175
189
  if (!spliceLeaseProvider || !channelStore)
176
190
  return;
@@ -449,12 +463,12 @@ function createOmniaHost(config, dependencies = {}) {
449
463
  const preparedOpen = new Map();
450
464
  const preparedSplice = new Map();
451
465
  const preparedFactory = new Map();
452
- const vtxoPools = new Map();
453
- const vtxos = new Map();
454
- const vtxoReceipts = new Map();
466
+ const vtxoPools = hostMap('omnia:vtxo:pool:');
467
+ const vtxos = hostMap('omnia:vtxo:');
468
+ const vtxoReceipts = hostMap('omnia:vtxo:receipt:');
455
469
  const preparedVtxoReceipts = new Map();
456
- const pools = new Map();
457
- const poolPositions = new Map();
470
+ const pools = hostMap('omnia:pool:');
471
+ const poolPositions = hostMap('omnia:pool:position:');
458
472
  if (!controlServer) {
459
473
  const readOnly = !dependencies.signer || !dependencies.leaseProvider;
460
474
  if (readOnly) {
@@ -567,6 +581,8 @@ function createOmniaHost(config, dependencies = {}) {
567
581
  if (ownedObservationStore)
568
582
  await ownedObservationStore.close();
569
583
  ownedObservationStore = undefined;
584
+ ownedHostStateStore?.close();
585
+ ownedHostStateStore = undefined;
570
586
  ownedStore?.close();
571
587
  ownedStore = undefined;
572
588
  started = false;
@@ -0,0 +1,31 @@
1
+ import { SyncSqliteStore } from '@totemsdk/storage/sqlite-sync';
2
+ /**
3
+ * Durable Map facade for host state (factories, virtual channels, splices,
4
+ * VTXOs, pools). Backed by `@totemsdk/storage/sqlite-sync` (same `totemsdk_kv`
5
+ * table + versioned codec as the channel/operation stores).
6
+ *
7
+ * Values are plain objects/arrays/bigints/Uint8Arrays. Runtime-only handles
8
+ * (`localSigner`, signer/lease/swarm objects) are stripped; Map/Set/Date are
9
+ * explicitly tagged so they round-trip. A function anywhere else is dropped
10
+ * rather than persisted (never serialise authority material by accident).
11
+ */
12
+ export declare const HOST_STATE_FORMAT_KEY = "omnia:store:host-state:format";
13
+ export declare const HOST_STATE_FORMAT = 1;
14
+ export declare class PersistentMap<V> extends Map<string, V> {
15
+ private readonly kv;
16
+ private readonly namespace;
17
+ constructor(kv: SyncSqliteStore, namespace: string);
18
+ private key;
19
+ private keysInNamespace;
20
+ get(id: string): V | undefined;
21
+ set(id: string, value: V): this;
22
+ has(id: string): boolean;
23
+ delete(id: string): boolean;
24
+ clear(): void;
25
+ get size(): number;
26
+ keys(): MapIterator<string>;
27
+ values(): MapIterator<V>;
28
+ entries(): MapIterator<[string, V]>;
29
+ [Symbol.iterator](): MapIterator<[string, V]>;
30
+ forEach(callbackfn: (value: V, key: string, map: Map<string, V>) => void): void;
31
+ }
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PersistentMap = exports.HOST_STATE_FORMAT = exports.HOST_STATE_FORMAT_KEY = void 0;
4
+ /**
5
+ * Durable Map facade for host state (factories, virtual channels, splices,
6
+ * VTXOs, pools). Backed by `@totemsdk/storage/sqlite-sync` (same `totemsdk_kv`
7
+ * table + versioned codec as the channel/operation stores).
8
+ *
9
+ * Values are plain objects/arrays/bigints/Uint8Arrays. Runtime-only handles
10
+ * (`localSigner`, signer/lease/swarm objects) are stripped; Map/Set/Date are
11
+ * explicitly tagged so they round-trip. A function anywhere else is dropped
12
+ * rather than persisted (never serialise authority material by accident).
13
+ */
14
+ exports.HOST_STATE_FORMAT_KEY = 'omnia:store:host-state:format';
15
+ exports.HOST_STATE_FORMAT = 1;
16
+ const EPHEMERAL_KEYS = new Set([
17
+ 'localSigner',
18
+ 'signer',
19
+ 'leaseProvider',
20
+ 'signerPort',
21
+ 'swarm',
22
+ 'routing',
23
+ ]);
24
+ function encode(value, seen = new WeakSet()) {
25
+ if (value === null || value === undefined)
26
+ return null;
27
+ if (typeof value === 'bigint')
28
+ return value;
29
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
30
+ return value;
31
+ }
32
+ if (typeof value === 'function')
33
+ return null;
34
+ if (value instanceof Uint8Array)
35
+ return value;
36
+ if (value instanceof Date)
37
+ return { $date: value.toISOString() };
38
+ if (Array.isArray(value)) {
39
+ return value.map((item) => encode(item, seen));
40
+ }
41
+ if (value instanceof Map) {
42
+ const entries = [];
43
+ for (const [key, item] of value.entries())
44
+ entries.push([String(key), encode(item, seen)]);
45
+ return { $map: entries };
46
+ }
47
+ if (value instanceof Set) {
48
+ return { $set: Array.from(value.values(), (item) => encode(item, seen)) };
49
+ }
50
+ if (typeof value === 'object') {
51
+ if (seen.has(value))
52
+ throw new Error('cannot persist cyclic host state');
53
+ seen.add(value);
54
+ const out = {};
55
+ for (const [key, item] of Object.entries(value)) {
56
+ if (EPHEMERAL_KEYS.has(key) || item === undefined || typeof item === 'function')
57
+ continue;
58
+ out[key] = encode(item, seen);
59
+ }
60
+ seen.delete(value);
61
+ return out;
62
+ }
63
+ return null;
64
+ }
65
+ function decode(value) {
66
+ if (value === null)
67
+ return null;
68
+ if (typeof value === 'bigint' ||
69
+ typeof value === 'string' ||
70
+ typeof value === 'number' ||
71
+ typeof value === 'boolean') {
72
+ return value;
73
+ }
74
+ if (value instanceof Uint8Array)
75
+ return value;
76
+ if (Array.isArray(value))
77
+ return value.map(decode);
78
+ if (typeof value === 'object') {
79
+ const record = value;
80
+ if (Array.isArray(record['$map'])) {
81
+ return new Map(record['$map'].map(([key, item]) => [key, decode(item)]));
82
+ }
83
+ if (Array.isArray(record['$set'])) {
84
+ return new Set(record['$set'].map(decode));
85
+ }
86
+ if (typeof record['$date'] === 'string')
87
+ return new Date(record['$date']);
88
+ const out = {};
89
+ for (const [key, item] of Object.entries(record))
90
+ out[key] = decode(item);
91
+ return out;
92
+ }
93
+ return value;
94
+ }
95
+ function iterator(values) {
96
+ let index = 0;
97
+ return {
98
+ next() {
99
+ if (index >= values.length)
100
+ return { done: true, value: undefined };
101
+ return { done: false, value: values[index++] };
102
+ },
103
+ [Symbol.iterator]() {
104
+ return this;
105
+ },
106
+ [Symbol.dispose]() {
107
+ index = values.length;
108
+ },
109
+ };
110
+ }
111
+ class PersistentMap extends Map {
112
+ kv;
113
+ namespace;
114
+ constructor(kv, namespace) {
115
+ super();
116
+ this.kv = kv;
117
+ this.namespace = namespace;
118
+ const format = kv.get(exports.HOST_STATE_FORMAT_KEY);
119
+ if (format !== null && format !== exports.HOST_STATE_FORMAT) {
120
+ throw new Error(`Unsupported omnia-host state format ${format} (this build supports ${exports.HOST_STATE_FORMAT}) — refusing to open`);
121
+ }
122
+ if (format === null)
123
+ kv.set(exports.HOST_STATE_FORMAT_KEY, exports.HOST_STATE_FORMAT);
124
+ }
125
+ key(id) {
126
+ return this.namespace + id;
127
+ }
128
+ keysInNamespace() {
129
+ return this.kv.keys().filter((key) => key.startsWith(this.namespace));
130
+ }
131
+ get(id) {
132
+ const raw = this.kv.get(this.key(id));
133
+ return raw === null ? undefined : decode(raw);
134
+ }
135
+ set(id, value) {
136
+ this.kv.set(this.key(id), encode(value));
137
+ return this;
138
+ }
139
+ has(id) {
140
+ return this.kv.has(this.key(id));
141
+ }
142
+ delete(id) {
143
+ return this.kv.remove(this.key(id));
144
+ }
145
+ clear() {
146
+ for (const key of this.keysInNamespace())
147
+ this.kv.remove(key);
148
+ }
149
+ get size() {
150
+ return this.keysInNamespace().length;
151
+ }
152
+ keys() {
153
+ return iterator(this.keysInNamespace().map((key) => key.slice(this.namespace.length)));
154
+ }
155
+ values() {
156
+ return iterator(Array.from(this.entries(), ([, value]) => value));
157
+ }
158
+ entries() {
159
+ return iterator(this.keysInNamespace().map((key) => [
160
+ key.slice(this.namespace.length),
161
+ decode(this.kv.get(key)),
162
+ ]));
163
+ }
164
+ [Symbol.iterator]() {
165
+ return this.entries();
166
+ }
167
+ forEach(callbackfn) {
168
+ for (const [key, value] of this.entries())
169
+ callbackfn(value, key, this);
170
+ }
171
+ }
172
+ exports.PersistentMap = PersistentMap;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@totemsdk/omnia-host",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Durable Omnia node daemon for channel lifecycle, routing, and control APIs",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -22,21 +22,21 @@
22
22
  ],
23
23
  "dependencies": {
24
24
  "ws": "^8.21.1",
25
+ "@totemsdk/chain-provider": "^0.3.0",
25
26
  "@totemsdk/core": "^1.4.0",
26
27
  "@totemsdk/identity": "^1.0.2",
27
- "@totemsdk/chain-provider": "^0.3.0",
28
28
  "@totemsdk/liquidity-bond": "^0.3.1",
29
- "@totemsdk/manifest": "^2.0.1",
29
+ "@totemsdk/manifest": "^2.0.2",
30
30
  "@totemsdk/minima-rpc": "^0.4.0",
31
31
  "@totemsdk/omnia": "^2.3.1",
32
+ "@totemsdk/omnia-pool": "^0.5.3",
32
33
  "@totemsdk/omnia-factory": "^0.7.0",
33
34
  "@totemsdk/omnia-router": "^0.4.0",
34
- "@totemsdk/omnia-pool": "^0.5.3",
35
35
  "@totemsdk/omnia-splice": "^0.3.0",
36
- "@totemsdk/p2p-hub": "^0.1.0",
37
36
  "@totemsdk/omnia-vtxo": "^0.6.1",
38
- "@totemsdk/storage": "^0.2.0",
39
- "@totemsdk/wots-lease": "^1.0.1"
37
+ "@totemsdk/wots-lease": "^1.0.1",
38
+ "@totemsdk/storage": "^0.2.1",
39
+ "@totemsdk/p2p-hub": "^0.1.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/jest": "^30.0.0",