moneyos 0.2.1 → 0.3.2
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/README.md +180 -25
- package/dist/cli/index.js +2169 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.cjs +720 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +405 -0
- package/dist/index.d.ts +300 -30
- package/dist/index.js +474 -240
- package/dist/index.js.map +1 -1
- package/package.json +30 -10
- package/dist/cli/index.mjs +0 -570
- package/dist/cli/index.mjs.map +0 -1
- package/dist/index.d.mts +0 -135
- package/dist/index.mjs +0 -429
- package/dist/index.mjs.map +0 -1
|
@@ -0,0 +1,2169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/index.ts
|
|
4
|
+
import { Command as Command8 } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/cli/commands/init.ts
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import { generatePrivateKey, privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
|
|
9
|
+
|
|
10
|
+
// src/core/backup-file.ts
|
|
11
|
+
import {
|
|
12
|
+
existsSync as existsSync2,
|
|
13
|
+
readdirSync,
|
|
14
|
+
statSync as statSync2
|
|
15
|
+
} from "fs";
|
|
16
|
+
import { join as join2, resolve } from "path";
|
|
17
|
+
|
|
18
|
+
// src/core/encrypted-wallet.ts
|
|
19
|
+
import {
|
|
20
|
+
createCipheriv,
|
|
21
|
+
createDecipheriv,
|
|
22
|
+
randomBytes,
|
|
23
|
+
scryptSync
|
|
24
|
+
} from "crypto";
|
|
25
|
+
import {
|
|
26
|
+
chmodSync,
|
|
27
|
+
closeSync,
|
|
28
|
+
existsSync,
|
|
29
|
+
fsyncSync,
|
|
30
|
+
mkdirSync,
|
|
31
|
+
openSync,
|
|
32
|
+
readFileSync,
|
|
33
|
+
renameSync,
|
|
34
|
+
statSync,
|
|
35
|
+
unlinkSync,
|
|
36
|
+
writeSync
|
|
37
|
+
} from "fs";
|
|
38
|
+
import { basename, dirname, join } from "path";
|
|
39
|
+
import { recoverMessageAddress } from "viem";
|
|
40
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
41
|
+
var DEFAULT_KDF = {
|
|
42
|
+
name: "scrypt",
|
|
43
|
+
N: 131072,
|
|
44
|
+
r: 8,
|
|
45
|
+
p: 1,
|
|
46
|
+
keyLength: 32
|
|
47
|
+
};
|
|
48
|
+
var SECURE_FILE_MODE = 384;
|
|
49
|
+
var SECURE_PARENT_MODE = 448;
|
|
50
|
+
function normalizePassphrase(passphrase) {
|
|
51
|
+
return passphrase.normalize("NFC");
|
|
52
|
+
}
|
|
53
|
+
function walletProofMessage(wallet) {
|
|
54
|
+
return [
|
|
55
|
+
"MoneyOS wallet metadata",
|
|
56
|
+
`v=${wallet.version}`,
|
|
57
|
+
`kind=${wallet.kind}`,
|
|
58
|
+
`address=${wallet.address}`,
|
|
59
|
+
`createdAt=${wallet.createdAt}`
|
|
60
|
+
].join("|");
|
|
61
|
+
}
|
|
62
|
+
function ensureParentDir(path) {
|
|
63
|
+
const dir = dirname(path);
|
|
64
|
+
if (!existsSync(dir)) {
|
|
65
|
+
mkdirSync(dir, { recursive: true, mode: SECURE_PARENT_MODE });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const mode = statSync(dir).mode & 511;
|
|
69
|
+
if ((mode & 63) !== 0) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Wallet directory ${dir} has insecure permissions (${mode.toString(8)}). Restrict it to 700 before continuing.`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function assertSecureFileMode(path, label) {
|
|
76
|
+
if (!existsSync(path)) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const mode = statSync(path).mode & 511;
|
|
80
|
+
if ((mode & 63) !== 0) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`${label} at ${path} has insecure permissions (${mode.toString(8)}). Restrict it to 600 before continuing.`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function parseWalletFile(raw, path) {
|
|
87
|
+
let parsed;
|
|
88
|
+
try {
|
|
89
|
+
parsed = JSON.parse(raw);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Encrypted wallet at ${path} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (!parsed || typeof parsed !== "object") {
|
|
96
|
+
throw new Error(`Encrypted wallet at ${path} must be a JSON object`);
|
|
97
|
+
}
|
|
98
|
+
const wallet = parsed;
|
|
99
|
+
if (wallet.version !== 1) {
|
|
100
|
+
throw new Error(`Encrypted wallet at ${path} has unsupported version`);
|
|
101
|
+
}
|
|
102
|
+
if (wallet.kind !== "encrypted-local-eoa") {
|
|
103
|
+
throw new Error(`Encrypted wallet at ${path} has unsupported kind`);
|
|
104
|
+
}
|
|
105
|
+
if (typeof wallet.address !== "string") {
|
|
106
|
+
throw new Error(`Encrypted wallet at ${path} is missing address metadata`);
|
|
107
|
+
}
|
|
108
|
+
if (typeof wallet.addressProof !== "string") {
|
|
109
|
+
throw new Error(`Encrypted wallet at ${path} is missing address proof`);
|
|
110
|
+
}
|
|
111
|
+
if (!wallet.kdf || wallet.kdf.name !== "scrypt" || !Number.isInteger(wallet.kdf.N) || !Number.isInteger(wallet.kdf.r) || !Number.isInteger(wallet.kdf.p) || !Number.isInteger(wallet.kdf.keyLength)) {
|
|
112
|
+
throw new Error(`Encrypted wallet at ${path} has invalid KDF parameters`);
|
|
113
|
+
}
|
|
114
|
+
if (!wallet.crypto || wallet.crypto.cipher !== "aes-256-gcm" || typeof wallet.crypto.salt !== "string" || typeof wallet.crypto.nonce !== "string" || typeof wallet.crypto.authTag !== "string" || typeof wallet.crypto.ciphertext !== "string") {
|
|
115
|
+
throw new Error(`Encrypted wallet at ${path} has invalid crypto metadata`);
|
|
116
|
+
}
|
|
117
|
+
if (typeof wallet.createdAt !== "string") {
|
|
118
|
+
throw new Error(`Encrypted wallet at ${path} is missing createdAt`);
|
|
119
|
+
}
|
|
120
|
+
return wallet;
|
|
121
|
+
}
|
|
122
|
+
function toMetadata(wallet) {
|
|
123
|
+
return {
|
|
124
|
+
version: wallet.version,
|
|
125
|
+
kind: wallet.kind,
|
|
126
|
+
address: wallet.address,
|
|
127
|
+
createdAt: wallet.createdAt
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
async function verifyWalletAddressProof(wallet, path) {
|
|
131
|
+
let recovered;
|
|
132
|
+
try {
|
|
133
|
+
recovered = await recoverMessageAddress({
|
|
134
|
+
message: walletProofMessage(wallet),
|
|
135
|
+
signature: wallet.addressProof
|
|
136
|
+
});
|
|
137
|
+
} catch {
|
|
138
|
+
throw new Error(`Encrypted wallet at ${path} has an invalid address proof`);
|
|
139
|
+
}
|
|
140
|
+
if (recovered.toLowerCase() !== wallet.address.toLowerCase()) {
|
|
141
|
+
throw new Error(`Encrypted wallet at ${path} failed address proof validation`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function deriveKey(passphrase, salt, kdf) {
|
|
145
|
+
return scryptSync(normalizePassphrase(passphrase), salt, kdf.keyLength, {
|
|
146
|
+
N: kdf.N,
|
|
147
|
+
r: kdf.r,
|
|
148
|
+
p: kdf.p,
|
|
149
|
+
maxmem: 256 * 1024 * 1024
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
function walletAad(kdf) {
|
|
153
|
+
return Buffer.from(
|
|
154
|
+
JSON.stringify({
|
|
155
|
+
name: kdf.name,
|
|
156
|
+
N: kdf.N,
|
|
157
|
+
r: kdf.r,
|
|
158
|
+
p: kdf.p,
|
|
159
|
+
keyLength: kdf.keyLength
|
|
160
|
+
}),
|
|
161
|
+
"utf8"
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
function writeFileAtomicSecure(path, contents) {
|
|
165
|
+
ensureParentDir(path);
|
|
166
|
+
assertSecureFileMode(path, "Wallet file");
|
|
167
|
+
const tmpPath = join(
|
|
168
|
+
dirname(path),
|
|
169
|
+
`.${basename(path)}.${randomBytes(6).toString("hex")}.tmp`
|
|
170
|
+
);
|
|
171
|
+
const fd = openSync(tmpPath, "wx", SECURE_FILE_MODE);
|
|
172
|
+
let writeError;
|
|
173
|
+
try {
|
|
174
|
+
writeSync(fd, contents);
|
|
175
|
+
fsyncSync(fd);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
writeError = error;
|
|
178
|
+
} finally {
|
|
179
|
+
closeSync(fd);
|
|
180
|
+
}
|
|
181
|
+
if (writeError) {
|
|
182
|
+
try {
|
|
183
|
+
unlinkSync(tmpPath);
|
|
184
|
+
} catch {
|
|
185
|
+
}
|
|
186
|
+
throw writeError;
|
|
187
|
+
}
|
|
188
|
+
renameSync(tmpPath, path);
|
|
189
|
+
chmodSync(path, SECURE_FILE_MODE);
|
|
190
|
+
}
|
|
191
|
+
async function encryptWallet(params) {
|
|
192
|
+
const account = privateKeyToAccount(params.privateKey);
|
|
193
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
194
|
+
const salt = randomBytes(16);
|
|
195
|
+
const nonce = randomBytes(12);
|
|
196
|
+
const key = deriveKey(params.passphrase, salt, DEFAULT_KDF);
|
|
197
|
+
const cipher = createCipheriv("aes-256-gcm", key, nonce);
|
|
198
|
+
cipher.setAAD(walletAad(DEFAULT_KDF));
|
|
199
|
+
const plaintext = Buffer.from(
|
|
200
|
+
JSON.stringify({ privateKey: params.privateKey }),
|
|
201
|
+
"utf8"
|
|
202
|
+
);
|
|
203
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
204
|
+
const authTag = cipher.getAuthTag();
|
|
205
|
+
const addressProof = await account.signMessage({
|
|
206
|
+
message: walletProofMessage({
|
|
207
|
+
version: 1,
|
|
208
|
+
kind: "encrypted-local-eoa",
|
|
209
|
+
address: account.address,
|
|
210
|
+
createdAt
|
|
211
|
+
})
|
|
212
|
+
});
|
|
213
|
+
return {
|
|
214
|
+
version: 1,
|
|
215
|
+
kind: "encrypted-local-eoa",
|
|
216
|
+
address: account.address,
|
|
217
|
+
createdAt,
|
|
218
|
+
addressProof,
|
|
219
|
+
kdf: DEFAULT_KDF,
|
|
220
|
+
crypto: {
|
|
221
|
+
cipher: "aes-256-gcm",
|
|
222
|
+
salt: salt.toString("base64"),
|
|
223
|
+
nonce: nonce.toString("base64"),
|
|
224
|
+
authTag: authTag.toString("base64"),
|
|
225
|
+
ciphertext: ciphertext.toString("base64")
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
async function decryptWalletFile(wallet, passphrase) {
|
|
230
|
+
try {
|
|
231
|
+
const salt = Buffer.from(wallet.crypto.salt, "base64");
|
|
232
|
+
const nonce = Buffer.from(wallet.crypto.nonce, "base64");
|
|
233
|
+
const authTag = Buffer.from(wallet.crypto.authTag, "base64");
|
|
234
|
+
const ciphertext = Buffer.from(wallet.crypto.ciphertext, "base64");
|
|
235
|
+
const key = deriveKey(passphrase, salt, wallet.kdf);
|
|
236
|
+
const decipher = createDecipheriv("aes-256-gcm", key, nonce);
|
|
237
|
+
decipher.setAAD(walletAad(wallet.kdf));
|
|
238
|
+
decipher.setAuthTag(authTag);
|
|
239
|
+
const plaintext = Buffer.concat([
|
|
240
|
+
decipher.update(ciphertext),
|
|
241
|
+
decipher.final()
|
|
242
|
+
]).toString("utf8");
|
|
243
|
+
const parsed = JSON.parse(plaintext);
|
|
244
|
+
if (typeof parsed.privateKey !== "string") {
|
|
245
|
+
throw new Error("wallet payload missing privateKey");
|
|
246
|
+
}
|
|
247
|
+
const privateKey = parsed.privateKey;
|
|
248
|
+
const derivedAddress = privateKeyToAccount(privateKey).address;
|
|
249
|
+
if (derivedAddress.toLowerCase() !== wallet.address.toLowerCase()) {
|
|
250
|
+
throw new Error("wallet address metadata mismatch");
|
|
251
|
+
}
|
|
252
|
+
return privateKey;
|
|
253
|
+
} catch {
|
|
254
|
+
throw new Error("Invalid password or corrupted wallet.");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
var FileEncryptedWalletStore = class {
|
|
258
|
+
walletPath;
|
|
259
|
+
constructor(walletPath) {
|
|
260
|
+
this.walletPath = walletPath;
|
|
261
|
+
}
|
|
262
|
+
exists() {
|
|
263
|
+
return existsSync(this.walletPath);
|
|
264
|
+
}
|
|
265
|
+
async metadata() {
|
|
266
|
+
if (!this.exists()) {
|
|
267
|
+
return void 0;
|
|
268
|
+
}
|
|
269
|
+
assertSecureFileMode(this.walletPath, "Wallet file");
|
|
270
|
+
const wallet = parseWalletFile(
|
|
271
|
+
readFileSync(this.walletPath, "utf8"),
|
|
272
|
+
this.walletPath
|
|
273
|
+
);
|
|
274
|
+
await verifyWalletAddressProof(wallet, this.walletPath);
|
|
275
|
+
return toMetadata(wallet);
|
|
276
|
+
}
|
|
277
|
+
async save(params) {
|
|
278
|
+
const wallet = await encryptWallet(params);
|
|
279
|
+
writeFileAtomicSecure(this.walletPath, JSON.stringify(wallet, null, 2));
|
|
280
|
+
return toMetadata(wallet);
|
|
281
|
+
}
|
|
282
|
+
async decrypt(passphrase) {
|
|
283
|
+
if (!this.exists()) {
|
|
284
|
+
throw new Error("No wallet configured. Run `moneyos init`.");
|
|
285
|
+
}
|
|
286
|
+
assertSecureFileMode(this.walletPath, "Wallet file");
|
|
287
|
+
const wallet = parseWalletFile(
|
|
288
|
+
readFileSync(this.walletPath, "utf8"),
|
|
289
|
+
this.walletPath
|
|
290
|
+
);
|
|
291
|
+
await verifyWalletAddressProof(wallet, this.walletPath);
|
|
292
|
+
return decryptWalletFile(wallet, passphrase);
|
|
293
|
+
}
|
|
294
|
+
async exportData() {
|
|
295
|
+
if (!this.exists()) {
|
|
296
|
+
throw new Error("No wallet configured. Run `moneyos init`.");
|
|
297
|
+
}
|
|
298
|
+
assertSecureFileMode(this.walletPath, "Wallet file");
|
|
299
|
+
const wallet = parseWalletFile(
|
|
300
|
+
readFileSync(this.walletPath, "utf8"),
|
|
301
|
+
this.walletPath
|
|
302
|
+
);
|
|
303
|
+
await verifyWalletAddressProof(wallet, this.walletPath);
|
|
304
|
+
return wallet;
|
|
305
|
+
}
|
|
306
|
+
async restore(data) {
|
|
307
|
+
const wallet = parseWalletFile(JSON.stringify(data), this.walletPath);
|
|
308
|
+
await verifyWalletAddressProof(wallet, this.walletPath);
|
|
309
|
+
writeFileAtomicSecure(this.walletPath, JSON.stringify(wallet, null, 2));
|
|
310
|
+
return toMetadata(wallet);
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
async function readEncryptedWalletFile(path) {
|
|
314
|
+
assertSecureFileMode(path, "Wallet file");
|
|
315
|
+
const wallet = parseWalletFile(readFileSync(path, "utf8"), path);
|
|
316
|
+
await verifyWalletAddressProof(wallet, path);
|
|
317
|
+
return wallet;
|
|
318
|
+
}
|
|
319
|
+
async function verifyEncryptedWalletPassphrase(wallet, passphrase) {
|
|
320
|
+
await verifyWalletAddressProof(wallet, "(in-memory wallet)");
|
|
321
|
+
await decryptWalletFile(wallet, passphrase);
|
|
322
|
+
return toMetadata(wallet);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// src/core/backup-file.ts
|
|
326
|
+
function formatTimestamp(now) {
|
|
327
|
+
const pad = (value) => value.toString().padStart(2, "0");
|
|
328
|
+
return [
|
|
329
|
+
now.getUTCFullYear().toString(),
|
|
330
|
+
pad(now.getUTCMonth() + 1),
|
|
331
|
+
pad(now.getUTCDate()),
|
|
332
|
+
"-",
|
|
333
|
+
pad(now.getUTCHours()),
|
|
334
|
+
pad(now.getUTCMinutes()),
|
|
335
|
+
pad(now.getUTCSeconds())
|
|
336
|
+
].join("");
|
|
337
|
+
}
|
|
338
|
+
var FileBackupProvider = class {
|
|
339
|
+
kind = "file";
|
|
340
|
+
store;
|
|
341
|
+
backupDir;
|
|
342
|
+
constructor(params) {
|
|
343
|
+
this.store = new FileEncryptedWalletStore(params.walletPath);
|
|
344
|
+
this.backupDir = params.backupDir;
|
|
345
|
+
}
|
|
346
|
+
defaultBackupPath(address) {
|
|
347
|
+
return join2(
|
|
348
|
+
this.backupDir,
|
|
349
|
+
`wallet-${address}-${formatTimestamp(/* @__PURE__ */ new Date())}.json`
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
async exportWallet(options = {}) {
|
|
353
|
+
if (options.outPath === "-") {
|
|
354
|
+
throw new Error("Refusing to export an encrypted wallet backup to stdout.");
|
|
355
|
+
}
|
|
356
|
+
const wallet = await this.store.exportData();
|
|
357
|
+
const targetPath = options.outPath ? resolve(options.outPath) : this.defaultBackupPath(wallet.address);
|
|
358
|
+
if (existsSync2(targetPath) && !options.allowOverwrite) {
|
|
359
|
+
throw new Error(
|
|
360
|
+
"Backup file already exists. Re-run with `--force` if you really want to overwrite it."
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
await new FileEncryptedWalletStore(targetPath).restore(wallet);
|
|
364
|
+
return targetPath;
|
|
365
|
+
}
|
|
366
|
+
async restoreWallet(fromPath, options) {
|
|
367
|
+
if (this.store.exists() && !options.allowOverwrite) {
|
|
368
|
+
throw new Error(
|
|
369
|
+
"A wallet already exists. Re-run with `--force` if you really want to overwrite it."
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
const sourcePath = resolve(fromPath);
|
|
373
|
+
const wallet = await readEncryptedWalletFile(sourcePath);
|
|
374
|
+
await verifyEncryptedWalletPassphrase(wallet, options.passphrase);
|
|
375
|
+
return this.store.restore(wallet);
|
|
376
|
+
}
|
|
377
|
+
async status() {
|
|
378
|
+
const exists = this.store.exists();
|
|
379
|
+
const metadata = exists ? await this.store.metadata() : void 0;
|
|
380
|
+
if (!existsSync2(this.backupDir)) {
|
|
381
|
+
return {
|
|
382
|
+
walletPath: this.store.walletPath,
|
|
383
|
+
backupDir: this.backupDir,
|
|
384
|
+
exists,
|
|
385
|
+
backupCount: 0,
|
|
386
|
+
address: metadata?.address
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
const files = readdirSync(this.backupDir).filter((name) => name.endsWith(".json")).map((name) => join2(this.backupDir, name)).sort(
|
|
390
|
+
(a, b) => statSync2(b).mtimeMs - statSync2(a).mtimeMs
|
|
391
|
+
);
|
|
392
|
+
return {
|
|
393
|
+
walletPath: this.store.walletPath,
|
|
394
|
+
backupDir: this.backupDir,
|
|
395
|
+
exists,
|
|
396
|
+
latestBackupPath: files[0],
|
|
397
|
+
backupCount: files.length,
|
|
398
|
+
address: metadata?.address
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
// src/cli/config.ts
|
|
404
|
+
import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
405
|
+
import { createHash } from "crypto";
|
|
406
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
407
|
+
import { homedir } from "os";
|
|
408
|
+
var CONFIG_DIR = join3(homedir(), ".moneyos");
|
|
409
|
+
var CONFIG_FILE = join3(CONFIG_DIR, "config.json");
|
|
410
|
+
function hasRemovedOnePasswordConfig(config) {
|
|
411
|
+
return config.keyStore?.kind === "1password";
|
|
412
|
+
}
|
|
413
|
+
function getRemovedOnePasswordCachedAddress(config) {
|
|
414
|
+
const address = config.keyStore?.address;
|
|
415
|
+
return typeof address === "string" ? address : void 0;
|
|
416
|
+
}
|
|
417
|
+
function getRemovedOnePasswordStorageMessage() {
|
|
418
|
+
return "This repo no longer supports the old 1Password-backed private-key storage path. Re-import the wallet into the local file path with `moneyos init --key <privateKey>`.";
|
|
419
|
+
}
|
|
420
|
+
function loadFileConfig(path = CONFIG_FILE) {
|
|
421
|
+
if (!existsSync3(path)) {
|
|
422
|
+
return {};
|
|
423
|
+
}
|
|
424
|
+
return JSON.parse(readFileSync2(path, "utf-8"));
|
|
425
|
+
}
|
|
426
|
+
function loadConfig() {
|
|
427
|
+
const config = { ...loadFileConfig() };
|
|
428
|
+
if (process.env.MONEYOS_PRIVATE_KEY) {
|
|
429
|
+
config.privateKey = process.env.MONEYOS_PRIVATE_KEY;
|
|
430
|
+
}
|
|
431
|
+
if (process.env.MONEYOS_RPC_URL) {
|
|
432
|
+
config.rpcUrl = process.env.MONEYOS_RPC_URL;
|
|
433
|
+
}
|
|
434
|
+
if (process.env.MONEYOS_CHAIN_ID) {
|
|
435
|
+
const parsed = Number(process.env.MONEYOS_CHAIN_ID);
|
|
436
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
437
|
+
throw new Error(
|
|
438
|
+
`Invalid MONEYOS_CHAIN_ID: "${process.env.MONEYOS_CHAIN_ID}" \u2014 must be a positive integer`
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
config.chainId = parsed;
|
|
442
|
+
}
|
|
443
|
+
return config;
|
|
444
|
+
}
|
|
445
|
+
function saveConfig(config, path = CONFIG_FILE) {
|
|
446
|
+
const safeConfig = { ...config };
|
|
447
|
+
delete safeConfig.privateKey;
|
|
448
|
+
const dir = dirname2(path);
|
|
449
|
+
if (!existsSync3(dir)) {
|
|
450
|
+
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
451
|
+
}
|
|
452
|
+
writeFileSync(path, JSON.stringify(safeConfig, null, 2), {
|
|
453
|
+
mode: 384
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
function getConfigPath() {
|
|
457
|
+
return CONFIG_FILE;
|
|
458
|
+
}
|
|
459
|
+
function getWalletPath(config) {
|
|
460
|
+
return config?.walletPath ?? join3(CONFIG_DIR, "wallet.json");
|
|
461
|
+
}
|
|
462
|
+
function getBackupDir(config) {
|
|
463
|
+
return config?.backupDir ?? join3(CONFIG_DIR, "backups");
|
|
464
|
+
}
|
|
465
|
+
function getSessionSocketPath() {
|
|
466
|
+
if (process.platform === "win32") {
|
|
467
|
+
const suffix = createHash("sha256").update(CONFIG_DIR).digest("hex").slice(0, 16);
|
|
468
|
+
return `\\\\.\\pipe\\moneyos-session-${suffix}`;
|
|
469
|
+
}
|
|
470
|
+
return join3(CONFIG_DIR, "session.sock");
|
|
471
|
+
}
|
|
472
|
+
function getSessionTokenPath() {
|
|
473
|
+
return join3(CONFIG_DIR, "session.token");
|
|
474
|
+
}
|
|
475
|
+
function hasLegacyPlaintextWalletConfig(config) {
|
|
476
|
+
return typeof config.privateKey === "string";
|
|
477
|
+
}
|
|
478
|
+
function getLegacyPlaintextWalletStorageMessage() {
|
|
479
|
+
return "Plaintext local wallet configs are no longer used for runtime access. Run `moneyos init` locally to encrypt your wallet into the new MoneyOS wallet file.";
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// src/cli/prompt.ts
|
|
483
|
+
async function promptHidden(question) {
|
|
484
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
485
|
+
throw new Error(
|
|
486
|
+
"This action requires a local terminal. Run it directly in your terminal, not through a non-interactive agent session."
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
return new Promise((resolve3, reject) => {
|
|
490
|
+
const stdin = process.stdin;
|
|
491
|
+
const stdout = process.stdout;
|
|
492
|
+
let value = "";
|
|
493
|
+
const cleanup = () => {
|
|
494
|
+
stdin.off("data", onData);
|
|
495
|
+
stdin.pause();
|
|
496
|
+
if (typeof stdin.setRawMode === "function") {
|
|
497
|
+
stdin.setRawMode(false);
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
const finish = (result, error) => {
|
|
501
|
+
cleanup();
|
|
502
|
+
stdout.write("\n");
|
|
503
|
+
if (error) {
|
|
504
|
+
reject(error);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
resolve3(result ?? "");
|
|
508
|
+
};
|
|
509
|
+
const onData = (chunk) => {
|
|
510
|
+
const text = chunk.toString("utf8");
|
|
511
|
+
for (const char of text) {
|
|
512
|
+
if (char === "") {
|
|
513
|
+
finish(void 0, new Error("Cancelled."));
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
if (char === "\r" || char === "\n") {
|
|
517
|
+
finish(value);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (char === "\x7F" || char === "\b") {
|
|
521
|
+
value = value.slice(0, -1);
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
value += char;
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
stdout.write(question);
|
|
528
|
+
if (typeof stdin.setRawMode === "function") {
|
|
529
|
+
stdin.setRawMode(true);
|
|
530
|
+
}
|
|
531
|
+
stdin.resume();
|
|
532
|
+
stdin.on("data", onData);
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// src/cli/session.ts
|
|
537
|
+
import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
|
|
538
|
+
import {
|
|
539
|
+
chmodSync as chmodSync2,
|
|
540
|
+
existsSync as existsSync4,
|
|
541
|
+
mkdirSync as mkdirSync3,
|
|
542
|
+
readFileSync as readFileSync3,
|
|
543
|
+
rmSync,
|
|
544
|
+
statSync as statSync3,
|
|
545
|
+
writeFileSync as writeFileSync2
|
|
546
|
+
} from "fs";
|
|
547
|
+
import net from "net";
|
|
548
|
+
import { EOL } from "os";
|
|
549
|
+
import { dirname as dirname3 } from "path";
|
|
550
|
+
import { spawn } from "child_process";
|
|
551
|
+
|
|
552
|
+
// src/core/eoa.ts
|
|
553
|
+
import {
|
|
554
|
+
createPublicClient,
|
|
555
|
+
createWalletClient,
|
|
556
|
+
http
|
|
557
|
+
} from "viem";
|
|
558
|
+
import { arbitrum, mainnet, polygon } from "viem/chains";
|
|
559
|
+
|
|
560
|
+
// packages/core/dist/index.js
|
|
561
|
+
var NATIVE_TOKEN_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
|
|
562
|
+
var tokens = {
|
|
563
|
+
ETH: {
|
|
564
|
+
symbol: "ETH",
|
|
565
|
+
name: "Ether",
|
|
566
|
+
decimals: 18,
|
|
567
|
+
addresses: {
|
|
568
|
+
42161: NATIVE_TOKEN_ADDRESS,
|
|
569
|
+
1: NATIVE_TOKEN_ADDRESS
|
|
570
|
+
}
|
|
571
|
+
},
|
|
572
|
+
POL: {
|
|
573
|
+
symbol: "POL",
|
|
574
|
+
name: "POL",
|
|
575
|
+
decimals: 18,
|
|
576
|
+
addresses: {
|
|
577
|
+
137: NATIVE_TOKEN_ADDRESS
|
|
578
|
+
}
|
|
579
|
+
},
|
|
580
|
+
USDC: {
|
|
581
|
+
symbol: "USDC",
|
|
582
|
+
name: "USD Coin",
|
|
583
|
+
decimals: 6,
|
|
584
|
+
addresses: {
|
|
585
|
+
42161: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
|
|
586
|
+
1: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
|
587
|
+
137: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359"
|
|
588
|
+
}
|
|
589
|
+
},
|
|
590
|
+
USDT: {
|
|
591
|
+
symbol: "USDT",
|
|
592
|
+
name: "Tether USD",
|
|
593
|
+
decimals: 6,
|
|
594
|
+
addresses: {
|
|
595
|
+
42161: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9",
|
|
596
|
+
1: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
|
597
|
+
137: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F"
|
|
598
|
+
}
|
|
599
|
+
},
|
|
600
|
+
RYZE: {
|
|
601
|
+
symbol: "RYZE",
|
|
602
|
+
name: "RYZE",
|
|
603
|
+
decimals: 18,
|
|
604
|
+
addresses: {
|
|
605
|
+
42161: "0x7712da72127d5dD213B621497D6E4899d5989e5C"
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
function getToken(symbol) {
|
|
610
|
+
return tokens[symbol.toUpperCase()];
|
|
611
|
+
}
|
|
612
|
+
function getTokenAddress(symbol, chainId) {
|
|
613
|
+
return getToken(symbol)?.addresses[chainId];
|
|
614
|
+
}
|
|
615
|
+
var chains = {
|
|
616
|
+
arbitrum: {
|
|
617
|
+
id: 42161,
|
|
618
|
+
name: "Arbitrum One",
|
|
619
|
+
rpcUrl: "https://arb1.arbitrum.io/rpc",
|
|
620
|
+
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
|
|
621
|
+
blockExplorer: "https://arbiscan.io"
|
|
622
|
+
},
|
|
623
|
+
ethereum: {
|
|
624
|
+
id: 1,
|
|
625
|
+
name: "Ethereum",
|
|
626
|
+
rpcUrl: "https://eth.public-rpc.com",
|
|
627
|
+
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
|
|
628
|
+
blockExplorer: "https://etherscan.io"
|
|
629
|
+
},
|
|
630
|
+
polygon: {
|
|
631
|
+
id: 137,
|
|
632
|
+
name: "Polygon",
|
|
633
|
+
rpcUrl: "https://polygon-rpc.com",
|
|
634
|
+
nativeCurrency: { name: "POL", symbol: "POL", decimals: 18 },
|
|
635
|
+
blockExplorer: "https://polygonscan.com"
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
var defaultChain = chains.arbitrum;
|
|
639
|
+
function getChain(idOrName) {
|
|
640
|
+
if (typeof idOrName === "number") {
|
|
641
|
+
return Object.values(chains).find((c) => c.id === idOrName);
|
|
642
|
+
}
|
|
643
|
+
return chains[idOrName.toLowerCase()];
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// src/core/signer.ts
|
|
647
|
+
import { nonceManager } from "viem";
|
|
648
|
+
import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
|
|
649
|
+
function privateKeyToManagedAccount(privateKey) {
|
|
650
|
+
return privateKeyToAccount2(privateKey, { nonceManager });
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// src/core/eoa.ts
|
|
654
|
+
var viemChainMap = {
|
|
655
|
+
42161: arbitrum,
|
|
656
|
+
1: mainnet,
|
|
657
|
+
137: polygon
|
|
658
|
+
};
|
|
659
|
+
function getViemChain(chainId) {
|
|
660
|
+
return viemChainMap[chainId];
|
|
661
|
+
}
|
|
662
|
+
var ViemReadClient = class {
|
|
663
|
+
clients = /* @__PURE__ */ new Map();
|
|
664
|
+
config;
|
|
665
|
+
constructor(config) {
|
|
666
|
+
this.config = config;
|
|
667
|
+
}
|
|
668
|
+
getClient(chainId) {
|
|
669
|
+
let client = this.clients.get(chainId);
|
|
670
|
+
if (!client) {
|
|
671
|
+
const chain = getViemChain(chainId);
|
|
672
|
+
const chainInfo = getChain(chainId);
|
|
673
|
+
const rpcUrl = chainId === this.config.defaultChainId ? this.config.rpcUrl : void 0;
|
|
674
|
+
client = createPublicClient({
|
|
675
|
+
chain,
|
|
676
|
+
transport: http(rpcUrl ?? chainInfo?.rpcUrl)
|
|
677
|
+
});
|
|
678
|
+
this.clients.set(chainId, client);
|
|
679
|
+
}
|
|
680
|
+
return client;
|
|
681
|
+
}
|
|
682
|
+
async getBalance(params) {
|
|
683
|
+
const client = this.getClient(params.chainId);
|
|
684
|
+
return client.getBalance({ address: params.address });
|
|
685
|
+
}
|
|
686
|
+
async readContract(params) {
|
|
687
|
+
const client = this.getClient(params.chainId);
|
|
688
|
+
return client.readContract({
|
|
689
|
+
address: params.address,
|
|
690
|
+
abi: params.abi,
|
|
691
|
+
functionName: params.functionName,
|
|
692
|
+
args: params.args
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
var EOAExecutor = class _EOAExecutor {
|
|
697
|
+
mode = "eoa";
|
|
698
|
+
signer;
|
|
699
|
+
walletClients = /* @__PURE__ */ new Map();
|
|
700
|
+
publicClients = /* @__PURE__ */ new Map();
|
|
701
|
+
config;
|
|
702
|
+
constructor(signer, config) {
|
|
703
|
+
this.signer = signer;
|
|
704
|
+
this.config = config;
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Convenience factory: build an EOAExecutor from a raw private key.
|
|
708
|
+
* Equivalent to `new EOAExecutor(privateKeyToAccount(privateKey), config)`.
|
|
709
|
+
* Kept as a helper so the common "I have a hex key" path stays one line
|
|
710
|
+
* while the constructor itself takes a viem `Account` to accommodate
|
|
711
|
+
* future keystore-backed signers (hardware, KMS, MPC).
|
|
712
|
+
*
|
|
713
|
+
* Attach viem's nonce manager so back-to-back live transactions use a
|
|
714
|
+
* pending-aware nonce source instead of relying on RPC fill behavior.
|
|
715
|
+
*/
|
|
716
|
+
static fromPrivateKey(privateKey, config) {
|
|
717
|
+
return new _EOAExecutor(privateKeyToManagedAccount(privateKey), config);
|
|
718
|
+
}
|
|
719
|
+
getWalletClient(chainId) {
|
|
720
|
+
let client = this.walletClients.get(chainId);
|
|
721
|
+
if (!client) {
|
|
722
|
+
const chain = getViemChain(chainId);
|
|
723
|
+
const chainInfo = getChain(chainId);
|
|
724
|
+
const rpcUrl = chainId === this.config.defaultChainId ? this.config.rpcUrl : void 0;
|
|
725
|
+
client = createWalletClient({
|
|
726
|
+
account: this.signer,
|
|
727
|
+
chain,
|
|
728
|
+
transport: http(rpcUrl ?? chainInfo?.rpcUrl)
|
|
729
|
+
});
|
|
730
|
+
this.walletClients.set(chainId, client);
|
|
731
|
+
}
|
|
732
|
+
return client;
|
|
733
|
+
}
|
|
734
|
+
getPublicClient(chainId) {
|
|
735
|
+
let client = this.publicClients.get(chainId);
|
|
736
|
+
if (!client) {
|
|
737
|
+
const chain = getViemChain(chainId);
|
|
738
|
+
const chainInfo = getChain(chainId);
|
|
739
|
+
const rpcUrl = chainId === this.config.defaultChainId ? this.config.rpcUrl : void 0;
|
|
740
|
+
client = createPublicClient({
|
|
741
|
+
chain,
|
|
742
|
+
transport: http(rpcUrl ?? chainInfo?.rpcUrl)
|
|
743
|
+
});
|
|
744
|
+
this.publicClients.set(chainId, client);
|
|
745
|
+
}
|
|
746
|
+
return client;
|
|
747
|
+
}
|
|
748
|
+
getAddress() {
|
|
749
|
+
return this.signer.address;
|
|
750
|
+
}
|
|
751
|
+
async send(call) {
|
|
752
|
+
const walletClient = this.getWalletClient(call.chainId);
|
|
753
|
+
const hash = await walletClient.sendTransaction({
|
|
754
|
+
account: walletClient.account,
|
|
755
|
+
to: call.to,
|
|
756
|
+
data: call.data,
|
|
757
|
+
value: call.value ?? 0n,
|
|
758
|
+
chain: walletClient.chain
|
|
759
|
+
});
|
|
760
|
+
const publicClient = this.getPublicClient(call.chainId);
|
|
761
|
+
const receipt = await publicClient.waitForTransactionReceipt({ hash });
|
|
762
|
+
if (receipt.status !== "success") {
|
|
763
|
+
throw new Error(
|
|
764
|
+
`Transaction ${hash} reverted on chain ${call.chainId} (block ${receipt.blockNumber}).`
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
return { hash, chainId: call.chainId };
|
|
768
|
+
}
|
|
769
|
+
capabilities() {
|
|
770
|
+
return {
|
|
771
|
+
sponsoredGas: false,
|
|
772
|
+
batching: false,
|
|
773
|
+
simulation: false
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
// src/cli/session.ts
|
|
779
|
+
var SESSION_CONTROL_TIMEOUT_MS = 750;
|
|
780
|
+
var SESSION_SEND_TIMEOUT_MS = 6e4;
|
|
781
|
+
var MAX_MESSAGE_BYTES = 32 * 1024;
|
|
782
|
+
var SECURE_DIR_MODE = 448;
|
|
783
|
+
var SECURE_FILE_MODE2 = 384;
|
|
784
|
+
var SERVER_SOCKET_TIMEOUT_MS = 5e3;
|
|
785
|
+
var SESSION_SHUTDOWN_TIMEOUT_MS = 15e3;
|
|
786
|
+
function isWindowsPipe(path) {
|
|
787
|
+
return path.startsWith("\\\\.\\pipe\\");
|
|
788
|
+
}
|
|
789
|
+
function removeFileIfPresent(path) {
|
|
790
|
+
if (existsSync4(path)) {
|
|
791
|
+
rmSync(path, { force: true });
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
function ensureSecureParent(path) {
|
|
795
|
+
if (isWindowsPipe(path)) {
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
const dir = dirname3(path);
|
|
799
|
+
if (!existsSync4(dir)) {
|
|
800
|
+
mkdirSync3(dir, { recursive: true, mode: SECURE_DIR_MODE });
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
const mode = statSync3(dir).mode & 511;
|
|
804
|
+
if ((mode & 63) !== 0) {
|
|
805
|
+
throw new Error(
|
|
806
|
+
`MoneyOS directory ${dir} has insecure permissions (${mode.toString(8)}). Restrict it to 700 before continuing.`
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
function createRequestId() {
|
|
811
|
+
return `${Date.now()}-${Math.random().toString(16).slice(2, 10)}`;
|
|
812
|
+
}
|
|
813
|
+
function loadSessionToken(tokenPath) {
|
|
814
|
+
try {
|
|
815
|
+
return readFileSync3(tokenPath, "utf8").trim();
|
|
816
|
+
} catch {
|
|
817
|
+
throw new Error("No active local MoneyOS session found.");
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
function writeSecureToken(tokenPath, token) {
|
|
821
|
+
ensureSecureParent(tokenPath);
|
|
822
|
+
writeFileSync2(tokenPath, `${token}
|
|
823
|
+
`, { mode: SECURE_FILE_MODE2 });
|
|
824
|
+
chmodSync2(tokenPath, SECURE_FILE_MODE2);
|
|
825
|
+
}
|
|
826
|
+
function sessionFilesGone(socketPath, tokenPath) {
|
|
827
|
+
const tokenMissing = !existsSync4(tokenPath);
|
|
828
|
+
const socketMissing = isWindowsPipe(socketPath) ? true : !existsSync4(socketPath);
|
|
829
|
+
return tokenMissing && socketMissing;
|
|
830
|
+
}
|
|
831
|
+
async function waitForSessionShutdown(socketPath, tokenPath, timeoutMs = SESSION_SHUTDOWN_TIMEOUT_MS) {
|
|
832
|
+
const startedAt = Date.now();
|
|
833
|
+
while (!sessionFilesGone(socketPath, tokenPath)) {
|
|
834
|
+
if (Date.now() - startedAt > timeoutMs) {
|
|
835
|
+
throw new Error(
|
|
836
|
+
"Timed out waiting for the previous MoneyOS session to shut down cleanly."
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
await new Promise((resolve3) => setTimeout(resolve3, 25));
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
function tokensMatch(expectedToken, receivedToken) {
|
|
843
|
+
const expected = Buffer.from(expectedToken, "utf8");
|
|
844
|
+
const received = Buffer.from(receivedToken, "utf8");
|
|
845
|
+
return expected.length === received.length && timingSafeEqual(expected, received);
|
|
846
|
+
}
|
|
847
|
+
function readLine(socket) {
|
|
848
|
+
return new Promise((resolve3, reject) => {
|
|
849
|
+
let buffer = "";
|
|
850
|
+
const onData = (chunk) => {
|
|
851
|
+
buffer += chunk.toString("utf8");
|
|
852
|
+
if (buffer.length > MAX_MESSAGE_BYTES) {
|
|
853
|
+
cleanup();
|
|
854
|
+
reject(new Error("Session response exceeded the maximum allowed size."));
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
const index = buffer.indexOf("\n");
|
|
858
|
+
if (index >= 0) {
|
|
859
|
+
cleanup();
|
|
860
|
+
resolve3(buffer.slice(0, index));
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
const onError = (error) => {
|
|
864
|
+
cleanup();
|
|
865
|
+
reject(error);
|
|
866
|
+
};
|
|
867
|
+
const onClose = () => {
|
|
868
|
+
cleanup();
|
|
869
|
+
reject(new Error("Session daemon closed the connection unexpectedly."));
|
|
870
|
+
};
|
|
871
|
+
const cleanup = () => {
|
|
872
|
+
socket.off("data", onData);
|
|
873
|
+
socket.off("error", onError);
|
|
874
|
+
socket.off("close", onClose);
|
|
875
|
+
};
|
|
876
|
+
socket.on("data", onData);
|
|
877
|
+
socket.once("error", onError);
|
|
878
|
+
socket.once("close", onClose);
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
async function sendSessionRequest(socketPath, tokenPath, request, timeoutMs = SESSION_CONTROL_TIMEOUT_MS) {
|
|
882
|
+
const fullRequest = {
|
|
883
|
+
...request,
|
|
884
|
+
token: loadSessionToken(tokenPath)
|
|
885
|
+
};
|
|
886
|
+
const socket = net.createConnection(socketPath);
|
|
887
|
+
return new Promise((resolve3, reject) => {
|
|
888
|
+
let settled = false;
|
|
889
|
+
const timer = setTimeout(() => {
|
|
890
|
+
settled = true;
|
|
891
|
+
socket.destroy();
|
|
892
|
+
reject(new Error("Timed out waiting for local MoneyOS session."));
|
|
893
|
+
}, timeoutMs);
|
|
894
|
+
const cleanup = () => {
|
|
895
|
+
clearTimeout(timer);
|
|
896
|
+
socket.removeAllListeners();
|
|
897
|
+
};
|
|
898
|
+
socket.once("connect", async () => {
|
|
899
|
+
try {
|
|
900
|
+
socket.write(`${JSON.stringify(fullRequest)}${EOL}`);
|
|
901
|
+
const line = await readLine(socket);
|
|
902
|
+
settled = true;
|
|
903
|
+
cleanup();
|
|
904
|
+
socket.end();
|
|
905
|
+
resolve3(JSON.parse(line));
|
|
906
|
+
} catch (error) {
|
|
907
|
+
settled = true;
|
|
908
|
+
cleanup();
|
|
909
|
+
socket.destroy();
|
|
910
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
911
|
+
}
|
|
912
|
+
});
|
|
913
|
+
socket.once("error", (error) => {
|
|
914
|
+
if (settled) {
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
settled = true;
|
|
918
|
+
cleanup();
|
|
919
|
+
reject(error);
|
|
920
|
+
});
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
async function getSessionStatus(socketPath, tokenPath) {
|
|
924
|
+
try {
|
|
925
|
+
const response = await sendSessionRequest(
|
|
926
|
+
socketPath,
|
|
927
|
+
tokenPath,
|
|
928
|
+
{
|
|
929
|
+
id: createRequestId(),
|
|
930
|
+
type: "status"
|
|
931
|
+
},
|
|
932
|
+
SESSION_CONTROL_TIMEOUT_MS
|
|
933
|
+
);
|
|
934
|
+
return response.ok ? response.result : void 0;
|
|
935
|
+
} catch {
|
|
936
|
+
removeFileIfPresent(socketPath);
|
|
937
|
+
removeFileIfPresent(tokenPath);
|
|
938
|
+
return void 0;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
async function lockSession(socketPath, tokenPath) {
|
|
942
|
+
try {
|
|
943
|
+
const response = await sendSessionRequest(
|
|
944
|
+
socketPath,
|
|
945
|
+
tokenPath,
|
|
946
|
+
{
|
|
947
|
+
id: createRequestId(),
|
|
948
|
+
type: "lock"
|
|
949
|
+
},
|
|
950
|
+
SESSION_CONTROL_TIMEOUT_MS
|
|
951
|
+
);
|
|
952
|
+
return response.ok;
|
|
953
|
+
} catch {
|
|
954
|
+
removeFileIfPresent(socketPath);
|
|
955
|
+
removeFileIfPresent(tokenPath);
|
|
956
|
+
return false;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
var SessionExecutionClient = class {
|
|
960
|
+
mode = "eoa";
|
|
961
|
+
socketPath;
|
|
962
|
+
tokenPath;
|
|
963
|
+
address;
|
|
964
|
+
constructor(params) {
|
|
965
|
+
this.socketPath = params.socketPath;
|
|
966
|
+
this.tokenPath = params.tokenPath;
|
|
967
|
+
this.address = params.address;
|
|
968
|
+
}
|
|
969
|
+
getAddress() {
|
|
970
|
+
return this.address;
|
|
971
|
+
}
|
|
972
|
+
async send(call) {
|
|
973
|
+
const response = await sendSessionRequest(
|
|
974
|
+
this.socketPath,
|
|
975
|
+
this.tokenPath,
|
|
976
|
+
{
|
|
977
|
+
id: createRequestId(),
|
|
978
|
+
type: "send",
|
|
979
|
+
params: {
|
|
980
|
+
to: call.to,
|
|
981
|
+
chainId: call.chainId,
|
|
982
|
+
data: call.data,
|
|
983
|
+
value: call.value?.toString()
|
|
984
|
+
}
|
|
985
|
+
},
|
|
986
|
+
SESSION_SEND_TIMEOUT_MS
|
|
987
|
+
);
|
|
988
|
+
if (!response.ok) {
|
|
989
|
+
throw new Error(response.error);
|
|
990
|
+
}
|
|
991
|
+
return response.result;
|
|
992
|
+
}
|
|
993
|
+
capabilities() {
|
|
994
|
+
return {
|
|
995
|
+
sponsoredGas: false,
|
|
996
|
+
batching: false,
|
|
997
|
+
simulation: false
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
};
|
|
1001
|
+
async function startSessionServer(start, hooks = {}) {
|
|
1002
|
+
ensureSecureParent(start.tokenPath);
|
|
1003
|
+
ensureSecureParent(start.socketPath);
|
|
1004
|
+
removeFileIfPresent(start.socketPath);
|
|
1005
|
+
removeFileIfPresent(start.tokenPath);
|
|
1006
|
+
const signer = privateKeyToManagedAccount(start.privateKey);
|
|
1007
|
+
const executor = hooks.executor ?? new EOAExecutor(signer, {
|
|
1008
|
+
defaultChainId: start.chainId,
|
|
1009
|
+
rpcUrl: start.rpcUrl
|
|
1010
|
+
});
|
|
1011
|
+
const expiresAt = new Date(Date.now() + start.ttlMs);
|
|
1012
|
+
const token = randomBytes2(32).toString("hex");
|
|
1013
|
+
writeSecureToken(start.tokenPath, token);
|
|
1014
|
+
let closed = false;
|
|
1015
|
+
const close = async () => {
|
|
1016
|
+
if (closed) return;
|
|
1017
|
+
closed = true;
|
|
1018
|
+
await new Promise((resolve3) => {
|
|
1019
|
+
server.close(() => {
|
|
1020
|
+
removeFileIfPresent(start.socketPath);
|
|
1021
|
+
removeFileIfPresent(start.tokenPath);
|
|
1022
|
+
hooks.onExit?.();
|
|
1023
|
+
resolve3();
|
|
1024
|
+
});
|
|
1025
|
+
});
|
|
1026
|
+
};
|
|
1027
|
+
const server = net.createServer((socket) => {
|
|
1028
|
+
let buffer = "";
|
|
1029
|
+
socket.setTimeout(SERVER_SOCKET_TIMEOUT_MS, () => {
|
|
1030
|
+
socket.destroy();
|
|
1031
|
+
});
|
|
1032
|
+
socket.on("data", async (chunk) => {
|
|
1033
|
+
buffer += chunk.toString("utf8");
|
|
1034
|
+
if (buffer.length > MAX_MESSAGE_BYTES) {
|
|
1035
|
+
socket.write(
|
|
1036
|
+
`${JSON.stringify({
|
|
1037
|
+
id: "unknown",
|
|
1038
|
+
ok: false,
|
|
1039
|
+
error: "Session request exceeded the maximum allowed size."
|
|
1040
|
+
})}${EOL}`
|
|
1041
|
+
);
|
|
1042
|
+
socket.end();
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
const index = buffer.indexOf("\n");
|
|
1046
|
+
if (index < 0) {
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
const raw = buffer.slice(0, index);
|
|
1050
|
+
buffer = buffer.slice(index + 1);
|
|
1051
|
+
let request;
|
|
1052
|
+
try {
|
|
1053
|
+
request = JSON.parse(raw);
|
|
1054
|
+
} catch {
|
|
1055
|
+
socket.write(
|
|
1056
|
+
`${JSON.stringify({
|
|
1057
|
+
id: "unknown",
|
|
1058
|
+
ok: false,
|
|
1059
|
+
error: "Invalid session request."
|
|
1060
|
+
})}${EOL}`
|
|
1061
|
+
);
|
|
1062
|
+
socket.end();
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
const respond = (response) => {
|
|
1066
|
+
socket.write(`${JSON.stringify(response)}${EOL}`);
|
|
1067
|
+
socket.end();
|
|
1068
|
+
};
|
|
1069
|
+
try {
|
|
1070
|
+
if (!tokensMatch(token, String(request.token ?? ""))) {
|
|
1071
|
+
respond({
|
|
1072
|
+
id: request.id,
|
|
1073
|
+
ok: false,
|
|
1074
|
+
error: "Session authentication failed."
|
|
1075
|
+
});
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
if (request.type === "status") {
|
|
1079
|
+
respond({
|
|
1080
|
+
id: request.id,
|
|
1081
|
+
ok: true,
|
|
1082
|
+
result: {
|
|
1083
|
+
address: executor.getAddress(),
|
|
1084
|
+
expiresAt: expiresAt.toISOString()
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
if (request.type === "lock") {
|
|
1090
|
+
respond({
|
|
1091
|
+
id: request.id,
|
|
1092
|
+
ok: true,
|
|
1093
|
+
result: { locked: true }
|
|
1094
|
+
});
|
|
1095
|
+
setImmediate(() => {
|
|
1096
|
+
void close();
|
|
1097
|
+
});
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
socket.setTimeout(SESSION_SEND_TIMEOUT_MS, () => {
|
|
1101
|
+
socket.destroy();
|
|
1102
|
+
});
|
|
1103
|
+
const result = await executor.send({
|
|
1104
|
+
to: request.params.to,
|
|
1105
|
+
chainId: request.params.chainId,
|
|
1106
|
+
data: request.params.data,
|
|
1107
|
+
value: request.params.value !== void 0 ? BigInt(request.params.value) : void 0
|
|
1108
|
+
});
|
|
1109
|
+
respond({
|
|
1110
|
+
id: request.id,
|
|
1111
|
+
ok: true,
|
|
1112
|
+
result
|
|
1113
|
+
});
|
|
1114
|
+
} catch (error) {
|
|
1115
|
+
respond({
|
|
1116
|
+
id: request.id,
|
|
1117
|
+
ok: false,
|
|
1118
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
});
|
|
1122
|
+
});
|
|
1123
|
+
await new Promise((resolve3, reject) => {
|
|
1124
|
+
server.once("error", (error) => {
|
|
1125
|
+
hooks.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
1126
|
+
reject(error);
|
|
1127
|
+
});
|
|
1128
|
+
server.listen(start.socketPath, () => {
|
|
1129
|
+
if (!isWindowsPipe(start.socketPath)) {
|
|
1130
|
+
chmodSync2(start.socketPath, SECURE_FILE_MODE2);
|
|
1131
|
+
}
|
|
1132
|
+
resolve3();
|
|
1133
|
+
});
|
|
1134
|
+
});
|
|
1135
|
+
const timer = setTimeout(() => {
|
|
1136
|
+
void close();
|
|
1137
|
+
}, start.ttlMs);
|
|
1138
|
+
timer.unref();
|
|
1139
|
+
return {
|
|
1140
|
+
address: executor.getAddress(),
|
|
1141
|
+
expiresAt: expiresAt.toISOString(),
|
|
1142
|
+
close
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
async function startDetachedSessionDaemon(params) {
|
|
1146
|
+
const existing = await getSessionStatus(params.socketPath, params.tokenPath);
|
|
1147
|
+
if (existing) {
|
|
1148
|
+
const locked = await lockSession(params.socketPath, params.tokenPath);
|
|
1149
|
+
if (!locked && !sessionFilesGone(params.socketPath, params.tokenPath)) {
|
|
1150
|
+
throw new Error(
|
|
1151
|
+
"Failed to replace the existing MoneyOS session. Run `moneyos auth lock` and try again."
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
if (locked) {
|
|
1155
|
+
await waitForSessionShutdown(params.socketPath, params.tokenPath);
|
|
1156
|
+
}
|
|
1157
|
+
} else {
|
|
1158
|
+
removeFileIfPresent(params.socketPath);
|
|
1159
|
+
removeFileIfPresent(params.tokenPath);
|
|
1160
|
+
}
|
|
1161
|
+
return new Promise((resolve3, reject) => {
|
|
1162
|
+
const child = spawn(
|
|
1163
|
+
process.execPath,
|
|
1164
|
+
[...process.execArgv, process.argv[1], "__session-daemon"],
|
|
1165
|
+
{
|
|
1166
|
+
detached: true,
|
|
1167
|
+
stdio: ["ignore", "ignore", "ignore", "ipc"]
|
|
1168
|
+
}
|
|
1169
|
+
);
|
|
1170
|
+
let settled = false;
|
|
1171
|
+
const finish = (error, status) => {
|
|
1172
|
+
if (settled) return;
|
|
1173
|
+
settled = true;
|
|
1174
|
+
child.removeAllListeners();
|
|
1175
|
+
if (error) {
|
|
1176
|
+
reject(error);
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
resolve3(status);
|
|
1180
|
+
};
|
|
1181
|
+
child.once("error", (error) => finish(error));
|
|
1182
|
+
child.once("exit", (code) => {
|
|
1183
|
+
if (!settled) {
|
|
1184
|
+
finish(
|
|
1185
|
+
new Error(
|
|
1186
|
+
`Session daemon exited unexpectedly with code ${code ?? "unknown"}.`
|
|
1187
|
+
)
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
});
|
|
1191
|
+
child.on("message", (message) => {
|
|
1192
|
+
const payload = message;
|
|
1193
|
+
if (payload?.type === "ready") {
|
|
1194
|
+
child.disconnect();
|
|
1195
|
+
child.unref();
|
|
1196
|
+
finish(void 0, {
|
|
1197
|
+
address: payload.address,
|
|
1198
|
+
expiresAt: payload.expiresAt
|
|
1199
|
+
});
|
|
1200
|
+
} else if (payload?.type === "error") {
|
|
1201
|
+
finish(new Error(payload.error));
|
|
1202
|
+
}
|
|
1203
|
+
});
|
|
1204
|
+
child.send(params);
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
async function runSessionDaemonProcess() {
|
|
1208
|
+
if (!process.send) {
|
|
1209
|
+
throw new Error("Session daemon must be started through MoneyOS.");
|
|
1210
|
+
}
|
|
1211
|
+
const start = await new Promise((resolve3, reject) => {
|
|
1212
|
+
const timeout = setTimeout(() => {
|
|
1213
|
+
reject(new Error("Session daemon did not receive startup parameters."));
|
|
1214
|
+
}, 1e3);
|
|
1215
|
+
process.once("message", (message) => {
|
|
1216
|
+
clearTimeout(timeout);
|
|
1217
|
+
const payload = message;
|
|
1218
|
+
if (payload?.type !== "start") {
|
|
1219
|
+
reject(new Error("Session daemon received invalid startup message."));
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
resolve3(payload);
|
|
1223
|
+
});
|
|
1224
|
+
});
|
|
1225
|
+
const shutdown = (code = 0) => {
|
|
1226
|
+
removeFileIfPresent(start.socketPath);
|
|
1227
|
+
removeFileIfPresent(start.tokenPath);
|
|
1228
|
+
process.exit(code);
|
|
1229
|
+
};
|
|
1230
|
+
const handle = await startSessionServer(start, {
|
|
1231
|
+
onError: (error) => {
|
|
1232
|
+
process.send?.({
|
|
1233
|
+
type: "error",
|
|
1234
|
+
error: error.message
|
|
1235
|
+
});
|
|
1236
|
+
shutdown(1);
|
|
1237
|
+
},
|
|
1238
|
+
onExit: () => shutdown()
|
|
1239
|
+
});
|
|
1240
|
+
process.send?.({
|
|
1241
|
+
type: "ready",
|
|
1242
|
+
address: handle.address,
|
|
1243
|
+
expiresAt: handle.expiresAt
|
|
1244
|
+
});
|
|
1245
|
+
process.on("SIGTERM", () => {
|
|
1246
|
+
void handle.close().then(() => shutdown());
|
|
1247
|
+
});
|
|
1248
|
+
process.on("SIGINT", () => {
|
|
1249
|
+
void handle.close().then(() => shutdown());
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// src/cli/commands/init.ts
|
|
1254
|
+
function parseChainId(value, fallback) {
|
|
1255
|
+
if (!value) return fallback;
|
|
1256
|
+
const parsed = Number(value);
|
|
1257
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
1258
|
+
throw new Error(`Invalid chain ID: "${value}"`);
|
|
1259
|
+
}
|
|
1260
|
+
return parsed;
|
|
1261
|
+
}
|
|
1262
|
+
var initCommand = new Command("init").description("Initialize MoneyOS with a new or imported encrypted wallet").option("-k, --key <privateKey>", "Import an existing private key").option("--force", "Overwrite the existing encrypted wallet").option("--chain <chainId>", "Default chain ID (default: 42161 Arbitrum)").option("--rpc <url>", "Custom RPC URL").action(async (options) => {
|
|
1263
|
+
const existing = loadFileConfig();
|
|
1264
|
+
const walletPath = getWalletPath(existing);
|
|
1265
|
+
const backupDir = getBackupDir(existing);
|
|
1266
|
+
const wallet = new FileEncryptedWalletStore(walletPath);
|
|
1267
|
+
if (wallet.exists() && !options.force) {
|
|
1268
|
+
const metadata = await wallet.metadata();
|
|
1269
|
+
console.log(`Already initialized.`);
|
|
1270
|
+
if (metadata?.address) {
|
|
1271
|
+
console.log(`Address: ${metadata.address}`);
|
|
1272
|
+
}
|
|
1273
|
+
console.log(`Wallet: ${walletPath}`);
|
|
1274
|
+
console.log(`Config: ${getConfigPath()}`);
|
|
1275
|
+
console.log(`
|
|
1276
|
+
To reinitialize, run: moneyos init --force --key <privateKey>`);
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
if (hasRemovedOnePasswordConfig(existing) && !options.key) {
|
|
1280
|
+
console.error(getRemovedOnePasswordStorageMessage());
|
|
1281
|
+
console.error(`Config: ${getConfigPath()}`);
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
try {
|
|
1285
|
+
const privateKey = options.key ?? (hasLegacyPlaintextWalletConfig(existing) ? existing.privateKey : generatePrivateKey());
|
|
1286
|
+
const account = privateKeyToAccount3(privateKey);
|
|
1287
|
+
const chainId = parseChainId(options.chain, existing.chainId ?? 42161);
|
|
1288
|
+
const rpcUrl = options.rpc ?? existing.rpcUrl;
|
|
1289
|
+
const passphrase = await promptHidden("Choose wallet password: ");
|
|
1290
|
+
if (passphrase.length < 8) {
|
|
1291
|
+
throw new Error("Wallet password must be at least 8 characters long.");
|
|
1292
|
+
}
|
|
1293
|
+
const confirmPassphrase = await promptHidden("Confirm wallet password: ");
|
|
1294
|
+
if (passphrase !== confirmPassphrase) {
|
|
1295
|
+
throw new Error("Wallet password confirmation did not match.");
|
|
1296
|
+
}
|
|
1297
|
+
await wallet.save({
|
|
1298
|
+
privateKey,
|
|
1299
|
+
passphrase
|
|
1300
|
+
});
|
|
1301
|
+
await lockSession(getSessionSocketPath(), getSessionTokenPath());
|
|
1302
|
+
saveConfig({
|
|
1303
|
+
chainId,
|
|
1304
|
+
rpcUrl,
|
|
1305
|
+
walletPath: existing.walletPath,
|
|
1306
|
+
backupDir: existing.backupDir
|
|
1307
|
+
});
|
|
1308
|
+
const backupProvider = new FileBackupProvider({
|
|
1309
|
+
walletPath,
|
|
1310
|
+
backupDir
|
|
1311
|
+
});
|
|
1312
|
+
const backupPath = await backupProvider.exportWallet();
|
|
1313
|
+
console.log(`MoneyOS initialized.`);
|
|
1314
|
+
console.log(`Address: ${account.address}`);
|
|
1315
|
+
console.log(`Wallet: ${walletPath}`);
|
|
1316
|
+
console.log(`Config: ${getConfigPath()}`);
|
|
1317
|
+
console.log(`Backup: ${backupPath}`);
|
|
1318
|
+
console.log(
|
|
1319
|
+
`
|
|
1320
|
+
Save your wallet password in your password manager of choice. MoneyOS does not store or sync it for you.`
|
|
1321
|
+
);
|
|
1322
|
+
if (hasLegacyPlaintextWalletConfig(existing) && !options.key) {
|
|
1323
|
+
console.log(`
|
|
1324
|
+
Imported legacy plaintext wallet into the encrypted wallet file.`);
|
|
1325
|
+
console.log(getLegacyPlaintextWalletStorageMessage());
|
|
1326
|
+
} else if (!options.key) {
|
|
1327
|
+
console.log(
|
|
1328
|
+
`
|
|
1329
|
+
This is a new account. Fund it before sending transactions.`
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
} catch (error) {
|
|
1333
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1334
|
+
process.exitCode = 1;
|
|
1335
|
+
}
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
// src/cli/commands/balance.ts
|
|
1339
|
+
import { Command as Command2 } from "commander";
|
|
1340
|
+
|
|
1341
|
+
// src/core/client.ts
|
|
1342
|
+
import {
|
|
1343
|
+
formatUnits as formatUnits2,
|
|
1344
|
+
parseUnits as parseUnits2,
|
|
1345
|
+
encodeFunctionData as encodeFunctionData2
|
|
1346
|
+
} from "viem";
|
|
1347
|
+
|
|
1348
|
+
// src/tools/swap.ts
|
|
1349
|
+
import {
|
|
1350
|
+
formatUnits,
|
|
1351
|
+
parseUnits,
|
|
1352
|
+
encodeFunctionData
|
|
1353
|
+
} from "viem";
|
|
1354
|
+
var ERC20_ABI = [
|
|
1355
|
+
{
|
|
1356
|
+
name: "approve",
|
|
1357
|
+
type: "function",
|
|
1358
|
+
stateMutability: "nonpayable",
|
|
1359
|
+
inputs: [
|
|
1360
|
+
{ name: "spender", type: "address" },
|
|
1361
|
+
{ name: "amount", type: "uint256" }
|
|
1362
|
+
],
|
|
1363
|
+
outputs: [{ name: "", type: "bool" }]
|
|
1364
|
+
},
|
|
1365
|
+
{
|
|
1366
|
+
name: "allowance",
|
|
1367
|
+
type: "function",
|
|
1368
|
+
stateMutability: "view",
|
|
1369
|
+
inputs: [
|
|
1370
|
+
{ name: "owner", type: "address" },
|
|
1371
|
+
{ name: "spender", type: "address" }
|
|
1372
|
+
],
|
|
1373
|
+
outputs: [{ name: "", type: "uint256" }]
|
|
1374
|
+
}
|
|
1375
|
+
];
|
|
1376
|
+
async function executeSwap(input, ctx) {
|
|
1377
|
+
const { tokenIn, tokenOut, amount, provider, chainId, slippage } = input;
|
|
1378
|
+
const { read, execute, assets } = ctx;
|
|
1379
|
+
const sender = execute.getAddress();
|
|
1380
|
+
const tokenInAddress = assets.getTokenAddress(tokenIn, chainId);
|
|
1381
|
+
const tokenOutAddress = assets.getTokenAddress(tokenOut, chainId);
|
|
1382
|
+
if (!tokenInAddress) {
|
|
1383
|
+
throw new Error(`Token ${tokenIn} not found on chain ${chainId}`);
|
|
1384
|
+
}
|
|
1385
|
+
if (!tokenOutAddress) {
|
|
1386
|
+
throw new Error(`Token ${tokenOut} not found on chain ${chainId}`);
|
|
1387
|
+
}
|
|
1388
|
+
const tokenInInfo = assets.getToken(tokenIn);
|
|
1389
|
+
const amountWei = parseUnits(amount, tokenInInfo.decimals);
|
|
1390
|
+
const quote = await provider.getQuote({
|
|
1391
|
+
chainId,
|
|
1392
|
+
tokenIn: tokenInAddress,
|
|
1393
|
+
tokenOut: tokenOutAddress,
|
|
1394
|
+
amount: amountWei,
|
|
1395
|
+
sender,
|
|
1396
|
+
slippage
|
|
1397
|
+
});
|
|
1398
|
+
const calldata = await provider.getCalldata(quote);
|
|
1399
|
+
const isNativeIn = tokenInAddress === assets.nativeTokenAddress;
|
|
1400
|
+
if (!isNativeIn) {
|
|
1401
|
+
const currentAllowance = await read.readContract({
|
|
1402
|
+
address: tokenInAddress,
|
|
1403
|
+
abi: ERC20_ABI,
|
|
1404
|
+
functionName: "allowance",
|
|
1405
|
+
args: [sender, calldata.to],
|
|
1406
|
+
chainId
|
|
1407
|
+
});
|
|
1408
|
+
if (currentAllowance < amountWei) {
|
|
1409
|
+
const approveData = encodeFunctionData({
|
|
1410
|
+
abi: ERC20_ABI,
|
|
1411
|
+
functionName: "approve",
|
|
1412
|
+
args: [calldata.to, amountWei]
|
|
1413
|
+
});
|
|
1414
|
+
await execute.send({ to: tokenInAddress, data: approveData, chainId });
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
const result = await execute.send({
|
|
1418
|
+
to: calldata.to,
|
|
1419
|
+
data: calldata.data,
|
|
1420
|
+
value: isNativeIn ? amountWei : calldata.value,
|
|
1421
|
+
chainId
|
|
1422
|
+
});
|
|
1423
|
+
const tokenOutInfo = assets.getToken(tokenOut);
|
|
1424
|
+
return {
|
|
1425
|
+
hash: result.hash,
|
|
1426
|
+
tokenIn: tokenInInfo.symbol,
|
|
1427
|
+
tokenOut: tokenOutInfo.symbol,
|
|
1428
|
+
amountIn: amount,
|
|
1429
|
+
amountOut: formatUnits(BigInt(quote.expectedOut), tokenOutInfo.decimals),
|
|
1430
|
+
chainId
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// src/core/client.ts
|
|
1435
|
+
var ERC20_ABI2 = [
|
|
1436
|
+
{
|
|
1437
|
+
name: "balanceOf",
|
|
1438
|
+
type: "function",
|
|
1439
|
+
stateMutability: "view",
|
|
1440
|
+
inputs: [{ name: "account", type: "address" }],
|
|
1441
|
+
outputs: [{ name: "", type: "uint256" }]
|
|
1442
|
+
},
|
|
1443
|
+
{
|
|
1444
|
+
name: "transfer",
|
|
1445
|
+
type: "function",
|
|
1446
|
+
stateMutability: "nonpayable",
|
|
1447
|
+
inputs: [
|
|
1448
|
+
{ name: "to", type: "address" },
|
|
1449
|
+
{ name: "amount", type: "uint256" }
|
|
1450
|
+
],
|
|
1451
|
+
outputs: [{ name: "", type: "bool" }]
|
|
1452
|
+
},
|
|
1453
|
+
{
|
|
1454
|
+
name: "decimals",
|
|
1455
|
+
type: "function",
|
|
1456
|
+
stateMutability: "view",
|
|
1457
|
+
inputs: [],
|
|
1458
|
+
outputs: [{ name: "", type: "uint8" }]
|
|
1459
|
+
},
|
|
1460
|
+
{
|
|
1461
|
+
name: "symbol",
|
|
1462
|
+
type: "function",
|
|
1463
|
+
stateMutability: "view",
|
|
1464
|
+
inputs: [],
|
|
1465
|
+
outputs: [{ name: "", type: "string" }]
|
|
1466
|
+
}
|
|
1467
|
+
];
|
|
1468
|
+
var DefaultAssetRegistry = class {
|
|
1469
|
+
nativeTokenAddress = NATIVE_TOKEN_ADDRESS;
|
|
1470
|
+
getToken = getToken;
|
|
1471
|
+
getTokenAddress = getTokenAddress;
|
|
1472
|
+
getChain = getChain;
|
|
1473
|
+
};
|
|
1474
|
+
var MoneyOS = class {
|
|
1475
|
+
read;
|
|
1476
|
+
executor;
|
|
1477
|
+
assets;
|
|
1478
|
+
runtimeConfig;
|
|
1479
|
+
constructor(config) {
|
|
1480
|
+
const provided = [];
|
|
1481
|
+
if (config.execute) provided.push("execute");
|
|
1482
|
+
if (config.privateKey) provided.push("privateKey");
|
|
1483
|
+
if (config.signer) provided.push("signer");
|
|
1484
|
+
if (provided.length > 1) {
|
|
1485
|
+
throw new Error(
|
|
1486
|
+
`MoneyOSConfig: pass at most one of \`execute\`, \`privateKey\`, or \`signer\`. Received: ${provided.join(", ")}.`
|
|
1487
|
+
);
|
|
1488
|
+
}
|
|
1489
|
+
this.runtimeConfig = {
|
|
1490
|
+
defaultChainId: config.chainId ?? defaultChain.id,
|
|
1491
|
+
rpcUrl: config.rpcUrl
|
|
1492
|
+
};
|
|
1493
|
+
this.read = config.read ?? new ViemReadClient(this.runtimeConfig);
|
|
1494
|
+
this.assets = config.assets ?? new DefaultAssetRegistry();
|
|
1495
|
+
if (config.execute) {
|
|
1496
|
+
this.executor = config.execute;
|
|
1497
|
+
} else if (config.signer) {
|
|
1498
|
+
this.executor = new EOAExecutor(config.signer, this.runtimeConfig);
|
|
1499
|
+
} else if (config.privateKey) {
|
|
1500
|
+
this.executor = EOAExecutor.fromPrivateKey(
|
|
1501
|
+
config.privateKey,
|
|
1502
|
+
this.runtimeConfig
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
get runtime() {
|
|
1507
|
+
return {
|
|
1508
|
+
read: this.read,
|
|
1509
|
+
execute: this.requireExecutor(),
|
|
1510
|
+
assets: this.assets,
|
|
1511
|
+
config: this.runtimeConfig
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
get address() {
|
|
1515
|
+
return this.requireExecutor().getAddress();
|
|
1516
|
+
}
|
|
1517
|
+
requireExecutor() {
|
|
1518
|
+
if (!this.executor) {
|
|
1519
|
+
throw new Error(
|
|
1520
|
+
"No signing account configured. Set `signer`, `privateKey`, or `execute` in MoneyOS config."
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
1523
|
+
return this.executor;
|
|
1524
|
+
}
|
|
1525
|
+
async balance(token, options) {
|
|
1526
|
+
const chainId = options?.chainId ?? this.runtimeConfig.defaultChainId;
|
|
1527
|
+
const account = options?.address ?? this.address;
|
|
1528
|
+
const tokenAddress = this.assets.getTokenAddress(token, chainId);
|
|
1529
|
+
if (!tokenAddress) {
|
|
1530
|
+
throw new Error(`Token ${token} not found on chain ${chainId}`);
|
|
1531
|
+
}
|
|
1532
|
+
const tokenInfo = this.assets.getToken(token);
|
|
1533
|
+
if (tokenAddress === NATIVE_TOKEN_ADDRESS) {
|
|
1534
|
+
const raw2 = await this.read.getBalance({ address: account, chainId });
|
|
1535
|
+
return {
|
|
1536
|
+
token: tokenInfo.name,
|
|
1537
|
+
symbol: tokenInfo.symbol,
|
|
1538
|
+
amount: formatUnits2(raw2, tokenInfo.decimals),
|
|
1539
|
+
rawAmount: raw2,
|
|
1540
|
+
decimals: tokenInfo.decimals,
|
|
1541
|
+
chainId
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
const raw = await this.read.readContract({
|
|
1545
|
+
address: tokenAddress,
|
|
1546
|
+
abi: ERC20_ABI2,
|
|
1547
|
+
functionName: "balanceOf",
|
|
1548
|
+
args: [account],
|
|
1549
|
+
chainId
|
|
1550
|
+
});
|
|
1551
|
+
return {
|
|
1552
|
+
token: tokenInfo.name,
|
|
1553
|
+
symbol: tokenInfo.symbol,
|
|
1554
|
+
amount: formatUnits2(raw, tokenInfo.decimals),
|
|
1555
|
+
rawAmount: raw,
|
|
1556
|
+
decimals: tokenInfo.decimals,
|
|
1557
|
+
chainId
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
async send(token, to, amount, options) {
|
|
1561
|
+
const execute = this.requireExecutor();
|
|
1562
|
+
const chainId = options?.chainId ?? this.runtimeConfig.defaultChainId;
|
|
1563
|
+
const from = execute.getAddress();
|
|
1564
|
+
const tokenAddress = this.assets.getTokenAddress(token, chainId);
|
|
1565
|
+
if (!tokenAddress) {
|
|
1566
|
+
throw new Error(`Token ${token} not found on chain ${chainId}`);
|
|
1567
|
+
}
|
|
1568
|
+
const tokenInfo = this.assets.getToken(token);
|
|
1569
|
+
const value = parseUnits2(amount, tokenInfo.decimals);
|
|
1570
|
+
if (tokenAddress === NATIVE_TOKEN_ADDRESS) {
|
|
1571
|
+
const result2 = await execute.send({ to, value, chainId });
|
|
1572
|
+
return {
|
|
1573
|
+
hash: result2.hash,
|
|
1574
|
+
from,
|
|
1575
|
+
to,
|
|
1576
|
+
amount,
|
|
1577
|
+
token: tokenInfo.symbol,
|
|
1578
|
+
chainId
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
const data = encodeFunctionData2({
|
|
1582
|
+
abi: ERC20_ABI2,
|
|
1583
|
+
functionName: "transfer",
|
|
1584
|
+
args: [to, value]
|
|
1585
|
+
});
|
|
1586
|
+
const result = await execute.send({ to: tokenAddress, data, chainId });
|
|
1587
|
+
return {
|
|
1588
|
+
hash: result.hash,
|
|
1589
|
+
from,
|
|
1590
|
+
to,
|
|
1591
|
+
amount,
|
|
1592
|
+
token: tokenInfo.symbol,
|
|
1593
|
+
chainId
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
async swap(tokenIn, tokenOut, amount, provider, options) {
|
|
1597
|
+
const execute = this.requireExecutor();
|
|
1598
|
+
const chainId = options?.chainId ?? this.runtimeConfig.defaultChainId;
|
|
1599
|
+
return executeSwap(
|
|
1600
|
+
{ tokenIn, tokenOut, amount, provider, chainId, slippage: options?.slippage },
|
|
1601
|
+
{ read: this.read, execute, assets: this.assets }
|
|
1602
|
+
);
|
|
1603
|
+
}
|
|
1604
|
+
};
|
|
1605
|
+
|
|
1606
|
+
// src/cli/wallet.ts
|
|
1607
|
+
function resolveEnvPrivateKey(explicit) {
|
|
1608
|
+
return explicit ?? process.env.MONEYOS_PRIVATE_KEY;
|
|
1609
|
+
}
|
|
1610
|
+
function getWalletStore(config, options = {}) {
|
|
1611
|
+
return new FileEncryptedWalletStore(options.walletPath ?? getWalletPath(config));
|
|
1612
|
+
}
|
|
1613
|
+
function getSessionPath(options = {}) {
|
|
1614
|
+
return options.sessionSocketPath ?? getSessionSocketPath();
|
|
1615
|
+
}
|
|
1616
|
+
function getTokenPath(options = {}) {
|
|
1617
|
+
return options.sessionTokenPath ?? getSessionTokenPath();
|
|
1618
|
+
}
|
|
1619
|
+
async function loadCliAddress(config, options = {}) {
|
|
1620
|
+
const envPrivateKey = resolveEnvPrivateKey(options.envPrivateKey);
|
|
1621
|
+
if (envPrivateKey) {
|
|
1622
|
+
const signer = privateKeyToManagedAccount(envPrivateKey);
|
|
1623
|
+
return {
|
|
1624
|
+
kind: "env",
|
|
1625
|
+
address: signer.address,
|
|
1626
|
+
source: "env"
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
if (hasRemovedOnePasswordConfig(config)) {
|
|
1630
|
+
throw new Error(getRemovedOnePasswordStorageMessage());
|
|
1631
|
+
}
|
|
1632
|
+
const wallet = getWalletStore(config, options);
|
|
1633
|
+
const metadata = await wallet.metadata();
|
|
1634
|
+
if (metadata?.address) {
|
|
1635
|
+
return {
|
|
1636
|
+
kind: "wallet-file",
|
|
1637
|
+
address: metadata.address,
|
|
1638
|
+
source: "encrypted-wallet"
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
if (hasLegacyPlaintextWalletConfig(config)) {
|
|
1642
|
+
throw new Error(getLegacyPlaintextWalletStorageMessage());
|
|
1643
|
+
}
|
|
1644
|
+
throw new Error("No wallet configured. Run `moneyos init`.");
|
|
1645
|
+
}
|
|
1646
|
+
async function buildCliMoneyOSConfig(config, options = {}) {
|
|
1647
|
+
const moneyosConfig = {
|
|
1648
|
+
chainId: options.chainId ?? config.chainId ?? 42161,
|
|
1649
|
+
rpcUrl: config.rpcUrl
|
|
1650
|
+
};
|
|
1651
|
+
if (!options.requireSigner) {
|
|
1652
|
+
return moneyosConfig;
|
|
1653
|
+
}
|
|
1654
|
+
const envPrivateKey = resolveEnvPrivateKey(options.envPrivateKey);
|
|
1655
|
+
if (envPrivateKey) {
|
|
1656
|
+
return {
|
|
1657
|
+
...moneyosConfig,
|
|
1658
|
+
signer: privateKeyToManagedAccount(envPrivateKey)
|
|
1659
|
+
};
|
|
1660
|
+
}
|
|
1661
|
+
if (hasRemovedOnePasswordConfig(config)) {
|
|
1662
|
+
throw new Error(getRemovedOnePasswordStorageMessage());
|
|
1663
|
+
}
|
|
1664
|
+
const socketPath = getSessionPath(options);
|
|
1665
|
+
const tokenPath = getTokenPath(options);
|
|
1666
|
+
const session = await getSessionStatus(socketPath, tokenPath);
|
|
1667
|
+
if (session) {
|
|
1668
|
+
return {
|
|
1669
|
+
...moneyosConfig,
|
|
1670
|
+
execute: new SessionExecutionClient({
|
|
1671
|
+
socketPath,
|
|
1672
|
+
tokenPath,
|
|
1673
|
+
address: session.address
|
|
1674
|
+
})
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
const wallet = getWalletStore(config, options);
|
|
1678
|
+
if (wallet.exists()) {
|
|
1679
|
+
throw new Error(
|
|
1680
|
+
"Wallet is locked. Run `moneyos auth unlock` locally before using write commands."
|
|
1681
|
+
);
|
|
1682
|
+
}
|
|
1683
|
+
if (hasLegacyPlaintextWalletConfig(config)) {
|
|
1684
|
+
throw new Error(getLegacyPlaintextWalletStorageMessage());
|
|
1685
|
+
}
|
|
1686
|
+
throw new Error("No wallet configured. Run `moneyos init`.");
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
// src/cli/commands/balance.ts
|
|
1690
|
+
var balanceCommand = new Command2("balance").description("Check token balance").argument("<token>", "Token symbol (e.g. USDC, ETH, RYZE)").option("-a, --address <address>", "Address to check (defaults to your own)").option("-c, --chain <chainId>", "Chain ID (default: 42161 Arbitrum)").action(async (token, options) => {
|
|
1691
|
+
const config = loadConfig();
|
|
1692
|
+
const chainId = options.chain ? parseInt(options.chain) : config.chainId;
|
|
1693
|
+
let address = options.address;
|
|
1694
|
+
let moneyos;
|
|
1695
|
+
try {
|
|
1696
|
+
if (!address) {
|
|
1697
|
+
const resolved = await loadCliAddress(config);
|
|
1698
|
+
address = resolved.address;
|
|
1699
|
+
}
|
|
1700
|
+
moneyos = new MoneyOS(
|
|
1701
|
+
await buildCliMoneyOSConfig(config, {
|
|
1702
|
+
chainId: chainId ?? 42161,
|
|
1703
|
+
requireSigner: false
|
|
1704
|
+
})
|
|
1705
|
+
);
|
|
1706
|
+
} catch (error) {
|
|
1707
|
+
console.error(
|
|
1708
|
+
error instanceof Error ? error.message : String(error)
|
|
1709
|
+
);
|
|
1710
|
+
process.exitCode = 1;
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1713
|
+
const result = await moneyos.balance(token, {
|
|
1714
|
+
address,
|
|
1715
|
+
chainId
|
|
1716
|
+
});
|
|
1717
|
+
console.log(`${result.amount} ${result.symbol}`);
|
|
1718
|
+
});
|
|
1719
|
+
|
|
1720
|
+
// src/cli/commands/send.ts
|
|
1721
|
+
import { Command as Command3 } from "commander";
|
|
1722
|
+
var sendCommand = new Command3("send").description("Send tokens to an address").argument("<amount>", "Amount to send (e.g. 10)").argument("<token>", "Token symbol (e.g. USDC, ETH, RYZE)").argument("<to>", "Recipient address").option("-c, --chain <chainId>", "Chain ID (default: 42161 Arbitrum)").action(async (amount, token, to, options) => {
|
|
1723
|
+
const config = loadConfig();
|
|
1724
|
+
const chainId = options.chain ? parseInt(options.chain) : config.chainId ?? 42161;
|
|
1725
|
+
let moneyos;
|
|
1726
|
+
try {
|
|
1727
|
+
moneyos = new MoneyOS(
|
|
1728
|
+
await buildCliMoneyOSConfig(config, {
|
|
1729
|
+
chainId,
|
|
1730
|
+
requireSigner: true
|
|
1731
|
+
})
|
|
1732
|
+
);
|
|
1733
|
+
} catch (error) {
|
|
1734
|
+
console.error(
|
|
1735
|
+
error instanceof Error ? error.message : String(error)
|
|
1736
|
+
);
|
|
1737
|
+
process.exitCode = 1;
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
const chain = getChain(chainId);
|
|
1741
|
+
console.log(
|
|
1742
|
+
`Sending ${amount} ${token.toUpperCase()} to ${to} on ${chain?.name ?? chainId}...`
|
|
1743
|
+
);
|
|
1744
|
+
const result = await moneyos.send(token, to, amount, {
|
|
1745
|
+
chainId
|
|
1746
|
+
});
|
|
1747
|
+
console.log(`Sent. tx: ${result.hash}`);
|
|
1748
|
+
});
|
|
1749
|
+
|
|
1750
|
+
// src/cli/commands/swap.ts
|
|
1751
|
+
import { Command as Command4 } from "commander";
|
|
1752
|
+
|
|
1753
|
+
// src/providers/odos.ts
|
|
1754
|
+
var ODOS_API = "https://api.odos.xyz";
|
|
1755
|
+
var ODOS_NATIVE_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
1756
|
+
var OdosProvider = class {
|
|
1757
|
+
name = "odos";
|
|
1758
|
+
apiKey;
|
|
1759
|
+
constructor(options) {
|
|
1760
|
+
this.apiKey = options?.apiKey;
|
|
1761
|
+
}
|
|
1762
|
+
toOdosAddress(address) {
|
|
1763
|
+
return address === NATIVE_TOKEN_ADDRESS ? ODOS_NATIVE_ADDRESS : address;
|
|
1764
|
+
}
|
|
1765
|
+
async getQuote(params) {
|
|
1766
|
+
const headers = {
|
|
1767
|
+
"Content-Type": "application/json"
|
|
1768
|
+
};
|
|
1769
|
+
if (this.apiKey) {
|
|
1770
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
1771
|
+
}
|
|
1772
|
+
const response = await fetch(`${ODOS_API}/sor/quote/v2`, {
|
|
1773
|
+
method: "POST",
|
|
1774
|
+
headers,
|
|
1775
|
+
body: JSON.stringify({
|
|
1776
|
+
chainId: params.chainId,
|
|
1777
|
+
inputTokens: [
|
|
1778
|
+
{
|
|
1779
|
+
tokenAddress: this.toOdosAddress(params.tokenIn),
|
|
1780
|
+
amount: params.amount.toString()
|
|
1781
|
+
}
|
|
1782
|
+
],
|
|
1783
|
+
outputTokens: [
|
|
1784
|
+
{
|
|
1785
|
+
tokenAddress: this.toOdosAddress(params.tokenOut),
|
|
1786
|
+
proportion: 1
|
|
1787
|
+
}
|
|
1788
|
+
],
|
|
1789
|
+
userAddr: params.sender,
|
|
1790
|
+
slippageLimitPercent: params.slippage ?? 1,
|
|
1791
|
+
referralCode: 0,
|
|
1792
|
+
compact: true
|
|
1793
|
+
})
|
|
1794
|
+
});
|
|
1795
|
+
if (!response.ok) {
|
|
1796
|
+
const text = await response.text();
|
|
1797
|
+
throw new Error(`Odos quote failed: ${response.status} ${text}`);
|
|
1798
|
+
}
|
|
1799
|
+
const data = await response.json();
|
|
1800
|
+
return {
|
|
1801
|
+
tokenIn: params.tokenIn,
|
|
1802
|
+
tokenOut: params.tokenOut,
|
|
1803
|
+
amountIn: params.amount.toString(),
|
|
1804
|
+
expectedOut: data.outAmounts[0],
|
|
1805
|
+
router: "",
|
|
1806
|
+
chainId: params.chainId,
|
|
1807
|
+
pathId: data.pathId,
|
|
1808
|
+
sender: params.sender
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1811
|
+
async getCalldata(quote) {
|
|
1812
|
+
const headers = {
|
|
1813
|
+
"Content-Type": "application/json"
|
|
1814
|
+
};
|
|
1815
|
+
if (this.apiKey) {
|
|
1816
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
1817
|
+
}
|
|
1818
|
+
const response = await fetch(`${ODOS_API}/sor/assemble`, {
|
|
1819
|
+
method: "POST",
|
|
1820
|
+
headers,
|
|
1821
|
+
body: JSON.stringify({
|
|
1822
|
+
userAddr: quote.sender,
|
|
1823
|
+
pathId: quote.pathId,
|
|
1824
|
+
simulate: false
|
|
1825
|
+
})
|
|
1826
|
+
});
|
|
1827
|
+
if (!response.ok) {
|
|
1828
|
+
const text = await response.text();
|
|
1829
|
+
throw new Error(`Odos assemble failed: ${response.status} ${text}`);
|
|
1830
|
+
}
|
|
1831
|
+
const data = await response.json();
|
|
1832
|
+
return {
|
|
1833
|
+
to: data.transaction.to,
|
|
1834
|
+
data: data.transaction.data,
|
|
1835
|
+
value: BigInt(data.transaction.value)
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
};
|
|
1839
|
+
|
|
1840
|
+
// src/cli/commands/swap.ts
|
|
1841
|
+
var swapCommand = new Command4("swap").description("Swap tokens").argument("<amount>", "Amount to swap (e.g. 100)").argument("<tokenIn>", "Token to sell (e.g. USDC)").argument("<tokenOut>", "Token to buy (e.g. RYZE)").option("-c, --chain <chainId>", "Chain ID (default: 42161 Arbitrum)").option("-s, --slippage <percent>", "Slippage tolerance in percent (default: 1)").action(async (amount, tokenIn, tokenOut, options) => {
|
|
1842
|
+
const config = loadConfig();
|
|
1843
|
+
const chainId = options.chain ? parseInt(options.chain) : config.chainId ?? 42161;
|
|
1844
|
+
let moneyos;
|
|
1845
|
+
try {
|
|
1846
|
+
moneyos = new MoneyOS(
|
|
1847
|
+
await buildCliMoneyOSConfig(config, {
|
|
1848
|
+
chainId,
|
|
1849
|
+
requireSigner: true
|
|
1850
|
+
})
|
|
1851
|
+
);
|
|
1852
|
+
} catch (error) {
|
|
1853
|
+
console.error(
|
|
1854
|
+
error instanceof Error ? error.message : String(error)
|
|
1855
|
+
);
|
|
1856
|
+
process.exitCode = 1;
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
const provider = new OdosProvider();
|
|
1860
|
+
const chain = getChain(chainId);
|
|
1861
|
+
const slippage = options.slippage ? parseFloat(options.slippage) : void 0;
|
|
1862
|
+
console.log(
|
|
1863
|
+
`Swapping ${amount} ${tokenIn.toUpperCase()} \u2192 ${tokenOut.toUpperCase()} on ${chain?.name ?? chainId}...`
|
|
1864
|
+
);
|
|
1865
|
+
const result = await moneyos.swap(tokenIn, tokenOut, amount, provider, {
|
|
1866
|
+
chainId,
|
|
1867
|
+
slippage
|
|
1868
|
+
});
|
|
1869
|
+
console.log(`Swapped. Expected: ~${result.amountOut} ${result.tokenOut}`);
|
|
1870
|
+
console.log(`tx: ${result.hash}`);
|
|
1871
|
+
});
|
|
1872
|
+
|
|
1873
|
+
// src/cli/commands/keystore.ts
|
|
1874
|
+
import { Command as Command5 } from "commander";
|
|
1875
|
+
|
|
1876
|
+
// src/cli/wallet-status.ts
|
|
1877
|
+
import { privateKeyToAccount as privateKeyToAccount4 } from "viem/accounts";
|
|
1878
|
+
async function resolveWalletStatus(config, walletPath = getWalletPath(config)) {
|
|
1879
|
+
if (hasRemovedOnePasswordConfig(config)) {
|
|
1880
|
+
return {
|
|
1881
|
+
kind: "unsupported",
|
|
1882
|
+
address: getRemovedOnePasswordCachedAddress(config),
|
|
1883
|
+
walletPath,
|
|
1884
|
+
reason: getRemovedOnePasswordStorageMessage()
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
const store = new FileEncryptedWalletStore(walletPath);
|
|
1888
|
+
if (store.exists()) {
|
|
1889
|
+
try {
|
|
1890
|
+
const metadata = await store.metadata();
|
|
1891
|
+
return {
|
|
1892
|
+
kind: metadata ? "encrypted" : "none",
|
|
1893
|
+
address: metadata?.address,
|
|
1894
|
+
walletPath
|
|
1895
|
+
};
|
|
1896
|
+
} catch (error) {
|
|
1897
|
+
return {
|
|
1898
|
+
kind: "invalid",
|
|
1899
|
+
walletPath,
|
|
1900
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
if (hasLegacyPlaintextWalletConfig(config)) {
|
|
1905
|
+
try {
|
|
1906
|
+
const address = privateKeyToAccount4(config.privateKey).address;
|
|
1907
|
+
return {
|
|
1908
|
+
kind: "legacy",
|
|
1909
|
+
walletPath,
|
|
1910
|
+
address,
|
|
1911
|
+
reason: getLegacyPlaintextWalletStorageMessage()
|
|
1912
|
+
};
|
|
1913
|
+
} catch (error) {
|
|
1914
|
+
return {
|
|
1915
|
+
kind: "legacy",
|
|
1916
|
+
walletPath,
|
|
1917
|
+
reason: error instanceof Error ? `${getLegacyPlaintextWalletStorageMessage()} (${error.message})` : getLegacyPlaintextWalletStorageMessage()
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
return {
|
|
1922
|
+
kind: "none",
|
|
1923
|
+
walletPath
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1926
|
+
function formatWalletStatus(status) {
|
|
1927
|
+
const lines = [];
|
|
1928
|
+
if (status.kind === "none") {
|
|
1929
|
+
lines.push("Wallet: (none)");
|
|
1930
|
+
lines.push(`Path: ${status.walletPath}`);
|
|
1931
|
+
lines.push("Status: no wallet configured \u2014 run `moneyos init`");
|
|
1932
|
+
return lines.join("\n");
|
|
1933
|
+
}
|
|
1934
|
+
if (status.kind === "unsupported") {
|
|
1935
|
+
lines.push("Wallet: removed legacy model");
|
|
1936
|
+
if (status.address) {
|
|
1937
|
+
lines.push(`Address: ${status.address} (cached metadata)`);
|
|
1938
|
+
}
|
|
1939
|
+
lines.push(`Path: ${status.walletPath}`);
|
|
1940
|
+
lines.push(
|
|
1941
|
+
`Status: removed \u2014 ${status.reason ?? "unsupported legacy config"}`
|
|
1942
|
+
);
|
|
1943
|
+
return lines.join("\n");
|
|
1944
|
+
}
|
|
1945
|
+
if (status.kind === "legacy") {
|
|
1946
|
+
lines.push("Wallet: legacy plaintext config");
|
|
1947
|
+
if (status.address) {
|
|
1948
|
+
lines.push(`Address: ${status.address}`);
|
|
1949
|
+
}
|
|
1950
|
+
lines.push(`Path: ${status.walletPath}`);
|
|
1951
|
+
lines.push(
|
|
1952
|
+
`Status: upgrade required \u2014 ${status.reason ?? "run `moneyos init`"}`
|
|
1953
|
+
);
|
|
1954
|
+
return lines.join("\n");
|
|
1955
|
+
}
|
|
1956
|
+
if (status.kind === "invalid") {
|
|
1957
|
+
lines.push("Wallet: encrypted local wallet");
|
|
1958
|
+
lines.push(`Path: ${status.walletPath}`);
|
|
1959
|
+
lines.push(`Status: invalid \u2014 ${status.reason ?? "wallet file is unreadable"}`);
|
|
1960
|
+
return lines.join("\n");
|
|
1961
|
+
}
|
|
1962
|
+
lines.push("Wallet: encrypted local wallet");
|
|
1963
|
+
if (status.address) {
|
|
1964
|
+
lines.push(`Address: ${status.address}`);
|
|
1965
|
+
}
|
|
1966
|
+
lines.push(`Path: ${status.walletPath}`);
|
|
1967
|
+
lines.push("Status: ready");
|
|
1968
|
+
return lines.join("\n");
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
// src/cli/commands/keystore.ts
|
|
1972
|
+
var keystoreCommand = new Command5("keystore").description(
|
|
1973
|
+
"Compatibility alias for wallet status"
|
|
1974
|
+
);
|
|
1975
|
+
keystoreCommand.command("status").description("Show the current wallet storage status").action(async () => {
|
|
1976
|
+
const config = loadFileConfig();
|
|
1977
|
+
const status = await resolveWalletStatus(config);
|
|
1978
|
+
console.log(formatWalletStatus(status));
|
|
1979
|
+
});
|
|
1980
|
+
|
|
1981
|
+
// src/cli/commands/auth.ts
|
|
1982
|
+
import { Command as Command6 } from "commander";
|
|
1983
|
+
var DEFAULT_TTL_MS = 15 * 60 * 1e3;
|
|
1984
|
+
function formatSessionStatus(params) {
|
|
1985
|
+
const lines = [];
|
|
1986
|
+
lines.push(`Session: ${params.state}`);
|
|
1987
|
+
if (params.address) {
|
|
1988
|
+
lines.push(`Address: ${params.address}`);
|
|
1989
|
+
}
|
|
1990
|
+
if (params.expiresAt) {
|
|
1991
|
+
lines.push(`Expires: ${params.expiresAt}`);
|
|
1992
|
+
}
|
|
1993
|
+
return lines.join("\n");
|
|
1994
|
+
}
|
|
1995
|
+
var authCommand = new Command6("auth").description(
|
|
1996
|
+
"Unlock, inspect, and lock the local MoneyOS wallet session"
|
|
1997
|
+
);
|
|
1998
|
+
authCommand.command("unlock").description("Unlock the local wallet and start a short-lived session").action(async () => {
|
|
1999
|
+
const config = loadFileConfig();
|
|
2000
|
+
const walletPath = getWalletPath(config);
|
|
2001
|
+
const wallet = new FileEncryptedWalletStore(walletPath);
|
|
2002
|
+
if (!wallet.exists()) {
|
|
2003
|
+
console.error("No encrypted wallet found. Run `moneyos init` first.");
|
|
2004
|
+
process.exitCode = 1;
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
2007
|
+
try {
|
|
2008
|
+
const passphrase = await promptHidden("Wallet password: ");
|
|
2009
|
+
if (passphrase.length === 0) {
|
|
2010
|
+
throw new Error("Wallet password cannot be empty.");
|
|
2011
|
+
}
|
|
2012
|
+
const privateKey = await wallet.decrypt(passphrase);
|
|
2013
|
+
const status = await startDetachedSessionDaemon({
|
|
2014
|
+
type: "start",
|
|
2015
|
+
privateKey,
|
|
2016
|
+
chainId: config.chainId ?? 42161,
|
|
2017
|
+
rpcUrl: config.rpcUrl,
|
|
2018
|
+
socketPath: getSessionSocketPath(),
|
|
2019
|
+
tokenPath: getSessionTokenPath(),
|
|
2020
|
+
ttlMs: DEFAULT_TTL_MS
|
|
2021
|
+
});
|
|
2022
|
+
console.log("Wallet unlocked.");
|
|
2023
|
+
console.log(
|
|
2024
|
+
formatSessionStatus({
|
|
2025
|
+
state: "unlocked",
|
|
2026
|
+
address: status.address,
|
|
2027
|
+
expiresAt: status.expiresAt
|
|
2028
|
+
})
|
|
2029
|
+
);
|
|
2030
|
+
} catch (error) {
|
|
2031
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
2032
|
+
process.exitCode = 1;
|
|
2033
|
+
}
|
|
2034
|
+
});
|
|
2035
|
+
authCommand.command("lock").description("Lock the local MoneyOS wallet session").action(async () => {
|
|
2036
|
+
const locked = await lockSession(
|
|
2037
|
+
getSessionSocketPath(),
|
|
2038
|
+
getSessionTokenPath()
|
|
2039
|
+
);
|
|
2040
|
+
console.log(
|
|
2041
|
+
formatSessionStatus({
|
|
2042
|
+
state: "locked"
|
|
2043
|
+
})
|
|
2044
|
+
);
|
|
2045
|
+
if (!locked) {
|
|
2046
|
+
console.log("No active local session was running.");
|
|
2047
|
+
}
|
|
2048
|
+
});
|
|
2049
|
+
authCommand.command("status").description("Show whether the local wallet session is unlocked").action(async () => {
|
|
2050
|
+
const status = await getSessionStatus(
|
|
2051
|
+
getSessionSocketPath(),
|
|
2052
|
+
getSessionTokenPath()
|
|
2053
|
+
);
|
|
2054
|
+
if (!status) {
|
|
2055
|
+
console.log(
|
|
2056
|
+
formatSessionStatus({
|
|
2057
|
+
state: "locked"
|
|
2058
|
+
})
|
|
2059
|
+
);
|
|
2060
|
+
return;
|
|
2061
|
+
}
|
|
2062
|
+
console.log(
|
|
2063
|
+
formatSessionStatus({
|
|
2064
|
+
state: "unlocked",
|
|
2065
|
+
address: status.address,
|
|
2066
|
+
expiresAt: status.expiresAt
|
|
2067
|
+
})
|
|
2068
|
+
);
|
|
2069
|
+
});
|
|
2070
|
+
|
|
2071
|
+
// src/cli/commands/backup.ts
|
|
2072
|
+
import { Command as Command7 } from "commander";
|
|
2073
|
+
function formatBackupStatus(params) {
|
|
2074
|
+
const lines = [];
|
|
2075
|
+
lines.push(`Wallet: ${params.exists ? "present" : "missing"}`);
|
|
2076
|
+
if (params.address) {
|
|
2077
|
+
lines.push(`Address: ${params.address}`);
|
|
2078
|
+
}
|
|
2079
|
+
lines.push(`Wallet at: ${params.walletPath}`);
|
|
2080
|
+
lines.push(`Backups: ${params.backupCount}`);
|
|
2081
|
+
lines.push(`Directory: ${params.backupDir}`);
|
|
2082
|
+
if (params.latestBackupPath) {
|
|
2083
|
+
lines.push(`Latest: ${params.latestBackupPath}`);
|
|
2084
|
+
}
|
|
2085
|
+
return lines.join("\n");
|
|
2086
|
+
}
|
|
2087
|
+
var backupCommand = new Command7("backup").description(
|
|
2088
|
+
"Export, restore, and inspect encrypted wallet backups"
|
|
2089
|
+
);
|
|
2090
|
+
backupCommand.command("export").description("Write a copy of the encrypted wallet backup").option("-o, --out <path>", "Custom path for the backup file").option("--force", "Overwrite an existing backup file at --out").action(async (options) => {
|
|
2091
|
+
const config = loadFileConfig();
|
|
2092
|
+
const provider = new FileBackupProvider({
|
|
2093
|
+
walletPath: getWalletPath(config),
|
|
2094
|
+
backupDir: getBackupDir(config)
|
|
2095
|
+
});
|
|
2096
|
+
try {
|
|
2097
|
+
const targetPath = await provider.exportWallet({
|
|
2098
|
+
outPath: options.out,
|
|
2099
|
+
allowOverwrite: Boolean(options.force)
|
|
2100
|
+
});
|
|
2101
|
+
console.log(`Backup exported to ${targetPath}`);
|
|
2102
|
+
console.log(
|
|
2103
|
+
"Save your wallet password in your password manager of choice. MoneyOS does not store or sync it for you."
|
|
2104
|
+
);
|
|
2105
|
+
} catch (error) {
|
|
2106
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
2107
|
+
process.exitCode = 1;
|
|
2108
|
+
}
|
|
2109
|
+
});
|
|
2110
|
+
backupCommand.command("restore").description("Restore the encrypted wallet from a backup file").argument("<path>", "Path to the encrypted backup file").option("--force", "Overwrite an existing encrypted wallet").action(async (path, options) => {
|
|
2111
|
+
const config = loadFileConfig();
|
|
2112
|
+
const provider = new FileBackupProvider({
|
|
2113
|
+
walletPath: getWalletPath(config),
|
|
2114
|
+
backupDir: getBackupDir(config)
|
|
2115
|
+
});
|
|
2116
|
+
try {
|
|
2117
|
+
const passphrase = await promptHidden(
|
|
2118
|
+
"Wallet password for backup verification: "
|
|
2119
|
+
);
|
|
2120
|
+
const metadata = await provider.restoreWallet(path, {
|
|
2121
|
+
passphrase,
|
|
2122
|
+
allowOverwrite: Boolean(options.force)
|
|
2123
|
+
});
|
|
2124
|
+
await lockSession(getSessionSocketPath(), getSessionTokenPath());
|
|
2125
|
+
console.log("Encrypted wallet restored.");
|
|
2126
|
+
console.log(`Address: ${metadata.address}`);
|
|
2127
|
+
console.log(`Wallet: ${getWalletPath(config)}`);
|
|
2128
|
+
} catch (error) {
|
|
2129
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
2130
|
+
process.exitCode = 1;
|
|
2131
|
+
}
|
|
2132
|
+
});
|
|
2133
|
+
backupCommand.command("status").description("Show the local wallet backup status").action(async () => {
|
|
2134
|
+
const config = loadFileConfig();
|
|
2135
|
+
const provider = new FileBackupProvider({
|
|
2136
|
+
walletPath: getWalletPath(config),
|
|
2137
|
+
backupDir: getBackupDir(config)
|
|
2138
|
+
});
|
|
2139
|
+
const status = await provider.status();
|
|
2140
|
+
console.log(formatBackupStatus(status));
|
|
2141
|
+
});
|
|
2142
|
+
|
|
2143
|
+
// src/cli/version.ts
|
|
2144
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
2145
|
+
import { fileURLToPath } from "url";
|
|
2146
|
+
import { dirname as dirname4, resolve as resolve2 } from "path";
|
|
2147
|
+
var packageJsonPath = resolve2(
|
|
2148
|
+
dirname4(fileURLToPath(import.meta.url)),
|
|
2149
|
+
"../../package.json"
|
|
2150
|
+
);
|
|
2151
|
+
var version = JSON.parse(
|
|
2152
|
+
readFileSync4(packageJsonPath, "utf8")
|
|
2153
|
+
).version;
|
|
2154
|
+
|
|
2155
|
+
// src/cli/index.ts
|
|
2156
|
+
var program = new Command8();
|
|
2157
|
+
program.name("moneyos").description("The operating system for money").version(version);
|
|
2158
|
+
program.addCommand(initCommand);
|
|
2159
|
+
program.addCommand(balanceCommand);
|
|
2160
|
+
program.addCommand(sendCommand);
|
|
2161
|
+
program.addCommand(swapCommand);
|
|
2162
|
+
program.addCommand(keystoreCommand);
|
|
2163
|
+
program.addCommand(authCommand);
|
|
2164
|
+
program.addCommand(backupCommand);
|
|
2165
|
+
program.command("__session-daemon", { hidden: true }).action(async () => {
|
|
2166
|
+
await runSessionDaemonProcess();
|
|
2167
|
+
});
|
|
2168
|
+
program.parse();
|
|
2169
|
+
//# sourceMappingURL=index.js.map
|