@cogcoin/client 0.5.1 → 0.5.3
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/README.md +6 -2
- package/dist/app-paths.d.ts +1 -0
- package/dist/app-paths.js +2 -0
- package/dist/bitcoind/client/follow-loop.d.ts +3 -4
- package/dist/bitcoind/client/follow-loop.js +16 -2
- package/dist/bitcoind/client/internal-types.d.ts +10 -2
- package/dist/bitcoind/client/managed-client.js +1 -1
- package/dist/cli/output.js +1 -1
- package/dist/cli/parse.d.ts +1 -1
- package/dist/cli/parse.js +4 -4
- package/dist/wallet/descriptor-normalization.d.ts +42 -0
- package/dist/wallet/descriptor-normalization.js +108 -0
- package/dist/wallet/lifecycle.d.ts +14 -0
- package/dist/wallet/lifecycle.js +168 -36
- package/dist/wallet/mining/control.js +4 -4
- package/dist/wallet/mining/runner.js +2 -2
- package/dist/wallet/read/context.d.ts +3 -0
- package/dist/wallet/read/context.js +120 -23
- package/dist/wallet/runtime.d.ts +1 -0
- package/dist/wallet/runtime.js +1 -0
- package/dist/wallet/state/crypto.js +12 -24
- package/dist/wallet/state/explicit-lock.d.ts +4 -0
- package/dist/wallet/state/explicit-lock.js +19 -0
- package/dist/wallet/tx/anchor.js +1 -0
- package/dist/wallet/tx/cog.js +3 -0
- package/dist/wallet/tx/domain-admin.js +1 -0
- package/dist/wallet/tx/domain-market.js +3 -0
- package/dist/wallet/tx/field.js +2 -0
- package/dist/wallet/tx/register.js +1 -0
- package/dist/wallet/tx/reputation.js +1 -0
- package/dist/wallet/types.d.ts +5 -0
- package/package.json +3 -2
package/dist/wallet/lifecycle.js
CHANGED
|
@@ -8,6 +8,7 @@ import { resolveManagedServicePaths } from "../bitcoind/service-paths.js";
|
|
|
8
8
|
import { createRpcClient } from "../bitcoind/node.js";
|
|
9
9
|
import { openSqliteStore } from "../sqlite/index.js";
|
|
10
10
|
import { readPortableWalletArchive, writePortableWalletArchive } from "./archive.js";
|
|
11
|
+
import { normalizeWalletDescriptorState, persistNormalizedWalletDescriptorStateIfNeeded, persistWalletStateUpdate, resolveNormalizedWalletDescriptorState, stripDescriptorChecksum, } from "./descriptor-normalization.js";
|
|
11
12
|
import { acquireFileLock } from "./fs/lock.js";
|
|
12
13
|
import { createInternalCoreWalletPassphrase, createMnemonicConfirmationChallenge, deriveWalletMaterialFromMnemonic, generateWalletMaterial, } from "./material.js";
|
|
13
14
|
import { resolveWalletRuntimePathsForTesting } from "./runtime.js";
|
|
@@ -16,6 +17,7 @@ import { loadClientConfig } from "./mining/config.js";
|
|
|
16
17
|
import { inspectMiningHookState } from "./mining/hooks.js";
|
|
17
18
|
import { loadMiningRuntimeStatus, saveMiningRuntimeStatus } from "./mining/runtime-artifacts.js";
|
|
18
19
|
import { normalizeMiningStateRecord } from "./mining/state.js";
|
|
20
|
+
import { clearWalletExplicitLock, loadWalletExplicitLock, saveWalletExplicitLock, } from "./state/explicit-lock.js";
|
|
19
21
|
import { clearUnlockSession, loadUnlockSession, saveUnlockSession } from "./state/session.js";
|
|
20
22
|
import { createDefaultWalletSecretProvider, createWalletRootId, createWalletSecretReference, } from "./state/provider.js";
|
|
21
23
|
import { loadWalletState, saveWalletState } from "./state/storage.js";
|
|
@@ -23,9 +25,6 @@ export const DEFAULT_UNLOCK_DURATION_MS = 15 * 60 * 1000;
|
|
|
23
25
|
function sanitizeWalletName(walletRootId) {
|
|
24
26
|
return `cogcoin-${walletRootId}`.replace(/[^a-zA-Z0-9._-]+/g, "-").slice(0, 63);
|
|
25
27
|
}
|
|
26
|
-
function stripDescriptorChecksum(descriptor) {
|
|
27
|
-
return descriptor.replace(/#[A-Za-z0-9]+$/, "");
|
|
28
|
-
}
|
|
29
28
|
async function pathExists(path) {
|
|
30
29
|
try {
|
|
31
30
|
await access(path, constants.F_OK);
|
|
@@ -144,6 +143,54 @@ function createUnlockSession(state, unlockUntilUnixMs, secretKeyId, nowUnixMs) {
|
|
|
144
143
|
wrappedSessionKeyMaterial: secretKeyId,
|
|
145
144
|
};
|
|
146
145
|
}
|
|
146
|
+
function createWalletExplicitLock(walletRootId, nowUnixMs) {
|
|
147
|
+
return {
|
|
148
|
+
schemaVersion: 1,
|
|
149
|
+
walletRootId,
|
|
150
|
+
lockedAtUnixMs: nowUnixMs,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
async function normalizeUnlockedWalletStateIfNeeded(options) {
|
|
154
|
+
let state = options.state;
|
|
155
|
+
let session = options.session;
|
|
156
|
+
let source = options.source;
|
|
157
|
+
if (options.dataDir !== undefined) {
|
|
158
|
+
const node = await (options.attachService ?? attachOrStartManagedBitcoindService)({
|
|
159
|
+
dataDir: options.dataDir,
|
|
160
|
+
chain: "main",
|
|
161
|
+
startHeight: 0,
|
|
162
|
+
walletRootId: state.walletRootId,
|
|
163
|
+
});
|
|
164
|
+
try {
|
|
165
|
+
const normalized = await persistNormalizedWalletDescriptorStateIfNeeded({
|
|
166
|
+
state,
|
|
167
|
+
access: {
|
|
168
|
+
provider: options.provider,
|
|
169
|
+
secretReference: createWalletSecretReference(state.walletRootId),
|
|
170
|
+
},
|
|
171
|
+
session,
|
|
172
|
+
paths: options.paths,
|
|
173
|
+
nowUnixMs: options.nowUnixMs,
|
|
174
|
+
replacePrimary: options.source === "backup",
|
|
175
|
+
rpc: (options.rpcFactory ?? createRpcClient)(node.rpc),
|
|
176
|
+
});
|
|
177
|
+
state = normalized.state;
|
|
178
|
+
session = normalized.session ?? session;
|
|
179
|
+
source = normalized.changed ? "primary" : options.source;
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
await node.stop?.().catch(() => undefined);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
session,
|
|
187
|
+
state: {
|
|
188
|
+
...state,
|
|
189
|
+
miningState: normalizeMiningStateRecord(state.miningState),
|
|
190
|
+
},
|
|
191
|
+
source,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
147
194
|
function createPortableWalletArchivePayload(state, exportedAtUnixMs) {
|
|
148
195
|
return {
|
|
149
196
|
schemaVersion: 1,
|
|
@@ -518,22 +565,16 @@ function createStoppedBackgroundRuntimeSnapshot(snapshot, nowUnixMs) {
|
|
|
518
565
|
};
|
|
519
566
|
}
|
|
520
567
|
async function persistRepairState(options) {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
primaryPath: options.paths.walletStatePath,
|
|
531
|
-
backupPath: options.paths.walletStateBackupPath,
|
|
532
|
-
}, nextState, {
|
|
533
|
-
provider: options.provider,
|
|
534
|
-
secretReference: createWalletSecretReference(nextState.walletRootId),
|
|
568
|
+
return await persistWalletStateUpdate({
|
|
569
|
+
state: options.state,
|
|
570
|
+
access: {
|
|
571
|
+
provider: options.provider,
|
|
572
|
+
secretReference: createWalletSecretReference(options.state.walletRootId),
|
|
573
|
+
},
|
|
574
|
+
paths: options.paths,
|
|
575
|
+
nowUnixMs: options.nowUnixMs,
|
|
576
|
+
replacePrimary: options.replacePrimary,
|
|
535
577
|
});
|
|
536
|
-
return nextState;
|
|
537
578
|
}
|
|
538
579
|
async function stopBackgroundMiningForRepair(options) {
|
|
539
580
|
const pid = options.snapshot.backgroundWorkerPid;
|
|
@@ -630,13 +671,12 @@ async function importDescriptorIntoManagedCoreWallet(state, provider, paths, dat
|
|
|
630
671
|
await createManagedWalletReplica(rpc, state.walletRootId, {
|
|
631
672
|
managedWalletPassphrase: state.managedCoreWallet.internalPassphrase,
|
|
632
673
|
});
|
|
633
|
-
const
|
|
634
|
-
const publicDescriptor = await rpc.getDescriptorInfo(state.descriptor.publicExternal);
|
|
674
|
+
const normalizedDescriptors = await resolveNormalizedWalletDescriptorState(state, rpc);
|
|
635
675
|
const walletName = sanitizeWalletName(state.walletRootId);
|
|
636
676
|
await rpc.walletPassphrase(walletName, state.managedCoreWallet.internalPassphrase, 10);
|
|
637
677
|
try {
|
|
638
678
|
const importResults = await rpc.importDescriptors(walletName, [{
|
|
639
|
-
desc:
|
|
679
|
+
desc: normalizedDescriptors.privateExternal,
|
|
640
680
|
timestamp: state.walletBirthTime,
|
|
641
681
|
active: false,
|
|
642
682
|
internal: false,
|
|
@@ -649,12 +689,12 @@ async function importDescriptorIntoManagedCoreWallet(state, provider, paths, dat
|
|
|
649
689
|
finally {
|
|
650
690
|
await rpc.walletLock(walletName).catch(() => undefined);
|
|
651
691
|
}
|
|
652
|
-
const derivedFunding = await rpc.deriveAddresses(
|
|
692
|
+
const derivedFunding = await rpc.deriveAddresses(normalizedDescriptors.publicExternal, [0, 0]);
|
|
653
693
|
if (derivedFunding[0] !== state.funding.address) {
|
|
654
694
|
throw new Error("wallet_funding_address_verification_failed");
|
|
655
695
|
}
|
|
656
696
|
const descriptors = await rpc.listDescriptors(walletName);
|
|
657
|
-
const importedDescriptor = descriptors.descriptors.find((entry) => entry.desc ===
|
|
697
|
+
const importedDescriptor = descriptors.descriptors.find((entry) => entry.desc === normalizedDescriptors.publicExternal);
|
|
658
698
|
if (importedDescriptor == null) {
|
|
659
699
|
throw new Error("wallet_descriptor_not_present_after_import");
|
|
660
700
|
}
|
|
@@ -666,7 +706,7 @@ async function importDescriptorIntoManagedCoreWallet(state, provider, paths, dat
|
|
|
666
706
|
privateKeysEnabled: true,
|
|
667
707
|
created: false,
|
|
668
708
|
proofStatus: "ready",
|
|
669
|
-
descriptorChecksum:
|
|
709
|
+
descriptorChecksum: normalizedDescriptors.checksum,
|
|
670
710
|
fundingAddress0: state.funding.address,
|
|
671
711
|
fundingScriptPubKeyHex0: state.funding.scriptPubKeyHex,
|
|
672
712
|
message: null,
|
|
@@ -677,14 +717,14 @@ async function importDescriptorIntoManagedCoreWallet(state, provider, paths, dat
|
|
|
677
717
|
lastWrittenAtUnixMs: nowUnixMs,
|
|
678
718
|
descriptor: {
|
|
679
719
|
...state.descriptor,
|
|
680
|
-
privateExternal:
|
|
681
|
-
publicExternal:
|
|
682
|
-
checksum:
|
|
720
|
+
privateExternal: normalizedDescriptors.privateExternal,
|
|
721
|
+
publicExternal: normalizedDescriptors.publicExternal,
|
|
722
|
+
checksum: normalizedDescriptors.checksum,
|
|
683
723
|
},
|
|
684
724
|
managedCoreWallet: {
|
|
685
725
|
...state.managedCoreWallet,
|
|
686
726
|
walletName,
|
|
687
|
-
descriptorChecksum:
|
|
727
|
+
descriptorChecksum: normalizedDescriptors.checksum,
|
|
688
728
|
fundingAddress0: verifiedReplica.fundingAddress0 ?? null,
|
|
689
729
|
fundingScriptPubKeyHex0: verifiedReplica.fundingScriptPubKeyHex0 ?? null,
|
|
690
730
|
proofStatus: "ready",
|
|
@@ -782,7 +822,7 @@ export async function loadUnlockedWalletState(options = {}) {
|
|
|
782
822
|
const nowUnixMs = options.nowUnixMs ?? Date.now();
|
|
783
823
|
const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
|
|
784
824
|
try {
|
|
785
|
-
|
|
825
|
+
let session = await loadUnlockSession(paths.walletUnlockSessionPath, {
|
|
786
826
|
provider,
|
|
787
827
|
});
|
|
788
828
|
if (session.unlockUntilUnixMs <= nowUnixMs) {
|
|
@@ -800,19 +840,97 @@ export async function loadUnlockedWalletState(options = {}) {
|
|
|
800
840
|
await clearUnlockSession(paths.walletUnlockSessionPath);
|
|
801
841
|
return null;
|
|
802
842
|
}
|
|
803
|
-
return {
|
|
843
|
+
return await normalizeUnlockedWalletStateIfNeeded({
|
|
844
|
+
provider,
|
|
804
845
|
session,
|
|
805
|
-
state:
|
|
806
|
-
...loaded.state,
|
|
807
|
-
miningState: normalizeMiningStateRecord(loaded.state.miningState),
|
|
808
|
-
},
|
|
846
|
+
state: loaded.state,
|
|
809
847
|
source: loaded.source,
|
|
810
|
-
|
|
848
|
+
nowUnixMs,
|
|
849
|
+
paths,
|
|
850
|
+
dataDir: options.dataDir,
|
|
851
|
+
attachService: options.attachService,
|
|
852
|
+
rpcFactory: options.rpcFactory,
|
|
853
|
+
});
|
|
811
854
|
}
|
|
812
855
|
catch {
|
|
813
856
|
return null;
|
|
814
857
|
}
|
|
815
858
|
}
|
|
859
|
+
export async function loadOrAutoUnlockWalletState(options = {}) {
|
|
860
|
+
const provider = options.provider ?? createDefaultWalletSecretProvider();
|
|
861
|
+
const nowUnixMs = options.nowUnixMs ?? Date.now();
|
|
862
|
+
const unlockDurationMs = options.unlockDurationMs ?? DEFAULT_UNLOCK_DURATION_MS;
|
|
863
|
+
const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
|
|
864
|
+
const loadExisting = () => loadUnlockedWalletState({
|
|
865
|
+
provider,
|
|
866
|
+
nowUnixMs,
|
|
867
|
+
paths,
|
|
868
|
+
dataDir: options.dataDir,
|
|
869
|
+
attachService: options.attachService,
|
|
870
|
+
rpcFactory: options.rpcFactory,
|
|
871
|
+
});
|
|
872
|
+
const existing = await loadExisting();
|
|
873
|
+
if (existing !== null) {
|
|
874
|
+
return existing;
|
|
875
|
+
}
|
|
876
|
+
const loadAndMaybeAutoUnlock = async () => {
|
|
877
|
+
const reloaded = await loadExisting();
|
|
878
|
+
if (reloaded !== null) {
|
|
879
|
+
return reloaded;
|
|
880
|
+
}
|
|
881
|
+
let loaded;
|
|
882
|
+
try {
|
|
883
|
+
loaded = await loadWalletState({
|
|
884
|
+
primaryPath: paths.walletStatePath,
|
|
885
|
+
backupPath: paths.walletStateBackupPath,
|
|
886
|
+
}, {
|
|
887
|
+
provider,
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
catch {
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
const explicitLock = await loadWalletExplicitLock(paths.walletExplicitLockPath);
|
|
894
|
+
if (explicitLock !== null) {
|
|
895
|
+
if (explicitLock.walletRootId === loaded.state.walletRootId) {
|
|
896
|
+
await clearUnlockSession(paths.walletUnlockSessionPath);
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
await clearWalletExplicitLock(paths.walletExplicitLockPath);
|
|
900
|
+
}
|
|
901
|
+
const secretReference = createWalletSecretReference(loaded.state.walletRootId);
|
|
902
|
+
const unlockUntilUnixMs = nowUnixMs + unlockDurationMs;
|
|
903
|
+
const session = createUnlockSession(loaded.state, unlockUntilUnixMs, secretReference.keyId, nowUnixMs);
|
|
904
|
+
await saveUnlockSession(paths.walletUnlockSessionPath, session, {
|
|
905
|
+
provider,
|
|
906
|
+
secretReference,
|
|
907
|
+
});
|
|
908
|
+
return await normalizeUnlockedWalletStateIfNeeded({
|
|
909
|
+
provider,
|
|
910
|
+
session,
|
|
911
|
+
state: loaded.state,
|
|
912
|
+
source: loaded.source,
|
|
913
|
+
nowUnixMs,
|
|
914
|
+
paths,
|
|
915
|
+
dataDir: options.dataDir,
|
|
916
|
+
attachService: options.attachService,
|
|
917
|
+
rpcFactory: options.rpcFactory,
|
|
918
|
+
});
|
|
919
|
+
};
|
|
920
|
+
if (options.controlLockHeld) {
|
|
921
|
+
return await loadAndMaybeAutoUnlock();
|
|
922
|
+
}
|
|
923
|
+
const controlLock = await acquireFileLock(paths.walletControlLockPath, {
|
|
924
|
+
purpose: "wallet-auto-unlock",
|
|
925
|
+
walletRootId: null,
|
|
926
|
+
});
|
|
927
|
+
try {
|
|
928
|
+
return await loadAndMaybeAutoUnlock();
|
|
929
|
+
}
|
|
930
|
+
finally {
|
|
931
|
+
await controlLock.release();
|
|
932
|
+
}
|
|
933
|
+
}
|
|
816
934
|
export async function initializeWallet(options) {
|
|
817
935
|
if (!options.prompter.isInteractive) {
|
|
818
936
|
throw new Error("wallet_init_requires_tty");
|
|
@@ -863,6 +981,7 @@ export async function initializeWallet(options) {
|
|
|
863
981
|
});
|
|
864
982
|
const verifiedState = await importDescriptorIntoManagedCoreWallet(initialState, provider, paths, options.dataDir, nowUnixMs, options.attachService, options.rpcFactory);
|
|
865
983
|
const unlockUntilUnixMs = nowUnixMs + unlockDurationMs;
|
|
984
|
+
await clearWalletExplicitLock(paths.walletExplicitLockPath);
|
|
866
985
|
await saveUnlockSession(paths.walletUnlockSessionPath, createUnlockSession(verifiedState, unlockUntilUnixMs, secretReference.keyId, nowUnixMs), {
|
|
867
986
|
provider,
|
|
868
987
|
secretReference,
|
|
@@ -896,6 +1015,7 @@ export async function unlockWallet(options = {}) {
|
|
|
896
1015
|
});
|
|
897
1016
|
const secretReference = createWalletSecretReference(loaded.state.walletRootId);
|
|
898
1017
|
const unlockUntilUnixMs = nowUnixMs + unlockDurationMs;
|
|
1018
|
+
await clearWalletExplicitLock(paths.walletExplicitLockPath);
|
|
899
1019
|
await saveUnlockSession(paths.walletUnlockSessionPath, createUnlockSession(loaded.state, unlockUntilUnixMs, secretReference.keyId, nowUnixMs), {
|
|
900
1020
|
provider,
|
|
901
1021
|
secretReference,
|
|
@@ -912,6 +1032,7 @@ export async function unlockWallet(options = {}) {
|
|
|
912
1032
|
}
|
|
913
1033
|
export async function lockWallet(options) {
|
|
914
1034
|
const provider = options.provider ?? createDefaultWalletSecretProvider();
|
|
1035
|
+
const nowUnixMs = options.nowUnixMs ?? Date.now();
|
|
915
1036
|
const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
|
|
916
1037
|
const controlLock = await acquireFileLock(paths.walletControlLockPath, {
|
|
917
1038
|
purpose: "wallet-lock",
|
|
@@ -947,6 +1068,9 @@ export async function lockWallet(options) {
|
|
|
947
1068
|
walletRootId = null;
|
|
948
1069
|
}
|
|
949
1070
|
await clearUnlockSession(paths.walletUnlockSessionPath);
|
|
1071
|
+
if (walletRootId !== null) {
|
|
1072
|
+
await saveWalletExplicitLock(paths.walletExplicitLockPath, createWalletExplicitLock(walletRootId, nowUnixMs));
|
|
1073
|
+
}
|
|
950
1074
|
return {
|
|
951
1075
|
walletRootId,
|
|
952
1076
|
coreLocked,
|
|
@@ -968,10 +1092,11 @@ export async function exportWallet(options) {
|
|
|
968
1092
|
walletRootId: null,
|
|
969
1093
|
});
|
|
970
1094
|
try {
|
|
971
|
-
const unlocked = await
|
|
1095
|
+
const unlocked = await loadOrAutoUnlockWalletState({
|
|
972
1096
|
provider,
|
|
973
1097
|
nowUnixMs,
|
|
974
1098
|
paths,
|
|
1099
|
+
controlLockHeld: true,
|
|
975
1100
|
});
|
|
976
1101
|
if (unlocked === null) {
|
|
977
1102
|
throw new Error("wallet_locked");
|
|
@@ -1050,6 +1175,7 @@ export async function importWallet(options) {
|
|
|
1050
1175
|
internalCoreWalletPassphrase: createInternalCoreWalletPassphrase(),
|
|
1051
1176
|
});
|
|
1052
1177
|
await clearUnlockSession(paths.walletUnlockSessionPath);
|
|
1178
|
+
await clearWalletExplicitLock(paths.walletExplicitLockPath);
|
|
1053
1179
|
await saveWalletState({
|
|
1054
1180
|
primaryPath: paths.walletStatePath,
|
|
1055
1181
|
backupPath: paths.walletStateBackupPath,
|
|
@@ -1062,6 +1188,7 @@ export async function importWallet(options) {
|
|
|
1062
1188
|
rpcFactory: options.rpcFactory,
|
|
1063
1189
|
});
|
|
1064
1190
|
const unlockUntilUnixMs = nowUnixMs + unlockDurationMs;
|
|
1191
|
+
await clearWalletExplicitLock(paths.walletExplicitLockPath);
|
|
1065
1192
|
await saveUnlockSession(paths.walletUnlockSessionPath, createUnlockSession(importedState, unlockUntilUnixMs, secretReference.keyId, nowUnixMs), {
|
|
1066
1193
|
provider,
|
|
1067
1194
|
secretReference,
|
|
@@ -1242,6 +1369,11 @@ export async function repairWallet(options) {
|
|
|
1242
1369
|
walletRootId: repairedState.walletRootId,
|
|
1243
1370
|
});
|
|
1244
1371
|
const bitcoindRpc = (options.rpcFactory ?? createRpcClient)(bitcoindHandle.rpc);
|
|
1372
|
+
const normalizedDescriptorState = await normalizeWalletDescriptorState(repairedState, bitcoindRpc);
|
|
1373
|
+
if (normalizedDescriptorState.changed) {
|
|
1374
|
+
repairedState = normalizedDescriptorState.state;
|
|
1375
|
+
repairStateNeedsPersist = true;
|
|
1376
|
+
}
|
|
1245
1377
|
let replica = await verifyManagedCoreWalletReplica(repairedState, options.dataDir, {
|
|
1246
1378
|
nodeHandle: bitcoindHandle,
|
|
1247
1379
|
attachService: options.attachService,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { acquireFileLock } from "../fs/lock.js";
|
|
3
|
-
import {
|
|
3
|
+
import { loadOrAutoUnlockWalletState } from "../lifecycle.js";
|
|
4
4
|
import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
|
|
5
5
|
import { saveWalletState } from "../state/storage.js";
|
|
6
6
|
import { createDefaultWalletSecretProvider, createWalletSecretReference, } from "../state/provider.js";
|
|
@@ -397,7 +397,7 @@ export async function enableMiningHooks(options) {
|
|
|
397
397
|
paths,
|
|
398
398
|
reason: "hooks-enable-mining",
|
|
399
399
|
});
|
|
400
|
-
const unlocked = await
|
|
400
|
+
const unlocked = await loadOrAutoUnlockWalletState({
|
|
401
401
|
provider,
|
|
402
402
|
nowUnixMs,
|
|
403
403
|
paths,
|
|
@@ -556,7 +556,7 @@ export async function disableMiningHooks(options) {
|
|
|
556
556
|
paths,
|
|
557
557
|
reason: "hooks-disable-mining",
|
|
558
558
|
});
|
|
559
|
-
const unlocked = await
|
|
559
|
+
const unlocked = await loadOrAutoUnlockWalletState({
|
|
560
560
|
provider,
|
|
561
561
|
nowUnixMs,
|
|
562
562
|
paths,
|
|
@@ -664,7 +664,7 @@ export async function setupBuiltInMining(options) {
|
|
|
664
664
|
paths,
|
|
665
665
|
reason: "mine-setup",
|
|
666
666
|
});
|
|
667
|
-
const unlocked = await
|
|
667
|
+
const unlocked = await loadOrAutoUnlockWalletState({
|
|
668
668
|
provider,
|
|
669
669
|
nowUnixMs,
|
|
670
670
|
paths,
|
|
@@ -10,7 +10,7 @@ import { COG_OPCODES, COG_PREFIX } from "../cogop/constants.js";
|
|
|
10
10
|
import { extractOpReturnPayloadFromScriptHex } from "../tx/register.js";
|
|
11
11
|
import { DEFAULT_WALLET_MUTATION_FEE_RATE_SAT_VB, buildWalletMutationTransaction, isAlreadyAcceptedError, isBroadcastUnknownError, saveWalletStatePreservingUnlock, } from "../tx/common.js";
|
|
12
12
|
import { acquireFileLock } from "../fs/lock.js";
|
|
13
|
-
import {
|
|
13
|
+
import { loadOrAutoUnlockWalletState } from "../lifecycle.js";
|
|
14
14
|
import { isMineableWalletDomain, openWalletReadContext, } from "../read/index.js";
|
|
15
15
|
import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
|
|
16
16
|
import { createDefaultWalletSecretProvider, } from "../state/provider.js";
|
|
@@ -1567,7 +1567,7 @@ async function publishCandidate(options) {
|
|
|
1567
1567
|
};
|
|
1568
1568
|
}
|
|
1569
1569
|
async function ensureBuiltInSetupIfNeeded(options) {
|
|
1570
|
-
const unlocked = await
|
|
1570
|
+
const unlocked = await loadOrAutoUnlockWalletState({
|
|
1571
1571
|
provider: options.provider,
|
|
1572
1572
|
paths: options.paths,
|
|
1573
1573
|
});
|
|
@@ -3,16 +3,19 @@ import { type WalletSecretProvider } from "../state/provider.js";
|
|
|
3
3
|
import type { WalletLocalStateStatus, WalletReadContext } from "./types.js";
|
|
4
4
|
import type { WalletRuntimePaths } from "../runtime.js";
|
|
5
5
|
declare function inspectWalletLocalState(options?: {
|
|
6
|
+
dataDir?: string;
|
|
6
7
|
passphrase?: Uint8Array | string;
|
|
7
8
|
secretProvider?: WalletSecretProvider;
|
|
8
9
|
now?: number;
|
|
9
10
|
paths?: WalletRuntimePaths;
|
|
11
|
+
walletControlLockHeld?: boolean;
|
|
10
12
|
}): Promise<WalletLocalStateStatus>;
|
|
11
13
|
export declare function openWalletReadContext(options: {
|
|
12
14
|
dataDir: string;
|
|
13
15
|
databasePath: string;
|
|
14
16
|
walletStatePassphrase?: Uint8Array | string;
|
|
15
17
|
secretProvider?: WalletSecretProvider;
|
|
18
|
+
walletControlLockHeld?: boolean;
|
|
16
19
|
startupTimeoutMs?: number;
|
|
17
20
|
now?: number;
|
|
18
21
|
paths?: WalletRuntimePaths;
|
|
@@ -5,12 +5,14 @@ import { createRpcClient } from "../../bitcoind/node.js";
|
|
|
5
5
|
import { UNINITIALIZED_WALLET_ROOT_ID } from "../../bitcoind/service-paths.js";
|
|
6
6
|
import { attachOrStartManagedBitcoindService, probeManagedBitcoindService, } from "../../bitcoind/service.js";
|
|
7
7
|
import {} from "../../bitcoind/types.js";
|
|
8
|
-
import {
|
|
8
|
+
import { loadOrAutoUnlockWalletState, verifyManagedCoreWalletReplica, } from "../lifecycle.js";
|
|
9
|
+
import { persistNormalizedWalletDescriptorStateIfNeeded } from "../descriptor-normalization.js";
|
|
9
10
|
import { inspectMiningControlPlane } from "../mining/index.js";
|
|
10
11
|
import { normalizeMiningStateRecord } from "../mining/state.js";
|
|
11
12
|
import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
|
|
13
|
+
import { loadWalletExplicitLock } from "../state/explicit-lock.js";
|
|
12
14
|
import { loadWalletState } from "../state/storage.js";
|
|
13
|
-
import { createDefaultWalletSecretProvider, } from "../state/provider.js";
|
|
15
|
+
import { createDefaultWalletSecretProvider, createWalletSecretReference, } from "../state/provider.js";
|
|
14
16
|
import { createWalletReadModel } from "./project.js";
|
|
15
17
|
const DEFAULT_SERVICE_START_TIMEOUT_MS = 10_000;
|
|
16
18
|
const STALE_HEARTBEAT_THRESHOLD_MS = 15_000;
|
|
@@ -23,6 +25,64 @@ async function pathExists(path) {
|
|
|
23
25
|
return false;
|
|
24
26
|
}
|
|
25
27
|
}
|
|
28
|
+
function isLockedWalletAccessError(error) {
|
|
29
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
30
|
+
return message === "wallet_envelope_missing_secret_provider"
|
|
31
|
+
|| message.startsWith("wallet_secret_missing_")
|
|
32
|
+
|| message.startsWith("wallet_secret_provider_");
|
|
33
|
+
}
|
|
34
|
+
function describeLockedWalletMessage(options) {
|
|
35
|
+
if (options.explicitlyLocked) {
|
|
36
|
+
return "Wallet state exists but is explicitly locked until `cogcoin unlock` is run.";
|
|
37
|
+
}
|
|
38
|
+
const message = options.accessError instanceof Error ? options.accessError.message : String(options.accessError ?? "");
|
|
39
|
+
if (message === "wallet_envelope_missing_secret_provider") {
|
|
40
|
+
return "Wallet state exists but requires the local wallet-state passphrase.";
|
|
41
|
+
}
|
|
42
|
+
if (message.startsWith("wallet_secret_provider_")) {
|
|
43
|
+
return "Wallet state exists but the local secret provider is unavailable.";
|
|
44
|
+
}
|
|
45
|
+
if (message.startsWith("wallet_secret_missing_")) {
|
|
46
|
+
return "Wallet state exists but its local secret-provider material is unavailable.";
|
|
47
|
+
}
|
|
48
|
+
return options.hasUnlockSessionFile
|
|
49
|
+
? "Wallet state exists but the unlock session is expired, invalid, or belongs to a different wallet root."
|
|
50
|
+
: "Wallet state exists but is currently locked.";
|
|
51
|
+
}
|
|
52
|
+
async function normalizeLoadedWalletStateForRead(options) {
|
|
53
|
+
if (options.dataDir === undefined) {
|
|
54
|
+
return options.loaded;
|
|
55
|
+
}
|
|
56
|
+
const node = await attachOrStartManagedBitcoindService({
|
|
57
|
+
dataDir: options.dataDir,
|
|
58
|
+
chain: "main",
|
|
59
|
+
startHeight: 0,
|
|
60
|
+
walletRootId: options.loaded.state.walletRootId,
|
|
61
|
+
});
|
|
62
|
+
try {
|
|
63
|
+
const access = typeof options.access === "string" || options.access instanceof Uint8Array
|
|
64
|
+
? options.access
|
|
65
|
+
: {
|
|
66
|
+
provider: options.access.provider,
|
|
67
|
+
secretReference: createWalletSecretReference(options.loaded.state.walletRootId),
|
|
68
|
+
};
|
|
69
|
+
const normalized = await persistNormalizedWalletDescriptorStateIfNeeded({
|
|
70
|
+
state: options.loaded.state,
|
|
71
|
+
access,
|
|
72
|
+
paths: options.paths,
|
|
73
|
+
nowUnixMs: options.now,
|
|
74
|
+
replacePrimary: options.loaded.source === "backup",
|
|
75
|
+
rpc: createRpcClient(node.rpc),
|
|
76
|
+
});
|
|
77
|
+
return {
|
|
78
|
+
source: normalized.changed ? "primary" : options.loaded.source,
|
|
79
|
+
state: normalized.state,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
await node.stop?.().catch(() => undefined);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
26
86
|
async function inspectWalletLocalState(options = {}) {
|
|
27
87
|
const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
|
|
28
88
|
const now = options.now ?? Date.now();
|
|
@@ -47,21 +107,63 @@ async function inspectWalletLocalState(options = {}) {
|
|
|
47
107
|
if (options.passphrase === undefined) {
|
|
48
108
|
try {
|
|
49
109
|
const provider = options.secretProvider ?? createDefaultWalletSecretProvider();
|
|
50
|
-
const unlocked = await
|
|
110
|
+
const unlocked = await loadOrAutoUnlockWalletState({
|
|
51
111
|
provider,
|
|
52
112
|
nowUnixMs: now,
|
|
53
113
|
paths,
|
|
114
|
+
dataDir: options.dataDir,
|
|
115
|
+
controlLockHeld: options.walletControlLockHeld,
|
|
54
116
|
});
|
|
55
117
|
if (unlocked === null) {
|
|
118
|
+
const explicitLock = await loadWalletExplicitLock(paths.walletExplicitLockPath);
|
|
119
|
+
const hasUnlockSessionFileNow = await pathExists(paths.walletUnlockSessionPath);
|
|
56
120
|
try {
|
|
57
|
-
await loadWalletState({
|
|
121
|
+
const loaded = await loadWalletState({
|
|
58
122
|
primaryPath: paths.walletStatePath,
|
|
59
123
|
backupPath: paths.walletStateBackupPath,
|
|
60
124
|
}, {
|
|
61
125
|
provider,
|
|
62
126
|
});
|
|
127
|
+
await normalizeLoadedWalletStateForRead({
|
|
128
|
+
loaded,
|
|
129
|
+
access: { provider },
|
|
130
|
+
dataDir: options.dataDir,
|
|
131
|
+
now,
|
|
132
|
+
paths,
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
availability: "locked",
|
|
136
|
+
walletRootId: loaded.state.walletRootId,
|
|
137
|
+
state: null,
|
|
138
|
+
source: loaded.source,
|
|
139
|
+
unlockUntilUnixMs: null,
|
|
140
|
+
hasPrimaryStateFile,
|
|
141
|
+
hasBackupStateFile,
|
|
142
|
+
hasUnlockSessionFile: hasUnlockSessionFileNow,
|
|
143
|
+
message: describeLockedWalletMessage({
|
|
144
|
+
explicitlyLocked: explicitLock?.walletRootId === loaded.state.walletRootId,
|
|
145
|
+
hasUnlockSessionFile: hasUnlockSessionFileNow,
|
|
146
|
+
}),
|
|
147
|
+
};
|
|
63
148
|
}
|
|
64
149
|
catch (error) {
|
|
150
|
+
if (isLockedWalletAccessError(error)) {
|
|
151
|
+
return {
|
|
152
|
+
availability: "locked",
|
|
153
|
+
walletRootId: null,
|
|
154
|
+
state: null,
|
|
155
|
+
source: null,
|
|
156
|
+
unlockUntilUnixMs: null,
|
|
157
|
+
hasPrimaryStateFile,
|
|
158
|
+
hasBackupStateFile,
|
|
159
|
+
hasUnlockSessionFile: hasUnlockSessionFileNow,
|
|
160
|
+
message: describeLockedWalletMessage({
|
|
161
|
+
accessError: error,
|
|
162
|
+
explicitlyLocked: false,
|
|
163
|
+
hasUnlockSessionFile: hasUnlockSessionFileNow,
|
|
164
|
+
}),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
65
167
|
return {
|
|
66
168
|
availability: "local-state-corrupt",
|
|
67
169
|
walletRootId: null,
|
|
@@ -70,23 +172,10 @@ async function inspectWalletLocalState(options = {}) {
|
|
|
70
172
|
unlockUntilUnixMs: null,
|
|
71
173
|
hasPrimaryStateFile,
|
|
72
174
|
hasBackupStateFile,
|
|
73
|
-
hasUnlockSessionFile,
|
|
175
|
+
hasUnlockSessionFile: hasUnlockSessionFileNow,
|
|
74
176
|
message: error instanceof Error ? error.message : String(error),
|
|
75
177
|
};
|
|
76
178
|
}
|
|
77
|
-
return {
|
|
78
|
-
availability: "locked",
|
|
79
|
-
walletRootId: null,
|
|
80
|
-
state: null,
|
|
81
|
-
source: null,
|
|
82
|
-
unlockUntilUnixMs: null,
|
|
83
|
-
hasPrimaryStateFile,
|
|
84
|
-
hasBackupStateFile,
|
|
85
|
-
hasUnlockSessionFile,
|
|
86
|
-
message: hasUnlockSessionFile
|
|
87
|
-
? "Wallet state exists but the unlock session is expired, invalid, or belongs to a different wallet root."
|
|
88
|
-
: "Wallet state exists but is currently locked.",
|
|
89
|
-
};
|
|
90
179
|
}
|
|
91
180
|
return {
|
|
92
181
|
availability: "ready",
|
|
@@ -99,7 +188,7 @@ async function inspectWalletLocalState(options = {}) {
|
|
|
99
188
|
unlockUntilUnixMs: unlocked.session.unlockUntilUnixMs,
|
|
100
189
|
hasPrimaryStateFile,
|
|
101
190
|
hasBackupStateFile,
|
|
102
|
-
hasUnlockSessionFile,
|
|
191
|
+
hasUnlockSessionFile: true,
|
|
103
192
|
message: null,
|
|
104
193
|
};
|
|
105
194
|
}
|
|
@@ -118,10 +207,16 @@ async function inspectWalletLocalState(options = {}) {
|
|
|
118
207
|
}
|
|
119
208
|
}
|
|
120
209
|
try {
|
|
121
|
-
const loaded = await
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
210
|
+
const loaded = await normalizeLoadedWalletStateForRead({
|
|
211
|
+
loaded: await loadWalletState({
|
|
212
|
+
primaryPath: paths.walletStatePath,
|
|
213
|
+
backupPath: paths.walletStateBackupPath,
|
|
214
|
+
}, options.passphrase),
|
|
215
|
+
access: options.passphrase,
|
|
216
|
+
dataDir: options.dataDir,
|
|
217
|
+
now,
|
|
218
|
+
paths,
|
|
219
|
+
});
|
|
125
220
|
return {
|
|
126
221
|
availability: "ready",
|
|
127
222
|
walletRootId: loaded.state.walletRootId,
|
|
@@ -408,8 +503,10 @@ export async function openWalletReadContext(options) {
|
|
|
408
503
|
const startupTimeoutMs = options.startupTimeoutMs ?? DEFAULT_SERVICE_START_TIMEOUT_MS;
|
|
409
504
|
const now = options.now ?? Date.now();
|
|
410
505
|
const localState = await inspectWalletLocalState({
|
|
506
|
+
dataDir: options.dataDir,
|
|
411
507
|
passphrase: options.walletStatePassphrase,
|
|
412
508
|
secretProvider: options.secretProvider,
|
|
509
|
+
walletControlLockHeld: options.walletControlLockHeld,
|
|
413
510
|
now,
|
|
414
511
|
paths: options.paths,
|
|
415
512
|
});
|
package/dist/wallet/runtime.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface WalletRuntimePaths {
|
|
|
10
10
|
walletStatePath: string;
|
|
11
11
|
walletStateBackupPath: string;
|
|
12
12
|
walletUnlockSessionPath: string;
|
|
13
|
+
walletExplicitLockPath: string;
|
|
13
14
|
walletControlLockPath: string;
|
|
14
15
|
bitcoindLockPath: string;
|
|
15
16
|
bitcoindStatusPath: string;
|
package/dist/wallet/runtime.js
CHANGED
|
@@ -12,6 +12,7 @@ export function resolveWalletRuntimePathsForTesting(resolution = {}) {
|
|
|
12
12
|
walletStatePath: paths.walletStatePath,
|
|
13
13
|
walletStateBackupPath: paths.walletStateBackupPath,
|
|
14
14
|
walletUnlockSessionPath: paths.walletUnlockSessionPath,
|
|
15
|
+
walletExplicitLockPath: paths.walletExplicitLockPath,
|
|
15
16
|
walletControlLockPath: paths.walletControlLockPath,
|
|
16
17
|
bitcoindLockPath: paths.bitcoindLockPath,
|
|
17
18
|
bitcoindStatusPath: paths.bitcoindStatusPath,
|