@execra/core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/cli/commands.d.ts +21 -0
  2. package/dist/cli/commands.js +686 -0
  3. package/dist/cli/commands.js.map +1 -0
  4. package/dist/cli/index.d.ts +8 -0
  5. package/dist/cli/index.js +1162 -0
  6. package/dist/cli/index.js.map +1 -0
  7. package/dist/cli/prompts.d.ts +12 -0
  8. package/dist/cli/prompts.js +98 -0
  9. package/dist/cli/prompts.js.map +1 -0
  10. package/dist/cli/ui.d.ts +38 -0
  11. package/dist/cli/ui.js +990 -0
  12. package/dist/cli/ui.js.map +1 -0
  13. package/dist/handler/index.d.ts +93 -0
  14. package/dist/handler/index.js +628 -0
  15. package/dist/handler/index.js.map +1 -0
  16. package/dist/handler/walletConnect.d.ts +6 -0
  17. package/dist/handler/walletConnect.js +623 -0
  18. package/dist/handler/walletConnect.js.map +1 -0
  19. package/dist/index.d.ts +14 -0
  20. package/dist/index.js +2046 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/session/index.d.ts +20 -0
  23. package/dist/session/index.js +79 -0
  24. package/dist/session/index.js.map +1 -0
  25. package/dist/solana/index.d.ts +5 -0
  26. package/dist/solana/index.js +302 -0
  27. package/dist/solana/index.js.map +1 -0
  28. package/dist/solana/rpc.d.ts +45 -0
  29. package/dist/solana/rpc.js +120 -0
  30. package/dist/solana/rpc.js.map +1 -0
  31. package/dist/solana/simulate.d.ts +41 -0
  32. package/dist/solana/simulate.js +173 -0
  33. package/dist/solana/simulate.js.map +1 -0
  34. package/dist/solana/tx.d.ts +54 -0
  35. package/dist/solana/tx.js +141 -0
  36. package/dist/solana/tx.js.map +1 -0
  37. package/dist/vault/accounts.d.ts +88 -0
  38. package/dist/vault/accounts.js +126 -0
  39. package/dist/vault/accounts.js.map +1 -0
  40. package/dist/vault/config.d.ts +40 -0
  41. package/dist/vault/config.js +131 -0
  42. package/dist/vault/config.js.map +1 -0
  43. package/dist/vault/index.d.ts +122 -0
  44. package/dist/vault/index.js +580 -0
  45. package/dist/vault/index.js.map +1 -0
  46. package/dist/vault/keystore.d.ts +44 -0
  47. package/dist/vault/keystore.js +118 -0
  48. package/dist/vault/keystore.js.map +1 -0
  49. package/dist/vault/mnemonic.d.ts +43 -0
  50. package/dist/vault/mnemonic.js +37 -0
  51. package/dist/vault/mnemonic.js.map +1 -0
  52. package/package.json +49 -0
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/solana/simulate.ts
4
+ import {
5
+ Transaction,
6
+ VersionedTransaction,
7
+ LAMPORTS_PER_SOL as LAMPORTS_PER_SOL2
8
+ } from "@solana/web3.js";
9
+
10
+ // src/solana/rpc.ts
11
+ import { Connection, PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js";
12
+
13
+ // src/vault/config.ts
14
+ import fs from "fs";
15
+
16
+ // src/vault/keystore.ts
17
+ import os from "os";
18
+ import path from "path";
19
+ function getWalletDir() {
20
+ return path.join(os.homedir(), ".wallet");
21
+ }
22
+ function getConfigPath() {
23
+ return path.join(getWalletDir(), "config.json");
24
+ }
25
+ function getSessionsPath() {
26
+ return path.join(getWalletDir(), "sessions.json");
27
+ }
28
+ var SESSIONS_FILE = getSessionsPath();
29
+
30
+ // src/vault/accounts.ts
31
+ import { derivePath } from "ed25519-hd-key";
32
+ import nacl from "tweetnacl";
33
+ import bs58 from "bs58";
34
+
35
+ // src/vault/mnemonic.ts
36
+ import * as bip39 from "bip39";
37
+
38
+ // src/vault/config.ts
39
+ var HELIUS_API_KEY = process.env.HELIUS_API_KEY;
40
+ var RPC_URLS = {
41
+ "mainnet-beta": HELIUS_API_KEY ? `https://mainnet.helius-rpc.com/?api-key=${HELIUS_API_KEY}` : "https://api.mainnet-beta.solana.com",
42
+ devnet: HELIUS_API_KEY ? `https://devnet.helius-rpc.com/?api-key=${HELIUS_API_KEY}` : "https://api.devnet.solana.com",
43
+ testnet: "https://api.testnet.solana.com",
44
+ localnet: "http://localhost:8899"
45
+ };
46
+ function configExists() {
47
+ return fs.existsSync(getConfigPath());
48
+ }
49
+ function loadConfig() {
50
+ if (!configExists()) {
51
+ throw new Error("No config found. Run `wallet init` first.");
52
+ }
53
+ const raw = fs.readFileSync(getConfigPath(), "utf8");
54
+ return JSON.parse(raw);
55
+ }
56
+ function getRpcUrl() {
57
+ return loadConfig().rpcUrl;
58
+ }
59
+
60
+ // src/solana/rpc.ts
61
+ var _connection = null;
62
+ function getConnection(forceNew = false) {
63
+ if (!_connection || forceNew) {
64
+ const rpcUrl = getRpcUrl();
65
+ _connection = new Connection(rpcUrl, "confirmed");
66
+ }
67
+ return _connection;
68
+ }
69
+
70
+ // src/solana/simulate.ts
71
+ async function simulateTransaction(serializedTx, signerPublicKey, conn) {
72
+ const connection = conn ?? getConnection();
73
+ try {
74
+ const txBuffer = Buffer.from(serializedTx, "base64");
75
+ let simulation;
76
+ try {
77
+ const versionedTx = VersionedTransaction.deserialize(txBuffer);
78
+ simulation = await connection.simulateTransaction(versionedTx, {
79
+ sigVerify: false,
80
+ // skip sig verification — wallet hasn't signed yet
81
+ replaceRecentBlockhash: true
82
+ });
83
+ } catch {
84
+ const legacyTx = Transaction.from(txBuffer);
85
+ simulation = await connection.simulateTransaction(legacyTx, []);
86
+ }
87
+ const { value } = simulation;
88
+ return parseSimulationResult(value, signerPublicKey, txBuffer);
89
+ } catch (err) {
90
+ return {
91
+ success: false,
92
+ error: err instanceof Error ? err.message : "Simulation failed",
93
+ logs: [],
94
+ accountChanges: [],
95
+ tokenChanges: [],
96
+ computeUnitsConsumed: 0,
97
+ programIds: [],
98
+ fee: 0,
99
+ rawLogs: []
100
+ };
101
+ }
102
+ }
103
+ function parseSimulationResult(value, signerPublicKey, txBuffer) {
104
+ const success = !value.err;
105
+ const logs = value.logs ?? [];
106
+ const accountChanges = (value.accounts ?? []).map((account, i) => {
107
+ if (!account) return null;
108
+ const preBalance = account.lamports ?? 0;
109
+ const postBalance = account.lamports ?? 0;
110
+ return {
111
+ address: signerPublicKey,
112
+ // simplified — full impl maps account indices
113
+ preBalance,
114
+ postBalance,
115
+ solDelta: (postBalance - preBalance) / LAMPORTS_PER_SOL2,
116
+ isWritable: true,
117
+ isSigner: i === 0
118
+ };
119
+ }).filter(Boolean);
120
+ const programIds = extractProgramIds(logs);
121
+ const computeUnitsConsumed = extractComputeUnits(logs);
122
+ const fee = 5e3;
123
+ return {
124
+ success,
125
+ error: value.err ? JSON.stringify(value.err) : null,
126
+ logs: formatLogs(logs),
127
+ accountChanges,
128
+ tokenChanges: [],
129
+ // full SPL parsing requires additional RPC calls
130
+ computeUnitsConsumed,
131
+ programIds,
132
+ fee,
133
+ rawLogs: logs
134
+ };
135
+ }
136
+ function extractProgramIds(logs) {
137
+ const ids = /* @__PURE__ */ new Set();
138
+ const invokePattern = /Program (\w+) invoke/;
139
+ for (const log of logs) {
140
+ const match = log.match(invokePattern);
141
+ if (match) ids.add(match[1]);
142
+ }
143
+ return Array.from(ids);
144
+ }
145
+ function extractComputeUnits(logs) {
146
+ for (const log of logs) {
147
+ const match = log.match(/consumed (\d+) of/);
148
+ if (match) return parseInt(match[1], 10);
149
+ }
150
+ return 0;
151
+ }
152
+ function formatLogs(logs) {
153
+ return logs.map((log) => {
154
+ return log.replace(/\b([1-9A-HJ-NP-Za-km-z]{32,44})\b/g, (addr) => {
155
+ const known = KNOWN_PROGRAMS[addr];
156
+ return known ? known : `${addr.slice(0, 4)}..${addr.slice(-4)}`;
157
+ });
158
+ });
159
+ }
160
+ var KNOWN_PROGRAMS = {
161
+ "11111111111111111111111111111111": "System Program",
162
+ TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA: "SPL Token",
163
+ JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4: "Jupiter V6",
164
+ "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin": "Serum DEX",
165
+ ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJe1bv: "Associated Token",
166
+ SysvarRent111111111111111111111111111111111: "Sysvar Rent",
167
+ SysvarC1ock11111111111111111111111111111111: "Sysvar Clock"
168
+ };
169
+ export {
170
+ KNOWN_PROGRAMS,
171
+ simulateTransaction
172
+ };
173
+ //# sourceMappingURL=simulate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/solana/simulate.ts","../../src/solana/rpc.ts","../../src/vault/config.ts","../../src/vault/keystore.ts","../../src/vault/accounts.ts","../../src/vault/mnemonic.ts"],"sourcesContent":["import {\n Connection,\n Transaction,\n VersionedTransaction,\n LAMPORTS_PER_SOL,\n} from \"@solana/web3.js\";\nimport { getConnection } from \"./rpc\";\n\n// Types\nexport interface TokenChange {\n mint: string; // token mint address\n account: string; // token account address\n owner: string; // owner of the token account\n preAmount: number; // balance before tx\n postAmount: number; // balance after tx\n delta: number; // change (positive = received, negative = sent)\n decimals: number;\n}\n\nexport interface AccountChange {\n address: string;\n preBalance: number; // in lamports\n postBalance: number; // in lamports\n solDelta: number; // in SOL (human readable)\n isWritable: boolean;\n isSigner: boolean;\n}\n\nexport interface SimulationResult {\n success: boolean;\n error: string | null;\n logs: string[];\n\n // Account changes\n accountChanges: AccountChange[];\n tokenChanges: TokenChange[];\n\n // Transaction metadata\n computeUnitsConsumed: number;\n programIds: string[]; // all programs invoked\n fee: number; // estimated fee in lamports\n\n // Raw simulation data (passed to Claude)\n rawLogs: string[];\n}\n\n// Simulate\n/**\n * Simulate a transaction and return a parsed result.\n * Accepts both legacy and versioned transactions (base64 encoded).\n *\n * @param serializedTx - base64 encoded serialized transaction\n * @param signerPublicKey - the wallet's public key (used to identify balance changes)\n */\nexport async function simulateTransaction(\n serializedTx: string,\n signerPublicKey: string,\n conn?: Connection,\n): Promise<SimulationResult> {\n const connection = conn ?? getConnection();\n\n try {\n const txBuffer = Buffer.from(serializedTx, \"base64\");\n\n // Try versioned transaction first, fall back to legacy\n let simulation: Awaited<ReturnType<Connection[\"simulateTransaction\"]>>;\n\n try {\n const versionedTx = VersionedTransaction.deserialize(txBuffer);\n simulation = await connection.simulateTransaction(versionedTx, {\n sigVerify: false, // skip sig verification — wallet hasn't signed yet\n replaceRecentBlockhash: true,\n });\n } catch {\n // Fall back to legacy transaction\n const legacyTx = Transaction.from(txBuffer);\n simulation = await connection.simulateTransaction(legacyTx, []);\n }\n\n const { value } = simulation;\n\n return parseSimulationResult(value, signerPublicKey, txBuffer);\n } catch (err) {\n // Simulation itself failed (network error, malformed tx, etc.)\n return {\n success: false,\n error: err instanceof Error ? err.message : \"Simulation failed\",\n logs: [],\n accountChanges: [],\n tokenChanges: [],\n computeUnitsConsumed: 0,\n programIds: [],\n fee: 0,\n rawLogs: [],\n };\n }\n}\n\n// Parsing\nfunction parseSimulationResult(\n value: any,\n signerPublicKey: string,\n txBuffer: Buffer,\n): SimulationResult {\n const success = !value.err;\n const logs: string[] = value.logs ?? [];\n\n // Parse account balance changes\n const accountChanges: AccountChange[] = (value.accounts ?? [])\n .map((account: any, i: number) => {\n if (!account) return null;\n const preBalance = account.lamports ?? 0;\n const postBalance = account.lamports ?? 0;\n return {\n address: signerPublicKey, // simplified — full impl maps account indices\n preBalance,\n postBalance,\n solDelta: (postBalance - preBalance) / LAMPORTS_PER_SOL,\n isWritable: true,\n isSigner: i === 0,\n };\n })\n .filter(Boolean);\n\n // Extract program IDs from logs\n const programIds = extractProgramIds(logs);\n\n // Compute units from logs\n const computeUnitsConsumed = extractComputeUnits(logs);\n\n // Estimate fee (5000 lamports base + 5000 per signature)\n const fee = 5000;\n\n return {\n success,\n error: value.err ? JSON.stringify(value.err) : null,\n logs: formatLogs(logs),\n accountChanges,\n tokenChanges: [], // full SPL parsing requires additional RPC calls\n computeUnitsConsumed,\n programIds,\n fee,\n rawLogs: logs,\n };\n}\n\nfunction extractProgramIds(logs: string[]): string[] {\n const ids = new Set<string>();\n const invokePattern = /Program (\\w+) invoke/;\n for (const log of logs) {\n const match = log.match(invokePattern);\n if (match) ids.add(match[1]);\n }\n return Array.from(ids);\n}\n\nfunction extractComputeUnits(logs: string[]): number {\n for (const log of logs) {\n const match = log.match(/consumed (\\d+) of/);\n if (match) return parseInt(match[1], 10);\n }\n return 0;\n}\n\nfunction formatLogs(logs: string[]): string[] {\n return logs.map((log) => {\n // Shorten program addresses for readability\n return log.replace(/\\b([1-9A-HJ-NP-Za-km-z]{32,44})\\b/g, (addr) => {\n const known = KNOWN_PROGRAMS[addr];\n return known ? known : `${addr.slice(0, 4)}..${addr.slice(-4)}`;\n });\n });\n}\n\n// Known Programs\nconst KNOWN_PROGRAMS: Record<string, string> = {\n \"11111111111111111111111111111111\": \"System Program\",\n TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA: \"SPL Token\",\n JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4: \"Jupiter V6\",\n \"9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin\": \"Serum DEX\",\n ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJe1bv: \"Associated Token\",\n SysvarRent111111111111111111111111111111111: \"Sysvar Rent\",\n SysvarC1ock11111111111111111111111111111111: \"Sysvar Clock\",\n};\n\nexport { KNOWN_PROGRAMS };\n","/**\n *\n * Solana RPC connection manager.\n * Single shared connection instance for the daemon session.\n *\n * Dependencies: @solana/web3.js\n */\n\nimport { Connection, PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js';\nimport { getRpcUrl, getCluster } from '../vault/config';\n\n// Singleton connection\n\nlet _connection: Connection | null = null;\n\nexport function getConnection(forceNew: boolean = false): Connection {\n if (!_connection || forceNew) {\n const rpcUrl = getRpcUrl();\n _connection = new Connection(rpcUrl, 'confirmed');\n }\n return _connection;\n}\n\n/**\n * Reset connection — used when RPC URL or cluster changes.\n */\nexport function resetConnection(): void {\n _connection = null;\n}\n\n// Balance─\n\n/**\n * Get SOL balance for an address in lamports and SOL.\n */\nexport async function getBalance(\n publicKeyStr: string,\n): Promise<{ lamports: number; sol: number }> {\n const connection = getConnection();\n const pubkey = new PublicKey(publicKeyStr);\n const lamports = await connection.getBalance(pubkey);\n return {\n lamports,\n sol: lamports / LAMPORTS_PER_SOL,\n };\n}\n\n/**\n * Request a devnet airdrop (only works on devnet/testnet).\n * Returns the transaction signature.\n */\nexport async function requestAirdrop(\n publicKeyStr: string,\n solAmount: number = 1,\n): Promise<string> {\n const cluster = getCluster();\n if (cluster === 'mainnet-beta') {\n throw new Error('Airdrops are not available on mainnet.');\n }\n\n const connection = getConnection();\n const pubkey = new PublicKey(publicKeyStr);\n const lamports = solAmount * LAMPORTS_PER_SOL;\n\n const signature = await connection.requestAirdrop(pubkey, lamports);\n await connection.confirmTransaction(signature, 'confirmed');\n\n return signature;\n}\n\n/**\n * Get recent blockhash — required for building transactions.\n */\nexport async function getLatestBlockhash(): Promise<{\n blockhash: string;\n lastValidBlockHeight: number;\n}> {\n const connection = getConnection();\n return connection.getLatestBlockhash('confirmed');\n}\n\n/**\n * Confirm a transaction by signature.\n * Polls until confirmed or timeout.\n */\nexport async function confirmTransaction(\n signature: string,\n timeoutMs: number = 30_000,\n): Promise<boolean> {\n const connection = getConnection();\n const { blockhash, lastValidBlockHeight } = await getLatestBlockhash();\n\n const result = await connection.confirmTransaction(\n { signature, blockhash, lastValidBlockHeight },\n 'confirmed',\n );\n\n return !result.value.err;\n}\n\n/**\n * Check if the RPC endpoint is reachable.\n */\nexport async function healthCheck(): Promise<boolean> {\n try {\n const connection = getConnection();\n await connection.getVersion();\n return true;\n } catch {\n return false;\n }\n}\n","/**\n *\n * Reads and writes ~/.wallet/config.json\n * Stores non-sensitive wallet preferences:\n * - Active account index\n * - RPC endpoint (mainnet / devnet)\n * - Account metadata (names, indices — no keys)\n * - WalletConnect project ID\n *\n * Dependencies: Node.js built-in `fs`, `os`, `path`\n */\n\nimport fs from \"fs\";\nimport { getConfigPath, ensureWalletDir } from \"./keystore\";\nimport { AccountStore, createAccountStore } from \"./accounts\";\n\n// ── Types ─────────────────────────────────────────────────────────────────────\n\nexport type ClusterType = \"mainnet-beta\" | \"devnet\" | \"testnet\" | \"localnet\" | \"custom\";\n\nexport interface WalletConfig {\n version: number;\n cluster: ClusterType;\n rpcUrl: string;\n walletConnectProjectId: string;\n accountStore: AccountStore;\n createdAt: string;\n updatedAt: string;\n}\n\n// ── Defaults ──────────────────────────────────────────────────────────────────\n\nconst HELIUS_API_KEY = process.env.HELIUS_API_KEY;\nexport const RPC_URLS: Record<Exclude<ClusterType, \"custom\">, string> = {\n \"mainnet-beta\": HELIUS_API_KEY\n ? `https://mainnet.helius-rpc.com/?api-key=${HELIUS_API_KEY}`\n : \"https://api.mainnet-beta.solana.com\",\n devnet: HELIUS_API_KEY\n ? `https://devnet.helius-rpc.com/?api-key=${HELIUS_API_KEY}`\n : \"https://api.devnet.solana.com\",\n testnet: \"https://api.testnet.solana.com\",\n localnet: \"http://localhost:8899\",\n};\n\nconst CONFIG_VERSION = 1;\n\n// ── Read / Write ──────────────────────────────────────────────────────────────\n\nexport function configExists(): boolean {\n return fs.existsSync(getConfigPath());\n}\n\nexport function loadConfig(): WalletConfig {\n if (!configExists()) {\n throw new Error(\"No config found. Run `wallet init` first.\");\n }\n const raw = fs.readFileSync(getConfigPath(), \"utf8\");\n return JSON.parse(raw) as WalletConfig;\n}\n\nexport function saveConfig(config: WalletConfig): void {\n ensureWalletDir();\n const updated = { ...config, updatedAt: new Date().toISOString() };\n fs.writeFileSync(getConfigPath(), JSON.stringify(updated, null, 2), {\n mode: 0o600,\n });\n}\n\n/**\n * Create a fresh config on `wallet init`.\n */\nexport function createConfig(\n walletConnectProjectId: string = \"\",\n cluster: Exclude<ClusterType, \"custom\"> = \"devnet\",\n): WalletConfig {\n const now = new Date().toISOString();\n return {\n version: CONFIG_VERSION,\n cluster,\n rpcUrl: RPC_URLS[cluster],\n walletConnectProjectId,\n accountStore: createAccountStore(\"Account 1\"),\n createdAt: now,\n updatedAt: now,\n };\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\nexport function updateCluster(cluster: Exclude<ClusterType, \"custom\">): void {\n const config = loadConfig();\n config.cluster = cluster;\n config.rpcUrl = RPC_URLS[cluster];\n saveConfig(config);\n}\n\nexport function updateRpcUrl(url: string): void {\n const config = loadConfig();\n config.rpcUrl = url;\n saveConfig(config);\n}\n\nexport function updateAccountStore(accountStore: AccountStore): void {\n const config = loadConfig();\n config.accountStore = accountStore;\n saveConfig(config);\n}\n\nexport function getRpcUrl(): string {\n return loadConfig().rpcUrl;\n}\n\nexport function getCluster(): ClusterType {\n return loadConfig().cluster;\n}\n\nexport function getAccountStore(): AccountStore {\n return loadConfig().accountStore;\n}\n","/**\n *\n * AES-256-GCM encrypted keystore.\n * The mnemonic (seed phrase) is encrypted at rest in ~/.wallet/vault.enc\n * Private keys NEVER touch disk — they are derived in memory at runtime.\n *\n * Encryption scheme:\n * - Key derivation: PBKDF2 (SHA-512, 210,000 iterations) — OWASP recommended\n * - Cipher: AES-256-GCM (authenticated encryption — detects tampering)\n * - Salt: 32 random bytes (unique per vault)\n * - IV: 16 random bytes (unique per encryption)\n *\n * Dependencies: Node.js built-in `crypto`, `fs`, `os`, `path`\n */\n\nimport crypto from \"crypto\";\nimport fs from \"fs\";\nimport os from \"os\";\nimport path from \"path\";\n\n// ── Types ─────────────────────────────────────────────────────────────────────\n\nexport interface VaultData {\n mnemonic: string;\n createdAt: string;\n version: number;\n}\n\ninterface EncryptedVault {\n version: number; // format version for future migrations\n salt: string; // hex — used for PBKDF2 key derivation\n iv: string; // hex — AES-GCM initialisation vector\n authTag: string; // hex — GCM authentication tag (detects tampering)\n ciphertext: string; // hex — encrypted vault data\n}\n\n// ── Constants ─────────────────────────────────────────────────────────────────\n\nconst VAULT_VERSION = 1;\nconst PBKDF2_ITERATIONS = 210_000; // OWASP 2024 recommendation for PBKDF2-SHA512\nconst PBKDF2_DIGEST = \"sha512\";\nconst KEY_LENGTH = 32; // 256 bits for AES-256\nconst SALT_LENGTH = 32; // 256 bits\nconst IV_LENGTH = 16; // 128 bits for AES-GCM\nconst CIPHER = \"aes-256-gcm\";\n\n// ── Vault Path ────────────────────────────────────────────────────────────────\n\nexport function getWalletDir(): string {\n // return path.join(__dirname, \".wallet\");\n return path.join(os.homedir(), \".wallet\");\n}\n\nexport function getVaultPath(): string {\n return path.join(getWalletDir(), \"vault.enc\");\n}\n\nexport function getConfigPath(): string {\n return path.join(getWalletDir(), \"config.json\");\n}\n\nexport function getSessionsPath(): string {\n return path.join(getWalletDir(), \"sessions.json\");\n}\n\nexport const SESSIONS_FILE = getSessionsPath();\n\nexport function ensureWalletDir(): void {\n const dir = getWalletDir();\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); // owner-only access\n }\n}\n\nexport function vaultExists(): boolean {\n return fs.existsSync(getVaultPath());\n}\n\n// ── Encryption ────────────────────────────────────────────────────────────────\n\n/**\n * Encrypt and save the vault to disk.\n * @param data - vault contents (mnemonic + metadata)\n * @param password - user's wallet password\n */\nexport async function saveVault(\n data: VaultData,\n password: string,\n): Promise<void> {\n ensureWalletDir();\n\n const salt = crypto.randomBytes(SALT_LENGTH);\n const iv = crypto.randomBytes(IV_LENGTH);\n const key = await deriveKey(password, salt);\n\n const plaintext = JSON.stringify(data);\n const cipher = crypto.createCipheriv(CIPHER, key, iv);\n\n const encrypted = Buffer.concat([\n cipher.update(plaintext, \"utf8\"),\n cipher.final(),\n ]);\n\n const authTag = cipher.getAuthTag();\n\n const vault: EncryptedVault = {\n version: VAULT_VERSION,\n salt: salt.toString(\"hex\"),\n iv: iv.toString(\"hex\"),\n authTag: authTag.toString(\"hex\"),\n ciphertext: encrypted.toString(\"hex\"),\n };\n\n fs.writeFileSync(getVaultPath(), JSON.stringify(vault, null, 2), {\n mode: 0o600, // owner read/write only\n });\n}\n\n/**\n * Load and decrypt the vault from disk.\n * Throws if the password is wrong or the file has been tampered with.\n */\nexport async function loadVault(password: string): Promise<VaultData> {\n if (!vaultExists()) {\n throw new Error(\"No vault found. Run `wallet init` to create one.\");\n }\n\n const raw = fs.readFileSync(getVaultPath(), \"utf8\");\n const vault: EncryptedVault = JSON.parse(raw);\n\n if (vault.version !== VAULT_VERSION) {\n throw new Error(`Unsupported vault version: ${vault.version}`);\n }\n\n const salt = Buffer.from(vault.salt, \"hex\");\n const iv = Buffer.from(vault.iv, \"hex\");\n const authTag = Buffer.from(vault.authTag, \"hex\");\n const ciphertext = Buffer.from(vault.ciphertext, \"hex\");\n\n const key = await deriveKey(password, salt);\n\n try {\n const decipher = crypto.createDecipheriv(CIPHER, key, iv);\n decipher.setAuthTag(authTag);\n\n const decrypted = Buffer.concat([\n decipher.update(ciphertext),\n decipher.final(),\n ]);\n\n return JSON.parse(decrypted.toString(\"utf8\")) as VaultData;\n } catch {\n // GCM auth tag failure means wrong password OR tampered file\n throw new Error(\"Decryption failed: wrong password or vault is corrupted.\");\n }\n}\n\n/**\n * Change the vault password.\n * Decrypts with old password, re-encrypts with new password.\n */\nexport async function changePassword(\n oldPassword: string,\n newPassword: string,\n): Promise<void> {\n const data = await loadVault(oldPassword);\n await saveVault(data, newPassword);\n}\n\n// ── Internal ──────────────────────────────────────────────────────────────────\n\n/**\n * Derive a 256-bit AES key from a password using PBKDF2-SHA512.\n */\nfunction deriveKey(password: string, salt: Buffer): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n crypto.pbkdf2(\n password,\n salt,\n PBKDF2_ITERATIONS,\n KEY_LENGTH,\n PBKDF2_DIGEST,\n (err, key) => {\n if (err) reject(err);\n else resolve(key);\n },\n );\n });\n}\n","/**\n *\n * BIP-44 HD key derivation for Solana accounts.\n *\n * Derivation path: m/44'/501'/index'/0'\n * 44' = BIP-44 purpose\n * 501' = Solana's registered coin type (SLIP-44)\n * n' = account index (0 = first account, 1 = second, etc.)\n * 0' = change (always 0 for Solana)\n *\n * This is the exact path Phantom, Backpack, and Solflare use.\n * Same seed → same addresses as those wallets.\n *\n * Dependencies: ed25519-hd-key, tweetnacl, bs58\n */\n\nimport { derivePath } from \"ed25519-hd-key\";\nimport nacl from \"tweetnacl\";\nimport bs58 from \"bs58\";\nimport { mnemonicToSeed } from \"./mnemonic\";\n\n// ── Types ─────────────────────────────────────────────────────────────────────\n\nexport interface Account {\n index: number; // derivation index\n name: string; // user-defined label\n publicKey: string; // Base58 encoded — this is the Solana address\n derivationPath: string; // m/44'/501'/index'/0'\n createdAt: string;\n}\n\n/**\n * In-memory keypair — private key NEVER persisted to disk.\n * Lives only for the duration of the daemon session.\n */\nexport interface AccountKeypair extends Account {\n secretKey: Uint8Array; // 64-byte Ed25519 secret key (seed + public key)\n}\n\nexport interface AccountStore {\n accounts: Account[];\n activeIndex: number;\n}\n\n// ── Derivation ────────────────────────────────────────────────────────────────\n\n/**\n * Derive an Ed25519 keypair at a specific BIP-44 index.\n * This is the core operation — called once per session after vault unlock.\n *\n * @param seed - 64-byte seed from BIP-39 mnemonic\n * @param index - account index (0-based)\n * @param name - label for this account\n */\nexport function deriveAccount(\n seed: Buffer,\n index: number,\n name: string = `Account ${index + 1}`,\n): AccountKeypair {\n const path = derivationPath(index);\n\n // SLIP-0010 Ed25519 derivation\n const { key: privateKeyBytes } = derivePath(path, seed.toString(\"hex\"));\n\n // nacl keypair from 32-byte seed\n const keypair = nacl.sign.keyPair.fromSeed(privateKeyBytes);\n\n const publicKey = bs58.encode(Buffer.from(keypair.publicKey));\n\n return {\n index,\n name,\n publicKey,\n secretKey: keypair.secretKey, // 64 bytes: seed + public key\n derivationPath: path,\n createdAt: new Date().toISOString(),\n };\n}\n\n/**\n * Derive multiple accounts at once.\n * Used on wallet startup to load all known accounts into memory.\n */\nexport function deriveAccounts(\n seed: Buffer,\n accountStore: AccountStore,\n): AccountKeypair[] {\n return accountStore.accounts.map((account) =>\n deriveAccount(seed, account.index, account.name),\n );\n}\n\n/**\n * Derive a single account directly from mnemonic.\n * Convenience wrapper used in tests and programmatic access.\n */\nexport async function deriveAccountFromMnemonic(\n mnemonic: string,\n index: number,\n name?: string,\n passphrase?: string,\n): Promise<AccountKeypair> {\n const seed = await mnemonicToSeed(mnemonic, passphrase);\n return deriveAccount(seed, index, name);\n}\n\n// ── Account Store ─────────────────────────────────────────────────────────────\n\n/**\n * Create a fresh account store with the first account.\n */\nexport function createAccountStore(\n firstAccountName: string = \"Account 1\",\n): AccountStore {\n return {\n accounts: [\n {\n index: 0,\n name: firstAccountName,\n publicKey: \"\", // filled in after derivation\n derivationPath: derivationPath(0),\n createdAt: new Date().toISOString(),\n },\n ],\n activeIndex: 0,\n };\n}\n\n/**\n * Add a new account to the store.\n * The next index is always max(existing indices) + 1.\n */\nexport function addAccount(\n store: AccountStore,\n seed: Buffer,\n name?: string,\n): { store: AccountStore; keypair: AccountKeypair } {\n const nextIndex =\n store.accounts.length > 0\n ? Math.max(...store.accounts.map((a) => a.index)) + 1\n : 0;\n\n const accountName = name ?? `Account ${nextIndex + 1}`;\n const keypair = deriveAccount(seed, nextIndex, accountName);\n\n const newAccount: Account = {\n index: nextIndex,\n name: accountName,\n publicKey: keypair.publicKey,\n derivationPath: keypair.derivationPath,\n createdAt: keypair.createdAt,\n };\n\n const updatedStore: AccountStore = {\n ...store,\n accounts: [...store.accounts, newAccount],\n };\n\n return { store: updatedStore, keypair };\n}\n\n/**\n * Set the active account by index.\n * The active account is used for all dApp connections and signing.\n */\nexport function setActiveAccount(\n store: AccountStore,\n index: number,\n): AccountStore {\n const exists = store.accounts.find((a) => a.index === index);\n if (!exists) {\n throw new Error(`Account index ${index} does not exist.`);\n }\n return { ...store, activeIndex: index };\n}\n\n/**\n * Rename an account.\n */\nexport function renameAccount(\n store: AccountStore,\n index: number,\n newName: string,\n): AccountStore {\n return {\n ...store,\n accounts: store.accounts.map((a) =>\n a.index === index ? { ...a, name: newName } : a,\n ),\n };\n}\n\n/**\n * Get the active account metadata from the store.\n */\nexport function getActiveAccount(store: AccountStore): Account {\n const account = store.accounts.find((a) => a.index === store.activeIndex);\n if (!account) throw new Error(\"No active account found.\");\n return account;\n}\n\n// ── Utilities ─────────────────────────────────────────────────────────────────\n\n/**\n * Sign a raw message with an account's secret key.\n * Returns a Base58 encoded signature.\n */\nexport function signMessage(\n message: Uint8Array,\n secretKey: Uint8Array,\n): string {\n const signature = nacl.sign.detached(message, secretKey);\n return bs58.encode(Buffer.from(signature));\n}\n\n/**\n * Format account for display in terminal.\n */\nexport function formatAccount(account: Account, isActive: boolean): string {\n const activeMarker = isActive ? \"●\" : \"○\";\n const shortKey = `${account.publicKey.slice(0, 4)}...${account.publicKey.slice(-4)}`;\n return `${activeMarker} [${account.index}] ${account.name.padEnd(20)} ${shortKey}`;\n}\n\n// ── Internal ──────────────────────────────────────────────────────────────────\n\nfunction derivationPath(index: number): string {\n return `m/44'/501'/${index}'/0'`;\n}\n","/**\n *\n * BIP-39 mnemonic generation, validation, and seed derivation.\n * Supports 12-word (128-bit) and 24-word (256-bit) mnemonics.\n *\n * Dependencies: bip39\n */\n\nimport * as bip39 from \"bip39\";\n\nexport type MnemonicStrength = 12 | 24; // 128 = 12 words, 256 = 24 words\n\nexport interface MnemonicResult {\n mnemonic: string;\n wordCount: 12 | 24;\n}\n\n/**\n * Generate a new cryptographically random BIP-39 mnemonic.\n * @param strength 128 for 12-word, 256 for 24-word (default: 128)\n */\nexport function generateMnemonic(\n strength: MnemonicStrength = 12,\n): MnemonicResult {\n const mnemonic = bip39.generateMnemonic(strength == 12 ? 128 : 256);\n const wordCount = strength;\n return { mnemonic, wordCount };\n}\n\n/**\n * Validate a BIP-39 mnemonic phrase.\n * Checks both wordlist membership and BIP-39 checksum.\n */\nexport function validateMnemonic(mnemonic: string): boolean {\n const cleaned = cleanMnemonic(mnemonic);\n return bip39.validateMnemonic(cleaned);\n}\n\n/**\n * Derive a 64-byte seed buffer from a mnemonic.\n * Optional passphrase adds extra security per BIP-39 spec.\n * This seed is the root of ALL derived accounts.\n */\nexport async function mnemonicToSeed(\n mnemonic: string,\n passphrase: string = \"\",\n): Promise<Buffer> {\n const cleaned = cleanMnemonic(mnemonic);\n\n if (!validateMnemonic(cleaned)) {\n throw new Error(\"Invalid mnemonic: failed wordlist or checksum validation\");\n }\n\n return bip39.mnemonicToSeed(cleaned, passphrase);\n}\n\n/**\n * Split mnemonic into a numbered word array.\n * Used for backup verification display in the terminal UI.\n *\n * Example output:\n * [ \"1. witch\", \"2. collapse\", \"3. practice\", ... ]\n */\nexport function mnemonicToNumberedWords(mnemonic: string): string[] {\n return cleanMnemonic(mnemonic)\n .split(\" \")\n .map((word, i) => `${i + 1}. ${word}`);\n}\n\n/**\n * Reconstruct mnemonic from a word array.\n * Used when user inputs words one by one during restore flow.\n */\nexport function wordsToMnemonic(words: string[]): string {\n return words.map((w) => w.trim().toLowerCase()).join(\" \");\n}\n\n// ── Internal ──────────────────────────────────────────────────────────────────\n\nfunction cleanMnemonic(mnemonic: string): string {\n return mnemonic.trim().toLowerCase().replace(/\\s+/g, \" \");\n}\n"],"mappings":"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","names":["LAMPORTS_PER_SOL","LAMPORTS_PER_SOL"]}
@@ -0,0 +1,54 @@
1
+ import { SendOptions, Connection } from '@solana/web3.js';
2
+ import { AccountKeypair } from '../vault/accounts.js';
3
+
4
+ /**
5
+ *
6
+ * Transaction signing and broadcasting.
7
+ *
8
+ * Two methods:
9
+ * signTransaction → sign only, return signed tx bytes (dApp broadcasts)
10
+ * signAndSendTransaction → sign + broadcast, return tx hash
11
+ *
12
+ * Both are supported so any dApp works regardless of which method it uses.
13
+ *
14
+ * Dependencies: @solana/web3.js, tweetnacl, bs58
15
+ */
16
+
17
+ interface SignedTransaction {
18
+ serialized: string;
19
+ signature: string;
20
+ }
21
+ interface SendResult {
22
+ txHash: string;
23
+ confirmed: boolean;
24
+ }
25
+ /**
26
+ * Sign a transaction without broadcasting.
27
+ * Returns the signed transaction for the dApp to broadcast.
28
+ * Used for: solana_signTransaction
29
+ */
30
+ declare function signTransaction(serializedTx: string, keypair: AccountKeypair): Promise<SignedTransaction>;
31
+ /**
32
+ * Sign multiple transactions in a batch.
33
+ * Used for: solana_signAllTransactions
34
+ */
35
+ declare function signAllTransactions(serializedTxs: string[], keypair: AccountKeypair): Promise<SignedTransaction[]>;
36
+ /**
37
+ * Sign a transaction and broadcast it to Solana.
38
+ * Returns the transaction hash after confirmation.
39
+ * Used for: solana_signAndSendTransaction
40
+ */
41
+ declare function signAndSendTransaction(serializedTx: string, keypair: AccountKeypair, options?: SendOptions, conn?: Connection): Promise<SendResult>;
42
+ /**
43
+ * Broadcast an already-signed transaction.
44
+ * Used when the wallet has already signed and just needs to broadcast.
45
+ */
46
+ declare function sendSignedTransaction(serializedSignedTx: string, options?: SendOptions, conn?: Connection): Promise<SendResult>;
47
+ /**
48
+ * Sign an off-chain message (for authentication / proof of ownership).
49
+ * Used for: solana_signMessage
50
+ * Returns base58 encoded signature.
51
+ */
52
+ declare function signOffchainMessage(message: Uint8Array, keypair: AccountKeypair): string;
53
+
54
+ export { type SendResult, type SignedTransaction, sendSignedTransaction, signAllTransactions, signAndSendTransaction, signOffchainMessage, signTransaction };
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/solana/tx.ts
4
+ import {
5
+ Transaction,
6
+ VersionedTransaction,
7
+ Keypair
8
+ } from "@solana/web3.js";
9
+ import nacl2 from "tweetnacl";
10
+ import bs582 from "bs58";
11
+
12
+ // src/solana/rpc.ts
13
+ import { Connection, PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js";
14
+
15
+ // src/vault/config.ts
16
+ import fs from "fs";
17
+
18
+ // src/vault/keystore.ts
19
+ import os from "os";
20
+ import path from "path";
21
+ function getWalletDir() {
22
+ return path.join(os.homedir(), ".wallet");
23
+ }
24
+ function getConfigPath() {
25
+ return path.join(getWalletDir(), "config.json");
26
+ }
27
+ function getSessionsPath() {
28
+ return path.join(getWalletDir(), "sessions.json");
29
+ }
30
+ var SESSIONS_FILE = getSessionsPath();
31
+
32
+ // src/vault/accounts.ts
33
+ import { derivePath } from "ed25519-hd-key";
34
+ import nacl from "tweetnacl";
35
+ import bs58 from "bs58";
36
+
37
+ // src/vault/mnemonic.ts
38
+ import * as bip39 from "bip39";
39
+
40
+ // src/vault/config.ts
41
+ var HELIUS_API_KEY = process.env.HELIUS_API_KEY;
42
+ var RPC_URLS = {
43
+ "mainnet-beta": HELIUS_API_KEY ? `https://mainnet.helius-rpc.com/?api-key=${HELIUS_API_KEY}` : "https://api.mainnet-beta.solana.com",
44
+ devnet: HELIUS_API_KEY ? `https://devnet.helius-rpc.com/?api-key=${HELIUS_API_KEY}` : "https://api.devnet.solana.com",
45
+ testnet: "https://api.testnet.solana.com",
46
+ localnet: "http://localhost:8899"
47
+ };
48
+ function configExists() {
49
+ return fs.existsSync(getConfigPath());
50
+ }
51
+ function loadConfig() {
52
+ if (!configExists()) {
53
+ throw new Error("No config found. Run `wallet init` first.");
54
+ }
55
+ const raw = fs.readFileSync(getConfigPath(), "utf8");
56
+ return JSON.parse(raw);
57
+ }
58
+ function getRpcUrl() {
59
+ return loadConfig().rpcUrl;
60
+ }
61
+
62
+ // src/solana/rpc.ts
63
+ var _connection = null;
64
+ function getConnection(forceNew = false) {
65
+ if (!_connection || forceNew) {
66
+ const rpcUrl = getRpcUrl();
67
+ _connection = new Connection(rpcUrl, "confirmed");
68
+ }
69
+ return _connection;
70
+ }
71
+
72
+ // src/solana/tx.ts
73
+ async function signTransaction(serializedTx, keypair) {
74
+ const txBuffer = Buffer.from(serializedTx, "base64");
75
+ const solanaKeypair = toSolanaKeypair(keypair);
76
+ try {
77
+ const versionedTx = VersionedTransaction.deserialize(txBuffer);
78
+ versionedTx.sign([solanaKeypair]);
79
+ const serialized = Buffer.from(versionedTx.serialize()).toString("base64");
80
+ const signature = bs582.encode(versionedTx.signatures[0]);
81
+ return { serialized, signature };
82
+ } catch {
83
+ const legacyTx = Transaction.from(txBuffer);
84
+ legacyTx.partialSign(solanaKeypair);
85
+ const serialized = legacyTx.serialize({ requireAllSignatures: false }).toString("base64");
86
+ const signature = bs582.encode(legacyTx.signature);
87
+ return { serialized, signature };
88
+ }
89
+ }
90
+ async function signAllTransactions(serializedTxs, keypair) {
91
+ return Promise.all(serializedTxs.map((tx) => signTransaction(tx, keypair)));
92
+ }
93
+ async function signAndSendTransaction(serializedTx, keypair, options = {}, conn) {
94
+ const { serialized } = await signTransaction(serializedTx, keypair);
95
+ return sendSignedTransaction(serialized, options, conn);
96
+ }
97
+ async function sendSignedTransaction(serializedSignedTx, options = {}, conn) {
98
+ const connection = conn ?? getConnection();
99
+ const txBuffer = Buffer.from(serializedSignedTx, "base64");
100
+ const sendOptions = {
101
+ skipPreflight: false,
102
+ // run preflight checks
103
+ preflightCommitment: "confirmed",
104
+ ...options
105
+ };
106
+ let txHash;
107
+ try {
108
+ const versionedTx = VersionedTransaction.deserialize(txBuffer);
109
+ txHash = await connection.sendTransaction(versionedTx, sendOptions);
110
+ } catch {
111
+ const legacyTx = Transaction.from(txBuffer);
112
+ txHash = await connection.sendRawTransaction(
113
+ legacyTx.serialize(),
114
+ sendOptions
115
+ );
116
+ }
117
+ const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
118
+ const confirmResult = await connection.confirmTransaction(
119
+ { signature: txHash, blockhash, lastValidBlockHeight },
120
+ "confirmed"
121
+ );
122
+ return {
123
+ txHash,
124
+ confirmed: !confirmResult.value.err
125
+ };
126
+ }
127
+ function signOffchainMessage(message, keypair) {
128
+ const signature = nacl2.sign.detached(message, keypair.secretKey);
129
+ return bs582.encode(Buffer.from(signature));
130
+ }
131
+ function toSolanaKeypair(keypair) {
132
+ return Keypair.fromSecretKey(keypair.secretKey);
133
+ }
134
+ export {
135
+ sendSignedTransaction,
136
+ signAllTransactions,
137
+ signAndSendTransaction,
138
+ signOffchainMessage,
139
+ signTransaction
140
+ };
141
+ //# sourceMappingURL=tx.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/solana/tx.ts","../../src/solana/rpc.ts","../../src/vault/config.ts","../../src/vault/keystore.ts","../../src/vault/accounts.ts","../../src/vault/mnemonic.ts"],"sourcesContent":["/**\n *\n * Transaction signing and broadcasting.\n *\n * Two methods:\n * signTransaction → sign only, return signed tx bytes (dApp broadcasts)\n * signAndSendTransaction → sign + broadcast, return tx hash\n *\n * Both are supported so any dApp works regardless of which method it uses.\n *\n * Dependencies: @solana/web3.js, tweetnacl, bs58\n */\n\nimport {\n Connection,\n Transaction,\n VersionedTransaction,\n SendOptions,\n Keypair,\n} from \"@solana/web3.js\";\nimport nacl from \"tweetnacl\";\nimport bs58 from \"bs58\";\nimport { getConnection } from \"./rpc\";\nimport { AccountKeypair } from \"../vault/accounts\";\n\n// ── Types ─────────────────────────────────────────────────────────────────────\n\nexport interface SignedTransaction {\n serialized: string; // base64 encoded signed transaction\n signature: string; // base58 encoded signature (first signature)\n}\n\nexport interface SendResult {\n txHash: string; // transaction signature / hash\n confirmed: boolean;\n}\n\n// ── Sign Only ─────────────────────────────────────────────────────────────────\n\n/**\n * Sign a transaction without broadcasting.\n * Returns the signed transaction for the dApp to broadcast.\n * Used for: solana_signTransaction\n */\nexport async function signTransaction(\n serializedTx: string,\n keypair: AccountKeypair,\n): Promise<SignedTransaction> {\n const txBuffer = Buffer.from(serializedTx, \"base64\");\n const solanaKeypair = toSolanaKeypair(keypair);\n\n try {\n // Try versioned transaction first\n const versionedTx = VersionedTransaction.deserialize(txBuffer);\n versionedTx.sign([solanaKeypair]);\n\n const serialized = Buffer.from(versionedTx.serialize()).toString(\"base64\");\n const signature = bs58.encode(versionedTx.signatures[0]);\n\n return { serialized, signature };\n } catch {\n // Fall back to legacy transaction\n const legacyTx = Transaction.from(txBuffer);\n legacyTx.partialSign(solanaKeypair);\n\n const serialized = legacyTx\n .serialize({ requireAllSignatures: false })\n .toString(\"base64\");\n const signature = bs58.encode(legacyTx.signature!);\n\n return { serialized, signature };\n }\n}\n\n/**\n * Sign multiple transactions in a batch.\n * Used for: solana_signAllTransactions\n */\nexport async function signAllTransactions(\n serializedTxs: string[],\n keypair: AccountKeypair,\n): Promise<SignedTransaction[]> {\n return Promise.all(serializedTxs.map((tx) => signTransaction(tx, keypair)));\n}\n\n// ── Sign and Send ─────────────────────────────────────────────────────────────\n\n/**\n * Sign a transaction and broadcast it to Solana.\n * Returns the transaction hash after confirmation.\n * Used for: solana_signAndSendTransaction\n */\nexport async function signAndSendTransaction(\n serializedTx: string,\n keypair: AccountKeypair,\n options: SendOptions = {},\n conn?: Connection,\n): Promise<SendResult> {\n const { serialized } = await signTransaction(serializedTx, keypair);\n return sendSignedTransaction(serialized, options, conn);\n}\n\n/**\n * Broadcast an already-signed transaction.\n * Used when the wallet has already signed and just needs to broadcast.\n */\nexport async function sendSignedTransaction(\n serializedSignedTx: string,\n options: SendOptions = {},\n conn?: Connection,\n): Promise<SendResult> {\n const connection = conn ?? getConnection();\n const txBuffer = Buffer.from(serializedSignedTx, \"base64\");\n\n const sendOptions: SendOptions = {\n skipPreflight: false, // run preflight checks\n preflightCommitment: \"confirmed\",\n ...options,\n };\n\n let txHash: string;\n\n try {\n // Try versioned first\n const versionedTx = VersionedTransaction.deserialize(txBuffer);\n txHash = await connection.sendTransaction(versionedTx, sendOptions);\n } catch {\n // Fall back to legacy\n const legacyTx = Transaction.from(txBuffer);\n txHash = await connection.sendRawTransaction(\n legacyTx.serialize(),\n sendOptions,\n );\n }\n\n // Wait for confirmation\n const { blockhash, lastValidBlockHeight } =\n await connection.getLatestBlockhash();\n const confirmResult = await connection.confirmTransaction(\n { signature: txHash, blockhash, lastValidBlockHeight },\n \"confirmed\",\n );\n\n return {\n txHash,\n confirmed: !confirmResult.value.err,\n };\n}\n\n// ── Message Signing ───────────────────────────────────────────────────────────\n\n/**\n * Sign an off-chain message (for authentication / proof of ownership).\n * Used for: solana_signMessage\n * Returns base58 encoded signature.\n */\nexport function signOffchainMessage(\n message: Uint8Array,\n keypair: AccountKeypair,\n): string {\n const signature = nacl.sign.detached(message, keypair.secretKey);\n return bs58.encode(Buffer.from(signature));\n}\n\n// ── Internal ──────────────────────────────────────────────────────────────────\n\n/**\n * Convert our AccountKeypair to a @solana/web3.js Keypair.\n * The secretKey is already in the correct 64-byte format.\n */\nfunction toSolanaKeypair(keypair: AccountKeypair): Keypair {\n return Keypair.fromSecretKey(keypair.secretKey);\n}\n","/**\n *\n * Solana RPC connection manager.\n * Single shared connection instance for the daemon session.\n *\n * Dependencies: @solana/web3.js\n */\n\nimport { Connection, PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js';\nimport { getRpcUrl, getCluster } from '../vault/config';\n\n// Singleton connection\n\nlet _connection: Connection | null = null;\n\nexport function getConnection(forceNew: boolean = false): Connection {\n if (!_connection || forceNew) {\n const rpcUrl = getRpcUrl();\n _connection = new Connection(rpcUrl, 'confirmed');\n }\n return _connection;\n}\n\n/**\n * Reset connection — used when RPC URL or cluster changes.\n */\nexport function resetConnection(): void {\n _connection = null;\n}\n\n// Balance─\n\n/**\n * Get SOL balance for an address in lamports and SOL.\n */\nexport async function getBalance(\n publicKeyStr: string,\n): Promise<{ lamports: number; sol: number }> {\n const connection = getConnection();\n const pubkey = new PublicKey(publicKeyStr);\n const lamports = await connection.getBalance(pubkey);\n return {\n lamports,\n sol: lamports / LAMPORTS_PER_SOL,\n };\n}\n\n/**\n * Request a devnet airdrop (only works on devnet/testnet).\n * Returns the transaction signature.\n */\nexport async function requestAirdrop(\n publicKeyStr: string,\n solAmount: number = 1,\n): Promise<string> {\n const cluster = getCluster();\n if (cluster === 'mainnet-beta') {\n throw new Error('Airdrops are not available on mainnet.');\n }\n\n const connection = getConnection();\n const pubkey = new PublicKey(publicKeyStr);\n const lamports = solAmount * LAMPORTS_PER_SOL;\n\n const signature = await connection.requestAirdrop(pubkey, lamports);\n await connection.confirmTransaction(signature, 'confirmed');\n\n return signature;\n}\n\n/**\n * Get recent blockhash — required for building transactions.\n */\nexport async function getLatestBlockhash(): Promise<{\n blockhash: string;\n lastValidBlockHeight: number;\n}> {\n const connection = getConnection();\n return connection.getLatestBlockhash('confirmed');\n}\n\n/**\n * Confirm a transaction by signature.\n * Polls until confirmed or timeout.\n */\nexport async function confirmTransaction(\n signature: string,\n timeoutMs: number = 30_000,\n): Promise<boolean> {\n const connection = getConnection();\n const { blockhash, lastValidBlockHeight } = await getLatestBlockhash();\n\n const result = await connection.confirmTransaction(\n { signature, blockhash, lastValidBlockHeight },\n 'confirmed',\n );\n\n return !result.value.err;\n}\n\n/**\n * Check if the RPC endpoint is reachable.\n */\nexport async function healthCheck(): Promise<boolean> {\n try {\n const connection = getConnection();\n await connection.getVersion();\n return true;\n } catch {\n return false;\n }\n}\n","/**\n *\n * Reads and writes ~/.wallet/config.json\n * Stores non-sensitive wallet preferences:\n * - Active account index\n * - RPC endpoint (mainnet / devnet)\n * - Account metadata (names, indices — no keys)\n * - WalletConnect project ID\n *\n * Dependencies: Node.js built-in `fs`, `os`, `path`\n */\n\nimport fs from \"fs\";\nimport { getConfigPath, ensureWalletDir } from \"./keystore\";\nimport { AccountStore, createAccountStore } from \"./accounts\";\n\n// ── Types ─────────────────────────────────────────────────────────────────────\n\nexport type ClusterType = \"mainnet-beta\" | \"devnet\" | \"testnet\" | \"localnet\" | \"custom\";\n\nexport interface WalletConfig {\n version: number;\n cluster: ClusterType;\n rpcUrl: string;\n walletConnectProjectId: string;\n accountStore: AccountStore;\n createdAt: string;\n updatedAt: string;\n}\n\n// ── Defaults ──────────────────────────────────────────────────────────────────\n\nconst HELIUS_API_KEY = process.env.HELIUS_API_KEY;\nexport const RPC_URLS: Record<Exclude<ClusterType, \"custom\">, string> = {\n \"mainnet-beta\": HELIUS_API_KEY\n ? `https://mainnet.helius-rpc.com/?api-key=${HELIUS_API_KEY}`\n : \"https://api.mainnet-beta.solana.com\",\n devnet: HELIUS_API_KEY\n ? `https://devnet.helius-rpc.com/?api-key=${HELIUS_API_KEY}`\n : \"https://api.devnet.solana.com\",\n testnet: \"https://api.testnet.solana.com\",\n localnet: \"http://localhost:8899\",\n};\n\nconst CONFIG_VERSION = 1;\n\n// ── Read / Write ──────────────────────────────────────────────────────────────\n\nexport function configExists(): boolean {\n return fs.existsSync(getConfigPath());\n}\n\nexport function loadConfig(): WalletConfig {\n if (!configExists()) {\n throw new Error(\"No config found. Run `wallet init` first.\");\n }\n const raw = fs.readFileSync(getConfigPath(), \"utf8\");\n return JSON.parse(raw) as WalletConfig;\n}\n\nexport function saveConfig(config: WalletConfig): void {\n ensureWalletDir();\n const updated = { ...config, updatedAt: new Date().toISOString() };\n fs.writeFileSync(getConfigPath(), JSON.stringify(updated, null, 2), {\n mode: 0o600,\n });\n}\n\n/**\n * Create a fresh config on `wallet init`.\n */\nexport function createConfig(\n walletConnectProjectId: string = \"\",\n cluster: Exclude<ClusterType, \"custom\"> = \"devnet\",\n): WalletConfig {\n const now = new Date().toISOString();\n return {\n version: CONFIG_VERSION,\n cluster,\n rpcUrl: RPC_URLS[cluster],\n walletConnectProjectId,\n accountStore: createAccountStore(\"Account 1\"),\n createdAt: now,\n updatedAt: now,\n };\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\nexport function updateCluster(cluster: Exclude<ClusterType, \"custom\">): void {\n const config = loadConfig();\n config.cluster = cluster;\n config.rpcUrl = RPC_URLS[cluster];\n saveConfig(config);\n}\n\nexport function updateRpcUrl(url: string): void {\n const config = loadConfig();\n config.rpcUrl = url;\n saveConfig(config);\n}\n\nexport function updateAccountStore(accountStore: AccountStore): void {\n const config = loadConfig();\n config.accountStore = accountStore;\n saveConfig(config);\n}\n\nexport function getRpcUrl(): string {\n return loadConfig().rpcUrl;\n}\n\nexport function getCluster(): ClusterType {\n return loadConfig().cluster;\n}\n\nexport function getAccountStore(): AccountStore {\n return loadConfig().accountStore;\n}\n","/**\n *\n * AES-256-GCM encrypted keystore.\n * The mnemonic (seed phrase) is encrypted at rest in ~/.wallet/vault.enc\n * Private keys NEVER touch disk — they are derived in memory at runtime.\n *\n * Encryption scheme:\n * - Key derivation: PBKDF2 (SHA-512, 210,000 iterations) — OWASP recommended\n * - Cipher: AES-256-GCM (authenticated encryption — detects tampering)\n * - Salt: 32 random bytes (unique per vault)\n * - IV: 16 random bytes (unique per encryption)\n *\n * Dependencies: Node.js built-in `crypto`, `fs`, `os`, `path`\n */\n\nimport crypto from \"crypto\";\nimport fs from \"fs\";\nimport os from \"os\";\nimport path from \"path\";\n\n// ── Types ─────────────────────────────────────────────────────────────────────\n\nexport interface VaultData {\n mnemonic: string;\n createdAt: string;\n version: number;\n}\n\ninterface EncryptedVault {\n version: number; // format version for future migrations\n salt: string; // hex — used for PBKDF2 key derivation\n iv: string; // hex — AES-GCM initialisation vector\n authTag: string; // hex — GCM authentication tag (detects tampering)\n ciphertext: string; // hex — encrypted vault data\n}\n\n// ── Constants ─────────────────────────────────────────────────────────────────\n\nconst VAULT_VERSION = 1;\nconst PBKDF2_ITERATIONS = 210_000; // OWASP 2024 recommendation for PBKDF2-SHA512\nconst PBKDF2_DIGEST = \"sha512\";\nconst KEY_LENGTH = 32; // 256 bits for AES-256\nconst SALT_LENGTH = 32; // 256 bits\nconst IV_LENGTH = 16; // 128 bits for AES-GCM\nconst CIPHER = \"aes-256-gcm\";\n\n// ── Vault Path ────────────────────────────────────────────────────────────────\n\nexport function getWalletDir(): string {\n // return path.join(__dirname, \".wallet\");\n return path.join(os.homedir(), \".wallet\");\n}\n\nexport function getVaultPath(): string {\n return path.join(getWalletDir(), \"vault.enc\");\n}\n\nexport function getConfigPath(): string {\n return path.join(getWalletDir(), \"config.json\");\n}\n\nexport function getSessionsPath(): string {\n return path.join(getWalletDir(), \"sessions.json\");\n}\n\nexport const SESSIONS_FILE = getSessionsPath();\n\nexport function ensureWalletDir(): void {\n const dir = getWalletDir();\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); // owner-only access\n }\n}\n\nexport function vaultExists(): boolean {\n return fs.existsSync(getVaultPath());\n}\n\n// ── Encryption ────────────────────────────────────────────────────────────────\n\n/**\n * Encrypt and save the vault to disk.\n * @param data - vault contents (mnemonic + metadata)\n * @param password - user's wallet password\n */\nexport async function saveVault(\n data: VaultData,\n password: string,\n): Promise<void> {\n ensureWalletDir();\n\n const salt = crypto.randomBytes(SALT_LENGTH);\n const iv = crypto.randomBytes(IV_LENGTH);\n const key = await deriveKey(password, salt);\n\n const plaintext = JSON.stringify(data);\n const cipher = crypto.createCipheriv(CIPHER, key, iv);\n\n const encrypted = Buffer.concat([\n cipher.update(plaintext, \"utf8\"),\n cipher.final(),\n ]);\n\n const authTag = cipher.getAuthTag();\n\n const vault: EncryptedVault = {\n version: VAULT_VERSION,\n salt: salt.toString(\"hex\"),\n iv: iv.toString(\"hex\"),\n authTag: authTag.toString(\"hex\"),\n ciphertext: encrypted.toString(\"hex\"),\n };\n\n fs.writeFileSync(getVaultPath(), JSON.stringify(vault, null, 2), {\n mode: 0o600, // owner read/write only\n });\n}\n\n/**\n * Load and decrypt the vault from disk.\n * Throws if the password is wrong or the file has been tampered with.\n */\nexport async function loadVault(password: string): Promise<VaultData> {\n if (!vaultExists()) {\n throw new Error(\"No vault found. Run `wallet init` to create one.\");\n }\n\n const raw = fs.readFileSync(getVaultPath(), \"utf8\");\n const vault: EncryptedVault = JSON.parse(raw);\n\n if (vault.version !== VAULT_VERSION) {\n throw new Error(`Unsupported vault version: ${vault.version}`);\n }\n\n const salt = Buffer.from(vault.salt, \"hex\");\n const iv = Buffer.from(vault.iv, \"hex\");\n const authTag = Buffer.from(vault.authTag, \"hex\");\n const ciphertext = Buffer.from(vault.ciphertext, \"hex\");\n\n const key = await deriveKey(password, salt);\n\n try {\n const decipher = crypto.createDecipheriv(CIPHER, key, iv);\n decipher.setAuthTag(authTag);\n\n const decrypted = Buffer.concat([\n decipher.update(ciphertext),\n decipher.final(),\n ]);\n\n return JSON.parse(decrypted.toString(\"utf8\")) as VaultData;\n } catch {\n // GCM auth tag failure means wrong password OR tampered file\n throw new Error(\"Decryption failed: wrong password or vault is corrupted.\");\n }\n}\n\n/**\n * Change the vault password.\n * Decrypts with old password, re-encrypts with new password.\n */\nexport async function changePassword(\n oldPassword: string,\n newPassword: string,\n): Promise<void> {\n const data = await loadVault(oldPassword);\n await saveVault(data, newPassword);\n}\n\n// ── Internal ──────────────────────────────────────────────────────────────────\n\n/**\n * Derive a 256-bit AES key from a password using PBKDF2-SHA512.\n */\nfunction deriveKey(password: string, salt: Buffer): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n crypto.pbkdf2(\n password,\n salt,\n PBKDF2_ITERATIONS,\n KEY_LENGTH,\n PBKDF2_DIGEST,\n (err, key) => {\n if (err) reject(err);\n else resolve(key);\n },\n );\n });\n}\n","/**\n *\n * BIP-44 HD key derivation for Solana accounts.\n *\n * Derivation path: m/44'/501'/index'/0'\n * 44' = BIP-44 purpose\n * 501' = Solana's registered coin type (SLIP-44)\n * n' = account index (0 = first account, 1 = second, etc.)\n * 0' = change (always 0 for Solana)\n *\n * This is the exact path Phantom, Backpack, and Solflare use.\n * Same seed → same addresses as those wallets.\n *\n * Dependencies: ed25519-hd-key, tweetnacl, bs58\n */\n\nimport { derivePath } from \"ed25519-hd-key\";\nimport nacl from \"tweetnacl\";\nimport bs58 from \"bs58\";\nimport { mnemonicToSeed } from \"./mnemonic\";\n\n// ── Types ─────────────────────────────────────────────────────────────────────\n\nexport interface Account {\n index: number; // derivation index\n name: string; // user-defined label\n publicKey: string; // Base58 encoded — this is the Solana address\n derivationPath: string; // m/44'/501'/index'/0'\n createdAt: string;\n}\n\n/**\n * In-memory keypair — private key NEVER persisted to disk.\n * Lives only for the duration of the daemon session.\n */\nexport interface AccountKeypair extends Account {\n secretKey: Uint8Array; // 64-byte Ed25519 secret key (seed + public key)\n}\n\nexport interface AccountStore {\n accounts: Account[];\n activeIndex: number;\n}\n\n// ── Derivation ────────────────────────────────────────────────────────────────\n\n/**\n * Derive an Ed25519 keypair at a specific BIP-44 index.\n * This is the core operation — called once per session after vault unlock.\n *\n * @param seed - 64-byte seed from BIP-39 mnemonic\n * @param index - account index (0-based)\n * @param name - label for this account\n */\nexport function deriveAccount(\n seed: Buffer,\n index: number,\n name: string = `Account ${index + 1}`,\n): AccountKeypair {\n const path = derivationPath(index);\n\n // SLIP-0010 Ed25519 derivation\n const { key: privateKeyBytes } = derivePath(path, seed.toString(\"hex\"));\n\n // nacl keypair from 32-byte seed\n const keypair = nacl.sign.keyPair.fromSeed(privateKeyBytes);\n\n const publicKey = bs58.encode(Buffer.from(keypair.publicKey));\n\n return {\n index,\n name,\n publicKey,\n secretKey: keypair.secretKey, // 64 bytes: seed + public key\n derivationPath: path,\n createdAt: new Date().toISOString(),\n };\n}\n\n/**\n * Derive multiple accounts at once.\n * Used on wallet startup to load all known accounts into memory.\n */\nexport function deriveAccounts(\n seed: Buffer,\n accountStore: AccountStore,\n): AccountKeypair[] {\n return accountStore.accounts.map((account) =>\n deriveAccount(seed, account.index, account.name),\n );\n}\n\n/**\n * Derive a single account directly from mnemonic.\n * Convenience wrapper used in tests and programmatic access.\n */\nexport async function deriveAccountFromMnemonic(\n mnemonic: string,\n index: number,\n name?: string,\n passphrase?: string,\n): Promise<AccountKeypair> {\n const seed = await mnemonicToSeed(mnemonic, passphrase);\n return deriveAccount(seed, index, name);\n}\n\n// ── Account Store ─────────────────────────────────────────────────────────────\n\n/**\n * Create a fresh account store with the first account.\n */\nexport function createAccountStore(\n firstAccountName: string = \"Account 1\",\n): AccountStore {\n return {\n accounts: [\n {\n index: 0,\n name: firstAccountName,\n publicKey: \"\", // filled in after derivation\n derivationPath: derivationPath(0),\n createdAt: new Date().toISOString(),\n },\n ],\n activeIndex: 0,\n };\n}\n\n/**\n * Add a new account to the store.\n * The next index is always max(existing indices) + 1.\n */\nexport function addAccount(\n store: AccountStore,\n seed: Buffer,\n name?: string,\n): { store: AccountStore; keypair: AccountKeypair } {\n const nextIndex =\n store.accounts.length > 0\n ? Math.max(...store.accounts.map((a) => a.index)) + 1\n : 0;\n\n const accountName = name ?? `Account ${nextIndex + 1}`;\n const keypair = deriveAccount(seed, nextIndex, accountName);\n\n const newAccount: Account = {\n index: nextIndex,\n name: accountName,\n publicKey: keypair.publicKey,\n derivationPath: keypair.derivationPath,\n createdAt: keypair.createdAt,\n };\n\n const updatedStore: AccountStore = {\n ...store,\n accounts: [...store.accounts, newAccount],\n };\n\n return { store: updatedStore, keypair };\n}\n\n/**\n * Set the active account by index.\n * The active account is used for all dApp connections and signing.\n */\nexport function setActiveAccount(\n store: AccountStore,\n index: number,\n): AccountStore {\n const exists = store.accounts.find((a) => a.index === index);\n if (!exists) {\n throw new Error(`Account index ${index} does not exist.`);\n }\n return { ...store, activeIndex: index };\n}\n\n/**\n * Rename an account.\n */\nexport function renameAccount(\n store: AccountStore,\n index: number,\n newName: string,\n): AccountStore {\n return {\n ...store,\n accounts: store.accounts.map((a) =>\n a.index === index ? { ...a, name: newName } : a,\n ),\n };\n}\n\n/**\n * Get the active account metadata from the store.\n */\nexport function getActiveAccount(store: AccountStore): Account {\n const account = store.accounts.find((a) => a.index === store.activeIndex);\n if (!account) throw new Error(\"No active account found.\");\n return account;\n}\n\n// ── Utilities ─────────────────────────────────────────────────────────────────\n\n/**\n * Sign a raw message with an account's secret key.\n * Returns a Base58 encoded signature.\n */\nexport function signMessage(\n message: Uint8Array,\n secretKey: Uint8Array,\n): string {\n const signature = nacl.sign.detached(message, secretKey);\n return bs58.encode(Buffer.from(signature));\n}\n\n/**\n * Format account for display in terminal.\n */\nexport function formatAccount(account: Account, isActive: boolean): string {\n const activeMarker = isActive ? \"●\" : \"○\";\n const shortKey = `${account.publicKey.slice(0, 4)}...${account.publicKey.slice(-4)}`;\n return `${activeMarker} [${account.index}] ${account.name.padEnd(20)} ${shortKey}`;\n}\n\n// ── Internal ──────────────────────────────────────────────────────────────────\n\nfunction derivationPath(index: number): string {\n return `m/44'/501'/${index}'/0'`;\n}\n","/**\n *\n * BIP-39 mnemonic generation, validation, and seed derivation.\n * Supports 12-word (128-bit) and 24-word (256-bit) mnemonics.\n *\n * Dependencies: bip39\n */\n\nimport * as bip39 from \"bip39\";\n\nexport type MnemonicStrength = 12 | 24; // 128 = 12 words, 256 = 24 words\n\nexport interface MnemonicResult {\n mnemonic: string;\n wordCount: 12 | 24;\n}\n\n/**\n * Generate a new cryptographically random BIP-39 mnemonic.\n * @param strength 128 for 12-word, 256 for 24-word (default: 128)\n */\nexport function generateMnemonic(\n strength: MnemonicStrength = 12,\n): MnemonicResult {\n const mnemonic = bip39.generateMnemonic(strength == 12 ? 128 : 256);\n const wordCount = strength;\n return { mnemonic, wordCount };\n}\n\n/**\n * Validate a BIP-39 mnemonic phrase.\n * Checks both wordlist membership and BIP-39 checksum.\n */\nexport function validateMnemonic(mnemonic: string): boolean {\n const cleaned = cleanMnemonic(mnemonic);\n return bip39.validateMnemonic(cleaned);\n}\n\n/**\n * Derive a 64-byte seed buffer from a mnemonic.\n * Optional passphrase adds extra security per BIP-39 spec.\n * This seed is the root of ALL derived accounts.\n */\nexport async function mnemonicToSeed(\n mnemonic: string,\n passphrase: string = \"\",\n): Promise<Buffer> {\n const cleaned = cleanMnemonic(mnemonic);\n\n if (!validateMnemonic(cleaned)) {\n throw new Error(\"Invalid mnemonic: failed wordlist or checksum validation\");\n }\n\n return bip39.mnemonicToSeed(cleaned, passphrase);\n}\n\n/**\n * Split mnemonic into a numbered word array.\n * Used for backup verification display in the terminal UI.\n *\n * Example output:\n * [ \"1. witch\", \"2. collapse\", \"3. practice\", ... ]\n */\nexport function mnemonicToNumberedWords(mnemonic: string): string[] {\n return cleanMnemonic(mnemonic)\n .split(\" \")\n .map((word, i) => `${i + 1}. ${word}`);\n}\n\n/**\n * Reconstruct mnemonic from a word array.\n * Used when user inputs words one by one during restore flow.\n */\nexport function wordsToMnemonic(words: string[]): string {\n return words.map((w) => w.trim().toLowerCase()).join(\" \");\n}\n\n// ── Internal ──────────────────────────────────────────────────────────────────\n\nfunction cleanMnemonic(mnemonic: string): string {\n return mnemonic.trim().toLowerCase().replace(/\\s+/g, \" \");\n}\n"],"mappings":"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","names":["nacl","bs58","bs58","nacl"]}
@@ -0,0 +1,88 @@
1
+ /**
2
+ *
3
+ * BIP-44 HD key derivation for Solana accounts.
4
+ *
5
+ * Derivation path: m/44'/501'/index'/0'
6
+ * 44' = BIP-44 purpose
7
+ * 501' = Solana's registered coin type (SLIP-44)
8
+ * n' = account index (0 = first account, 1 = second, etc.)
9
+ * 0' = change (always 0 for Solana)
10
+ *
11
+ * This is the exact path Phantom, Backpack, and Solflare use.
12
+ * Same seed → same addresses as those wallets.
13
+ *
14
+ * Dependencies: ed25519-hd-key, tweetnacl, bs58
15
+ */
16
+ interface Account {
17
+ index: number;
18
+ name: string;
19
+ publicKey: string;
20
+ derivationPath: string;
21
+ createdAt: string;
22
+ }
23
+ /**
24
+ * In-memory keypair — private key NEVER persisted to disk.
25
+ * Lives only for the duration of the daemon session.
26
+ */
27
+ interface AccountKeypair extends Account {
28
+ secretKey: Uint8Array;
29
+ }
30
+ interface AccountStore {
31
+ accounts: Account[];
32
+ activeIndex: number;
33
+ }
34
+ /**
35
+ * Derive an Ed25519 keypair at a specific BIP-44 index.
36
+ * This is the core operation — called once per session after vault unlock.
37
+ *
38
+ * @param seed - 64-byte seed from BIP-39 mnemonic
39
+ * @param index - account index (0-based)
40
+ * @param name - label for this account
41
+ */
42
+ declare function deriveAccount(seed: Buffer, index: number, name?: string): AccountKeypair;
43
+ /**
44
+ * Derive multiple accounts at once.
45
+ * Used on wallet startup to load all known accounts into memory.
46
+ */
47
+ declare function deriveAccounts(seed: Buffer, accountStore: AccountStore): AccountKeypair[];
48
+ /**
49
+ * Derive a single account directly from mnemonic.
50
+ * Convenience wrapper used in tests and programmatic access.
51
+ */
52
+ declare function deriveAccountFromMnemonic(mnemonic: string, index: number, name?: string, passphrase?: string): Promise<AccountKeypair>;
53
+ /**
54
+ * Create a fresh account store with the first account.
55
+ */
56
+ declare function createAccountStore(firstAccountName?: string): AccountStore;
57
+ /**
58
+ * Add a new account to the store.
59
+ * The next index is always max(existing indices) + 1.
60
+ */
61
+ declare function addAccount(store: AccountStore, seed: Buffer, name?: string): {
62
+ store: AccountStore;
63
+ keypair: AccountKeypair;
64
+ };
65
+ /**
66
+ * Set the active account by index.
67
+ * The active account is used for all dApp connections and signing.
68
+ */
69
+ declare function setActiveAccount(store: AccountStore, index: number): AccountStore;
70
+ /**
71
+ * Rename an account.
72
+ */
73
+ declare function renameAccount(store: AccountStore, index: number, newName: string): AccountStore;
74
+ /**
75
+ * Get the active account metadata from the store.
76
+ */
77
+ declare function getActiveAccount(store: AccountStore): Account;
78
+ /**
79
+ * Sign a raw message with an account's secret key.
80
+ * Returns a Base58 encoded signature.
81
+ */
82
+ declare function signMessage(message: Uint8Array, secretKey: Uint8Array): string;
83
+ /**
84
+ * Format account for display in terminal.
85
+ */
86
+ declare function formatAccount(account: Account, isActive: boolean): string;
87
+
88
+ export { type Account, type AccountKeypair, type AccountStore, addAccount, createAccountStore, deriveAccount, deriveAccountFromMnemonic, deriveAccounts, formatAccount, getActiveAccount, renameAccount, setActiveAccount, signMessage };