@cogcoin/client 0.5.6 → 0.5.8

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.
Files changed (71) hide show
  1. package/README.md +11 -2
  2. package/dist/bitcoind/bootstrap/getblock-archive.d.ts +39 -0
  3. package/dist/bitcoind/bootstrap/getblock-archive.js +548 -0
  4. package/dist/bitcoind/bootstrap.d.ts +1 -0
  5. package/dist/bitcoind/bootstrap.js +1 -0
  6. package/dist/bitcoind/client/factory.js +92 -30
  7. package/dist/bitcoind/client/managed-client.d.ts +1 -1
  8. package/dist/bitcoind/client/managed-client.js +22 -2
  9. package/dist/bitcoind/client/sync-engine.js +7 -0
  10. package/dist/bitcoind/errors.js +18 -0
  11. package/dist/bitcoind/indexer-daemon-main.js +78 -0
  12. package/dist/bitcoind/indexer-daemon.d.ts +3 -1
  13. package/dist/bitcoind/indexer-daemon.js +13 -6
  14. package/dist/bitcoind/node.js +2 -0
  15. package/dist/bitcoind/progress/constants.d.ts +1 -0
  16. package/dist/bitcoind/progress/constants.js +1 -0
  17. package/dist/bitcoind/progress/controller.d.ts +22 -0
  18. package/dist/bitcoind/progress/controller.js +48 -23
  19. package/dist/bitcoind/progress/formatting.js +25 -0
  20. package/dist/bitcoind/progress/render-policy.d.ts +35 -0
  21. package/dist/bitcoind/progress/render-policy.js +81 -0
  22. package/dist/bitcoind/service-paths.js +2 -6
  23. package/dist/bitcoind/service.d.ts +5 -1
  24. package/dist/bitcoind/service.js +92 -54
  25. package/dist/bitcoind/testing.d.ts +2 -1
  26. package/dist/bitcoind/testing.js +2 -1
  27. package/dist/bitcoind/types.d.ts +35 -1
  28. package/dist/cli/commands/follow.js +2 -0
  29. package/dist/cli/commands/getblock-archive-restart.d.ts +5 -0
  30. package/dist/cli/commands/getblock-archive-restart.js +15 -0
  31. package/dist/cli/commands/mining-admin.js +4 -0
  32. package/dist/cli/commands/mining-read.js +8 -5
  33. package/dist/cli/commands/mining-runtime.js +4 -0
  34. package/dist/cli/commands/status.js +2 -0
  35. package/dist/cli/commands/sync.js +2 -0
  36. package/dist/cli/commands/wallet-admin.js +29 -3
  37. package/dist/cli/commands/wallet-mutation.js +57 -4
  38. package/dist/cli/commands/wallet-read.js +2 -0
  39. package/dist/cli/context.js +5 -3
  40. package/dist/cli/mutation-command-groups.d.ts +2 -1
  41. package/dist/cli/mutation-command-groups.js +5 -0
  42. package/dist/cli/mutation-json.d.ts +18 -2
  43. package/dist/cli/mutation-json.js +47 -0
  44. package/dist/cli/mutation-success.d.ts +1 -0
  45. package/dist/cli/mutation-success.js +2 -2
  46. package/dist/cli/output.js +97 -1
  47. package/dist/cli/parse.d.ts +1 -1
  48. package/dist/cli/parse.js +127 -3
  49. package/dist/cli/preview-json.d.ts +10 -1
  50. package/dist/cli/preview-json.js +30 -0
  51. package/dist/cli/prompt.js +1 -1
  52. package/dist/cli/runner.js +20 -0
  53. package/dist/cli/signals.js +1 -1
  54. package/dist/cli/types.d.ts +11 -4
  55. package/dist/cli/wallet-format.js +6 -0
  56. package/dist/wallet/lifecycle.d.ts +18 -1
  57. package/dist/wallet/lifecycle.js +170 -81
  58. package/dist/wallet/mining/visualizer.d.ts +11 -6
  59. package/dist/wallet/mining/visualizer.js +32 -15
  60. package/dist/wallet/reset.js +39 -27
  61. package/dist/wallet/runtime.d.ts +12 -1
  62. package/dist/wallet/runtime.js +53 -11
  63. package/dist/wallet/state/provider.d.ts +1 -0
  64. package/dist/wallet/state/provider.js +119 -3
  65. package/dist/wallet/state/seed-index.d.ts +43 -0
  66. package/dist/wallet/state/seed-index.js +151 -0
  67. package/dist/wallet/tx/anchor.d.ts +22 -0
  68. package/dist/wallet/tx/anchor.js +201 -8
  69. package/dist/wallet/tx/index.d.ts +1 -1
  70. package/dist/wallet/tx/index.js +1 -1
  71. package/package.json +1 -1
@@ -12,6 +12,14 @@ import { runSyncCommand } from "./commands/sync.js";
12
12
  import { runWalletAdminCommand } from "./commands/wallet-admin.js";
13
13
  import { runWalletMutationCommand } from "./commands/wallet-mutation.js";
14
14
  import { runWalletReadCommand } from "./commands/wallet-read.js";
15
+ import { findWalletSeedRecord, loadWalletSeedIndex } from "../wallet/state/seed-index.js";
16
+ function commandUsesExistingWalletSeed(parsed) {
17
+ return parsed.seedName !== null
18
+ && parsed.seedName !== "main"
19
+ && parsed.command !== "restore"
20
+ && parsed.command !== "wallet-delete"
21
+ && parsed.command !== "wallet-restore";
22
+ }
15
23
  export async function runCli(argv, contextOverrides = {}) {
16
24
  const context = createDefaultContext(contextOverrides);
17
25
  let parsed;
@@ -41,6 +49,15 @@ export async function runCli(argv, contextOverrides = {}) {
41
49
  return parsed.help ? 0 : 2;
42
50
  }
43
51
  try {
52
+ if (commandUsesExistingWalletSeed(parsed)) {
53
+ const mainPaths = context.resolveWalletRuntimePaths("main");
54
+ const seedIndex = await loadWalletSeedIndex({
55
+ paths: mainPaths,
56
+ });
57
+ if (seedIndex.seeds.length > 0 && findWalletSeedRecord(seedIndex, parsed.seedName) === null) {
58
+ throw new Error("wallet_seed_not_found");
59
+ }
60
+ }
44
61
  if (parsed.command === "sync") {
45
62
  return runSyncCommand(parsed, context);
46
63
  }
@@ -76,6 +93,7 @@ export async function runCli(argv, contextOverrides = {}) {
76
93
  || parsed.command === "wallet-export"
77
94
  || parsed.command === "wallet-import"
78
95
  || parsed.command === "wallet-init"
96
+ || parsed.command === "wallet-delete"
79
97
  || parsed.command === "wallet-restore"
80
98
  || parsed.command === "wallet-show-mnemonic"
81
99
  || parsed.command === "wallet-unlock"
@@ -83,7 +101,9 @@ export async function runCli(argv, contextOverrides = {}) {
83
101
  return runWalletAdminCommand(parsed, context);
84
102
  }
85
103
  if (parsed.command === "anchor"
104
+ || parsed.command === "anchor-clear"
86
105
  || parsed.command === "domain-anchor"
106
+ || parsed.command === "domain-anchor-clear"
87
107
  || parsed.command === "register"
88
108
  || parsed.command === "domain-register"
89
109
  || parsed.command === "transfer"
@@ -19,7 +19,7 @@ export function createStopSignalWatcher(signalSource, stderr, client, forceExit)
19
19
  };
20
20
  const onFirstSignal = () => {
21
21
  closing = true;
22
- writeLine(stderr, "Stopping managed Cogcoin client...");
22
+ writeLine(stderr, "Detaching from managed Cogcoin client...");
23
23
  void client.close().then(() => {
24
24
  settle(0);
25
25
  }, () => {
@@ -5,17 +5,17 @@ import { attachOrStartManagedBitcoindService, probeManagedBitcoindService, stopM
5
5
  import { openSqliteStore } from "../sqlite/index.js";
6
6
  import type { ClientStoreAdapter } from "../types.js";
7
7
  import type { WalletRuntimePaths } from "../wallet/runtime.js";
8
- import type { exportWallet, WalletPrompter, importWallet, initializeWallet, lockWallet, previewResetWallet, repairWallet, resetWallet, restoreWalletFromMnemonic, showWalletMnemonic, unlockWallet } from "../wallet/lifecycle.js";
8
+ import type { exportWallet, WalletPrompter, importWallet, initializeWallet, deleteImportedWalletSeed, lockWallet, previewResetWallet, repairWallet, resetWallet, restoreWalletFromMnemonic, showWalletMnemonic, unlockWallet } from "../wallet/lifecycle.js";
9
9
  import type { openWalletReadContext } from "../wallet/read/index.js";
10
10
  import { loadWalletExplicitLock } from "../wallet/state/explicit-lock.js";
11
11
  import { loadUnlockSession } from "../wallet/state/session.js";
12
12
  import { loadRawWalletStateEnvelope, loadWalletState } from "../wallet/state/storage.js";
13
13
  import type { WalletSecretProvider } from "../wallet/state/provider.js";
14
14
  import type { disableMiningHooks, enableMiningHooks, followMiningLog, inspectMiningControlPlane, readMiningLog, runForegroundMining, setupBuiltInMining, startBackgroundMining, stopBackgroundMining } from "../wallet/mining/index.js";
15
- import type { anchorDomain, buyDomain, claimCogLock, clearDomainDelegate, clearDomainEndpoint, clearDomainMiner, clearField, createField, giveReputation, lockCogToDomain, registerDomain, reclaimCogLock, revokeReputation, sendCog, setField, setDomainCanonical, setDomainDelegate, setDomainEndpoint, setDomainMiner, sellDomain, transferDomain } from "../wallet/tx/index.js";
15
+ import type { anchorDomain, clearPendingAnchor, buyDomain, claimCogLock, clearDomainDelegate, clearDomainEndpoint, clearDomainMiner, clearField, createField, giveReputation, lockCogToDomain, registerDomain, reclaimCogLock, revokeReputation, sendCog, setField, setDomainCanonical, setDomainDelegate, setDomainEndpoint, setDomainMiner, sellDomain, transferDomain } from "../wallet/tx/index.js";
16
16
  export type ProgressOutput = "auto" | "tty" | "none";
17
17
  export type OutputMode = "text" | "json" | "preview-json";
18
- export type CommandName = "init" | "restore" | "reset" | "repair" | "sync" | "status" | "follow" | "bitcoin-start" | "bitcoin-stop" | "bitcoin-status" | "indexer-start" | "indexer-stop" | "indexer-status" | "unlock" | "anchor" | "domain-anchor" | "register" | "domain-register" | "transfer" | "domain-transfer" | "sell" | "domain-sell" | "unsell" | "domain-unsell" | "buy" | "domain-buy" | "domain-endpoint-set" | "domain-endpoint-clear" | "domain-delegate-set" | "domain-delegate-clear" | "domain-miner-set" | "domain-miner-clear" | "domain-canonical" | "field-list" | "field-show" | "field-create" | "field-set" | "field-clear" | "send" | "claim" | "reclaim" | "cog-send" | "cog-claim" | "cog-reclaim" | "cog-lock" | "rep-give" | "rep-revoke" | "cog-balance" | "cog-locks" | "hooks-mining-enable" | "hooks-mining-disable" | "hooks-mining-status" | "mine" | "mine-start" | "mine-stop" | "mine-setup" | "mine-status" | "mine-log" | "wallet-export" | "wallet-import" | "wallet-init" | "wallet-restore" | "wallet-show-mnemonic" | "wallet-lock" | "wallet-unlock" | "wallet-status" | "wallet-address" | "wallet-ids" | "address" | "ids" | "balance" | "locks" | "domain-list" | "domains" | "domain-show" | "show" | "fields" | "field";
18
+ export type CommandName = "init" | "restore" | "reset" | "repair" | "sync" | "status" | "follow" | "bitcoin-start" | "bitcoin-stop" | "bitcoin-status" | "indexer-start" | "indexer-stop" | "indexer-status" | "unlock" | "anchor" | "anchor-clear" | "domain-anchor" | "domain-anchor-clear" | "register" | "domain-register" | "transfer" | "domain-transfer" | "sell" | "domain-sell" | "unsell" | "domain-unsell" | "buy" | "domain-buy" | "domain-endpoint-set" | "domain-endpoint-clear" | "domain-delegate-set" | "domain-delegate-clear" | "domain-miner-set" | "domain-miner-clear" | "domain-canonical" | "field-list" | "field-show" | "field-create" | "field-set" | "field-clear" | "send" | "claim" | "reclaim" | "cog-send" | "cog-claim" | "cog-reclaim" | "cog-lock" | "rep-give" | "rep-revoke" | "cog-balance" | "cog-locks" | "hooks-mining-enable" | "hooks-mining-disable" | "hooks-mining-status" | "mine" | "mine-start" | "mine-stop" | "mine-setup" | "mine-status" | "mine-log" | "wallet-export" | "wallet-import" | "wallet-init" | "wallet-delete" | "wallet-restore" | "wallet-show-mnemonic" | "wallet-lock" | "wallet-unlock" | "wallet-status" | "wallet-address" | "wallet-ids" | "address" | "ids" | "balance" | "locks" | "domain-list" | "domains" | "domain-show" | "show" | "fields" | "field";
19
19
  export interface WritableLike {
20
20
  isTTY?: boolean;
21
21
  write(chunk: string): void;
@@ -36,6 +36,7 @@ export interface ParsedCliArgs {
36
36
  dbPath: string | null;
37
37
  dataDir: string | null;
38
38
  progressOutput: ProgressOutput;
39
+ seedName: string | null;
39
40
  unlockFor: string | null;
40
41
  assumeYes: boolean;
41
42
  forceRace: boolean;
@@ -95,6 +96,10 @@ export interface CliRunnerContext {
95
96
  dataDir?: string;
96
97
  walletRootId?: string;
97
98
  progressOutput?: ProgressOutput;
99
+ confirmGetblockArchiveRestart?: (options: {
100
+ currentArchiveEndHeight: number | null;
101
+ nextArchiveEndHeight: number;
102
+ }) => Promise<boolean>;
98
103
  }) => Promise<ManagedClientLike>;
99
104
  attachManagedBitcoindService?: typeof attachOrStartManagedBitcoindService;
100
105
  probeManagedBitcoindService?: typeof probeManagedBitcoindService;
@@ -115,11 +120,13 @@ export interface CliRunnerContext {
115
120
  previewResetWallet?: typeof previewResetWallet;
116
121
  exportWallet?: typeof exportWallet;
117
122
  importWallet?: typeof importWallet;
123
+ deleteImportedWalletSeed?: typeof deleteImportedWalletSeed;
118
124
  showWalletMnemonic?: typeof showWalletMnemonic;
119
125
  unlockWallet?: typeof unlockWallet;
120
126
  lockWallet?: typeof lockWallet;
121
127
  registerDomain?: typeof registerDomain;
122
128
  anchorDomain?: typeof anchorDomain;
129
+ clearPendingAnchor?: typeof clearPendingAnchor;
123
130
  transferDomain?: typeof transferDomain;
124
131
  sellDomain?: typeof sellDomain;
125
132
  buyDomain?: typeof buyDomain;
@@ -156,7 +163,7 @@ export interface CliRunnerContext {
156
163
  readPackageVersion?: () => Promise<string>;
157
164
  resolveDefaultBitcoindDataDir?: () => string;
158
165
  resolveDefaultClientDatabasePath?: () => string;
159
- resolveWalletRuntimePaths?: () => WalletRuntimePaths;
166
+ resolveWalletRuntimePaths?: (seedName?: string | null) => WalletRuntimePaths;
160
167
  }
161
168
  export interface StopSignalWatcher {
162
169
  cleanup(): void;
@@ -104,6 +104,12 @@ export function getRepairRecommendation(context) {
104
104
  return null;
105
105
  }
106
106
  export function getMutationRecommendation(context) {
107
+ const clearableAnchorFamily = (context.localState.state?.proactiveFamilies ?? []).find((family) => family.type === "anchor"
108
+ && family.status === "draft"
109
+ && family.currentStep === "reserved");
110
+ if (clearableAnchorFamily?.domainName != null) {
111
+ return `Run \`cogcoin anchor clear ${clearableAnchorFamily.domainName}\` to cancel the local pending anchor reservation, or rerun \`cogcoin anchor ${clearableAnchorFamily.domainName}\` to continue anchoring.`;
112
+ }
107
113
  const unresolvedFamily = (context.localState.state?.proactiveFamilies ?? []).find((family) => (family.type === "anchor" || family.type === "field")
108
114
  && (family.status === "broadcast-unknown" || family.status === "repair-required"));
109
115
  if (unresolvedFamily !== undefined) {
@@ -12,7 +12,7 @@ export interface WalletPrompter {
12
12
  writeLine(message: string): void;
13
13
  prompt(message: string): Promise<string>;
14
14
  promptHidden?(message: string): Promise<string>;
15
- clearSensitiveDisplay?(scope: "mnemonic-reveal"): void | Promise<void>;
15
+ clearSensitiveDisplay?(scope: "mnemonic-reveal" | "restore-mnemonic-entry"): void | Promise<void>;
16
16
  }
17
17
  export interface WalletInitializationResult {
18
18
  walletRootId: string;
@@ -42,12 +42,18 @@ export interface WalletImportResult {
42
42
  state: WalletStateV1;
43
43
  }
44
44
  export interface WalletRestoreResult {
45
+ seedName?: string | null;
45
46
  walletRootId: string;
46
47
  fundingAddress: string;
47
48
  unlockUntilUnixMs: number;
48
49
  state: WalletStateV1;
49
50
  warnings?: string[];
50
51
  }
52
+ export interface WalletDeleteResult {
53
+ seedName: string;
54
+ walletRootId: string;
55
+ deleted: boolean;
56
+ }
51
57
  export interface WalletRepairResult {
52
58
  walletRootId: string;
53
59
  recoveredFromBackup: boolean;
@@ -214,6 +220,17 @@ export declare function restoreWalletFromMnemonic(options: {
214
220
  attachService?: typeof attachOrStartManagedBitcoindService;
215
221
  rpcFactory?: (config: Parameters<typeof createRpcClient>[0]) => WalletLifecycleRpcClient;
216
222
  }): Promise<WalletRestoreResult>;
223
+ export declare function deleteImportedWalletSeed(options: {
224
+ dataDir: string;
225
+ provider?: WalletSecretProvider;
226
+ prompter: WalletPrompter;
227
+ assumeYes?: boolean;
228
+ nowUnixMs?: number;
229
+ paths?: WalletRuntimePaths;
230
+ attachService?: typeof attachOrStartManagedBitcoindService;
231
+ probeBitcoindService?: typeof probeManagedBitcoindService;
232
+ rpcFactory?: (config: Parameters<typeof createRpcClient>[0]) => WalletLifecycleRpcClient;
233
+ }): Promise<WalletDeleteResult>;
217
234
  export declare function repairWallet(options: {
218
235
  dataDir: string;
219
236
  databasePath: string;
@@ -11,7 +11,7 @@ import { readPortableWalletArchive, writePortableWalletArchive } from "./archive
11
11
  import { normalizeWalletDescriptorState, persistNormalizedWalletDescriptorStateIfNeeded, persistWalletStateUpdate, resolveNormalizedWalletDescriptorState, stripDescriptorChecksum, } from "./descriptor-normalization.js";
12
12
  import { acquireFileLock, clearOrphanedFileLock } from "./fs/lock.js";
13
13
  import { createInternalCoreWalletPassphrase, createMnemonicConfirmationChallenge, deriveWalletMaterialFromMnemonic, generateWalletMaterial, isEnglishMnemonicWord, validateEnglishMnemonic, } from "./material.js";
14
- import { resolveWalletRuntimePathsForTesting } from "./runtime.js";
14
+ import { deriveWalletRuntimePathsForSeed, resolveWalletRuntimePathsForTesting, } from "./runtime.js";
15
15
  import { requestMiningGenerationPreemption } from "./mining/coordination.js";
16
16
  import { loadClientConfig } from "./mining/config.js";
17
17
  import { inspectMiningHookState } from "./mining/hooks.js";
@@ -21,6 +21,7 @@ import { renderWalletMnemonicRevealArt } from "./mnemonic-art.js";
21
21
  import { clearWalletExplicitLock, loadWalletExplicitLock, saveWalletExplicitLock, } from "./state/explicit-lock.js";
22
22
  import { clearWalletPendingInitializationState, loadWalletPendingInitializationStateOrNull, saveWalletPendingInitializationState, } from "./state/pending-init.js";
23
23
  import { clearUnlockSession, loadUnlockSession, saveUnlockSession } from "./state/session.js";
24
+ import { addImportedWalletSeedRecord, assertValidImportedWalletSeedName, ensureMainWalletSeedIndexRecord, findWalletSeedRecord, loadWalletSeedIndex, removeWalletSeedRecord, } from "./state/seed-index.js";
24
25
  import { createDefaultWalletSecretProvider, createWalletPendingInitSecretReference, createWalletRootId, createWalletSecretReference, } from "./state/provider.js";
25
26
  import { extractWalletRootIdHintFromWalletStateEnvelope, loadRawWalletStateEnvelope, loadWalletState, saveWalletState, } from "./state/storage.js";
26
27
  export const DEFAULT_UNLOCK_DURATION_MS = 15 * 60 * 1000;
@@ -57,7 +58,7 @@ function resolvePendingInitializationStoragePaths(paths) {
57
58
  async function clearPendingInitialization(paths, provider) {
58
59
  await clearWalletPendingInitializationState(resolvePendingInitializationStoragePaths(paths), {
59
60
  provider,
60
- secretReference: createWalletPendingInitSecretReference(paths.stateRoot),
61
+ secretReference: createWalletPendingInitSecretReference(paths.walletStateRoot),
61
62
  });
62
63
  }
63
64
  async function loadOrCreatePendingInitializationMaterial(options) {
@@ -73,7 +74,7 @@ async function loadOrCreatePendingInitializationMaterial(options) {
73
74
  await clearPendingInitialization(options.paths, options.provider);
74
75
  }
75
76
  const material = generateWalletMaterial();
76
- const secretReference = createWalletPendingInitSecretReference(options.paths.stateRoot);
77
+ const secretReference = createWalletPendingInitSecretReference(options.paths.walletStateRoot);
77
78
  const pendingState = {
78
79
  schemaVersion: 1,
79
80
  createdAtUnixMs: options.nowUnixMs,
@@ -372,7 +373,7 @@ async function promptForArchivePassphrase(prompter, promptPrefix) {
372
373
  async function promptForRestoreMnemonic(prompter) {
373
374
  const words = [];
374
375
  for (let index = 0; index < 24; index += 1) {
375
- const word = (await promptHiddenValue(prompter, `Word ${index + 1} of 24: `)).toLowerCase();
376
+ const word = (await promptRequiredValue(prompter, `Word ${index + 1} of 24: `)).toLowerCase();
376
377
  if (!isEnglishMnemonicWord(word)) {
377
378
  throw new Error("wallet_restore_mnemonic_invalid");
378
379
  }
@@ -405,6 +406,12 @@ async function confirmOverwriteIfNeeded(prompter, path) {
405
406
  throw new Error("wallet_export_overwrite_declined");
406
407
  }
407
408
  }
409
+ async function confirmYesNo(prompter, message) {
410
+ const answer = (await prompter.prompt(message)).trim().toLowerCase();
411
+ if (answer !== "yes") {
412
+ throw new Error("wallet_delete_confirmation_required");
413
+ }
414
+ }
408
415
  async function readManagedSnapshotTip(options) {
409
416
  const daemon = await attachOrStartIndexerDaemon({
410
417
  dataDir: options.dataDir,
@@ -693,6 +700,16 @@ function createSilentNonInteractivePrompter() {
693
700
  },
694
701
  };
695
702
  }
703
+ function resolveMainWalletPaths(paths) {
704
+ return deriveWalletRuntimePathsForSeed(paths, "main");
705
+ }
706
+ async function loadSharedWalletSeedIndex(paths, nowUnixMs) {
707
+ const mainPaths = resolveMainWalletPaths(paths);
708
+ return await loadWalletSeedIndex({
709
+ paths: mainPaths,
710
+ nowUnixMs,
711
+ });
712
+ }
696
713
  function applyRepairStoppedMiningState(state) {
697
714
  const miningState = normalizeMiningStateRecord(state.miningState);
698
715
  return {
@@ -1125,6 +1142,9 @@ export async function initializeWallet(options) {
1125
1142
  const nowUnixMs = options.nowUnixMs ?? Date.now();
1126
1143
  const unlockDurationMs = options.unlockDurationMs ?? DEFAULT_UNLOCK_DURATION_MS;
1127
1144
  const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
1145
+ if (paths.selectedSeedName !== "main") {
1146
+ throw new Error("wallet_init_seed_not_supported");
1147
+ }
1128
1148
  const controlLock = await acquireFileLock(paths.walletControlLockPath, {
1129
1149
  purpose: "wallet-init",
1130
1150
  walletRootId: null,
@@ -1185,6 +1205,11 @@ export async function initializeWallet(options) {
1185
1205
  secretReference,
1186
1206
  });
1187
1207
  await clearPendingInitialization(paths, provider);
1208
+ await ensureMainWalletSeedIndexRecord({
1209
+ paths: resolveMainWalletPaths(paths),
1210
+ walletRootId,
1211
+ nowUnixMs,
1212
+ });
1188
1213
  return {
1189
1214
  walletRootId,
1190
1215
  fundingAddress: verifiedState.funding.address,
@@ -1404,6 +1429,9 @@ export async function importWallet(options) {
1404
1429
  const nowUnixMs = options.nowUnixMs ?? Date.now();
1405
1430
  const unlockDurationMs = options.unlockDurationMs ?? DEFAULT_UNLOCK_DURATION_MS;
1406
1431
  const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
1432
+ if (paths.selectedSeedName !== "main") {
1433
+ throw new Error("wallet_import_seed_not_supported");
1434
+ }
1407
1435
  const controlLock = await acquireFileLock(paths.walletControlLockPath, {
1408
1436
  purpose: "wallet-import",
1409
1437
  walletRootId: null,
@@ -1467,6 +1495,11 @@ export async function importWallet(options) {
1467
1495
  databasePath: options.databasePath,
1468
1496
  walletRootId: importedState.walletRootId,
1469
1497
  }).then((daemon) => daemon.close());
1498
+ await ensureMainWalletSeedIndexRecord({
1499
+ paths: resolveMainWalletPaths(paths),
1500
+ walletRootId: importedState.walletRootId,
1501
+ nowUnixMs,
1502
+ });
1470
1503
  return {
1471
1504
  archivePath: options.archivePath,
1472
1505
  walletRootId: importedState.walletRootId,
@@ -1487,98 +1520,154 @@ export async function restoreWalletFromMnemonic(options) {
1487
1520
  const nowUnixMs = options.nowUnixMs ?? Date.now();
1488
1521
  const unlockDurationMs = options.unlockDurationMs ?? DEFAULT_UNLOCK_DURATION_MS;
1489
1522
  const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
1523
+ const seedName = assertValidImportedWalletSeedName(paths.selectedSeedName);
1490
1524
  const controlLock = await acquireFileLock(paths.walletControlLockPath, {
1491
1525
  purpose: "wallet-restore",
1492
1526
  walletRootId: null,
1493
1527
  });
1494
1528
  try {
1495
- const rawEnvelope = await loadRawWalletStateEnvelope({
1529
+ const mainPaths = resolveMainWalletPaths(paths);
1530
+ const seedIndex = await loadSharedWalletSeedIndex(paths, nowUnixMs);
1531
+ if (findWalletSeedRecord(seedIndex, "main") === null) {
1532
+ throw new Error("wallet_restore_requires_main_wallet");
1533
+ }
1534
+ if (findWalletSeedRecord(seedIndex, seedName) !== null) {
1535
+ throw new Error("wallet_seed_name_exists");
1536
+ }
1537
+ await ensureWalletNotInitialized(paths, provider);
1538
+ let promptPhaseStarted = false;
1539
+ let mnemonicPhrase;
1540
+ try {
1541
+ promptPhaseStarted = true;
1542
+ mnemonicPhrase = await promptForRestoreMnemonic(options.prompter);
1543
+ }
1544
+ finally {
1545
+ if (promptPhaseStarted) {
1546
+ await options.prompter.clearSensitiveDisplay?.("restore-mnemonic-entry");
1547
+ }
1548
+ }
1549
+ await clearPendingInitialization(paths, provider);
1550
+ const material = deriveWalletMaterialFromMnemonic(mnemonicPhrase);
1551
+ const walletRootId = createWalletRootId();
1552
+ const internalCoreWalletPassphrase = createInternalCoreWalletPassphrase();
1553
+ const secretReference = createWalletSecretReference(walletRootId);
1554
+ const secret = randomBytes(32);
1555
+ await provider.storeSecret(secretReference.keyId, secret);
1556
+ const initialState = createInitialWalletState({
1557
+ walletRootId,
1558
+ nowUnixMs,
1559
+ material,
1560
+ internalCoreWalletPassphrase,
1561
+ });
1562
+ await clearUnlockSession(paths.walletUnlockSessionPath);
1563
+ await clearWalletExplicitLock(paths.walletExplicitLockPath);
1564
+ await saveWalletState({
1496
1565
  primaryPath: paths.walletStatePath,
1497
1566
  backupPath: paths.walletStateBackupPath,
1567
+ }, initialState, {
1568
+ provider,
1569
+ secretReference,
1570
+ });
1571
+ const restoredState = await recreateManagedCoreWalletReplica(initialState, provider, paths, options.dataDir, nowUnixMs, {
1572
+ attachService: options.attachService,
1573
+ rpcFactory: options.rpcFactory,
1574
+ });
1575
+ const unlockUntilUnixMs = nowUnixMs + unlockDurationMs;
1576
+ await clearWalletExplicitLock(paths.walletExplicitLockPath);
1577
+ await saveUnlockSession(paths.walletUnlockSessionPath, createUnlockSession(restoredState, unlockUntilUnixMs, secretReference.keyId, nowUnixMs), {
1578
+ provider,
1579
+ secretReference,
1498
1580
  });
1499
- const replacementStateExists = rawEnvelope !== null
1500
- || await pathExists(paths.walletStatePath)
1501
- || await pathExists(paths.walletStateBackupPath);
1502
- const replacementCoreWalletExists = await detectExistingManagedWalletReplica(options.dataDir);
1503
- const mnemonicPhrase = await promptForRestoreMnemonic(options.prompter);
1504
1581
  await clearPendingInitialization(paths, provider);
1505
- if (replacementStateExists || replacementCoreWalletExists) {
1506
- await confirmRestoreReplacement(options.prompter);
1582
+ await addImportedWalletSeedRecord({
1583
+ paths: mainPaths,
1584
+ seedName,
1585
+ walletRootId,
1586
+ nowUnixMs,
1587
+ });
1588
+ return {
1589
+ seedName,
1590
+ walletRootId,
1591
+ fundingAddress: restoredState.funding.address,
1592
+ unlockUntilUnixMs,
1593
+ state: restoredState,
1594
+ warnings: [],
1595
+ };
1596
+ }
1597
+ finally {
1598
+ await controlLock.release();
1599
+ }
1600
+ }
1601
+ export async function deleteImportedWalletSeed(options) {
1602
+ const provider = options.provider ?? createDefaultWalletSecretProvider();
1603
+ const nowUnixMs = options.nowUnixMs ?? Date.now();
1604
+ const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
1605
+ if ((paths.selectedSeedName ?? "main") === "main") {
1606
+ throw new Error("wallet_delete_main_not_supported");
1607
+ }
1608
+ const seedName = assertValidImportedWalletSeedName(paths.selectedSeedName);
1609
+ const controlLock = await acquireFileLock(paths.walletControlLockPath, {
1610
+ purpose: "wallet-delete",
1611
+ walletRootId: null,
1612
+ });
1613
+ try {
1614
+ const mainPaths = resolveMainWalletPaths(paths);
1615
+ const seedIndex = await loadSharedWalletSeedIndex(paths, nowUnixMs);
1616
+ const seedRecord = findWalletSeedRecord(seedIndex, seedName);
1617
+ if (seedRecord === null) {
1618
+ throw new Error("wallet_seed_not_found");
1507
1619
  }
1508
- let previousWalletRootId = extractWalletRootIdHintFromWalletStateEnvelope(rawEnvelope?.envelope ?? null);
1509
- try {
1510
- const loaded = await loadWalletState({
1511
- primaryPath: paths.walletStatePath,
1512
- backupPath: paths.walletStateBackupPath,
1513
- }, {
1514
- provider,
1515
- });
1516
- previousWalletRootId = loaded.state.walletRootId;
1620
+ if (seedRecord.kind !== "imported") {
1621
+ throw new Error("wallet_delete_main_not_supported");
1517
1622
  }
1518
- catch {
1519
- previousWalletRootId = previousWalletRootId ?? null;
1623
+ if (!options.assumeYes) {
1624
+ await confirmYesNo(options.prompter, `Delete imported seed "${seedName}" and release its local wallet artifacts? Type yes to continue: `);
1520
1625
  }
1521
- const miningLock = await acquireFileLock(paths.miningControlLockPath, {
1522
- purpose: "wallet-restore",
1523
- walletRootId: previousWalletRootId,
1524
- });
1525
- try {
1526
- const warnings = [];
1527
- const material = deriveWalletMaterialFromMnemonic(mnemonicPhrase);
1528
- const walletRootId = createWalletRootId();
1529
- const internalCoreWalletPassphrase = createInternalCoreWalletPassphrase();
1530
- const secretReference = createWalletSecretReference(walletRootId);
1531
- const secret = randomBytes(32);
1532
- await provider.storeSecret(secretReference.keyId, secret);
1533
- const initialState = createInitialWalletState({
1534
- walletRootId,
1535
- nowUnixMs,
1536
- material,
1537
- internalCoreWalletPassphrase,
1538
- });
1539
- await clearUnlockSession(paths.walletUnlockSessionPath);
1540
- await clearWalletExplicitLock(paths.walletExplicitLockPath);
1541
- await saveWalletState({
1542
- primaryPath: paths.walletStatePath,
1543
- backupPath: paths.walletStateBackupPath,
1544
- }, initialState, {
1545
- provider,
1546
- secretReference,
1547
- });
1548
- const restoredState = await recreateManagedCoreWalletReplica(initialState, provider, paths, options.dataDir, nowUnixMs, {
1549
- attachService: options.attachService,
1550
- rpcFactory: options.rpcFactory,
1551
- });
1552
- const unlockUntilUnixMs = nowUnixMs + unlockDurationMs;
1553
- await clearWalletExplicitLock(paths.walletExplicitLockPath);
1554
- await saveUnlockSession(paths.walletUnlockSessionPath, createUnlockSession(restoredState, unlockUntilUnixMs, secretReference.keyId, nowUnixMs), {
1555
- provider,
1556
- secretReference,
1626
+ const probeManagedBitcoind = options.probeBitcoindService ?? probeManagedBitcoindService;
1627
+ const managedBitcoindProbe = await probeManagedBitcoind({
1628
+ dataDir: options.dataDir,
1629
+ chain: "main",
1630
+ startHeight: 0,
1631
+ }).catch(() => ({
1632
+ compatibility: "unreachable",
1633
+ status: null,
1634
+ error: null,
1635
+ }));
1636
+ if (managedBitcoindProbe.compatibility !== "compatible" && managedBitcoindProbe.compatibility !== "unreachable") {
1637
+ throw new Error(managedBitcoindProbe.error ?? "managed_bitcoind_protocol_error");
1638
+ }
1639
+ if (managedBitcoindProbe.compatibility === "compatible") {
1640
+ const node = await (options.attachService ?? attachOrStartManagedBitcoindService)({
1641
+ dataDir: options.dataDir,
1642
+ chain: "main",
1643
+ startHeight: 0,
1557
1644
  });
1558
- await clearPendingInitialization(paths, provider);
1559
- if (previousWalletRootId !== null && previousWalletRootId !== walletRootId) {
1560
- try {
1561
- await clearPreviousManagedWalletRuntime({
1562
- dataDir: options.dataDir,
1563
- walletRootId: previousWalletRootId,
1564
- });
1565
- }
1566
- catch (error) {
1567
- warnings.push(formatRestoreCleanupWarning(error));
1568
- }
1569
- await provider.deleteSecret(createWalletSecretReference(previousWalletRootId).keyId).catch(() => undefined);
1645
+ const rpc = (options.rpcFactory ?? createRpcClient)(node.rpc);
1646
+ const walletName = sanitizeWalletName(seedRecord.walletRootId);
1647
+ if (rpc.unloadWallet != null) {
1648
+ await rpc.unloadWallet(walletName, false).catch(() => undefined);
1570
1649
  }
1571
- return {
1572
- walletRootId,
1573
- fundingAddress: restoredState.funding.address,
1574
- unlockUntilUnixMs,
1575
- state: restoredState,
1576
- warnings,
1577
- };
1578
- }
1579
- finally {
1580
- await miningLock.release();
1581
1650
  }
1651
+ await clearUnlockSession(paths.walletUnlockSessionPath).catch(() => undefined);
1652
+ await clearWalletExplicitLock(paths.walletExplicitLockPath).catch(() => undefined);
1653
+ await clearPendingInitialization(paths, provider).catch(() => undefined);
1654
+ await provider.deleteSecret(createWalletSecretReference(seedRecord.walletRootId).keyId).catch(() => undefined);
1655
+ await rm(paths.walletStateRoot, { recursive: true, force: true }).catch(() => undefined);
1656
+ await rm(paths.walletRuntimeRoot, { recursive: true, force: true }).catch(() => undefined);
1657
+ await rm(join(options.dataDir, "wallets", sanitizeWalletName(seedRecord.walletRootId)), {
1658
+ recursive: true,
1659
+ force: true,
1660
+ }).catch(() => undefined);
1661
+ await removeWalletSeedRecord({
1662
+ paths: mainPaths,
1663
+ seedName,
1664
+ nowUnixMs,
1665
+ });
1666
+ return {
1667
+ seedName,
1668
+ walletRootId: seedRecord.walletRootId,
1669
+ deleted: true,
1670
+ };
1582
1671
  }
1583
1672
  finally {
1584
1673
  await controlLock.release();
@@ -1,9 +1,10 @@
1
- import type { ProgressOutputMode } from "../../bitcoind/types.js";
1
+ import type { BootstrapProgress, ProgressOutputMode } from "../../bitcoind/types.js";
2
+ import { createFollowSceneState } from "../../bitcoind/progress/follow-scene.js";
3
+ import { type RenderClock, type TtyRenderStream } from "../../bitcoind/progress/render-policy.js";
2
4
  import type { MiningRuntimeStatusV1 } from "./types.js";
3
- interface RenderStream {
4
- isTTY?: boolean;
5
- columns?: number;
6
- write(chunk: string): boolean | void;
5
+ interface VisualizerRendererLike {
6
+ renderFollowScene(progress: BootstrapProgress, cogcoinSyncHeight: number | null, cogcoinSyncTargetHeight: number | null, followScene: ReturnType<typeof createFollowSceneState>, statusFieldText?: string): void;
7
+ close(): void;
7
8
  }
8
9
  export declare function describeMiningVisualizerStatus(snapshot: MiningRuntimeStatusV1): string;
9
10
  export declare function describeMiningVisualizerProgress(snapshot: MiningRuntimeStatusV1): string;
@@ -11,7 +12,11 @@ export declare class MiningFollowVisualizer {
11
12
  #private;
12
13
  constructor(options?: {
13
14
  progressOutput?: ProgressOutputMode;
14
- stream?: RenderStream;
15
+ stream?: TtyRenderStream;
16
+ platform?: NodeJS.Platform;
17
+ env?: NodeJS.ProcessEnv;
18
+ clock?: RenderClock;
19
+ rendererFactory?: (stream: TtyRenderStream) => VisualizerRendererLike;
15
20
  });
16
21
  update(snapshot: MiningRuntimeStatusV1): void;
17
22
  close(): void;
@@ -1,5 +1,6 @@
1
1
  import { createBootstrapProgress } from "../../bitcoind/progress/formatting.js";
2
2
  import { createFollowSceneState, syncFollowSceneState, } from "../../bitcoind/progress/follow-scene.js";
3
+ import { DEFAULT_RENDER_CLOCK, resolveTtyRenderPolicy, TtyRenderThrottle, } from "../../bitcoind/progress/render-policy.js";
3
4
  import { TtyProgressRenderer } from "../../bitcoind/progress/tty-renderer.js";
4
5
  const VISUALIZER_PROGRESS_SNAPSHOT = {
5
6
  url: "",
@@ -8,15 +9,6 @@ const VISUALIZER_PROGRESS_SNAPSHOT = {
8
9
  sha256: "",
9
10
  sizeBytes: 1,
10
11
  };
11
- function shouldRenderToTty(progressOutput, stream) {
12
- if (progressOutput === "none") {
13
- return false;
14
- }
15
- if (progressOutput === "tty") {
16
- return true;
17
- }
18
- return stream.isTTY === true;
19
- }
20
12
  export function describeMiningVisualizerStatus(snapshot) {
21
13
  switch (snapshot.currentPhase) {
22
14
  case "resuming":
@@ -99,24 +91,52 @@ export function describeMiningVisualizerProgress(snapshot) {
99
91
  }
100
92
  export class MiningFollowVisualizer {
101
93
  #renderer;
94
+ #clock;
95
+ #renderThrottle;
102
96
  #progress = createBootstrapProgress("follow_tip", VISUALIZER_PROGRESS_SNAPSHOT);
103
97
  #scene = createFollowSceneState();
98
+ #latestSnapshot = null;
104
99
  constructor(options = {}) {
105
100
  const stream = options.stream ?? process.stderr;
106
101
  const progressOutput = options.progressOutput ?? "auto";
107
- this.#renderer = shouldRenderToTty(progressOutput, stream)
108
- ? new TtyProgressRenderer(stream)
102
+ const renderPolicy = resolveTtyRenderPolicy(progressOutput, stream, {
103
+ platform: options.platform,
104
+ env: options.env,
105
+ });
106
+ this.#clock = options.clock ?? DEFAULT_RENDER_CLOCK;
107
+ this.#renderer = renderPolicy.enabled
108
+ ? options.rendererFactory?.(stream) ?? new TtyProgressRenderer(stream)
109
109
  : null;
110
+ this.#renderThrottle = new TtyRenderThrottle({
111
+ clock: this.#clock,
112
+ intervalMs: renderPolicy.repaintIntervalMs,
113
+ onRender: () => {
114
+ this.#renderLatestSnapshot();
115
+ },
116
+ throttled: renderPolicy.linuxHeadlessThrottle,
117
+ });
110
118
  }
111
119
  update(snapshot) {
112
120
  if (this.#renderer === null) {
113
121
  return;
114
122
  }
123
+ this.#latestSnapshot = snapshot;
124
+ this.#renderThrottle.request();
125
+ }
126
+ close() {
127
+ this.#renderThrottle.flush();
128
+ this.#renderer?.close();
129
+ }
130
+ #renderLatestSnapshot() {
131
+ if (this.#renderer === null || this.#latestSnapshot === null) {
132
+ return;
133
+ }
134
+ const snapshot = this.#latestSnapshot;
115
135
  const indexedHeight = snapshot.indexerTipHeight ?? snapshot.coreBestHeight ?? null;
116
136
  const nodeHeight = snapshot.coreBestHeight ?? indexedHeight;
117
137
  this.#progress.phase = "follow_tip";
118
138
  this.#progress.message = describeMiningVisualizerProgress(snapshot);
119
- this.#progress.updatedAt = Date.now();
139
+ this.#progress.updatedAt = this.#clock.now();
120
140
  this.#progress.blocks = nodeHeight;
121
141
  this.#progress.targetHeight = nodeHeight;
122
142
  this.#progress.etaSeconds = null;
@@ -128,7 +148,4 @@ export class MiningFollowVisualizer {
128
148
  });
129
149
  this.#renderer.renderFollowScene(this.#progress, indexedHeight, nodeHeight, this.#scene, describeMiningVisualizerStatus(snapshot));
130
150
  }
131
- close() {
132
- this.#renderer?.close();
133
- }
134
151
  }