@quantulabs/8004-mcp 0.1.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.
- package/LICENSE +21 -0
- package/README.md +370 -0
- package/dist/chains/evm/index.d.ts +2 -0
- package/dist/chains/evm/index.d.ts.map +1 -0
- package/dist/chains/evm/index.js +3 -0
- package/dist/chains/evm/index.js.map +1 -0
- package/dist/chains/evm/provider.d.ts +48 -0
- package/dist/chains/evm/provider.d.ts.map +1 -0
- package/dist/chains/evm/provider.js +446 -0
- package/dist/chains/evm/provider.js.map +1 -0
- package/dist/chains/index.d.ts +3 -0
- package/dist/chains/index.d.ts.map +1 -0
- package/dist/chains/index.js +4 -0
- package/dist/chains/index.js.map +1 -0
- package/dist/chains/solana/data-source.d.ts +17 -0
- package/dist/chains/solana/data-source.d.ts.map +1 -0
- package/dist/chains/solana/data-source.js +65 -0
- package/dist/chains/solana/data-source.js.map +1 -0
- package/dist/chains/solana/index.d.ts +8 -0
- package/dist/chains/solana/index.d.ts.map +1 -0
- package/dist/chains/solana/index.js +9 -0
- package/dist/chains/solana/index.js.map +1 -0
- package/dist/chains/solana/parsers.d.ts +11 -0
- package/dist/chains/solana/parsers.d.ts.map +1 -0
- package/dist/chains/solana/parsers.js +94 -0
- package/dist/chains/solana/parsers.js.map +1 -0
- package/dist/chains/solana/provider.d.ts +33 -0
- package/dist/chains/solana/provider.d.ts.map +1 -0
- package/dist/chains/solana/provider.js +407 -0
- package/dist/chains/solana/provider.js.map +1 -0
- package/dist/chains/solana/state.d.ts +55 -0
- package/dist/chains/solana/state.d.ts.map +1 -0
- package/dist/chains/solana/state.js +162 -0
- package/dist/chains/solana/state.js.map +1 -0
- package/dist/chains/solana/tools/atom.d.ts +45 -0
- package/dist/chains/solana/tools/atom.d.ts.map +1 -0
- package/dist/chains/solana/tools/atom.js +115 -0
- package/dist/chains/solana/tools/atom.js.map +1 -0
- package/dist/chains/solana/tools/validation.d.ts +45 -0
- package/dist/chains/solana/tools/validation.d.ts.map +1 -0
- package/dist/chains/solana/tools/validation.js +212 -0
- package/dist/chains/solana/tools/validation.js.map +1 -0
- package/dist/chains/solana/tools/wallet.d.ts +45 -0
- package/dist/chains/solana/tools/wallet.d.ts.map +1 -0
- package/dist/chains/solana/tools/wallet.js +103 -0
- package/dist/chains/solana/tools/wallet.js.map +1 -0
- package/dist/config/defaults.d.ts +68 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +247 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/env.d.ts +33 -0
- package/dist/config/env.d.ts.map +1 -0
- package/dist/config/env.js +42 -0
- package/dist/config/env.js.map +1 -0
- package/dist/config/index.d.ts +3 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +3 -0
- package/dist/config/index.js.map +1 -0
- package/dist/core/cache/agent-cache.d.ts +36 -0
- package/dist/core/cache/agent-cache.d.ts.map +1 -0
- package/dist/core/cache/agent-cache.js +87 -0
- package/dist/core/cache/agent-cache.js.map +1 -0
- package/dist/core/cache/data-source.d.ts +31 -0
- package/dist/core/cache/data-source.d.ts.map +1 -0
- package/dist/core/cache/data-source.js +6 -0
- package/dist/core/cache/data-source.js.map +1 -0
- package/dist/core/cache/index.d.ts +7 -0
- package/dist/core/cache/index.d.ts.map +1 -0
- package/dist/core/cache/index.js +9 -0
- package/dist/core/cache/index.js.map +1 -0
- package/dist/core/cache/lazy-cache.d.ts +30 -0
- package/dist/core/cache/lazy-cache.d.ts.map +1 -0
- package/dist/core/cache/lazy-cache.js +79 -0
- package/dist/core/cache/lazy-cache.js.map +1 -0
- package/dist/core/cache/slim-store.d.ts +66 -0
- package/dist/core/cache/slim-store.d.ts.map +1 -0
- package/dist/core/cache/slim-store.js +285 -0
- package/dist/core/cache/slim-store.js.map +1 -0
- package/dist/core/cache/sqlite-store.d.ts +99 -0
- package/dist/core/cache/sqlite-store.d.ts.map +1 -0
- package/dist/core/cache/sqlite-store.js +470 -0
- package/dist/core/cache/sqlite-store.js.map +1 -0
- package/dist/core/cache/sync-manager.d.ts +38 -0
- package/dist/core/cache/sync-manager.d.ts.map +1 -0
- package/dist/core/cache/sync-manager.js +213 -0
- package/dist/core/cache/sync-manager.js.map +1 -0
- package/dist/core/errors/index.d.ts +2 -0
- package/dist/core/errors/index.d.ts.map +1 -0
- package/dist/core/errors/index.js +2 -0
- package/dist/core/errors/index.js.map +1 -0
- package/dist/core/errors/mcp-error.d.ts +39 -0
- package/dist/core/errors/mcp-error.d.ts.map +1 -0
- package/dist/core/errors/mcp-error.js +79 -0
- package/dist/core/errors/mcp-error.js.map +1 -0
- package/dist/core/interfaces/agent.d.ts +84 -0
- package/dist/core/interfaces/agent.d.ts.map +1 -0
- package/dist/core/interfaces/agent.js +40 -0
- package/dist/core/interfaces/agent.js.map +1 -0
- package/dist/core/interfaces/chain-provider.d.ts +50 -0
- package/dist/core/interfaces/chain-provider.d.ts.map +1 -0
- package/dist/core/interfaces/chain-provider.js +5 -0
- package/dist/core/interfaces/chain-provider.js.map +1 -0
- package/dist/core/interfaces/feedback.d.ts +46 -0
- package/dist/core/interfaces/feedback.d.ts.map +1 -0
- package/dist/core/interfaces/feedback.js +3 -0
- package/dist/core/interfaces/feedback.js.map +1 -0
- package/dist/core/interfaces/index.d.ts +6 -0
- package/dist/core/interfaces/index.d.ts.map +1 -0
- package/dist/core/interfaces/index.js +7 -0
- package/dist/core/interfaces/index.js.map +1 -0
- package/dist/core/interfaces/reputation.d.ts +47 -0
- package/dist/core/interfaces/reputation.d.ts.map +1 -0
- package/dist/core/interfaces/reputation.js +30 -0
- package/dist/core/interfaces/reputation.js.map +1 -0
- package/dist/core/interfaces/x402.d.ts +226 -0
- package/dist/core/interfaces/x402.d.ts.map +1 -0
- package/dist/core/interfaces/x402.js +120 -0
- package/dist/core/interfaces/x402.js.map +1 -0
- package/dist/core/parsers/common.d.ts +31 -0
- package/dist/core/parsers/common.d.ts.map +1 -0
- package/dist/core/parsers/common.js +185 -0
- package/dist/core/parsers/common.js.map +1 -0
- package/dist/core/parsers/index.d.ts +2 -0
- package/dist/core/parsers/index.d.ts.map +1 -0
- package/dist/core/parsers/index.js +2 -0
- package/dist/core/parsers/index.js.map +1 -0
- package/dist/core/registry/chain-registry.d.ts +26 -0
- package/dist/core/registry/chain-registry.d.ts.map +1 -0
- package/dist/core/registry/chain-registry.js +141 -0
- package/dist/core/registry/chain-registry.js.map +1 -0
- package/dist/core/registry/index.d.ts +3 -0
- package/dist/core/registry/index.d.ts.map +1 -0
- package/dist/core/registry/index.js +3 -0
- package/dist/core/registry/index.js.map +1 -0
- package/dist/core/registry/tool-registry.d.ts +23 -0
- package/dist/core/registry/tool-registry.d.ts.map +1 -0
- package/dist/core/registry/tool-registry.js +66 -0
- package/dist/core/registry/tool-registry.js.map +1 -0
- package/dist/core/serializers/common.d.ts +20 -0
- package/dist/core/serializers/common.d.ts.map +1 -0
- package/dist/core/serializers/common.js +76 -0
- package/dist/core/serializers/common.js.map +1 -0
- package/dist/core/serializers/index.d.ts +2 -0
- package/dist/core/serializers/index.d.ts.map +1 -0
- package/dist/core/serializers/index.js +2 -0
- package/dist/core/serializers/index.js.map +1 -0
- package/dist/core/services/index.d.ts +3 -0
- package/dist/core/services/index.d.ts.map +1 -0
- package/dist/core/services/index.js +3 -0
- package/dist/core/services/index.js.map +1 -0
- package/dist/core/services/ipfs-service.d.ts +59 -0
- package/dist/core/services/ipfs-service.d.ts.map +1 -0
- package/dist/core/services/ipfs-service.js +84 -0
- package/dist/core/services/ipfs-service.js.map +1 -0
- package/dist/core/utils/tags.d.ts +38 -0
- package/dist/core/utils/tags.d.ts.map +1 -0
- package/dist/core/utils/tags.js +77 -0
- package/dist/core/utils/tags.js.map +1 -0
- package/dist/core/utils/value-encoding.d.ts +36 -0
- package/dist/core/utils/value-encoding.d.ts.map +1 -0
- package/dist/core/utils/value-encoding.js +196 -0
- package/dist/core/utils/value-encoding.js.map +1 -0
- package/dist/core/wallet/index.d.ts +2 -0
- package/dist/core/wallet/index.d.ts.map +1 -0
- package/dist/core/wallet/index.js +3 -0
- package/dist/core/wallet/index.js.map +1 -0
- package/dist/core/wallet/wallet-manager.d.ts +86 -0
- package/dist/core/wallet/wallet-manager.d.ts.map +1 -0
- package/dist/core/wallet/wallet-manager.js +783 -0
- package/dist/core/wallet/wallet-manager.js.map +1 -0
- package/dist/core/x402/index.d.ts +2 -0
- package/dist/core/x402/index.d.ts.map +1 -0
- package/dist/core/x402/index.js +3 -0
- package/dist/core/x402/index.js.map +1 -0
- package/dist/core/x402/proof-validator.d.ts +57 -0
- package/dist/core/x402/proof-validator.d.ts.map +1 -0
- package/dist/core/x402/proof-validator.js +103 -0
- package/dist/core/x402/proof-validator.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +135 -0
- package/dist/index.js.map +1 -0
- package/dist/state/global-state.d.ts +98 -0
- package/dist/state/global-state.d.ts.map +1 -0
- package/dist/state/global-state.js +258 -0
- package/dist/state/global-state.js.map +1 -0
- package/dist/tools/definitions.d.ts +10 -0
- package/dist/tools/definitions.d.ts.map +1 -0
- package/dist/tools/definitions.js +163 -0
- package/dist/tools/definitions.js.map +1 -0
- package/dist/tools/unified/agent.d.ts +5 -0
- package/dist/tools/unified/agent.d.ts.map +1 -0
- package/dist/tools/unified/agent.js +300 -0
- package/dist/tools/unified/agent.js.map +1 -0
- package/dist/tools/unified/cache.d.ts +5 -0
- package/dist/tools/unified/cache.d.ts.map +1 -0
- package/dist/tools/unified/cache.js +207 -0
- package/dist/tools/unified/cache.js.map +1 -0
- package/dist/tools/unified/collection.d.ts +5 -0
- package/dist/tools/unified/collection.d.ts.map +1 -0
- package/dist/tools/unified/collection.js +162 -0
- package/dist/tools/unified/collection.js.map +1 -0
- package/dist/tools/unified/config.d.ts +5 -0
- package/dist/tools/unified/config.d.ts.map +1 -0
- package/dist/tools/unified/config.js +217 -0
- package/dist/tools/unified/config.js.map +1 -0
- package/dist/tools/unified/crawler.d.ts +5 -0
- package/dist/tools/unified/crawler.d.ts.map +1 -0
- package/dist/tools/unified/crawler.js +212 -0
- package/dist/tools/unified/crawler.js.map +1 -0
- package/dist/tools/unified/feedback.d.ts +5 -0
- package/dist/tools/unified/feedback.d.ts.map +1 -0
- package/dist/tools/unified/feedback.js +274 -0
- package/dist/tools/unified/feedback.js.map +1 -0
- package/dist/tools/unified/ipfs.d.ts +5 -0
- package/dist/tools/unified/ipfs.d.ts.map +1 -0
- package/dist/tools/unified/ipfs.js +156 -0
- package/dist/tools/unified/ipfs.js.map +1 -0
- package/dist/tools/unified/oasf.d.ts +5 -0
- package/dist/tools/unified/oasf.d.ts.map +1 -0
- package/dist/tools/unified/oasf.js +167 -0
- package/dist/tools/unified/oasf.js.map +1 -0
- package/dist/tools/unified/registration.d.ts +5 -0
- package/dist/tools/unified/registration.d.ts.map +1 -0
- package/dist/tools/unified/registration.js +223 -0
- package/dist/tools/unified/registration.js.map +1 -0
- package/dist/tools/unified/reputation.d.ts +5 -0
- package/dist/tools/unified/reputation.d.ts.map +1 -0
- package/dist/tools/unified/reputation.js +121 -0
- package/dist/tools/unified/reputation.js.map +1 -0
- package/dist/tools/unified/wallet.d.ts +5 -0
- package/dist/tools/unified/wallet.d.ts.map +1 -0
- package/dist/tools/unified/wallet.js +434 -0
- package/dist/tools/unified/wallet.js.map +1 -0
- package/dist/tools/unified/write-operations.d.ts +5 -0
- package/dist/tools/unified/write-operations.d.ts.map +1 -0
- package/dist/tools/unified/write-operations.js +582 -0
- package/dist/tools/unified/write-operations.js.map +1 -0
- package/dist/tools/unified/x402.d.ts +5 -0
- package/dist/tools/unified/x402.d.ts.map +1 -0
- package/dist/tools/unified/x402.js +594 -0
- package/dist/tools/unified/x402.js.map +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
// Multi-wallet manager with AES-256-GCM encryption and Argon2id key derivation
|
|
2
|
+
// Supports Solana (Ed25519) and EVM (secp256k1) wallets
|
|
3
|
+
import { randomBytes, createCipheriv, createDecipheriv } from 'crypto';
|
|
4
|
+
import { Keypair } from '@solana/web3.js';
|
|
5
|
+
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
|
|
6
|
+
import * as argon2 from 'argon2';
|
|
7
|
+
import { promises as fs } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { homedir } from 'os';
|
|
10
|
+
// Crypto constants
|
|
11
|
+
const ALGORITHM = 'aes-256-gcm';
|
|
12
|
+
const KEY_LENGTH = 32; // 256 bits
|
|
13
|
+
const NONCE_LENGTH = 12; // 96 bits for GCM
|
|
14
|
+
const SALT_LENGTH = 32; // 256 bits
|
|
15
|
+
// Argon2id parameters (OWASP recommended for password hashing)
|
|
16
|
+
const ARGON2_OPTIONS = {
|
|
17
|
+
type: argon2.argon2id,
|
|
18
|
+
memoryCost: 65536, // 64 MB
|
|
19
|
+
timeCost: 3, // 3 iterations
|
|
20
|
+
parallelism: 4, // 4 parallel threads
|
|
21
|
+
hashLength: KEY_LENGTH,
|
|
22
|
+
};
|
|
23
|
+
// Auto-lock configuration
|
|
24
|
+
const DEFAULT_AUTO_LOCK_MS = 15 * 60 * 1000; // 15 minutes default
|
|
25
|
+
const MIN_AUTO_LOCK_MS = 60 * 1000; // 1 minute minimum
|
|
26
|
+
const MAX_AUTO_LOCK_MS = 24 * 60 * 60 * 1000; // 24 hours maximum
|
|
27
|
+
export class WalletManager {
|
|
28
|
+
walletDir;
|
|
29
|
+
walletsPath;
|
|
30
|
+
unlockedWallets = new Map();
|
|
31
|
+
walletSessions = new Map();
|
|
32
|
+
autoLockMs = DEFAULT_AUTO_LOCK_MS;
|
|
33
|
+
constructor(walletDir) {
|
|
34
|
+
this.walletDir = walletDir ?? join(homedir(), '.8004-mcp');
|
|
35
|
+
this.walletsPath = join(this.walletDir, 'wallets');
|
|
36
|
+
}
|
|
37
|
+
// Configure auto-lock timeout (in milliseconds)
|
|
38
|
+
setAutoLockTimeout(ms) {
|
|
39
|
+
this.autoLockMs = Math.max(MIN_AUTO_LOCK_MS, Math.min(MAX_AUTO_LOCK_MS, ms));
|
|
40
|
+
}
|
|
41
|
+
// Get current auto-lock timeout
|
|
42
|
+
getAutoLockTimeout() {
|
|
43
|
+
return this.autoLockMs;
|
|
44
|
+
}
|
|
45
|
+
// Start or refresh auto-lock timer for a wallet
|
|
46
|
+
refreshAutoLock(name) {
|
|
47
|
+
const session = this.walletSessions.get(name);
|
|
48
|
+
if (session) {
|
|
49
|
+
// Clear existing timer
|
|
50
|
+
clearTimeout(session.timer);
|
|
51
|
+
// Set new timer
|
|
52
|
+
session.timer = setTimeout(() => this.autoLockWallet(name), this.autoLockMs);
|
|
53
|
+
session.lastActivity = Date.now();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Auto-lock a wallet (called by timer)
|
|
57
|
+
autoLockWallet(name) {
|
|
58
|
+
const session = this.walletSessions.get(name);
|
|
59
|
+
if (session) {
|
|
60
|
+
// Securely clear the wallet from memory
|
|
61
|
+
this.secureWipe(session.wallet);
|
|
62
|
+
this.walletSessions.delete(name);
|
|
63
|
+
this.unlockedWallets.delete(name);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Securely wipe sensitive data from memory
|
|
67
|
+
secureWipe(wallet) {
|
|
68
|
+
// Overwrite Solana keypair secret key with zeros
|
|
69
|
+
if (wallet.solanaKeypair) {
|
|
70
|
+
const secretKey = wallet.solanaKeypair.secretKey;
|
|
71
|
+
for (let i = 0; i < secretKey.length; i++) {
|
|
72
|
+
secretKey[i] = 0;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Overwrite EVM private key string (limited in JS, but try)
|
|
76
|
+
if (wallet.evmPrivateKey) {
|
|
77
|
+
// TypeScript won't let us mutate, but we can delete the reference
|
|
78
|
+
wallet.evmPrivateKey = undefined;
|
|
79
|
+
}
|
|
80
|
+
if (wallet.evmAccount) {
|
|
81
|
+
wallet.evmAccount = undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Touch a wallet to refresh its auto-lock timer (call on every operation)
|
|
85
|
+
touchWallet(name) {
|
|
86
|
+
if (this.walletSessions.has(name)) {
|
|
87
|
+
this.refreshAutoLock(name);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
// Derive encryption key from password using Argon2id
|
|
93
|
+
async deriveKey(password, salt) {
|
|
94
|
+
const hash = await argon2.hash(password, {
|
|
95
|
+
...ARGON2_OPTIONS,
|
|
96
|
+
salt,
|
|
97
|
+
raw: true,
|
|
98
|
+
});
|
|
99
|
+
return Buffer.from(hash);
|
|
100
|
+
}
|
|
101
|
+
// Encrypt private key with AES-256-GCM
|
|
102
|
+
encrypt(secretKey, key, nonce) {
|
|
103
|
+
const cipher = createCipheriv(ALGORITHM, key, nonce);
|
|
104
|
+
const ciphertext = Buffer.concat([
|
|
105
|
+
cipher.update(Buffer.from(secretKey)),
|
|
106
|
+
cipher.final(),
|
|
107
|
+
]);
|
|
108
|
+
const authTag = cipher.getAuthTag();
|
|
109
|
+
return { ciphertext, authTag };
|
|
110
|
+
}
|
|
111
|
+
// Decrypt private key with AES-256-GCM
|
|
112
|
+
decrypt(ciphertext, key, nonce, authTag) {
|
|
113
|
+
const decipher = createDecipheriv(ALGORITHM, key, nonce);
|
|
114
|
+
decipher.setAuthTag(authTag);
|
|
115
|
+
return Buffer.concat([
|
|
116
|
+
decipher.update(ciphertext),
|
|
117
|
+
decipher.final(),
|
|
118
|
+
]);
|
|
119
|
+
}
|
|
120
|
+
// Ensure wallets directory exists
|
|
121
|
+
async ensureDir() {
|
|
122
|
+
try {
|
|
123
|
+
await fs.mkdir(this.walletsPath, { recursive: true, mode: 0o700 });
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
if (err.code !== 'EEXIST')
|
|
127
|
+
throw err;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Get wallet file path from name
|
|
131
|
+
getWalletPath(name) {
|
|
132
|
+
// Sanitize name for filesystem
|
|
133
|
+
const safeName = name.toLowerCase().replace(/[^a-z0-9-_]/g, '-');
|
|
134
|
+
return join(this.walletsPath, `${safeName}.enc`);
|
|
135
|
+
}
|
|
136
|
+
// Check if wallet exists by name
|
|
137
|
+
async exists(name) {
|
|
138
|
+
try {
|
|
139
|
+
await fs.access(this.getWalletPath(name));
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Read wallet file (without decrypting)
|
|
147
|
+
async readWalletFile(name) {
|
|
148
|
+
try {
|
|
149
|
+
const content = await fs.readFile(this.getWalletPath(name), 'utf-8');
|
|
150
|
+
return JSON.parse(content);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// List all wallets
|
|
157
|
+
async list() {
|
|
158
|
+
await this.ensureDir();
|
|
159
|
+
try {
|
|
160
|
+
const files = await fs.readdir(this.walletsPath);
|
|
161
|
+
const wallets = [];
|
|
162
|
+
for (const file of files) {
|
|
163
|
+
if (!file.endsWith('.enc'))
|
|
164
|
+
continue;
|
|
165
|
+
const filePath = join(this.walletsPath, file);
|
|
166
|
+
try {
|
|
167
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
168
|
+
const wallet = JSON.parse(content);
|
|
169
|
+
wallets.push({
|
|
170
|
+
name: wallet.name,
|
|
171
|
+
chainType: wallet.chainType,
|
|
172
|
+
address: wallet.address,
|
|
173
|
+
publicKey: wallet.publicKey,
|
|
174
|
+
createdAt: wallet.createdAt,
|
|
175
|
+
isUnlocked: this.unlockedWallets.has(wallet.name),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// Skip invalid files
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// Sort by name
|
|
183
|
+
wallets.sort((a, b) => a.name.localeCompare(b.name));
|
|
184
|
+
return {
|
|
185
|
+
wallets,
|
|
186
|
+
unlockedCount: this.unlockedWallets.size,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return { wallets: [], unlockedCount: 0 };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Get wallet info by name
|
|
194
|
+
async getInfo(name) {
|
|
195
|
+
const walletFile = await this.readWalletFile(name);
|
|
196
|
+
if (!walletFile)
|
|
197
|
+
return null;
|
|
198
|
+
return {
|
|
199
|
+
name: walletFile.name,
|
|
200
|
+
chainType: walletFile.chainType,
|
|
201
|
+
address: walletFile.address,
|
|
202
|
+
publicKey: walletFile.publicKey,
|
|
203
|
+
createdAt: walletFile.createdAt,
|
|
204
|
+
isUnlocked: this.unlockedWallets.has(walletFile.name),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
// Create new wallet
|
|
208
|
+
async create(name, chainType, password) {
|
|
209
|
+
// Check if wallet already exists
|
|
210
|
+
if (await this.exists(name)) {
|
|
211
|
+
throw new Error(`Wallet "${name}" already exists. Use a different name or delete the existing wallet.`);
|
|
212
|
+
}
|
|
213
|
+
// Validate password strength
|
|
214
|
+
if (password.length < 8) {
|
|
215
|
+
throw new Error('Password must be at least 8 characters long.');
|
|
216
|
+
}
|
|
217
|
+
// Validate name
|
|
218
|
+
if (!name || name.length < 1 || name.length > 50) {
|
|
219
|
+
throw new Error('Wallet name must be between 1 and 50 characters.');
|
|
220
|
+
}
|
|
221
|
+
// Generate keys based on chain type
|
|
222
|
+
let secretKey;
|
|
223
|
+
let publicKey;
|
|
224
|
+
let address;
|
|
225
|
+
if (chainType === 'solana') {
|
|
226
|
+
const keypair = Keypair.generate();
|
|
227
|
+
secretKey = keypair.secretKey;
|
|
228
|
+
publicKey = keypair.publicKey.toBase58();
|
|
229
|
+
address = publicKey; // Solana address is the public key
|
|
230
|
+
}
|
|
231
|
+
else if (chainType === 'evm') {
|
|
232
|
+
const privateKey = generatePrivateKey();
|
|
233
|
+
const account = privateKeyToAccount(privateKey);
|
|
234
|
+
// Convert hex private key to bytes (remove 0x prefix)
|
|
235
|
+
secretKey = Buffer.from(privateKey.slice(2), 'hex');
|
|
236
|
+
publicKey = account.address; // For EVM, we store the address as public key
|
|
237
|
+
address = account.address;
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
throw new Error(`Unsupported chain type: ${chainType}`);
|
|
241
|
+
}
|
|
242
|
+
// Generate cryptographic random values
|
|
243
|
+
const salt = randomBytes(SALT_LENGTH);
|
|
244
|
+
const nonce = randomBytes(NONCE_LENGTH);
|
|
245
|
+
// Derive encryption key from password
|
|
246
|
+
const key = await this.deriveKey(password, salt);
|
|
247
|
+
// Encrypt the secret key
|
|
248
|
+
const { ciphertext, authTag } = this.encrypt(secretKey, key, nonce);
|
|
249
|
+
// Create wallet file
|
|
250
|
+
const walletFile = {
|
|
251
|
+
version: 2,
|
|
252
|
+
chainType,
|
|
253
|
+
algorithm: 'aes-256-gcm',
|
|
254
|
+
kdf: 'argon2id',
|
|
255
|
+
kdfParams: {
|
|
256
|
+
memoryCost: ARGON2_OPTIONS.memoryCost,
|
|
257
|
+
timeCost: ARGON2_OPTIONS.timeCost,
|
|
258
|
+
parallelism: ARGON2_OPTIONS.parallelism,
|
|
259
|
+
},
|
|
260
|
+
salt: salt.toString('base64'),
|
|
261
|
+
nonce: nonce.toString('base64'),
|
|
262
|
+
authTag: authTag.toString('base64'),
|
|
263
|
+
ciphertext: ciphertext.toString('base64'),
|
|
264
|
+
publicKey,
|
|
265
|
+
address,
|
|
266
|
+
name,
|
|
267
|
+
createdAt: new Date().toISOString(),
|
|
268
|
+
updatedAt: new Date().toISOString(),
|
|
269
|
+
};
|
|
270
|
+
// Ensure directory exists and write file
|
|
271
|
+
await this.ensureDir();
|
|
272
|
+
const filePath = this.getWalletPath(name);
|
|
273
|
+
await fs.writeFile(filePath, JSON.stringify(walletFile, null, 2), {
|
|
274
|
+
encoding: 'utf-8',
|
|
275
|
+
mode: 0o600, // Read/write for owner only
|
|
276
|
+
});
|
|
277
|
+
// Auto-unlock after creation with auto-lock timer
|
|
278
|
+
if (chainType === 'solana') {
|
|
279
|
+
const keypair = Keypair.fromSecretKey(secretKey);
|
|
280
|
+
this.unlockedWallets.set(name, {
|
|
281
|
+
name,
|
|
282
|
+
chainType,
|
|
283
|
+
address,
|
|
284
|
+
publicKey,
|
|
285
|
+
solanaKeypair: keypair,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
const privateKeyHex = `0x${Buffer.from(secretKey).toString('hex')}`;
|
|
290
|
+
const account = privateKeyToAccount(privateKeyHex);
|
|
291
|
+
this.unlockedWallets.set(name, {
|
|
292
|
+
name,
|
|
293
|
+
chainType,
|
|
294
|
+
address,
|
|
295
|
+
publicKey,
|
|
296
|
+
evmAccount: account,
|
|
297
|
+
evmPrivateKey: privateKeyHex,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
// Create session with auto-lock timer
|
|
301
|
+
const wallet = this.unlockedWallets.get(name);
|
|
302
|
+
const timer = setTimeout(() => this.autoLockWallet(name), this.autoLockMs);
|
|
303
|
+
this.walletSessions.set(name, {
|
|
304
|
+
wallet,
|
|
305
|
+
timer,
|
|
306
|
+
lastActivity: Date.now(),
|
|
307
|
+
});
|
|
308
|
+
return {
|
|
309
|
+
name,
|
|
310
|
+
chainType,
|
|
311
|
+
address,
|
|
312
|
+
publicKey,
|
|
313
|
+
filePath,
|
|
314
|
+
message: `Wallet "${name}" created and unlocked. Fund this address: ${address}`,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
// Import existing private key
|
|
318
|
+
async import(name, chainType, privateKey, password) {
|
|
319
|
+
// Check if wallet already exists
|
|
320
|
+
if (await this.exists(name)) {
|
|
321
|
+
throw new Error(`Wallet "${name}" already exists. Use a different name.`);
|
|
322
|
+
}
|
|
323
|
+
// Validate password strength
|
|
324
|
+
if (password.length < 8) {
|
|
325
|
+
throw new Error('Password must be at least 8 characters long.');
|
|
326
|
+
}
|
|
327
|
+
// Parse private key based on chain type
|
|
328
|
+
let secretKey;
|
|
329
|
+
let publicKey;
|
|
330
|
+
let address;
|
|
331
|
+
if (chainType === 'solana') {
|
|
332
|
+
const keypair = this.parseSolanaPrivateKey(privateKey);
|
|
333
|
+
secretKey = keypair.secretKey;
|
|
334
|
+
publicKey = keypair.publicKey.toBase58();
|
|
335
|
+
address = publicKey;
|
|
336
|
+
}
|
|
337
|
+
else if (chainType === 'evm') {
|
|
338
|
+
const { account, privateKeyBytes } = this.parseEvmPrivateKey(privateKey);
|
|
339
|
+
secretKey = privateKeyBytes;
|
|
340
|
+
publicKey = account.address;
|
|
341
|
+
address = account.address;
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
throw new Error(`Unsupported chain type: ${chainType}`);
|
|
345
|
+
}
|
|
346
|
+
// Generate cryptographic random values
|
|
347
|
+
const salt = randomBytes(SALT_LENGTH);
|
|
348
|
+
const nonce = randomBytes(NONCE_LENGTH);
|
|
349
|
+
// Derive encryption key from password
|
|
350
|
+
const key = await this.deriveKey(password, salt);
|
|
351
|
+
// Encrypt the secret key
|
|
352
|
+
const { ciphertext, authTag } = this.encrypt(secretKey, key, nonce);
|
|
353
|
+
// Create wallet file
|
|
354
|
+
const walletFile = {
|
|
355
|
+
version: 2,
|
|
356
|
+
chainType,
|
|
357
|
+
algorithm: 'aes-256-gcm',
|
|
358
|
+
kdf: 'argon2id',
|
|
359
|
+
kdfParams: {
|
|
360
|
+
memoryCost: ARGON2_OPTIONS.memoryCost,
|
|
361
|
+
timeCost: ARGON2_OPTIONS.timeCost,
|
|
362
|
+
parallelism: ARGON2_OPTIONS.parallelism,
|
|
363
|
+
},
|
|
364
|
+
salt: salt.toString('base64'),
|
|
365
|
+
nonce: nonce.toString('base64'),
|
|
366
|
+
authTag: authTag.toString('base64'),
|
|
367
|
+
ciphertext: ciphertext.toString('base64'),
|
|
368
|
+
publicKey,
|
|
369
|
+
address,
|
|
370
|
+
name,
|
|
371
|
+
createdAt: new Date().toISOString(),
|
|
372
|
+
updatedAt: new Date().toISOString(),
|
|
373
|
+
};
|
|
374
|
+
// Ensure directory exists and write file
|
|
375
|
+
await this.ensureDir();
|
|
376
|
+
const filePath = this.getWalletPath(name);
|
|
377
|
+
await fs.writeFile(filePath, JSON.stringify(walletFile, null, 2), {
|
|
378
|
+
encoding: 'utf-8',
|
|
379
|
+
mode: 0o600,
|
|
380
|
+
});
|
|
381
|
+
// Auto-unlock after import with auto-lock timer
|
|
382
|
+
if (chainType === 'solana') {
|
|
383
|
+
const keypair = Keypair.fromSecretKey(secretKey);
|
|
384
|
+
this.unlockedWallets.set(name, {
|
|
385
|
+
name,
|
|
386
|
+
chainType,
|
|
387
|
+
address,
|
|
388
|
+
publicKey,
|
|
389
|
+
solanaKeypair: keypair,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
const privateKeyHex = `0x${Buffer.from(secretKey).toString('hex')}`;
|
|
394
|
+
const account = privateKeyToAccount(privateKeyHex);
|
|
395
|
+
this.unlockedWallets.set(name, {
|
|
396
|
+
name,
|
|
397
|
+
chainType,
|
|
398
|
+
address,
|
|
399
|
+
publicKey,
|
|
400
|
+
evmAccount: account,
|
|
401
|
+
evmPrivateKey: privateKeyHex,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
// Create session with auto-lock timer
|
|
405
|
+
const importedWallet = this.unlockedWallets.get(name);
|
|
406
|
+
const importTimer = setTimeout(() => this.autoLockWallet(name), this.autoLockMs);
|
|
407
|
+
this.walletSessions.set(name, {
|
|
408
|
+
wallet: importedWallet,
|
|
409
|
+
timer: importTimer,
|
|
410
|
+
lastActivity: Date.now(),
|
|
411
|
+
});
|
|
412
|
+
return {
|
|
413
|
+
name,
|
|
414
|
+
chainType,
|
|
415
|
+
address,
|
|
416
|
+
publicKey,
|
|
417
|
+
filePath,
|
|
418
|
+
message: `Wallet "${name}" imported and unlocked. Address: ${address}`,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
// Parse Solana private key from various formats
|
|
422
|
+
parseSolanaPrivateKey(privateKey) {
|
|
423
|
+
try {
|
|
424
|
+
const trimmed = privateKey.trim();
|
|
425
|
+
// JSON array format [1,2,3,...]
|
|
426
|
+
if (trimmed.startsWith('[')) {
|
|
427
|
+
const arr = JSON.parse(trimmed);
|
|
428
|
+
return Keypair.fromSecretKey(Uint8Array.from(arr));
|
|
429
|
+
}
|
|
430
|
+
// Hex format
|
|
431
|
+
if (trimmed.startsWith('0x') || (trimmed.length === 128 && /^[0-9a-fA-F]+$/.test(trimmed))) {
|
|
432
|
+
const hex = trimmed.startsWith('0x') ? trimmed.slice(2) : trimmed;
|
|
433
|
+
return Keypair.fromSecretKey(Uint8Array.from(Buffer.from(hex, 'hex')));
|
|
434
|
+
}
|
|
435
|
+
// Base64 format
|
|
436
|
+
const decoded = Buffer.from(trimmed, 'base64');
|
|
437
|
+
if (decoded.length === 64) {
|
|
438
|
+
return Keypair.fromSecretKey(Uint8Array.from(decoded));
|
|
439
|
+
}
|
|
440
|
+
throw new Error('Invalid private key format');
|
|
441
|
+
}
|
|
442
|
+
catch (err) {
|
|
443
|
+
throw new Error(`Failed to parse Solana private key: ${err instanceof Error ? err.message : 'Invalid format'}`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
// Parse EVM private key from various formats
|
|
447
|
+
parseEvmPrivateKey(privateKey) {
|
|
448
|
+
try {
|
|
449
|
+
const trimmed = privateKey.trim();
|
|
450
|
+
let privateKeyHex;
|
|
451
|
+
// 0x prefixed hex
|
|
452
|
+
if (trimmed.startsWith('0x')) {
|
|
453
|
+
privateKeyHex = trimmed;
|
|
454
|
+
}
|
|
455
|
+
// Unprefixed hex (64 chars = 32 bytes)
|
|
456
|
+
else if (trimmed.length === 64 && /^[0-9a-fA-F]+$/.test(trimmed)) {
|
|
457
|
+
privateKeyHex = `0x${trimmed}`;
|
|
458
|
+
}
|
|
459
|
+
// Base64
|
|
460
|
+
else {
|
|
461
|
+
const decoded = Buffer.from(trimmed, 'base64');
|
|
462
|
+
if (decoded.length === 32) {
|
|
463
|
+
privateKeyHex = `0x${decoded.toString('hex')}`;
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
throw new Error('Invalid private key length');
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
const account = privateKeyToAccount(privateKeyHex);
|
|
470
|
+
const privateKeyBytes = Buffer.from(privateKeyHex.slice(2), 'hex');
|
|
471
|
+
return { account, privateKeyBytes };
|
|
472
|
+
}
|
|
473
|
+
catch (err) {
|
|
474
|
+
throw new Error(`Failed to parse EVM private key: ${err instanceof Error ? err.message : 'Invalid format'}`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
// Unlock wallet with password
|
|
478
|
+
async unlock(name, password) {
|
|
479
|
+
// Check if wallet exists
|
|
480
|
+
if (!(await this.exists(name))) {
|
|
481
|
+
throw new Error(`Wallet "${name}" not found. Use wallet_list to see available wallets.`);
|
|
482
|
+
}
|
|
483
|
+
// Already unlocked? Refresh timer and return
|
|
484
|
+
if (this.unlockedWallets.has(name)) {
|
|
485
|
+
const wallet = this.unlockedWallets.get(name);
|
|
486
|
+
this.refreshAutoLock(name);
|
|
487
|
+
const timeoutMinutes = Math.round(this.autoLockMs / 60000);
|
|
488
|
+
return {
|
|
489
|
+
name: wallet.name,
|
|
490
|
+
chainType: wallet.chainType,
|
|
491
|
+
address: wallet.address,
|
|
492
|
+
message: `Wallet "${name}" is already unlocked. Timer refreshed (${timeoutMinutes} min).`,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
// Read wallet file
|
|
496
|
+
const walletFile = await this.readWalletFile(name);
|
|
497
|
+
if (!walletFile) {
|
|
498
|
+
throw new Error('Failed to read wallet file.');
|
|
499
|
+
}
|
|
500
|
+
// Validate version and algorithm
|
|
501
|
+
if (walletFile.version !== 2 || walletFile.algorithm !== 'aes-256-gcm') {
|
|
502
|
+
throw new Error('Unsupported wallet format.');
|
|
503
|
+
}
|
|
504
|
+
// Decode values
|
|
505
|
+
const salt = Buffer.from(walletFile.salt, 'base64');
|
|
506
|
+
const nonce = Buffer.from(walletFile.nonce, 'base64');
|
|
507
|
+
const authTag = Buffer.from(walletFile.authTag, 'base64');
|
|
508
|
+
const ciphertext = Buffer.from(walletFile.ciphertext, 'base64');
|
|
509
|
+
// Derive key from password
|
|
510
|
+
const key = await this.deriveKey(password, salt);
|
|
511
|
+
// Decrypt
|
|
512
|
+
let secretKey;
|
|
513
|
+
try {
|
|
514
|
+
secretKey = this.decrypt(ciphertext, key, nonce, authTag);
|
|
515
|
+
}
|
|
516
|
+
catch {
|
|
517
|
+
throw new Error('Incorrect password.');
|
|
518
|
+
}
|
|
519
|
+
// Reconstruct keys based on chain type
|
|
520
|
+
if (walletFile.chainType === 'solana') {
|
|
521
|
+
const keypair = Keypair.fromSecretKey(Uint8Array.from(secretKey));
|
|
522
|
+
// Verify public key matches
|
|
523
|
+
if (keypair.publicKey.toBase58() !== walletFile.publicKey) {
|
|
524
|
+
throw new Error('Wallet integrity check failed.');
|
|
525
|
+
}
|
|
526
|
+
this.unlockedWallets.set(name, {
|
|
527
|
+
name: walletFile.name,
|
|
528
|
+
chainType: walletFile.chainType,
|
|
529
|
+
address: walletFile.address,
|
|
530
|
+
publicKey: walletFile.publicKey,
|
|
531
|
+
solanaKeypair: keypair,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
else if (walletFile.chainType === 'evm') {
|
|
535
|
+
const privateKeyHex = `0x${secretKey.toString('hex')}`;
|
|
536
|
+
const account = privateKeyToAccount(privateKeyHex);
|
|
537
|
+
// Verify address matches
|
|
538
|
+
if (account.address.toLowerCase() !== walletFile.address.toLowerCase()) {
|
|
539
|
+
throw new Error('Wallet integrity check failed.');
|
|
540
|
+
}
|
|
541
|
+
this.unlockedWallets.set(name, {
|
|
542
|
+
name: walletFile.name,
|
|
543
|
+
chainType: walletFile.chainType,
|
|
544
|
+
address: walletFile.address,
|
|
545
|
+
publicKey: walletFile.publicKey,
|
|
546
|
+
evmAccount: account,
|
|
547
|
+
evmPrivateKey: privateKeyHex,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
// Clear sensitive data from decryption
|
|
551
|
+
secretKey.fill(0);
|
|
552
|
+
// Create session with auto-lock timer
|
|
553
|
+
const wallet = this.unlockedWallets.get(name);
|
|
554
|
+
const timer = setTimeout(() => this.autoLockWallet(name), this.autoLockMs);
|
|
555
|
+
this.walletSessions.set(name, {
|
|
556
|
+
wallet,
|
|
557
|
+
timer,
|
|
558
|
+
lastActivity: Date.now(),
|
|
559
|
+
});
|
|
560
|
+
const timeoutMinutes = Math.round(this.autoLockMs / 60000);
|
|
561
|
+
return {
|
|
562
|
+
name: walletFile.name,
|
|
563
|
+
chainType: walletFile.chainType,
|
|
564
|
+
address: walletFile.address,
|
|
565
|
+
message: `Wallet "${name}" unlocked successfully. Auto-locks after ${timeoutMinutes} min of inactivity.`,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
// Lock a specific wallet (securely wipes memory)
|
|
569
|
+
lock(name) {
|
|
570
|
+
const session = this.walletSessions.get(name);
|
|
571
|
+
if (session) {
|
|
572
|
+
clearTimeout(session.timer);
|
|
573
|
+
this.secureWipe(session.wallet);
|
|
574
|
+
this.walletSessions.delete(name);
|
|
575
|
+
}
|
|
576
|
+
if (this.unlockedWallets.has(name)) {
|
|
577
|
+
this.unlockedWallets.delete(name);
|
|
578
|
+
return true;
|
|
579
|
+
}
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
// Lock all wallets (securely wipes all memory)
|
|
583
|
+
lockAll() {
|
|
584
|
+
// Clear all timers and wipe memory
|
|
585
|
+
for (const [, session] of this.walletSessions.entries()) {
|
|
586
|
+
clearTimeout(session.timer);
|
|
587
|
+
this.secureWipe(session.wallet);
|
|
588
|
+
}
|
|
589
|
+
this.walletSessions.clear();
|
|
590
|
+
const count = this.unlockedWallets.size;
|
|
591
|
+
this.unlockedWallets.clear();
|
|
592
|
+
return count;
|
|
593
|
+
}
|
|
594
|
+
// Check if wallet is unlocked
|
|
595
|
+
isUnlocked(name) {
|
|
596
|
+
return this.unlockedWallets.has(name);
|
|
597
|
+
}
|
|
598
|
+
// Get session info for all unlocked wallets
|
|
599
|
+
getSessionInfo() {
|
|
600
|
+
const sessions = [];
|
|
601
|
+
for (const [name, session] of this.walletSessions.entries()) {
|
|
602
|
+
sessions.push({
|
|
603
|
+
name,
|
|
604
|
+
lastActivity: session.lastActivity,
|
|
605
|
+
chainType: session.wallet.chainType,
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
return sessions;
|
|
609
|
+
}
|
|
610
|
+
// Get unlocked wallet (throws if locked) - refreshes auto-lock timer
|
|
611
|
+
getUnlockedWallet(name) {
|
|
612
|
+
const wallet = this.unlockedWallets.get(name);
|
|
613
|
+
if (!wallet) {
|
|
614
|
+
throw new Error(`Wallet "${name}" is locked. Use wallet_unlock to unlock it first.`);
|
|
615
|
+
}
|
|
616
|
+
// Refresh auto-lock timer on access
|
|
617
|
+
this.refreshAutoLock(name);
|
|
618
|
+
return wallet;
|
|
619
|
+
}
|
|
620
|
+
// Get Solana keypair for unlocked wallet (throws if not Solana or locked)
|
|
621
|
+
getSolanaKeypair(name) {
|
|
622
|
+
const wallet = this.getUnlockedWallet(name);
|
|
623
|
+
if (wallet.chainType !== 'solana' || !wallet.solanaKeypair) {
|
|
624
|
+
throw new Error(`Wallet "${name}" is not a Solana wallet.`);
|
|
625
|
+
}
|
|
626
|
+
return wallet.solanaKeypair;
|
|
627
|
+
}
|
|
628
|
+
// Get EVM account for unlocked wallet (throws if not EVM or locked)
|
|
629
|
+
getEvmAccount(name) {
|
|
630
|
+
const wallet = this.getUnlockedWallet(name);
|
|
631
|
+
if (wallet.chainType !== 'evm' || !wallet.evmAccount) {
|
|
632
|
+
throw new Error(`Wallet "${name}" is not an EVM wallet.`);
|
|
633
|
+
}
|
|
634
|
+
return wallet.evmAccount;
|
|
635
|
+
}
|
|
636
|
+
// Get any unlocked Solana keypair (for backward compatibility)
|
|
637
|
+
getAnyUnlockedSolanaKeypair() {
|
|
638
|
+
for (const wallet of this.unlockedWallets.values()) {
|
|
639
|
+
if (wallet.chainType === 'solana' && wallet.solanaKeypair) {
|
|
640
|
+
return wallet.solanaKeypair;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return null;
|
|
644
|
+
}
|
|
645
|
+
// Get any unlocked EVM account
|
|
646
|
+
getAnyUnlockedEvmAccount() {
|
|
647
|
+
for (const wallet of this.unlockedWallets.values()) {
|
|
648
|
+
if (wallet.chainType === 'evm' && wallet.evmAccount) {
|
|
649
|
+
return wallet.evmAccount;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
// Get any unlocked EVM private key (for SDK initialization)
|
|
655
|
+
getAnyUnlockedEvmPrivateKey() {
|
|
656
|
+
for (const wallet of this.unlockedWallets.values()) {
|
|
657
|
+
if (wallet.chainType === 'evm' && wallet.evmPrivateKey) {
|
|
658
|
+
return wallet.evmPrivateKey;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
663
|
+
// Export wallet as encrypted backup
|
|
664
|
+
async export(name, currentPassword, exportPassword) {
|
|
665
|
+
// First unlock to verify password
|
|
666
|
+
if (!this.isUnlocked(name)) {
|
|
667
|
+
await this.unlock(name, currentPassword);
|
|
668
|
+
}
|
|
669
|
+
// Read current file and return it (optionally re-encrypt with new password)
|
|
670
|
+
const walletFile = await this.readWalletFile(name);
|
|
671
|
+
if (!walletFile) {
|
|
672
|
+
throw new Error('Failed to read wallet file.');
|
|
673
|
+
}
|
|
674
|
+
// If export password is the same or not provided, return current file
|
|
675
|
+
if (!exportPassword || exportPassword === currentPassword) {
|
|
676
|
+
return JSON.stringify(walletFile, null, 2);
|
|
677
|
+
}
|
|
678
|
+
// Re-encrypt with new password
|
|
679
|
+
const wallet = this.getUnlockedWallet(name);
|
|
680
|
+
let secretKey;
|
|
681
|
+
if (wallet.chainType === 'solana' && wallet.solanaKeypair) {
|
|
682
|
+
secretKey = wallet.solanaKeypair.secretKey;
|
|
683
|
+
}
|
|
684
|
+
else if (wallet.chainType === 'evm' && wallet.evmAccount) {
|
|
685
|
+
// Need to extract private key from account - not directly accessible
|
|
686
|
+
// We'll need to re-derive from the current encrypted state
|
|
687
|
+
const salt = Buffer.from(walletFile.salt, 'base64');
|
|
688
|
+
const nonce = Buffer.from(walletFile.nonce, 'base64');
|
|
689
|
+
const authTag = Buffer.from(walletFile.authTag, 'base64');
|
|
690
|
+
const ciphertext = Buffer.from(walletFile.ciphertext, 'base64');
|
|
691
|
+
const key = await this.deriveKey(currentPassword, salt);
|
|
692
|
+
secretKey = this.decrypt(ciphertext, key, nonce, authTag);
|
|
693
|
+
}
|
|
694
|
+
else {
|
|
695
|
+
throw new Error('Cannot export wallet.');
|
|
696
|
+
}
|
|
697
|
+
// Generate new salt/nonce for export
|
|
698
|
+
const salt = randomBytes(SALT_LENGTH);
|
|
699
|
+
const nonce = randomBytes(NONCE_LENGTH);
|
|
700
|
+
const key = await this.deriveKey(exportPassword, salt);
|
|
701
|
+
const { ciphertext, authTag } = this.encrypt(secretKey, key, nonce);
|
|
702
|
+
const exportData = {
|
|
703
|
+
...walletFile,
|
|
704
|
+
salt: salt.toString('base64'),
|
|
705
|
+
nonce: nonce.toString('base64'),
|
|
706
|
+
authTag: authTag.toString('base64'),
|
|
707
|
+
ciphertext: ciphertext.toString('base64'),
|
|
708
|
+
updatedAt: new Date().toISOString(),
|
|
709
|
+
};
|
|
710
|
+
return JSON.stringify(exportData, null, 2);
|
|
711
|
+
}
|
|
712
|
+
// Delete wallet (requires password confirmation)
|
|
713
|
+
async delete(name, password) {
|
|
714
|
+
// Verify password first
|
|
715
|
+
await this.unlock(name, password);
|
|
716
|
+
this.lock(name);
|
|
717
|
+
// Delete file
|
|
718
|
+
await fs.unlink(this.getWalletPath(name));
|
|
719
|
+
}
|
|
720
|
+
// Change password
|
|
721
|
+
async changePassword(name, currentPassword, newPassword) {
|
|
722
|
+
// Validate new password
|
|
723
|
+
if (newPassword.length < 8) {
|
|
724
|
+
throw new Error('New password must be at least 8 characters long.');
|
|
725
|
+
}
|
|
726
|
+
// Unlock with current password
|
|
727
|
+
if (!this.isUnlocked(name)) {
|
|
728
|
+
await this.unlock(name, currentPassword);
|
|
729
|
+
}
|
|
730
|
+
const wallet = this.getUnlockedWallet(name);
|
|
731
|
+
const walletFile = await this.readWalletFile(name);
|
|
732
|
+
if (!walletFile) {
|
|
733
|
+
throw new Error('Failed to read wallet file.');
|
|
734
|
+
}
|
|
735
|
+
// Get secret key
|
|
736
|
+
let secretKey;
|
|
737
|
+
if (wallet.chainType === 'solana' && wallet.solanaKeypair) {
|
|
738
|
+
secretKey = wallet.solanaKeypair.secretKey;
|
|
739
|
+
}
|
|
740
|
+
else if (wallet.chainType === 'evm') {
|
|
741
|
+
const salt = Buffer.from(walletFile.salt, 'base64');
|
|
742
|
+
const nonce = Buffer.from(walletFile.nonce, 'base64');
|
|
743
|
+
const authTag = Buffer.from(walletFile.authTag, 'base64');
|
|
744
|
+
const ciphertext = Buffer.from(walletFile.ciphertext, 'base64');
|
|
745
|
+
const key = await this.deriveKey(currentPassword, salt);
|
|
746
|
+
secretKey = this.decrypt(ciphertext, key, nonce, authTag);
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
throw new Error('Cannot change password for this wallet.');
|
|
750
|
+
}
|
|
751
|
+
// Generate new salt/nonce
|
|
752
|
+
const salt = randomBytes(SALT_LENGTH);
|
|
753
|
+
const nonce = randomBytes(NONCE_LENGTH);
|
|
754
|
+
const key = await this.deriveKey(newPassword, salt);
|
|
755
|
+
const { ciphertext, authTag } = this.encrypt(secretKey, key, nonce);
|
|
756
|
+
// Update wallet file
|
|
757
|
+
const updatedFile = {
|
|
758
|
+
...walletFile,
|
|
759
|
+
salt: salt.toString('base64'),
|
|
760
|
+
nonce: nonce.toString('base64'),
|
|
761
|
+
authTag: authTag.toString('base64'),
|
|
762
|
+
ciphertext: ciphertext.toString('base64'),
|
|
763
|
+
updatedAt: new Date().toISOString(),
|
|
764
|
+
};
|
|
765
|
+
// Write file
|
|
766
|
+
await fs.writeFile(this.getWalletPath(name), JSON.stringify(updatedFile, null, 2), {
|
|
767
|
+
encoding: 'utf-8',
|
|
768
|
+
mode: 0o600,
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
// Singleton instance
|
|
773
|
+
let walletManagerInstance = null;
|
|
774
|
+
export function getWalletManager() {
|
|
775
|
+
if (!walletManagerInstance) {
|
|
776
|
+
walletManagerInstance = new WalletManager();
|
|
777
|
+
}
|
|
778
|
+
return walletManagerInstance;
|
|
779
|
+
}
|
|
780
|
+
export function setWalletManager(manager) {
|
|
781
|
+
walletManagerInstance = manager;
|
|
782
|
+
}
|
|
783
|
+
//# sourceMappingURL=wallet-manager.js.map
|