@bopen-io/wallet-toolbox 2.15.0-bopen.2 → 2.15.1
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/CHANGELOG.md +183 -0
- package/README.md +127 -25
- package/docs/client.md +312 -154
- package/docs/monitor.md +2 -2
- package/docs/no-send-expiry.md +7 -2
- package/docs/services.md +143 -60
- package/docs/wallet.md +208 -117
- package/out/src/CWIStyleWalletManager.d.ts +36 -5
- package/out/src/CWIStyleWalletManager.d.ts.map +1 -1
- package/out/src/CWIStyleWalletManager.js +356 -60
- package/out/src/CWIStyleWalletManager.js.map +1 -1
- package/out/src/Wallet.d.ts.map +1 -1
- package/out/src/Wallet.js +8 -3
- package/out/src/Wallet.js.map +1 -1
- package/out/src/WalletAuthenticationManager.d.ts +3 -0
- package/out/src/WalletAuthenticationManager.d.ts.map +1 -1
- package/out/src/WalletAuthenticationManager.js +86 -3
- package/out/src/WalletAuthenticationManager.js.map +1 -1
- package/out/src/WalletPermissionsManager.d.ts +63 -9
- package/out/src/WalletPermissionsManager.d.ts.map +1 -1
- package/out/src/WalletPermissionsManager.js +213 -49
- package/out/src/WalletPermissionsManager.js.map +1 -1
- package/out/src/monitor/Monitor.js +1 -1
- package/out/src/monitor/Monitor.js.map +1 -1
- package/out/src/monitor/monitorValidation.d.ts +2 -1
- package/out/src/monitor/monitorValidation.d.ts.map +1 -1
- package/out/src/monitor/monitorValidation.js +2 -2
- package/out/src/monitor/monitorValidation.js.map +1 -1
- package/out/src/monitor/tasks/TaskNewHeader.d.ts.map +1 -1
- package/out/src/monitor/tasks/TaskNewHeader.js +1 -1
- package/out/src/monitor/tasks/TaskNewHeader.js.map +1 -1
- package/out/src/monitor/tasks/TaskReviewUtxos.d.ts.map +1 -1
- package/out/src/monitor/tasks/TaskReviewUtxos.js +2 -1
- package/out/src/monitor/tasks/TaskReviewUtxos.js.map +1 -1
- package/out/src/sdk/WERR_errors.d.ts +4 -0
- package/out/src/sdk/WERR_errors.d.ts.map +1 -1
- package/out/src/sdk/WERR_errors.js +4 -0
- package/out/src/sdk/WERR_errors.js.map +1 -1
- package/out/src/sdk/WalletErrorFromJson.d.ts.map +1 -1
- package/out/src/sdk/WalletErrorFromJson.js +23 -2
- package/out/src/sdk/WalletErrorFromJson.js.map +1 -1
- package/out/src/sdk/WalletStorage.interfaces.d.ts +11 -0
- package/out/src/sdk/WalletStorage.interfaces.d.ts.map +1 -1
- package/out/src/sdk/types.d.ts +12 -1
- package/out/src/sdk/types.d.ts.map +1 -1
- package/out/src/sdk/types.js +19 -2
- package/out/src/sdk/types.js.map +1 -1
- package/out/src/services/Services.d.ts.map +1 -1
- package/out/src/services/Services.js +5 -3
- package/out/src/services/Services.js.map +1 -1
- package/out/src/services/chaintracker/BHServiceClient.js +2 -2
- package/out/src/services/chaintracker/BHServiceClient.js.map +1 -1
- package/out/src/services/chaintracker/chaintracks/Chaintracks.js +2 -2
- package/out/src/services/chaintracker/chaintracks/Chaintracks.js.map +1 -1
- package/out/src/services/chaintracker/chaintracks/ChaintracksServiceClient.d.ts.map +1 -1
- package/out/src/services/chaintracker/chaintracks/ChaintracksServiceClient.js +3 -1
- package/out/src/services/chaintracker/chaintracks/ChaintracksServiceClient.js.map +1 -1
- package/out/src/services/chaintracker/chaintracks/GoChaintracksServiceClient.d.ts.map +1 -1
- package/out/src/services/chaintracker/chaintracks/GoChaintracksServiceClient.js +3 -1
- package/out/src/services/chaintracker/chaintracks/GoChaintracksServiceClient.js.map +1 -1
- package/out/src/services/chaintracker/chaintracks/Ingest/BulkIngestorBase.js +1 -1
- package/out/src/services/chaintracker/chaintracks/Ingest/BulkIngestorBase.js.map +1 -1
- package/out/src/services/chaintracker/chaintracks/Ingest/LiveIngestorBase.js +1 -1
- package/out/src/services/chaintracker/chaintracks/Ingest/LiveIngestorBase.js.map +1 -1
- package/out/src/services/chaintracker/chaintracks/Ingest/LiveIngestorChaintracksSSE.js +1 -1
- package/out/src/services/chaintracker/chaintracks/Ingest/LiveIngestorChaintracksSSE.js.map +1 -1
- package/out/src/services/chaintracker/chaintracks/Ingest/WhatsOnChainIngestorWs.d.ts.map +1 -1
- package/out/src/services/chaintracker/chaintracks/Ingest/WhatsOnChainIngestorWs.js +2 -0
- package/out/src/services/chaintracker/chaintracks/Ingest/WhatsOnChainIngestorWs.js.map +1 -1
- package/out/src/services/chaintracker/chaintracks/Storage/BulkStorageBase.js +2 -2
- package/out/src/services/chaintracker/chaintracks/Storage/BulkStorageBase.js.map +1 -1
- package/out/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageBase.js +6 -6
- package/out/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageBase.js.map +1 -1
- package/out/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageIdb.js +2 -2
- package/out/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageIdb.js.map +1 -1
- package/out/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageKnex.js +2 -2
- package/out/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageKnex.js.map +1 -1
- package/out/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageNoDb.d.ts +1 -0
- package/out/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageNoDb.d.ts.map +1 -1
- package/out/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageNoDb.js +11 -3
- package/out/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageNoDb.js.map +1 -1
- package/out/src/services/chaintracker/chaintracks/util/BulkFileDataManager.d.ts.map +1 -1
- package/out/src/services/chaintracker/chaintracks/util/BulkFileDataManager.js +13 -9
- package/out/src/services/chaintracker/chaintracks/util/BulkFileDataManager.js.map +1 -1
- package/out/src/services/chaintracker/chaintracks/util/BulkFilesReader.d.ts.map +1 -1
- package/out/src/services/chaintracker/chaintracks/util/BulkFilesReader.js +2 -2
- package/out/src/services/chaintracker/chaintracks/util/BulkFilesReader.js.map +1 -1
- package/out/src/services/chaintracker/chaintracks/util/InlineBulkFileDataValidator.d.ts.map +1 -1
- package/out/src/services/chaintracker/chaintracks/util/InlineBulkFileDataValidator.js +2 -2
- package/out/src/services/chaintracker/chaintracks/util/InlineBulkFileDataValidator.js.map +1 -1
- package/out/src/services/chaintracker/chaintracks/util/blockHeaderUtilities.d.ts +23 -5
- package/out/src/services/chaintracker/chaintracks/util/blockHeaderUtilities.d.ts.map +1 -1
- package/out/src/services/chaintracker/chaintracks/util/blockHeaderUtilities.js +58 -16
- package/out/src/services/chaintracker/chaintracks/util/blockHeaderUtilities.js.map +1 -1
- package/out/src/services/createDefaultWalletServicesOptions.d.ts +6 -3
- package/out/src/services/createDefaultWalletServicesOptions.d.ts.map +1 -1
- package/out/src/services/createDefaultWalletServicesOptions.js +18 -6
- package/out/src/services/createDefaultWalletServicesOptions.js.map +1 -1
- package/out/src/services/networkConfig.d.ts +7 -0
- package/out/src/services/networkConfig.d.ts.map +1 -1
- package/out/src/services/networkConfig.js +25 -2
- package/out/src/services/networkConfig.js.map +1 -1
- package/out/src/services/providers/SdkWhatsOnChain.d.ts +8 -2
- package/out/src/services/providers/SdkWhatsOnChain.d.ts.map +1 -1
- package/out/src/services/providers/SdkWhatsOnChain.js +17 -6
- package/out/src/services/providers/SdkWhatsOnChain.js.map +1 -1
- package/out/src/services/validateMerklePathResult.d.ts +4 -3
- package/out/src/services/validateMerklePathResult.d.ts.map +1 -1
- package/out/src/services/validateMerklePathResult.js +6 -6
- package/out/src/services/validateMerklePathResult.js.map +1 -1
- package/out/src/signer/actionBatch/ActionBatchWorkspace.d.ts.map +1 -1
- package/out/src/signer/actionBatch/ActionBatchWorkspace.js +3 -2
- package/out/src/signer/actionBatch/ActionBatchWorkspace.js.map +1 -1
- package/out/src/signer/methods/buildSignableTransaction.d.ts.map +1 -1
- package/out/src/signer/methods/buildSignableTransaction.js +7 -3
- package/out/src/signer/methods/buildSignableTransaction.js.map +1 -1
- package/out/src/signer/methods/createAction.d.ts +32 -1
- package/out/src/signer/methods/createAction.d.ts.map +1 -1
- package/out/src/signer/methods/createAction.js +56 -1
- package/out/src/signer/methods/createAction.js.map +1 -1
- package/out/src/signer/methods/internalizeAction.js +1 -1
- package/out/src/signer/methods/internalizeAction.js.map +1 -1
- package/out/src/signer/methods/signAction.js +8 -1
- package/out/src/signer/methods/signAction.js.map +1 -1
- package/out/src/storage/StorageIdb.js +4 -4
- package/out/src/storage/StorageIdb.js.map +1 -1
- package/out/src/storage/StorageKnex.d.ts +6 -2
- package/out/src/storage/StorageKnex.d.ts.map +1 -1
- package/out/src/storage/StorageKnex.js +114 -0
- package/out/src/storage/StorageKnex.js.map +1 -1
- package/out/src/storage/WalletStorageManager.d.ts.map +1 -1
- package/out/src/storage/WalletStorageManager.js +5 -0
- package/out/src/storage/WalletStorageManager.js.map +1 -1
- package/out/src/storage/methods/ListOutputsSpecOp.d.ts.map +1 -1
- package/out/src/storage/methods/ListOutputsSpecOp.js +84 -0
- package/out/src/storage/methods/ListOutputsSpecOp.js.map +1 -1
- package/out/src/storage/methods/attemptToPostReqsToNetwork.d.ts.map +1 -1
- package/out/src/storage/methods/attemptToPostReqsToNetwork.js +7 -3
- package/out/src/storage/methods/attemptToPostReqsToNetwork.js.map +1 -1
- package/out/src/storage/methods/createAction.d.ts +5 -0
- package/out/src/storage/methods/createAction.d.ts.map +1 -1
- package/out/src/storage/methods/createAction.js +13 -6
- package/out/src/storage/methods/createAction.js.map +1 -1
- package/out/src/storage/methods/generateChange.d.ts +6 -0
- package/out/src/storage/methods/generateChange.d.ts.map +1 -1
- package/out/src/storage/methods/generateChange.js +10 -3
- package/out/src/storage/methods/generateChange.js.map +1 -1
- package/out/src/storage/methods/internalizeAction.d.ts +8 -1
- package/out/src/storage/methods/internalizeAction.d.ts.map +1 -1
- package/out/src/storage/methods/internalizeAction.js +106 -42
- package/out/src/storage/methods/internalizeAction.js.map +1 -1
- package/out/src/storage/methods/listOutputsKnex.js +1 -1
- package/out/src/storage/methods/listOutputsKnex.js.map +1 -1
- package/out/src/storage/methods/noSendExpiry.d.ts.map +1 -1
- package/out/src/storage/methods/noSendExpiry.js +14 -4
- package/out/src/storage/methods/noSendExpiry.js.map +1 -1
- package/out/src/storage/remoting/StorageClientBase.d.ts.map +1 -1
- package/out/src/storage/remoting/StorageClientBase.js +15 -5
- package/out/src/storage/remoting/StorageClientBase.js.map +1 -1
- package/out/src/storage/remoting/StorageServer.d.ts.map +1 -1
- package/out/src/storage/remoting/StorageServer.js +12 -1
- package/out/src/storage/remoting/StorageServer.js.map +1 -1
- package/out/src/storage/remoting/syncChunkBinary.d.ts +10 -0
- package/out/src/storage/remoting/syncChunkBinary.d.ts.map +1 -1
- package/out/src/storage/remoting/syncChunkBinary.js +15 -11
- package/out/src/storage/remoting/syncChunkBinary.js.map +1 -1
- package/out/src/storage/schema/KnexMigrations.d.ts +1 -0
- package/out/src/storage/schema/KnexMigrations.d.ts.map +1 -1
- package/out/src/storage/schema/KnexMigrations.js +26 -4
- package/out/src/storage/schema/KnexMigrations.js.map +1 -1
- package/out/src/storage/sync/liveSyncGuards.d.ts +11 -0
- package/out/src/storage/sync/liveSyncGuards.d.ts.map +1 -0
- package/out/src/storage/sync/liveSyncGuards.js +22 -0
- package/out/src/storage/sync/liveSyncGuards.js.map +1 -0
- package/out/src/utility/actionOutputAuthorizations.d.ts +10 -0
- package/out/src/utility/actionOutputAuthorizations.d.ts.map +1 -0
- package/out/src/utility/actionOutputAuthorizations.js +25 -0
- package/out/src/utility/actionOutputAuthorizations.js.map +1 -0
- package/out/src/utility/index.all.d.ts +1 -0
- package/out/src/utility/index.all.d.ts.map +1 -1
- package/out/src/utility/index.all.js +1 -0
- package/out/src/utility/index.all.js.map +1 -1
- package/out/src/utility/index.client.d.ts +1 -0
- package/out/src/utility/index.client.d.ts.map +1 -1
- package/out/src/utility/index.client.js +1 -0
- package/out/src/utility/index.client.js.map +1 -1
- package/out/src/utility/numericTotalOutputs.d.ts +7 -0
- package/out/src/utility/numericTotalOutputs.d.ts.map +1 -0
- package/out/src/utility/numericTotalOutputs.js +15 -0
- package/out/src/utility/numericTotalOutputs.js.map +1 -0
- package/out/src/utility/utilityHelpers.d.ts.map +1 -1
- package/out/src/utility/utilityHelpers.js +2 -0
- package/out/src/utility/utilityHelpers.js.map +1 -1
- package/package.json +43 -46
|
@@ -24,6 +24,18 @@ exports.ARGON2ID_MAX_PARALLELISM = 16;
|
|
|
24
24
|
exports.KDF_MAX_HASH_LENGTH = 64;
|
|
25
25
|
exports.PBKDF2_MAX_ITERATIONS = 10_000_000;
|
|
26
26
|
exports.MAX_STATE_SNAPSHOT_BYTES = 16 * 1024 * 1024;
|
|
27
|
+
const MAX_PENDING_REGISTRATION_TOKEN_BYTES = 64 * 1024;
|
|
28
|
+
function readSerializedOutpoint(reader, allowUnpublished) {
|
|
29
|
+
const outpointLen = reader.readVarIntNumStrict(false);
|
|
30
|
+
const currentOutpoint = (0, utils_1.toUTF8)(reader.read(outpointLen));
|
|
31
|
+
if ((currentOutpoint.length === 0 && !allowUnpublished) ||
|
|
32
|
+
currentOutpoint.length > 128 ||
|
|
33
|
+
(currentOutpoint.length > 0 && !/^[^\s.:]+[.:]\d+$/.test(currentOutpoint)) ||
|
|
34
|
+
(allowUnpublished && !reader.eof())) {
|
|
35
|
+
throw new Error('Serialized UMP token contains an invalid outpoint.');
|
|
36
|
+
}
|
|
37
|
+
return currentOutpoint;
|
|
38
|
+
}
|
|
27
39
|
function isPositiveIntegerInRange(value, maximum) {
|
|
28
40
|
return typeof value === 'number' && Number.isInteger(value) && value > 0 && value <= maximum;
|
|
29
41
|
}
|
|
@@ -355,14 +367,15 @@ class OverlayUMPTokenInteractor {
|
|
|
355
367
|
// spent by another candidate's transaction history. Only unrelated (forked)
|
|
356
368
|
// tokens remain indeterminate.
|
|
357
369
|
if (matchingTokens.length > 1) {
|
|
358
|
-
const
|
|
370
|
+
const pinnedLineage = await this.findPinnedLineage(matchingTokens, resolution.answer.outputs, options?.pinnedOutpoint);
|
|
371
|
+
const newest = this.resolveNewestToken(pinnedLineage ?? matchingTokens, resolution.answer.outputs);
|
|
359
372
|
if (newest != null) {
|
|
360
373
|
this.lookupDone(lookupKind, 'found', diagnostics, startedAt, {
|
|
361
374
|
supersededTokens: matchingTokens.length - 1
|
|
362
375
|
});
|
|
363
376
|
return newest;
|
|
364
377
|
}
|
|
365
|
-
const pinned = options?.pinnedOutpoint
|
|
378
|
+
const pinned = pinnedLineage == null && options?.pinnedOutpoint
|
|
366
379
|
? matchingTokens.find(token => token.currentOutpoint === options.pinnedOutpoint)
|
|
367
380
|
: undefined;
|
|
368
381
|
if (pinned != null) {
|
|
@@ -388,6 +401,162 @@ class OverlayUMPTokenInteractor {
|
|
|
388
401
|
this.lookupFailed(lookupKind, reason, diagnostics, startedAt);
|
|
389
402
|
throw new UMPTokenLookupError(reason, diagnostics);
|
|
390
403
|
}
|
|
404
|
+
/** Keep a verified pin and its proven descendants together, excluding unrelated forks. */
|
|
405
|
+
async findPinnedLineage(tokens, outputs, pinnedOutpoint) {
|
|
406
|
+
if (pinnedOutpoint == null)
|
|
407
|
+
return undefined;
|
|
408
|
+
const candidates = new Map(tokens.map(token => [token.currentOutpoint, token]));
|
|
409
|
+
const pinSource = candidates.has(pinnedOutpoint) ? this.findLookupTransaction(outputs, pinnedOutpoint) : undefined;
|
|
410
|
+
const relationships = await Promise.all(outputs.map(output => this.pinRelationship(output, candidates, pinnedOutpoint, pinSource)));
|
|
411
|
+
const related = new Set();
|
|
412
|
+
let anchorSeen = false;
|
|
413
|
+
for (const relationship of relationships) {
|
|
414
|
+
if (relationship == null)
|
|
415
|
+
continue;
|
|
416
|
+
anchorSeen = true;
|
|
417
|
+
if (relationship.selected)
|
|
418
|
+
related.add(relationship.outpoint);
|
|
419
|
+
}
|
|
420
|
+
return anchorSeen ? tokens.filter(token => related.has(token.currentOutpoint)) : undefined;
|
|
421
|
+
}
|
|
422
|
+
async pinRelationship(output, candidates, pin, pinSource) {
|
|
423
|
+
try {
|
|
424
|
+
const tx = this.readTokenHistory(output.beef);
|
|
425
|
+
const outpoint = `${tx.id('hex')}.${output.outputIndex}`;
|
|
426
|
+
if (!candidates.has(outpoint))
|
|
427
|
+
return undefined;
|
|
428
|
+
if (outpoint === pin)
|
|
429
|
+
return { outpoint, selected: true };
|
|
430
|
+
const descent = this.descendsFromPin(tx, pin, pinSource);
|
|
431
|
+
if (descent == null)
|
|
432
|
+
return undefined;
|
|
433
|
+
return { outpoint, selected: descent === 'verified' && (await this.hasCompleteUpdateEvidence(tx)) };
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
// A malformed copy cannot establish an anchor or hide a deeper copy.
|
|
437
|
+
return undefined;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
/** Preserve unmined funding-ancestry checks without adding a network dependency. */
|
|
441
|
+
async hasCompleteUpdateEvidence(tx) {
|
|
442
|
+
try {
|
|
443
|
+
return await tx.verify('scripts only');
|
|
444
|
+
}
|
|
445
|
+
catch {
|
|
446
|
+
// An input reference alone does not establish a usable token update.
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
descendsFromPin(tx, pin, pinSource) {
|
|
451
|
+
const pending = [tx];
|
|
452
|
+
const visited = new Set();
|
|
453
|
+
let incomplete = false;
|
|
454
|
+
while (pending.length > 0) {
|
|
455
|
+
const current = pending.pop();
|
|
456
|
+
const txid = current.id('hex');
|
|
457
|
+
if (visited.has(txid))
|
|
458
|
+
continue;
|
|
459
|
+
visited.add(txid);
|
|
460
|
+
for (const [inputIndex] of current.inputs.entries()) {
|
|
461
|
+
const step = this.pinInputDescent(current, inputIndex, pin, pinSource);
|
|
462
|
+
if (step.descent === 'verified')
|
|
463
|
+
return step.descent;
|
|
464
|
+
incomplete = incomplete || step.descent === 'incomplete';
|
|
465
|
+
if (step.predecessor != null)
|
|
466
|
+
pending.push(step.predecessor);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return incomplete ? 'incomplete' : undefined;
|
|
470
|
+
}
|
|
471
|
+
pinInputDescent(tx, inputIndex, pin, pinSource) {
|
|
472
|
+
const input = tx.inputs[inputIndex];
|
|
473
|
+
const sourceTxid = input.sourceTXID ?? input.sourceTransaction?.id('hex');
|
|
474
|
+
const isPin = sourceTxid != null && `${sourceTxid}.${input.sourceOutputIndex}` === pin;
|
|
475
|
+
const source = input.sourceTransaction ?? (isPin ? pinSource : undefined);
|
|
476
|
+
const refused = { descent: isPin ? 'incomplete' : undefined };
|
|
477
|
+
if (source == null || !this.isAuthenticatedUMPOutput(source.outputs[input.sourceOutputIndex]))
|
|
478
|
+
return refused;
|
|
479
|
+
if (!this.verifyUMPSpend(tx, inputIndex, source))
|
|
480
|
+
return refused;
|
|
481
|
+
return isPin ? { descent: 'verified' } : { predecessor: source };
|
|
482
|
+
}
|
|
483
|
+
isAuthenticatedUMPOutput(output) {
|
|
484
|
+
if (output?.satoshis !== 1)
|
|
485
|
+
return false;
|
|
486
|
+
try {
|
|
487
|
+
const { fields } = decodeAuthenticatedUMPFields(output.lockingScript);
|
|
488
|
+
return (fields.length >= 11 &&
|
|
489
|
+
fields[6]?.length === 32 &&
|
|
490
|
+
fields[7]?.length === 32 &&
|
|
491
|
+
fields.slice(0, 11).every(field => field.length > 0 && field.length <= exports.MAX_STATE_SNAPSHOT_BYTES));
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
// A funding input or unauthenticated predecessor is not a UMP anchor.
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
/** Check token-control continuity at every edge, including confirmed transactions. */
|
|
499
|
+
verifyUMPSpend(tx, inputIndex, source) {
|
|
500
|
+
try {
|
|
501
|
+
const input = tx.inputs[inputIndex];
|
|
502
|
+
const sourceTxid = source.id('hex');
|
|
503
|
+
if (input.unlockingScript == null || (input.sourceTXID != null && input.sourceTXID !== sourceTxid))
|
|
504
|
+
return false;
|
|
505
|
+
const output = source.outputs[input.sourceOutputIndex];
|
|
506
|
+
return new sdk_1.Spend({
|
|
507
|
+
sourceTXID: sourceTxid,
|
|
508
|
+
sourceOutputIndex: input.sourceOutputIndex,
|
|
509
|
+
sourceSatoshis: output.satoshis ?? 0,
|
|
510
|
+
lockingScript: output.lockingScript,
|
|
511
|
+
transactionVersion: tx.version,
|
|
512
|
+
otherInputs: tx.inputs.filter((_, index) => index !== inputIndex),
|
|
513
|
+
unlockingScript: input.unlockingScript,
|
|
514
|
+
inputSequence: input.sequence ?? 0xffffffff,
|
|
515
|
+
inputIndex,
|
|
516
|
+
outputs: tx.outputs,
|
|
517
|
+
lockTime: tx.lockTime
|
|
518
|
+
}).validate();
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
findLookupTransaction(outputs, outpoint) {
|
|
525
|
+
for (const output of outputs) {
|
|
526
|
+
try {
|
|
527
|
+
const tx = this.readTokenHistory(output.beef);
|
|
528
|
+
if (`${tx.id('hex')}.${output.outputIndex}` === outpoint)
|
|
529
|
+
return tx;
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
// A malformed copy does not hide another host's usable anchor.
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return undefined;
|
|
536
|
+
}
|
|
537
|
+
/** Link explicit overlay history even when an ancestor already has a Merkle proof. */
|
|
538
|
+
readTokenHistory(bytes) {
|
|
539
|
+
const tx = sdk_1.Transaction.fromBEEF(bytes);
|
|
540
|
+
const beef = sdk_1.Beef.fromBinary(bytes);
|
|
541
|
+
const pending = [tx];
|
|
542
|
+
const visited = new Set();
|
|
543
|
+
while (pending.length > 0) {
|
|
544
|
+
const current = pending.pop();
|
|
545
|
+
const txid = current.id('hex');
|
|
546
|
+
if (visited.has(txid))
|
|
547
|
+
continue;
|
|
548
|
+
visited.add(txid);
|
|
549
|
+
for (const input of current.inputs) {
|
|
550
|
+
const sourceTxid = input.sourceTXID ?? input.sourceTransaction?.id('hex');
|
|
551
|
+
if (input.sourceTransaction == null && sourceTxid != null) {
|
|
552
|
+
input.sourceTransaction = beef.findAtomicTransaction(sourceTxid);
|
|
553
|
+
}
|
|
554
|
+
if (input.sourceTransaction != null)
|
|
555
|
+
pending.push(input.sourceTransaction);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return tx;
|
|
559
|
+
}
|
|
391
560
|
/**
|
|
392
561
|
* Picks the newest rendition among distinct verified tokens, when possible.
|
|
393
562
|
*
|
|
@@ -413,7 +582,7 @@ class OverlayUMPTokenInteractor {
|
|
|
413
582
|
const evidenceByCandidate = new Map();
|
|
414
583
|
for (const output of outputs) {
|
|
415
584
|
try {
|
|
416
|
-
const tx =
|
|
585
|
+
const tx = this.readTokenHistory(output.beef);
|
|
417
586
|
const outpoint = `${tx.id('hex')}.${output.outputIndex}`;
|
|
418
587
|
if (!candidates.has(outpoint))
|
|
419
588
|
continue;
|
|
@@ -916,6 +1085,8 @@ class CWIStyleWalletManager {
|
|
|
916
1085
|
* Optional function to fund a new Wallet after the new-user flow.
|
|
917
1086
|
*/
|
|
918
1087
|
newWalletFunder;
|
|
1088
|
+
/** Persists only encrypted UMP fields before an irreversible funding attempt. */
|
|
1089
|
+
pendingRegistrationTokenSaver;
|
|
919
1090
|
/**
|
|
920
1091
|
* Builds the underlying wallet for a specific profile.
|
|
921
1092
|
*/
|
|
@@ -932,6 +1103,8 @@ class CWIStyleWalletManager {
|
|
|
932
1103
|
* The current UMP token in use.
|
|
933
1104
|
*/
|
|
934
1105
|
currentUMPToken;
|
|
1106
|
+
/** Unpublished token retained through an interrupted new-user registration. */
|
|
1107
|
+
pendingRegistrationToken;
|
|
935
1108
|
/**
|
|
936
1109
|
* Temporarily retained presentation key.
|
|
937
1110
|
*/
|
|
@@ -952,6 +1125,7 @@ class CWIStyleWalletManager {
|
|
|
952
1125
|
* List of loaded non-default profiles.
|
|
953
1126
|
*/
|
|
954
1127
|
profiles = [];
|
|
1128
|
+
pendingProfileNames = new Set();
|
|
955
1129
|
/**
|
|
956
1130
|
* The underlying wallet instance for the *active* profile.
|
|
957
1131
|
*/
|
|
@@ -975,8 +1149,9 @@ class CWIStyleWalletManager {
|
|
|
975
1149
|
* @param newWalletFunder Optional function to fund a new wallet.
|
|
976
1150
|
* @param stateSnapshot Optional previously saved state snapshot.
|
|
977
1151
|
* @param kdfConfig Optional KDF configuration for new UMP tokens.
|
|
1152
|
+
* @param pendingRegistrationTokenSaver Optional durable store for an unpublished UMP token.
|
|
978
1153
|
*/
|
|
979
|
-
constructor(...[adminOriginator, walletBuilder, interactor, recoveryKeySaver, passwordRetriever, newWalletFunder, stateSnapshot, kdfConfig, telemetry]) {
|
|
1154
|
+
constructor(...[adminOriginator, walletBuilder, interactor, recoveryKeySaver, passwordRetriever, newWalletFunder, stateSnapshot, kdfConfig, telemetry, pendingRegistrationTokenSaver]) {
|
|
980
1155
|
this.adminOriginator = adminOriginator;
|
|
981
1156
|
this.walletBuilder = walletBuilder;
|
|
982
1157
|
this.telemetry = new sdk_1.Telemetry(telemetry);
|
|
@@ -985,6 +1160,7 @@ class CWIStyleWalletManager {
|
|
|
985
1160
|
this.passwordRetriever = passwordRetriever;
|
|
986
1161
|
this.authenticated = false;
|
|
987
1162
|
this.newWalletFunder = newWalletFunder;
|
|
1163
|
+
this.pendingRegistrationTokenSaver = pendingRegistrationTokenSaver;
|
|
988
1164
|
// Initialize KDF config with Argon2id defaults for v3 tokens
|
|
989
1165
|
const kdfAlgorithm = kdfConfig?.algorithm ?? 'argon2id';
|
|
990
1166
|
this.kdfConfig = {
|
|
@@ -1027,6 +1203,40 @@ class CWIStyleWalletManager {
|
|
|
1027
1203
|
}
|
|
1028
1204
|
}
|
|
1029
1205
|
// --- Authentication Methods ---
|
|
1206
|
+
/**
|
|
1207
|
+
* Restores an unpublished UMP token before authentication. The token contains
|
|
1208
|
+
* an encrypted wallet root, but no plaintext root, snapshot key, or on-chain outpoint.
|
|
1209
|
+
*/
|
|
1210
|
+
loadPendingRegistrationToken(bytes) {
|
|
1211
|
+
if (this.authenticated || this.authenticationFlow !== 'unknown' || this.currentUMPToken != null) {
|
|
1212
|
+
throw new Error('Pending registration token must be loaded before authentication.');
|
|
1213
|
+
}
|
|
1214
|
+
if (!Array.isArray(bytes) ||
|
|
1215
|
+
bytes.length === 0 ||
|
|
1216
|
+
bytes.length > MAX_PENDING_REGISTRATION_TOKEN_BYTES ||
|
|
1217
|
+
bytes.some(byte => !Number.isInteger(byte) || byte < 0 || byte > 255)) {
|
|
1218
|
+
throw new Error('Pending registration token is invalid.');
|
|
1219
|
+
}
|
|
1220
|
+
try {
|
|
1221
|
+
const token = this.deserializeUMPToken(bytes, true);
|
|
1222
|
+
if (token.currentOutpoint != null ||
|
|
1223
|
+
token.umpVersion !== 3 ||
|
|
1224
|
+
!isValidKdfConfig(token.passwordKdf) ||
|
|
1225
|
+
token.profilesEncrypted != null ||
|
|
1226
|
+
token.passwordSalt.length !== 32 ||
|
|
1227
|
+
token.presentationHash.length !== 32 ||
|
|
1228
|
+
token.recoveryHash.length !== 32) {
|
|
1229
|
+
throw new Error('Invalid unpublished token fields.');
|
|
1230
|
+
}
|
|
1231
|
+
this.pendingRegistrationToken = token;
|
|
1232
|
+
}
|
|
1233
|
+
catch {
|
|
1234
|
+
throw new Error('Pending registration token is invalid.');
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
hasPendingRegistrationToken() {
|
|
1238
|
+
return this.pendingRegistrationToken != null;
|
|
1239
|
+
}
|
|
1030
1240
|
/**
|
|
1031
1241
|
* Provides the presentation key. A WAB operator pin may be supplied by the
|
|
1032
1242
|
* authentication manager; normal lookup and lineage resolution always run
|
|
@@ -1044,6 +1254,7 @@ class CWIStyleWalletManager {
|
|
|
1044
1254
|
}
|
|
1045
1255
|
this.authenticationFlow = 'unknown';
|
|
1046
1256
|
const hash = (0, Hash_1.sha256)(key);
|
|
1257
|
+
const pendingTokenMatches = this.pendingRegistrationToken == null || (0, utils_1.toHex)(this.pendingRegistrationToken.presentationHash) === (0, utils_1.toHex)(hash);
|
|
1047
1258
|
const startedAt = Date.now();
|
|
1048
1259
|
this.telemetry.capture({
|
|
1049
1260
|
name: 'wallet-toolbox.authentication.account-lookup.started',
|
|
@@ -1070,14 +1281,19 @@ class CWIStyleWalletManager {
|
|
|
1070
1281
|
}
|
|
1071
1282
|
if (token == null) {
|
|
1072
1283
|
// No token found -> New user
|
|
1284
|
+
if (!pendingTokenMatches) {
|
|
1285
|
+
throw new Error('Pending registration token belongs to another presentation key.');
|
|
1286
|
+
}
|
|
1073
1287
|
this.authenticationFlow = 'new-user';
|
|
1074
1288
|
this.presentationKey = key;
|
|
1289
|
+
this.currentUMPToken = this.pendingRegistrationToken;
|
|
1075
1290
|
}
|
|
1076
1291
|
else {
|
|
1077
1292
|
// Found token -> existing user
|
|
1078
1293
|
this.authenticationFlow = 'existing-user';
|
|
1079
1294
|
this.presentationKey = key;
|
|
1080
1295
|
this.currentUMPToken = token;
|
|
1296
|
+
this.clearPublishedPendingRegistration(token);
|
|
1081
1297
|
}
|
|
1082
1298
|
this.telemetry.capture({
|
|
1083
1299
|
name: 'wallet-toolbox.authentication.account-lookup.completed',
|
|
@@ -1139,6 +1355,62 @@ class CWIStyleWalletManager {
|
|
|
1139
1355
|
}
|
|
1140
1356
|
if (this.presentationKey == null)
|
|
1141
1357
|
throw new Error('No presentation key provided for new-user flow.');
|
|
1358
|
+
let newToken;
|
|
1359
|
+
let rootPrimaryKey;
|
|
1360
|
+
if (this.pendingRegistrationToken != null) {
|
|
1361
|
+
newToken = this.pendingRegistrationToken;
|
|
1362
|
+
rootPrimaryKey = await this.unlockPendingRegistrationToken(newToken, this.presentationKey, password);
|
|
1363
|
+
}
|
|
1364
|
+
else {
|
|
1365
|
+
const generated = await this.buildPendingRegistrationToken(password, this.presentationKey);
|
|
1366
|
+
newToken = generated.token;
|
|
1367
|
+
rootPrimaryKey = generated.rootPrimaryKey;
|
|
1368
|
+
this.pendingRegistrationToken = newToken;
|
|
1369
|
+
}
|
|
1370
|
+
this.currentUMPToken = newToken;
|
|
1371
|
+
// The caller must durably store the same encrypted token before a faucet
|
|
1372
|
+
// output can be redeemed. Repeating the write also repairs a prior failed
|
|
1373
|
+
// persistence attempt before any retry reaches the funder.
|
|
1374
|
+
if (this.pendingRegistrationTokenSaver != null) {
|
|
1375
|
+
await this.pendingRegistrationTokenSaver(this.serializeUMPToken(newToken, true));
|
|
1376
|
+
}
|
|
1377
|
+
await this.setupRoot(rootPrimaryKey, undefined, false);
|
|
1378
|
+
await this.activateProfile(exports.DEFAULT_PROFILE_ID);
|
|
1379
|
+
// Fund the *default* wallet if funder provided
|
|
1380
|
+
if (this.newWalletFunder != null && this.underlying != null) {
|
|
1381
|
+
try {
|
|
1382
|
+
await this.newWalletFunder(this.presentationKey, this.underlying, this.adminOriginator);
|
|
1383
|
+
}
|
|
1384
|
+
catch (error) {
|
|
1385
|
+
this.telemetry.capture({
|
|
1386
|
+
name: 'wallet-toolbox.authentication.new-wallet-funding.failed',
|
|
1387
|
+
component: CWI_COMPONENT,
|
|
1388
|
+
severity: 'error',
|
|
1389
|
+
error: new Error('New wallet funding failed.')
|
|
1390
|
+
});
|
|
1391
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1392
|
+
throw new Error(`Failed to fund new wallet before publishing UMP token: ${message}`, { cause: error });
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
if (this.underlying == null)
|
|
1396
|
+
throw new Error('Default profile wallet not built before attempting to publish UMP token.');
|
|
1397
|
+
newToken.currentOutpoint = await this.UMPTokenInteractor.buildAndSend(this.underlying, this.adminOriginator, newToken);
|
|
1398
|
+
this.pendingRegistrationToken = undefined;
|
|
1399
|
+
this.authenticated = true;
|
|
1400
|
+
}
|
|
1401
|
+
async unlockPendingRegistrationToken(token, presentationKey, password) {
|
|
1402
|
+
try {
|
|
1403
|
+
const passwordKey = await derivePasswordKey(token, (0, utils_1.toArray)(password, 'utf8'));
|
|
1404
|
+
const rootPrimaryKey = new sdk_1.SymmetricKey(this.XOR(presentationKey, passwordKey)).decrypt(token.passwordPresentationPrimary);
|
|
1405
|
+
if (rootPrimaryKey.length !== 32)
|
|
1406
|
+
throw new Error('Invalid root key length.');
|
|
1407
|
+
return rootPrimaryKey;
|
|
1408
|
+
}
|
|
1409
|
+
catch {
|
|
1410
|
+
throw new Error('Pending registration token could not be unlocked with this password.');
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
async buildPendingRegistrationToken(password, presentationKey) {
|
|
1142
1414
|
// Generate new keys/salt
|
|
1143
1415
|
const recoveryKey = (0, sdk_1.Random)(32);
|
|
1144
1416
|
await this.recoveryKeySaver(recoveryKey);
|
|
@@ -1147,52 +1419,31 @@ class CWIStyleWalletManager {
|
|
|
1147
1419
|
const rootPrimaryKey = (0, sdk_1.Random)(32);
|
|
1148
1420
|
const rootPrivilegedKey = (0, sdk_1.Random)(32);
|
|
1149
1421
|
// Build XOR-combined symmetric keys
|
|
1150
|
-
const presentationPassword = new sdk_1.SymmetricKey(this.XOR(
|
|
1151
|
-
const presentationRecovery = new sdk_1.SymmetricKey(this.XOR(
|
|
1422
|
+
const presentationPassword = new sdk_1.SymmetricKey(this.XOR(presentationKey, passwordKey));
|
|
1423
|
+
const presentationRecovery = new sdk_1.SymmetricKey(this.XOR(presentationKey, recoveryKey));
|
|
1152
1424
|
const recoveryPassword = new sdk_1.SymmetricKey(this.XOR(recoveryKey, passwordKey));
|
|
1153
1425
|
const primaryPassword = new sdk_1.SymmetricKey(this.XOR(rootPrimaryKey, passwordKey));
|
|
1154
1426
|
const tempPrivilegedKeyManager = new PrivilegedKeyManager_1.PrivilegedKeyManager(async () => new sdk_1.PrivateKey(rootPrivilegedKey));
|
|
1155
1427
|
const wrapKey = async (plaintext) => (await tempPrivilegedKeyManager.encrypt({ plaintext, protocolID: [2, 'admin key wrapping'], keyID: '1' }))
|
|
1156
1428
|
.ciphertext;
|
|
1157
1429
|
// Build new UMP token (v3 with KDF metadata, no profiles initially)
|
|
1158
|
-
const
|
|
1430
|
+
const token = {
|
|
1159
1431
|
passwordSalt,
|
|
1160
1432
|
passwordPresentationPrimary: presentationPassword.encrypt(rootPrimaryKey),
|
|
1161
1433
|
passwordRecoveryPrimary: recoveryPassword.encrypt(rootPrimaryKey),
|
|
1162
1434
|
presentationRecoveryPrimary: presentationRecovery.encrypt(rootPrimaryKey),
|
|
1163
1435
|
passwordPrimaryPrivileged: primaryPassword.encrypt(rootPrivilegedKey),
|
|
1164
1436
|
presentationRecoveryPrivileged: presentationRecovery.encrypt(rootPrivilegedKey),
|
|
1165
|
-
presentationHash: (0, Hash_1.sha256)(
|
|
1437
|
+
presentationHash: (0, Hash_1.sha256)(presentationKey),
|
|
1166
1438
|
recoveryHash: (0, Hash_1.sha256)(recoveryKey),
|
|
1167
|
-
presentationKeyEncrypted: await wrapKey(
|
|
1439
|
+
presentationKeyEncrypted: await wrapKey(presentationKey),
|
|
1168
1440
|
passwordKeyEncrypted: await wrapKey(passwordKey),
|
|
1169
1441
|
recoveryKeyEncrypted: await wrapKey(recoveryKey),
|
|
1170
1442
|
profilesEncrypted: undefined,
|
|
1171
1443
|
umpVersion: 3,
|
|
1172
1444
|
passwordKdf: this.kdfConfig
|
|
1173
1445
|
};
|
|
1174
|
-
|
|
1175
|
-
await this.setupRoot(rootPrimaryKey);
|
|
1176
|
-
await this.switchProfile(exports.DEFAULT_PROFILE_ID);
|
|
1177
|
-
// Fund the *default* wallet if funder provided
|
|
1178
|
-
if (this.newWalletFunder != null && this.underlying != null) {
|
|
1179
|
-
try {
|
|
1180
|
-
await this.newWalletFunder(this.presentationKey, this.underlying, this.adminOriginator);
|
|
1181
|
-
}
|
|
1182
|
-
catch (error) {
|
|
1183
|
-
this.telemetry.capture({
|
|
1184
|
-
name: 'wallet-toolbox.authentication.new-wallet-funding.failed',
|
|
1185
|
-
component: CWI_COMPONENT,
|
|
1186
|
-
severity: 'error',
|
|
1187
|
-
error: new Error('New wallet funding failed.')
|
|
1188
|
-
});
|
|
1189
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1190
|
-
throw new Error(`Failed to fund new wallet before publishing UMP token: ${message}`, { cause: error });
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
if (this.underlying == null)
|
|
1194
|
-
throw new Error('Default profile wallet not built before attempting to publish UMP token.');
|
|
1195
|
-
this.currentUMPToken.currentOutpoint = await this.UMPTokenInteractor.buildAndSend(this.underlying, this.adminOriginator, newToken);
|
|
1446
|
+
return { token, rootPrimaryKey };
|
|
1196
1447
|
}
|
|
1197
1448
|
/**
|
|
1198
1449
|
* Provides the recovery key.
|
|
@@ -1325,6 +1576,7 @@ class CWIStyleWalletManager {
|
|
|
1325
1576
|
// Setup root infrastructure, load profiles, and switch to the loaded active profile
|
|
1326
1577
|
await this.setupRoot(rootPrimaryKey); // Will automatically load profiles
|
|
1327
1578
|
await this.switchProfile(activeProfileId); // Switch to the profile saved in the snapshot
|
|
1579
|
+
this.clearPublishedPendingRegistration(token);
|
|
1328
1580
|
this.authenticationFlow = 'existing-user'; // Loading implies existing user
|
|
1329
1581
|
this.telemetry.capture({
|
|
1330
1582
|
name: 'wallet-toolbox.snapshot.loaded',
|
|
@@ -1373,6 +1625,15 @@ class CWIStyleWalletManager {
|
|
|
1373
1625
|
this.saveSnapshot();
|
|
1374
1626
|
return true;
|
|
1375
1627
|
}
|
|
1628
|
+
clearPublishedPendingRegistration(token) {
|
|
1629
|
+
// A conflicting wallet root can share the same presentation key; match the
|
|
1630
|
+
// encrypted root field before discarding the unpublished token.
|
|
1631
|
+
if (this.pendingRegistrationToken != null &&
|
|
1632
|
+
(0, utils_1.toHex)(this.pendingRegistrationToken.presentationHash) === (0, utils_1.toHex)(token.presentationHash) &&
|
|
1633
|
+
(0, utils_1.toHex)(this.pendingRegistrationToken.passwordPresentationPrimary) === (0, utils_1.toHex)(token.passwordPresentationPrimary)) {
|
|
1634
|
+
this.pendingRegistrationToken = undefined;
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1376
1637
|
/**
|
|
1377
1638
|
* Destroys the wallet state, clearing keys, tokens, and profiles.
|
|
1378
1639
|
*/
|
|
@@ -1382,6 +1643,7 @@ class CWIStyleWalletManager {
|
|
|
1382
1643
|
this.authenticated = false;
|
|
1383
1644
|
this.rootPrimaryKey = undefined;
|
|
1384
1645
|
this.currentUMPToken = undefined;
|
|
1646
|
+
this.pendingRegistrationToken = undefined;
|
|
1385
1647
|
this.presentationKey = undefined;
|
|
1386
1648
|
this.recoveryKey = undefined;
|
|
1387
1649
|
this.profiles = [];
|
|
@@ -1434,7 +1696,10 @@ class CWIStyleWalletManager {
|
|
|
1434
1696
|
throw new Error('Wallet not fully initialized or authenticated.');
|
|
1435
1697
|
}
|
|
1436
1698
|
// Ensure name is unique (including 'default')
|
|
1437
|
-
|
|
1699
|
+
const normalizedName = name.toLowerCase();
|
|
1700
|
+
if (name === 'default' ||
|
|
1701
|
+
this.pendingProfileNames.has(normalizedName) ||
|
|
1702
|
+
this.profiles.some(p => p.name.toLowerCase() === normalizedName)) {
|
|
1438
1703
|
throw new Error(`Profile name "${name}" is already in use.`);
|
|
1439
1704
|
}
|
|
1440
1705
|
const newProfile = {
|
|
@@ -1444,14 +1709,26 @@ class CWIStyleWalletManager {
|
|
|
1444
1709
|
privilegedPad: (0, sdk_1.Random)(32),
|
|
1445
1710
|
createdAt: Math.floor(Date.now() / 1000)
|
|
1446
1711
|
};
|
|
1447
|
-
|
|
1448
|
-
//
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1712
|
+
// Reserve the case-insensitive name before the first await. The profile
|
|
1713
|
+
// itself remains unpublished until updateFactors accepts the new token.
|
|
1714
|
+
this.pendingProfileNames.add(normalizedName);
|
|
1715
|
+
try {
|
|
1716
|
+
// Only keep the new profile once the UMP token holds it, so a failed update
|
|
1717
|
+
// leaves the list as it was and a retry is not rejected as a duplicate.
|
|
1718
|
+
// Update the UMP token with the new profile list
|
|
1719
|
+
await this.updateFactors(this.currentUMPToken.passwordSalt,
|
|
1720
|
+
// Need to re-derive/decrypt factors needed for re-encryption
|
|
1721
|
+
await this.getFactor('passwordKey'), await this.getFactor('presentationKey'), await this.getFactor('recoveryKey'), this.rootPrimaryKey, await this.getFactor('privilegedKey'), // Get ROOT privileged key
|
|
1722
|
+
// Built last, after the awaits above: arguments are evaluated left to
|
|
1723
|
+
// right, and updateFactors captures the token it consumes before its first
|
|
1724
|
+
// await, so this list and that token are one consistent snapshot. Built
|
|
1725
|
+
// earlier, a profile added by an overlapping call would be dropped.
|
|
1726
|
+
[...this.profiles, newProfile]);
|
|
1727
|
+
return newProfile.id;
|
|
1728
|
+
}
|
|
1729
|
+
finally {
|
|
1730
|
+
this.pendingProfileNames.delete(normalizedName);
|
|
1731
|
+
}
|
|
1455
1732
|
}
|
|
1456
1733
|
/**
|
|
1457
1734
|
* Deletes a profile by its ID.
|
|
@@ -1474,16 +1751,27 @@ class CWIStyleWalletManager {
|
|
|
1474
1751
|
if (profileIndex === -1) {
|
|
1475
1752
|
throw new Error('Profile not found.');
|
|
1476
1753
|
}
|
|
1477
|
-
// Remove the profile
|
|
1478
|
-
|
|
1754
|
+
// Remove the profile only once the UMP token no longer holds it, so a
|
|
1755
|
+
// failed update leaves the profile (and the active wallet) in place.
|
|
1756
|
+
const wasActive = this.activeProfileId.every((x, i) => x === profileId[i]);
|
|
1479
1757
|
// If the deleted profile was active, switch to default
|
|
1480
|
-
if (
|
|
1758
|
+
if (wasActive) {
|
|
1481
1759
|
await this.switchProfile(exports.DEFAULT_PROFILE_ID); // This rebuilds the wallet
|
|
1482
1760
|
}
|
|
1483
1761
|
// Update the UMP token
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1762
|
+
try {
|
|
1763
|
+
await this.updateFactors(this.currentUMPToken.passwordSalt, await this.getFactor('passwordKey'), await this.getFactor('presentationKey'), await this.getFactor('recoveryKey'), this.rootPrimaryKey, await this.getFactor('privilegedKey'), // Get ROOT privileged key
|
|
1764
|
+
// Built last, after the awaits, for the same reason as in addProfile.
|
|
1765
|
+
this.profiles.filter(p => !p.id.every((x, i) => x === profileId[i])));
|
|
1766
|
+
}
|
|
1767
|
+
catch (error) {
|
|
1768
|
+
// Back to the profile we left, unless the token was published before the
|
|
1769
|
+
// failure. A failed switch back must not hide the original error.
|
|
1770
|
+
if (wasActive && this.profiles.some(p => p.id.every((x, i) => x === profileId[i]))) {
|
|
1771
|
+
await this.switchProfile(profileId).catch(() => { });
|
|
1772
|
+
}
|
|
1773
|
+
throw error;
|
|
1774
|
+
}
|
|
1487
1775
|
}
|
|
1488
1776
|
/**
|
|
1489
1777
|
* Switches the active profile. This re-derives keys and rebuilds the underlying wallet.
|
|
@@ -1494,6 +1782,13 @@ class CWIStyleWalletManager {
|
|
|
1494
1782
|
if (!this.authenticated || this.rootPrimaryKey == null || this.rootPrivilegedKeyManager == null) {
|
|
1495
1783
|
throw new Error('Cannot switch profile: Wallet not authenticated or root keys missing.');
|
|
1496
1784
|
}
|
|
1785
|
+
await this.activateProfile(profileId);
|
|
1786
|
+
}
|
|
1787
|
+
/** Build an internal profile wallet while an unpublished registration is not yet authenticated. */
|
|
1788
|
+
async activateProfile(profileId) {
|
|
1789
|
+
if (this.rootPrimaryKey == null || this.rootPrivilegedKeyManager == null) {
|
|
1790
|
+
throw new Error('Cannot activate profile: root keys are missing.');
|
|
1791
|
+
}
|
|
1497
1792
|
let profilePrimaryKey;
|
|
1498
1793
|
let profilePrivilegedPad; // Pad for the target profile
|
|
1499
1794
|
if (profileId.every(x => x === 0)) {
|
|
@@ -1770,7 +2065,12 @@ class CWIStyleWalletManager {
|
|
|
1770
2065
|
);
|
|
1771
2066
|
// Update the manager's state
|
|
1772
2067
|
this.currentUMPToken = newTokenData;
|
|
1773
|
-
//
|
|
2068
|
+
// Keep the profiles in step with the token that now holds them. Callers
|
|
2069
|
+
// pass a new list rather than changing this.profiles up front, so a
|
|
2070
|
+
// failed update leaves it untouched.
|
|
2071
|
+
if (profiles != null) {
|
|
2072
|
+
this.profiles = profiles;
|
|
2073
|
+
}
|
|
1774
2074
|
}
|
|
1775
2075
|
finally {
|
|
1776
2076
|
// Switch back if we temporarily switched
|
|
@@ -1789,8 +2089,8 @@ class CWIStyleWalletManager {
|
|
|
1789
2089
|
* Serializes a UMP token to binary format (Version 3 with KDF metadata, Version 2 with profiles).
|
|
1790
2090
|
* V3 Layout: [1 byte version=3] + [11 * (varint len + bytes) for standard fields] + [1 byte profile_flag] + [IF flag=1 THEN varint len + profile bytes] + [1 byte kdf_flag] + [IF flag=1 THEN kdf metadata] + [varint len + outpoint bytes]
|
|
1791
2091
|
*/
|
|
1792
|
-
serializeUMPToken(token) {
|
|
1793
|
-
if (!token.currentOutpoint) {
|
|
2092
|
+
serializeUMPToken(token, allowUnpublished = false) {
|
|
2093
|
+
if (!token.currentOutpoint && !allowUnpublished) {
|
|
1794
2094
|
throw new Error('Token must have outpoint for serialization');
|
|
1795
2095
|
}
|
|
1796
2096
|
const writer = new utils_1.Writer();
|
|
@@ -1847,7 +2147,7 @@ class CWIStyleWalletManager {
|
|
|
1847
2147
|
writer.writeUInt8(0); // Flag indicating no KDF metadata
|
|
1848
2148
|
}
|
|
1849
2149
|
// Write outpoint string
|
|
1850
|
-
const outpointBytes = (0, utils_1.toArray)(token.currentOutpoint, 'utf8');
|
|
2150
|
+
const outpointBytes = (0, utils_1.toArray)(token.currentOutpoint ?? '', 'utf8');
|
|
1851
2151
|
writer.writeVarIntNum(outpointBytes.length);
|
|
1852
2152
|
writer.write(outpointBytes);
|
|
1853
2153
|
return writer.toArray();
|
|
@@ -1855,7 +2155,7 @@ class CWIStyleWalletManager {
|
|
|
1855
2155
|
/**
|
|
1856
2156
|
* Deserializes a UMP token from binary format (Handles Version 1, 2, and 3).
|
|
1857
2157
|
*/
|
|
1858
|
-
deserializeUMPToken(bin) {
|
|
2158
|
+
deserializeUMPToken(bin, allowUnpublished = false) {
|
|
1859
2159
|
const reader = new utils_1.Reader(bin);
|
|
1860
2160
|
const version = reader.readUInt8();
|
|
1861
2161
|
if (version !== 1 && version !== 2 && version !== 3) {
|
|
@@ -1913,12 +2213,7 @@ class CWIStyleWalletManager {
|
|
|
1913
2213
|
}
|
|
1914
2214
|
}
|
|
1915
2215
|
// Read outpoint string
|
|
1916
|
-
const
|
|
1917
|
-
const outpointBytes = reader.read(outpointLen);
|
|
1918
|
-
const currentOutpoint = (0, utils_1.toUTF8)(outpointBytes);
|
|
1919
|
-
if (currentOutpoint.length > 128 || !/^[^\s.:]+[.:]\d+$/.test(currentOutpoint)) {
|
|
1920
|
-
throw new Error('Serialized UMP token contains an invalid outpoint.');
|
|
1921
|
-
}
|
|
2216
|
+
const currentOutpoint = readSerializedOutpoint(reader, allowUnpublished);
|
|
1922
2217
|
const token = {
|
|
1923
2218
|
passwordSalt,
|
|
1924
2219
|
passwordPresentationPrimary,
|
|
@@ -1932,7 +2227,7 @@ class CWIStyleWalletManager {
|
|
|
1932
2227
|
passwordKeyEncrypted,
|
|
1933
2228
|
recoveryKeyEncrypted,
|
|
1934
2229
|
profilesEncrypted,
|
|
1935
|
-
currentOutpoint,
|
|
2230
|
+
...(currentOutpoint.length > 0 ? { currentOutpoint } : {}),
|
|
1936
2231
|
umpVersion,
|
|
1937
2232
|
passwordKdf
|
|
1938
2233
|
};
|
|
@@ -1946,7 +2241,7 @@ class CWIStyleWalletManager {
|
|
|
1946
2241
|
* @param rootPrimaryKey The user's root primary key (32 bytes).
|
|
1947
2242
|
* @param ephemeralRootPrivilegedKey Optional root privileged key (e.g., during recovery flows).
|
|
1948
2243
|
*/
|
|
1949
|
-
|
|
2244
|
+
setupRoot(rootKey, ephemeralRootPrivilegedKey, authenticated = true) {
|
|
1950
2245
|
if (this.currentUMPToken == null) {
|
|
1951
2246
|
throw new Error('A UMP token must exist before setting up root infrastructure!');
|
|
1952
2247
|
}
|
|
@@ -2006,9 +2301,10 @@ class CWIStyleWalletManager {
|
|
|
2006
2301
|
throw new Error(`Failed to load profiles: ${message}`, { cause: error });
|
|
2007
2302
|
}
|
|
2008
2303
|
}
|
|
2009
|
-
this.authenticated =
|
|
2304
|
+
this.authenticated = authenticated;
|
|
2010
2305
|
// Note: We don't call switchProfile here anymore.
|
|
2011
2306
|
// It's called by the auth methods (providePassword/provideRecoveryKey) or loadSnapshot after this.
|
|
2307
|
+
return Promise.resolve();
|
|
2012
2308
|
}
|
|
2013
2309
|
/*
|
|
2014
2310
|
* ---------------------------------------------------------------------------------------
|