@totemsdk/omnia-host 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -21
- package/NOTICE +60 -0
- package/README.md +44 -0
- package/dist/api/jsonrpc.d.ts +12 -0
- package/dist/api/jsonrpc.js +100 -1
- package/dist/api/methods.d.ts +135 -3
- package/dist/api/methods.js +1986 -45
- package/dist/chain/totem-node-adapter.js +1 -1
- package/dist/cli.js +67 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.js +33 -1
- package/dist/identity.js +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +20 -1
- package/dist/lifecycle.d.ts +22 -0
- package/dist/lifecycle.js +422 -5
- package/dist/observe/bus.d.ts +13 -0
- package/dist/observe/bus.js +29 -0
- package/dist/observe/emitter.d.ts +61 -0
- package/dist/observe/emitter.js +133 -0
- package/dist/observe/index.d.ts +5 -0
- package/dist/observe/index.js +21 -0
- package/dist/observe/snapshot.d.ts +38 -0
- package/dist/observe/snapshot.js +101 -0
- package/dist/observe/store.d.ts +31 -0
- package/dist/observe/store.js +82 -0
- package/dist/observe/types.d.ts +106 -0
- package/dist/observe/types.js +14 -0
- package/dist/pairing.d.ts +109 -0
- package/dist/pairing.js +231 -0
- package/dist/reply-cache.d.ts +21 -0
- package/dist/reply-cache.js +62 -0
- package/dist/signer-port.d.ts +27 -0
- package/dist/signer-port.js +79 -0
- package/dist/signing.d.ts +11 -0
- package/dist/signing.js +27 -0
- package/dist/stores/operations.d.ts +3 -2
- package/dist/stores/operations.js +96 -83
- package/dist/stores/sqlite-store.d.ts +8 -5
- package/dist/stores/sqlite-store.js +45 -42
- package/package.json +16 -14
package/dist/api/methods.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.channelStateView = channelStateView;
|
|
4
|
+
exports.requestPeerReply = requestPeerReply;
|
|
4
5
|
exports.createHostMethods = createHostMethods;
|
|
5
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");
|
|
6
10
|
const omnia_router_1 = require("@totemsdk/omnia-router");
|
|
7
11
|
const omnia_factory_1 = require("@totemsdk/omnia-factory");
|
|
8
12
|
const omnia_splice_1 = require("@totemsdk/omnia-splice");
|
|
13
|
+
const emitter_js_1 = require("../observe/emitter.js");
|
|
9
14
|
function channelSummary(channel) {
|
|
10
15
|
const parties = channel.parties.map((party) => party.partyId);
|
|
11
16
|
return {
|
|
@@ -148,16 +153,36 @@ async function withOperation(context, params, run) {
|
|
|
148
153
|
if (existing && !['pending', 'failed'].includes(existing.status)) {
|
|
149
154
|
throw new Error(`Operation ${id} is already ${existing.status}`);
|
|
150
155
|
}
|
|
151
|
-
if (!existing)
|
|
156
|
+
if (!existing) {
|
|
152
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
|
+
}
|
|
153
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
|
+
});
|
|
154
169
|
try {
|
|
155
170
|
const result = await run();
|
|
156
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
|
+
});
|
|
157
177
|
return result;
|
|
158
178
|
}
|
|
159
179
|
catch (error) {
|
|
160
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
|
+
});
|
|
161
186
|
throw error;
|
|
162
187
|
}
|
|
163
188
|
}
|
|
@@ -176,7 +201,7 @@ async function persistProgramUpdate(context, channelId, run) {
|
|
|
176
201
|
context.channels.set(channelId, result.channel);
|
|
177
202
|
const remoteParty = channel.parties.find((party) => party.partyId !== context.localParticipant.partyId);
|
|
178
203
|
if (context.swarm && remoteParty) {
|
|
179
|
-
const peer = await context.swarm.connectToPeer(remoteParty.publicKeyDigest, channelId);
|
|
204
|
+
const peer = await context.swarm.connectToPeer(remoteParty.publicKeyDigest, channelId, remoteParty.transportPubkey);
|
|
180
205
|
await peer.sendMessage({ type: 'STATE_UPDATE', channelId, nonce: Date.now(), payload: result.signedState });
|
|
181
206
|
}
|
|
182
207
|
return {
|
|
@@ -187,37 +212,1447 @@ async function persistProgramUpdate(context, channelId, run) {
|
|
|
187
212
|
};
|
|
188
213
|
}
|
|
189
214
|
/**
|
|
190
|
-
*
|
|
191
|
-
*
|
|
215
|
+
* Send a proposal and wait for its ACK/ERROR reply.
|
|
216
|
+
*
|
|
217
|
+
* The message is resent with the SAME nonce when the peer reconnects (a
|
|
218
|
+
* proposal or its reply may have been lost when the old stream closed) and
|
|
219
|
+
* once mid-flight in case the reply was lost without the stream closing.
|
|
220
|
+
* Receivers are expected to deduplicate by `(type, channelId, nonce)` so a
|
|
221
|
+
* resend never consumes another WOTS leaf or yields a different signature.
|
|
192
222
|
*/
|
|
193
|
-
function
|
|
223
|
+
function requestPeerReply(peer, msg, channelId, timeoutMs, types = ['ACK', 'ERROR']) {
|
|
194
224
|
return new Promise((resolve) => {
|
|
195
225
|
let done = false;
|
|
196
|
-
let
|
|
226
|
+
let timer;
|
|
227
|
+
let fallback;
|
|
228
|
+
const unsubs = [];
|
|
197
229
|
const finish = (reply) => {
|
|
198
230
|
if (done)
|
|
199
231
|
return;
|
|
200
232
|
done = true;
|
|
201
|
-
|
|
202
|
-
|
|
233
|
+
if (timer)
|
|
234
|
+
clearTimeout(timer);
|
|
235
|
+
if (fallback)
|
|
236
|
+
clearTimeout(fallback);
|
|
237
|
+
for (const unsub of unsubs)
|
|
203
238
|
unsub();
|
|
204
239
|
resolve(reply);
|
|
205
240
|
};
|
|
206
|
-
|
|
207
|
-
if (
|
|
241
|
+
const resend = (reason) => {
|
|
242
|
+
if (done)
|
|
208
243
|
return;
|
|
209
|
-
if (
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
244
|
+
if (process.env.OMNIA_DEBUG) {
|
|
245
|
+
console.error('[peer] resend', msg.type, msg.nonce, reason, channelId);
|
|
246
|
+
}
|
|
247
|
+
void peer.sendMessage(msg).catch(() => { });
|
|
248
|
+
};
|
|
249
|
+
unsubs.push(peer.onMessage((reply) => {
|
|
250
|
+
if (reply.channelId !== channelId)
|
|
251
|
+
return;
|
|
252
|
+
if (msg.nonce !== undefined && reply.nonce !== undefined && reply.nonce !== msg.nonce)
|
|
253
|
+
return;
|
|
254
|
+
if (types.includes(reply.type))
|
|
255
|
+
finish(reply);
|
|
256
|
+
}));
|
|
257
|
+
if (peer.onReconnected) {
|
|
258
|
+
unsubs.push(peer.onReconnected(() => resend('reconnected')));
|
|
259
|
+
}
|
|
260
|
+
fallback = setTimeout(() => resend('no reply'), Math.min(10_000, Math.floor(timeoutMs / 2)));
|
|
261
|
+
timer = setTimeout(() => finish(null), timeoutMs);
|
|
262
|
+
void peer.sendMessage(msg).catch(() => { });
|
|
213
263
|
});
|
|
214
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
|
+
}
|
|
215
1607
|
function createHostMethods(context) {
|
|
216
1608
|
const methods = new Map();
|
|
217
1609
|
const register = (name, handler, alias) => {
|
|
218
1610
|
methods.set(name, handler);
|
|
219
1611
|
methods.set(alias, handler);
|
|
220
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
|
+
};
|
|
221
1656
|
const getChannels = (raw) => {
|
|
222
1657
|
const params = paramsObject(raw);
|
|
223
1658
|
const tokenId = typeof params.tokenId === 'string' ? params.tokenId : undefined;
|
|
@@ -238,12 +1673,23 @@ function createHostMethods(context) {
|
|
|
238
1673
|
return channelStateView(channel);
|
|
239
1674
|
};
|
|
240
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
|
+
};
|
|
241
1687
|
const getRoute = async (raw) => {
|
|
242
1688
|
context.refreshRouting?.();
|
|
243
1689
|
const params = paramsObject(raw);
|
|
244
1690
|
const route = await context.routing.getRoute({
|
|
245
|
-
from: requiredString(params, 'fromPartyId'),
|
|
246
|
-
to: requiredString(params, 'toPartyId'),
|
|
1691
|
+
from: normalizeRouteNode(requiredString(params, 'fromPartyId')),
|
|
1692
|
+
to: normalizeRouteNode(requiredString(params, 'toPartyId')),
|
|
247
1693
|
amount: requiredBigInt(params, 'amount'),
|
|
248
1694
|
tokenId: requiredString(params, 'tokenId'),
|
|
249
1695
|
targetTokenId: typeof params.targetTokenId === 'string' ? params.targetTokenId : undefined,
|
|
@@ -278,7 +1724,10 @@ function createHostMethods(context) {
|
|
|
278
1724
|
address: context.identity.address,
|
|
279
1725
|
publicKeyDigest: context.identity.publicKeyDigest,
|
|
280
1726
|
identityId: context.identity.identityId,
|
|
1727
|
+
settlementAddress: context.localParticipant?.settlementAddress,
|
|
281
1728
|
delegation: context.identity.delegation,
|
|
1729
|
+
custodyMode: context.custodyMode ?? 'self',
|
|
1730
|
+
p2pHubUrl: context.p2pHubUrl ?? null,
|
|
282
1731
|
};
|
|
283
1732
|
};
|
|
284
1733
|
register('totem_omniaWhoami', whoami, 'omnia/whoami');
|
|
@@ -291,8 +1740,14 @@ function createHostMethods(context) {
|
|
|
291
1740
|
register('totem_omniaGetManifest', getManifest, 'omnia/getManifest');
|
|
292
1741
|
// Fail-fast rule: without signing material only the read-only surface is
|
|
293
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
|
+
}
|
|
294
1749
|
if (context.readOnly || !context.signer || !context.leaseProvider) {
|
|
295
|
-
return methods;
|
|
1750
|
+
return observeMethods(methods);
|
|
296
1751
|
}
|
|
297
1752
|
const openChannel = async (raw) => {
|
|
298
1753
|
const params = paramsObject(raw);
|
|
@@ -304,10 +1759,14 @@ function createHostMethods(context) {
|
|
|
304
1759
|
publicKeyDigest: remotePublicKeyDigest,
|
|
305
1760
|
addressIndex: typeof params.remoteAddressIndex === 'number' ? params.remoteAddressIndex : 0,
|
|
306
1761
|
settlementAddress: typeof params.remoteSettlementAddress === 'string' ? params.remoteSettlementAddress : undefined,
|
|
1762
|
+
transportPubkey: typeof params.remoteTransportPubkey === 'string' ? params.remoteTransportPubkey : undefined,
|
|
307
1763
|
};
|
|
308
1764
|
const result = await withOperation(context, params, async () => {
|
|
309
1765
|
const createParams = {
|
|
310
|
-
localParty:
|
|
1766
|
+
localParty: {
|
|
1767
|
+
...context.localParticipant,
|
|
1768
|
+
transportPubkey: context.localParticipant.transportPubkey ?? context.transportPubkey,
|
|
1769
|
+
},
|
|
311
1770
|
remoteParty: remote,
|
|
312
1771
|
localAmount: requiredBigInt(params, 'localAmount'),
|
|
313
1772
|
remoteAmount: requiredBigInt(params, 'remoteAmount'),
|
|
@@ -349,7 +1808,26 @@ function createHostMethods(context) {
|
|
|
349
1808
|
if (typeof params.fundingScript === 'string') {
|
|
350
1809
|
createParams.fundingScript = params.fundingScript;
|
|
351
1810
|
}
|
|
352
|
-
|
|
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);
|
|
353
1831
|
}
|
|
354
1832
|
const created = await (0, omnia_1.createChannel)(createParams, context.chainProvider);
|
|
355
1833
|
context.channels.set(created.channel.channelId, {
|
|
@@ -358,12 +1836,22 @@ function createHostMethods(context) {
|
|
|
358
1836
|
});
|
|
359
1837
|
let acknowledged = true;
|
|
360
1838
|
if (context.swarm) {
|
|
361
|
-
const peer = await context.swarm.connectToPeer(remote.publicKeyDigest, created.channel.channelId);
|
|
362
|
-
await peer.sendMessage({ type: 'CHANNEL_PROPOSAL', channelId: created.channel.channelId, nonce: Date.now(), payload: created.proposal });
|
|
1839
|
+
const peer = await context.swarm.connectToPeer(remote.publicKeyDigest, created.channel.channelId, remote.transportPubkey);
|
|
363
1840
|
// Give the peer time to accept so a follow-up pay() is not lost.
|
|
364
|
-
const reply = await
|
|
1841
|
+
const reply = await requestPeerReply(peer, { type: 'CHANNEL_PROPOSAL', channelId: created.channel.channelId, nonce: Date.now(), payload: created.proposal }, created.channel.channelId, 20_000);
|
|
365
1842
|
acknowledged = reply?.type === 'ACK';
|
|
366
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
|
+
});
|
|
367
1855
|
return { success: true, channelId: created.channel.channelId, fundingTxId: created.channel.fundingTxId, acknowledged };
|
|
368
1856
|
});
|
|
369
1857
|
return result;
|
|
@@ -384,23 +1872,128 @@ function createHostMethods(context) {
|
|
|
384
1872
|
throw new Error('Channel counterparty not found');
|
|
385
1873
|
if (amount <= 0n || (channel.balances[localPartyId] ?? 0n) < amount)
|
|
386
1874
|
throw new Error('Insufficient channel balance');
|
|
1875
|
+
const beforeBalances = balancesJson(channel);
|
|
387
1876
|
const result = await (0, omnia_1.updateState)(channel, {
|
|
388
1877
|
newBalances: {
|
|
389
1878
|
...channel.balances,
|
|
390
1879
|
[localPartyId]: (channel.balances[localPartyId] ?? 0n) - amount,
|
|
391
1880
|
[remotePartyId]: (channel.balances[remotePartyId] ?? 0n) + amount,
|
|
392
1881
|
},
|
|
393
|
-
}, context.leaseProvider, context.signer);
|
|
1882
|
+
}, context.leaseProvider, context.signer, context.signerPort);
|
|
394
1883
|
if (result.error)
|
|
395
1884
|
throw new Error(result.error);
|
|
396
1885
|
context.channels.set(channelId, result.channel);
|
|
397
|
-
|
|
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
|
+
});
|
|
1897
|
+
const remoteParty = channel.parties.find((party) => party.partyId === remotePartyId);
|
|
1898
|
+
const peer = context.swarm ? await context.swarm.connectToPeer(remoteParty.publicKeyDigest, channelId, remoteParty.transportPubkey) : undefined;
|
|
398
1899
|
if (peer)
|
|
399
1900
|
await peer.sendMessage({ type: 'STATE_UPDATE', channelId, nonce: Date.now(), payload: result.signedState });
|
|
400
1901
|
return { success: true, channelId, sequence: result.channel.currentSequence, localBalance: result.channel.balances[localPartyId].toString(), remoteBalance: result.channel.balances[remotePartyId].toString() };
|
|
401
1902
|
});
|
|
402
1903
|
};
|
|
403
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);
|
|
404
1997
|
const programTransition = async (raw) => {
|
|
405
1998
|
const params = paramsObject(raw);
|
|
406
1999
|
requireMutationContext(context);
|
|
@@ -467,14 +2060,13 @@ function createHostMethods(context) {
|
|
|
467
2060
|
let settlementTxHex;
|
|
468
2061
|
if (context.swarm && remoteParty) {
|
|
469
2062
|
const request = (0, omnia_1.buildSettlementSigningRequest)(closing, { partyAddresses });
|
|
470
|
-
const peer = await context.swarm.connectToPeer(remoteParty.publicKeyDigest, channelId);
|
|
471
|
-
await peer
|
|
2063
|
+
const peer = await context.swarm.connectToPeer(remoteParty.publicKeyDigest, channelId, remoteParty.transportPubkey);
|
|
2064
|
+
const reply = await requestPeerReply(peer, {
|
|
472
2065
|
type: 'SETTLEMENT_PROPOSAL',
|
|
473
2066
|
channelId,
|
|
474
2067
|
nonce: Date.now(),
|
|
475
2068
|
payload: { signRequest: true, ...request },
|
|
476
|
-
});
|
|
477
|
-
const reply = await waitForPeerReply(peer, channelId, 30_000);
|
|
2069
|
+
}, channelId, 30_000);
|
|
478
2070
|
const replyPayload = reply?.payload;
|
|
479
2071
|
if (reply?.type !== 'ACK' || typeof replyPayload?.signature !== 'string') {
|
|
480
2072
|
throw new Error(`Counterparty did not co-sign the settlement (${reply?.type ?? 'timeout'})`);
|
|
@@ -494,6 +2086,14 @@ function createHostMethods(context) {
|
|
|
494
2086
|
counterpartySignatures,
|
|
495
2087
|
});
|
|
496
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
|
+
});
|
|
497
2097
|
return { success: true, channelId, settlementTxId: settlement.settlementPayload.txpowId, finalBalances: Object.fromEntries(Object.entries(settlement.settlementPayload.balances).map(([key, value]) => [key, value.toString()])) };
|
|
498
2098
|
});
|
|
499
2099
|
};
|
|
@@ -506,13 +2106,19 @@ function createHostMethods(context) {
|
|
|
506
2106
|
if (!channel)
|
|
507
2107
|
throw new Error(`Channel ${channelId} not found`);
|
|
508
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
|
+
});
|
|
509
2115
|
return { success: true, channelId };
|
|
510
2116
|
});
|
|
511
2117
|
};
|
|
512
2118
|
register('totem_omniaCloseChannel', closeChannel, 'omnia/closeChannel');
|
|
513
2119
|
const createFactoryMethod = async (raw) => {
|
|
514
2120
|
const params = paramsObject(raw);
|
|
515
|
-
if (!context.factories || !context.factoryBundles
|
|
2121
|
+
if (!context.factories || !context.factoryBundles)
|
|
516
2122
|
throw new Error('Factory manager is not configured');
|
|
517
2123
|
const participants = params.participants;
|
|
518
2124
|
if (!Array.isArray(participants) || participants.length < 2)
|
|
@@ -528,17 +2134,128 @@ function createHostMethods(context) {
|
|
|
528
2134
|
contributionAmount: BigInt(requiredString(item, 'contributionAmount')),
|
|
529
2135
|
fundingCoinId: typeof item.fundingCoinId === 'string' ? item.fundingCoinId : undefined,
|
|
530
2136
|
settlementAddress: typeof item.settlementAddress === 'string' ? item.settlementAddress : undefined,
|
|
2137
|
+
transportPubkey: typeof item.transportPubkey === 'string' ? item.transportPubkey : undefined,
|
|
531
2138
|
};
|
|
532
2139
|
});
|
|
2140
|
+
const localPartyId = context.localParticipant?.partyId;
|
|
2141
|
+
if (!localPartyId)
|
|
2142
|
+
throw new Error('Local participant is not configured');
|
|
2143
|
+
const localBundle = context.factoryBundles[localPartyId];
|
|
2144
|
+
if (!localBundle)
|
|
2145
|
+
throw new Error('No local factory lease bundle');
|
|
2146
|
+
const remote = parsed.find((participant) => participant.partyId !== localPartyId);
|
|
2147
|
+
const localSigner = context.signerPort && (context.custodyMode ?? 'self') !== 'hosted'
|
|
2148
|
+
? async (digest) => {
|
|
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);
|
|
2160
|
+
}
|
|
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;
|
|
2179
|
+
let fundingPeer;
|
|
2180
|
+
const remoteSigner = context.swarm && remote
|
|
2181
|
+
? async (digest) => {
|
|
2182
|
+
fundingPeer ??= await context.swarm.connectToPeer(remote.publicKeyDigest, 'factory-funding', remote.transportPubkey);
|
|
2183
|
+
const channelId = 'factory-funding';
|
|
2184
|
+
const reply = await requestPeerReply(fundingPeer, {
|
|
2185
|
+
type: 'FACTORY_FUNDING_REQUEST',
|
|
2186
|
+
channelId,
|
|
2187
|
+
nonce: Date.now(),
|
|
2188
|
+
payload: { digest: '0x' + Buffer.from(digest).toString('hex') },
|
|
2189
|
+
}, channelId, 30_000, ['FACTORY_FUNDING_RESPONSE', 'ERROR']);
|
|
2190
|
+
const signature = reply?.payload?.signature;
|
|
2191
|
+
if (!signature) {
|
|
2192
|
+
throw new Error(`Counterparty did not co-sign factory funding (${reply?.type ?? 'timeout'})`);
|
|
2193
|
+
}
|
|
2194
|
+
return Uint8Array.from(Buffer.from(signature.replace(/^0x/i, ''), 'hex'));
|
|
2195
|
+
}
|
|
2196
|
+
: undefined;
|
|
2197
|
+
const funding = Array.isArray(params.funding)
|
|
2198
|
+
? params.funding.map((value) => {
|
|
2199
|
+
if (!value || typeof value !== 'object')
|
|
2200
|
+
throw new Error('Invalid factory funding material');
|
|
2201
|
+
const item = value;
|
|
2202
|
+
const hex = (key) => {
|
|
2203
|
+
const raw = item[key];
|
|
2204
|
+
if (typeof raw !== 'string')
|
|
2205
|
+
return undefined;
|
|
2206
|
+
return Buffer.from(raw.replace(/^0x/i, ''), 'hex');
|
|
2207
|
+
};
|
|
2208
|
+
const coinProof = hex('coinProof');
|
|
2209
|
+
if (!coinProof)
|
|
2210
|
+
throw new Error('factory funding material requires coinProof');
|
|
2211
|
+
const partyId = requiredString(item, 'partyId');
|
|
2212
|
+
const signature = hex('signature');
|
|
2213
|
+
const signer = signature
|
|
2214
|
+
? undefined
|
|
2215
|
+
: partyId === localPartyId
|
|
2216
|
+
? localSigner
|
|
2217
|
+
: remoteSigner;
|
|
2218
|
+
return {
|
|
2219
|
+
partyId,
|
|
2220
|
+
coinProof,
|
|
2221
|
+
...(typeof item.coinAddress === 'string' ? { coinAddress: item.coinAddress } : {}),
|
|
2222
|
+
...(typeof item.script === 'string' ? { script: item.script } : {}),
|
|
2223
|
+
...(hex('scriptProof') ? { scriptProof: hex('scriptProof') } : {}),
|
|
2224
|
+
...(signature ? { signature } : signer ? { signer } : {}),
|
|
2225
|
+
};
|
|
2226
|
+
})
|
|
2227
|
+
: undefined;
|
|
533
2228
|
const tokenId = typeof params.tokenId === 'string' ? params.tokenId : '0x00';
|
|
534
|
-
const bundles = context.factoryBundles;
|
|
535
|
-
const proposer = parsed.find((participant) => bundles[participant.partyId]);
|
|
536
|
-
if (!proposer)
|
|
537
|
-
throw new Error('No local factory lease bundle matches a participant');
|
|
538
2229
|
return withOperation(context, params, async () => {
|
|
539
|
-
|
|
2230
|
+
// Off-chain when no funding material is supplied; with funding material the
|
|
2231
|
+
// SDK builds, signs, and submits the on-chain factory funding TX.
|
|
2232
|
+
let factory = await (0, omnia_factory_1.createFactory)(parsed, tokenId, localBundle, context.chainProvider, 0, funding);
|
|
2233
|
+
if (context.swarm && remote) {
|
|
2234
|
+
const peer = await context.swarm.connectToPeer(remote.publicKeyDigest, factory.factoryId, remote.transportPubkey);
|
|
2235
|
+
const reply = await requestPeerReply(peer, {
|
|
2236
|
+
type: 'FACTORY_PROPOSAL',
|
|
2237
|
+
channelId: factory.factoryId,
|
|
2238
|
+
nonce: Date.now(),
|
|
2239
|
+
payload: { factory },
|
|
2240
|
+
}, factory.factoryId, 30_000, ['FACTORY_ACCEPT', 'ERROR']);
|
|
2241
|
+
if (reply?.type !== 'FACTORY_ACCEPT') {
|
|
2242
|
+
throw new Error(`Counterparty did not accept the factory (${reply?.type ?? 'timeout'})`);
|
|
2243
|
+
}
|
|
2244
|
+
const accepted = reply.payload?.factory;
|
|
2245
|
+
if (!accepted)
|
|
2246
|
+
throw new Error('Factory accept reply missing factory');
|
|
2247
|
+
factory = accepted;
|
|
2248
|
+
}
|
|
540
2249
|
context.factories.set(factory.factoryId, factory);
|
|
541
|
-
|
|
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
|
+
});
|
|
2258
|
+
return { success: true, factoryId: factory.factoryId, status: factory.status };
|
|
542
2259
|
});
|
|
543
2260
|
};
|
|
544
2261
|
register('totem_omniaCreateFactory', createFactoryMethod, 'omnia/createFactory');
|
|
@@ -557,14 +2274,123 @@ function createHostMethods(context) {
|
|
|
557
2274
|
if (!amounts || typeof amounts !== 'object')
|
|
558
2275
|
throw new Error('amounts is required');
|
|
559
2276
|
const parsedAmounts = Object.fromEntries(Object.entries(amounts).map(([key, value]) => [key, BigInt(String(value))]));
|
|
2277
|
+
const localPartyId = context.localParticipant?.partyId;
|
|
2278
|
+
if (!localPartyId)
|
|
2279
|
+
throw new Error('Local participant is not configured');
|
|
2280
|
+
const localBundle = context.factoryBundles[localPartyId];
|
|
2281
|
+
if (!localBundle)
|
|
2282
|
+
throw new Error('No local factory lease bundle');
|
|
560
2283
|
return withOperation(context, params, async () => {
|
|
561
|
-
const
|
|
2284
|
+
const plan = (0, omnia_factory_1.buildVirtualChannelOpen)(factory, parties, parsedAmounts);
|
|
2285
|
+
const localSig = await (0, omnia_factory_1.signVirtualChannelOpen)(factory, plan, localBundle);
|
|
2286
|
+
const signatures = { [localSig.partyId]: localSig };
|
|
2287
|
+
const remote = factory.participants.find((participant) => participant.partyId !== localPartyId);
|
|
2288
|
+
const peer = context.swarm && remote
|
|
2289
|
+
? await context.swarm.connectToPeer(remote.publicKeyDigest, plan.vcId, remote.transportPubkey)
|
|
2290
|
+
: undefined;
|
|
2291
|
+
if (peer) {
|
|
2292
|
+
const reply = await requestPeerReply(peer, {
|
|
2293
|
+
type: 'VC_PROPOSAL',
|
|
2294
|
+
channelId: plan.vcId,
|
|
2295
|
+
nonce: Date.now(),
|
|
2296
|
+
payload: { kind: 'vc-open', factoryId, vcId: plan.vcId, plan },
|
|
2297
|
+
}, plan.vcId, 30_000, ['VC_ACCEPT', 'ERROR']);
|
|
2298
|
+
const sig = reply?.payload?.signature;
|
|
2299
|
+
if (reply?.type !== 'VC_ACCEPT' || !sig) {
|
|
2300
|
+
throw new Error(`Counterparty did not sign the virtual-channel open (${reply?.type ?? 'timeout'})`);
|
|
2301
|
+
}
|
|
2302
|
+
signatures[sig.partyId] = sig;
|
|
2303
|
+
}
|
|
2304
|
+
const result = (0, omnia_factory_1.assembleVirtualChannelOpen)(factory, plan, signatures);
|
|
2305
|
+
await localBundle.leaseProvider.commitKeyUse(localSig.reservationId, `factory-vc-open-${factoryId}`);
|
|
2306
|
+
if (peer) {
|
|
2307
|
+
await peer.sendMessage({
|
|
2308
|
+
type: 'VC_FINALIZED',
|
|
2309
|
+
channelId: plan.vcId,
|
|
2310
|
+
nonce: Date.now(),
|
|
2311
|
+
payload: { kind: 'vc-open', factory: result.factory, channel: result.channel },
|
|
2312
|
+
});
|
|
2313
|
+
}
|
|
562
2314
|
context.factories.set(factoryId, result.factory);
|
|
563
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
|
+
});
|
|
564
2324
|
return { success: true, channelId: result.channel.channelId, factoryId };
|
|
565
2325
|
});
|
|
566
2326
|
};
|
|
567
2327
|
register('totem_omniaOpenVirtualChannel', openVirtual, 'omnia/openVirtualChannel');
|
|
2328
|
+
const closeVirtual = async (raw) => {
|
|
2329
|
+
const params = paramsObject(raw);
|
|
2330
|
+
if (!context.factories || !context.factoryBundles)
|
|
2331
|
+
throw new Error('Factory manager is not configured');
|
|
2332
|
+
const factoryId = requiredString(params, 'factoryId');
|
|
2333
|
+
const channelId = requiredString(params, 'channelId');
|
|
2334
|
+
const factory = context.factories.get(factoryId);
|
|
2335
|
+
if (!factory)
|
|
2336
|
+
throw new Error(`Factory ${factoryId} not found`);
|
|
2337
|
+
const channel = context.channels.get(channelId);
|
|
2338
|
+
if (!channel)
|
|
2339
|
+
throw new Error(`Channel ${channelId} not found`);
|
|
2340
|
+
const localPartyId = context.localParticipant?.partyId;
|
|
2341
|
+
if (!localPartyId)
|
|
2342
|
+
throw new Error('Local participant is not configured');
|
|
2343
|
+
const localBundle = context.factoryBundles[localPartyId];
|
|
2344
|
+
if (!localBundle)
|
|
2345
|
+
throw new Error('No local factory lease bundle');
|
|
2346
|
+
return withOperation(context, params, async () => {
|
|
2347
|
+
const plan = (0, omnia_factory_1.buildVirtualChannelClose)(factory, channel);
|
|
2348
|
+
const localSig = await (0, omnia_factory_1.signVirtualChannelClose)(factory, plan, localBundle);
|
|
2349
|
+
const signatures = { [localSig.partyId]: localSig };
|
|
2350
|
+
const remote = factory.participants.find((participant) => participant.partyId !== localPartyId);
|
|
2351
|
+
const peer = context.swarm && remote
|
|
2352
|
+
? await context.swarm.connectToPeer(remote.publicKeyDigest, plan.vcId, remote.transportPubkey)
|
|
2353
|
+
: undefined;
|
|
2354
|
+
if (peer) {
|
|
2355
|
+
const reply = await requestPeerReply(peer, {
|
|
2356
|
+
type: 'VC_PROPOSAL',
|
|
2357
|
+
channelId: plan.vcId,
|
|
2358
|
+
nonce: Date.now(),
|
|
2359
|
+
payload: { kind: 'vc-close', factoryId, vcId: plan.vcId, plan },
|
|
2360
|
+
}, plan.vcId, 30_000, ['VC_ACCEPT', 'ERROR']);
|
|
2361
|
+
const sig = reply?.payload?.signature;
|
|
2362
|
+
if (reply?.type !== 'VC_ACCEPT' || !sig) {
|
|
2363
|
+
throw new Error(`Counterparty did not sign the virtual-channel close (${reply?.type ?? 'timeout'})`);
|
|
2364
|
+
}
|
|
2365
|
+
signatures[sig.partyId] = sig;
|
|
2366
|
+
}
|
|
2367
|
+
const updated = (0, omnia_factory_1.assembleVirtualChannelClose)(factory, plan, signatures);
|
|
2368
|
+
await localBundle.leaseProvider.commitKeyUse(localSig.reservationId, `factory-vc-close-${factoryId}`);
|
|
2369
|
+
if (peer) {
|
|
2370
|
+
await peer.sendMessage({
|
|
2371
|
+
type: 'VC_FINALIZED',
|
|
2372
|
+
channelId: plan.vcId,
|
|
2373
|
+
nonce: Date.now(),
|
|
2374
|
+
payload: { kind: 'vc-close', factory: updated, channelId },
|
|
2375
|
+
});
|
|
2376
|
+
}
|
|
2377
|
+
context.factories.set(factoryId, updated);
|
|
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
|
+
});
|
|
2385
|
+
return {
|
|
2386
|
+
success: true,
|
|
2387
|
+
factoryId,
|
|
2388
|
+
channelId,
|
|
2389
|
+
allocations: Object.fromEntries(Object.entries(updated.allocations).map(([k, v]) => [k, v.toString()])),
|
|
2390
|
+
};
|
|
2391
|
+
});
|
|
2392
|
+
};
|
|
2393
|
+
register('totem_omniaCloseVirtualChannel', closeVirtual, 'omnia/closeVirtualChannel');
|
|
568
2394
|
const closeFactoryMethod = async (raw) => {
|
|
569
2395
|
const params = paramsObject(raw);
|
|
570
2396
|
if (!context.factories || !context.factoryBundles || !context.chainProvider)
|
|
@@ -573,29 +2399,132 @@ function createHostMethods(context) {
|
|
|
573
2399
|
const factory = context.factories.get(factoryId);
|
|
574
2400
|
if (!factory)
|
|
575
2401
|
throw new Error(`Factory ${factoryId} not found`);
|
|
2402
|
+
const coinProofRaw = params.coinProof
|
|
2403
|
+
?? params.broadcastProofs?.coinProofs;
|
|
2404
|
+
const coinProofHex = Array.isArray(coinProofRaw) ? coinProofRaw[0] : coinProofRaw;
|
|
2405
|
+
const coinProof = typeof coinProofHex === 'string'
|
|
2406
|
+
? Buffer.from(coinProofHex.replace(/^0x/i, ''), 'hex')
|
|
2407
|
+
: undefined;
|
|
576
2408
|
return withOperation(context, params, async () => {
|
|
577
|
-
const
|
|
2409
|
+
const localPartyId = context.localParticipant?.partyId;
|
|
2410
|
+
const localBundle = localPartyId ? context.factoryBundles[localPartyId] : undefined;
|
|
2411
|
+
if (!localPartyId || !localBundle)
|
|
2412
|
+
throw new Error('No local factory lease bundle');
|
|
2413
|
+
const remote = factory.participants.find((participant) => participant.partyId !== localPartyId);
|
|
2414
|
+
let payload;
|
|
2415
|
+
if (context.swarm && remote) {
|
|
2416
|
+
// Cooperative close: build/sign locally, collect the counterparty
|
|
2417
|
+
// signature over the same draft, then assemble and broadcast once.
|
|
2418
|
+
const draft = (0, omnia_factory_1.buildFactorySettlement)(factory);
|
|
2419
|
+
const localSig = await (0, omnia_factory_1.signFactorySettlement)(factory, draft, localBundle);
|
|
2420
|
+
const signatures = { [localSig.partyId]: localSig };
|
|
2421
|
+
const peer = await context.swarm.connectToPeer(remote.publicKeyDigest, factoryId, remote.transportPubkey);
|
|
2422
|
+
const message = {
|
|
2423
|
+
type: 'FACTORY_CLOSE_PROPOSAL',
|
|
2424
|
+
channelId: factoryId,
|
|
2425
|
+
nonce: Date.now(),
|
|
2426
|
+
payload: { factoryId, draft },
|
|
2427
|
+
};
|
|
2428
|
+
const reply = await requestPeerReply(peer, message, factoryId, 30_000, ['FACTORY_CLOSE_ACCEPT', 'ERROR']);
|
|
2429
|
+
const remoteSig = reply?.payload?.signature;
|
|
2430
|
+
if (reply?.type !== 'FACTORY_CLOSE_ACCEPT' || !remoteSig) {
|
|
2431
|
+
throw new Error(`Counterparty did not co-sign the factory close (${reply?.type ?? 'timeout'})`);
|
|
2432
|
+
}
|
|
2433
|
+
signatures[remoteSig.partyId] = remoteSig;
|
|
2434
|
+
payload = await (0, omnia_factory_1.assembleFactorySettlement)(factory, draft, signatures, context.chainProvider, { coinProof });
|
|
2435
|
+
await localBundle.leaseProvider.commitKeyUse(localSig.reservationId, `factory-settlement-${factoryId}`);
|
|
2436
|
+
await peer.sendMessage({
|
|
2437
|
+
type: 'FACTORY_CLOSE_FINALIZED',
|
|
2438
|
+
channelId: factoryId,
|
|
2439
|
+
nonce: message.nonce,
|
|
2440
|
+
payload: { factoryId, txId: payload.txpowId },
|
|
2441
|
+
});
|
|
2442
|
+
}
|
|
2443
|
+
else {
|
|
2444
|
+
payload = await (0, omnia_factory_1.closeFactory)(factory, context.factoryBundles, context.chainProvider, { coinProof });
|
|
2445
|
+
}
|
|
578
2446
|
context.factories.set(factoryId, { ...factory, status: 'closed' });
|
|
579
|
-
|
|
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
|
+
});
|
|
2454
|
+
const finalAllocations = payload.finalAllocations;
|
|
2455
|
+
return { success: true, factoryId, settlementTxId: payload.txpowId, finalAllocations: Object.fromEntries(Object.entries(finalAllocations).map(([key, value]) => [key, value.toString()])) };
|
|
580
2456
|
});
|
|
581
2457
|
};
|
|
582
2458
|
register('totem_omniaCloseFactory', closeFactoryMethod, 'omnia/closeFactory');
|
|
583
2459
|
const splice = (type) => async (raw) => {
|
|
584
2460
|
const params = paramsObject(raw);
|
|
585
|
-
if (!context.spliceLeaseProvider || !context.spliceProposals)
|
|
2461
|
+
if (!context.spliceLeaseProvider || !context.spliceProposals || !context.spliceAcceptances) {
|
|
586
2462
|
throw new Error('Splice manager is not configured');
|
|
2463
|
+
}
|
|
587
2464
|
const channelId = requiredString(params, 'channelId');
|
|
588
2465
|
const channel = context.channels.get(channelId);
|
|
589
2466
|
if (!channel)
|
|
590
2467
|
throw new Error(`Channel ${channelId} not found`);
|
|
2468
|
+
const localPartyId = context.localParticipant?.partyId;
|
|
2469
|
+
if (!localPartyId)
|
|
2470
|
+
throw new Error('Local participant is not configured');
|
|
2471
|
+
const remote = channel.parties.find((party) => party.partyId !== localPartyId);
|
|
2472
|
+
if (!context.swarm || !remote)
|
|
2473
|
+
throw new Error('Splice requires a counterparty');
|
|
2474
|
+
const swarm = context.swarm;
|
|
591
2475
|
return withOperation(context, params, async () => {
|
|
592
|
-
const
|
|
2476
|
+
const provider = context.spliceLeaseProvider;
|
|
2477
|
+
const quiesced = await (0, omnia_splice_1.quiesceChannel)(channel, provider);
|
|
593
2478
|
const proposal = type === 'in'
|
|
594
|
-
? await (0, omnia_splice_1.proposeSpliceIn)(quiesced, requiredString(params, 'additionalCoinId'), requiredBigInt(params, 'additionalAmount'),
|
|
595
|
-
: await (0, omnia_splice_1.proposeSpliceOut)(quiesced, requiredBigInt(params, 'withdrawAmount'), requiredString(params, 'withdrawAddress'),
|
|
2479
|
+
? await (0, omnia_splice_1.proposeSpliceIn)(quiesced, requiredString(params, 'additionalCoinId'), requiredBigInt(params, 'additionalAmount'), provider, params.newBalances)
|
|
2480
|
+
: await (0, omnia_splice_1.proposeSpliceOut)(quiesced, requiredBigInt(params, 'withdrawAmount'), requiredString(params, 'withdrawAddress'), provider, params.newBalances);
|
|
596
2481
|
context.spliceProposals.set(proposal.spliceId, proposal);
|
|
597
|
-
|
|
598
|
-
|
|
2482
|
+
const peer = await swarm.connectToPeer(remote.publicKeyDigest, channelId, remote.transportPubkey);
|
|
2483
|
+
const proposalMessage = {
|
|
2484
|
+
type: 'SPLICE_PROPOSAL',
|
|
2485
|
+
channelId,
|
|
2486
|
+
nonce: Date.now(),
|
|
2487
|
+
payload: { proposal },
|
|
2488
|
+
};
|
|
2489
|
+
const reply = await requestPeerReply(peer, proposalMessage, channelId, 30_000, ['SPLICE_ACCEPT', 'ERROR']);
|
|
2490
|
+
const acceptance = reply?.payload?.acceptance;
|
|
2491
|
+
if (reply?.type !== 'SPLICE_ACCEPT' || !acceptance) {
|
|
2492
|
+
throw new Error(`Counterparty did not accept the splice (${reply?.type ?? 'timeout'})`);
|
|
2493
|
+
}
|
|
2494
|
+
context.spliceAcceptances.set(proposal.spliceId, acceptance);
|
|
2495
|
+
const spliced = await (0, omnia_splice_1.finalizeSplice)(quiesced, proposal, acceptance, {
|
|
2496
|
+
...(provider.broadcast ? { broadcast: provider.broadcast } : {}),
|
|
2497
|
+
proposerLeaseProvider: provider.wotsLease,
|
|
2498
|
+
});
|
|
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
|
+
});
|
|
2509
|
+
await peer.sendMessage({
|
|
2510
|
+
type: 'SPLICE_FINALIZED',
|
|
2511
|
+
channelId,
|
|
2512
|
+
nonce: proposalMessage.nonce,
|
|
2513
|
+
payload: {
|
|
2514
|
+
channel: spliced,
|
|
2515
|
+
txId: spliced.spliceFundingTxId,
|
|
2516
|
+
spliceId: proposal.spliceId,
|
|
2517
|
+
},
|
|
2518
|
+
});
|
|
2519
|
+
return {
|
|
2520
|
+
success: true,
|
|
2521
|
+
channelId,
|
|
2522
|
+
spliceId: proposal.spliceId,
|
|
2523
|
+
spliceType: spliced.spliceType,
|
|
2524
|
+
totalValue: spliced.totalValue.toString(),
|
|
2525
|
+
balances: Object.fromEntries(Object.entries(spliced.balances).map(([partyId, amount]) => [partyId, amount.toString()])),
|
|
2526
|
+
fundingCoinId: spliced.spliceFundingCoinId,
|
|
2527
|
+
};
|
|
599
2528
|
});
|
|
600
2529
|
};
|
|
601
2530
|
register('totem_omniaSpliceIn', splice('in'), 'omnia/spliceIn');
|
|
@@ -643,6 +2572,18 @@ function createHostMethods(context) {
|
|
|
643
2572
|
: await (0, omnia_router_1.executeMultiHopPayment)(ops, context.channels, parsedRoute, parsedRequest, leaseProviders);
|
|
644
2573
|
if (!result.success)
|
|
645
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
|
+
});
|
|
646
2587
|
return {
|
|
647
2588
|
success: true,
|
|
648
2589
|
preimage: result.preimage,
|
|
@@ -651,5 +2592,5 @@ function createHostMethods(context) {
|
|
|
651
2592
|
});
|
|
652
2593
|
};
|
|
653
2594
|
register('totem_omniaPayMultiHop', payMultiHop, 'omnia/payMultiHop');
|
|
654
|
-
return methods;
|
|
2595
|
+
return observeMethods(methods);
|
|
655
2596
|
}
|