@metamask-previews/wallet-cli 0.0.0-preview-d548cc9a9 → 0.0.0-preview-11d78534e
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/dist/daemon/wallet-factory.cjs +0 -33
- package/dist/daemon/wallet-factory.cjs.map +1 -1
- package/dist/daemon/wallet-factory.d.cts.map +1 -1
- package/dist/daemon/wallet-factory.d.mts.map +1 -1
- package/dist/daemon/wallet-factory.mjs +0 -33
- package/dist/daemon/wallet-factory.mjs.map +1 -1
- package/package.json +1 -3
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createWallet = void 0;
|
|
4
|
-
const analytics_controller_1 = require("@metamask/analytics-controller");
|
|
5
|
-
const messenger_1 = require("@metamask/messenger");
|
|
6
4
|
const remote_feature_flag_controller_1 = require("@metamask/remote-feature-flag-controller");
|
|
7
5
|
const storage_service_1 = require("@metamask/storage-service");
|
|
8
6
|
const wallet_1 = require("@metamask/wallet");
|
|
@@ -56,12 +54,6 @@ function buildInstanceOptions(infuraProjectId) {
|
|
|
56
54
|
},
|
|
57
55
|
networkController: {
|
|
58
56
|
infuraProjectId,
|
|
59
|
-
// The CLI does not report analytics, so no endpoint is treated as public
|
|
60
|
-
// and no events are sampled.
|
|
61
|
-
analyticsOptions: {
|
|
62
|
-
isRpcEndpointUrlPublic: () => false,
|
|
63
|
-
rpcServiceEventsSampleRate: 0,
|
|
64
|
-
},
|
|
65
57
|
},
|
|
66
58
|
remoteFeatureFlagController: {
|
|
67
59
|
clientConfigApiService: new remote_feature_flag_controller_1.ClientConfigApiService({
|
|
@@ -95,29 +87,6 @@ function buildInstanceOptions(infuraProjectId) {
|
|
|
95
87
|
},
|
|
96
88
|
};
|
|
97
89
|
}
|
|
98
|
-
/**
|
|
99
|
-
* Register handlers for the `AnalyticsController` actions that
|
|
100
|
-
* `NetworkController` calls to emit RPC service analytics. The daemon does not
|
|
101
|
-
* report analytics, so these do nothing: `getState` returns an empty analytics
|
|
102
|
-
* ID, which stops the controller before it tracks anything, and `trackEvent` is
|
|
103
|
-
* a no-op. Registering them keeps those messenger calls from throwing.
|
|
104
|
-
*
|
|
105
|
-
* @param messenger - The wallet's root messenger.
|
|
106
|
-
*/
|
|
107
|
-
function registerAnalyticsStubHandlers(messenger) {
|
|
108
|
-
// Actions can only be registered under a messenger of their own namespace,
|
|
109
|
-
// and registering them delegates them up to the root automatically, so
|
|
110
|
-
// `NetworkController` (delegated the same actions) can call them.
|
|
111
|
-
const analyticsMessenger = new messenger_1.Messenger({
|
|
112
|
-
namespace: 'AnalyticsController',
|
|
113
|
-
parent: messenger,
|
|
114
|
-
});
|
|
115
|
-
analyticsMessenger.registerActionHandler('AnalyticsController:getState', () => ({
|
|
116
|
-
...(0, analytics_controller_1.getDefaultAnalyticsControllerState)(),
|
|
117
|
-
analyticsId: '',
|
|
118
|
-
}));
|
|
119
|
-
analyticsMessenger.registerActionHandler('AnalyticsController:trackEvent', () => undefined);
|
|
120
|
-
}
|
|
121
90
|
/**
|
|
122
91
|
* Create a configured `Wallet` for daemon use, backed by a SQLite key-value
|
|
123
92
|
* store for controller-state persistence.
|
|
@@ -178,7 +147,6 @@ async function createWallet({ databasePath, password, srp, infuraProjectId, log,
|
|
|
178
147
|
state,
|
|
179
148
|
instanceOptions: buildInstanceOptions(infuraProjectId),
|
|
180
149
|
});
|
|
181
|
-
registerAnalyticsStubHandlers(wallet.messenger);
|
|
182
150
|
unsubscribe = (0, persistence_js_1.subscribeToChanges)(wallet.messenger, wallet.controllerMetadata, store, logFn);
|
|
183
151
|
// Complete post-construction controller setup before serving requests —
|
|
184
152
|
// e.g. `NetworkController.init` applies the selected network so a provider
|
|
@@ -254,7 +222,6 @@ async function loadPersistedState(store, infuraProjectId, logFn) {
|
|
|
254
222
|
const probe = new wallet_1.Wallet({
|
|
255
223
|
instanceOptions: buildInstanceOptions(infuraProjectId),
|
|
256
224
|
});
|
|
257
|
-
registerAnalyticsStubHandlers(probe.messenger);
|
|
258
225
|
try {
|
|
259
226
|
return (0, persistence_js_1.loadState)(store, probe.controllerMetadata);
|
|
260
227
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wallet-factory.cjs","sourceRoot":"","sources":["../../src/daemon/wallet-factory.ts"],"names":[],"mappings":"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","sourcesContent":["import { getDefaultAnalyticsControllerState } from '@metamask/analytics-controller';\nimport type {\n AnalyticsControllerGetStateAction,\n AnalyticsControllerTrackEventAction,\n} from '@metamask/analytics-controller';\nimport { Messenger } from '@metamask/messenger';\nimport {\n ClientConfigApiService,\n ClientType,\n DistributionType,\n EnvironmentType,\n} from '@metamask/remote-feature-flag-controller';\nimport { InMemoryStorageAdapter } from '@metamask/storage-service';\nimport type { Json } from '@metamask/utils';\nimport {\n AlwaysOnlineAdapter,\n importSecretRecoveryPhrase,\n Wallet,\n} from '@metamask/wallet';\nimport type {\n DefaultActions,\n DefaultEvents,\n RootMessenger,\n WalletOptions,\n} from '@metamask/wallet';\nimport { rm } from 'node:fs/promises';\n\nimport { KeyValueStore } from '../persistence/KeyValueStore.js';\nimport { loadState, subscribeToChanges } from '../persistence/persistence.js';\nimport type { Password, Srp } from './secrets.js';\nimport type { Logger } from './types.js';\n\nconst IN_MEMORY_DATABASE_PATH = ':memory:';\n\nexport type CreateWalletConfig = {\n databasePath: string;\n password?: Password;\n srp: Srp;\n infuraProjectId: string;\n log?: Logger;\n};\n\nexport type CreateWalletResult = {\n wallet: Wallet;\n /**\n * Tear down everything `createWallet` set up, in the order that keeps\n * in-flight persistence writes valid: stop the state-change subscription,\n * destroy the wallet, then close the store (closing first would cause a\n * teardown-time persistence write to fail). Resilient — a failure in any\n * step is logged and the remaining steps still run — and idempotent (repeat\n * calls coalesce onto the same teardown).\n */\n dispose: () => Promise<void>;\n};\n\n/**\n * Build the per-instance options the daemon's `Wallet` is constructed with.\n *\n * Returns a fresh set on every call so the metadata probe and the real wallet\n * never share adapter/service instances (the probe is destroyed, which may\n * tear those instances down).\n *\n * Only the slots wired on `@metamask/wallet` today are populated:\n * - `storageService` — backed by an in-memory adapter. This is the wallet's\n * large-blob store (snap source, caches), distinct from the SQLite\n * `KeyValueStore` that persists controller state; no wired controller\n * offloads durable data to it yet, so in-memory suffices.\n * - `connectivityController` — the `AlwaysOnlineAdapter` exported for\n * node-like hosts that have no platform connectivity signal.\n * - `networkController` — the Infura project ID used to reach Infura RPC\n * endpoints.\n * - `remoteFeatureFlagController` — a `ClientConfigApiService` fetching real\n * flags over the network.\n * - `approvalController` — a no-op `showApprovalRequest` (the daemon is\n * headless).\n * - `transactionController` — swaps processing disabled and no client hooks;\n * see the slot's inline comment for why the daemon relies on the\n * controller's defaults for everything else.\n *\n * The optional `keyringController` slot is intentionally omitted so the\n * controller's built-in defaults (e.g. the PBKDF2 encryptor) apply.\n *\n * @param infuraProjectId - The Infura project ID for the `NetworkController`.\n * @returns The `instanceOptions` for the `Wallet` constructor.\n */\nfunction buildInstanceOptions(\n infuraProjectId: string,\n): WalletOptions['instanceOptions'] {\n return {\n approvalController: {\n // TODO: surface approval requests over the daemon transport.\n showApprovalRequest: (): undefined => undefined,\n },\n connectivityController: {\n connectivityAdapter: new AlwaysOnlineAdapter(),\n },\n gasFeeController: {\n // Identifies the CLI to the gas estimation API via the `X-Client-Id`\n // header.\n clientId: 'cli',\n },\n networkController: {\n infuraProjectId,\n // The CLI does not report analytics, so no endpoint is treated as public\n // and no events are sampled.\n analyticsOptions: {\n isRpcEndpointUrlPublic: (): boolean => false,\n rpcServiceEventsSampleRate: 0,\n },\n },\n remoteFeatureFlagController: {\n clientConfigApiService: new ClientConfigApiService({\n fetch: globalThis.fetch,\n config: {\n // TODO: switch to a CLI-specific `ClientType` once one exists; until\n // then `Extension` buckets feature flags as an extension client.\n client: ClientType.Extension,\n distribution: DistributionType.Main,\n environment: EnvironmentType.Production,\n },\n }),\n getMetaMetricsId: (): string => 'cli',\n clientVersion: '0.0.0',\n },\n storageService: {\n storage: new InMemoryStorageAdapter(),\n },\n transactionController: {\n // The CLI exposes no swaps surface, so skip the swaps-specific\n // post-processing a full wallet client runs (mobile makes the same\n // choice; the extension keeps swaps enabled).\n disableSwaps: true,\n // No CLI-specific transaction hooks: the controller's built-in publish\n // path broadcasts through the wired `NetworkController` provider, and the\n // remaining hooks (metrics, notifications, gas-fee tokens) are client-UI\n // concerns the headless daemon has no equivalent for. Every other option\n // (`getPermittedAccounts`, `isSimulationEnabled`, `trace`, …) is left at\n // the controller's default.\n hooks: {},\n },\n };\n}\n\n/**\n * Register handlers for the `AnalyticsController` actions that\n * `NetworkController` calls to emit RPC service analytics. The daemon does not\n * report analytics, so these do nothing: `getState` returns an empty analytics\n * ID, which stops the controller before it tracks anything, and `trackEvent` is\n * a no-op. Registering them keeps those messenger calls from throwing.\n *\n * @param messenger - The wallet's root messenger.\n */\nfunction registerAnalyticsStubHandlers(messenger: Wallet['messenger']): void {\n // Actions can only be registered under a messenger of their own namespace,\n // and registering them delegates them up to the root automatically, so\n // `NetworkController` (delegated the same actions) can call them.\n const analyticsMessenger = new Messenger<\n 'AnalyticsController',\n AnalyticsControllerGetStateAction | AnalyticsControllerTrackEventAction,\n never,\n RootMessenger<DefaultActions, DefaultEvents>\n >({\n namespace: 'AnalyticsController',\n parent: messenger as RootMessenger<DefaultActions, DefaultEvents>,\n });\n analyticsMessenger.registerActionHandler(\n 'AnalyticsController:getState',\n () => ({\n ...getDefaultAnalyticsControllerState(),\n analyticsId: '',\n }),\n );\n analyticsMessenger.registerActionHandler(\n 'AnalyticsController:trackEvent',\n () => undefined,\n );\n}\n\n/**\n * Create a configured `Wallet` for daemon use, backed by a SQLite key-value\n * store for controller-state persistence.\n *\n * Loads any previously-persisted controller state from the store, seeds the\n * wallet with it, then subscribes the store to subsequent state changes so all\n * persist-flagged properties are written through.\n *\n * If the store does not yet contain a keyring vault (first run), the supplied\n * secret recovery phrase is imported using the supplied password. On\n * subsequent runs, the persisted vault is reused: when a password is\n * supplied, the wallet is unlocked via `KeyringController:submitPassword` so\n * keyring-bound messenger actions work immediately; when no password is\n * supplied, the wallet starts locked and the caller is expected to invoke\n * `mm wallet unlock` before any keyring-bound operation. First-run startup\n * without a password is rejected (the SRP cannot be imported without one).\n * On a subsequent run, a wrong password rejects startup with a `Failed to\n * unlock the persisted vault` error that wraps the `submitPassword` rejection.\n *\n * On any failure after the store is opened, the store is closed (and the wallet\n * destroyed, if constructed). On a first-run failure, the on-disk database is\n * also removed so a retry does not latch onto an orphaned partial vault — this\n * covers in-process failures only; a hard crash (SIGKILL/power loss) mid-import\n * can still leave a vault on disk.\n *\n * @param config - Wallet configuration.\n * @param config.databasePath - Path to the SQLite database file (or\n * `':memory:'` for ephemeral use).\n * @param config.password - The wallet password. Optional on subsequent runs;\n * when omitted, the daemon starts with a locked keyring. Required on first\n * run (to import the SRP).\n * @param config.srp - The secret recovery phrase (BIP-39 mnemonic). Used\n * only on first run.\n * @param config.infuraProjectId - The Infura project ID for the\n * `NetworkController`.\n * @param config.log - Optional logger for persistence-write and teardown\n * failures. Without it, failures fall back to `console.error` (which a detached\n * daemon's `stdio: 'ignore'` discards).\n * @returns The `Wallet` and a `dispose` handle that tears it down.\n */\nexport async function createWallet({\n databasePath,\n password,\n srp,\n infuraProjectId,\n log,\n}: CreateWalletConfig): Promise<CreateWalletResult> {\n const logFn = log ?? ((message: string): void => console.error(message));\n const store = new KeyValueStore(databasePath);\n let wallet: Wallet | undefined;\n let unsubscribe: (() => void) | undefined;\n let wasFirstRun = false;\n\n try {\n const state = await loadPersistedState(store, infuraProjectId, logFn);\n wasFirstRun = !hasPersistedKeyring(state);\n\n // Validate the first-run precondition BEFORE constructing the wallet,\n // so a doomed startup doesn't build a Wallet (and wire persistence\n // handlers) just to tear it down.\n if (wasFirstRun && password === undefined) {\n throw new Error(\n 'A password is required on first run to import the secret recovery phrase. ' +\n 'Pass `--password` (or `MM_WALLET_PASSWORD`) on `mm daemon start`.',\n );\n }\n\n wallet = new Wallet({\n state,\n instanceOptions: buildInstanceOptions(infuraProjectId),\n });\n registerAnalyticsStubHandlers(wallet.messenger);\n unsubscribe = subscribeToChanges(\n wallet.messenger,\n wallet.controllerMetadata,\n store,\n logFn,\n );\n\n // Complete post-construction controller setup before serving requests —\n // e.g. `NetworkController.init` applies the selected network so a provider\n // is available. `init` settles every step independently; a rejected step\n // leaves the wallet only partially usable, so abort startup (the catch\n // below tears down and, on first run, removes the partial database) rather\n // than serving a degraded daemon.\n const initResults = await wallet.init();\n const initFailures = initResults.filter(\n (result): result is PromiseRejectedResult => result.status === 'rejected',\n );\n for (const failure of initFailures) {\n logFn(`Wallet init step failed: ${String(failure.reason)}`);\n }\n if (initFailures.length > 0) {\n const firstReason = String(initFailures[0].reason);\n throw new Error(\n `Wallet initialization failed (${initFailures.length} step(s)); refusing to serve a partially initialized wallet. First failure: ${firstReason}`,\n );\n }\n\n if (wasFirstRun) {\n // The precondition check above throws when `wasFirstRun && password ===\n // undefined`, so `password` is defined here. TS does not correlate the\n // two separate variables, so it cannot narrow `password` from\n // `wasFirstRun` alone — hence the assertion.\n await importSecretRecoveryPhrase(\n wallet,\n (password as Password).unwrap(),\n srp.unwrap(),\n );\n } else if (password !== undefined) {\n try {\n await wallet.messenger.call(\n 'KeyringController:submitPassword',\n password.unwrap(),\n );\n } catch (error) {\n throw new Error(\n `Failed to unlock the persisted vault: ${String(error)}`,\n );\n }\n }\n\n let disposePromise: Promise<void> | undefined;\n return {\n wallet,\n dispose: async () =>\n (disposePromise ??= teardown(unsubscribe, wallet, store, logFn)),\n };\n } catch (error) {\n await teardown(unsubscribe, wallet, store, logFn);\n\n if (wasFirstRun && databasePath !== IN_MEMORY_DATABASE_PATH) {\n // Best-effort cleanup of the on-disk SQLite files (main, WAL, SHM) so a\n // partially-persisted KeyringController vault cannot mislead the next run\n // into skipping SRP import. Covers in-process failures only — a crash\n // (SIGKILL/power loss) mid-import leaves the vault on disk.\n await Promise.all(\n [databasePath, `${databasePath}-wal`, `${databasePath}-shm`].map(\n (path) =>\n rm(path, { force: true }).catch((rmError: unknown) => {\n logFn(\n `Failed to remove ${path} during first-run cleanup: ${String(rmError)}`,\n );\n }),\n ),\n );\n }\n\n throw error;\n }\n}\n\n/**\n * Load persisted controller state, filtered to currently persist-flagged\n * properties.\n *\n * `loadState` filters against the live controller metadata, but that metadata\n * is only knowable from a constructed `Wallet` — and its output is what seeds\n * the real wallet. So this constructs a short-lived probe purely to read the\n * metadata, then tears it down.\n *\n * TODO: drop the probe once `@metamask/wallet` exposes controller metadata\n * without constructing a `Wallet`.\n *\n * @param store - The key-value store to read from.\n * @param infuraProjectId - The Infura project ID for the probe's\n * `NetworkController`.\n * @param logFn - Logger for a probe-teardown failure.\n * @returns The filtered persisted state, suitable for the `Wallet` `state`\n * option.\n */\nasync function loadPersistedState(\n store: KeyValueStore,\n infuraProjectId: string,\n logFn: Logger,\n): Promise<Record<string, Record<string, Json>>> {\n const probe = new Wallet({\n instanceOptions: buildInstanceOptions(infuraProjectId),\n });\n registerAnalyticsStubHandlers(probe.messenger);\n try {\n return loadState(store, probe.controllerMetadata);\n } finally {\n await probe.destroy().catch((error: unknown) => {\n logFn(`Metadata probe destroy failed: ${String(error)}`);\n });\n }\n}\n\n/**\n * Persistence-safe teardown of a wallet and its store; see {@link\n * CreateWalletResult.dispose} for the ordering rationale. Each step is\n * best-effort, so a failure is logged and the remaining steps still run.\n *\n * @param unsubscribe - The persistence-subscription unsubscribe function, if\n * one was registered.\n * @param wallet - The wallet to destroy, if one was constructed.\n * @param store - The store to close.\n * @param logFn - Logger for step failures.\n */\nasync function teardown(\n unsubscribe: (() => void) | undefined,\n wallet: Wallet | undefined,\n store: KeyValueStore,\n logFn: Logger,\n): Promise<void> {\n if (unsubscribe) {\n try {\n unsubscribe();\n } catch (error) {\n logFn(`Persistence unsubscribe failed during teardown: ${String(error)}`);\n }\n }\n if (wallet) {\n try {\n await wallet.destroy();\n } catch (error) {\n logFn(`wallet.destroy() failed during teardown: ${String(error)}`);\n }\n }\n try {\n store.close();\n } catch (error) {\n logFn(`store.close() failed during teardown: ${String(error)}`);\n }\n}\n\n/**\n * Determine whether the loaded state already contains a keyring vault.\n *\n * The KeyringController persists its `vault` once an SRP has been imported, so\n * its presence indicates that first-run setup completed before.\n *\n * @param state - The state loaded from the key-value store.\n * @returns True if a KeyringController vault string is present.\n */\nfunction hasPersistedKeyring(\n state: Record<string, Record<string, unknown>>,\n): boolean {\n return typeof state.KeyringController?.vault === 'string';\n}\n"]}
|
|
1
|
+
{"version":3,"file":"wallet-factory.cjs","sourceRoot":"","sources":["../../src/daemon/wallet-factory.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ClientConfigApiService,\n ClientType,\n DistributionType,\n EnvironmentType,\n} from '@metamask/remote-feature-flag-controller';\nimport { InMemoryStorageAdapter } from '@metamask/storage-service';\nimport type { Json } from '@metamask/utils';\nimport {\n AlwaysOnlineAdapter,\n importSecretRecoveryPhrase,\n Wallet,\n} from '@metamask/wallet';\nimport type { WalletOptions } from '@metamask/wallet';\nimport { rm } from 'node:fs/promises';\n\nimport { KeyValueStore } from '../persistence/KeyValueStore.js';\nimport { loadState, subscribeToChanges } from '../persistence/persistence.js';\nimport type { Password, Srp } from './secrets.js';\nimport type { Logger } from './types.js';\n\nconst IN_MEMORY_DATABASE_PATH = ':memory:';\n\nexport type CreateWalletConfig = {\n databasePath: string;\n password?: Password;\n srp: Srp;\n infuraProjectId: string;\n log?: Logger;\n};\n\nexport type CreateWalletResult = {\n wallet: Wallet;\n /**\n * Tear down everything `createWallet` set up, in the order that keeps\n * in-flight persistence writes valid: stop the state-change subscription,\n * destroy the wallet, then close the store (closing first would cause a\n * teardown-time persistence write to fail). Resilient — a failure in any\n * step is logged and the remaining steps still run — and idempotent (repeat\n * calls coalesce onto the same teardown).\n */\n dispose: () => Promise<void>;\n};\n\n/**\n * Build the per-instance options the daemon's `Wallet` is constructed with.\n *\n * Returns a fresh set on every call so the metadata probe and the real wallet\n * never share adapter/service instances (the probe is destroyed, which may\n * tear those instances down).\n *\n * Only the slots wired on `@metamask/wallet` today are populated:\n * - `storageService` — backed by an in-memory adapter. This is the wallet's\n * large-blob store (snap source, caches), distinct from the SQLite\n * `KeyValueStore` that persists controller state; no wired controller\n * offloads durable data to it yet, so in-memory suffices.\n * - `connectivityController` — the `AlwaysOnlineAdapter` exported for\n * node-like hosts that have no platform connectivity signal.\n * - `networkController` — the Infura project ID used to reach Infura RPC\n * endpoints.\n * - `remoteFeatureFlagController` — a `ClientConfigApiService` fetching real\n * flags over the network.\n * - `approvalController` — a no-op `showApprovalRequest` (the daemon is\n * headless).\n * - `transactionController` — swaps processing disabled and no client hooks;\n * see the slot's inline comment for why the daemon relies on the\n * controller's defaults for everything else.\n *\n * The optional `keyringController` slot is intentionally omitted so the\n * controller's built-in defaults (e.g. the PBKDF2 encryptor) apply.\n *\n * @param infuraProjectId - The Infura project ID for the `NetworkController`.\n * @returns The `instanceOptions` for the `Wallet` constructor.\n */\nfunction buildInstanceOptions(\n infuraProjectId: string,\n): WalletOptions['instanceOptions'] {\n return {\n approvalController: {\n // TODO: surface approval requests over the daemon transport.\n showApprovalRequest: (): undefined => undefined,\n },\n connectivityController: {\n connectivityAdapter: new AlwaysOnlineAdapter(),\n },\n gasFeeController: {\n // Identifies the CLI to the gas estimation API via the `X-Client-Id`\n // header.\n clientId: 'cli',\n },\n networkController: {\n infuraProjectId,\n },\n remoteFeatureFlagController: {\n clientConfigApiService: new ClientConfigApiService({\n fetch: globalThis.fetch,\n config: {\n // TODO: switch to a CLI-specific `ClientType` once one exists; until\n // then `Extension` buckets feature flags as an extension client.\n client: ClientType.Extension,\n distribution: DistributionType.Main,\n environment: EnvironmentType.Production,\n },\n }),\n getMetaMetricsId: (): string => 'cli',\n clientVersion: '0.0.0',\n },\n storageService: {\n storage: new InMemoryStorageAdapter(),\n },\n transactionController: {\n // The CLI exposes no swaps surface, so skip the swaps-specific\n // post-processing a full wallet client runs (mobile makes the same\n // choice; the extension keeps swaps enabled).\n disableSwaps: true,\n // No CLI-specific transaction hooks: the controller's built-in publish\n // path broadcasts through the wired `NetworkController` provider, and the\n // remaining hooks (metrics, notifications, gas-fee tokens) are client-UI\n // concerns the headless daemon has no equivalent for. Every other option\n // (`getPermittedAccounts`, `isSimulationEnabled`, `trace`, …) is left at\n // the controller's default.\n hooks: {},\n },\n };\n}\n\n/**\n * Create a configured `Wallet` for daemon use, backed by a SQLite key-value\n * store for controller-state persistence.\n *\n * Loads any previously-persisted controller state from the store, seeds the\n * wallet with it, then subscribes the store to subsequent state changes so all\n * persist-flagged properties are written through.\n *\n * If the store does not yet contain a keyring vault (first run), the supplied\n * secret recovery phrase is imported using the supplied password. On\n * subsequent runs, the persisted vault is reused: when a password is\n * supplied, the wallet is unlocked via `KeyringController:submitPassword` so\n * keyring-bound messenger actions work immediately; when no password is\n * supplied, the wallet starts locked and the caller is expected to invoke\n * `mm wallet unlock` before any keyring-bound operation. First-run startup\n * without a password is rejected (the SRP cannot be imported without one).\n * On a subsequent run, a wrong password rejects startup with a `Failed to\n * unlock the persisted vault` error that wraps the `submitPassword` rejection.\n *\n * On any failure after the store is opened, the store is closed (and the wallet\n * destroyed, if constructed). On a first-run failure, the on-disk database is\n * also removed so a retry does not latch onto an orphaned partial vault — this\n * covers in-process failures only; a hard crash (SIGKILL/power loss) mid-import\n * can still leave a vault on disk.\n *\n * @param config - Wallet configuration.\n * @param config.databasePath - Path to the SQLite database file (or\n * `':memory:'` for ephemeral use).\n * @param config.password - The wallet password. Optional on subsequent runs;\n * when omitted, the daemon starts with a locked keyring. Required on first\n * run (to import the SRP).\n * @param config.srp - The secret recovery phrase (BIP-39 mnemonic). Used\n * only on first run.\n * @param config.infuraProjectId - The Infura project ID for the\n * `NetworkController`.\n * @param config.log - Optional logger for persistence-write and teardown\n * failures. Without it, failures fall back to `console.error` (which a detached\n * daemon's `stdio: 'ignore'` discards).\n * @returns The `Wallet` and a `dispose` handle that tears it down.\n */\nexport async function createWallet({\n databasePath,\n password,\n srp,\n infuraProjectId,\n log,\n}: CreateWalletConfig): Promise<CreateWalletResult> {\n const logFn = log ?? ((message: string): void => console.error(message));\n const store = new KeyValueStore(databasePath);\n let wallet: Wallet | undefined;\n let unsubscribe: (() => void) | undefined;\n let wasFirstRun = false;\n\n try {\n const state = await loadPersistedState(store, infuraProjectId, logFn);\n wasFirstRun = !hasPersistedKeyring(state);\n\n // Validate the first-run precondition BEFORE constructing the wallet,\n // so a doomed startup doesn't build a Wallet (and wire persistence\n // handlers) just to tear it down.\n if (wasFirstRun && password === undefined) {\n throw new Error(\n 'A password is required on first run to import the secret recovery phrase. ' +\n 'Pass `--password` (or `MM_WALLET_PASSWORD`) on `mm daemon start`.',\n );\n }\n\n wallet = new Wallet({\n state,\n instanceOptions: buildInstanceOptions(infuraProjectId),\n });\n unsubscribe = subscribeToChanges(\n wallet.messenger,\n wallet.controllerMetadata,\n store,\n logFn,\n );\n\n // Complete post-construction controller setup before serving requests —\n // e.g. `NetworkController.init` applies the selected network so a provider\n // is available. `init` settles every step independently; a rejected step\n // leaves the wallet only partially usable, so abort startup (the catch\n // below tears down and, on first run, removes the partial database) rather\n // than serving a degraded daemon.\n const initResults = await wallet.init();\n const initFailures = initResults.filter(\n (result): result is PromiseRejectedResult => result.status === 'rejected',\n );\n for (const failure of initFailures) {\n logFn(`Wallet init step failed: ${String(failure.reason)}`);\n }\n if (initFailures.length > 0) {\n const firstReason = String(initFailures[0].reason);\n throw new Error(\n `Wallet initialization failed (${initFailures.length} step(s)); refusing to serve a partially initialized wallet. First failure: ${firstReason}`,\n );\n }\n\n if (wasFirstRun) {\n // The precondition check above throws when `wasFirstRun && password ===\n // undefined`, so `password` is defined here. TS does not correlate the\n // two separate variables, so it cannot narrow `password` from\n // `wasFirstRun` alone — hence the assertion.\n await importSecretRecoveryPhrase(\n wallet,\n (password as Password).unwrap(),\n srp.unwrap(),\n );\n } else if (password !== undefined) {\n try {\n await wallet.messenger.call(\n 'KeyringController:submitPassword',\n password.unwrap(),\n );\n } catch (error) {\n throw new Error(\n `Failed to unlock the persisted vault: ${String(error)}`,\n );\n }\n }\n\n let disposePromise: Promise<void> | undefined;\n return {\n wallet,\n dispose: async () =>\n (disposePromise ??= teardown(unsubscribe, wallet, store, logFn)),\n };\n } catch (error) {\n await teardown(unsubscribe, wallet, store, logFn);\n\n if (wasFirstRun && databasePath !== IN_MEMORY_DATABASE_PATH) {\n // Best-effort cleanup of the on-disk SQLite files (main, WAL, SHM) so a\n // partially-persisted KeyringController vault cannot mislead the next run\n // into skipping SRP import. Covers in-process failures only — a crash\n // (SIGKILL/power loss) mid-import leaves the vault on disk.\n await Promise.all(\n [databasePath, `${databasePath}-wal`, `${databasePath}-shm`].map(\n (path) =>\n rm(path, { force: true }).catch((rmError: unknown) => {\n logFn(\n `Failed to remove ${path} during first-run cleanup: ${String(rmError)}`,\n );\n }),\n ),\n );\n }\n\n throw error;\n }\n}\n\n/**\n * Load persisted controller state, filtered to currently persist-flagged\n * properties.\n *\n * `loadState` filters against the live controller metadata, but that metadata\n * is only knowable from a constructed `Wallet` — and its output is what seeds\n * the real wallet. So this constructs a short-lived probe purely to read the\n * metadata, then tears it down.\n *\n * TODO: drop the probe once `@metamask/wallet` exposes controller metadata\n * without constructing a `Wallet`.\n *\n * @param store - The key-value store to read from.\n * @param infuraProjectId - The Infura project ID for the probe's\n * `NetworkController`.\n * @param logFn - Logger for a probe-teardown failure.\n * @returns The filtered persisted state, suitable for the `Wallet` `state`\n * option.\n */\nasync function loadPersistedState(\n store: KeyValueStore,\n infuraProjectId: string,\n logFn: Logger,\n): Promise<Record<string, Record<string, Json>>> {\n const probe = new Wallet({\n instanceOptions: buildInstanceOptions(infuraProjectId),\n });\n try {\n return loadState(store, probe.controllerMetadata);\n } finally {\n await probe.destroy().catch((error: unknown) => {\n logFn(`Metadata probe destroy failed: ${String(error)}`);\n });\n }\n}\n\n/**\n * Persistence-safe teardown of a wallet and its store; see {@link\n * CreateWalletResult.dispose} for the ordering rationale. Each step is\n * best-effort, so a failure is logged and the remaining steps still run.\n *\n * @param unsubscribe - The persistence-subscription unsubscribe function, if\n * one was registered.\n * @param wallet - The wallet to destroy, if one was constructed.\n * @param store - The store to close.\n * @param logFn - Logger for step failures.\n */\nasync function teardown(\n unsubscribe: (() => void) | undefined,\n wallet: Wallet | undefined,\n store: KeyValueStore,\n logFn: Logger,\n): Promise<void> {\n if (unsubscribe) {\n try {\n unsubscribe();\n } catch (error) {\n logFn(`Persistence unsubscribe failed during teardown: ${String(error)}`);\n }\n }\n if (wallet) {\n try {\n await wallet.destroy();\n } catch (error) {\n logFn(`wallet.destroy() failed during teardown: ${String(error)}`);\n }\n }\n try {\n store.close();\n } catch (error) {\n logFn(`store.close() failed during teardown: ${String(error)}`);\n }\n}\n\n/**\n * Determine whether the loaded state already contains a keyring vault.\n *\n * The KeyringController persists its `vault` once an SRP has been imported, so\n * its presence indicates that first-run setup completed before.\n *\n * @param state - The state loaded from the key-value store.\n * @returns True if a KeyringController vault string is present.\n */\nfunction hasPersistedKeyring(\n state: Record<string, Record<string, unknown>>,\n): boolean {\n return typeof state.KeyringController?.vault === 'string';\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wallet-factory.d.cts","sourceRoot":"","sources":["../../src/daemon/wallet-factory.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"wallet-factory.d.cts","sourceRoot":"","sources":["../../src/daemon/wallet-factory.ts"],"names":[],"mappings":"AAQA,OAAO,EAGL,MAAM,EACP,yBAAyB;AAM1B,OAAO,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,sBAAqB;AAClD,OAAO,KAAK,EAAE,MAAM,EAAE,oBAAmB;AAIzC,MAAM,MAAM,kBAAkB,GAAG;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,GAAG,EAAE,GAAG,CAAC;IACT,eAAe,EAAE,MAAM,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;OAOG;IACH,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AAoFF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,wBAAsB,YAAY,CAAC,EACjC,YAAY,EACZ,QAAQ,EACR,GAAG,EACH,eAAe,EACf,GAAG,GACJ,EAAE,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAuGlD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wallet-factory.d.mts","sourceRoot":"","sources":["../../src/daemon/wallet-factory.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"wallet-factory.d.mts","sourceRoot":"","sources":["../../src/daemon/wallet-factory.ts"],"names":[],"mappings":"AAQA,OAAO,EAGL,MAAM,EACP,yBAAyB;AAM1B,OAAO,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,sBAAqB;AAClD,OAAO,KAAK,EAAE,MAAM,EAAE,oBAAmB;AAIzC,MAAM,MAAM,kBAAkB,GAAG;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,GAAG,EAAE,GAAG,CAAC;IACT,eAAe,EAAE,MAAM,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;OAOG;IACH,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AAoFF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,wBAAsB,YAAY,CAAC,EACjC,YAAY,EACZ,QAAQ,EACR,GAAG,EACH,eAAe,EACf,GAAG,GACJ,EAAE,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAuGlD"}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { getDefaultAnalyticsControllerState } from "@metamask/analytics-controller";
|
|
2
|
-
import { Messenger } from "@metamask/messenger";
|
|
3
1
|
import { ClientConfigApiService, ClientType, DistributionType, EnvironmentType } from "@metamask/remote-feature-flag-controller";
|
|
4
2
|
import { InMemoryStorageAdapter } from "@metamask/storage-service";
|
|
5
3
|
import { AlwaysOnlineAdapter, importSecretRecoveryPhrase, Wallet } from "@metamask/wallet";
|
|
@@ -53,12 +51,6 @@ function buildInstanceOptions(infuraProjectId) {
|
|
|
53
51
|
},
|
|
54
52
|
networkController: {
|
|
55
53
|
infuraProjectId,
|
|
56
|
-
// The CLI does not report analytics, so no endpoint is treated as public
|
|
57
|
-
// and no events are sampled.
|
|
58
|
-
analyticsOptions: {
|
|
59
|
-
isRpcEndpointUrlPublic: () => false,
|
|
60
|
-
rpcServiceEventsSampleRate: 0,
|
|
61
|
-
},
|
|
62
54
|
},
|
|
63
55
|
remoteFeatureFlagController: {
|
|
64
56
|
clientConfigApiService: new ClientConfigApiService({
|
|
@@ -92,29 +84,6 @@ function buildInstanceOptions(infuraProjectId) {
|
|
|
92
84
|
},
|
|
93
85
|
};
|
|
94
86
|
}
|
|
95
|
-
/**
|
|
96
|
-
* Register handlers for the `AnalyticsController` actions that
|
|
97
|
-
* `NetworkController` calls to emit RPC service analytics. The daemon does not
|
|
98
|
-
* report analytics, so these do nothing: `getState` returns an empty analytics
|
|
99
|
-
* ID, which stops the controller before it tracks anything, and `trackEvent` is
|
|
100
|
-
* a no-op. Registering them keeps those messenger calls from throwing.
|
|
101
|
-
*
|
|
102
|
-
* @param messenger - The wallet's root messenger.
|
|
103
|
-
*/
|
|
104
|
-
function registerAnalyticsStubHandlers(messenger) {
|
|
105
|
-
// Actions can only be registered under a messenger of their own namespace,
|
|
106
|
-
// and registering them delegates them up to the root automatically, so
|
|
107
|
-
// `NetworkController` (delegated the same actions) can call them.
|
|
108
|
-
const analyticsMessenger = new Messenger({
|
|
109
|
-
namespace: 'AnalyticsController',
|
|
110
|
-
parent: messenger,
|
|
111
|
-
});
|
|
112
|
-
analyticsMessenger.registerActionHandler('AnalyticsController:getState', () => ({
|
|
113
|
-
...getDefaultAnalyticsControllerState(),
|
|
114
|
-
analyticsId: '',
|
|
115
|
-
}));
|
|
116
|
-
analyticsMessenger.registerActionHandler('AnalyticsController:trackEvent', () => undefined);
|
|
117
|
-
}
|
|
118
87
|
/**
|
|
119
88
|
* Create a configured `Wallet` for daemon use, backed by a SQLite key-value
|
|
120
89
|
* store for controller-state persistence.
|
|
@@ -175,7 +144,6 @@ export async function createWallet({ databasePath, password, srp, infuraProjectI
|
|
|
175
144
|
state,
|
|
176
145
|
instanceOptions: buildInstanceOptions(infuraProjectId),
|
|
177
146
|
});
|
|
178
|
-
registerAnalyticsStubHandlers(wallet.messenger);
|
|
179
147
|
unsubscribe = subscribeToChanges(wallet.messenger, wallet.controllerMetadata, store, logFn);
|
|
180
148
|
// Complete post-construction controller setup before serving requests —
|
|
181
149
|
// e.g. `NetworkController.init` applies the selected network so a provider
|
|
@@ -250,7 +218,6 @@ async function loadPersistedState(store, infuraProjectId, logFn) {
|
|
|
250
218
|
const probe = new Wallet({
|
|
251
219
|
instanceOptions: buildInstanceOptions(infuraProjectId),
|
|
252
220
|
});
|
|
253
|
-
registerAnalyticsStubHandlers(probe.messenger);
|
|
254
221
|
try {
|
|
255
222
|
return loadState(store, probe.controllerMetadata);
|
|
256
223
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wallet-factory.mjs","sourceRoot":"","sources":["../../src/daemon/wallet-factory.ts"],"names":[],"mappings":"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","sourcesContent":["import { getDefaultAnalyticsControllerState } from '@metamask/analytics-controller';\nimport type {\n AnalyticsControllerGetStateAction,\n AnalyticsControllerTrackEventAction,\n} from '@metamask/analytics-controller';\nimport { Messenger } from '@metamask/messenger';\nimport {\n ClientConfigApiService,\n ClientType,\n DistributionType,\n EnvironmentType,\n} from '@metamask/remote-feature-flag-controller';\nimport { InMemoryStorageAdapter } from '@metamask/storage-service';\nimport type { Json } from '@metamask/utils';\nimport {\n AlwaysOnlineAdapter,\n importSecretRecoveryPhrase,\n Wallet,\n} from '@metamask/wallet';\nimport type {\n DefaultActions,\n DefaultEvents,\n RootMessenger,\n WalletOptions,\n} from '@metamask/wallet';\nimport { rm } from 'node:fs/promises';\n\nimport { KeyValueStore } from '../persistence/KeyValueStore.js';\nimport { loadState, subscribeToChanges } from '../persistence/persistence.js';\nimport type { Password, Srp } from './secrets.js';\nimport type { Logger } from './types.js';\n\nconst IN_MEMORY_DATABASE_PATH = ':memory:';\n\nexport type CreateWalletConfig = {\n databasePath: string;\n password?: Password;\n srp: Srp;\n infuraProjectId: string;\n log?: Logger;\n};\n\nexport type CreateWalletResult = {\n wallet: Wallet;\n /**\n * Tear down everything `createWallet` set up, in the order that keeps\n * in-flight persistence writes valid: stop the state-change subscription,\n * destroy the wallet, then close the store (closing first would cause a\n * teardown-time persistence write to fail). Resilient — a failure in any\n * step is logged and the remaining steps still run — and idempotent (repeat\n * calls coalesce onto the same teardown).\n */\n dispose: () => Promise<void>;\n};\n\n/**\n * Build the per-instance options the daemon's `Wallet` is constructed with.\n *\n * Returns a fresh set on every call so the metadata probe and the real wallet\n * never share adapter/service instances (the probe is destroyed, which may\n * tear those instances down).\n *\n * Only the slots wired on `@metamask/wallet` today are populated:\n * - `storageService` — backed by an in-memory adapter. This is the wallet's\n * large-blob store (snap source, caches), distinct from the SQLite\n * `KeyValueStore` that persists controller state; no wired controller\n * offloads durable data to it yet, so in-memory suffices.\n * - `connectivityController` — the `AlwaysOnlineAdapter` exported for\n * node-like hosts that have no platform connectivity signal.\n * - `networkController` — the Infura project ID used to reach Infura RPC\n * endpoints.\n * - `remoteFeatureFlagController` — a `ClientConfigApiService` fetching real\n * flags over the network.\n * - `approvalController` — a no-op `showApprovalRequest` (the daemon is\n * headless).\n * - `transactionController` — swaps processing disabled and no client hooks;\n * see the slot's inline comment for why the daemon relies on the\n * controller's defaults for everything else.\n *\n * The optional `keyringController` slot is intentionally omitted so the\n * controller's built-in defaults (e.g. the PBKDF2 encryptor) apply.\n *\n * @param infuraProjectId - The Infura project ID for the `NetworkController`.\n * @returns The `instanceOptions` for the `Wallet` constructor.\n */\nfunction buildInstanceOptions(\n infuraProjectId: string,\n): WalletOptions['instanceOptions'] {\n return {\n approvalController: {\n // TODO: surface approval requests over the daemon transport.\n showApprovalRequest: (): undefined => undefined,\n },\n connectivityController: {\n connectivityAdapter: new AlwaysOnlineAdapter(),\n },\n gasFeeController: {\n // Identifies the CLI to the gas estimation API via the `X-Client-Id`\n // header.\n clientId: 'cli',\n },\n networkController: {\n infuraProjectId,\n // The CLI does not report analytics, so no endpoint is treated as public\n // and no events are sampled.\n analyticsOptions: {\n isRpcEndpointUrlPublic: (): boolean => false,\n rpcServiceEventsSampleRate: 0,\n },\n },\n remoteFeatureFlagController: {\n clientConfigApiService: new ClientConfigApiService({\n fetch: globalThis.fetch,\n config: {\n // TODO: switch to a CLI-specific `ClientType` once one exists; until\n // then `Extension` buckets feature flags as an extension client.\n client: ClientType.Extension,\n distribution: DistributionType.Main,\n environment: EnvironmentType.Production,\n },\n }),\n getMetaMetricsId: (): string => 'cli',\n clientVersion: '0.0.0',\n },\n storageService: {\n storage: new InMemoryStorageAdapter(),\n },\n transactionController: {\n // The CLI exposes no swaps surface, so skip the swaps-specific\n // post-processing a full wallet client runs (mobile makes the same\n // choice; the extension keeps swaps enabled).\n disableSwaps: true,\n // No CLI-specific transaction hooks: the controller's built-in publish\n // path broadcasts through the wired `NetworkController` provider, and the\n // remaining hooks (metrics, notifications, gas-fee tokens) are client-UI\n // concerns the headless daemon has no equivalent for. Every other option\n // (`getPermittedAccounts`, `isSimulationEnabled`, `trace`, …) is left at\n // the controller's default.\n hooks: {},\n },\n };\n}\n\n/**\n * Register handlers for the `AnalyticsController` actions that\n * `NetworkController` calls to emit RPC service analytics. The daemon does not\n * report analytics, so these do nothing: `getState` returns an empty analytics\n * ID, which stops the controller before it tracks anything, and `trackEvent` is\n * a no-op. Registering them keeps those messenger calls from throwing.\n *\n * @param messenger - The wallet's root messenger.\n */\nfunction registerAnalyticsStubHandlers(messenger: Wallet['messenger']): void {\n // Actions can only be registered under a messenger of their own namespace,\n // and registering them delegates them up to the root automatically, so\n // `NetworkController` (delegated the same actions) can call them.\n const analyticsMessenger = new Messenger<\n 'AnalyticsController',\n AnalyticsControllerGetStateAction | AnalyticsControllerTrackEventAction,\n never,\n RootMessenger<DefaultActions, DefaultEvents>\n >({\n namespace: 'AnalyticsController',\n parent: messenger as RootMessenger<DefaultActions, DefaultEvents>,\n });\n analyticsMessenger.registerActionHandler(\n 'AnalyticsController:getState',\n () => ({\n ...getDefaultAnalyticsControllerState(),\n analyticsId: '',\n }),\n );\n analyticsMessenger.registerActionHandler(\n 'AnalyticsController:trackEvent',\n () => undefined,\n );\n}\n\n/**\n * Create a configured `Wallet` for daemon use, backed by a SQLite key-value\n * store for controller-state persistence.\n *\n * Loads any previously-persisted controller state from the store, seeds the\n * wallet with it, then subscribes the store to subsequent state changes so all\n * persist-flagged properties are written through.\n *\n * If the store does not yet contain a keyring vault (first run), the supplied\n * secret recovery phrase is imported using the supplied password. On\n * subsequent runs, the persisted vault is reused: when a password is\n * supplied, the wallet is unlocked via `KeyringController:submitPassword` so\n * keyring-bound messenger actions work immediately; when no password is\n * supplied, the wallet starts locked and the caller is expected to invoke\n * `mm wallet unlock` before any keyring-bound operation. First-run startup\n * without a password is rejected (the SRP cannot be imported without one).\n * On a subsequent run, a wrong password rejects startup with a `Failed to\n * unlock the persisted vault` error that wraps the `submitPassword` rejection.\n *\n * On any failure after the store is opened, the store is closed (and the wallet\n * destroyed, if constructed). On a first-run failure, the on-disk database is\n * also removed so a retry does not latch onto an orphaned partial vault — this\n * covers in-process failures only; a hard crash (SIGKILL/power loss) mid-import\n * can still leave a vault on disk.\n *\n * @param config - Wallet configuration.\n * @param config.databasePath - Path to the SQLite database file (or\n * `':memory:'` for ephemeral use).\n * @param config.password - The wallet password. Optional on subsequent runs;\n * when omitted, the daemon starts with a locked keyring. Required on first\n * run (to import the SRP).\n * @param config.srp - The secret recovery phrase (BIP-39 mnemonic). Used\n * only on first run.\n * @param config.infuraProjectId - The Infura project ID for the\n * `NetworkController`.\n * @param config.log - Optional logger for persistence-write and teardown\n * failures. Without it, failures fall back to `console.error` (which a detached\n * daemon's `stdio: 'ignore'` discards).\n * @returns The `Wallet` and a `dispose` handle that tears it down.\n */\nexport async function createWallet({\n databasePath,\n password,\n srp,\n infuraProjectId,\n log,\n}: CreateWalletConfig): Promise<CreateWalletResult> {\n const logFn = log ?? ((message: string): void => console.error(message));\n const store = new KeyValueStore(databasePath);\n let wallet: Wallet | undefined;\n let unsubscribe: (() => void) | undefined;\n let wasFirstRun = false;\n\n try {\n const state = await loadPersistedState(store, infuraProjectId, logFn);\n wasFirstRun = !hasPersistedKeyring(state);\n\n // Validate the first-run precondition BEFORE constructing the wallet,\n // so a doomed startup doesn't build a Wallet (and wire persistence\n // handlers) just to tear it down.\n if (wasFirstRun && password === undefined) {\n throw new Error(\n 'A password is required on first run to import the secret recovery phrase. ' +\n 'Pass `--password` (or `MM_WALLET_PASSWORD`) on `mm daemon start`.',\n );\n }\n\n wallet = new Wallet({\n state,\n instanceOptions: buildInstanceOptions(infuraProjectId),\n });\n registerAnalyticsStubHandlers(wallet.messenger);\n unsubscribe = subscribeToChanges(\n wallet.messenger,\n wallet.controllerMetadata,\n store,\n logFn,\n );\n\n // Complete post-construction controller setup before serving requests —\n // e.g. `NetworkController.init` applies the selected network so a provider\n // is available. `init` settles every step independently; a rejected step\n // leaves the wallet only partially usable, so abort startup (the catch\n // below tears down and, on first run, removes the partial database) rather\n // than serving a degraded daemon.\n const initResults = await wallet.init();\n const initFailures = initResults.filter(\n (result): result is PromiseRejectedResult => result.status === 'rejected',\n );\n for (const failure of initFailures) {\n logFn(`Wallet init step failed: ${String(failure.reason)}`);\n }\n if (initFailures.length > 0) {\n const firstReason = String(initFailures[0].reason);\n throw new Error(\n `Wallet initialization failed (${initFailures.length} step(s)); refusing to serve a partially initialized wallet. First failure: ${firstReason}`,\n );\n }\n\n if (wasFirstRun) {\n // The precondition check above throws when `wasFirstRun && password ===\n // undefined`, so `password` is defined here. TS does not correlate the\n // two separate variables, so it cannot narrow `password` from\n // `wasFirstRun` alone — hence the assertion.\n await importSecretRecoveryPhrase(\n wallet,\n (password as Password).unwrap(),\n srp.unwrap(),\n );\n } else if (password !== undefined) {\n try {\n await wallet.messenger.call(\n 'KeyringController:submitPassword',\n password.unwrap(),\n );\n } catch (error) {\n throw new Error(\n `Failed to unlock the persisted vault: ${String(error)}`,\n );\n }\n }\n\n let disposePromise: Promise<void> | undefined;\n return {\n wallet,\n dispose: async () =>\n (disposePromise ??= teardown(unsubscribe, wallet, store, logFn)),\n };\n } catch (error) {\n await teardown(unsubscribe, wallet, store, logFn);\n\n if (wasFirstRun && databasePath !== IN_MEMORY_DATABASE_PATH) {\n // Best-effort cleanup of the on-disk SQLite files (main, WAL, SHM) so a\n // partially-persisted KeyringController vault cannot mislead the next run\n // into skipping SRP import. Covers in-process failures only — a crash\n // (SIGKILL/power loss) mid-import leaves the vault on disk.\n await Promise.all(\n [databasePath, `${databasePath}-wal`, `${databasePath}-shm`].map(\n (path) =>\n rm(path, { force: true }).catch((rmError: unknown) => {\n logFn(\n `Failed to remove ${path} during first-run cleanup: ${String(rmError)}`,\n );\n }),\n ),\n );\n }\n\n throw error;\n }\n}\n\n/**\n * Load persisted controller state, filtered to currently persist-flagged\n * properties.\n *\n * `loadState` filters against the live controller metadata, but that metadata\n * is only knowable from a constructed `Wallet` — and its output is what seeds\n * the real wallet. So this constructs a short-lived probe purely to read the\n * metadata, then tears it down.\n *\n * TODO: drop the probe once `@metamask/wallet` exposes controller metadata\n * without constructing a `Wallet`.\n *\n * @param store - The key-value store to read from.\n * @param infuraProjectId - The Infura project ID for the probe's\n * `NetworkController`.\n * @param logFn - Logger for a probe-teardown failure.\n * @returns The filtered persisted state, suitable for the `Wallet` `state`\n * option.\n */\nasync function loadPersistedState(\n store: KeyValueStore,\n infuraProjectId: string,\n logFn: Logger,\n): Promise<Record<string, Record<string, Json>>> {\n const probe = new Wallet({\n instanceOptions: buildInstanceOptions(infuraProjectId),\n });\n registerAnalyticsStubHandlers(probe.messenger);\n try {\n return loadState(store, probe.controllerMetadata);\n } finally {\n await probe.destroy().catch((error: unknown) => {\n logFn(`Metadata probe destroy failed: ${String(error)}`);\n });\n }\n}\n\n/**\n * Persistence-safe teardown of a wallet and its store; see {@link\n * CreateWalletResult.dispose} for the ordering rationale. Each step is\n * best-effort, so a failure is logged and the remaining steps still run.\n *\n * @param unsubscribe - The persistence-subscription unsubscribe function, if\n * one was registered.\n * @param wallet - The wallet to destroy, if one was constructed.\n * @param store - The store to close.\n * @param logFn - Logger for step failures.\n */\nasync function teardown(\n unsubscribe: (() => void) | undefined,\n wallet: Wallet | undefined,\n store: KeyValueStore,\n logFn: Logger,\n): Promise<void> {\n if (unsubscribe) {\n try {\n unsubscribe();\n } catch (error) {\n logFn(`Persistence unsubscribe failed during teardown: ${String(error)}`);\n }\n }\n if (wallet) {\n try {\n await wallet.destroy();\n } catch (error) {\n logFn(`wallet.destroy() failed during teardown: ${String(error)}`);\n }\n }\n try {\n store.close();\n } catch (error) {\n logFn(`store.close() failed during teardown: ${String(error)}`);\n }\n}\n\n/**\n * Determine whether the loaded state already contains a keyring vault.\n *\n * The KeyringController persists its `vault` once an SRP has been imported, so\n * its presence indicates that first-run setup completed before.\n *\n * @param state - The state loaded from the key-value store.\n * @returns True if a KeyringController vault string is present.\n */\nfunction hasPersistedKeyring(\n state: Record<string, Record<string, unknown>>,\n): boolean {\n return typeof state.KeyringController?.vault === 'string';\n}\n"]}
|
|
1
|
+
{"version":3,"file":"wallet-factory.mjs","sourceRoot":"","sources":["../../src/daemon/wallet-factory.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ClientConfigApiService,\n ClientType,\n DistributionType,\n EnvironmentType,\n} from '@metamask/remote-feature-flag-controller';\nimport { InMemoryStorageAdapter } from '@metamask/storage-service';\nimport type { Json } from '@metamask/utils';\nimport {\n AlwaysOnlineAdapter,\n importSecretRecoveryPhrase,\n Wallet,\n} from '@metamask/wallet';\nimport type { WalletOptions } from '@metamask/wallet';\nimport { rm } from 'node:fs/promises';\n\nimport { KeyValueStore } from '../persistence/KeyValueStore.js';\nimport { loadState, subscribeToChanges } from '../persistence/persistence.js';\nimport type { Password, Srp } from './secrets.js';\nimport type { Logger } from './types.js';\n\nconst IN_MEMORY_DATABASE_PATH = ':memory:';\n\nexport type CreateWalletConfig = {\n databasePath: string;\n password?: Password;\n srp: Srp;\n infuraProjectId: string;\n log?: Logger;\n};\n\nexport type CreateWalletResult = {\n wallet: Wallet;\n /**\n * Tear down everything `createWallet` set up, in the order that keeps\n * in-flight persistence writes valid: stop the state-change subscription,\n * destroy the wallet, then close the store (closing first would cause a\n * teardown-time persistence write to fail). Resilient — a failure in any\n * step is logged and the remaining steps still run — and idempotent (repeat\n * calls coalesce onto the same teardown).\n */\n dispose: () => Promise<void>;\n};\n\n/**\n * Build the per-instance options the daemon's `Wallet` is constructed with.\n *\n * Returns a fresh set on every call so the metadata probe and the real wallet\n * never share adapter/service instances (the probe is destroyed, which may\n * tear those instances down).\n *\n * Only the slots wired on `@metamask/wallet` today are populated:\n * - `storageService` — backed by an in-memory adapter. This is the wallet's\n * large-blob store (snap source, caches), distinct from the SQLite\n * `KeyValueStore` that persists controller state; no wired controller\n * offloads durable data to it yet, so in-memory suffices.\n * - `connectivityController` — the `AlwaysOnlineAdapter` exported for\n * node-like hosts that have no platform connectivity signal.\n * - `networkController` — the Infura project ID used to reach Infura RPC\n * endpoints.\n * - `remoteFeatureFlagController` — a `ClientConfigApiService` fetching real\n * flags over the network.\n * - `approvalController` — a no-op `showApprovalRequest` (the daemon is\n * headless).\n * - `transactionController` — swaps processing disabled and no client hooks;\n * see the slot's inline comment for why the daemon relies on the\n * controller's defaults for everything else.\n *\n * The optional `keyringController` slot is intentionally omitted so the\n * controller's built-in defaults (e.g. the PBKDF2 encryptor) apply.\n *\n * @param infuraProjectId - The Infura project ID for the `NetworkController`.\n * @returns The `instanceOptions` for the `Wallet` constructor.\n */\nfunction buildInstanceOptions(\n infuraProjectId: string,\n): WalletOptions['instanceOptions'] {\n return {\n approvalController: {\n // TODO: surface approval requests over the daemon transport.\n showApprovalRequest: (): undefined => undefined,\n },\n connectivityController: {\n connectivityAdapter: new AlwaysOnlineAdapter(),\n },\n gasFeeController: {\n // Identifies the CLI to the gas estimation API via the `X-Client-Id`\n // header.\n clientId: 'cli',\n },\n networkController: {\n infuraProjectId,\n },\n remoteFeatureFlagController: {\n clientConfigApiService: new ClientConfigApiService({\n fetch: globalThis.fetch,\n config: {\n // TODO: switch to a CLI-specific `ClientType` once one exists; until\n // then `Extension` buckets feature flags as an extension client.\n client: ClientType.Extension,\n distribution: DistributionType.Main,\n environment: EnvironmentType.Production,\n },\n }),\n getMetaMetricsId: (): string => 'cli',\n clientVersion: '0.0.0',\n },\n storageService: {\n storage: new InMemoryStorageAdapter(),\n },\n transactionController: {\n // The CLI exposes no swaps surface, so skip the swaps-specific\n // post-processing a full wallet client runs (mobile makes the same\n // choice; the extension keeps swaps enabled).\n disableSwaps: true,\n // No CLI-specific transaction hooks: the controller's built-in publish\n // path broadcasts through the wired `NetworkController` provider, and the\n // remaining hooks (metrics, notifications, gas-fee tokens) are client-UI\n // concerns the headless daemon has no equivalent for. Every other option\n // (`getPermittedAccounts`, `isSimulationEnabled`, `trace`, …) is left at\n // the controller's default.\n hooks: {},\n },\n };\n}\n\n/**\n * Create a configured `Wallet` for daemon use, backed by a SQLite key-value\n * store for controller-state persistence.\n *\n * Loads any previously-persisted controller state from the store, seeds the\n * wallet with it, then subscribes the store to subsequent state changes so all\n * persist-flagged properties are written through.\n *\n * If the store does not yet contain a keyring vault (first run), the supplied\n * secret recovery phrase is imported using the supplied password. On\n * subsequent runs, the persisted vault is reused: when a password is\n * supplied, the wallet is unlocked via `KeyringController:submitPassword` so\n * keyring-bound messenger actions work immediately; when no password is\n * supplied, the wallet starts locked and the caller is expected to invoke\n * `mm wallet unlock` before any keyring-bound operation. First-run startup\n * without a password is rejected (the SRP cannot be imported without one).\n * On a subsequent run, a wrong password rejects startup with a `Failed to\n * unlock the persisted vault` error that wraps the `submitPassword` rejection.\n *\n * On any failure after the store is opened, the store is closed (and the wallet\n * destroyed, if constructed). On a first-run failure, the on-disk database is\n * also removed so a retry does not latch onto an orphaned partial vault — this\n * covers in-process failures only; a hard crash (SIGKILL/power loss) mid-import\n * can still leave a vault on disk.\n *\n * @param config - Wallet configuration.\n * @param config.databasePath - Path to the SQLite database file (or\n * `':memory:'` for ephemeral use).\n * @param config.password - The wallet password. Optional on subsequent runs;\n * when omitted, the daemon starts with a locked keyring. Required on first\n * run (to import the SRP).\n * @param config.srp - The secret recovery phrase (BIP-39 mnemonic). Used\n * only on first run.\n * @param config.infuraProjectId - The Infura project ID for the\n * `NetworkController`.\n * @param config.log - Optional logger for persistence-write and teardown\n * failures. Without it, failures fall back to `console.error` (which a detached\n * daemon's `stdio: 'ignore'` discards).\n * @returns The `Wallet` and a `dispose` handle that tears it down.\n */\nexport async function createWallet({\n databasePath,\n password,\n srp,\n infuraProjectId,\n log,\n}: CreateWalletConfig): Promise<CreateWalletResult> {\n const logFn = log ?? ((message: string): void => console.error(message));\n const store = new KeyValueStore(databasePath);\n let wallet: Wallet | undefined;\n let unsubscribe: (() => void) | undefined;\n let wasFirstRun = false;\n\n try {\n const state = await loadPersistedState(store, infuraProjectId, logFn);\n wasFirstRun = !hasPersistedKeyring(state);\n\n // Validate the first-run precondition BEFORE constructing the wallet,\n // so a doomed startup doesn't build a Wallet (and wire persistence\n // handlers) just to tear it down.\n if (wasFirstRun && password === undefined) {\n throw new Error(\n 'A password is required on first run to import the secret recovery phrase. ' +\n 'Pass `--password` (or `MM_WALLET_PASSWORD`) on `mm daemon start`.',\n );\n }\n\n wallet = new Wallet({\n state,\n instanceOptions: buildInstanceOptions(infuraProjectId),\n });\n unsubscribe = subscribeToChanges(\n wallet.messenger,\n wallet.controllerMetadata,\n store,\n logFn,\n );\n\n // Complete post-construction controller setup before serving requests —\n // e.g. `NetworkController.init` applies the selected network so a provider\n // is available. `init` settles every step independently; a rejected step\n // leaves the wallet only partially usable, so abort startup (the catch\n // below tears down and, on first run, removes the partial database) rather\n // than serving a degraded daemon.\n const initResults = await wallet.init();\n const initFailures = initResults.filter(\n (result): result is PromiseRejectedResult => result.status === 'rejected',\n );\n for (const failure of initFailures) {\n logFn(`Wallet init step failed: ${String(failure.reason)}`);\n }\n if (initFailures.length > 0) {\n const firstReason = String(initFailures[0].reason);\n throw new Error(\n `Wallet initialization failed (${initFailures.length} step(s)); refusing to serve a partially initialized wallet. First failure: ${firstReason}`,\n );\n }\n\n if (wasFirstRun) {\n // The precondition check above throws when `wasFirstRun && password ===\n // undefined`, so `password` is defined here. TS does not correlate the\n // two separate variables, so it cannot narrow `password` from\n // `wasFirstRun` alone — hence the assertion.\n await importSecretRecoveryPhrase(\n wallet,\n (password as Password).unwrap(),\n srp.unwrap(),\n );\n } else if (password !== undefined) {\n try {\n await wallet.messenger.call(\n 'KeyringController:submitPassword',\n password.unwrap(),\n );\n } catch (error) {\n throw new Error(\n `Failed to unlock the persisted vault: ${String(error)}`,\n );\n }\n }\n\n let disposePromise: Promise<void> | undefined;\n return {\n wallet,\n dispose: async () =>\n (disposePromise ??= teardown(unsubscribe, wallet, store, logFn)),\n };\n } catch (error) {\n await teardown(unsubscribe, wallet, store, logFn);\n\n if (wasFirstRun && databasePath !== IN_MEMORY_DATABASE_PATH) {\n // Best-effort cleanup of the on-disk SQLite files (main, WAL, SHM) so a\n // partially-persisted KeyringController vault cannot mislead the next run\n // into skipping SRP import. Covers in-process failures only — a crash\n // (SIGKILL/power loss) mid-import leaves the vault on disk.\n await Promise.all(\n [databasePath, `${databasePath}-wal`, `${databasePath}-shm`].map(\n (path) =>\n rm(path, { force: true }).catch((rmError: unknown) => {\n logFn(\n `Failed to remove ${path} during first-run cleanup: ${String(rmError)}`,\n );\n }),\n ),\n );\n }\n\n throw error;\n }\n}\n\n/**\n * Load persisted controller state, filtered to currently persist-flagged\n * properties.\n *\n * `loadState` filters against the live controller metadata, but that metadata\n * is only knowable from a constructed `Wallet` — and its output is what seeds\n * the real wallet. So this constructs a short-lived probe purely to read the\n * metadata, then tears it down.\n *\n * TODO: drop the probe once `@metamask/wallet` exposes controller metadata\n * without constructing a `Wallet`.\n *\n * @param store - The key-value store to read from.\n * @param infuraProjectId - The Infura project ID for the probe's\n * `NetworkController`.\n * @param logFn - Logger for a probe-teardown failure.\n * @returns The filtered persisted state, suitable for the `Wallet` `state`\n * option.\n */\nasync function loadPersistedState(\n store: KeyValueStore,\n infuraProjectId: string,\n logFn: Logger,\n): Promise<Record<string, Record<string, Json>>> {\n const probe = new Wallet({\n instanceOptions: buildInstanceOptions(infuraProjectId),\n });\n try {\n return loadState(store, probe.controllerMetadata);\n } finally {\n await probe.destroy().catch((error: unknown) => {\n logFn(`Metadata probe destroy failed: ${String(error)}`);\n });\n }\n}\n\n/**\n * Persistence-safe teardown of a wallet and its store; see {@link\n * CreateWalletResult.dispose} for the ordering rationale. Each step is\n * best-effort, so a failure is logged and the remaining steps still run.\n *\n * @param unsubscribe - The persistence-subscription unsubscribe function, if\n * one was registered.\n * @param wallet - The wallet to destroy, if one was constructed.\n * @param store - The store to close.\n * @param logFn - Logger for step failures.\n */\nasync function teardown(\n unsubscribe: (() => void) | undefined,\n wallet: Wallet | undefined,\n store: KeyValueStore,\n logFn: Logger,\n): Promise<void> {\n if (unsubscribe) {\n try {\n unsubscribe();\n } catch (error) {\n logFn(`Persistence unsubscribe failed during teardown: ${String(error)}`);\n }\n }\n if (wallet) {\n try {\n await wallet.destroy();\n } catch (error) {\n logFn(`wallet.destroy() failed during teardown: ${String(error)}`);\n }\n }\n try {\n store.close();\n } catch (error) {\n logFn(`store.close() failed during teardown: ${String(error)}`);\n }\n}\n\n/**\n * Determine whether the loaded state already contains a keyring vault.\n *\n * The KeyringController persists its `vault` once an SRP has been imported, so\n * its presence indicates that first-run setup completed before.\n *\n * @param state - The state loaded from the key-value store.\n * @returns True if a KeyringController vault string is present.\n */\nfunction hasPersistedKeyring(\n state: Record<string, Record<string, unknown>>,\n): boolean {\n return typeof state.KeyringController?.vault === 'string';\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metamask-previews/wallet-cli",
|
|
3
|
-
"version": "0.0.0-preview-
|
|
3
|
+
"version": "0.0.0-preview-11d78534e",
|
|
4
4
|
"description": "The CLI of @metamask/wallet",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Ethereum",
|
|
@@ -48,9 +48,7 @@
|
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@inquirer/confirm": "^6.0.11",
|
|
50
50
|
"@inquirer/password": "^5.1.1",
|
|
51
|
-
"@metamask/analytics-controller": "^1.2.1",
|
|
52
51
|
"@metamask/base-controller": "^9.1.0",
|
|
53
|
-
"@metamask/messenger": "^2.0.0",
|
|
54
52
|
"@metamask/remote-feature-flag-controller": "^4.2.2",
|
|
55
53
|
"@metamask/rpc-errors": "^7.0.2",
|
|
56
54
|
"@metamask/scure-bip39": "^2.1.1",
|