@totemsdk/omnia-host 0.8.0 → 0.9.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/api/jsonrpc.js +6 -2
- package/dist/api/methods.d.ts +6 -0
- package/dist/api/methods.js +103 -3
- package/dist/chain/totem-node-adapter.d.ts +6 -0
- package/dist/chain/totem-node-adapter.js +19 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +1 -0
- package/dist/lifecycle.js +6 -0
- package/package.json +8 -8
package/dist/api/jsonrpc.js
CHANGED
|
@@ -27,8 +27,12 @@ async function dispatch(request, methods) {
|
|
|
27
27
|
return response(request.id, await method(request.params));
|
|
28
28
|
}
|
|
29
29
|
catch (error) {
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
// Surface the handler's message to the caller. Masking it as "Internal
|
|
31
|
+
// error" made operational failures (broadcast rejection, missing manager,
|
|
32
|
+
// malformed params) impossible to diagnose from the client side.
|
|
33
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
34
|
+
console.error('[jsonrpc] handler error:', error instanceof Error ? error.stack ?? message : message);
|
|
35
|
+
return errorResponse(request.id, -32000, message);
|
|
32
36
|
}
|
|
33
37
|
}
|
|
34
38
|
async function readBody(req) {
|
package/dist/api/methods.d.ts
CHANGED
|
@@ -17,6 +17,12 @@ export interface HostApiContext {
|
|
|
17
17
|
chainProvider?: ChainStateProvider;
|
|
18
18
|
swarm?: OmniaSwarm;
|
|
19
19
|
localParticipant?: ChannelParticipant;
|
|
20
|
+
/**
|
|
21
|
+
* Signs the funding-transaction digest with the funding coin's key, so
|
|
22
|
+
* openChannel can assemble the funding witness without the caller reproducing
|
|
23
|
+
* the funding draft. Configured from the node's `sign` command.
|
|
24
|
+
*/
|
|
25
|
+
fundingSigner?: (digest: Uint8Array) => Promise<Uint8Array> | Uint8Array;
|
|
20
26
|
factories?: Map<string, ChannelFactory>;
|
|
21
27
|
factoryBundles?: Record<string, WotsLeaseBundle>;
|
|
22
28
|
spliceProposals?: Map<string, SpliceProposal>;
|
package/dist/api/methods.js
CHANGED
|
@@ -48,8 +48,10 @@ function channelStateView(channel) {
|
|
|
48
48
|
totalValue: channel.totalValue,
|
|
49
49
|
currentSequence: channel.currentSequence,
|
|
50
50
|
fundingAddress: channel.fundingAddress,
|
|
51
|
+
fundingScript: channel.fundingScript,
|
|
51
52
|
fundingTxId: channel.fundingTxId,
|
|
52
53
|
fundingCoinId: channel.fundingCoinId,
|
|
54
|
+
contestDelayBlocks: channel.contestDelayBlocks,
|
|
53
55
|
parties: channel.parties,
|
|
54
56
|
balances: channel.balances,
|
|
55
57
|
pendingHTLCs: channel.pendingHTLCs,
|
|
@@ -184,6 +186,32 @@ async function persistProgramUpdate(context, channelId, run) {
|
|
|
184
186
|
balances: balancesJson(result.channel),
|
|
185
187
|
};
|
|
186
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Wait for an ACK/ERROR from the counterparty for a given channel. Returns the
|
|
191
|
+
* reply message (so callers can read a co-signature) or null on timeout.
|
|
192
|
+
*/
|
|
193
|
+
function waitForPeerReply(peer, channelId, timeoutMs) {
|
|
194
|
+
return new Promise((resolve) => {
|
|
195
|
+
let done = false;
|
|
196
|
+
let unsub;
|
|
197
|
+
const finish = (reply) => {
|
|
198
|
+
if (done)
|
|
199
|
+
return;
|
|
200
|
+
done = true;
|
|
201
|
+
clearTimeout(timer);
|
|
202
|
+
if (unsub)
|
|
203
|
+
unsub();
|
|
204
|
+
resolve(reply);
|
|
205
|
+
};
|
|
206
|
+
unsub = peer.onMessage((msg) => {
|
|
207
|
+
if (msg.channelId !== channelId)
|
|
208
|
+
return;
|
|
209
|
+
if (msg.type === 'ACK' || msg.type === 'ERROR')
|
|
210
|
+
finish(msg);
|
|
211
|
+
});
|
|
212
|
+
const timer = setTimeout(() => finish(null), timeoutMs);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
187
215
|
function createHostMethods(context) {
|
|
188
216
|
const methods = new Map();
|
|
189
217
|
const register = (name, handler, alias) => {
|
|
@@ -285,18 +313,58 @@ function createHostMethods(context) {
|
|
|
285
313
|
remoteAmount: requiredBigInt(params, 'remoteAmount'),
|
|
286
314
|
tokenId: typeof params.tokenId === 'string' ? params.tokenId : undefined,
|
|
287
315
|
fundingCoinId: requiredString(params, 'fundingCoinId'),
|
|
288
|
-
|
|
316
|
+
contestDelayBlocks: typeof params.contestDelayBlocks === 'number' ? params.contestDelayBlocks : undefined,
|
|
289
317
|
};
|
|
318
|
+
const fundingWitnessHex = typeof params.fundingWitnessHex === 'string' ? params.fundingWitnessHex : undefined;
|
|
319
|
+
if (fundingWitnessHex) {
|
|
320
|
+
createParams.fundingWitnessBytes = Buffer.from(fundingWitnessHex.replace(/^0x/i, ''), 'hex');
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
// Assemble the funding witness: createChannel builds the funding draft,
|
|
324
|
+
// a funding signer signs its digest, and the caller supplies the input
|
|
325
|
+
// proofs (serialized CoinProof + ScriptProof). The signer comes from the
|
|
326
|
+
// configured funding signer, or from the node's `sign` command using the
|
|
327
|
+
// `fundingPubkey` the caller passes.
|
|
328
|
+
const fundingPubkey = typeof params.fundingPubkey === 'string' ? params.fundingPubkey : undefined;
|
|
329
|
+
const nodeAdapter = context.chainProvider;
|
|
330
|
+
const signer = context.fundingSigner ?? (fundingPubkey && typeof nodeAdapter?.signData === 'function'
|
|
331
|
+
? async (digest) => {
|
|
332
|
+
const sig = await nodeAdapter.signData(fundingPubkey, '0x' + Buffer.from(digest).toString('hex'));
|
|
333
|
+
return Uint8Array.from(Buffer.from(sig.replace(/^0x/i, ''), 'hex'));
|
|
334
|
+
}
|
|
335
|
+
: undefined);
|
|
336
|
+
if (!signer) {
|
|
337
|
+
throw new Error('openChannel requires fundingWitnessHex, or a fundingPubkey + funding proofs');
|
|
338
|
+
}
|
|
339
|
+
if (typeof params.fundingScript !== 'string' && !Array.isArray(params.fundingScriptProofsHex)) {
|
|
340
|
+
throw new Error('openChannel requires fundingScript (or serialized fundingScriptProofsHex) for the funding input address');
|
|
341
|
+
}
|
|
342
|
+
const toBytes = (v) => Array.isArray(v) ? v.map((h) => Buffer.from(String(h).replace(/^0x/i, ''), 'hex')) : [];
|
|
343
|
+
createParams.fundingProofs = {
|
|
344
|
+
coinProofs: toBytes(params.fundingCoinProofsHex),
|
|
345
|
+
scriptProofs: toBytes(params.fundingScriptProofsHex),
|
|
346
|
+
};
|
|
347
|
+
// The funding coin's address script: createChannel derives the required
|
|
348
|
+
// single-leaf ScriptProof from it, so callers need not hand-roll it.
|
|
349
|
+
if (typeof params.fundingScript === 'string') {
|
|
350
|
+
createParams.fundingScript = params.fundingScript;
|
|
351
|
+
}
|
|
352
|
+
createParams.fundingSigner = signer;
|
|
353
|
+
}
|
|
290
354
|
const created = await (0, omnia_1.createChannel)(createParams, context.chainProvider);
|
|
291
355
|
context.channels.set(created.channel.channelId, {
|
|
292
356
|
...created.channel,
|
|
293
357
|
localSigner: context.signer,
|
|
294
358
|
});
|
|
359
|
+
let acknowledged = true;
|
|
295
360
|
if (context.swarm) {
|
|
296
361
|
const peer = await context.swarm.connectToPeer(remote.publicKeyDigest, created.channel.channelId);
|
|
297
362
|
await peer.sendMessage({ type: 'CHANNEL_PROPOSAL', channelId: created.channel.channelId, nonce: Date.now(), payload: created.proposal });
|
|
363
|
+
// Give the peer time to accept so a follow-up pay() is not lost.
|
|
364
|
+
const reply = await waitForPeerReply(peer, created.channel.channelId, 20_000);
|
|
365
|
+
acknowledged = reply?.type === 'ACK';
|
|
298
366
|
}
|
|
299
|
-
return { success: true, channelId: created.channel.channelId, fundingTxId: created.channel.fundingTxId };
|
|
367
|
+
return { success: true, channelId: created.channel.channelId, fundingTxId: created.channel.fundingTxId, acknowledged };
|
|
300
368
|
});
|
|
301
369
|
return result;
|
|
302
370
|
};
|
|
@@ -387,11 +455,43 @@ function createHostMethods(context) {
|
|
|
387
455
|
if (!channel)
|
|
388
456
|
throw new Error(`Channel ${channelId} not found`);
|
|
389
457
|
const closing = (0, omnia_1.markChannelClosing)(channel, 'mutual');
|
|
458
|
+
const partyAddresses = Object.fromEntries(closing.parties.map((party) => [party.partyId, party.settlementAddress ?? party.publicKeyDigest]));
|
|
459
|
+
// Cooperative close needs both eltoo MULTISIG(2 A B) signatures. Build the
|
|
460
|
+
// exact settlement draft, ask the counterparty to co-sign it, then include
|
|
461
|
+
// both signatures in the broadcast witness.
|
|
462
|
+
const localPartyId = context.localParticipant?.partyId;
|
|
463
|
+
const remoteParty = localPartyId
|
|
464
|
+
? closing.parties.find((party) => party.partyId !== localPartyId)
|
|
465
|
+
: undefined;
|
|
466
|
+
let counterpartySignatures;
|
|
467
|
+
let settlementTxHex;
|
|
468
|
+
if (context.swarm && remoteParty) {
|
|
469
|
+
const request = (0, omnia_1.buildSettlementSigningRequest)(closing, { partyAddresses });
|
|
470
|
+
const peer = await context.swarm.connectToPeer(remoteParty.publicKeyDigest, channelId);
|
|
471
|
+
await peer.sendMessage({
|
|
472
|
+
type: 'SETTLEMENT_PROPOSAL',
|
|
473
|
+
channelId,
|
|
474
|
+
nonce: Date.now(),
|
|
475
|
+
payload: { signRequest: true, ...request },
|
|
476
|
+
});
|
|
477
|
+
const reply = await waitForPeerReply(peer, channelId, 30_000);
|
|
478
|
+
const replyPayload = reply?.payload;
|
|
479
|
+
if (reply?.type !== 'ACK' || typeof replyPayload?.signature !== 'string') {
|
|
480
|
+
throw new Error(`Counterparty did not co-sign the settlement (${reply?.type ?? 'timeout'})`);
|
|
481
|
+
}
|
|
482
|
+
counterpartySignatures = [{
|
|
483
|
+
partyId: replyPayload.partyId ?? remoteParty.partyId,
|
|
484
|
+
signature: Buffer.from(replyPayload.signature.replace(/^0x/i, ''), 'hex'),
|
|
485
|
+
}];
|
|
486
|
+
settlementTxHex = request.settlementTxHex;
|
|
487
|
+
}
|
|
390
488
|
const settlement = await (0, omnia_1.proposeSettlement)(closing, context.leaseProvider, {
|
|
391
489
|
signer: context.signer,
|
|
392
490
|
chainProvider: context.chainProvider,
|
|
393
491
|
broadcastProofs: parsedProofs,
|
|
394
|
-
partyAddresses
|
|
492
|
+
partyAddresses,
|
|
493
|
+
settlementTxHex,
|
|
494
|
+
counterpartySignatures,
|
|
395
495
|
});
|
|
396
496
|
context.channels.set(channelId, (0, omnia_1.markChannelClosed)(closing));
|
|
397
497
|
return { success: true, channelId, settlementTxId: settlement.settlementPayload.txpowId, finalBalances: Object.fromEntries(Object.entries(settlement.settlementPayload.balances).map(([key, value]) => [key, value.toString()])) };
|
|
@@ -16,6 +16,12 @@ export interface ConfirmationOptions {
|
|
|
16
16
|
}
|
|
17
17
|
export interface TotemNodeAdapter extends ChainStateProvider {
|
|
18
18
|
getMode(): Promise<string | undefined>;
|
|
19
|
+
/**
|
|
20
|
+
* Sign arbitrary data with a wallet key (`sign publickey:<0x> data:<0x>`).
|
|
21
|
+
* Used to produce the funding witness signature: createChannel builds the
|
|
22
|
+
* funding digest, and this signs it with the funding coin's key.
|
|
23
|
+
*/
|
|
24
|
+
signData(pubkeyHex: string, dataHex: string): Promise<string>;
|
|
19
25
|
waitForConfirmation(coinId: string, options?: ConfirmationOptions): Promise<void>;
|
|
20
26
|
subscribe(listener: (tip: ChainTip) => void): () => void;
|
|
21
27
|
close(): void;
|
|
@@ -37,6 +37,10 @@ function createTotemNodeAdapter(options) {
|
|
|
37
37
|
searchTokens: (query) => provider.searchTokens(query),
|
|
38
38
|
getTokensByCreator: (address) => provider.getTokensByCreator(address),
|
|
39
39
|
broadcastTxPoW: (txpowHex) => provider.broadcastTxPoW(txpowHex),
|
|
40
|
+
getMinTxPowWork: () => provider.getMinTxPowWork ? provider.getMinTxPowWork() : Promise.resolve(null),
|
|
41
|
+
...(provider.submitTransaction
|
|
42
|
+
? { submitTransaction: provider.submitTransaction.bind(provider) }
|
|
43
|
+
: {}),
|
|
40
44
|
async getMode() {
|
|
41
45
|
const result = await client.runCommand('getmode');
|
|
42
46
|
if (!result || typeof result !== 'object')
|
|
@@ -44,6 +48,21 @@ function createTotemNodeAdapter(options) {
|
|
|
44
48
|
const mode = result.mode;
|
|
45
49
|
return typeof mode === 'string' ? mode : undefined;
|
|
46
50
|
},
|
|
51
|
+
async signData(pubkeyHex, dataHex) {
|
|
52
|
+
const result = await client.runCommand('sign', { publickey: pubkeyHex, data: dataHex });
|
|
53
|
+
const unwrap = (v) => {
|
|
54
|
+
if (typeof v === 'string')
|
|
55
|
+
return v;
|
|
56
|
+
if (v && typeof v === 'object' && typeof v.response === 'string') {
|
|
57
|
+
return v.response;
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
};
|
|
61
|
+
const sig = unwrap(result) ?? unwrap(result?.response);
|
|
62
|
+
if (!sig)
|
|
63
|
+
throw new Error('signData: unexpected node response');
|
|
64
|
+
return sig;
|
|
65
|
+
},
|
|
47
66
|
async waitForConfirmation(coinId, confirmationOptions = {}) {
|
|
48
67
|
const timeoutMs = confirmationOptions.timeoutMs ?? 120_000;
|
|
49
68
|
const interval = confirmationOptions.pollIntervalMs ?? pollIntervalMs;
|
package/dist/config.d.ts
CHANGED
|
@@ -11,6 +11,12 @@ export interface OmniaHostConfig {
|
|
|
11
11
|
localAddressIndex?: number;
|
|
12
12
|
localSettlementAddress?: string;
|
|
13
13
|
nodeMode?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Public key (0x…) that owns the funding coin. When set, the host signs the
|
|
16
|
+
* funding-transaction digest via the node's `sign` command to assemble the
|
|
17
|
+
* channel funding witness (OMNIA_FUNDING_PUBKEY).
|
|
18
|
+
*/
|
|
19
|
+
fundingPubkey?: string;
|
|
14
20
|
wsPath: string;
|
|
15
21
|
/** BIP39 mnemonic or hex seed (same formats @totemsdk/core accepts). */
|
|
16
22
|
seed?: string;
|
package/dist/config.js
CHANGED
|
@@ -75,6 +75,7 @@ function loadConfigFromEnv(env = process.env) {
|
|
|
75
75
|
localAddressIndex: optionalInteger(env.OMNIA_LOCAL_ADDRESS_INDEX),
|
|
76
76
|
localSettlementAddress: optional(env.OMNIA_LOCAL_SETTLEMENT_ADDRESS),
|
|
77
77
|
nodeMode: optional(env.OMNIA_NODE_MODE),
|
|
78
|
+
fundingPubkey: optional(env.OMNIA_FUNDING_PUBKEY),
|
|
78
79
|
wsPath: env.OMNIA_WS_PATH?.trim() || '/rpc',
|
|
79
80
|
seed,
|
|
80
81
|
keyfile,
|
package/dist/lifecycle.js
CHANGED
|
@@ -107,6 +107,12 @@ function createOmniaHost(config, dependencies = {}) {
|
|
|
107
107
|
spliceProposals: dependencies.spliceProposals,
|
|
108
108
|
spliceAcceptances: dependencies.spliceAcceptances,
|
|
109
109
|
spliceLeaseProvider: dependencies.spliceLeaseProvider,
|
|
110
|
+
fundingSigner: config.fundingPubkey
|
|
111
|
+
? async (digest) => {
|
|
112
|
+
const sig = await chainAdapter.signData(config.fundingPubkey, '0x' + Buffer.from(digest).toString('hex'));
|
|
113
|
+
return Uint8Array.from(Buffer.from(sig.replace(/^0x/i, ''), 'hex'));
|
|
114
|
+
}
|
|
115
|
+
: undefined,
|
|
110
116
|
readOnly,
|
|
111
117
|
identity: dependencies.identity,
|
|
112
118
|
manifest: dependencies.manifest,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@totemsdk/omnia-host",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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,17 +22,17 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"better-sqlite3": "^12.2.0",
|
|
24
24
|
"ws": "^8.21.1",
|
|
25
|
-
"@totemsdk/chain-provider": "^0.
|
|
25
|
+
"@totemsdk/chain-provider": "^0.3.0",
|
|
26
|
+
"@totemsdk/core": "^1.4.0",
|
|
26
27
|
"@totemsdk/identity": "^1.0.1",
|
|
27
|
-
"@totemsdk/
|
|
28
|
+
"@totemsdk/omnia": "^2.2.0",
|
|
28
29
|
"@totemsdk/manifest": "^2.0.0",
|
|
29
|
-
"@totemsdk/omnia": "^
|
|
30
|
-
"@totemsdk/omnia-factory": "^0.5.0",
|
|
30
|
+
"@totemsdk/omnia-factory": "^0.6.0",
|
|
31
31
|
"@totemsdk/omnia-router": "^0.3.0",
|
|
32
32
|
"@totemsdk/omnia-splice": "^0.3.0",
|
|
33
|
-
"@totemsdk/omnia-vtxo": "^0.
|
|
34
|
-
"@totemsdk/minima-rpc": "^0.2.2",
|
|
33
|
+
"@totemsdk/omnia-vtxo": "^0.6.0",
|
|
35
34
|
"@totemsdk/storage": "^0.1.0",
|
|
35
|
+
"@totemsdk/minima-rpc": "^0.4.0",
|
|
36
36
|
"@totemsdk/wots-lease": "^1.0.1"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"jest": "^30.4.2",
|
|
44
44
|
"ts-jest": "^29.4.11",
|
|
45
45
|
"typescript": "5.9.3",
|
|
46
|
-
"@totemsdk/txpow": "^1.0
|
|
46
|
+
"@totemsdk/txpow": "^1.1.0"
|
|
47
47
|
},
|
|
48
48
|
"engines": {
|
|
49
49
|
"node": ">=20.0.0"
|