auramaxx 1.0.0-alpha.4
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/LICENSE +26 -0
- package/README.md +112 -0
- package/bin/aurawallet.js +121 -0
- package/docs/ADAPTERS.md +467 -0
- package/docs/API.md +2679 -0
- package/docs/APPS.md +198 -0
- package/docs/ARCHITECTURE.md +350 -0
- package/docs/AUTH.md +698 -0
- package/docs/BEST-PRACTICES.md +121 -0
- package/docs/CLI.md +61 -0
- package/docs/DEVELOPING-APPS.md +452 -0
- package/docs/EXTENSION.md +97 -0
- package/docs/JOBS.md +33 -0
- package/docs/MCP.md +76 -0
- package/docs/PROTOCOL.md +142 -0
- package/docs/SETUP.md +219 -0
- package/docs/WORKSPACE.md +672 -0
- package/docs/agent-auth.md +63 -0
- package/docs/aura-file.md +48 -0
- package/docs/credentials.md +53 -0
- package/docs/external/getting-started.md +65 -0
- package/docs/external/overview.md +45 -0
- package/docs/external/use-cases.md +48 -0
- package/docs/external/why-aura.md +35 -0
- package/docs/jobs/connect-agent.md +77 -0
- package/docs/jobs/migrate-from-dotenv.md +79 -0
- package/docs/jobs/recover-from-lockout.md +72 -0
- package/docs/jobs/secure-ci.md +63 -0
- package/docs/oauth2.md +42 -0
- package/docs/passkeys.md +60 -0
- package/docs/security.md +540 -0
- package/docs/specs/aura-open-protocol.md +61 -0
- package/docs/specs/aura-provider-plugin.md +24 -0
- package/docs/specs/aura-registry-model.md +31 -0
- package/docs/specs/fixtures/invalid-bad-key.aura +1 -0
- package/docs/specs/fixtures/invalid-bad-unicode-escape.aura +1 -0
- package/docs/specs/fixtures/invalid-duplicate-key.aura +2 -0
- package/docs/specs/fixtures/valid-basic.aura +4 -0
- package/docs/specs/fixtures/valid-provider-ref.aura +1 -0
- package/docs/specs/fixtures/valid-quoted-escapes.aura +2 -0
- package/docs/templates/RELEASE_NOTES_TEMPLATE.md +22 -0
- package/docs/totp.md +40 -0
- package/docs/wallet/AI.md +508 -0
- package/docs/wallet/DEVELOPING-STRATEGIES.md +713 -0
- package/docs/wallet/README.md +47 -0
- package/docs/wallet/STRATEGY.md +89 -0
- package/next.config.ts +21 -0
- package/package.json +151 -0
- package/postcss.config.mjs +8 -0
- package/prisma/migrations/20260214170000_baseline/migration.sql +511 -0
- package/prisma/migrations/20260216214537_add_passkey_model/migration.sql +18 -0
- package/prisma/migrations/20260217150500_add_credential_access_audit/migration.sql +31 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +447 -0
- package/public/logo-chevron.svg +31 -0
- package/public/logo-concentric.svg +31 -0
- package/public/logo-crosshatch.svg +39 -0
- package/public/logo-dashed.svg +39 -0
- package/public/logo-horizontal.svg +31 -0
- package/public/logo-m56.svg +64 -0
- package/public/logo.webp +0 -0
- package/scripts/add-app.js +245 -0
- package/scripts/init.sh +57 -0
- package/scripts/migrate-apikeys-to-credentials.ts +35 -0
- package/scripts/sandbox-agent-flow.sh +235 -0
- package/scripts/sandbox.sh +175 -0
- package/scripts/validate-job-docs.mjs +125 -0
- package/server/abi/SwapHelper.json +438 -0
- package/server/cli/approval.ts +447 -0
- package/server/cli/commands/app.ts +204 -0
- package/server/cli/commands/cron.ts +24 -0
- package/server/cli/commands/doctor.ts +1007 -0
- package/server/cli/commands/env.ts +456 -0
- package/server/cli/commands/init.ts +752 -0
- package/server/cli/commands/mcp.ts +125 -0
- package/server/cli/commands/restore.ts +314 -0
- package/server/cli/commands/shell-hook.ts +468 -0
- package/server/cli/commands/start.ts +62 -0
- package/server/cli/commands/status.ts +59 -0
- package/server/cli/commands/stop.ts +14 -0
- package/server/cli/commands/token.ts +180 -0
- package/server/cli/commands/unlock.ts +49 -0
- package/server/cli/commands/vault.ts +417 -0
- package/server/cli/index.ts +328 -0
- package/server/cli/lib/aura-parser.ts +64 -0
- package/server/cli/lib/credential-create.ts +74 -0
- package/server/cli/lib/credential-resolve.ts +254 -0
- package/server/cli/lib/dotenv-migrate.ts +116 -0
- package/server/cli/lib/dotenv-parser.ts +146 -0
- package/server/cli/lib/http.ts +91 -0
- package/server/cli/lib/init-steps.ts +76 -0
- package/server/cli/lib/local-agent-trust.ts +45 -0
- package/server/cli/lib/process.ts +136 -0
- package/server/cli/lib/prompt.ts +85 -0
- package/server/cli/lib/theme.ts +240 -0
- package/server/cli/socket.ts +570 -0
- package/server/cli/transport-client.ts +50 -0
- package/server/cron/index.ts +137 -0
- package/server/cron/job.ts +31 -0
- package/server/cron/jobs/balance-sync.ts +436 -0
- package/server/cron/jobs/incoming-scan.ts +506 -0
- package/server/cron/jobs/native-price.ts +70 -0
- package/server/cron/jobs/orphan-cleanup.ts +40 -0
- package/server/cron/jobs/strategy-runner.ts +175 -0
- package/server/cron/scheduler.ts +125 -0
- package/server/index.ts +406 -0
- package/server/lib/adapters/factory.ts +110 -0
- package/server/lib/adapters/index.ts +19 -0
- package/server/lib/adapters/router.ts +297 -0
- package/server/lib/adapters/telegram.ts +645 -0
- package/server/lib/adapters/types.ts +89 -0
- package/server/lib/adapters/webhook.ts +95 -0
- package/server/lib/address.ts +49 -0
- package/server/lib/agent-auth/contracts.ts +1194 -0
- package/server/lib/agent-profiles.ts +328 -0
- package/server/lib/ai.ts +285 -0
- package/server/lib/api-registry/contracts.ts +86 -0
- package/server/lib/api-registry/validation.ts +172 -0
- package/server/lib/apikey-migration.ts +189 -0
- package/server/lib/app-installer.ts +505 -0
- package/server/lib/app-tokens.ts +247 -0
- package/server/lib/auth.ts +314 -0
- package/server/lib/batch.ts +242 -0
- package/server/lib/cold.ts +874 -0
- package/server/lib/config.ts +381 -0
- package/server/lib/credential-access-audit.ts +85 -0
- package/server/lib/credential-access-policy.ts +110 -0
- package/server/lib/credential-health.ts +343 -0
- package/server/lib/credential-import.ts +487 -0
- package/server/lib/credential-scope.ts +87 -0
- package/server/lib/credential-shares.ts +190 -0
- package/server/lib/credential-transport.ts +342 -0
- package/server/lib/credential-vault.ts +77 -0
- package/server/lib/credentials.ts +333 -0
- package/server/lib/crypto.ts +8 -0
- package/server/lib/db.ts +15 -0
- package/server/lib/defaults.ts +366 -0
- package/server/lib/dex/index.ts +80 -0
- package/server/lib/dex/relay.ts +235 -0
- package/server/lib/dex/types.ts +59 -0
- package/server/lib/dex/uniswap.ts +370 -0
- package/server/lib/e2e-agent/artifacts.ts +36 -0
- package/server/lib/e2e-agent/contracts.ts +112 -0
- package/server/lib/e2e-agent/validation.ts +135 -0
- package/server/lib/encrypt.ts +128 -0
- package/server/lib/error.ts +20 -0
- package/server/lib/events.ts +205 -0
- package/server/lib/hot.ts +357 -0
- package/server/lib/key-fingerprint.ts +28 -0
- package/server/lib/logger.ts +331 -0
- package/server/lib/network.ts +137 -0
- package/server/lib/notifications.ts +219 -0
- package/server/lib/oauth2-refresh.ts +241 -0
- package/server/lib/oursecret.ts +54 -0
- package/server/lib/passkey-credential.ts +360 -0
- package/server/lib/passkey.ts +68 -0
- package/server/lib/permissions.ts +248 -0
- package/server/lib/pino.ts +24 -0
- package/server/lib/policy-preview.ts +138 -0
- package/server/lib/price.ts +338 -0
- package/server/lib/prices.ts +34 -0
- package/server/lib/project-scope.ts +239 -0
- package/server/lib/resolve-action.ts +427 -0
- package/server/lib/resolve.ts +36 -0
- package/server/lib/sessions.ts +632 -0
- package/server/lib/solana/connection.ts +26 -0
- package/server/lib/solana/jupiter.ts +128 -0
- package/server/lib/solana/transfer.ts +108 -0
- package/server/lib/solana/wallet.ts +136 -0
- package/server/lib/strategy/emits.ts +21 -0
- package/server/lib/strategy/engine.ts +1305 -0
- package/server/lib/strategy/executor.ts +115 -0
- package/server/lib/strategy/hook-context.ts +158 -0
- package/server/lib/strategy/hooks.ts +990 -0
- package/server/lib/strategy/index.ts +28 -0
- package/server/lib/strategy/installer.ts +305 -0
- package/server/lib/strategy/loader.ts +256 -0
- package/server/lib/strategy/message.ts +235 -0
- package/server/lib/strategy/repository.ts +218 -0
- package/server/lib/strategy/session-logger.ts +693 -0
- package/server/lib/strategy/sources.ts +288 -0
- package/server/lib/strategy/state.ts +189 -0
- package/server/lib/strategy/templates.ts +403 -0
- package/server/lib/strategy/tick.ts +404 -0
- package/server/lib/strategy/types.ts +230 -0
- package/server/lib/swap.ts +3 -0
- package/server/lib/temp.ts +86 -0
- package/server/lib/token-metadata.ts +86 -0
- package/server/lib/token-safety.ts +200 -0
- package/server/lib/token-search.ts +444 -0
- package/server/lib/totp.ts +194 -0
- package/server/lib/transactions.ts +123 -0
- package/server/lib/transport.ts +75 -0
- package/server/lib/txhistory/decoder.ts +262 -0
- package/server/lib/txhistory/enricher.ts +652 -0
- package/server/lib/txhistory/index.ts +391 -0
- package/server/lib/txhistory/signatures.ts +59 -0
- package/server/lib/verified-summary.ts +421 -0
- package/server/mcp/profile-policy.ts +30 -0
- package/server/mcp/server.ts +619 -0
- package/server/mcp/tools.ts +523 -0
- package/server/middleware/auth.ts +119 -0
- package/server/middleware/requestLogger.ts +84 -0
- package/server/routes/actions.ts +459 -0
- package/server/routes/adapters.ts +703 -0
- package/server/routes/addressbook.ts +113 -0
- package/server/routes/ai.ts +34 -0
- package/server/routes/apikeys.ts +295 -0
- package/server/routes/apps.ts +601 -0
- package/server/routes/auth.ts +457 -0
- package/server/routes/backup.ts +340 -0
- package/server/routes/batch.ts +270 -0
- package/server/routes/bookmarks.ts +162 -0
- package/server/routes/credential-shares.ts +198 -0
- package/server/routes/credential-vaults.ts +154 -0
- package/server/routes/credentials.ts +1290 -0
- package/server/routes/dashboard.ts +71 -0
- package/server/routes/defaults.ts +124 -0
- package/server/routes/fund.ts +229 -0
- package/server/routes/import.ts +352 -0
- package/server/routes/launch.ts +665 -0
- package/server/routes/lock.ts +54 -0
- package/server/routes/logs.ts +68 -0
- package/server/routes/nuke.ts +111 -0
- package/server/routes/passkey-credentials.ts +99 -0
- package/server/routes/passkey.ts +346 -0
- package/server/routes/portfolio.ts +217 -0
- package/server/routes/price.ts +63 -0
- package/server/routes/resolve.ts +31 -0
- package/server/routes/security.ts +45 -0
- package/server/routes/send-evm.ts +241 -0
- package/server/routes/send-solana.ts +281 -0
- package/server/routes/send.ts +178 -0
- package/server/routes/setup.ts +210 -0
- package/server/routes/strategy.ts +894 -0
- package/server/routes/swap-evm.ts +353 -0
- package/server/routes/swap-solana.ts +177 -0
- package/server/routes/swap.ts +356 -0
- package/server/routes/token.ts +247 -0
- package/server/routes/unlock.ts +403 -0
- package/server/routes/wallet-assets.ts +361 -0
- package/server/routes/wallet-transactions.ts +515 -0
- package/server/routes/wallet.ts +710 -0
- package/server/types.ts +146 -0
- package/skills/aurawallet/SKILL.md +739 -0
- package/skills/aurawallet-setup/SKILL.md +74 -0
- package/skills/security-review/SKILL.md +148 -0
- package/src/app/api/agent-requests/route.ts +30 -0
- package/src/app/api/apps/install/route.ts +126 -0
- package/src/app/api/apps/manifests/route.ts +16 -0
- package/src/app/api/apps/static/[...path]/route.ts +57 -0
- package/src/app/api/events/route.ts +92 -0
- package/src/app/api/page.tsx +212 -0
- package/src/app/api/workspace/[id]/apps/[wid]/route.ts +119 -0
- package/src/app/api/workspace/[id]/apps/route.ts +81 -0
- package/src/app/api/workspace/[id]/export/route.ts +67 -0
- package/src/app/api/workspace/[id]/route.ts +168 -0
- package/src/app/api/workspace/auth.ts +34 -0
- package/src/app/api/workspace/config/route.ts +106 -0
- package/src/app/api/workspace/import/route.ts +127 -0
- package/src/app/api/workspace/route.ts +116 -0
- package/src/app/app/page.tsx +2122 -0
- package/src/app/apple-icon.png +0 -0
- package/src/app/docs/page.tsx +178 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +572 -0
- package/src/app/health/page.tsx +5 -0
- package/src/app/hello/page.tsx +15 -0
- package/src/app/icon.png +0 -0
- package/src/app/layout.tsx +34 -0
- package/src/app/page.tsx +986 -0
- package/src/app/providers.tsx +90 -0
- package/src/app/share/[token]/page.tsx +295 -0
- package/src/components/ChainSelector.tsx +144 -0
- package/src/components/HumanActionBar.tsx +695 -0
- package/src/components/NotificationDrawer.tsx +129 -0
- package/src/components/apps/AgentKeysApp.tsx +490 -0
- package/src/components/apps/App.tsx +153 -0
- package/src/components/apps/AppGrid.tsx +15 -0
- package/src/components/apps/DetailedAddressDrawer.tsx +325 -0
- package/src/components/apps/DraggableApp.tsx +562 -0
- package/src/components/apps/IFrameApp.tsx +73 -0
- package/src/components/apps/LogsApp.tsx +360 -0
- package/src/components/apps/SendApp.tsx +394 -0
- package/src/components/apps/SetupWizardApp.tsx +1004 -0
- package/src/components/apps/SystemDefaultsApp.tsx +845 -0
- package/src/components/apps/ThirdPartyApp.tsx +428 -0
- package/src/components/apps/TokenApp.tsx +319 -0
- package/src/components/apps/TransactionsApp.tsx +438 -0
- package/src/components/apps/WalletDetailApp.tsx +1505 -0
- package/src/components/apps/index.ts +13 -0
- package/src/components/design-system/Button.tsx +53 -0
- package/src/components/design-system/ChainIndicator.tsx +65 -0
- package/src/components/design-system/ChainSelector.tsx +137 -0
- package/src/components/design-system/ConfirmationModal.tsx +106 -0
- package/src/components/design-system/ConfirmationPopover.tsx +81 -0
- package/src/components/design-system/Drawer.tsx +123 -0
- package/src/components/design-system/FilterDropdown.tsx +72 -0
- package/src/components/design-system/Modal.tsx +206 -0
- package/src/components/design-system/Popover.tsx +142 -0
- package/src/components/design-system/TextInput.tsx +85 -0
- package/src/components/design-system/Toggle.tsx +58 -0
- package/src/components/design-system/index.ts +11 -0
- package/src/components/docs/DocsThemeToggle.tsx +49 -0
- package/src/components/health/CredentialHealthDashboard.tsx +214 -0
- package/src/components/icons/ChainIcons.tsx +72 -0
- package/src/components/layout/AppStoreDrawer.tsx +369 -0
- package/src/components/layout/ContentArea.tsx +21 -0
- package/src/components/layout/TabBar.tsx +278 -0
- package/src/components/layout/WalletSidebar.tsx +1033 -0
- package/src/components/layout/index.ts +4 -0
- package/src/components/marketing/AuraWalletSpecOverlay.tsx +635 -0
- package/src/components/marketing/DeviceMorphExperience.tsx +216 -0
- package/src/components/vault/ApiKeysConsole.tsx +1080 -0
- package/src/components/vault/AuditConsole.tsx +584 -0
- package/src/components/vault/CredentialDetail.tsx +455 -0
- package/src/components/vault/CredentialEmpty.tsx +55 -0
- package/src/components/vault/CredentialField.tsx +361 -0
- package/src/components/vault/CredentialForm.tsx +1212 -0
- package/src/components/vault/CredentialList.tsx +165 -0
- package/src/components/vault/CredentialRow.tsx +97 -0
- package/src/components/vault/CredentialShareModal.tsx +178 -0
- package/src/components/vault/CredentialVault.tsx +754 -0
- package/src/components/vault/CredentialWalletWidget.tsx +103 -0
- package/src/components/vault/ImportCredentialsModal.tsx +515 -0
- package/src/components/vault/LargeTypeModal.tsx +64 -0
- package/src/components/vault/PasswordGenerator.tsx +224 -0
- package/src/components/vault/TOTPDisplay.tsx +123 -0
- package/src/components/vault/VaultSidebar.tsx +413 -0
- package/src/components/vault/types.ts +54 -0
- package/src/context/AuthContext.tsx +337 -0
- package/src/context/PriceContext.tsx +113 -0
- package/src/context/ThemeContext.tsx +164 -0
- package/src/context/WebSocketContext.tsx +269 -0
- package/src/context/WorkspaceContext.tsx +668 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useAgentActions.ts +368 -0
- package/src/hooks/useBalance.ts +103 -0
- package/src/hooks/useBalances.ts +129 -0
- package/src/instrumentation.ts +12 -0
- package/src/lib/api.ts +449 -0
- package/src/lib/app-loader.ts +148 -0
- package/src/lib/app-registry.ts +178 -0
- package/src/lib/app-sdk.ts +157 -0
- package/src/lib/audit-console-adapter.ts +151 -0
- package/src/lib/auth-client.ts +75 -0
- package/src/lib/config.ts +74 -0
- package/src/lib/crypto.ts +112 -0
- package/src/lib/db.ts +21 -0
- package/src/lib/docs.ts +390 -0
- package/src/lib/events.ts +361 -0
- package/src/lib/pino.ts +24 -0
- package/src/lib/theme-handlers.ts +168 -0
- package/src/lib/theme.ts +351 -0
- package/src/lib/tokenData.ts +378 -0
- package/src/lib/vault-crypto.ts +129 -0
- package/src/lib/websocket-server.ts +302 -0
- package/src/lib/websocket-setup.ts +79 -0
- package/src/lib/wordlist.ts +2050 -0
- package/src/lib/workspace-handlers.ts +285 -0
- package/start.sh +80 -0
- package/tailwind.config.ts +99 -0
- package/tsconfig.json +42 -0
|
@@ -0,0 +1,874 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { ethers } from 'ethers';
|
|
4
|
+
import { Keypair, Transaction, VersionedTransaction } from '@solana/web3.js';
|
|
5
|
+
import { WalletInfo, EncryptedData } from '../types';
|
|
6
|
+
import { encryptPrivateKey, decryptPrivateKey } from './encrypt';
|
|
7
|
+
import { DATA_PATHS } from './config';
|
|
8
|
+
import { deriveSolanaColdKeypair } from './solana/wallet';
|
|
9
|
+
import { unlockCredentialVault, lockCredentialVault, lockAllCredentialVaults } from './credential-vault';
|
|
10
|
+
import { ensureOurSecretForVault } from './oursecret';
|
|
11
|
+
|
|
12
|
+
// Cold wallet derivation path: m/44'/60'/0'/0/0
|
|
13
|
+
const COLD_PATH = "m/44'/60'/0'/0/0";
|
|
14
|
+
const COLD_FILE = 'cold.json';
|
|
15
|
+
const VAULT_PREFIX = 'vault-';
|
|
16
|
+
const PRIMARY_VAULT_ID = 'primary';
|
|
17
|
+
const AGENT_VAULT_NAME = 'agent';
|
|
18
|
+
|
|
19
|
+
export type VaultMode = 'primary' | 'linked' | 'independent';
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Vault session stored in memory while a vault is unlocked
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
interface VaultSession {
|
|
25
|
+
id: string;
|
|
26
|
+
mnemonic: string;
|
|
27
|
+
address: string; // EVM
|
|
28
|
+
solanaKeypair: Keypair; // Solana
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// In-memory storage - survives as long as process runs
|
|
32
|
+
const vaultSessions = new Map<string, VaultSession>();
|
|
33
|
+
let primaryVaultId: string | null = null;
|
|
34
|
+
let primaryVaultPassword: string | null = null; // stored while primary is unlocked
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Vault file on disk
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
interface VaultFile {
|
|
40
|
+
address: string;
|
|
41
|
+
solanaAddress?: string;
|
|
42
|
+
encrypted: EncryptedData;
|
|
43
|
+
createdAt: string;
|
|
44
|
+
name?: string;
|
|
45
|
+
mode?: VaultMode;
|
|
46
|
+
linkedTo?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Public vault info (safe to expose)
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
export interface VaultInfo {
|
|
53
|
+
id: string;
|
|
54
|
+
name?: string;
|
|
55
|
+
address: string;
|
|
56
|
+
solanaAddress?: string;
|
|
57
|
+
mode: VaultMode;
|
|
58
|
+
linkedTo?: string;
|
|
59
|
+
isUnlocked: boolean;
|
|
60
|
+
isPrimary: boolean;
|
|
61
|
+
createdAt: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface CreateVaultResult {
|
|
65
|
+
id: string;
|
|
66
|
+
address: string;
|
|
67
|
+
solanaAddress: string;
|
|
68
|
+
mnemonic: string;
|
|
69
|
+
name?: string;
|
|
70
|
+
mode: VaultMode;
|
|
71
|
+
linkedTo?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface CreateVaultOptions {
|
|
75
|
+
mode?: Exclude<VaultMode, 'primary'>;
|
|
76
|
+
linkedTo?: string;
|
|
77
|
+
seedOnboardingSecret?: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// File path helpers
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
function getVaultFilePath(id: string): string {
|
|
84
|
+
return path.join(DATA_PATHS.wallets, `${VAULT_PREFIX}${id}.json`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getLegacyColdFilePath(): string {
|
|
88
|
+
return path.join(DATA_PATHS.wallets, COLD_FILE);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Auto-migrate cold.json → vault-primary.json if needed.
|
|
93
|
+
* Called on first access that needs to list vaults.
|
|
94
|
+
*/
|
|
95
|
+
let migrationDone = false;
|
|
96
|
+
function ensureMigration(): void {
|
|
97
|
+
if (migrationDone) return;
|
|
98
|
+
migrationDone = true;
|
|
99
|
+
|
|
100
|
+
const legacyPath = getLegacyColdFilePath();
|
|
101
|
+
const primaryPath = getVaultFilePath(PRIMARY_VAULT_ID);
|
|
102
|
+
|
|
103
|
+
if (fs.existsSync(legacyPath) && !fs.existsSync(primaryPath)) {
|
|
104
|
+
// Migrate: copy cold.json content into vault-primary.json
|
|
105
|
+
const raw = fs.readFileSync(legacyPath, 'utf-8');
|
|
106
|
+
fs.writeFileSync(primaryPath, raw);
|
|
107
|
+
// Remove old file
|
|
108
|
+
fs.unlinkSync(legacyPath);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// If primary vault file exists, ensure primaryVaultId is set
|
|
112
|
+
if (fs.existsSync(primaryPath) && !primaryVaultId) {
|
|
113
|
+
primaryVaultId = PRIMARY_VAULT_ID;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isVaultMode(value: unknown): value is VaultMode {
|
|
118
|
+
return value === 'primary' || value === 'linked' || value === 'independent';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function readVaultFile(id: string): VaultFile {
|
|
122
|
+
const filePath = getVaultFilePath(id);
|
|
123
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8')) as VaultFile;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function writeVaultFile(id: string, data: VaultFile): void {
|
|
127
|
+
const filePath = getVaultFilePath(id);
|
|
128
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function resolveVaultMode(id: string, data: VaultFile): VaultMode {
|
|
132
|
+
if (id === PRIMARY_VAULT_ID || id === primaryVaultId) return 'primary';
|
|
133
|
+
if (isVaultMode(data.mode) && data.mode !== 'primary') return data.mode;
|
|
134
|
+
return 'independent';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function resolveLinkedTo(id: string, data: VaultFile, mode: VaultMode): string | undefined {
|
|
138
|
+
if (mode !== 'linked') return undefined;
|
|
139
|
+
if (typeof data.linkedTo === 'string' && data.linkedTo.trim()) return data.linkedTo;
|
|
140
|
+
// Default linked target is primary when omitted.
|
|
141
|
+
return primaryVaultId || PRIMARY_VAULT_ID;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Vault CRUD
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* List all vault files on disk. Returns VaultInfo for each.
|
|
150
|
+
*/
|
|
151
|
+
export function listVaults(): VaultInfo[] {
|
|
152
|
+
ensureMigration();
|
|
153
|
+
|
|
154
|
+
const walletsDir = DATA_PATHS.wallets;
|
|
155
|
+
if (!fs.existsSync(walletsDir)) return [];
|
|
156
|
+
|
|
157
|
+
const files = fs.readdirSync(walletsDir);
|
|
158
|
+
const vaults: VaultInfo[] = [];
|
|
159
|
+
|
|
160
|
+
for (const file of files) {
|
|
161
|
+
if (!file.startsWith(VAULT_PREFIX) || !file.endsWith('.json')) continue;
|
|
162
|
+
const id = file.slice(VAULT_PREFIX.length, -5); // strip prefix and .json
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const raw = fs.readFileSync(path.join(walletsDir, file), 'utf-8');
|
|
166
|
+
const data: VaultFile = JSON.parse(raw);
|
|
167
|
+
const mode = resolveVaultMode(id, data);
|
|
168
|
+
const linkedTo = resolveLinkedTo(id, data, mode);
|
|
169
|
+
vaults.push({
|
|
170
|
+
id,
|
|
171
|
+
name: data.name,
|
|
172
|
+
address: data.address,
|
|
173
|
+
solanaAddress: data.solanaAddress,
|
|
174
|
+
mode,
|
|
175
|
+
linkedTo,
|
|
176
|
+
isUnlocked: vaultSessions.has(id),
|
|
177
|
+
isPrimary: id === primaryVaultId || id === PRIMARY_VAULT_ID,
|
|
178
|
+
createdAt: data.createdAt,
|
|
179
|
+
});
|
|
180
|
+
} catch {
|
|
181
|
+
// skip corrupt files
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Sort: primary first, linked next, then independent by createdAt
|
|
186
|
+
vaults.sort((a, b) => {
|
|
187
|
+
if (a.isPrimary) return -1;
|
|
188
|
+
if (b.isPrimary) return 1;
|
|
189
|
+
if (a.mode === 'linked' && b.mode !== 'linked') return -1;
|
|
190
|
+
if (b.mode === 'linked' && a.mode !== 'linked') return 1;
|
|
191
|
+
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return vaults;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Create a new vault with a fresh mnemonic.
|
|
199
|
+
*/
|
|
200
|
+
export function createVault(password: string, name?: string, options: CreateVaultOptions = {}): CreateVaultResult {
|
|
201
|
+
ensureMigration();
|
|
202
|
+
|
|
203
|
+
const id = generateVaultId();
|
|
204
|
+
const filePath = getVaultFilePath(id);
|
|
205
|
+
|
|
206
|
+
if (fs.existsSync(filePath)) {
|
|
207
|
+
throw new Error(`Vault file already exists: ${id}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const mnemonic = ethers.Mnemonic.entropyToPhrase(ethers.randomBytes(16));
|
|
211
|
+
const hdNode = ethers.HDNodeWallet.fromPhrase(mnemonic, undefined, COLD_PATH);
|
|
212
|
+
const solanaKeypair = deriveSolanaColdKeypair(mnemonic);
|
|
213
|
+
const solanaAddress = solanaKeypair.publicKey.toBase58();
|
|
214
|
+
|
|
215
|
+
const mode: Exclude<VaultMode, 'primary'> = options.mode || 'independent';
|
|
216
|
+
const linkedTo = mode === 'linked'
|
|
217
|
+
? (options.linkedTo || primaryVaultId || PRIMARY_VAULT_ID)
|
|
218
|
+
: undefined;
|
|
219
|
+
if (mode === 'linked') {
|
|
220
|
+
if (!linkedTo) throw new Error('linked vault requires a linkedTo target');
|
|
221
|
+
if (!fs.existsSync(getVaultFilePath(linkedTo))) {
|
|
222
|
+
throw new Error(`linkedTo vault not found: ${linkedTo}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const encrypted = encryptPrivateKey(mnemonic, password);
|
|
227
|
+
|
|
228
|
+
const vaultFile: VaultFile = {
|
|
229
|
+
address: hdNode.address,
|
|
230
|
+
solanaAddress,
|
|
231
|
+
encrypted,
|
|
232
|
+
createdAt: new Date().toISOString(),
|
|
233
|
+
name,
|
|
234
|
+
mode,
|
|
235
|
+
linkedTo,
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
fs.writeFileSync(filePath, JSON.stringify(vaultFile, null, 2));
|
|
239
|
+
|
|
240
|
+
// Auto-unlock
|
|
241
|
+
vaultSessions.set(id, {
|
|
242
|
+
id,
|
|
243
|
+
mnemonic,
|
|
244
|
+
address: hdNode.address,
|
|
245
|
+
solanaKeypair,
|
|
246
|
+
});
|
|
247
|
+
unlockCredentialVault(id);
|
|
248
|
+
|
|
249
|
+
if (options.seedOnboardingSecret !== false) {
|
|
250
|
+
try {
|
|
251
|
+
ensureOurSecretForVault(id);
|
|
252
|
+
} catch (err) {
|
|
253
|
+
console.warn('[cold] Failed to seed OURSECRET onboarding note:', err);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
id,
|
|
259
|
+
address: hdNode.address,
|
|
260
|
+
solanaAddress,
|
|
261
|
+
mnemonic,
|
|
262
|
+
name,
|
|
263
|
+
mode,
|
|
264
|
+
linkedTo,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Import a vault from an existing mnemonic.
|
|
270
|
+
*/
|
|
271
|
+
export function importVault(mnemonic: string, password: string, name?: string, options: CreateVaultOptions = {}): VaultInfo {
|
|
272
|
+
ensureMigration();
|
|
273
|
+
|
|
274
|
+
// Normalize
|
|
275
|
+
const normalizedMnemonic = mnemonic.trim().toLowerCase().split(/\s+/).join(' ');
|
|
276
|
+
if (!ethers.Mnemonic.isValidMnemonic(normalizedMnemonic)) {
|
|
277
|
+
throw new Error('Invalid seed phrase');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const id = generateVaultId();
|
|
281
|
+
const filePath = getVaultFilePath(id);
|
|
282
|
+
|
|
283
|
+
const hdNode = ethers.HDNodeWallet.fromPhrase(normalizedMnemonic, undefined, COLD_PATH);
|
|
284
|
+
const solanaKeypair = deriveSolanaColdKeypair(normalizedMnemonic);
|
|
285
|
+
const solanaAddress = solanaKeypair.publicKey.toBase58();
|
|
286
|
+
|
|
287
|
+
const mode: Exclude<VaultMode, 'primary'> = options.mode || 'independent';
|
|
288
|
+
const linkedTo = mode === 'linked'
|
|
289
|
+
? (options.linkedTo || primaryVaultId || PRIMARY_VAULT_ID)
|
|
290
|
+
: undefined;
|
|
291
|
+
if (mode === 'linked') {
|
|
292
|
+
if (!linkedTo) throw new Error('linked vault requires a linkedTo target');
|
|
293
|
+
if (!fs.existsSync(getVaultFilePath(linkedTo))) {
|
|
294
|
+
throw new Error(`linkedTo vault not found: ${linkedTo}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const encrypted = encryptPrivateKey(normalizedMnemonic, password);
|
|
299
|
+
|
|
300
|
+
const vaultFile: VaultFile = {
|
|
301
|
+
address: hdNode.address,
|
|
302
|
+
solanaAddress,
|
|
303
|
+
encrypted,
|
|
304
|
+
createdAt: new Date().toISOString(),
|
|
305
|
+
name,
|
|
306
|
+
mode,
|
|
307
|
+
linkedTo,
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
fs.writeFileSync(filePath, JSON.stringify(vaultFile, null, 2));
|
|
311
|
+
|
|
312
|
+
// Auto-unlock
|
|
313
|
+
vaultSessions.set(id, {
|
|
314
|
+
id,
|
|
315
|
+
mnemonic: normalizedMnemonic,
|
|
316
|
+
address: hdNode.address,
|
|
317
|
+
solanaKeypair,
|
|
318
|
+
});
|
|
319
|
+
unlockCredentialVault(id);
|
|
320
|
+
|
|
321
|
+
if (options.seedOnboardingSecret !== false) {
|
|
322
|
+
try {
|
|
323
|
+
ensureOurSecretForVault(id);
|
|
324
|
+
} catch (err) {
|
|
325
|
+
console.warn('[cold] Failed to seed OURSECRET onboarding note:', err);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
id,
|
|
331
|
+
name,
|
|
332
|
+
address: hdNode.address,
|
|
333
|
+
solanaAddress,
|
|
334
|
+
mode,
|
|
335
|
+
linkedTo,
|
|
336
|
+
isUnlocked: true,
|
|
337
|
+
isPrimary: false,
|
|
338
|
+
createdAt: vaultFile.createdAt,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Unlock a specific vault.
|
|
344
|
+
*/
|
|
345
|
+
export function unlockVault(id: string, password: string): boolean {
|
|
346
|
+
ensureMigration();
|
|
347
|
+
|
|
348
|
+
const filePath = getVaultFilePath(id);
|
|
349
|
+
if (!fs.existsSync(filePath)) {
|
|
350
|
+
throw new Error(`Vault not found: ${id}`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const data: VaultFile = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
const mnemonic = decryptPrivateKey(data.encrypted, password);
|
|
357
|
+
const solanaKeypair = deriveSolanaColdKeypair(mnemonic);
|
|
358
|
+
|
|
359
|
+
vaultSessions.set(id, {
|
|
360
|
+
id,
|
|
361
|
+
mnemonic,
|
|
362
|
+
address: data.address,
|
|
363
|
+
solanaKeypair,
|
|
364
|
+
});
|
|
365
|
+
unlockCredentialVault(id);
|
|
366
|
+
|
|
367
|
+
// Cache primary password while unlocked so linked vaults can auto-unlock.
|
|
368
|
+
if (id === primaryVaultId || id === PRIMARY_VAULT_ID) {
|
|
369
|
+
primaryVaultPassword = password;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return true;
|
|
373
|
+
} catch {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Lock a specific vault.
|
|
380
|
+
*/
|
|
381
|
+
export function lockVault(id: string): void {
|
|
382
|
+
vaultSessions.delete(id);
|
|
383
|
+
lockCredentialVault(id);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Lock ALL vaults.
|
|
388
|
+
*/
|
|
389
|
+
export function lockAllVaults(): void {
|
|
390
|
+
vaultSessions.clear();
|
|
391
|
+
primaryVaultPassword = null;
|
|
392
|
+
lockAllCredentialVaults();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Get the mnemonic for a specific vault (must be unlocked).
|
|
397
|
+
*/
|
|
398
|
+
export function getVaultMnemonic(id: string): string | null {
|
|
399
|
+
return vaultSessions.get(id)?.mnemonic ?? null;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Check if a specific vault is unlocked.
|
|
404
|
+
*/
|
|
405
|
+
export function isVaultUnlocked(id: string): boolean {
|
|
406
|
+
return vaultSessions.has(id);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Get a vault's EVM address (from file, doesn't need unlock).
|
|
411
|
+
*/
|
|
412
|
+
export function getVaultAddress(id: string): string | null {
|
|
413
|
+
const session = vaultSessions.get(id);
|
|
414
|
+
if (session) return session.address;
|
|
415
|
+
|
|
416
|
+
const filePath = getVaultFilePath(id);
|
|
417
|
+
if (!fs.existsSync(filePath)) return null;
|
|
418
|
+
|
|
419
|
+
const data: VaultFile = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
420
|
+
return data.address;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Get a vault's Solana address (from file, doesn't need unlock).
|
|
425
|
+
*/
|
|
426
|
+
export function getVaultSolanaAddress(id: string): string | null {
|
|
427
|
+
const session = vaultSessions.get(id);
|
|
428
|
+
if (session) return session.solanaKeypair.publicKey.toBase58();
|
|
429
|
+
|
|
430
|
+
const filePath = getVaultFilePath(id);
|
|
431
|
+
if (!fs.existsSync(filePath)) return null;
|
|
432
|
+
|
|
433
|
+
const data: VaultFile = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
434
|
+
return data.solanaAddress || null;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Get a vault's Solana keypair (requires unlock).
|
|
439
|
+
*/
|
|
440
|
+
export function getVaultSolanaKeypair(id: string): Keypair | null {
|
|
441
|
+
return vaultSessions.get(id)?.solanaKeypair ?? null;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Delete a vault file and lock it.
|
|
446
|
+
*/
|
|
447
|
+
export function deleteVault(id: string): void {
|
|
448
|
+
lockVault(id);
|
|
449
|
+
|
|
450
|
+
const filePath = getVaultFilePath(id);
|
|
451
|
+
if (fs.existsSync(filePath)) {
|
|
452
|
+
fs.unlinkSync(filePath);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (primaryVaultId === id) {
|
|
456
|
+
primaryVaultId = null;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Export seed for a specific vault (must be unlocked).
|
|
462
|
+
*/
|
|
463
|
+
export function exportVaultSeed(id: string): string | null {
|
|
464
|
+
return getVaultMnemonic(id);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Sign an EVM transaction with a specific vault.
|
|
469
|
+
*/
|
|
470
|
+
export async function signWithVault(
|
|
471
|
+
vaultId: string,
|
|
472
|
+
transaction: ethers.TransactionRequest,
|
|
473
|
+
provider: ethers.Provider
|
|
474
|
+
): Promise<string> {
|
|
475
|
+
const session = vaultSessions.get(vaultId);
|
|
476
|
+
if (!session) {
|
|
477
|
+
throw new Error(`Vault ${vaultId} is locked. Unlock it first.`);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const hdNode = ethers.HDNodeWallet.fromPhrase(session.mnemonic, undefined, COLD_PATH);
|
|
481
|
+
const wallet = hdNode.connect(provider);
|
|
482
|
+
const tx = await wallet.sendTransaction(transaction);
|
|
483
|
+
return tx.hash;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Sign a Solana transaction with a specific vault's keypair.
|
|
488
|
+
*/
|
|
489
|
+
export function signSolanaVaultTransaction(vaultId: string, tx: Transaction): void {
|
|
490
|
+
const session = vaultSessions.get(vaultId);
|
|
491
|
+
if (!session) {
|
|
492
|
+
throw new Error(`Vault ${vaultId} is locked. Unlock it first.`);
|
|
493
|
+
}
|
|
494
|
+
tx.partialSign(session.solanaKeypair);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ---------------------------------------------------------------------------
|
|
498
|
+
// Generate short vault ID
|
|
499
|
+
// ---------------------------------------------------------------------------
|
|
500
|
+
function generateVaultId(): string {
|
|
501
|
+
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
502
|
+
let id = '';
|
|
503
|
+
const bytes = ethers.randomBytes(6);
|
|
504
|
+
for (let i = 0; i < 6; i++) {
|
|
505
|
+
id += chars[bytes[i] % chars.length];
|
|
506
|
+
}
|
|
507
|
+
return id;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Get the cached primary vault password (available while primary is unlocked).
|
|
512
|
+
*/
|
|
513
|
+
export function getPrimaryVaultPassword(): string | null {
|
|
514
|
+
return primaryVaultPassword;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Ensure the default "agent" vault exists as a LINKED vault.
|
|
519
|
+
* This is only called during primary creation/import, not on every unlock/startup.
|
|
520
|
+
*/
|
|
521
|
+
export function ensureDefaultLinkedAgentVault(): { created: boolean; vaultId: string | null } {
|
|
522
|
+
ensureMigration();
|
|
523
|
+
if (!primaryVaultId || !primaryVaultPassword) {
|
|
524
|
+
return { created: false, vaultId: null };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const existing = listVaults().find(v => (v.name || '').trim().toLowerCase() === AGENT_VAULT_NAME);
|
|
528
|
+
if (existing) {
|
|
529
|
+
return { created: false, vaultId: existing.id };
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const created = createVault(primaryVaultPassword, AGENT_VAULT_NAME, {
|
|
533
|
+
mode: 'linked',
|
|
534
|
+
linkedTo: primaryVaultId,
|
|
535
|
+
seedOnboardingSecret: false,
|
|
536
|
+
});
|
|
537
|
+
return { created: true, vaultId: created.id };
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Auto-unlock linked vaults when primary is unlocked.
|
|
542
|
+
* Independent vaults remain locked until explicitly unlocked with their own password.
|
|
543
|
+
*/
|
|
544
|
+
export function autoUnlockLinkedVaults(): number {
|
|
545
|
+
if (!primaryVaultPassword) return 0;
|
|
546
|
+
|
|
547
|
+
const vaults = listVaults();
|
|
548
|
+
let unlocked = 0;
|
|
549
|
+
for (const v of vaults) {
|
|
550
|
+
if (v.isPrimary || v.isUnlocked || v.mode !== 'linked') continue;
|
|
551
|
+
try {
|
|
552
|
+
if (unlockVault(v.id, primaryVaultPassword)) unlocked++;
|
|
553
|
+
} catch (err) {
|
|
554
|
+
console.warn(
|
|
555
|
+
`[cold] Linked vault unlock failed for "${v.id}"${v.name ? ` (${v.name})` : ''}: ${String(err)}`
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return unlocked;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Resolve the linked vault group for UI filtering.
|
|
564
|
+
* - Selecting primary or a linked vault returns root + all linked siblings.
|
|
565
|
+
* - Selecting independent vault returns only itself.
|
|
566
|
+
*/
|
|
567
|
+
export function getLinkedVaultGroup(vaultId: string): string[] {
|
|
568
|
+
const vaults = listVaults();
|
|
569
|
+
const selected = vaults.find(v => v.id === vaultId);
|
|
570
|
+
if (!selected) return [vaultId];
|
|
571
|
+
if (selected.mode === 'independent') return [selected.id];
|
|
572
|
+
|
|
573
|
+
const rootId = selected.mode === 'linked'
|
|
574
|
+
? (selected.linkedTo || selected.id)
|
|
575
|
+
: selected.id;
|
|
576
|
+
|
|
577
|
+
const group = vaults
|
|
578
|
+
.filter(v => v.id === rootId || (v.mode === 'linked' && v.linkedTo === rootId))
|
|
579
|
+
.map(v => v.id);
|
|
580
|
+
|
|
581
|
+
return group.length > 0 ? group : [vaultId];
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// ---------------------------------------------------------------------------
|
|
585
|
+
// Reset for testing (clears all module state)
|
|
586
|
+
// ---------------------------------------------------------------------------
|
|
587
|
+
export function _resetForTesting(): void {
|
|
588
|
+
vaultSessions.clear();
|
|
589
|
+
primaryVaultId = null;
|
|
590
|
+
primaryVaultPassword = null;
|
|
591
|
+
migrationDone = false;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// ===========================================================================
|
|
595
|
+
// BACKWARD COMPATIBILITY — all original exports still work via primary vault
|
|
596
|
+
// ===========================================================================
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Get the primary vault ID (or null if none exists).
|
|
600
|
+
*/
|
|
601
|
+
export function getPrimaryVaultId(): string | null {
|
|
602
|
+
ensureMigration();
|
|
603
|
+
return primaryVaultId;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
export function hasColdWallet(): boolean {
|
|
607
|
+
ensureMigration();
|
|
608
|
+
if (primaryVaultId && fs.existsSync(getVaultFilePath(primaryVaultId))) {
|
|
609
|
+
return true;
|
|
610
|
+
}
|
|
611
|
+
// Also check legacy path as fallback
|
|
612
|
+
return fs.existsSync(getLegacyColdFilePath());
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
export function isUnlocked(): boolean {
|
|
616
|
+
// Returns true if ANY vault is unlocked (backward compat)
|
|
617
|
+
return vaultSessions.size > 0;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
export interface CreateWalletResult extends WalletInfo {
|
|
621
|
+
mnemonic: string;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
export function createColdWallet(password: string): CreateWalletResult {
|
|
625
|
+
ensureMigration();
|
|
626
|
+
|
|
627
|
+
if (hasColdWallet()) {
|
|
628
|
+
throw new Error('Cold wallet already exists. Delete it first if you want to recreate.');
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Create as the primary vault
|
|
632
|
+
const result = createVault(password, undefined, { seedOnboardingSecret: false });
|
|
633
|
+
|
|
634
|
+
// Set as primary
|
|
635
|
+
primaryVaultId = result.id;
|
|
636
|
+
|
|
637
|
+
// Rename to vault-primary.json
|
|
638
|
+
const currentPath = getVaultFilePath(result.id);
|
|
639
|
+
const primaryPath = getVaultFilePath(PRIMARY_VAULT_ID);
|
|
640
|
+
fs.renameSync(currentPath, primaryPath);
|
|
641
|
+
|
|
642
|
+
// Update session key
|
|
643
|
+
const session = vaultSessions.get(result.id)!;
|
|
644
|
+
vaultSessions.delete(result.id);
|
|
645
|
+
vaultSessions.set(PRIMARY_VAULT_ID, { ...session, id: PRIMARY_VAULT_ID });
|
|
646
|
+
primaryVaultId = PRIMARY_VAULT_ID;
|
|
647
|
+
primaryVaultPassword = password;
|
|
648
|
+
unlockCredentialVault(PRIMARY_VAULT_ID);
|
|
649
|
+
|
|
650
|
+
// Persist primary mode metadata after rename.
|
|
651
|
+
try {
|
|
652
|
+
const primaryFile = readVaultFile(PRIMARY_VAULT_ID);
|
|
653
|
+
primaryFile.mode = 'primary';
|
|
654
|
+
delete primaryFile.linkedTo;
|
|
655
|
+
writeVaultFile(PRIMARY_VAULT_ID, primaryFile);
|
|
656
|
+
} catch (err) {
|
|
657
|
+
console.warn('[cold] Failed to persist primary vault mode metadata:', err);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Seed primary onboarding secret after final vault ID is stable.
|
|
661
|
+
try {
|
|
662
|
+
ensureOurSecretForVault(PRIMARY_VAULT_ID);
|
|
663
|
+
} catch (err) {
|
|
664
|
+
console.warn('[cold] Failed to seed primary OURSECRET onboarding note:', err);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Create default linked agent vault once at setup time.
|
|
668
|
+
try {
|
|
669
|
+
ensureDefaultLinkedAgentVault();
|
|
670
|
+
} catch (err) {
|
|
671
|
+
console.warn('[cold] Failed to create default linked agent vault:', err);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
return {
|
|
675
|
+
address: result.address,
|
|
676
|
+
tier: 'cold',
|
|
677
|
+
chain: 'all',
|
|
678
|
+
createdAt: new Date().toISOString(),
|
|
679
|
+
mnemonic: result.mnemonic,
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
export function exportSeed(): string | null {
|
|
684
|
+
if (!primaryVaultId) return null;
|
|
685
|
+
return getVaultMnemonic(primaryVaultId);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
export function unlock(password: string): boolean {
|
|
689
|
+
ensureMigration();
|
|
690
|
+
|
|
691
|
+
if (!primaryVaultId) {
|
|
692
|
+
throw new Error('No cold wallet found');
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return unlockVault(primaryVaultId, password);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Rotate the primary vault password by re-encrypting the stored mnemonic wrapper.
|
|
700
|
+
* Returns false when the current password is invalid.
|
|
701
|
+
*/
|
|
702
|
+
export function rotatePrimaryVaultPassword(currentPassword: string, newPassword: string): boolean {
|
|
703
|
+
ensureMigration();
|
|
704
|
+
|
|
705
|
+
const vaultId = primaryVaultId || PRIMARY_VAULT_ID;
|
|
706
|
+
const filePath = getVaultFilePath(vaultId);
|
|
707
|
+
if (!fs.existsSync(filePath)) {
|
|
708
|
+
throw new Error('No cold wallet found');
|
|
709
|
+
}
|
|
710
|
+
if (newPassword.length < 8) {
|
|
711
|
+
throw new Error('Password must be at least 8 characters');
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const vaultFile = readVaultFile(vaultId);
|
|
715
|
+
let mnemonic: string;
|
|
716
|
+
try {
|
|
717
|
+
mnemonic = decryptPrivateKey(vaultFile.encrypted, currentPassword);
|
|
718
|
+
} catch {
|
|
719
|
+
return false;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
vaultFile.encrypted = encryptPrivateKey(mnemonic, newPassword);
|
|
723
|
+
writeVaultFile(vaultId, vaultFile);
|
|
724
|
+
|
|
725
|
+
if (primaryVaultPassword === currentPassword) {
|
|
726
|
+
primaryVaultPassword = newPassword;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return true;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
export function lock(): void {
|
|
733
|
+
lockAllVaults();
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
export function getColdWalletAddress(): string | null {
|
|
737
|
+
if (!primaryVaultId) {
|
|
738
|
+
ensureMigration();
|
|
739
|
+
if (!primaryVaultId) return null;
|
|
740
|
+
}
|
|
741
|
+
return getVaultAddress(primaryVaultId);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
export function getMnemonic(): string | null {
|
|
745
|
+
if (!primaryVaultId) return null;
|
|
746
|
+
return getVaultMnemonic(primaryVaultId);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
export async function signWithColdWallet(
|
|
750
|
+
transaction: ethers.TransactionRequest,
|
|
751
|
+
provider: ethers.Provider
|
|
752
|
+
): Promise<string> {
|
|
753
|
+
if (!primaryVaultId) {
|
|
754
|
+
throw new Error('No primary vault configured');
|
|
755
|
+
}
|
|
756
|
+
return signWithVault(primaryVaultId, transaction, provider);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
export function getColdWalletInfo(): WalletInfo | null {
|
|
760
|
+
ensureMigration();
|
|
761
|
+
if (!primaryVaultId) return null;
|
|
762
|
+
|
|
763
|
+
const filePath = getVaultFilePath(primaryVaultId);
|
|
764
|
+
if (!fs.existsSync(filePath)) return null;
|
|
765
|
+
|
|
766
|
+
const data: VaultFile = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
767
|
+
|
|
768
|
+
return {
|
|
769
|
+
address: data.address,
|
|
770
|
+
tier: 'cold',
|
|
771
|
+
chain: 'all',
|
|
772
|
+
createdAt: data.createdAt,
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
export function getSolanaColdAddress(): string | null {
|
|
777
|
+
if (!primaryVaultId) {
|
|
778
|
+
ensureMigration();
|
|
779
|
+
if (!primaryVaultId) return null;
|
|
780
|
+
}
|
|
781
|
+
return getVaultSolanaAddress(primaryVaultId);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
export function getSolanaColdKeypair(): Keypair | null {
|
|
785
|
+
if (!primaryVaultId) return null;
|
|
786
|
+
return getVaultSolanaKeypair(primaryVaultId);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
export function signSolanaColdTransaction(tx: Transaction): void {
|
|
790
|
+
if (!primaryVaultId) {
|
|
791
|
+
throw new Error('No primary vault configured');
|
|
792
|
+
}
|
|
793
|
+
signSolanaVaultTransaction(primaryVaultId, tx);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
export function deleteColdWallet(): void {
|
|
797
|
+
if (primaryVaultId) {
|
|
798
|
+
deleteVault(primaryVaultId);
|
|
799
|
+
}
|
|
800
|
+
// Also remove legacy file if it exists
|
|
801
|
+
const legacyPath = getLegacyColdFilePath();
|
|
802
|
+
if (fs.existsSync(legacyPath)) {
|
|
803
|
+
fs.unlinkSync(legacyPath);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
export function importColdWallet(mnemonic: string, password: string): WalletInfo {
|
|
808
|
+
ensureMigration();
|
|
809
|
+
|
|
810
|
+
if (hasColdWallet()) {
|
|
811
|
+
throw new Error('Cold wallet already exists. Delete it first if you want to import.');
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const info = importVault(mnemonic, password, undefined, { seedOnboardingSecret: false });
|
|
815
|
+
|
|
816
|
+
// Make it the primary vault
|
|
817
|
+
const currentPath = getVaultFilePath(info.id);
|
|
818
|
+
const primaryPath = getVaultFilePath(PRIMARY_VAULT_ID);
|
|
819
|
+
fs.renameSync(currentPath, primaryPath);
|
|
820
|
+
|
|
821
|
+
// Update session key
|
|
822
|
+
const session = vaultSessions.get(info.id)!;
|
|
823
|
+
vaultSessions.delete(info.id);
|
|
824
|
+
vaultSessions.set(PRIMARY_VAULT_ID, { ...session, id: PRIMARY_VAULT_ID });
|
|
825
|
+
primaryVaultId = PRIMARY_VAULT_ID;
|
|
826
|
+
primaryVaultPassword = password;
|
|
827
|
+
unlockCredentialVault(PRIMARY_VAULT_ID);
|
|
828
|
+
|
|
829
|
+
// Persist primary mode metadata after rename.
|
|
830
|
+
try {
|
|
831
|
+
const primaryFile = readVaultFile(PRIMARY_VAULT_ID);
|
|
832
|
+
primaryFile.mode = 'primary';
|
|
833
|
+
delete primaryFile.linkedTo;
|
|
834
|
+
writeVaultFile(PRIMARY_VAULT_ID, primaryFile);
|
|
835
|
+
} catch (err) {
|
|
836
|
+
console.warn('[cold] Failed to persist primary vault mode metadata:', err);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Seed primary onboarding secret after final vault ID is stable.
|
|
840
|
+
try {
|
|
841
|
+
ensureOurSecretForVault(PRIMARY_VAULT_ID);
|
|
842
|
+
} catch (err) {
|
|
843
|
+
console.warn('[cold] Failed to seed primary OURSECRET onboarding note:', err);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// Create default linked agent vault once at import time.
|
|
847
|
+
try {
|
|
848
|
+
ensureDefaultLinkedAgentVault();
|
|
849
|
+
} catch (err) {
|
|
850
|
+
console.warn('[cold] Failed to create default linked agent vault:', err);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
return {
|
|
854
|
+
address: info.address,
|
|
855
|
+
tier: 'cold',
|
|
856
|
+
chain: 'all',
|
|
857
|
+
createdAt: info.createdAt,
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Derive a hot wallet at a specific index
|
|
862
|
+
// Hot wallets use path: m/44'/60'/1'/0/N
|
|
863
|
+
export function deriveHotWallet(index: number): ethers.HDNodeWallet {
|
|
864
|
+
if (!primaryVaultId) {
|
|
865
|
+
throw new Error('No primary vault configured');
|
|
866
|
+
}
|
|
867
|
+
const mnemonic = getVaultMnemonic(primaryVaultId);
|
|
868
|
+
if (!mnemonic) {
|
|
869
|
+
throw new Error('Primary vault is locked. Unlock it first.');
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const hotPath = `m/44'/60'/1'/0/${index}`;
|
|
873
|
+
return ethers.HDNodeWallet.fromPhrase(mnemonic, undefined, hotPath);
|
|
874
|
+
}
|