@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
@@ -13,6 +13,7 @@ import { loadMiningRuntimeStatus } from "./mining/runtime-artifacts.js";
13
13
  import { resolveWalletRuntimePathsForTesting } from "./runtime.js";
14
14
  import { loadWalletExplicitLock } from "./state/explicit-lock.js";
15
15
  import { createDefaultWalletSecretProvider, createWalletRootId, createWalletSecretReference, } from "./state/provider.js";
16
+ import { loadWalletSeedIndex } from "./state/seed-index.js";
16
17
  import { extractWalletRootIdHintFromWalletStateEnvelope, loadRawWalletStateEnvelope, loadWalletState, saveWalletState, } from "./state/storage.js";
17
18
  import { confirmTypedAcknowledgement } from "./tx/confirm.js";
18
19
  function sanitizeWalletName(walletRootId) {
@@ -519,6 +520,12 @@ async function preflightReset(options) {
519
520
  }
520
521
  const tracked = await collectTrackedManagedProcesses(options.paths);
521
522
  const secretProviderKeyId = rawEnvelope?.envelope.secretProvider?.keyId ?? null;
523
+ const seedIndex = await loadWalletSeedIndex({
524
+ paths: options.paths,
525
+ }).catch(() => null);
526
+ const importedSeedSecretProviderKeyIds = [...new Set((seedIndex?.seeds ?? [])
527
+ .filter((seed) => seed.kind === "imported")
528
+ .map((seed) => createWalletSecretReference(seed.walletRootId).keyId))];
522
529
  return {
523
530
  dataRoot: options.paths.dataRoot,
524
531
  removedRoots,
@@ -531,6 +538,7 @@ async function preflightReset(options) {
531
538
  : "passphrase-wrapped",
532
539
  envelopeSource: rawEnvelope?.source ?? null,
533
540
  secretProviderKeyId,
541
+ importedSeedSecretProviderKeyIds,
534
542
  explicitLock,
535
543
  rawEnvelope,
536
544
  },
@@ -719,7 +727,8 @@ export async function previewResetWallet(options) {
719
727
  : null,
720
728
  },
721
729
  trackedProcessKinds: preflight.trackedProcessKinds,
722
- willDeleteOsSecrets: preflight.wallet.secretProviderKeyId !== null,
730
+ willDeleteOsSecrets: preflight.wallet.secretProviderKeyId !== null
731
+ || preflight.wallet.importedSeedSecretProviderKeyIds.length > 0,
723
732
  removedPaths,
724
733
  };
725
734
  }
@@ -766,9 +775,7 @@ export async function resetWallet(options) {
766
775
  let rootsDeleted = false;
767
776
  let committed = false;
768
777
  let newProviderKeyId = null;
769
- let secretCleanupStatus = preflight.wallet.secretProviderKeyId === null
770
- ? "not-found"
771
- : "not-found";
778
+ let secretCleanupStatus = "not-found";
772
779
  const deletedSecretRefs = [];
773
780
  const failedSecretRefs = [];
774
781
  const preservedSecretRefs = [];
@@ -850,41 +857,46 @@ export async function resetWallet(options) {
850
857
  await restoreStagedArtifacts(stagedSnapshotArtifacts);
851
858
  }
852
859
  committed = true;
853
- if (walletAction === "deleted") {
854
- if (preflight.wallet.secretProviderKeyId !== null) {
855
- try {
856
- await provider.deleteSecret(preflight.wallet.secretProviderKeyId);
857
- deletedSecretRefs.push(preflight.wallet.secretProviderKeyId);
858
- secretCleanupStatus = "deleted";
859
- }
860
- catch {
861
- failedSecretRefs.push(preflight.wallet.secretProviderKeyId);
862
- secretCleanupStatus = "failed";
863
- throw new Error("reset_secret_cleanup_failed");
864
- }
865
- }
866
- }
867
- else if (walletAction === "retain-mnemonic" && preflight.wallet.secretProviderKeyId !== null) {
860
+ const deleteTrackedSecretReference = async (keyId) => {
868
861
  try {
869
- if (preflight.wallet.secretProviderKeyId !== newProviderKeyId) {
870
- await provider.deleteSecret(preflight.wallet.secretProviderKeyId);
871
- deletedSecretRefs.push(preflight.wallet.secretProviderKeyId);
872
- secretCleanupStatus = "deleted";
873
- }
862
+ await provider.deleteSecret(keyId);
863
+ deletedSecretRefs.push(keyId);
874
864
  }
875
865
  catch {
876
- failedSecretRefs.push(preflight.wallet.secretProviderKeyId);
866
+ failedSecretRefs.push(keyId);
877
867
  secretCleanupStatus = "failed";
878
868
  throw new Error("reset_secret_cleanup_failed");
879
869
  }
870
+ };
871
+ for (const importedSecretKeyId of preflight.wallet.importedSeedSecretProviderKeyIds) {
872
+ await deleteTrackedSecretReference(importedSecretKeyId);
873
+ }
874
+ if (walletAction === "deleted") {
875
+ if (preflight.wallet.secretProviderKeyId !== null) {
876
+ await deleteTrackedSecretReference(preflight.wallet.secretProviderKeyId);
877
+ }
878
+ }
879
+ else if (walletAction === "retain-mnemonic" && preflight.wallet.secretProviderKeyId !== null) {
880
+ if (preflight.wallet.secretProviderKeyId !== newProviderKeyId) {
881
+ await deleteTrackedSecretReference(preflight.wallet.secretProviderKeyId);
882
+ }
880
883
  }
881
884
  else if (preflight.wallet.secretProviderKeyId !== null) {
882
885
  preservedSecretRefs.push(preflight.wallet.secretProviderKeyId);
883
886
  }
884
- if (preflight.wallet.secretProviderKeyId === null && preflight.wallet.present && preflight.wallet.rawEnvelope === null) {
887
+ if (failedSecretRefs.length > 0) {
888
+ secretCleanupStatus = "failed";
889
+ }
890
+ else if (deletedSecretRefs.length > 0) {
891
+ secretCleanupStatus = "deleted";
892
+ }
893
+ else if (preflight.wallet.secretProviderKeyId === null
894
+ && preflight.wallet.importedSeedSecretProviderKeyIds.length === 0
895
+ && preflight.wallet.present
896
+ && preflight.wallet.rawEnvelope === null) {
885
897
  secretCleanupStatus = "unknown";
886
898
  }
887
- else if (deletedSecretRefs.length === 0 && failedSecretRefs.length === 0) {
899
+ else if (deletedSecretRefs.length === 0) {
888
900
  secretCleanupStatus = "not-found";
889
901
  }
890
902
  return {
@@ -1,13 +1,23 @@
1
1
  import type { CogcoinPathResolution } from "../app-paths.js";
2
+ export type WalletSeedKind = "main" | "imported";
3
+ export interface WalletRuntimePathResolution extends CogcoinPathResolution {
4
+ seedName?: string | null;
5
+ }
2
6
  export interface WalletRuntimePaths {
3
7
  dataRoot: string;
4
8
  clientDataDir: string;
5
9
  clientConfigPath: string;
6
10
  runtimeRoot: string;
11
+ walletRuntimeRoot: string;
7
12
  hooksRoot: string;
8
13
  stateRoot: string;
14
+ walletStateRoot: string;
15
+ seedRegistryPath: string;
16
+ selectedSeedName: string;
17
+ selectedSeedKind: WalletSeedKind;
9
18
  bitcoinDataDir: string;
10
19
  indexerRoot: string;
20
+ walletStateDirectory: string;
11
21
  walletStatePath: string;
12
22
  walletStateBackupPath: string;
13
23
  walletInitPendingPath: string;
@@ -27,4 +37,5 @@ export interface WalletRuntimePaths {
27
37
  miningEventsPath: string;
28
38
  miningControlLockPath: string;
29
39
  }
30
- export declare function resolveWalletRuntimePathsForTesting(resolution?: CogcoinPathResolution): WalletRuntimePaths;
40
+ export declare function deriveWalletRuntimePathsForSeed(basePaths: WalletRuntimePaths, seedName: string | null | undefined): WalletRuntimePaths;
41
+ export declare function resolveWalletRuntimePathsForTesting(resolution?: WalletRuntimePathResolution): WalletRuntimePaths;
@@ -1,21 +1,63 @@
1
+ import { join } from "node:path";
1
2
  import { resolveCogcoinPathsForTesting } from "../app-paths.js";
3
+ function resolveSeedLayout(sharedStateRoot, sharedRuntimeRoot, seedName) {
4
+ if (seedName === "main") {
5
+ return {
6
+ seedKind: "main",
7
+ walletStateRoot: sharedStateRoot,
8
+ walletRuntimeRoot: sharedRuntimeRoot,
9
+ };
10
+ }
11
+ return {
12
+ seedKind: "imported",
13
+ walletStateRoot: join(sharedStateRoot, "seeds", seedName),
14
+ walletRuntimeRoot: join(sharedRuntimeRoot, "seeds", seedName),
15
+ };
16
+ }
17
+ export function deriveWalletRuntimePathsForSeed(basePaths, seedName) {
18
+ const resolvedSeedName = seedName ?? "main";
19
+ const seedLayout = resolveSeedLayout(basePaths.stateRoot, basePaths.runtimeRoot, resolvedSeedName);
20
+ return {
21
+ ...basePaths,
22
+ walletRuntimeRoot: seedLayout.walletRuntimeRoot,
23
+ walletStateRoot: seedLayout.walletStateRoot,
24
+ selectedSeedName: resolvedSeedName,
25
+ selectedSeedKind: seedLayout.seedKind,
26
+ walletStateDirectory: seedLayout.walletStateRoot,
27
+ walletStatePath: join(seedLayout.walletStateRoot, "wallet-state.enc"),
28
+ walletStateBackupPath: join(seedLayout.walletStateRoot, "wallet-state.enc.bak"),
29
+ walletInitPendingPath: join(seedLayout.walletStateRoot, "wallet-init-pending.enc"),
30
+ walletInitPendingBackupPath: join(seedLayout.walletStateRoot, "wallet-init-pending.enc.bak"),
31
+ walletUnlockSessionPath: join(seedLayout.walletRuntimeRoot, "wallet-unlock-session.enc"),
32
+ walletExplicitLockPath: join(seedLayout.walletRuntimeRoot, "wallet-explicit-lock.json"),
33
+ miningRoot: join(seedLayout.walletRuntimeRoot, "mining"),
34
+ miningStatusPath: join(seedLayout.walletRuntimeRoot, "mining", "status.json"),
35
+ miningEventsPath: join(seedLayout.walletRuntimeRoot, "mining", "events.jsonl"),
36
+ };
37
+ }
2
38
  export function resolveWalletRuntimePathsForTesting(resolution = {}) {
3
39
  const paths = resolveCogcoinPathsForTesting(resolution);
4
- return {
40
+ return deriveWalletRuntimePathsForSeed({
5
41
  dataRoot: paths.dataRoot,
6
42
  clientDataDir: paths.clientDataDir,
7
43
  clientConfigPath: paths.clientConfigPath,
8
44
  runtimeRoot: paths.runtimeRoot,
45
+ walletRuntimeRoot: paths.runtimeRoot,
9
46
  hooksRoot: paths.hooksRoot,
10
47
  stateRoot: paths.stateRoot,
48
+ walletStateRoot: paths.stateRoot,
49
+ seedRegistryPath: join(paths.stateRoot, "seed-index.json"),
50
+ selectedSeedName: "main",
51
+ selectedSeedKind: "main",
11
52
  bitcoinDataDir: paths.bitcoinDataDir,
12
53
  indexerRoot: paths.indexerRoot,
13
- walletStatePath: paths.walletStatePath,
14
- walletStateBackupPath: paths.walletStateBackupPath,
15
- walletInitPendingPath: paths.walletInitPendingPath,
16
- walletInitPendingBackupPath: paths.walletInitPendingBackupPath,
17
- walletUnlockSessionPath: paths.walletUnlockSessionPath,
18
- walletExplicitLockPath: paths.walletExplicitLockPath,
54
+ walletStateDirectory: paths.stateRoot,
55
+ walletStatePath: join(paths.stateRoot, "wallet-state.enc"),
56
+ walletStateBackupPath: join(paths.stateRoot, "wallet-state.enc.bak"),
57
+ walletInitPendingPath: join(paths.stateRoot, "wallet-init-pending.enc"),
58
+ walletInitPendingBackupPath: join(paths.stateRoot, "wallet-init-pending.enc.bak"),
59
+ walletUnlockSessionPath: join(paths.runtimeRoot, "wallet-unlock-session.enc"),
60
+ walletExplicitLockPath: join(paths.runtimeRoot, "wallet-explicit-lock.json"),
19
61
  walletControlLockPath: paths.walletControlLockPath,
20
62
  bitcoindLockPath: paths.bitcoindLockPath,
21
63
  bitcoindStatusPath: paths.bitcoindStatusPath,
@@ -24,9 +66,9 @@ export function resolveWalletRuntimePathsForTesting(resolution = {}) {
24
66
  hooksMiningDir: paths.hooksMiningDir,
25
67
  hooksMiningEntrypointPath: paths.hooksMiningEntrypointPath,
26
68
  hooksMiningPackageJsonPath: paths.hooksMiningPackageJsonPath,
27
- miningRoot: paths.miningRoot,
28
- miningStatusPath: paths.miningStatusPath,
29
- miningEventsPath: paths.miningEventsPath,
69
+ miningRoot: join(paths.runtimeRoot, "mining"),
70
+ miningStatusPath: join(paths.runtimeRoot, "mining", "status.json"),
71
+ miningEventsPath: join(paths.runtimeRoot, "mining", "events.jsonl"),
30
72
  miningControlLockPath: paths.miningControlLockPath,
31
- };
73
+ }, resolution.seedName);
32
74
  }
@@ -11,6 +11,7 @@ export type LinuxSecretToolRunner = (args: readonly string[], options?: LinuxSec
11
11
  export interface DefaultWalletSecretProviderFactoryOptions {
12
12
  platform?: NodeJS.Platform;
13
13
  linuxSecretToolRunner?: LinuxSecretToolRunner;
14
+ stateRoot?: string;
14
15
  }
15
16
  export interface WalletSecretReference {
16
17
  kind: string;
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { execFile, spawn } from "node:child_process";
3
- import { mkdir, readFile, rm } from "node:fs/promises";
3
+ import { access, constants, mkdir, readFile, rm } from "node:fs/promises";
4
4
  import { dirname, join } from "node:path";
5
5
  import { promisify } from "node:util";
6
6
  import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
@@ -42,6 +42,15 @@ function createLinuxSecretToolAttributes(keyId) {
42
42
  function createLinuxSecretToolError(message, cause) {
43
43
  return cause === undefined ? new Error(message) : new Error(message, { cause });
44
44
  }
45
+ function isWalletSecretProviderMessage(error, message) {
46
+ return error instanceof Error && error.message === message;
47
+ }
48
+ function sanitizeSecretKeyId(keyId) {
49
+ return keyId.replace(/[^a-zA-Z0-9._-]+/g, "-");
50
+ }
51
+ function resolveSecretDirectoryPath(options) {
52
+ return join(options.stateRoot ?? resolveWalletRuntimePathsForTesting().stateRoot, "secrets");
53
+ }
45
54
  function isLinuxSecretServiceUnavailableMessage(stderr) {
46
55
  const normalized = stderr.trim().toLowerCase();
47
56
  if (normalized.length === 0) {
@@ -233,6 +242,110 @@ class LinuxSecretToolWalletSecretProvider {
233
242
  }
234
243
  }
235
244
  }
245
+ class LinuxLocalFileWalletSecretProvider {
246
+ kind = "linux-local-file";
247
+ #directoryPath;
248
+ constructor(directoryPath) {
249
+ this.#directoryPath = directoryPath;
250
+ }
251
+ #resolveSecretPath(keyId) {
252
+ return join(this.#directoryPath, `${sanitizeSecretKeyId(keyId)}.secret`);
253
+ }
254
+ async hasSecret(keyId) {
255
+ try {
256
+ await access(this.#resolveSecretPath(keyId), constants.F_OK);
257
+ return true;
258
+ }
259
+ catch {
260
+ return false;
261
+ }
262
+ }
263
+ async loadSecret(keyId) {
264
+ try {
265
+ const encoded = await readFile(this.#resolveSecretPath(keyId), "utf8");
266
+ return base64ToBytes(encoded.trim());
267
+ }
268
+ catch (error) {
269
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
270
+ throw new Error(`wallet_secret_missing_${keyId}`);
271
+ }
272
+ throw createLinuxSecretToolError("wallet_secret_provider_linux_runtime_error", error);
273
+ }
274
+ }
275
+ async storeSecret(keyId, secret) {
276
+ try {
277
+ await mkdir(this.#directoryPath, { recursive: true, mode: 0o700 });
278
+ await writeFileAtomic(this.#resolveSecretPath(keyId), `${bytesToBase64(secret)}\n`, { mode: 0o600 });
279
+ }
280
+ catch (error) {
281
+ throw createLinuxSecretToolError("wallet_secret_provider_linux_runtime_error", error);
282
+ }
283
+ }
284
+ async deleteSecret(keyId) {
285
+ await rm(this.#resolveSecretPath(keyId), { force: true }).catch(() => undefined);
286
+ }
287
+ }
288
+ class LinuxFallbackWalletSecretProvider {
289
+ kind = "linux-secret-service-fallback";
290
+ #secretService;
291
+ #fileProvider;
292
+ constructor(options) {
293
+ this.#secretService = options.secretService;
294
+ this.#fileProvider = options.fileProvider;
295
+ }
296
+ async #loadFromFileOrRethrow(keyId, error) {
297
+ try {
298
+ return await this.#fileProvider.loadSecret(keyId);
299
+ }
300
+ catch (fileError) {
301
+ if (isWalletSecretProviderMessage(fileError, `wallet_secret_missing_${keyId}`)
302
+ && (error.message === "wallet_secret_provider_linux_secret_tool_missing"
303
+ || error.message === "wallet_secret_provider_linux_secret_service_unavailable")) {
304
+ throw error;
305
+ }
306
+ throw fileError;
307
+ }
308
+ }
309
+ async loadSecret(keyId) {
310
+ try {
311
+ return await this.#secretService.loadSecret(keyId);
312
+ }
313
+ catch (error) {
314
+ if (!(error instanceof Error)) {
315
+ throw error;
316
+ }
317
+ if (error.message === "wallet_secret_provider_linux_secret_tool_missing"
318
+ || error.message === "wallet_secret_provider_linux_secret_service_unavailable"
319
+ || error.message === `wallet_secret_missing_${keyId}`) {
320
+ return await this.#loadFromFileOrRethrow(keyId, error);
321
+ }
322
+ if (error.message === "wallet_secret_provider_linux_runtime_error"
323
+ && await this.#fileProvider.hasSecret(keyId)) {
324
+ return await this.#fileProvider.loadSecret(keyId);
325
+ }
326
+ throw error;
327
+ }
328
+ }
329
+ async storeSecret(keyId, secret) {
330
+ try {
331
+ await this.#secretService.storeSecret(keyId, secret);
332
+ }
333
+ catch (error) {
334
+ if (isWalletSecretProviderMessage(error, "wallet_secret_provider_linux_secret_tool_missing")
335
+ || isWalletSecretProviderMessage(error, "wallet_secret_provider_linux_secret_service_unavailable")) {
336
+ await this.#fileProvider.storeSecret(keyId, secret);
337
+ return;
338
+ }
339
+ throw error;
340
+ }
341
+ }
342
+ async deleteSecret(keyId) {
343
+ await Promise.allSettled([
344
+ this.#secretService.deleteSecret(keyId),
345
+ this.#fileProvider.deleteSecret(keyId),
346
+ ]);
347
+ }
348
+ }
236
349
  class WindowsDpapiWalletSecretProvider {
237
350
  kind = "windows-dpapi";
238
351
  #directoryPath;
@@ -276,10 +389,13 @@ function createWalletSecretProviderForPlatform(platform, options = {}) {
276
389
  return new MacOsKeychainWalletSecretProvider();
277
390
  }
278
391
  if (platform === "win32") {
279
- return new WindowsDpapiWalletSecretProvider(join(resolveWalletRuntimePathsForTesting().stateRoot, "secrets"));
392
+ return new WindowsDpapiWalletSecretProvider(resolveSecretDirectoryPath(options));
280
393
  }
281
394
  if (platform === "linux") {
282
- return new LinuxSecretToolWalletSecretProvider(options.linuxSecretToolRunner);
395
+ return new LinuxFallbackWalletSecretProvider({
396
+ secretService: new LinuxSecretToolWalletSecretProvider(options.linuxSecretToolRunner),
397
+ fileProvider: new LinuxLocalFileWalletSecretProvider(resolveSecretDirectoryPath(options)),
398
+ });
283
399
  }
284
400
  throw new Error(`wallet_secret_provider_unsupported_${platform}`);
285
401
  }
@@ -0,0 +1,43 @@
1
+ import type { WalletSeedKind, WalletRuntimePaths } from "../runtime.js";
2
+ import { type RawWalletStateEnvelope } from "./storage.js";
3
+ export interface WalletSeedRecord {
4
+ name: string;
5
+ kind: WalletSeedKind;
6
+ walletRootId: string;
7
+ createdAtUnixMs: number;
8
+ restoredAtUnixMs: number | null;
9
+ }
10
+ export interface WalletSeedIndexV1 {
11
+ schemaVersion: 1;
12
+ lastWrittenAtUnixMs: number;
13
+ seeds: WalletSeedRecord[];
14
+ }
15
+ export declare function normalizeWalletSeedName(name: string): string;
16
+ export declare function isValidWalletSeedName(name: string): boolean;
17
+ export declare function assertValidImportedWalletSeedName(name: string): string;
18
+ export declare function findWalletSeedRecord(index: WalletSeedIndexV1, seedName: string): WalletSeedRecord | null;
19
+ export declare function loadWalletSeedIndex(options: {
20
+ paths: Pick<WalletRuntimePaths, "seedRegistryPath" | "walletStatePath" | "walletStateBackupPath">;
21
+ nowUnixMs?: number;
22
+ loadRawWalletStateEnvelope?: (paths: {
23
+ primaryPath: string;
24
+ backupPath: string;
25
+ }) => Promise<RawWalletStateEnvelope | null>;
26
+ }): Promise<WalletSeedIndexV1>;
27
+ export declare function saveWalletSeedIndex(paths: Pick<WalletRuntimePaths, "seedRegistryPath">, index: WalletSeedIndexV1): Promise<void>;
28
+ export declare function ensureMainWalletSeedIndexRecord(options: {
29
+ paths: Pick<WalletRuntimePaths, "seedRegistryPath" | "walletStatePath" | "walletStateBackupPath">;
30
+ walletRootId: string;
31
+ nowUnixMs?: number;
32
+ }): Promise<WalletSeedIndexV1>;
33
+ export declare function addImportedWalletSeedRecord(options: {
34
+ paths: Pick<WalletRuntimePaths, "seedRegistryPath" | "walletStatePath" | "walletStateBackupPath">;
35
+ seedName: string;
36
+ walletRootId: string;
37
+ nowUnixMs?: number;
38
+ }): Promise<WalletSeedIndexV1>;
39
+ export declare function removeWalletSeedRecord(options: {
40
+ paths: Pick<WalletRuntimePaths, "seedRegistryPath" | "walletStatePath" | "walletStateBackupPath">;
41
+ seedName: string;
42
+ nowUnixMs?: number;
43
+ }): Promise<WalletSeedIndexV1>;
@@ -0,0 +1,151 @@
1
+ import { mkdir, readFile } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+ import { writeFileAtomic } from "../fs/atomic.js";
4
+ import { extractWalletRootIdHintFromWalletStateEnvelope, loadRawWalletStateEnvelope, } from "./storage.js";
5
+ function createEmptySeedIndex(nowUnixMs) {
6
+ return {
7
+ schemaVersion: 1,
8
+ lastWrittenAtUnixMs: nowUnixMs,
9
+ seeds: [],
10
+ };
11
+ }
12
+ function sortSeedRecords(seeds) {
13
+ return [...seeds].sort((left, right) => left.name.localeCompare(right.name));
14
+ }
15
+ async function readSeedIndexFile(path) {
16
+ try {
17
+ const parsed = JSON.parse(await readFile(path, "utf8"));
18
+ if (parsed?.schemaVersion !== 1 || !Array.isArray(parsed.seeds)) {
19
+ throw new Error("wallet_seed_index_invalid");
20
+ }
21
+ return {
22
+ schemaVersion: 1,
23
+ lastWrittenAtUnixMs: typeof parsed.lastWrittenAtUnixMs === "number" ? parsed.lastWrittenAtUnixMs : 0,
24
+ seeds: sortSeedRecords(parsed.seeds),
25
+ };
26
+ }
27
+ catch (error) {
28
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
29
+ return null;
30
+ }
31
+ throw new Error("wallet_seed_index_invalid");
32
+ }
33
+ }
34
+ export function normalizeWalletSeedName(name) {
35
+ return name.trim().toLowerCase();
36
+ }
37
+ export function isValidWalletSeedName(name) {
38
+ return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name);
39
+ }
40
+ export function assertValidImportedWalletSeedName(name) {
41
+ const normalized = normalizeWalletSeedName(name);
42
+ if (!isValidWalletSeedName(normalized)) {
43
+ throw new Error("wallet_seed_name_invalid");
44
+ }
45
+ if (normalized === "main") {
46
+ throw new Error("wallet_seed_name_reserved");
47
+ }
48
+ return normalized;
49
+ }
50
+ export function findWalletSeedRecord(index, seedName) {
51
+ const normalized = normalizeWalletSeedName(seedName);
52
+ return index.seeds.find((seed) => seed.name === normalized) ?? null;
53
+ }
54
+ export async function loadWalletSeedIndex(options) {
55
+ const nowUnixMs = options.nowUnixMs ?? Date.now();
56
+ const stored = await readSeedIndexFile(options.paths.seedRegistryPath);
57
+ if (stored !== null) {
58
+ return stored;
59
+ }
60
+ const rawEnvelope = await (options.loadRawWalletStateEnvelope ?? loadRawWalletStateEnvelope)({
61
+ primaryPath: options.paths.walletStatePath,
62
+ backupPath: options.paths.walletStateBackupPath,
63
+ }).catch(() => null);
64
+ const mainWalletRootId = extractWalletRootIdHintFromWalletStateEnvelope(rawEnvelope?.envelope ?? null);
65
+ if (mainWalletRootId === null) {
66
+ return createEmptySeedIndex(nowUnixMs);
67
+ }
68
+ return {
69
+ schemaVersion: 1,
70
+ lastWrittenAtUnixMs: nowUnixMs,
71
+ seeds: [{
72
+ name: "main",
73
+ kind: "main",
74
+ walletRootId: mainWalletRootId,
75
+ createdAtUnixMs: nowUnixMs,
76
+ restoredAtUnixMs: null,
77
+ }],
78
+ };
79
+ }
80
+ export async function saveWalletSeedIndex(paths, index) {
81
+ await mkdir(dirname(paths.seedRegistryPath), { recursive: true });
82
+ await writeFileAtomic(paths.seedRegistryPath, `${JSON.stringify({
83
+ ...index,
84
+ seeds: sortSeedRecords(index.seeds),
85
+ }, null, 2)}\n`, { mode: 0o600 });
86
+ }
87
+ export async function ensureMainWalletSeedIndexRecord(options) {
88
+ const nowUnixMs = options.nowUnixMs ?? Date.now();
89
+ const index = await loadWalletSeedIndex({
90
+ paths: options.paths,
91
+ nowUnixMs,
92
+ });
93
+ const existing = findWalletSeedRecord(index, "main");
94
+ const seeds = index.seeds.filter((seed) => seed.name !== "main");
95
+ seeds.push({
96
+ name: "main",
97
+ kind: "main",
98
+ walletRootId: options.walletRootId,
99
+ createdAtUnixMs: existing?.createdAtUnixMs ?? nowUnixMs,
100
+ restoredAtUnixMs: existing?.restoredAtUnixMs ?? null,
101
+ });
102
+ const nextIndex = {
103
+ schemaVersion: 1,
104
+ lastWrittenAtUnixMs: nowUnixMs,
105
+ seeds: sortSeedRecords(seeds),
106
+ };
107
+ await saveWalletSeedIndex(options.paths, nextIndex);
108
+ return nextIndex;
109
+ }
110
+ export async function addImportedWalletSeedRecord(options) {
111
+ const nowUnixMs = options.nowUnixMs ?? Date.now();
112
+ const seedName = assertValidImportedWalletSeedName(options.seedName);
113
+ const index = await loadWalletSeedIndex({
114
+ paths: options.paths,
115
+ nowUnixMs,
116
+ });
117
+ if (findWalletSeedRecord(index, seedName) !== null) {
118
+ throw new Error("wallet_seed_name_exists");
119
+ }
120
+ const nextIndex = {
121
+ schemaVersion: 1,
122
+ lastWrittenAtUnixMs: nowUnixMs,
123
+ seeds: sortSeedRecords([
124
+ ...index.seeds,
125
+ {
126
+ name: seedName,
127
+ kind: "imported",
128
+ walletRootId: options.walletRootId,
129
+ createdAtUnixMs: nowUnixMs,
130
+ restoredAtUnixMs: nowUnixMs,
131
+ },
132
+ ]),
133
+ };
134
+ await saveWalletSeedIndex(options.paths, nextIndex);
135
+ return nextIndex;
136
+ }
137
+ export async function removeWalletSeedRecord(options) {
138
+ const nowUnixMs = options.nowUnixMs ?? Date.now();
139
+ const seedName = normalizeWalletSeedName(options.seedName);
140
+ const index = await loadWalletSeedIndex({
141
+ paths: options.paths,
142
+ nowUnixMs,
143
+ });
144
+ const nextIndex = {
145
+ schemaVersion: 1,
146
+ lastWrittenAtUnixMs: nowUnixMs,
147
+ seeds: sortSeedRecords(index.seeds.filter((seed) => seed.name !== seedName)),
148
+ };
149
+ await saveWalletSeedIndex(options.paths, nextIndex);
150
+ return nextIndex;
151
+ }
@@ -4,6 +4,7 @@ import type { RpcTransaction } from "../../bitcoind/types.js";
4
4
  import type { WalletPrompter } from "../lifecycle.js";
5
5
  import { type WalletRuntimePaths } from "../runtime.js";
6
6
  import { type WalletSecretProvider } from "../state/provider.js";
7
+ import type { ProactiveFamilyStateRecord } from "../types.js";
7
8
  import { openWalletReadContext } from "../read/index.js";
8
9
  import { type WalletMutationRpcClient } from "./common.js";
9
10
  interface WalletAnchorRpcClient extends WalletMutationRpcClient {
@@ -36,5 +37,26 @@ export interface AnchorDomainResult {
36
37
  status: "live" | "confirmed";
37
38
  reusedExisting: boolean;
38
39
  }
40
+ export interface ClearPendingAnchorOptions {
41
+ domainName: string;
42
+ dataDir: string;
43
+ databasePath: string;
44
+ provider?: WalletSecretProvider;
45
+ prompter: WalletPrompter;
46
+ assumeYes?: boolean;
47
+ nowUnixMs?: number;
48
+ paths?: WalletRuntimePaths;
49
+ openReadContext?: typeof openWalletReadContext;
50
+ attachService?: typeof attachOrStartManagedBitcoindService;
51
+ rpcFactory?: (config: Parameters<typeof createRpcClient>[0]) => WalletAnchorRpcClient;
52
+ }
53
+ export interface ClearPendingAnchorResult {
54
+ domainName: string;
55
+ cleared: boolean;
56
+ previousFamilyStatus: ProactiveFamilyStateRecord["status"] | null;
57
+ previousFamilyStep: ProactiveFamilyStateRecord["currentStep"] | null;
58
+ releasedDedicatedIndex: number | null;
59
+ }
39
60
  export declare function anchorDomain(options: AnchorDomainOptions): Promise<AnchorDomainResult>;
61
+ export declare function clearPendingAnchor(options: ClearPendingAnchorOptions): Promise<ClearPendingAnchorResult>;
40
62
  export {};