@totemsdk/omnia-host 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -21
- package/NOTICE +60 -0
- package/README.md +44 -0
- package/dist/api/jsonrpc.d.ts +12 -0
- package/dist/api/jsonrpc.js +100 -1
- package/dist/api/methods.d.ts +101 -3
- package/dist/api/methods.js +1664 -23
- package/dist/chain/totem-node-adapter.js +1 -1
- package/dist/cli.js +65 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.js +33 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +20 -1
- package/dist/lifecycle.d.ts +15 -0
- package/dist/lifecycle.js +171 -0
- package/dist/observe/bus.d.ts +13 -0
- package/dist/observe/bus.js +29 -0
- package/dist/observe/emitter.d.ts +61 -0
- package/dist/observe/emitter.js +133 -0
- package/dist/observe/index.d.ts +5 -0
- package/dist/observe/index.js +21 -0
- package/dist/observe/snapshot.d.ts +38 -0
- package/dist/observe/snapshot.js +101 -0
- package/dist/observe/store.d.ts +31 -0
- package/dist/observe/store.js +82 -0
- package/dist/observe/types.d.ts +106 -0
- package/dist/observe/types.js +14 -0
- package/dist/pairing.d.ts +109 -0
- package/dist/pairing.js +231 -0
- package/dist/signer-port.d.ts +27 -0
- package/dist/signer-port.js +79 -0
- package/dist/stores/operations.d.ts +3 -2
- package/dist/stores/operations.js +96 -83
- package/dist/stores/sqlite-store.d.ts +8 -5
- package/dist/stores/sqlite-store.js +45 -42
- package/package.json +11 -9
package/dist/api/methods.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
2147
|
+
const localSigner = context.signerPort && (context.custodyMode ?? 'self') !== 'hosted'
|
|
571
2148
|
? async (digest) => {
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
}
|
|
582
|
-
|
|
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
|
-
:
|
|
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
|
}
|