@totemsdk/omnia-host 0.10.0 → 0.12.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.
@@ -4,9 +4,13 @@ exports.channelStateView = channelStateView;
4
4
  exports.requestPeerReply = requestPeerReply;
5
5
  exports.createHostMethods = createHostMethods;
6
6
  const omnia_1 = require("@totemsdk/omnia");
7
+ const omnia_vtxo_1 = require("@totemsdk/omnia-vtxo");
8
+ const omnia_pool_1 = require("@totemsdk/omnia-pool");
9
+ const liquidity_bond_1 = require("@totemsdk/liquidity-bond");
7
10
  const omnia_router_1 = require("@totemsdk/omnia-router");
8
11
  const omnia_factory_1 = require("@totemsdk/omnia-factory");
9
12
  const omnia_splice_1 = require("@totemsdk/omnia-splice");
13
+ const emitter_js_1 = require("../observe/emitter.js");
10
14
  function channelSummary(channel) {
11
15
  const parties = channel.parties.map((party) => party.partyId);
12
16
  return {
@@ -149,16 +153,36 @@ async function withOperation(context, params, run) {
149
153
  if (existing && !['pending', 'failed'].includes(existing.status)) {
150
154
  throw new Error(`Operation ${id} is already ${existing.status}`);
151
155
  }
152
- if (!existing)
156
+ if (!existing) {
153
157
  context.operations.create(id, params);
158
+ await context.observe?.emit({
159
+ domain: 'operation', kind: 'operation.created', phase: 'proposed',
160
+ entity: { type: 'operation', id }, operationId: id,
161
+ data: { request: params },
162
+ });
163
+ }
154
164
  context.operations.transition(id, existing?.status === 'failed' ? 'failed' : 'pending', 'executing');
165
+ await context.observe?.emit({
166
+ domain: 'operation', kind: 'operation.executing', phase: 'executing',
167
+ entity: { type: 'operation', id }, operationId: id,
168
+ });
155
169
  try {
156
170
  const result = await run();
157
171
  context.operations.transition(id, 'executing', 'committed', { result });
172
+ await context.observe?.emit({
173
+ domain: 'operation', kind: 'operation.committed', phase: 'committed',
174
+ entity: { type: 'operation', id }, operationId: id,
175
+ data: { result },
176
+ });
158
177
  return result;
159
178
  }
160
179
  catch (error) {
161
180
  context.operations.transition(id, 'executing', 'failed', { error: error instanceof Error ? error.message : String(error) });
181
+ await context.observe?.emit({
182
+ domain: 'operation', kind: 'operation.failed', phase: 'failed',
183
+ entity: { type: 'operation', id }, operationId: id,
184
+ data: { error: error instanceof Error ? error.message : String(error) },
185
+ });
162
186
  throw error;
163
187
  }
164
188
  }
@@ -238,12 +262,1397 @@ function requestPeerReply(peer, msg, channelId, timeoutMs, types = ['ACK', 'ERRO
238
262
  void peer.sendMessage(msg).catch(() => { });
239
263
  });
240
264
  }
265
+ function signingIndicesParam(value) {
266
+ if (!value || typeof value !== 'object')
267
+ throw new Error('signingIndices is required');
268
+ const item = value;
269
+ const num = (key) => {
270
+ const parsed = Number(item[key]);
271
+ if (!Number.isInteger(parsed) || parsed < 0)
272
+ throw new Error(`signingIndices.${key} must be a non-negative integer`);
273
+ return parsed;
274
+ };
275
+ return { addressIndex: num('addressIndex'), l1: num('l1'), l2: num('l2') };
276
+ }
277
+ function hexBytes(value, label) {
278
+ if (typeof value !== 'string')
279
+ throw new Error(`${label} must be a hex string`);
280
+ return Buffer.from(value.replace(/^0x/i, ''), 'hex');
281
+ }
282
+ function registerPreparedPayMethods(methods, context) {
283
+ const preparePay = async (raw) => {
284
+ const params = paramsObject(raw);
285
+ const channelId = requiredString(params, 'channelId');
286
+ const amount = requiredBigInt(params, 'amount');
287
+ const localPartyId = context.localParticipant?.partyId;
288
+ const localPkd = context.localParticipant?.publicKeyDigest;
289
+ if (!localPartyId || !localPkd)
290
+ throw new Error('Local participant is not configured');
291
+ if (!context.preparedPay)
292
+ throw new Error('Prepared-pay store is not configured');
293
+ const channel = context.channels.get(channelId);
294
+ if (!channel)
295
+ throw new Error(`Channel ${channelId} not found`);
296
+ const remotePartyId = channel.parties.find((party) => party.partyId !== localPartyId)?.partyId;
297
+ if (!remotePartyId)
298
+ throw new Error('Channel counterparty not found');
299
+ if (amount <= 0n || (channel.balances[localPartyId] ?? 0n) < amount) {
300
+ throw new Error('Insufficient channel balance');
301
+ }
302
+ if ((context.custodyMode ?? 'self') === 'hosted') {
303
+ throw new Error('preparePay requires an explicit hosted custody disclosure context (not yet configured)');
304
+ }
305
+ return withOperation(context, params, async () => {
306
+ const preparedOperationId = operationId(params);
307
+ const unsigned = (0, omnia_1.buildStateUpdate)(channel, {
308
+ newSequence: channel.currentSequence + 1,
309
+ newBalances: {
310
+ ...channel.balances,
311
+ [localPartyId]: (channel.balances[localPartyId] ?? 0n) - amount,
312
+ [remotePartyId]: (channel.balances[remotePartyId] ?? 0n) + amount,
313
+ },
314
+ }, { publicKeyDigest: localPkd });
315
+ const expiresAt = Date.now() + 120_000;
316
+ const base = {
317
+ domain: 'channel',
318
+ signers: [localPkd],
319
+ custody: (0, omnia_1.makeSelfCustody)(),
320
+ expiresAt,
321
+ };
322
+ const intent = {
323
+ ...base,
324
+ requestId: `${preparedOperationId}:intent`,
325
+ kind: 'intent',
326
+ digest: unsigned.updateDigest,
327
+ context: { summary: `Authorize payment of ${amount.toString()} on channel ${channelId}` },
328
+ };
329
+ const update = {
330
+ ...base,
331
+ requestId: `${preparedOperationId}:update`,
332
+ kind: 'chain',
333
+ digest: unsigned.updateDigest,
334
+ context: { summary: 'Sign the channel update transaction' },
335
+ };
336
+ const settlement = {
337
+ ...base,
338
+ requestId: `${preparedOperationId}:settlement`,
339
+ kind: 'chain',
340
+ digest: unsigned.settlementDigest,
341
+ context: { summary: 'Sign the settlement close package' },
342
+ };
343
+ for (const [key, entry] of context.preparedPay) {
344
+ if (entry.expiresAt <= Date.now())
345
+ context.preparedPay.delete(key);
346
+ }
347
+ context.preparedPay.set(preparedOperationId, { channelId, unsigned, intent, update, settlement, expiresAt });
348
+ return {
349
+ success: true,
350
+ status: 'signature-required',
351
+ operationId: preparedOperationId,
352
+ channelId,
353
+ requests: { intent, update, settlement },
354
+ expiresAt,
355
+ };
356
+ });
357
+ };
358
+ methods.set('totem_omniaPreparePay', preparePay);
359
+ methods.set('omnia/preparePay', preparePay);
360
+ const prepareChannelUpdate = async (raw) => {
361
+ const params = paramsObject(raw);
362
+ const channelId = requiredString(params, 'channelId');
363
+ const localPartyId = context.localParticipant?.partyId;
364
+ const localPkd = context.localParticipant?.publicKeyDigest;
365
+ if (!localPartyId || !localPkd)
366
+ throw new Error('Local participant is not configured');
367
+ if (!context.preparedPay)
368
+ throw new Error('Prepared-pay store is not configured');
369
+ const channel = context.channels.get(channelId);
370
+ if (!channel)
371
+ throw new Error(`Channel ${channelId} not found`);
372
+ if ((context.custodyMode ?? 'self') === 'hosted') {
373
+ throw new Error('prepareChannelUpdate requires an explicit hosted custody disclosure context (not yet configured)');
374
+ }
375
+ const rawBalances = params.newBalances;
376
+ const newBalances = rawBalances && typeof rawBalances === 'object'
377
+ ? Object.fromEntries(Object.entries(rawBalances).map(([partyId, value]) => [partyId, BigInt(String(value))]))
378
+ : channel.balances;
379
+ const programTransition = params.programTransition;
380
+ return withOperation(context, params, async () => {
381
+ const preparedOperationId = operationId(params);
382
+ const unsigned = (0, omnia_1.buildStateUpdate)(channel, { newSequence: channel.currentSequence + 1, newBalances, programTransition }, { publicKeyDigest: localPkd });
383
+ const expiresAt = Date.now() + 120_000;
384
+ const base = {
385
+ domain: 'channel',
386
+ signers: [localPkd],
387
+ custody: (0, omnia_1.makeSelfCustody)(),
388
+ expiresAt,
389
+ };
390
+ const summary = typeof params.memo === 'string' && params.memo ? params.memo : 'Channel state update';
391
+ const intent = {
392
+ ...base,
393
+ requestId: `${preparedOperationId}:intent`,
394
+ kind: 'intent',
395
+ digest: unsigned.updateDigest,
396
+ context: { summary: `Authorize ${summary}` },
397
+ };
398
+ const update = {
399
+ ...base,
400
+ requestId: `${preparedOperationId}:update`,
401
+ kind: 'chain',
402
+ digest: unsigned.updateDigest,
403
+ context: { summary: 'Sign the channel update transaction' },
404
+ };
405
+ const settlement = {
406
+ ...base,
407
+ requestId: `${preparedOperationId}:settlement`,
408
+ kind: 'chain',
409
+ digest: unsigned.settlementDigest,
410
+ context: { summary: 'Sign the settlement close package' },
411
+ };
412
+ for (const [key, entry] of context.preparedPay) {
413
+ if (entry.expiresAt <= Date.now())
414
+ context.preparedPay.delete(key);
415
+ }
416
+ context.preparedPay.set(preparedOperationId, { channelId, unsigned, intent, update, settlement, expiresAt });
417
+ return {
418
+ success: true,
419
+ status: 'signature-required',
420
+ operationId: preparedOperationId,
421
+ channelId,
422
+ requests: { intent, update, settlement },
423
+ expiresAt,
424
+ };
425
+ });
426
+ };
427
+ methods.set('totem_omniaPrepareChannelUpdate', prepareChannelUpdate);
428
+ methods.set('omnia/prepareChannelUpdate', prepareChannelUpdate);
429
+ const submitPay = async (raw) => {
430
+ const params = paramsObject(raw);
431
+ const preparedOperationId = requiredString(params, 'preparedOperationId');
432
+ const localPkd = context.localParticipant?.publicKeyDigest;
433
+ if (!localPkd)
434
+ throw new Error('Local participant is not configured');
435
+ if (!context.preparedPay)
436
+ throw new Error('Prepared-pay store is not configured');
437
+ return withOperation(context, params, async () => {
438
+ const entry = context.preparedPay.get(preparedOperationId);
439
+ if (!entry)
440
+ throw new Error(`Prepared payment ${preparedOperationId} not found`);
441
+ if (entry.expiresAt <= Date.now()) {
442
+ context.preparedPay.delete(preparedOperationId);
443
+ throw new Error(`Prepared payment ${preparedOperationId} has expired`);
444
+ }
445
+ const intentSignature = hexBytes(params.intentSignature, 'intentSignature');
446
+ const updateSignature = hexBytes(params.updateSignature, 'updateSignature');
447
+ const settlementSignature = hexBytes(params.settlementSignature, 'settlementSignature');
448
+ const updateIndices = signingIndicesParam(params.updateIndices);
449
+ const settlementIndices = signingIndicesParam(params.settlementIndices);
450
+ if (!(0, omnia_1.verifyChannelSignature)(intentSignature, (0, omnia_1.signRequestPayload)(entry.intent), localPkd)) {
451
+ throw new Error('intent signature does not verify');
452
+ }
453
+ if (!(0, omnia_1.verifyChannelSignature)(updateSignature, entry.unsigned.updateDigest, localPkd)) {
454
+ throw new Error('update signature does not verify');
455
+ }
456
+ if (!(0, omnia_1.verifyChannelSignature)(settlementSignature, entry.unsigned.settlementDigest, localPkd)) {
457
+ throw new Error('settlement signature does not verify');
458
+ }
459
+ const state = (0, omnia_1.finalizeStateUpdateWithIndices)(entry.unsigned, updateSignature, updateIndices, settlementSignature, settlementIndices);
460
+ const channel = entry.unsigned.channel;
461
+ const updatedChannel = {
462
+ ...channel,
463
+ balances: state.balances,
464
+ currentSequence: state.sequence,
465
+ pendingProposal: { sequence: state.sequence, payloadHash: entry.unsigned.payloadHash },
466
+ stateLog: [
467
+ ...channel.stateLog,
468
+ {
469
+ sequence: state.sequence,
470
+ timestamp: Date.now(),
471
+ balances: { ...state.balances },
472
+ htlcCount: entry.unsigned.pendingHTLCs.length,
473
+ event: 'update',
474
+ },
475
+ ],
476
+ updatedAt: Date.now(),
477
+ };
478
+ context.channels.set(entry.channelId, updatedChannel);
479
+ const localPartyId = context.localParticipant.partyId;
480
+ const remote = channel.parties.find((party) => party.partyId !== localPartyId);
481
+ if (!remote)
482
+ throw new Error('Channel counterparty not found');
483
+ if (context.swarm) {
484
+ const peer = await context.swarm.connectToPeer(remote.publicKeyDigest, entry.channelId, remote.transportPubkey);
485
+ await peer.sendMessage({
486
+ type: 'STATE_UPDATE',
487
+ channelId: entry.channelId,
488
+ nonce: Date.now(),
489
+ payload: state,
490
+ });
491
+ }
492
+ context.preparedPay.delete(preparedOperationId);
493
+ return {
494
+ success: true,
495
+ channelId: entry.channelId,
496
+ sequence: state.sequence,
497
+ localBalance: state.balances[localPartyId].toString(),
498
+ remoteBalance: state.balances[remote.partyId].toString(),
499
+ };
500
+ });
501
+ };
502
+ methods.set('totem_omniaSubmitPay', submitPay);
503
+ methods.set('omnia/submitPay', submitPay);
504
+ methods.set('totem_omniaSubmitChannelUpdate', submitPay);
505
+ methods.set('omnia/submitChannelUpdate', submitPay);
506
+ const prepareSettle = async (raw) => {
507
+ const params = paramsObject(raw);
508
+ const channelId = requiredString(params, 'channelId');
509
+ const localPartyId = context.localParticipant?.partyId;
510
+ const localPkd = context.localParticipant?.publicKeyDigest;
511
+ if (!localPartyId || !localPkd)
512
+ throw new Error('Local participant is not configured');
513
+ if (!context.preparedSettle)
514
+ throw new Error('Prepared-settle store is not configured');
515
+ if (!context.swarm)
516
+ throw new Error('prepareSettle requires swarm connectivity');
517
+ const channel = context.channels.get(channelId);
518
+ if (!channel)
519
+ throw new Error(`Channel ${channelId} not found`);
520
+ if (!['active', 'closing_mutual'].includes(channel.status)) {
521
+ throw new Error(`Channel ${channelId} is not settleable (${channel.status})`);
522
+ }
523
+ const closing = (0, omnia_1.markChannelClosing)(channel, 'mutual');
524
+ const partyAddresses = Object.fromEntries(closing.parties.map((party) => [party.partyId, party.settlementAddress ?? party.publicKeyDigest]));
525
+ const remote = closing.parties.find((party) => party.partyId !== localPartyId);
526
+ if (!remote)
527
+ throw new Error('Channel counterparty not found');
528
+ const request = (0, omnia_1.buildSettlementSigningRequest)(closing, { partyAddresses });
529
+ return withOperation(context, params, async () => {
530
+ const peer = await context.swarm.connectToPeer(remote.publicKeyDigest, channelId, remote.transportPubkey);
531
+ const reply = await requestPeerReply(peer, {
532
+ type: 'SETTLEMENT_PROPOSAL',
533
+ channelId,
534
+ nonce: Date.now(),
535
+ payload: { signRequest: true, ...request },
536
+ }, channelId, 30_000);
537
+ const payload = reply?.payload;
538
+ if (reply?.type !== 'ACK' || typeof payload?.signature !== 'string') {
539
+ throw new Error(`Counterparty did not co-sign the settlement (${reply?.type ?? 'timeout'})`);
540
+ }
541
+ const counterpartySignature = {
542
+ partyId: payload.partyId ?? remote.partyId,
543
+ signature: Buffer.from(payload.signature.replace(/^0x/i, ''), 'hex'),
544
+ };
545
+ const preparedOperationId = operationId(params);
546
+ const expiresAt = Date.now() + 120_000;
547
+ const digest = Buffer.from(request.digestHex.replace(/^0x/i, ''), 'hex');
548
+ const base = {
549
+ domain: 'channel',
550
+ signers: [localPkd],
551
+ custody: (0, omnia_1.makeSelfCustody)(),
552
+ expiresAt,
553
+ };
554
+ const intent = {
555
+ ...base,
556
+ requestId: `${preparedOperationId}:intent`,
557
+ kind: 'intent',
558
+ digest,
559
+ context: { summary: 'Authorize cooperative settlement' },
560
+ };
561
+ const settlement = {
562
+ ...base,
563
+ requestId: `${preparedOperationId}:settlement`,
564
+ kind: 'chain',
565
+ digest,
566
+ context: { summary: 'Sign the cooperative settlement transaction' },
567
+ };
568
+ for (const [key, entry] of context.preparedSettle) {
569
+ if (entry.expiresAt <= Date.now())
570
+ context.preparedSettle.delete(key);
571
+ }
572
+ context.preparedSettle.set(preparedOperationId, {
573
+ channelId,
574
+ closing,
575
+ request,
576
+ counterpartySignature,
577
+ intent,
578
+ settlement,
579
+ expiresAt,
580
+ });
581
+ return {
582
+ success: true,
583
+ status: 'signature-required',
584
+ operationId: preparedOperationId,
585
+ channelId,
586
+ requests: { intent, settlement },
587
+ expiresAt,
588
+ };
589
+ });
590
+ };
591
+ methods.set('totem_omniaPrepareSettle', prepareSettle);
592
+ methods.set('omnia/prepareSettle', prepareSettle);
593
+ const submitSettle = async (raw) => {
594
+ const params = paramsObject(raw);
595
+ const preparedOperationId = requiredString(params, 'preparedOperationId');
596
+ const localPkd = context.localParticipant?.publicKeyDigest;
597
+ if (!localPkd)
598
+ throw new Error('Local participant is not configured');
599
+ if (!context.preparedSettle)
600
+ throw new Error('Prepared-settle store is not configured');
601
+ return withOperation(context, params, async () => {
602
+ const entry = context.preparedSettle.get(preparedOperationId);
603
+ if (!entry)
604
+ throw new Error(`Prepared settlement ${preparedOperationId} not found`);
605
+ if (entry.expiresAt <= Date.now()) {
606
+ context.preparedSettle.delete(preparedOperationId);
607
+ throw new Error(`Prepared settlement ${preparedOperationId} has expired`);
608
+ }
609
+ const intentSignature = hexBytes(params.intentSignature, 'intentSignature');
610
+ const settlementSignature = hexBytes(params.settlementSignature, 'settlementSignature');
611
+ const digest = Buffer.from(entry.request.digestHex.replace(/^0x/i, ''), 'hex');
612
+ if (!(0, omnia_1.verifyChannelSignature)(intentSignature, (0, omnia_1.signRequestPayload)(entry.intent), localPkd)) {
613
+ throw new Error('intent signature does not verify');
614
+ }
615
+ if (!(0, omnia_1.verifyChannelSignature)(settlementSignature, digest, localPkd)) {
616
+ throw new Error('settlement signature does not verify');
617
+ }
618
+ if (!context.chainProvider)
619
+ throw new Error('Settlement requires a chain provider');
620
+ const rawProofs = params.broadcastProofs;
621
+ const broadcastProofs = rawProofs && typeof rawProofs === 'object'
622
+ && Array.isArray(rawProofs.coinProofs)
623
+ && Array.isArray(rawProofs.scriptProofs)
624
+ ? {
625
+ coinProofs: rawProofs.coinProofs.map((hex) => Buffer.from(String(hex).replace(/^0x/i, ''), 'hex')),
626
+ scriptProofs: rawProofs.scriptProofs.map((hex) => Buffer.from(String(hex).replace(/^0x/i, ''), 'hex')),
627
+ }
628
+ : undefined;
629
+ const settlementIndices = params.settlementIndices
630
+ ? signingIndicesParam(params.settlementIndices)
631
+ : undefined;
632
+ const settlement = await (0, omnia_1.proposeSettlement)(entry.closing, context.leaseProvider, {
633
+ chainProvider: context.chainProvider,
634
+ broadcastProofs,
635
+ partyAddresses: entry.request.partyAddresses,
636
+ settlementTxHex: entry.request.settlementTxHex,
637
+ counterpartySignatures: [entry.counterpartySignature],
638
+ localSignature: settlementSignature,
639
+ localIndices: settlementIndices,
640
+ localPartyId: context.localParticipant.partyId,
641
+ });
642
+ context.channels.set(entry.channelId, (0, omnia_1.markChannelClosed)(entry.closing));
643
+ context.preparedSettle.delete(preparedOperationId);
644
+ return {
645
+ success: true,
646
+ channelId: entry.channelId,
647
+ settlementTxId: settlement.settlementPayload.txpowId,
648
+ finalBalances: Object.fromEntries(Object.entries(settlement.settlementPayload.balances).map(([key, value]) => [key, value.toString()])),
649
+ };
650
+ });
651
+ };
652
+ methods.set('totem_omniaSubmitSettle', submitSettle);
653
+ methods.set('omnia/submitSettle', submitSettle);
654
+ const prepareOpenChannel = async (raw) => {
655
+ const params = paramsObject(raw);
656
+ const localParticipant = context.localParticipant;
657
+ const localPkd = localParticipant?.publicKeyDigest;
658
+ if (!localParticipant || !localPkd)
659
+ throw new Error('Local participant is not configured');
660
+ if (!context.preparedOpen)
661
+ throw new Error('Prepared-open store is not configured');
662
+ if (!context.chainProvider)
663
+ throw new Error('prepareOpenChannel requires a chain provider');
664
+ const remote = {
665
+ partyId: requiredString(params, 'remotePartyId'),
666
+ publicKeyDigest: requiredString(params, 'remotePublicKeyDigest'),
667
+ addressIndex: typeof params.remoteAddressIndex === 'number' ? params.remoteAddressIndex : 0,
668
+ settlementAddress: typeof params.remoteSettlementAddress === 'string' ? params.remoteSettlementAddress : undefined,
669
+ transportPubkey: typeof params.remoteTransportPubkey === 'string' ? params.remoteTransportPubkey : undefined,
670
+ };
671
+ const createParams = {
672
+ localParty: {
673
+ ...localParticipant,
674
+ transportPubkey: localParticipant.transportPubkey ?? context.transportPubkey,
675
+ },
676
+ remoteParty: remote,
677
+ localAmount: requiredBigInt(params, 'localAmount'),
678
+ remoteAmount: requiredBigInt(params, 'remoteAmount'),
679
+ tokenId: typeof params.tokenId === 'string' ? params.tokenId : undefined,
680
+ fundingCoinId: requiredString(params, 'fundingCoinId'),
681
+ contestDelayBlocks: typeof params.contestDelayBlocks === 'number' ? params.contestDelayBlocks : undefined,
682
+ fundingScript: typeof params.fundingScript === 'string' ? params.fundingScript : undefined,
683
+ };
684
+ return withOperation(context, params, async () => {
685
+ const plan = await (0, omnia_1.buildChannelOpening)(createParams, context.chainProvider);
686
+ const preparedOperationId = operationId(params);
687
+ const expiresAt = Date.now() + 300_000;
688
+ const base = {
689
+ domain: 'channel',
690
+ signers: [localPkd],
691
+ custody: (0, omnia_1.makeSelfCustody)(),
692
+ expiresAt,
693
+ };
694
+ const summary = `Open channel ${plan.channel.channelId} (${plan.channel.totalValue} units)`;
695
+ const intent = {
696
+ ...base,
697
+ requestId: `${preparedOperationId}:intent`,
698
+ kind: 'intent',
699
+ digest: plan.fundingDigest,
700
+ context: {
701
+ summary: `Authorize ${summary}`,
702
+ amount: plan.channel.totalValue.toString(),
703
+ asset: plan.channel.tokenId,
704
+ terms: `Counterparty ${remote.partyId}; channel ${plan.channel.channelId}`,
705
+ },
706
+ };
707
+ const funding = {
708
+ ...base,
709
+ requestId: `${preparedOperationId}:funding`,
710
+ kind: 'chain',
711
+ digest: plan.fundingDigest,
712
+ context: { summary: 'Sign the channel funding transaction' },
713
+ };
714
+ for (const [key, entry] of context.preparedOpen) {
715
+ if (entry.expiresAt <= Date.now())
716
+ context.preparedOpen.delete(key);
717
+ }
718
+ context.preparedOpen.set(preparedOperationId, {
719
+ plan,
720
+ fundingScript: createParams.fundingScript,
721
+ intent,
722
+ funding,
723
+ expiresAt,
724
+ });
725
+ return {
726
+ success: true,
727
+ status: 'signature-required',
728
+ operationId: preparedOperationId,
729
+ channelId: plan.channel.channelId,
730
+ fundingAddress: plan.fundingAddress,
731
+ fundingCoinId: plan.outputCoinId,
732
+ inputCoinId: plan.inputCoinId,
733
+ totalValue: plan.channel.totalValue.toString(),
734
+ requests: { intent, funding },
735
+ expiresAt,
736
+ };
737
+ });
738
+ };
739
+ methods.set('totem_omniaPrepareOpenChannel', prepareOpenChannel);
740
+ methods.set('omnia/prepareOpenChannel', prepareOpenChannel);
741
+ const submitOpenChannel = async (raw) => {
742
+ const params = paramsObject(raw);
743
+ const preparedOperationId = requiredString(params, 'preparedOperationId');
744
+ const localPkd = context.localParticipant?.publicKeyDigest;
745
+ if (!localPkd)
746
+ throw new Error('Local participant is not configured');
747
+ if (!context.preparedOpen)
748
+ throw new Error('Prepared-open store is not configured');
749
+ if (!context.chainProvider)
750
+ throw new Error('submitOpenChannel requires a chain provider');
751
+ return withOperation(context, params, async () => {
752
+ const entry = context.preparedOpen.get(preparedOperationId);
753
+ if (!entry)
754
+ throw new Error(`Prepared channel opening ${preparedOperationId} not found`);
755
+ if (entry.expiresAt <= Date.now()) {
756
+ context.preparedOpen.delete(preparedOperationId);
757
+ throw new Error(`Prepared channel opening ${preparedOperationId} has expired`);
758
+ }
759
+ const intentSignature = hexBytes(params.intentSignature, 'intentSignature');
760
+ const fundingSignature = hexBytes(params.fundingSignature, 'fundingSignature');
761
+ if (!(0, omnia_1.verifyChannelSignature)(intentSignature, (0, omnia_1.signRequestPayload)(entry.intent), localPkd)) {
762
+ throw new Error('intent signature does not verify');
763
+ }
764
+ if (!(0, omnia_1.verifyChannelSignature)(fundingSignature, entry.plan.fundingDigest, localPkd)) {
765
+ throw new Error('funding signature does not verify');
766
+ }
767
+ const toBytes = (value) => Array.isArray(value) ? value.map((item) => Buffer.from(String(item).replace(/^0x/i, ''), 'hex')) : [];
768
+ const fundingWitnessHex = typeof params.fundingWitnessHex === 'string' ? params.fundingWitnessHex : undefined;
769
+ let finalizeOpts;
770
+ if (fundingWitnessHex) {
771
+ finalizeOpts = { fundingWitnessBytes: Buffer.from(fundingWitnessHex.replace(/^0x/i, ''), 'hex') };
772
+ }
773
+ else {
774
+ finalizeOpts = {
775
+ fundingSignature,
776
+ fundingProofs: {
777
+ coinProofs: toBytes(params.fundingCoinProofsHex),
778
+ scriptProofs: toBytes(params.fundingScriptProofsHex),
779
+ },
780
+ fundingScript: typeof params.fundingScript === 'string'
781
+ ? params.fundingScript
782
+ : entry.fundingScript,
783
+ };
784
+ }
785
+ const { channel, proposal, fundingTxId } = await (0, omnia_1.finalizeChannelOpening)(entry.plan, finalizeOpts, context.chainProvider);
786
+ context.channels.set(channel.channelId, { ...channel, localSigner: context.signer });
787
+ let acknowledged = true;
788
+ if (context.swarm) {
789
+ const remote = channel.parties.find((party) => party.partyId !== context.localParticipant.partyId);
790
+ if (remote) {
791
+ const peer = await context.swarm.connectToPeer(remote.publicKeyDigest, channel.channelId, remote.transportPubkey);
792
+ const reply = await requestPeerReply(peer, { type: 'CHANNEL_PROPOSAL', channelId: channel.channelId, nonce: Date.now(), payload: proposal }, channel.channelId, 20_000);
793
+ acknowledged = reply?.type === 'ACK';
794
+ }
795
+ }
796
+ context.preparedOpen.delete(preparedOperationId);
797
+ return { success: true, channelId: channel.channelId, fundingTxId, acknowledged };
798
+ });
799
+ };
800
+ methods.set('totem_omniaSubmitOpenChannel', submitOpenChannel);
801
+ methods.set('omnia/submitOpenChannel', submitOpenChannel);
802
+ const prepareSplice = (type) => async (raw) => {
803
+ const params = paramsObject(raw);
804
+ if (!context.preparedSplice || !context.spliceAcceptances) {
805
+ throw new Error('Splice manager is not configured');
806
+ }
807
+ if (!context.swarm)
808
+ throw new Error('Splice preparation requires swarm connectivity');
809
+ const channelId = requiredString(params, 'channelId');
810
+ const channel = context.channels.get(channelId);
811
+ if (!channel)
812
+ throw new Error(`Channel ${channelId} not found`);
813
+ const localPartyId = context.localParticipant?.partyId;
814
+ const localPkd = context.localParticipant?.publicKeyDigest;
815
+ if (!localPartyId || !localPkd)
816
+ throw new Error('Local participant is not configured');
817
+ if (channel.status !== 'active')
818
+ throw new Error(`Channel ${channelId} is not active (${channel.status})`);
819
+ const pending = channel.pendingHTLCs.filter((h) => h.status === 'pending');
820
+ if (pending.length > 0)
821
+ throw new Error(`Cannot splice with ${pending.length} pending HTLC(s)`);
822
+ const remote = channel.parties.find((party) => party.partyId !== localPartyId);
823
+ if (!remote)
824
+ throw new Error('Channel counterparty not found');
825
+ const nextSequence = channel.currentSequence + 1;
826
+ const quiesceUnsigned = (0, omnia_1.buildStateUpdate)(channel, { newSequence: nextSequence, newBalances: { ...channel.balances } }, { publicKeyDigest: localPkd });
827
+ const quiescedChannel = {
828
+ ...channel,
829
+ currentSequence: nextSequence,
830
+ balances: { ...channel.balances },
831
+ pendingHTLCs: [],
832
+ latestState: null,
833
+ status: 'quiesced',
834
+ quiesceSignedState: quiesceUnsigned.unsignedClosePackage,
835
+ updatedAt: Date.now(),
836
+ };
837
+ const parsedBalances = params.newBalances && typeof params.newBalances === 'object'
838
+ ? Object.fromEntries(Object.entries(params.newBalances).map(([partyId, value]) => [partyId, BigInt(String(value))]))
839
+ : undefined;
840
+ const unsigned = type === 'in'
841
+ ? (0, omnia_splice_1.buildSpliceInProposal)(quiescedChannel, requiredString(params, 'additionalCoinId'), requiredBigInt(params, 'additionalAmount'), localPkd, parsedBalances)
842
+ : (0, omnia_splice_1.buildSpliceOutProposal)(quiescedChannel, requiredBigInt(params, 'withdrawAmount'), requiredString(params, 'withdrawAddress'), localPkd, parsedBalances);
843
+ return withOperation(context, params, async () => {
844
+ const placeholder = {
845
+ spliceId: unsigned.spliceId,
846
+ channelId: unsigned.channelId,
847
+ params: unsigned.params,
848
+ spliceTxHex: unsigned.spliceTxHex,
849
+ spliceTxDraft: unsigned.spliceTxDraft,
850
+ proposerPublicKeyDigest: localPkd,
851
+ proposerSignature: new Uint8Array(0),
852
+ proposerReservationId: '',
853
+ proposerSigningIndices: { addressIndex: 0, l1: 0, l2: 0 },
854
+ proposedAt: Date.now(),
855
+ };
856
+ const peer = await context.swarm.connectToPeer(remote.publicKeyDigest, channelId, remote.transportPubkey);
857
+ const reply = await requestPeerReply(peer, { type: 'SPLICE_PROPOSAL', channelId, nonce: Date.now(), payload: { proposal: placeholder } }, channelId, 30_000, ['SPLICE_ACCEPT', 'ERROR']);
858
+ const acceptance = reply?.payload?.acceptance;
859
+ if (reply?.type !== 'SPLICE_ACCEPT' || !acceptance) {
860
+ throw new Error(`Counterparty did not accept the splice (${reply?.type ?? 'timeout'})`);
861
+ }
862
+ context.spliceAcceptances.set(unsigned.spliceId, acceptance);
863
+ const preparedOperationId = operationId(params);
864
+ const expiresAt = Date.now() + 300_000;
865
+ const base = {
866
+ domain: 'splice',
867
+ signers: [localPkd],
868
+ custody: (0, omnia_1.makeSelfCustody)(),
869
+ expiresAt,
870
+ };
871
+ const quiesceIntent = {
872
+ ...base,
873
+ domain: 'channel',
874
+ requestId: `${preparedOperationId}:quiesce:intent`,
875
+ kind: 'intent',
876
+ digest: quiesceUnsigned.updateDigest,
877
+ context: { summary: `Authorize quiesce before ${type === 'in' ? 'splice-in' : 'splice-out'}` },
878
+ };
879
+ const quiesceUpdate = {
880
+ ...base,
881
+ domain: 'channel',
882
+ requestId: `${preparedOperationId}:quiesce:update`,
883
+ kind: 'chain',
884
+ digest: quiesceUnsigned.updateDigest,
885
+ context: { summary: 'Sign the quiesce channel update' },
886
+ };
887
+ const quiesceSettlement = {
888
+ ...base,
889
+ domain: 'channel',
890
+ requestId: `${preparedOperationId}:quiesce:settlement`,
891
+ kind: 'chain',
892
+ digest: quiesceUnsigned.settlementDigest,
893
+ context: { summary: 'Sign the quiesce settlement package' },
894
+ };
895
+ const intent = {
896
+ ...base,
897
+ requestId: `${preparedOperationId}:intent`,
898
+ kind: 'intent',
899
+ digest: unsigned.digest,
900
+ context: {
901
+ summary: `Authorize ${type === 'in' ? 'splice-in' : 'splice-out'} on ${channelId}`,
902
+ amount: unsigned.params.newTotalValue.toString(),
903
+ },
904
+ };
905
+ const splice = {
906
+ ...base,
907
+ requestId: `${preparedOperationId}:splice`,
908
+ kind: 'chain',
909
+ digest: unsigned.digest,
910
+ context: { summary: 'Sign the splice transaction' },
911
+ };
912
+ for (const [key, entry] of context.preparedSplice) {
913
+ if (entry.expiresAt <= Date.now())
914
+ context.preparedSplice.delete(key);
915
+ }
916
+ context.preparedSplice.set(preparedOperationId, {
917
+ channelId,
918
+ quiescedChannel,
919
+ quiesce: {
920
+ unsigned: quiesceUnsigned,
921
+ intent: quiesceIntent,
922
+ update: quiesceUpdate,
923
+ settlement: quiesceSettlement,
924
+ },
925
+ unsigned,
926
+ acceptance,
927
+ intent,
928
+ splice,
929
+ expiresAt,
930
+ });
931
+ return {
932
+ success: true,
933
+ status: 'signature-required',
934
+ operationId: preparedOperationId,
935
+ channelId,
936
+ spliceId: unsigned.spliceId,
937
+ spliceType: type,
938
+ requests: {
939
+ quiesce: { intent: quiesceIntent, update: quiesceUpdate, settlement: quiesceSettlement },
940
+ splice: { intent, chain: splice },
941
+ },
942
+ expiresAt,
943
+ };
944
+ });
945
+ };
946
+ methods.set('totem_omniaPrepareSpliceIn', prepareSplice('in'));
947
+ methods.set('omnia/prepareSpliceIn', prepareSplice('in'));
948
+ methods.set('totem_omniaPrepareSpliceOut', prepareSplice('out'));
949
+ methods.set('omnia/prepareSpliceOut', prepareSplice('out'));
950
+ const submitSplice = async (raw) => {
951
+ const params = paramsObject(raw);
952
+ const preparedOperationId = requiredString(params, 'preparedOperationId');
953
+ const localPkd = context.localParticipant?.publicKeyDigest;
954
+ if (!localPkd)
955
+ throw new Error('Local participant is not configured');
956
+ if (!context.preparedSplice)
957
+ throw new Error('Prepared-splice store is not configured');
958
+ return withOperation(context, params, async () => {
959
+ const entry = context.preparedSplice.get(preparedOperationId);
960
+ if (!entry)
961
+ throw new Error(`Prepared splice ${preparedOperationId} not found`);
962
+ if (entry.expiresAt <= Date.now()) {
963
+ context.preparedSplice.delete(preparedOperationId);
964
+ throw new Error(`Prepared splice ${preparedOperationId} has expired`);
965
+ }
966
+ const quiesceIntentSignature = hexBytes(params.quiesceIntentSignature, 'quiesceIntentSignature');
967
+ const quiesceUpdateSignature = hexBytes(params.quiesceUpdateSignature, 'quiesceUpdateSignature');
968
+ const quiesceSettlementSignature = hexBytes(params.quiesceSettlementSignature, 'quiesceSettlementSignature');
969
+ const spliceIntentSignature = hexBytes(params.spliceIntentSignature, 'spliceIntentSignature');
970
+ const spliceSignature = hexBytes(params.spliceSignature, 'spliceSignature');
971
+ const updateIndices = signingIndicesParam(params.quiesceUpdateIndices);
972
+ const settlementIndices = signingIndicesParam(params.quiesceSettlementIndices);
973
+ const spliceIndices = params.spliceIndices
974
+ ? signingIndicesParam(params.spliceIndices)
975
+ : { addressIndex: 0, l1: 0, l2: 0 };
976
+ if (!(0, omnia_1.verifyChannelSignature)(quiesceIntentSignature, (0, omnia_1.signRequestPayload)(entry.quiesce.intent), localPkd)) {
977
+ throw new Error('quiesce intent signature does not verify');
978
+ }
979
+ if (!(0, omnia_1.verifyChannelSignature)(quiesceUpdateSignature, entry.quiesce.unsigned.updateDigest, localPkd)) {
980
+ throw new Error('quiesce update signature does not verify');
981
+ }
982
+ if (!(0, omnia_1.verifyChannelSignature)(quiesceSettlementSignature, entry.quiesce.unsigned.settlementDigest, localPkd)) {
983
+ throw new Error('quiesce settlement signature does not verify');
984
+ }
985
+ if (!(0, omnia_1.verifyChannelSignature)(spliceIntentSignature, (0, omnia_1.signRequestPayload)(entry.intent), localPkd)) {
986
+ throw new Error('splice intent signature does not verify');
987
+ }
988
+ if (!(0, omnia_1.verifyChannelSignature)(spliceSignature, entry.unsigned.digest, localPkd)) {
989
+ throw new Error('splice signature does not verify');
990
+ }
991
+ if (!context.chainProvider?.broadcastTxPoW) {
992
+ throw new Error('Splice requires a chain provider with broadcastTxPoW');
993
+ }
994
+ const quiesceState = (0, omnia_1.finalizeStateUpdateWithIndices)(entry.quiesce.unsigned, quiesceUpdateSignature, updateIndices, quiesceSettlementSignature, settlementIndices);
995
+ const quiescedChannel = {
996
+ ...entry.quiescedChannel,
997
+ balances: quiesceState.balances,
998
+ currentSequence: quiesceState.sequence,
999
+ pendingHTLCs: [],
1000
+ latestState: quiesceState,
1001
+ quiesceSignedState: quiesceState,
1002
+ status: 'quiesced',
1003
+ updatedAt: Date.now(),
1004
+ };
1005
+ const { digest: _digest, ...unsignedBase } = entry.unsigned;
1006
+ const proposal = {
1007
+ ...unsignedBase,
1008
+ proposerSignature: spliceSignature,
1009
+ proposerReservationId: '',
1010
+ proposerSigningIndices: spliceIndices,
1011
+ proposedAt: Date.now(),
1012
+ };
1013
+ const spliced = await (0, omnia_splice_1.finalizeSplice)(quiescedChannel, proposal, entry.acceptance, {
1014
+ broadcast: (txHex) => context.chainProvider.broadcastTxPoW(txHex),
1015
+ });
1016
+ context.channels.set(entry.channelId, spliced);
1017
+ if (context.swarm) {
1018
+ const remote = spliced.parties.find((party) => party.partyId !== context.localParticipant.partyId);
1019
+ if (remote) {
1020
+ const peer = await context.swarm.connectToPeer(remote.publicKeyDigest, entry.channelId, remote.transportPubkey);
1021
+ await peer.sendMessage({
1022
+ type: 'SPLICE_FINALIZED',
1023
+ channelId: entry.channelId,
1024
+ nonce: Date.now(),
1025
+ payload: {
1026
+ channel: spliced,
1027
+ txId: spliced.spliceFundingTxId,
1028
+ spliceId: entry.unsigned.spliceId,
1029
+ },
1030
+ });
1031
+ }
1032
+ }
1033
+ context.preparedSplice.delete(preparedOperationId);
1034
+ return {
1035
+ success: true,
1036
+ channelId: entry.channelId,
1037
+ spliceId: entry.unsigned.spliceId,
1038
+ spliceType: spliced.spliceType,
1039
+ totalValue: spliced.totalValue.toString(),
1040
+ balances: Object.fromEntries(Object.entries(spliced.balances).map(([partyId, amount]) => [partyId, amount.toString()])),
1041
+ fundingCoinId: spliced.spliceFundingCoinId,
1042
+ };
1043
+ });
1044
+ };
1045
+ methods.set('totem_omniaSubmitSplice', submitSplice);
1046
+ methods.set('omnia/submitSplice', submitSplice);
1047
+ const prepareFactory = async (raw) => {
1048
+ const params = paramsObject(raw);
1049
+ const localPkd = context.localParticipant?.publicKeyDigest;
1050
+ if (!localPkd)
1051
+ throw new Error('Local participant is not configured');
1052
+ if (!context.preparedFactory)
1053
+ throw new Error('Prepared-factory store is not configured');
1054
+ const participantsRaw = params.participants;
1055
+ if (!Array.isArray(participantsRaw) || participantsRaw.length < 2) {
1056
+ throw new Error('participants must contain at least two factory participants');
1057
+ }
1058
+ const participants = participantsRaw.map((value) => {
1059
+ if (!value || typeof value !== 'object')
1060
+ throw new Error('Invalid factory participant');
1061
+ const item = value;
1062
+ return {
1063
+ partyId: requiredString(item, 'partyId'),
1064
+ publicKeyDigest: requiredString(item, 'publicKeyDigest'),
1065
+ addressIndex: Number(item.addressIndex ?? 0),
1066
+ contributionAmount: BigInt(requiredString(item, 'contributionAmount')),
1067
+ fundingCoinId: typeof item.fundingCoinId === 'string' ? item.fundingCoinId : undefined,
1068
+ settlementAddress: typeof item.settlementAddress === 'string' ? item.settlementAddress : undefined,
1069
+ transportPubkey: typeof item.transportPubkey === 'string' ? item.transportPubkey : undefined,
1070
+ };
1071
+ });
1072
+ const toBytes = (value) => typeof value === 'string' ? Buffer.from(value.replace(/^0x/i, ''), 'hex') : undefined;
1073
+ const fundingMaterials = Array.isArray(params.funding)
1074
+ ? params.funding.map((value) => {
1075
+ if (!value || typeof value !== 'object')
1076
+ throw new Error('Invalid factory funding material');
1077
+ const item = value;
1078
+ const coinProof = toBytes(item.coinProof);
1079
+ if (!coinProof)
1080
+ throw new Error('factory funding material requires coinProof');
1081
+ return {
1082
+ partyId: requiredString(item, 'partyId'),
1083
+ coinProof,
1084
+ ...(typeof item.coinAddress === 'string' ? { coinAddress: item.coinAddress } : {}),
1085
+ ...(typeof item.script === 'string' ? { script: item.script } : {}),
1086
+ ...(toBytes(item.scriptProof) ? { scriptProof: toBytes(item.scriptProof) } : {}),
1087
+ };
1088
+ })
1089
+ : undefined;
1090
+ const tokenId = typeof params.tokenId === 'string' ? params.tokenId : '0x00';
1091
+ const tokenScale = typeof params.tokenScale === 'number' ? params.tokenScale : 0;
1092
+ const remote = participants.find((participant) => participant.publicKeyDigest !== localPkd);
1093
+ return withOperation(context, params, async () => {
1094
+ const plan = await (0, omnia_factory_1.buildFactoryOpening)(participants, tokenId, tokenScale, context.chainProvider, fundingMaterials);
1095
+ let remoteFundingSignature;
1096
+ if (plan.onChain && context.swarm && remote) {
1097
+ const peer = await context.swarm.connectToPeer(remote.publicKeyDigest, plan.factory.factoryId, remote.transportPubkey);
1098
+ const reply = await requestPeerReply(peer, {
1099
+ type: 'FACTORY_FUNDING_REQUEST',
1100
+ channelId: plan.factory.factoryId,
1101
+ nonce: Date.now(),
1102
+ payload: { digest: '0x' + Buffer.from(plan.fundingDigest).toString('hex') },
1103
+ }, plan.factory.factoryId, 30_000, ['FACTORY_FUNDING_RESPONSE', 'ERROR']);
1104
+ const signature = reply?.payload?.signature;
1105
+ if (!signature)
1106
+ throw new Error(`Counterparty did not co-sign factory funding (${reply?.type ?? 'timeout'})`);
1107
+ remoteFundingSignature = Uint8Array.from(Buffer.from(signature.replace(/^0x/i, ''), 'hex'));
1108
+ }
1109
+ const preparedOperationId = operationId(params);
1110
+ const expiresAt = Date.now() + 300_000;
1111
+ const commitment = Buffer.from(plan.factory.pendingCommitment, 'hex');
1112
+ const base = {
1113
+ domain: 'factory',
1114
+ signers: [localPkd],
1115
+ custody: (0, omnia_1.makeSelfCustody)(),
1116
+ expiresAt,
1117
+ };
1118
+ const intent = {
1119
+ ...base,
1120
+ requestId: `${preparedOperationId}:intent`,
1121
+ kind: 'intent',
1122
+ digest: commitment,
1123
+ context: {
1124
+ summary: `Authorize factory opening (${plan.factory.totalValue} units)`,
1125
+ amount: plan.factory.totalValue.toString(),
1126
+ asset: plan.factory.tokenId,
1127
+ },
1128
+ };
1129
+ const commitmentRequest = {
1130
+ ...base,
1131
+ requestId: `${preparedOperationId}:commitment`,
1132
+ kind: 'chain',
1133
+ digest: commitment,
1134
+ context: { summary: 'Sign the factory funding/opening commitment' },
1135
+ };
1136
+ for (const [key, entry] of context.preparedFactory) {
1137
+ if (entry.expiresAt <= Date.now())
1138
+ context.preparedFactory.delete(key);
1139
+ }
1140
+ context.preparedFactory.set(preparedOperationId, {
1141
+ plan,
1142
+ remotePartyId: remote?.partyId,
1143
+ remoteFundingSignature,
1144
+ intent,
1145
+ commitment: commitmentRequest,
1146
+ expiresAt,
1147
+ });
1148
+ return {
1149
+ success: true,
1150
+ status: 'signature-required',
1151
+ operationId: preparedOperationId,
1152
+ factoryId: plan.factory.factoryId,
1153
+ onChain: plan.onChain,
1154
+ fundingCoinId: plan.fundingCoinId,
1155
+ totalValue: plan.factory.totalValue.toString(),
1156
+ requests: { intent, commitment: commitmentRequest },
1157
+ expiresAt,
1158
+ };
1159
+ });
1160
+ };
1161
+ methods.set('totem_omniaPrepareFactory', prepareFactory);
1162
+ methods.set('omnia/prepareFactory', prepareFactory);
1163
+ const submitFactory = async (raw) => {
1164
+ const params = paramsObject(raw);
1165
+ const preparedOperationId = requiredString(params, 'preparedOperationId');
1166
+ const localPkd = context.localParticipant?.publicKeyDigest;
1167
+ const localPartyId = context.localParticipant?.partyId;
1168
+ if (!localPkd || !localPartyId)
1169
+ throw new Error('Local participant is not configured');
1170
+ if (!context.preparedFactory)
1171
+ throw new Error('Prepared-factory store is not configured');
1172
+ return withOperation(context, params, async () => {
1173
+ const entry = context.preparedFactory.get(preparedOperationId);
1174
+ if (!entry)
1175
+ throw new Error(`Prepared factory ${preparedOperationId} not found`);
1176
+ if (entry.expiresAt <= Date.now()) {
1177
+ context.preparedFactory.delete(preparedOperationId);
1178
+ throw new Error(`Prepared factory ${preparedOperationId} has expired`);
1179
+ }
1180
+ const intentSignature = hexBytes(params.intentSignature, 'intentSignature');
1181
+ const commitmentSignature = hexBytes(params.commitmentSignature, 'commitmentSignature');
1182
+ const commitment = Buffer.from(entry.plan.factory.pendingCommitment, 'hex');
1183
+ if (!(0, omnia_1.verifyChannelSignature)(intentSignature, (0, omnia_1.signRequestPayload)(entry.intent), localPkd)) {
1184
+ throw new Error('intent signature does not verify');
1185
+ }
1186
+ if (!(0, omnia_1.verifyChannelSignature)(commitmentSignature, commitment, localPkd)) {
1187
+ throw new Error('factory commitment signature does not verify');
1188
+ }
1189
+ if (!context.chainProvider && entry.plan.onChain)
1190
+ throw new Error('Factory funding requires a chain provider');
1191
+ const fundingSignatures = { [localPartyId]: commitmentSignature };
1192
+ if (entry.remotePartyId && entry.remoteFundingSignature) {
1193
+ fundingSignatures[entry.remotePartyId] = entry.remoteFundingSignature;
1194
+ }
1195
+ let factory = await (0, omnia_factory_1.finalizeFactoryOpening)(entry.plan, {
1196
+ fundingSignatures: entry.plan.onChain ? fundingSignatures : undefined,
1197
+ openingSignatures: { [localPartyId]: commitmentSignature },
1198
+ }, context.chainProvider);
1199
+ context.factories?.set(factory.factoryId, factory);
1200
+ if (context.swarm && entry.remotePartyId) {
1201
+ const remote = factory.participants.find((participant) => participant.partyId === entry.remotePartyId);
1202
+ if (remote) {
1203
+ const peer = await context.swarm.connectToPeer(remote.publicKeyDigest, factory.factoryId, remote.transportPubkey);
1204
+ const reply = await requestPeerReply(peer, { type: 'FACTORY_PROPOSAL', channelId: factory.factoryId, nonce: Date.now(), payload: { factory } }, factory.factoryId, 30_000, ['FACTORY_ACCEPT', 'ERROR']);
1205
+ if (reply?.type !== 'FACTORY_ACCEPT') {
1206
+ throw new Error(`Counterparty did not accept the factory (${reply?.type ?? 'timeout'})`);
1207
+ }
1208
+ const accepted = reply.payload?.factory;
1209
+ if (!accepted)
1210
+ throw new Error('Factory accept reply missing factory');
1211
+ factory = accepted;
1212
+ context.factories?.set(factory.factoryId, factory);
1213
+ }
1214
+ }
1215
+ context.preparedFactory.delete(preparedOperationId);
1216
+ return { success: true, factoryId: factory.factoryId, status: factory.status, fundingTxId: factory.fundingTxId };
1217
+ });
1218
+ };
1219
+ methods.set('totem_omniaSubmitFactory', submitFactory);
1220
+ methods.set('omnia/submitFactory', submitFactory);
1221
+ const createVtxoPoolMethod = async (raw) => {
1222
+ const params = paramsObject(raw);
1223
+ if (!context.vtxoPools)
1224
+ throw new Error('VTXO stores are not configured');
1225
+ const localPkd = context.localParticipant?.publicKeyDigest;
1226
+ const operator = typeof params.operator === 'string' ? params.operator : localPkd;
1227
+ if (!operator)
1228
+ throw new Error('VTXO pool operator is required');
1229
+ const pool = (0, omnia_vtxo_1.createPool)({
1230
+ operator,
1231
+ tokenId: typeof params.tokenId === 'string' ? params.tokenId : '0x00',
1232
+ totalCapacity: requiredBigInt(params, 'totalCapacity'),
1233
+ nonce: requiredString(params, 'nonce'),
1234
+ ...(params.policy && typeof params.policy === 'object' ? { policy: params.policy } : {}),
1235
+ }, typeof params.now === 'number' ? params.now : Date.now());
1236
+ context.vtxoPools.set(pool.poolId, pool);
1237
+ return { success: true, pool: jsonSafe(pool) };
1238
+ };
1239
+ methods.set('totem_omniaCreateVtxoPool', createVtxoPoolMethod);
1240
+ methods.set('omnia/createVtxoPool', createVtxoPoolMethod);
1241
+ const mintVtxoMethod = async (raw) => {
1242
+ const params = paramsObject(raw);
1243
+ if (!context.vtxoPools || !context.vtxos || !context.vtxoReceipts)
1244
+ throw new Error('VTXO stores are not configured');
1245
+ const pool = context.vtxoPools.get(requiredString(params, 'poolId'));
1246
+ if (!pool)
1247
+ throw new Error(`VTXO pool ${String(params.poolId)} not found`);
1248
+ const minted = (0, omnia_vtxo_1.mintVtxo)(pool, {
1249
+ owner: requiredString(params, 'owner'),
1250
+ amount: requiredBigInt(params, 'amount'),
1251
+ nonce: requiredString(params, 'nonce'),
1252
+ expiresAt: typeof params.expiresAt === 'number' ? params.expiresAt : undefined,
1253
+ fundingProof: params.fundingProof,
1254
+ }, typeof params.now === 'number' ? params.now : undefined);
1255
+ context.vtxoPools.set(pool.poolId, minted.pool);
1256
+ context.vtxos.set(minted.vtxo.vtxoId, minted.vtxo);
1257
+ context.vtxoReceipts.set(minted.receipt.receiptId, minted.receipt);
1258
+ return { success: true, pool: jsonSafe(minted.pool), vtxo: jsonSafe(minted.vtxo), receipt: jsonSafe(minted.receipt) };
1259
+ };
1260
+ methods.set('totem_omniaMintVtxo', mintVtxoMethod);
1261
+ methods.set('omnia/mintVtxo', mintVtxoMethod);
1262
+ const transferVtxoMethod = async (raw) => {
1263
+ const params = paramsObject(raw);
1264
+ if (!context.vtxos || !context.vtxoReceipts)
1265
+ throw new Error('VTXO stores are not configured');
1266
+ const vtxo = context.vtxos.get(requiredString(params, 'vtxoId'));
1267
+ if (!vtxo)
1268
+ throw new Error(`VTXO ${String(params.vtxoId)} not found`);
1269
+ const result = (0, omnia_vtxo_1.transferVtxo)(vtxo, {
1270
+ recipient: requiredString(params, 'recipient'),
1271
+ amount: requiredBigInt(params, 'amount'),
1272
+ nonce: requiredString(params, 'nonce'),
1273
+ changeNonce: typeof params.changeNonce === 'string' ? params.changeNonce : undefined,
1274
+ }, typeof params.now === 'number' ? params.now : undefined);
1275
+ context.vtxos.set(result.input.vtxoId, result.input);
1276
+ context.vtxos.set(result.output.vtxoId, result.output);
1277
+ if (result.change)
1278
+ context.vtxos.set(result.change.vtxoId, result.change);
1279
+ context.vtxoReceipts.set(result.receipt.receiptId, result.receipt);
1280
+ return {
1281
+ success: true,
1282
+ input: jsonSafe(result.input),
1283
+ output: jsonSafe(result.output),
1284
+ change: result.change ? jsonSafe(result.change) : undefined,
1285
+ receipt: jsonSafe(result.receipt),
1286
+ };
1287
+ };
1288
+ methods.set('totem_omniaTransferVtxo', transferVtxoMethod);
1289
+ methods.set('omnia/transferVtxo', transferVtxoMethod);
1290
+ const refreshVtxoMethod = async (raw) => {
1291
+ const params = paramsObject(raw);
1292
+ if (!context.vtxos || !context.vtxoReceipts)
1293
+ throw new Error('VTXO stores are not configured');
1294
+ const vtxo = context.vtxos.get(requiredString(params, 'vtxoId'));
1295
+ if (!vtxo)
1296
+ throw new Error(`VTXO ${String(params.vtxoId)} not found`);
1297
+ const result = (0, omnia_vtxo_1.refreshVtxo)(vtxo, {
1298
+ newEpoch: Number(params.newEpoch),
1299
+ nonce: requiredString(params, 'nonce'),
1300
+ }, typeof params.now === 'number' ? params.now : undefined);
1301
+ context.vtxos.set(result.old.vtxoId, result.old);
1302
+ context.vtxos.set(result.refreshed.vtxoId, result.refreshed);
1303
+ if (context.vtxoPools) {
1304
+ const pool = context.vtxoPools.get(vtxo.poolId);
1305
+ if (pool)
1306
+ context.vtxoPools.set(pool.poolId, { ...pool, epoch: result.refreshed.epoch });
1307
+ }
1308
+ context.vtxoReceipts.set(result.receipt.receiptId, result.receipt);
1309
+ return { success: true, old: jsonSafe(result.old), refreshed: jsonSafe(result.refreshed), receipt: jsonSafe(result.receipt) };
1310
+ };
1311
+ methods.set('totem_omniaRefreshVtxo', refreshVtxoMethod);
1312
+ methods.set('omnia/refreshVtxo', refreshVtxoMethod);
1313
+ const initiateVtxoExitMethod = async (raw) => {
1314
+ const params = paramsObject(raw);
1315
+ if (!context.vtxos || !context.vtxoReceipts)
1316
+ throw new Error('VTXO stores are not configured');
1317
+ const vtxo = context.vtxos.get(requiredString(params, 'vtxoId'));
1318
+ if (!vtxo)
1319
+ throw new Error(`VTXO ${String(params.vtxoId)} not found`);
1320
+ const now = typeof params.now === 'number' ? params.now : undefined;
1321
+ const draft = (0, omnia_vtxo_1.createExitDraft)(vtxo, now);
1322
+ const exiting = (0, omnia_vtxo_1.markExiting)(vtxo, now);
1323
+ context.vtxos.set(exiting.vtxoId, exiting);
1324
+ context.vtxoReceipts.set(draft.receipt.receiptId, draft.receipt);
1325
+ return { success: true, vtxo: jsonSafe(exiting), draft: jsonSafe(draft.draft), receipt: jsonSafe(draft.receipt) };
1326
+ };
1327
+ methods.set('totem_omniaInitiateVtxoExit', initiateVtxoExitMethod);
1328
+ methods.set('omnia/initiateVtxoExit', initiateVtxoExitMethod);
1329
+ const completeVtxoExitMethod = async (raw) => {
1330
+ const params = paramsObject(raw);
1331
+ if (!context.vtxos)
1332
+ throw new Error('VTXO stores are not configured');
1333
+ const vtxo = context.vtxos.get(requiredString(params, 'vtxoId'));
1334
+ if (!vtxo)
1335
+ throw new Error(`VTXO ${String(params.vtxoId)} not found`);
1336
+ const exited = (0, omnia_vtxo_1.markExited)(vtxo, {
1337
+ txpowId: typeof params.txpowId === 'string' ? params.txpowId : undefined,
1338
+ now: typeof params.now === 'number' ? params.now : undefined,
1339
+ });
1340
+ context.vtxos.set(exited.vtxoId, exited);
1341
+ return { success: true, vtxo: jsonSafe(exited) };
1342
+ };
1343
+ methods.set('totem_omniaCompleteVtxoExit', completeVtxoExitMethod);
1344
+ methods.set('omnia/completeVtxoExit', completeVtxoExitMethod);
1345
+ const prepareVtxoReceipt = async (raw) => {
1346
+ const params = paramsObject(raw);
1347
+ if (!context.vtxoReceipts || !context.vtxos || !context.preparedVtxoReceipts) {
1348
+ throw new Error('VTXO stores are not configured');
1349
+ }
1350
+ const localPkd = context.localParticipant?.publicKeyDigest;
1351
+ if (!localPkd)
1352
+ throw new Error('Local participant is not configured');
1353
+ const receipt = context.vtxoReceipts.get(requiredString(params, 'receiptId'));
1354
+ if (!receipt)
1355
+ throw new Error(`VTXO receipt ${String(params.receiptId)} not found`);
1356
+ const outputs = receipt.outputIds.map((id) => {
1357
+ const vtxo = context.vtxos.get(id);
1358
+ if (!vtxo)
1359
+ throw new Error(`VTXO ${id} referenced by receipt is not stored`);
1360
+ return vtxo;
1361
+ });
1362
+ const root = typeof params.root === 'string' ? params.root : outputs[0]?.proof.root;
1363
+ const epoch = typeof params.epoch === 'number' ? params.epoch : receipt.epoch;
1364
+ if (!root)
1365
+ throw new Error('VTXO receipt root is required');
1366
+ const payload = (0, omnia_vtxo_1.buildVtxoReceiptPayload)(receipt, {
1367
+ outputs,
1368
+ root,
1369
+ epoch,
1370
+ previousEpoch: typeof params.previousEpoch === 'number' ? params.previousEpoch : undefined,
1371
+ inputsAmount: params.inputsAmount !== undefined ? BigInt(String(params.inputsAmount)) : undefined,
1372
+ });
1373
+ const request = {
1374
+ requestId: `vtxo-receipt-${receipt.receiptId}`,
1375
+ kind: 'chain',
1376
+ domain: 'vtxo',
1377
+ digest: payload,
1378
+ context: { summary: `Co-sign VTXO ${receipt.op} receipt` },
1379
+ signers: [localPkd],
1380
+ custody: (0, omnia_1.makeSelfCustody)(),
1381
+ expiresAt: Date.now() + 120_000,
1382
+ };
1383
+ const expiresAt = Date.now() + 120_000;
1384
+ context.preparedVtxoReceipts.set(receipt.receiptId, { receipt, payload, request, expiresAt });
1385
+ return { success: true, status: 'signature-required', receiptId: receipt.receiptId, requests: { receipt: request }, expiresAt };
1386
+ };
1387
+ methods.set('totem_omniaPrepareVtxoReceipt', prepareVtxoReceipt);
1388
+ methods.set('omnia/prepareVtxoReceipt', prepareVtxoReceipt);
1389
+ const submitVtxoReceipt = async (raw) => {
1390
+ const params = paramsObject(raw);
1391
+ const receiptId = requiredString(params, 'receiptId');
1392
+ const localPkd = context.localParticipant?.publicKeyDigest;
1393
+ if (!localPkd)
1394
+ throw new Error('Local participant is not configured');
1395
+ if (!context.preparedVtxoReceipts || !context.vtxoReceipts)
1396
+ throw new Error('VTXO stores are not configured');
1397
+ const entry = context.preparedVtxoReceipts.get(receiptId);
1398
+ if (!entry)
1399
+ throw new Error(`Prepared VTXO receipt ${receiptId} not found`);
1400
+ if (entry.expiresAt <= Date.now()) {
1401
+ context.preparedVtxoReceipts.delete(receiptId);
1402
+ throw new Error(`Prepared VTXO receipt ${receiptId} has expired`);
1403
+ }
1404
+ const signature = hexBytes(params.signature, 'signature');
1405
+ if (!(0, omnia_1.verifyChannelSignature)(signature, entry.payload, localPkd)) {
1406
+ throw new Error('VTXO receipt signature does not verify');
1407
+ }
1408
+ const signed = { ...entry.receipt, signature: '0x' + Buffer.from(signature).toString('hex') };
1409
+ context.vtxoReceipts.set(receiptId, signed);
1410
+ context.preparedVtxoReceipts.delete(receiptId);
1411
+ return { success: true, receipt: jsonSafe(signed) };
1412
+ };
1413
+ methods.set('totem_omniaSubmitVtxoReceipt', submitVtxoReceipt);
1414
+ methods.set('omnia/submitVtxoReceipt', submitVtxoReceipt);
1415
+ const createLiquidityPool = async (raw) => {
1416
+ const params = paramsObject(raw);
1417
+ if (!context.pools)
1418
+ throw new Error('Pool stores are not configured');
1419
+ const created = (0, omnia_pool_1.createOmniaPool)({
1420
+ poolId: requiredString(params, 'poolId'),
1421
+ operatorAddress: requiredString(params, 'operatorAddress'),
1422
+ tokenId: typeof params.tokenId === 'string' ? params.tokenId : '0x00',
1423
+ purpose: requiredString(params, 'purpose'),
1424
+ poolType: requiredString(params, 'poolType'),
1425
+ capacity: String(requiredBigInt(params, 'capacity')),
1426
+ feePolicy: params.feePolicy,
1427
+ riskPolicy: params.riskPolicy,
1428
+ lockTerms: params.lockTerms,
1429
+ operatorBond: params.operatorBond,
1430
+ metadata: params.metadata,
1431
+ }, (0, liquidity_bond_1.createEmptyLiquidityBondRegistryState)());
1432
+ context.pools.set(created.manifest.poolId, { manifest: created.manifest, registry: created.registry });
1433
+ return { success: true, pool: jsonSafe(created.manifest), registry: jsonSafe(created.registry) };
1434
+ };
1435
+ methods.set('totem_omniaCreateLiquidityPool', createLiquidityPool);
1436
+ methods.set('omnia/createLiquidityPool', createLiquidityPool);
1437
+ const depositToLiquidityPool = async (raw) => {
1438
+ const params = paramsObject(raw);
1439
+ if (!context.pools || !context.poolPositions)
1440
+ throw new Error('Pool stores are not configured');
1441
+ const poolId = requiredString(params, 'poolId');
1442
+ const stored = context.pools.get(poolId);
1443
+ if (!stored)
1444
+ throw new Error(`Pool ${poolId} not found`);
1445
+ const verifier = {
1446
+ verifyDeposit: async (request) => {
1447
+ if (!context.chainProvider)
1448
+ return { valid: false, reason: 'no chain provider' };
1449
+ const coin = await context.chainProvider.getCoin(request.coinId);
1450
+ if (!coin)
1451
+ return { valid: false, reason: 'coin not found' };
1452
+ if (coin.spent)
1453
+ return { valid: false, reason: 'coin is spent' };
1454
+ if (request.tokenId && coin.tokenid !== request.tokenId)
1455
+ return { valid: false, reason: 'token mismatch' };
1456
+ if (request.claimedAmount && BigInt(coin.amount) < BigInt(request.claimedAmount)) {
1457
+ return { valid: false, reason: 'insufficient amount' };
1458
+ }
1459
+ return { valid: true };
1460
+ },
1461
+ };
1462
+ const deposited = await (0, omnia_pool_1.depositToPool)({
1463
+ pool: stored.manifest,
1464
+ lpAddress: requiredString(params, 'lpAddress'),
1465
+ amount: String(requiredBigInt(params, 'amount')),
1466
+ purpose: requiredString(params, 'purpose'),
1467
+ underlyingUtxoRef: typeof params.underlyingUtxoRef === 'string' ? params.underlyingUtxoRef : undefined,
1468
+ underlyingRefs: params.underlyingRefs,
1469
+ metadata: params.metadata,
1470
+ chainProvider: verifier,
1471
+ }, stored.registry);
1472
+ context.pools.set(poolId, { manifest: deposited.pool, registry: deposited.state });
1473
+ context.poolPositions.set(deposited.position.positionId, { position: deposited.position, registry: deposited.state });
1474
+ return {
1475
+ success: true,
1476
+ poolId,
1477
+ position: jsonSafe(deposited.position),
1478
+ receipt: jsonSafe(deposited.receipt),
1479
+ registry: jsonSafe(deposited.state),
1480
+ };
1481
+ };
1482
+ methods.set('totem_omniaDepositToPool', depositToLiquidityPool);
1483
+ methods.set('omnia/depositToPool', depositToLiquidityPool);
1484
+ const allocatePoolCapital = async (raw) => {
1485
+ const params = paramsObject(raw);
1486
+ if (!context.poolPositions)
1487
+ throw new Error('Pool stores are not configured');
1488
+ const positionId = requiredString(params, 'positionId');
1489
+ const stored = context.poolPositions.get(positionId);
1490
+ if (!stored)
1491
+ throw new Error(`Pool position ${positionId} not found`);
1492
+ const allocated = await (0, omnia_pool_1.allocatePositionCapital)({
1493
+ position: stored.position,
1494
+ amount: String(requiredBigInt(params, 'amount')),
1495
+ allocationType: requiredString(params, 'allocationType'),
1496
+ purpose: requiredString(params, 'purpose'),
1497
+ target: params.target,
1498
+ metadata: params.metadata,
1499
+ }, stored.registry);
1500
+ context.poolPositions.set(positionId, { ...stored, position: allocated.position, registry: allocated.registry, allocation: allocated.allocation });
1501
+ return {
1502
+ success: true,
1503
+ position: jsonSafe(allocated.position),
1504
+ allocation: jsonSafe(allocated.allocation),
1505
+ registry: jsonSafe(allocated.registry),
1506
+ };
1507
+ };
1508
+ methods.set('totem_omniaAllocatePoolCapital', allocatePoolCapital);
1509
+ methods.set('omnia/allocatePoolCapital', allocatePoolCapital);
1510
+ const releasePoolAllocation = async (raw) => {
1511
+ const params = paramsObject(raw);
1512
+ if (!context.poolPositions)
1513
+ throw new Error('Pool stores are not configured');
1514
+ const positionId = requiredString(params, 'positionId');
1515
+ const stored = context.poolPositions.get(positionId);
1516
+ if (!stored)
1517
+ throw new Error(`Pool position ${positionId} not found`);
1518
+ if (!stored.allocation)
1519
+ throw new Error(`Pool position ${positionId} has no allocation`);
1520
+ const released = (0, omnia_pool_1.releaseAllocation)({ allocation: stored.allocation, position: stored.position }, stored.registry);
1521
+ context.poolPositions.set(positionId, { ...stored, position: released.position, registry: released.registry, allocation: undefined });
1522
+ return { success: true, position: jsonSafe(released.position), registry: jsonSafe(released.registry) };
1523
+ };
1524
+ methods.set('totem_omniaReleasePoolAllocation', releasePoolAllocation);
1525
+ methods.set('omnia/releasePoolAllocation', releasePoolAllocation);
1526
+ const withdrawPoolLiquidity = async (raw) => {
1527
+ const params = paramsObject(raw);
1528
+ if (!context.pools || !context.poolPositions)
1529
+ throw new Error('Pool stores are not configured');
1530
+ const positionId = requiredString(params, 'positionId');
1531
+ const stored = context.poolPositions.get(positionId);
1532
+ if (!stored)
1533
+ throw new Error(`Pool position ${positionId} not found`);
1534
+ const pool = context.pools.get(stored.position.poolId);
1535
+ if (!pool)
1536
+ throw new Error(`Pool ${stored.position.poolId} not found`);
1537
+ const withdrawn = (0, omnia_pool_1.withdrawLiquidity)(pool.manifest, stored.position, {
1538
+ positionId,
1539
+ amount: String(requiredBigInt(params, 'amount')),
1540
+ recipientAddress: requiredString(params, 'recipientAddress'),
1541
+ reason: typeof params.reason === 'string' ? params.reason : undefined,
1542
+ }, stored.registry);
1543
+ context.poolPositions.set(positionId, { ...stored, position: withdrawn.position, registry: withdrawn.state, intent: withdrawn.intent });
1544
+ return { success: true, intent: jsonSafe(withdrawn.intent), position: jsonSafe(withdrawn.position), registry: jsonSafe(withdrawn.state) };
1545
+ };
1546
+ methods.set('totem_omniaWithdrawPool', withdrawPoolLiquidity);
1547
+ methods.set('omnia/withdrawPool', withdrawPoolLiquidity);
1548
+ const approvePoolWithdrawal = async (raw) => {
1549
+ const params = paramsObject(raw);
1550
+ if (!context.poolPositions)
1551
+ throw new Error('Pool stores are not configured');
1552
+ const positionId = requiredString(params, 'positionId');
1553
+ const stored = context.poolPositions.get(positionId);
1554
+ if (!stored)
1555
+ throw new Error(`Pool position ${positionId} not found`);
1556
+ if (!stored.intent)
1557
+ throw new Error(`Pool position ${positionId} has no withdrawal intent`);
1558
+ const approved = (0, omnia_pool_1.approveWithdrawal)(stored.intent, stored.position, stored.registry);
1559
+ context.poolPositions.set(positionId, { ...stored, registry: approved.registry, intent: approved.intent });
1560
+ return { success: true, intent: jsonSafe(approved.intent), registry: jsonSafe(approved.registry) };
1561
+ };
1562
+ methods.set('totem_omniaApprovePoolWithdrawal', approvePoolWithdrawal);
1563
+ methods.set('omnia/approvePoolWithdrawal', approvePoolWithdrawal);
1564
+ const executePoolPayoutMethod = async (raw) => {
1565
+ const params = paramsObject(raw);
1566
+ if (!context.pools || !context.poolPositions)
1567
+ throw new Error('Pool stores are not configured');
1568
+ const positionId = requiredString(params, 'positionId');
1569
+ const stored = context.poolPositions.get(positionId);
1570
+ if (!stored)
1571
+ throw new Error(`Pool position ${positionId} not found`);
1572
+ if (!stored.intent)
1573
+ throw new Error(`Pool position ${positionId} has no withdrawal intent`);
1574
+ const pool = context.pools.get(stored.position.poolId);
1575
+ if (!pool)
1576
+ throw new Error(`Pool ${stored.position.poolId} not found`);
1577
+ const signIndices = params.signIndices ? signingIndicesParam(params.signIndices) : undefined;
1578
+ if (signIndices && !context.signer)
1579
+ throw new Error('Pool payout signing requires a signer');
1580
+ const paid = await (0, omnia_pool_1.executePoolPayout)({
1581
+ pool: pool.manifest,
1582
+ position: stored.position,
1583
+ intent: stored.intent,
1584
+ recipientAddress: requiredString(params, 'recipientAddress'),
1585
+ settlement: params.settlement,
1586
+ ...(signIndices && context.signer
1587
+ ? { rooting: { signer: context.signer, signIndices } }
1588
+ : {}),
1589
+ }, stored.registry);
1590
+ context.poolPositions.set(positionId, {
1591
+ ...stored,
1592
+ position: paid.position,
1593
+ registry: paid.registry,
1594
+ intent: paid.intent,
1595
+ });
1596
+ return {
1597
+ success: true,
1598
+ intent: jsonSafe(paid.intent),
1599
+ position: jsonSafe(paid.position),
1600
+ registry: jsonSafe(paid.registry),
1601
+ signedTransition: paid.signedTransition ? jsonSafe(paid.signedTransition) : undefined,
1602
+ };
1603
+ };
1604
+ methods.set('totem_omniaExecutePoolPayout', executePoolPayoutMethod);
1605
+ methods.set('omnia/executePoolPayout', executePoolPayoutMethod);
1606
+ }
241
1607
  function createHostMethods(context) {
242
1608
  const methods = new Map();
243
1609
  const register = (name, handler, alias) => {
244
1610
  methods.set(name, handler);
245
1611
  methods.set(alias, handler);
246
1612
  };
1613
+ const observeMethods = (map) => {
1614
+ const emitter = context.observe;
1615
+ if (!emitter)
1616
+ return map;
1617
+ const observed = new Map();
1618
+ const isQuiet = (name) => name.startsWith('totem_omniaGet') || name === 'totem_omniaWhoami' ||
1619
+ name === 'totem_omniaGetManifest';
1620
+ for (const [name, handler] of map) {
1621
+ observed.set(name, async (params) => {
1622
+ if (isQuiet(name))
1623
+ return handler(params);
1624
+ const startedAt = Date.now();
1625
+ const raw = paramsObject(params);
1626
+ const opId = typeof raw.operationId === 'string' ? raw.operationId : undefined;
1627
+ void emitter.emit({
1628
+ domain: 'command', kind: 'command.invoked', phase: 'observed',
1629
+ entity: { type: 'command', id: name },
1630
+ ...(opId ? { operationId: opId } : {}),
1631
+ data: { method: name, params },
1632
+ });
1633
+ try {
1634
+ const result = await handler(params);
1635
+ void emitter.emit({
1636
+ domain: 'command', kind: 'command.completed', phase: 'committed',
1637
+ entity: { type: 'command', id: name },
1638
+ ...(opId ? { operationId: opId } : {}),
1639
+ data: { method: name, durationMs: Date.now() - startedAt, result },
1640
+ });
1641
+ return result;
1642
+ }
1643
+ catch (error) {
1644
+ void emitter.emit({
1645
+ domain: 'command', kind: 'command.failed', phase: 'failed',
1646
+ entity: { type: 'command', id: name },
1647
+ ...(opId ? { operationId: opId } : {}),
1648
+ data: { method: name, durationMs: Date.now() - startedAt, error: error instanceof Error ? error.message : String(error) },
1649
+ });
1650
+ throw error;
1651
+ }
1652
+ });
1653
+ }
1654
+ return observed;
1655
+ };
247
1656
  const getChannels = (raw) => {
248
1657
  const params = paramsObject(raw);
249
1658
  const tokenId = typeof params.tokenId === 'string' ? params.tokenId : undefined;
@@ -264,12 +1673,23 @@ function createHostMethods(context) {
264
1673
  return channelStateView(channel);
265
1674
  };
266
1675
  register('totem_omniaGetChannelState', getChannelState, 'omnia/getChannelState');
1676
+ const normalizeRouteNode = (value) => {
1677
+ const lower = value.toLowerCase();
1678
+ for (const channel of context.channels.values()) {
1679
+ for (const party of channel.parties) {
1680
+ if (party.partyId === value || party.publicKeyDigest.toLowerCase() === lower) {
1681
+ return party.publicKeyDigest;
1682
+ }
1683
+ }
1684
+ }
1685
+ return value;
1686
+ };
267
1687
  const getRoute = async (raw) => {
268
1688
  context.refreshRouting?.();
269
1689
  const params = paramsObject(raw);
270
1690
  const route = await context.routing.getRoute({
271
- from: requiredString(params, 'fromPartyId'),
272
- to: requiredString(params, 'toPartyId'),
1691
+ from: normalizeRouteNode(requiredString(params, 'fromPartyId')),
1692
+ to: normalizeRouteNode(requiredString(params, 'toPartyId')),
273
1693
  amount: requiredBigInt(params, 'amount'),
274
1694
  tokenId: requiredString(params, 'tokenId'),
275
1695
  targetTokenId: typeof params.targetTokenId === 'string' ? params.targetTokenId : undefined,
@@ -304,7 +1724,10 @@ function createHostMethods(context) {
304
1724
  address: context.identity.address,
305
1725
  publicKeyDigest: context.identity.publicKeyDigest,
306
1726
  identityId: context.identity.identityId,
1727
+ settlementAddress: context.localParticipant?.settlementAddress,
307
1728
  delegation: context.identity.delegation,
1729
+ custodyMode: context.custodyMode ?? 'self',
1730
+ p2pHubUrl: context.p2pHubUrl ?? null,
308
1731
  };
309
1732
  };
310
1733
  register('totem_omniaWhoami', whoami, 'omnia/whoami');
@@ -317,8 +1740,14 @@ function createHostMethods(context) {
317
1740
  register('totem_omniaGetManifest', getManifest, 'omnia/getManifest');
318
1741
  // Fail-fast rule: without signing material only the read-only surface is
319
1742
  // registered. Mutation methods must never exist without a signer.
1743
+ // Coordinator mode: no host signing material. Expose the read surface plus
1744
+ // the non-signing prepare/submit flow; wallets supply signatures and indices.
1745
+ if (context.coordinator && !context.readOnly) {
1746
+ registerPreparedPayMethods(methods, context);
1747
+ return observeMethods(methods);
1748
+ }
320
1749
  if (context.readOnly || !context.signer || !context.leaseProvider) {
321
- return methods;
1750
+ return observeMethods(methods);
322
1751
  }
323
1752
  const openChannel = async (raw) => {
324
1753
  const params = paramsObject(raw);
@@ -379,7 +1808,26 @@ function createHostMethods(context) {
379
1808
  if (typeof params.fundingScript === 'string') {
380
1809
  createParams.fundingScript = params.fundingScript;
381
1810
  }
382
- createParams.fundingSigner = signer;
1811
+ // If the caller supplied the funding coin's public key, the node's
1812
+ // `sign` command is authoritative for that input. The host channel key
1813
+ // must not sign someone else's funding coin.
1814
+ createParams.fundingSigner = !fundingPubkey && context.signerPort && (context.custodyMode ?? 'self') !== 'hosted'
1815
+ ? async (digest) => context.signerPort.sign({
1816
+ requestId: `channel-funding-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
1817
+ kind: 'chain',
1818
+ domain: 'channel',
1819
+ digest,
1820
+ context: { summary: 'Channel funding input' },
1821
+ signers: [context.signerPort.publicKeyDigest],
1822
+ custody: (0, omnia_1.makeSelfCustody)(),
1823
+ expiresAt: Date.now() + 60_000,
1824
+ })
1825
+ : signer;
1826
+ }
1827
+ // RFC-035: establish the peer transport before broadcasting funding.
1828
+ // Otherwise a DHT timeout leaves the funding coin spent with no channel.
1829
+ if (context.swarm) {
1830
+ await context.swarm.connectToPeer(remote.publicKeyDigest, `omnia-open-preflight-${remotePartyId}`, remote.transportPubkey);
383
1831
  }
384
1832
  const created = await (0, omnia_1.createChannel)(createParams, context.chainProvider);
385
1833
  context.channels.set(created.channel.channelId, {
@@ -393,6 +1841,17 @@ function createHostMethods(context) {
393
1841
  const reply = await requestPeerReply(peer, { type: 'CHANNEL_PROPOSAL', channelId: created.channel.channelId, nonce: Date.now(), payload: created.proposal }, created.channel.channelId, 20_000);
394
1842
  acknowledged = reply?.type === 'ACK';
395
1843
  }
1844
+ await context.observe?.emit({
1845
+ domain: 'channel', kind: 'channel.opened', phase: 'accepted',
1846
+ entity: { type: 'channel', id: created.channel.channelId },
1847
+ parties: { local: context.localParticipant?.partyId, remote: remotePartyId },
1848
+ token: { tokenId: created.channel.tokenId, scale: created.channel.tokenScale ?? 0 },
1849
+ amounts: { capacity: created.channel.totalValue.toString(), balances: balancesJson(created.channel) },
1850
+ chain: { fundingCoinId: created.channel.fundingCoinId, txpowId: created.channel.fundingTxId },
1851
+ transitionHash: (0, emitter_js_1.channelTransitionHash)(created.channel),
1852
+ ...(typeof params.operationId === 'string' ? { operationId: params.operationId } : {}),
1853
+ data: { acknowledged },
1854
+ });
396
1855
  return { success: true, channelId: created.channel.channelId, fundingTxId: created.channel.fundingTxId, acknowledged };
397
1856
  });
398
1857
  return result;
@@ -413,16 +1872,28 @@ function createHostMethods(context) {
413
1872
  throw new Error('Channel counterparty not found');
414
1873
  if (amount <= 0n || (channel.balances[localPartyId] ?? 0n) < amount)
415
1874
  throw new Error('Insufficient channel balance');
1875
+ const beforeBalances = balancesJson(channel);
416
1876
  const result = await (0, omnia_1.updateState)(channel, {
417
1877
  newBalances: {
418
1878
  ...channel.balances,
419
1879
  [localPartyId]: (channel.balances[localPartyId] ?? 0n) - amount,
420
1880
  [remotePartyId]: (channel.balances[remotePartyId] ?? 0n) + amount,
421
1881
  },
422
- }, context.leaseProvider, context.signer);
1882
+ }, context.leaseProvider, context.signer, context.signerPort);
423
1883
  if (result.error)
424
1884
  throw new Error(result.error);
425
1885
  context.channels.set(channelId, result.channel);
1886
+ await context.observe?.emit({
1887
+ domain: 'channel', kind: 'channel.updated', phase: 'committed',
1888
+ entity: { type: 'channel', id: channelId },
1889
+ parties: { local: localPartyId, remote: remotePartyId },
1890
+ token: { tokenId: channel.tokenId, scale: channel.tokenScale ?? 0 },
1891
+ before: { balances: beforeBalances, sequence: channel.currentSequence },
1892
+ after: { balances: balancesJson(result.channel), sequence: result.channel.currentSequence },
1893
+ amounts: { amountAtoms: amount.toString(), balances: balancesJson(result.channel) },
1894
+ transitionHash: (0, emitter_js_1.channelTransitionHash)(result.channel),
1895
+ ...(typeof params.operationId === 'string' ? { operationId: params.operationId } : {}),
1896
+ });
426
1897
  const remoteParty = channel.parties.find((party) => party.partyId === remotePartyId);
427
1898
  const peer = context.swarm ? await context.swarm.connectToPeer(remoteParty.publicKeyDigest, channelId, remoteParty.transportPubkey) : undefined;
428
1899
  if (peer)
@@ -431,6 +1902,98 @@ function createHostMethods(context) {
431
1902
  });
432
1903
  };
433
1904
  register('totem_omniaPay', pay, 'omnia/pay');
1905
+ const sendHtlcState = async (channel, state) => {
1906
+ const localPartyId = context.localParticipant.partyId;
1907
+ const remote = channel.parties.find((party) => party.partyId !== localPartyId);
1908
+ if (!context.swarm || !remote)
1909
+ return;
1910
+ const peer = context.channelPeers?.get(channel.channelId)
1911
+ ?? await context.swarm.connectToPeer(remote.publicKeyDigest, channel.channelId, remote.transportPubkey);
1912
+ await peer.sendMessage({ type: 'STATE_UPDATE', channelId: channel.channelId, nonce: Date.now(), payload: { ...state, pendingHTLCs: channel.pendingHTLCs } });
1913
+ };
1914
+ const addHtlcMethod = async (raw) => {
1915
+ const params = paramsObject(raw);
1916
+ const channelId = requiredString(params, 'channelId');
1917
+ requireMutationContext(context);
1918
+ const result = await withOperation(context, params, async () => {
1919
+ const channel = context.channels.get(channelId);
1920
+ if (!channel)
1921
+ throw new Error(`Channel ${channelId} not found`);
1922
+ const added = await (0, omnia_1.addHTLC)(channel, {
1923
+ amount: requiredBigInt(params, 'amount'),
1924
+ hashlock: requiredString(params, 'hashlock'),
1925
+ timeoutBlock: requiredBigInt(params, 'timeoutBlock'),
1926
+ direction: (typeof params.direction === 'string' ? params.direction : 'offered'),
1927
+ counterpartPublicKeyDigest: requiredString(params, 'counterpartPublicKeyDigest'),
1928
+ }, context.leaseProvider, context.signer);
1929
+ if (added.error)
1930
+ throw new Error(added.error);
1931
+ context.channels.set(channelId, added.channel);
1932
+ await context.observe?.emit({
1933
+ domain: 'payment', kind: 'htlc.added', phase: 'committed',
1934
+ entity: { type: 'channel', id: channelId },
1935
+ amounts: { amountAtoms: requiredBigInt(params, 'amount').toString(), balances: balancesJson(added.channel) },
1936
+ transitionHash: (0, emitter_js_1.channelTransitionHash)(added.channel),
1937
+ ...(typeof params.operationId === 'string' ? { operationId: params.operationId } : {}),
1938
+ data: { htlcId: added.htlcId, hashlock: params.hashlock, timeoutBlock: String(params.timeoutBlock), direction: params.direction },
1939
+ });
1940
+ await sendHtlcState(added.channel, added.partialState);
1941
+ return { success: true, channelId, htlcId: added.htlcId, sequence: added.channel.currentSequence };
1942
+ });
1943
+ return result;
1944
+ };
1945
+ register('totem_omniaAddHTLC', addHtlcMethod, 'omnia/addHTLC');
1946
+ const fulfillHtlcMethod = async (raw) => {
1947
+ const params = paramsObject(raw);
1948
+ const channelId = requiredString(params, 'channelId');
1949
+ requireMutationContext(context);
1950
+ return withOperation(context, params, async () => {
1951
+ const channel = context.channels.get(channelId);
1952
+ if (!channel)
1953
+ throw new Error(`Channel ${channelId} not found`);
1954
+ const fulfilled = await (0, omnia_1.fulfillHTLC)(channel, requiredString(params, 'htlcId'), requiredString(params, 'preimage'), context.leaseProvider, context.signer);
1955
+ if (fulfilled.error)
1956
+ throw new Error(fulfilled.error);
1957
+ context.channels.set(channelId, fulfilled.channel);
1958
+ await context.observe?.emit({
1959
+ domain: 'payment', kind: 'htlc.fulfilled', phase: 'committed',
1960
+ entity: { type: 'channel', id: channelId },
1961
+ amounts: { balances: balancesJson(fulfilled.channel) },
1962
+ transitionHash: (0, emitter_js_1.channelTransitionHash)(fulfilled.channel),
1963
+ ...(typeof params.operationId === 'string' ? { operationId: params.operationId } : {}),
1964
+ data: { htlcId: params.htlcId },
1965
+ });
1966
+ await sendHtlcState(fulfilled.channel, fulfilled.partialState);
1967
+ return { success: true, channelId, sequence: fulfilled.channel.currentSequence };
1968
+ });
1969
+ };
1970
+ register('totem_omniaFulfillHTLC', fulfillHtlcMethod, 'omnia/fulfillHTLC');
1971
+ const timeoutHtlcMethod = async (raw) => {
1972
+ const params = paramsObject(raw);
1973
+ const channelId = requiredString(params, 'channelId');
1974
+ requireMutationContext(context);
1975
+ return withOperation(context, params, async () => {
1976
+ const channel = context.channels.get(channelId);
1977
+ if (!channel)
1978
+ throw new Error(`Channel ${channelId} not found`);
1979
+ const timedOut = await (0, omnia_1.timeoutHTLC)(channel, requiredString(params, 'htlcId'), context.leaseProvider, context.chainProvider, context.signer);
1980
+ if (timedOut.error)
1981
+ throw new Error(timedOut.error);
1982
+ context.channels.set(channelId, timedOut.channel);
1983
+ await context.observe?.emit({
1984
+ domain: 'payment', kind: 'htlc.timed_out', phase: 'recovered',
1985
+ entity: { type: 'channel', id: channelId },
1986
+ amounts: { balances: balancesJson(timedOut.channel) },
1987
+ transitionHash: (0, emitter_js_1.channelTransitionHash)(timedOut.channel),
1988
+ ...(typeof params.operationId === 'string' ? { operationId: params.operationId } : {}),
1989
+ data: { htlcId: params.htlcId },
1990
+ });
1991
+ await sendHtlcState(timedOut.channel, timedOut.partialState);
1992
+ return { success: true, channelId, sequence: timedOut.channel.currentSequence };
1993
+ });
1994
+ };
1995
+ register('totem_omniaTimeoutHTLC', timeoutHtlcMethod, 'omnia/timeoutHTLC');
1996
+ registerPreparedPayMethods(methods, context);
434
1997
  const programTransition = async (raw) => {
435
1998
  const params = paramsObject(raw);
436
1999
  requireMutationContext(context);
@@ -523,6 +2086,14 @@ function createHostMethods(context) {
523
2086
  counterpartySignatures,
524
2087
  });
525
2088
  context.channels.set(channelId, (0, omnia_1.markChannelClosed)(closing));
2089
+ await context.observe?.emit({
2090
+ domain: 'settlement', kind: 'settlement.committed', phase: 'confirmed',
2091
+ entity: { type: 'channel', id: channelId },
2092
+ chain: { txpowId: settlement.settlementPayload.txpowId },
2093
+ amounts: { balances: Object.fromEntries(Object.entries(settlement.settlementPayload.balances).map(([k, v]) => [k, v.toString()])) },
2094
+ transitionHash: (0, emitter_js_1.hashTransition)(['settlement', channelId, settlement.settlementPayload.txpowId]),
2095
+ ...(typeof params.operationId === 'string' ? { operationId: params.operationId } : {}),
2096
+ });
526
2097
  return { success: true, channelId, settlementTxId: settlement.settlementPayload.txpowId, finalBalances: Object.fromEntries(Object.entries(settlement.settlementPayload.balances).map(([key, value]) => [key, value.toString()])) };
527
2098
  });
528
2099
  };
@@ -535,6 +2106,12 @@ function createHostMethods(context) {
535
2106
  if (!channel)
536
2107
  throw new Error(`Channel ${channelId} not found`);
537
2108
  context.channels.set(channelId, (0, omnia_1.markChannelClosed)(channel));
2109
+ await context.observe?.emit({
2110
+ domain: 'channel', kind: 'channel.closed', phase: 'closed',
2111
+ entity: { type: 'channel', id: channelId },
2112
+ transitionHash: (0, emitter_js_1.channelTransitionHash)((0, omnia_1.markChannelClosed)(channel)),
2113
+ ...(typeof params.operationId === 'string' ? { operationId: params.operationId } : {}),
2114
+ });
538
2115
  return { success: true, channelId };
539
2116
  });
540
2117
  };
@@ -567,24 +2144,38 @@ function createHostMethods(context) {
567
2144
  if (!localBundle)
568
2145
  throw new Error('No local factory lease bundle');
569
2146
  const remote = parsed.find((participant) => participant.partyId !== localPartyId);
570
- const localSigner = context.signer && context.leaseProvider
2147
+ const localSigner = context.signerPort && (context.custodyMode ?? 'self') !== 'hosted'
571
2148
  ? async (digest) => {
572
- const reservation = await context.leaseProvider.reserveKeyUse({
573
- treeId: 'factory-funding',
574
- purpose: 'factory-funding',
575
- payloadHash: '0x' + Buffer.from(digest).toString('hex'),
576
- });
577
- try {
578
- const sig = await context.signer.sign(digest, reservation.indices);
579
- await context.leaseProvider.commitKeyUse(reservation.reservationId, 'factory-funding');
580
- return sig;
581
- }
582
- catch (err) {
583
- await context.leaseProvider.burnReservation(reservation.reservationId, 'factory-funding-failed').catch(() => { });
584
- throw err;
585
- }
2149
+ const request = {
2150
+ requestId: `factory-funding-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
2151
+ kind: 'chain',
2152
+ domain: 'factory',
2153
+ digest,
2154
+ context: { summary: 'Factory funding input' },
2155
+ signers: [context.signerPort.publicKeyDigest],
2156
+ custody: (0, omnia_1.makeSelfCustody)(),
2157
+ expiresAt: Date.now() + 60_000,
2158
+ };
2159
+ return context.signerPort.sign(request);
586
2160
  }
587
- : undefined;
2161
+ : context.signer && context.leaseProvider
2162
+ ? async (digest) => {
2163
+ const reservation = await context.leaseProvider.reserveKeyUse({
2164
+ treeId: 'factory-funding',
2165
+ purpose: 'factory-funding',
2166
+ payloadHash: '0x' + Buffer.from(digest).toString('hex'),
2167
+ });
2168
+ try {
2169
+ const sig = await context.signer.sign(digest, reservation.indices);
2170
+ await context.leaseProvider.commitKeyUse(reservation.reservationId, 'factory-funding');
2171
+ return sig;
2172
+ }
2173
+ catch (err) {
2174
+ await context.leaseProvider.burnReservation(reservation.reservationId, 'factory-funding-failed').catch(() => { });
2175
+ throw err;
2176
+ }
2177
+ }
2178
+ : undefined;
588
2179
  let fundingPeer;
589
2180
  const remoteSigner = context.swarm && remote
590
2181
  ? async (digest) => {
@@ -656,6 +2247,14 @@ function createHostMethods(context) {
656
2247
  factory = accepted;
657
2248
  }
658
2249
  context.factories.set(factory.factoryId, factory);
2250
+ await context.observe?.emit({
2251
+ domain: 'factory', kind: 'factory.opened', phase: 'accepted',
2252
+ entity: { type: 'factory', id: factory.factoryId },
2253
+ amounts: { balances: Object.fromEntries(Object.entries(factory.allocations ?? {}).map(([k, v]) => [k, v.toString()])) },
2254
+ transitionHash: (0, emitter_js_1.hashTransition)(['factory', factory.factoryId, factory.status]),
2255
+ ...(typeof params.operationId === 'string' ? { operationId: params.operationId } : {}),
2256
+ data: { participants: factory.participants },
2257
+ });
659
2258
  return { success: true, factoryId: factory.factoryId, status: factory.status };
660
2259
  });
661
2260
  };
@@ -714,6 +2313,14 @@ function createHostMethods(context) {
714
2313
  }
715
2314
  context.factories.set(factoryId, result.factory);
716
2315
  context.channels.set(result.channel.channelId, result.channel);
2316
+ await context.observe?.emit({
2317
+ domain: 'factory', kind: 'factory.vc_opened', phase: 'accepted',
2318
+ entity: { type: 'channel', id: result.channel.channelId },
2319
+ amounts: { capacity: result.channel.totalValue.toString(), balances: balancesJson(result.channel) },
2320
+ transitionHash: (0, emitter_js_1.channelTransitionHash)(result.channel),
2321
+ ...(typeof params.operationId === 'string' ? { operationId: params.operationId } : {}),
2322
+ data: { factoryId },
2323
+ });
717
2324
  return { success: true, channelId: result.channel.channelId, factoryId };
718
2325
  });
719
2326
  };
@@ -769,6 +2376,12 @@ function createHostMethods(context) {
769
2376
  }
770
2377
  context.factories.set(factoryId, updated);
771
2378
  context.channels.delete(channelId);
2379
+ await context.observe?.emit({
2380
+ domain: 'factory', kind: 'factory.vc_closed', phase: 'closed',
2381
+ entity: { type: 'channel', id: channelId },
2382
+ ...(typeof params.operationId === 'string' ? { operationId: params.operationId } : {}),
2383
+ data: { factoryId, allocations: Object.fromEntries(Object.entries(updated.allocations ?? {}).map(([k, v]) => [k, v.toString()])) },
2384
+ });
772
2385
  return {
773
2386
  success: true,
774
2387
  factoryId,
@@ -831,6 +2444,13 @@ function createHostMethods(context) {
831
2444
  payload = await (0, omnia_factory_1.closeFactory)(factory, context.factoryBundles, context.chainProvider, { coinProof });
832
2445
  }
833
2446
  context.factories.set(factoryId, { ...factory, status: 'closed' });
2447
+ await context.observe?.emit({
2448
+ domain: 'factory', kind: 'factory.closed', phase: 'closed',
2449
+ entity: { type: 'factory', id: factoryId },
2450
+ chain: { txpowId: payload.txpowId },
2451
+ amounts: { balances: Object.fromEntries(Object.entries(payload.finalAllocations).map(([k, v]) => [k, v.toString()])) },
2452
+ ...(typeof params.operationId === 'string' ? { operationId: params.operationId } : {}),
2453
+ });
834
2454
  const finalAllocations = payload.finalAllocations;
835
2455
  return { success: true, factoryId, settlementTxId: payload.txpowId, finalAllocations: Object.fromEntries(Object.entries(finalAllocations).map(([key, value]) => [key, value.toString()])) };
836
2456
  });
@@ -877,6 +2497,15 @@ function createHostMethods(context) {
877
2497
  proposerLeaseProvider: provider.wotsLease,
878
2498
  });
879
2499
  context.channels.set(channelId, spliced);
2500
+ await context.observe?.emit({
2501
+ domain: 'splice', kind: 'splice.completed', phase: 'confirmed',
2502
+ entity: { type: 'channel', id: channelId },
2503
+ amounts: { capacity: spliced.totalValue.toString(), balances: Object.fromEntries(Object.entries(spliced.balances).map(([k, v]) => [k, v.toString()])) },
2504
+ chain: { fundingCoinId: spliced.spliceFundingCoinId, txpowId: spliced.spliceFundingTxId },
2505
+ transitionHash: (0, emitter_js_1.hashTransition)(['splice', channelId, spliced.spliceFundingCoinId, spliced.spliceFundingTxId]),
2506
+ ...(typeof params.operationId === 'string' ? { operationId: params.operationId } : {}),
2507
+ data: { spliceId: proposal.spliceId, spliceType: spliced.spliceType },
2508
+ });
880
2509
  await peer.sendMessage({
881
2510
  type: 'SPLICE_FINALIZED',
882
2511
  channelId,
@@ -943,6 +2572,18 @@ function createHostMethods(context) {
943
2572
  : await (0, omnia_router_1.executeMultiHopPayment)(ops, context.channels, parsedRoute, parsedRequest, leaseProviders);
944
2573
  if (!result.success)
945
2574
  throw new Error(result.error ?? 'multi-hop payment failed');
2575
+ await context.observe?.emit({
2576
+ domain: 'payment', kind: 'payment.completed', phase: 'committed',
2577
+ entity: { type: 'payment', id: typeof params.operationId === 'string' ? params.operationId : parsedRequest.hashlock },
2578
+ ...(typeof params.operationId === 'string' ? { operationId: params.operationId } : {}),
2579
+ amounts: { amountAtoms: parsedRequest.amount.toString() },
2580
+ token: { tokenId: parsedRequest.tokenId, scale: 0 },
2581
+ transitionHash: (0, emitter_js_1.hashTransition)(['payment', parsedRequest.hashlock, result.settledHops.join(',')]),
2582
+ data: {
2583
+ settledHops: result.settledHops,
2584
+ hops: parsedRoute.hops.map((h) => ({ channelId: h.channelId, amount: String(h.amount) })),
2585
+ },
2586
+ });
946
2587
  return {
947
2588
  success: true,
948
2589
  preimage: result.preimage,
@@ -951,5 +2592,5 @@ function createHostMethods(context) {
951
2592
  });
952
2593
  };
953
2594
  register('totem_omniaPayMultiHop', payMultiHop, 'omnia/payMultiHop');
954
- return methods;
2595
+ return observeMethods(methods);
955
2596
  }