@macwlt/cli 0.0.1-alpha
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 +201 -0
- package/README.md +30 -0
- package/config.default.json +149 -0
- package/dist/index.d.ts +543 -0
- package/dist/index.js +2520 -0
- package/dist/index.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +2475 -0
- package/dist/main.js.map +1 -0
- package/native/com.macwlt.SigningService.xpc/Contents/Frameworks/libsecp256k1.6.dylib +0 -0
- package/native/com.macwlt.SigningService.xpc/Contents/Info.plist +30 -0
- package/native/com.macwlt.SigningService.xpc/Contents/MacOS/com.macwlt.SigningService +0 -0
- package/native/com.macwlt.SigningService.xpc/Contents/_CodeSignature/CodeResources +125 -0
- package/native/libmacwlt.dylib +0 -0
- package/package.json +52 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2520 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
// src/result.ts
|
|
4
|
+
function ok(value) {
|
|
5
|
+
return { ok: true, value };
|
|
6
|
+
}
|
|
7
|
+
function err(error) {
|
|
8
|
+
return { ok: false, error };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// src/base64.ts
|
|
12
|
+
function bytesToBase64(bytes) {
|
|
13
|
+
return Buffer.from(bytes).toString("base64");
|
|
14
|
+
}
|
|
15
|
+
function base64ToBytes(input) {
|
|
16
|
+
if (input.length === 0) return err({ kind: "empty" });
|
|
17
|
+
const normalized = input.trim();
|
|
18
|
+
if (normalized.length === 0) return err({ kind: "empty" });
|
|
19
|
+
const decoded = Buffer.from(normalized, "base64");
|
|
20
|
+
if (decoded.length === 0 || Buffer.from(decoded).toString("base64").replace(/=+$/, "") !== normalized.replace(/=+$/, "")) {
|
|
21
|
+
return err({ kind: "invalid", value: input });
|
|
22
|
+
}
|
|
23
|
+
return ok(new Uint8Array(decoded));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/command.ts
|
|
27
|
+
import { z as z8 } from "zod";
|
|
28
|
+
|
|
29
|
+
// src/config/GlobalConfig.ts
|
|
30
|
+
import { homedir } from "os";
|
|
31
|
+
|
|
32
|
+
// src/config/fileConfigStorage.ts
|
|
33
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
34
|
+
import { dirname } from "path";
|
|
35
|
+
var fileConfigStorage = {
|
|
36
|
+
async read(path) {
|
|
37
|
+
try {
|
|
38
|
+
return await readFile(path, "utf8");
|
|
39
|
+
} catch (caught) {
|
|
40
|
+
if (isNotFoundError(caught)) return void 0;
|
|
41
|
+
throw caught;
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
async write(path, contents2) {
|
|
45
|
+
await mkdir(dirname(path), { recursive: true, mode: 448 });
|
|
46
|
+
await writeFile(path, contents2, { encoding: "utf8", mode: 384 });
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
function isNotFoundError(caught) {
|
|
50
|
+
return caught instanceof Error && "code" in caught && caught.code === "ENOENT";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/config/parseGlobalConfigData.ts
|
|
54
|
+
import { z as z2 } from "zod";
|
|
55
|
+
|
|
56
|
+
// src/config/parseJsonValue.ts
|
|
57
|
+
import { z } from "zod";
|
|
58
|
+
var jsonValueSchema = z.lazy(
|
|
59
|
+
() => z.union([
|
|
60
|
+
z.null(),
|
|
61
|
+
z.boolean(),
|
|
62
|
+
z.number().finite(),
|
|
63
|
+
z.string(),
|
|
64
|
+
z.array(jsonValueSchema),
|
|
65
|
+
z.record(jsonValueSchema)
|
|
66
|
+
])
|
|
67
|
+
);
|
|
68
|
+
function parseJsonValue(input) {
|
|
69
|
+
const parsed = jsonValueSchema.safeParse(input);
|
|
70
|
+
if (!parsed.success) {
|
|
71
|
+
return err({
|
|
72
|
+
kind: "invalid-config",
|
|
73
|
+
message: parsed.error.issues.map((issue) => `${issue.path.join(".") || "value"}: ${issue.message}`).join("; ")
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return ok(parsed.data);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/config/parseGlobalConfigData.ts
|
|
80
|
+
var globalConfigDataSchema = z2.record(z2.unknown());
|
|
81
|
+
function parseGlobalConfigData(input) {
|
|
82
|
+
const parsed = globalConfigDataSchema.safeParse(input);
|
|
83
|
+
if (!parsed.success) {
|
|
84
|
+
return err({
|
|
85
|
+
kind: "invalid-config",
|
|
86
|
+
message: parsed.error.issues.map((issue) => `${issue.path.join(".") || "config"}: ${issue.message}`).join("; ")
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const data = {};
|
|
90
|
+
for (const [key, inputValue] of Object.entries(parsed.data)) {
|
|
91
|
+
const value = parseJsonValue(inputValue);
|
|
92
|
+
if (!value.ok) {
|
|
93
|
+
return err({
|
|
94
|
+
kind: "invalid-config",
|
|
95
|
+
message: `${key}: ${value.error.message}`
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
data[key] = value.value;
|
|
99
|
+
}
|
|
100
|
+
return ok(data);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/config/globalConfigPath.ts
|
|
104
|
+
import { join } from "path";
|
|
105
|
+
function globalConfigPath(homeDirectory) {
|
|
106
|
+
return join(homeDirectory, ".config", "macwlt", "config.json");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/config/GlobalConfig.ts
|
|
110
|
+
var GlobalConfig = class _GlobalConfig {
|
|
111
|
+
#path;
|
|
112
|
+
#storage;
|
|
113
|
+
#data;
|
|
114
|
+
#dirty = false;
|
|
115
|
+
constructor(path, storage, data) {
|
|
116
|
+
this.#path = path;
|
|
117
|
+
this.#storage = storage;
|
|
118
|
+
this.#data = data;
|
|
119
|
+
}
|
|
120
|
+
static async load(options = {}) {
|
|
121
|
+
const path = globalConfigPath(options.homeDirectory ?? homedir());
|
|
122
|
+
const storage = options.storage ?? fileConfigStorage;
|
|
123
|
+
let contents2;
|
|
124
|
+
try {
|
|
125
|
+
contents2 = await storage.read(path);
|
|
126
|
+
} catch (cause) {
|
|
127
|
+
return err({ kind: "read-failed", path, cause });
|
|
128
|
+
}
|
|
129
|
+
if (contents2 === void 0) {
|
|
130
|
+
return ok(new _GlobalConfig(path, storage, {}));
|
|
131
|
+
}
|
|
132
|
+
let input;
|
|
133
|
+
try {
|
|
134
|
+
input = JSON.parse(contents2);
|
|
135
|
+
} catch (caught) {
|
|
136
|
+
return err({
|
|
137
|
+
kind: "invalid-json",
|
|
138
|
+
path,
|
|
139
|
+
message: caught instanceof Error ? caught.message : String(caught)
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const parsed = parseGlobalConfigData(input);
|
|
143
|
+
if (!parsed.ok) return err({ ...parsed.error, path });
|
|
144
|
+
return ok(new _GlobalConfig(path, storage, parsed.value));
|
|
145
|
+
}
|
|
146
|
+
get path() {
|
|
147
|
+
return this.#path;
|
|
148
|
+
}
|
|
149
|
+
get dirty() {
|
|
150
|
+
return this.#dirty;
|
|
151
|
+
}
|
|
152
|
+
get data() {
|
|
153
|
+
return cloneData(this.#data);
|
|
154
|
+
}
|
|
155
|
+
get(key) {
|
|
156
|
+
if (!Object.hasOwn(this.#data, key)) return void 0;
|
|
157
|
+
const value = this.#data[key];
|
|
158
|
+
return value === void 0 ? void 0 : cloneValue(value);
|
|
159
|
+
}
|
|
160
|
+
set(key, input) {
|
|
161
|
+
if (key.length === 0) {
|
|
162
|
+
return err({ kind: "invalid-config", message: "config key must not be empty" });
|
|
163
|
+
}
|
|
164
|
+
const value = parseJsonValue(input);
|
|
165
|
+
if (!value.ok) return value;
|
|
166
|
+
this.#data = { ...this.#data, [key]: value.value };
|
|
167
|
+
this.#dirty = true;
|
|
168
|
+
return ok(void 0);
|
|
169
|
+
}
|
|
170
|
+
delete(key) {
|
|
171
|
+
if (!Object.hasOwn(this.#data, key)) return;
|
|
172
|
+
const { [key]: removed, ...remaining } = this.#data;
|
|
173
|
+
void removed;
|
|
174
|
+
this.#data = remaining;
|
|
175
|
+
this.#dirty = true;
|
|
176
|
+
}
|
|
177
|
+
replace(input) {
|
|
178
|
+
const data = parseGlobalConfigData(input);
|
|
179
|
+
if (!data.ok) return data;
|
|
180
|
+
this.#data = data.value;
|
|
181
|
+
this.#dirty = true;
|
|
182
|
+
return ok(void 0);
|
|
183
|
+
}
|
|
184
|
+
async save() {
|
|
185
|
+
const contents2 = `${JSON.stringify(this.#data, null, 2)}
|
|
186
|
+
`;
|
|
187
|
+
try {
|
|
188
|
+
await this.#storage.write(this.#path, contents2);
|
|
189
|
+
} catch (cause) {
|
|
190
|
+
return err({ kind: "write-failed", path: this.#path, cause });
|
|
191
|
+
}
|
|
192
|
+
this.#dirty = false;
|
|
193
|
+
return ok(void 0);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
function cloneData(data) {
|
|
197
|
+
return structuredClone(data);
|
|
198
|
+
}
|
|
199
|
+
function cloneValue(value) {
|
|
200
|
+
return structuredClone(value);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/config/createFileIfAbsent.ts
|
|
204
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
205
|
+
import { dirname as dirname2 } from "path";
|
|
206
|
+
async function createFileIfAbsent(path, contents2) {
|
|
207
|
+
await mkdir2(dirname2(path), { recursive: true, mode: 448 });
|
|
208
|
+
try {
|
|
209
|
+
await writeFile2(path, contents2, {
|
|
210
|
+
encoding: "utf8",
|
|
211
|
+
flag: "wx",
|
|
212
|
+
mode: 384
|
|
213
|
+
});
|
|
214
|
+
return "created";
|
|
215
|
+
} catch (caught) {
|
|
216
|
+
if (isAlreadyExistsError(caught)) return "already-exists";
|
|
217
|
+
throw caught;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function isAlreadyExistsError(caught) {
|
|
221
|
+
return caught instanceof Error && "code" in caught && caught.code === "EEXIST";
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// config.default.json
|
|
225
|
+
var config_default_default = {
|
|
226
|
+
ethereum: {
|
|
227
|
+
chains: {
|
|
228
|
+
"1": {
|
|
229
|
+
name: "Ethereum Mainnet",
|
|
230
|
+
rpcUrl: "https://ethereum-rpc.publicnode.com",
|
|
231
|
+
nativeAsset: {
|
|
232
|
+
symbol: "ETH"
|
|
233
|
+
},
|
|
234
|
+
assets: [
|
|
235
|
+
{
|
|
236
|
+
symbol: "USDC",
|
|
237
|
+
address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
symbol: "USDT",
|
|
241
|
+
address: "0xdac17f958d2ee523a2206206994597c13d831ec7"
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
symbol: "WETH",
|
|
245
|
+
address: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
|
|
246
|
+
}
|
|
247
|
+
]
|
|
248
|
+
},
|
|
249
|
+
"42161": {
|
|
250
|
+
name: "Arbitrum One",
|
|
251
|
+
rpcUrl: "https://arbitrum-one-rpc.publicnode.com",
|
|
252
|
+
nativeAsset: {
|
|
253
|
+
symbol: "ETH"
|
|
254
|
+
},
|
|
255
|
+
assets: [
|
|
256
|
+
{
|
|
257
|
+
symbol: "USDC",
|
|
258
|
+
address: "0xaf88d065e77c8cc2239327c5edb3a432268e5831"
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
symbol: "USDT",
|
|
262
|
+
address: "0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9"
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
symbol: "WETH",
|
|
266
|
+
address: "0x82af49447d8a07e3bd95bd0d56f35241523fbab1"
|
|
267
|
+
}
|
|
268
|
+
]
|
|
269
|
+
},
|
|
270
|
+
"8453": {
|
|
271
|
+
name: "Base",
|
|
272
|
+
rpcUrl: "https://base-rpc.publicnode.com",
|
|
273
|
+
nativeAsset: {
|
|
274
|
+
symbol: "ETH"
|
|
275
|
+
},
|
|
276
|
+
assets: [
|
|
277
|
+
{
|
|
278
|
+
symbol: "USDC",
|
|
279
|
+
address: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
symbol: "WETH",
|
|
283
|
+
address: "0x4200000000000000000000000000000000000006"
|
|
284
|
+
}
|
|
285
|
+
]
|
|
286
|
+
},
|
|
287
|
+
"10": {
|
|
288
|
+
name: "OP Mainnet",
|
|
289
|
+
rpcUrl: "https://optimism-rpc.publicnode.com",
|
|
290
|
+
nativeAsset: {
|
|
291
|
+
symbol: "ETH"
|
|
292
|
+
},
|
|
293
|
+
assets: [
|
|
294
|
+
{
|
|
295
|
+
symbol: "USDC",
|
|
296
|
+
address: "0x0b2c639c533813f4aa9d7837caf62653d097ff85"
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
symbol: "USDT",
|
|
300
|
+
address: "0x94b008aa00579c1307b0ef2c499ad98a8ce58e58"
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
symbol: "WETH",
|
|
304
|
+
address: "0x4200000000000000000000000000000000000006"
|
|
305
|
+
}
|
|
306
|
+
]
|
|
307
|
+
},
|
|
308
|
+
"137": {
|
|
309
|
+
name: "Polygon PoS",
|
|
310
|
+
rpcUrl: "https://polygon-bor-rpc.publicnode.com",
|
|
311
|
+
nativeAsset: {
|
|
312
|
+
symbol: "POL"
|
|
313
|
+
},
|
|
314
|
+
assets: [
|
|
315
|
+
{
|
|
316
|
+
symbol: "USDC",
|
|
317
|
+
address: "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359"
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
symbol: "USDT",
|
|
321
|
+
address: "0xc2132d05d31c914a87c6611c10748aeb04b58e8f"
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
symbol: "WETH",
|
|
325
|
+
address: "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619"
|
|
326
|
+
}
|
|
327
|
+
]
|
|
328
|
+
},
|
|
329
|
+
"56": {
|
|
330
|
+
name: "BNB Smart Chain",
|
|
331
|
+
rpcUrl: "https://bsc-rpc.publicnode.com",
|
|
332
|
+
nativeAsset: {
|
|
333
|
+
symbol: "BNB"
|
|
334
|
+
},
|
|
335
|
+
assets: [
|
|
336
|
+
{
|
|
337
|
+
symbol: "USDC",
|
|
338
|
+
address: "0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d"
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
symbol: "USDT",
|
|
342
|
+
address: "0x55d398326f99059ff775485246999027b3197955"
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
symbol: "WBNB",
|
|
346
|
+
address: "0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c"
|
|
347
|
+
}
|
|
348
|
+
]
|
|
349
|
+
},
|
|
350
|
+
"43114": {
|
|
351
|
+
name: "Avalanche C-Chain",
|
|
352
|
+
rpcUrl: "https://avalanche-c-chain-rpc.publicnode.com",
|
|
353
|
+
nativeAsset: {
|
|
354
|
+
symbol: "AVAX"
|
|
355
|
+
},
|
|
356
|
+
assets: [
|
|
357
|
+
{
|
|
358
|
+
symbol: "USDC",
|
|
359
|
+
address: "0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e"
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
symbol: "USDT",
|
|
363
|
+
address: "0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7"
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
symbol: "WAVAX",
|
|
367
|
+
address: "0xb31f66aa3c1e785363f0875a1b74e27b85fd66c7"
|
|
368
|
+
}
|
|
369
|
+
]
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// src/config/defaultGlobalConfigContents.ts
|
|
376
|
+
var parsedDefaultConfig = parseGlobalConfigData(config_default_default);
|
|
377
|
+
if (!parsedDefaultConfig.ok) {
|
|
378
|
+
throw new Error(
|
|
379
|
+
`invalid config.default.json: ${parsedDefaultConfig.error.message}`
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
var contents = `${JSON.stringify(parsedDefaultConfig.value, null, 2)}
|
|
383
|
+
`;
|
|
384
|
+
function defaultGlobalConfigContents() {
|
|
385
|
+
return contents;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// src/config/ensureGlobalConfig.ts
|
|
389
|
+
import { homedir as homedir2 } from "os";
|
|
390
|
+
async function ensureGlobalConfig(options = {}) {
|
|
391
|
+
const path = globalConfigPath(options.homeDirectory ?? homedir2());
|
|
392
|
+
const createFile = options.createFile ?? createFileIfAbsent;
|
|
393
|
+
try {
|
|
394
|
+
const creation = await createFile(
|
|
395
|
+
path,
|
|
396
|
+
defaultGlobalConfigContents()
|
|
397
|
+
);
|
|
398
|
+
return ok({ path, created: creation === "created" });
|
|
399
|
+
} catch (cause) {
|
|
400
|
+
return err({ kind: "write-failed", path, cause });
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// src/config/resetGlobalConfig.ts
|
|
405
|
+
import { homedir as homedir3 } from "os";
|
|
406
|
+
async function resetGlobalConfig(options = {}) {
|
|
407
|
+
const path = globalConfigPath(options.homeDirectory ?? homedir3());
|
|
408
|
+
const storage = options.storage ?? fileConfigStorage;
|
|
409
|
+
try {
|
|
410
|
+
await storage.write(path, defaultGlobalConfigContents());
|
|
411
|
+
return ok(path);
|
|
412
|
+
} catch (cause) {
|
|
413
|
+
return err({ kind: "write-failed", path, cause });
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// src/native.ts
|
|
418
|
+
import { dlopen, ptr, read } from "bun:ffi";
|
|
419
|
+
import { existsSync } from "fs";
|
|
420
|
+
import { join as join2, resolve } from "path";
|
|
421
|
+
var MACWLT_SUCCESS = 0;
|
|
422
|
+
var MACWLT_ERR_BUFFER_TOO_SMALL = 5;
|
|
423
|
+
var nativeSymbols = {
|
|
424
|
+
macwlt_wallet_create: { args: ["ptr"], returns: "int" },
|
|
425
|
+
macwlt_wallet_free: { args: ["ptr"], returns: "void" },
|
|
426
|
+
macwlt_last_error: { args: ["ptr"], returns: "int" },
|
|
427
|
+
macwlt_last_error_message: { args: ["ptr"], returns: "cstring" },
|
|
428
|
+
macwlt_reset_wallet: { args: ["ptr"], returns: "int" },
|
|
429
|
+
macwlt_bootstrap_wallet: { args: ["ptr", "ptr", "ptr"], returns: "int" },
|
|
430
|
+
macwlt_sign_psbt: { args: ["ptr", "ptr", "usize", "ptr", "ptr"], returns: "int" },
|
|
431
|
+
macwlt_sign_eth_tx: { args: ["ptr", "ptr", "usize", "ptr", "ptr"], returns: "int" },
|
|
432
|
+
macwlt_export_pubkey: { args: ["ptr", "cstring", "ptr", "ptr"], returns: "int" },
|
|
433
|
+
macwlt_export_address: { args: ["ptr", "cstring", "int", "ptr", "ptr"], returns: "int" }
|
|
434
|
+
};
|
|
435
|
+
function defaultLibraryPath(envPath) {
|
|
436
|
+
if (envPath && envPath.length > 0) return resolve(envPath);
|
|
437
|
+
const candidates = [
|
|
438
|
+
resolve(join2(import.meta.dir, "../native/libmacwlt.dylib")),
|
|
439
|
+
resolve(join2(process.cwd(), "build/libmacwlt.dylib")),
|
|
440
|
+
resolve(join2(process.cwd(), "../build/libmacwlt.dylib")),
|
|
441
|
+
resolve(join2(import.meta.dir, "../../build/libmacwlt.dylib")),
|
|
442
|
+
resolve(join2(import.meta.dir, "../../../build/libmacwlt.dylib"))
|
|
443
|
+
];
|
|
444
|
+
return candidates.find((candidate) => existsSync(candidate)) ?? candidates[0] ?? resolve("build/libmacwlt.dylib");
|
|
445
|
+
}
|
|
446
|
+
function openNativeClient(libraryPath) {
|
|
447
|
+
if (!existsSync(libraryPath)) return err({ kind: "missing-library", libraryPath });
|
|
448
|
+
let library;
|
|
449
|
+
try {
|
|
450
|
+
library = dlopen(libraryPath, nativeSymbols);
|
|
451
|
+
} catch (caught) {
|
|
452
|
+
return err({ kind: "load", libraryPath, message: messageFromUnknown(caught) });
|
|
453
|
+
}
|
|
454
|
+
const client = {
|
|
455
|
+
libraryPath,
|
|
456
|
+
close: () => library.close(),
|
|
457
|
+
withWallet: (body) => {
|
|
458
|
+
const walletResult = createWallet(library.symbols);
|
|
459
|
+
if (!walletResult.ok) return walletResult;
|
|
460
|
+
try {
|
|
461
|
+
return body(walletResult.value);
|
|
462
|
+
} finally {
|
|
463
|
+
walletResult.value.close();
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
return ok(client);
|
|
468
|
+
}
|
|
469
|
+
function createWallet(symbols) {
|
|
470
|
+
const walletSlot = new BigUint64Array(1);
|
|
471
|
+
const createStatus = symbols.macwlt_wallet_create(ptr(walletSlot));
|
|
472
|
+
const walletAddress = read.ptr(ptr(walletSlot));
|
|
473
|
+
if (createStatus !== MACWLT_SUCCESS || walletAddress === 0) {
|
|
474
|
+
return err({ kind: "wallet-create", message: "macwlt_wallet_create failed" });
|
|
475
|
+
}
|
|
476
|
+
const walletPointer = walletAddress;
|
|
477
|
+
const wallet = {
|
|
478
|
+
close: () => {
|
|
479
|
+
symbols.macwlt_wallet_free(walletPointer);
|
|
480
|
+
},
|
|
481
|
+
reset: () => {
|
|
482
|
+
const status = symbols.macwlt_reset_wallet(walletPointer);
|
|
483
|
+
if (status !== MACWLT_SUCCESS) {
|
|
484
|
+
return err(nativeError(symbols, walletPointer, "reset wallet", symbols.macwlt_last_error(walletPointer)));
|
|
485
|
+
}
|
|
486
|
+
return ok(void 0);
|
|
487
|
+
},
|
|
488
|
+
bootstrap: () => outputBytes(
|
|
489
|
+
symbols,
|
|
490
|
+
walletPointer,
|
|
491
|
+
"bootstrap",
|
|
492
|
+
33,
|
|
493
|
+
(output, length) => symbols.macwlt_bootstrap_wallet(walletPointer, output, length)
|
|
494
|
+
),
|
|
495
|
+
exportPubkey: (derivationPath) => outputBytes(
|
|
496
|
+
symbols,
|
|
497
|
+
walletPointer,
|
|
498
|
+
"export pubkey",
|
|
499
|
+
33,
|
|
500
|
+
(output, length) => symbols.macwlt_export_pubkey(walletPointer, cString(derivationPath), output, length)
|
|
501
|
+
),
|
|
502
|
+
exportAddress: (derivationPath, addressType) => {
|
|
503
|
+
const result = outputBytes(
|
|
504
|
+
symbols,
|
|
505
|
+
walletPointer,
|
|
506
|
+
"export address",
|
|
507
|
+
96,
|
|
508
|
+
(output, length) => symbols.macwlt_export_address(walletPointer, cString(derivationPath), nativeAddressType(addressType), output, length)
|
|
509
|
+
);
|
|
510
|
+
if (!result.ok) return result;
|
|
511
|
+
const terminator = result.value.indexOf(0);
|
|
512
|
+
const addressBytes = terminator >= 0 ? result.value.slice(0, terminator) : result.value;
|
|
513
|
+
return ok(new TextDecoder().decode(addressBytes));
|
|
514
|
+
},
|
|
515
|
+
signEthereumTransaction: (transaction) => outputBytes(
|
|
516
|
+
symbols,
|
|
517
|
+
walletPointer,
|
|
518
|
+
"sign ethereum transaction",
|
|
519
|
+
65,
|
|
520
|
+
(output, length) => symbols.macwlt_sign_eth_tx(walletPointer, ptr(transaction), transaction.length, output, length)
|
|
521
|
+
),
|
|
522
|
+
signPsbt: (psbt) => outputBytes(
|
|
523
|
+
symbols,
|
|
524
|
+
walletPointer,
|
|
525
|
+
"sign psbt",
|
|
526
|
+
Math.max(psbt.length + 1024, 1024),
|
|
527
|
+
(output, length) => symbols.macwlt_sign_psbt(walletPointer, ptr(psbt), psbt.length, output, length)
|
|
528
|
+
)
|
|
529
|
+
};
|
|
530
|
+
return ok(wallet);
|
|
531
|
+
}
|
|
532
|
+
function outputBytes(symbols, walletPointer, operation, initialCapacity, call) {
|
|
533
|
+
const firstLength = new BigUint64Array([BigInt(initialCapacity)]);
|
|
534
|
+
const firstBuffer = initialCapacity > 0 ? new Uint8Array(initialCapacity) : null;
|
|
535
|
+
const firstStatus = call(firstBuffer ? ptr(firstBuffer) : null, ptr(firstLength));
|
|
536
|
+
if (firstStatus === MACWLT_SUCCESS && firstBuffer) {
|
|
537
|
+
const lengthResult = sizeFromSlot(operation, firstLength);
|
|
538
|
+
if (!lengthResult.ok) return lengthResult;
|
|
539
|
+
return ok(firstBuffer.slice(0, lengthResult.value));
|
|
540
|
+
}
|
|
541
|
+
const firstError = symbols.macwlt_last_error(walletPointer);
|
|
542
|
+
if (firstError !== MACWLT_ERR_BUFFER_TOO_SMALL) {
|
|
543
|
+
return err(nativeError(symbols, walletPointer, operation, firstError));
|
|
544
|
+
}
|
|
545
|
+
const requiredLength = sizeFromSlot(operation, firstLength);
|
|
546
|
+
if (!requiredLength.ok) return requiredLength;
|
|
547
|
+
const retryLength = new BigUint64Array([BigInt(requiredLength.value)]);
|
|
548
|
+
const retryBuffer = new Uint8Array(requiredLength.value);
|
|
549
|
+
const retryStatus = call(ptr(retryBuffer), ptr(retryLength));
|
|
550
|
+
if (retryStatus !== MACWLT_SUCCESS) {
|
|
551
|
+
return err(nativeError(symbols, walletPointer, operation, symbols.macwlt_last_error(walletPointer)));
|
|
552
|
+
}
|
|
553
|
+
const finalLength = sizeFromSlot(operation, retryLength);
|
|
554
|
+
if (!finalLength.ok) return finalLength;
|
|
555
|
+
return ok(retryBuffer.slice(0, finalLength.value));
|
|
556
|
+
}
|
|
557
|
+
function sizeFromSlot(operation, slot) {
|
|
558
|
+
const size = slot[0] ?? 0n;
|
|
559
|
+
if (size > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
560
|
+
return err({ kind: "oversized-output", operation, size });
|
|
561
|
+
}
|
|
562
|
+
return ok(Number(size));
|
|
563
|
+
}
|
|
564
|
+
function cString(value) {
|
|
565
|
+
const encoded = new TextEncoder().encode(value);
|
|
566
|
+
const output = new Uint8Array(encoded.length + 1);
|
|
567
|
+
output.set(encoded);
|
|
568
|
+
return output;
|
|
569
|
+
}
|
|
570
|
+
function nativeAddressType(addressType) {
|
|
571
|
+
switch (addressType) {
|
|
572
|
+
case "bitcoin":
|
|
573
|
+
return 1;
|
|
574
|
+
case "bitcoin-testnet":
|
|
575
|
+
return 2;
|
|
576
|
+
case "ethereum":
|
|
577
|
+
return 3;
|
|
578
|
+
case "bitcoin-taproot":
|
|
579
|
+
return 4;
|
|
580
|
+
case "bitcoin-taproot-testnet":
|
|
581
|
+
return 5;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
function nativeError(symbols, walletPointer, operation, code) {
|
|
585
|
+
const nativeMessage = symbols.macwlt_last_error_message(walletPointer);
|
|
586
|
+
const message = nativeMessage ? nativeMessage.toString() : messageForNativeCode(code);
|
|
587
|
+
return { kind: "native", operation, code, message };
|
|
588
|
+
}
|
|
589
|
+
function messageForNativeCode(code) {
|
|
590
|
+
switch (code) {
|
|
591
|
+
case 1:
|
|
592
|
+
return "invalid argument";
|
|
593
|
+
case 2:
|
|
594
|
+
return "wallet data is unavailable";
|
|
595
|
+
case 3:
|
|
596
|
+
return "authentication is required";
|
|
597
|
+
case 4:
|
|
598
|
+
return "authentication failed";
|
|
599
|
+
case 5:
|
|
600
|
+
return "output buffer is too small";
|
|
601
|
+
case 6:
|
|
602
|
+
return "operation is unsupported";
|
|
603
|
+
case 7:
|
|
604
|
+
return "input parsing failed";
|
|
605
|
+
case 8:
|
|
606
|
+
return "signing failed";
|
|
607
|
+
case 9:
|
|
608
|
+
return "internal native error";
|
|
609
|
+
default:
|
|
610
|
+
return `unknown native error ${code}`;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
function messageFromUnknown(caught) {
|
|
614
|
+
if (caught instanceof Error) return caught.message;
|
|
615
|
+
return String(caught);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// src/nativeError.ts
|
|
619
|
+
function formatNativeError(error) {
|
|
620
|
+
switch (error.kind) {
|
|
621
|
+
case "missing-library":
|
|
622
|
+
return `native library not found at ${error.libraryPath}; reinstall @macwlt/cli, run make build in a checkout, or set MACWLT_LIB`;
|
|
623
|
+
case "load":
|
|
624
|
+
return `failed to load ${error.libraryPath}: ${error.message}`;
|
|
625
|
+
case "wallet-create":
|
|
626
|
+
return error.message;
|
|
627
|
+
case "native":
|
|
628
|
+
return `${error.operation} failed: ${error.message}`;
|
|
629
|
+
case "oversized-output":
|
|
630
|
+
return `${error.operation} produced an oversized output (${error.size.toString()} bytes)`;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
function formatExecutionError(error) {
|
|
634
|
+
return typeof error === "string" ? error : formatNativeError(error);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// src/parseFlags.ts
|
|
638
|
+
function parseFlags(args) {
|
|
639
|
+
let json = false;
|
|
640
|
+
const positionals = [];
|
|
641
|
+
const options = /* @__PURE__ */ new Map();
|
|
642
|
+
const switches = /* @__PURE__ */ new Set();
|
|
643
|
+
for (let index = 0; index < args.length; index++) {
|
|
644
|
+
const arg = args[index];
|
|
645
|
+
if (!arg) continue;
|
|
646
|
+
if (arg === "--json") {
|
|
647
|
+
json = true;
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
if (arg === "--reset" || arg === "--yes") {
|
|
651
|
+
switches.add(arg.slice(2));
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
if (!arg.startsWith("--")) {
|
|
655
|
+
positionals.push(arg);
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
const key = arg.slice(2);
|
|
659
|
+
if (key.length === 0) return err("empty option name");
|
|
660
|
+
const value = args[index + 1];
|
|
661
|
+
if (!value || value.startsWith("--")) return err(`missing value for --${key}`);
|
|
662
|
+
options.set(key, value);
|
|
663
|
+
index++;
|
|
664
|
+
}
|
|
665
|
+
return ok({ json, positionals, options, switches });
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// src/withWallet.ts
|
|
669
|
+
function runWithWallet(client, body) {
|
|
670
|
+
const result = client.withWallet(body);
|
|
671
|
+
if (!result.ok) return err(formatExecutionError(result.error));
|
|
672
|
+
return ok(result.value);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// src/hex.ts
|
|
676
|
+
function bytesToHex(bytes) {
|
|
677
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
678
|
+
}
|
|
679
|
+
function hexToBytes(input) {
|
|
680
|
+
const normalized = input.startsWith("0x") || input.startsWith("0X") ? input.slice(2) : input;
|
|
681
|
+
if (normalized.length === 0) return err({ kind: "empty" });
|
|
682
|
+
if (normalized.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(normalized)) {
|
|
683
|
+
return err({ kind: "invalid", value: input });
|
|
684
|
+
}
|
|
685
|
+
const bytes = new Uint8Array(normalized.length / 2);
|
|
686
|
+
for (let index = 0; index < bytes.length; index++) {
|
|
687
|
+
const pair = normalized.slice(index * 2, index * 2 + 2);
|
|
688
|
+
bytes[index] = Number.parseInt(pair, 16);
|
|
689
|
+
}
|
|
690
|
+
return ok(bytes);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// src/walletOutput.ts
|
|
694
|
+
function formatDataOutput(bytes, json, key) {
|
|
695
|
+
const hex = bytesToHex(bytes);
|
|
696
|
+
if (!json) return hex;
|
|
697
|
+
return JSON.stringify({ [key]: hex }, null, 2);
|
|
698
|
+
}
|
|
699
|
+
function formatPsbt(bytes, format) {
|
|
700
|
+
return format === "base64" ? bytesToBase64(bytes) : bytesToHex(bytes);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// src/commands/create.ts
|
|
704
|
+
var createCommand = {
|
|
705
|
+
name: "create",
|
|
706
|
+
describe() {
|
|
707
|
+
return " macwlt create [--reset] [--json]";
|
|
708
|
+
},
|
|
709
|
+
parse: parseCreate,
|
|
710
|
+
async run(ctx, args) {
|
|
711
|
+
const result = runWithWallet(ctx.client, (wallet) => {
|
|
712
|
+
if (args.reset) {
|
|
713
|
+
const reset = wallet.reset();
|
|
714
|
+
if (!reset.ok) return err(formatNativeError(reset.error));
|
|
715
|
+
}
|
|
716
|
+
const publicKey = wallet.bootstrap();
|
|
717
|
+
if (!publicKey.ok) return err(formatNativeError(publicKey.error));
|
|
718
|
+
return ok(formatDataOutput(publicKey.value, args.json, "jointPublicKey"));
|
|
719
|
+
});
|
|
720
|
+
return result;
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
function parseCreate(args) {
|
|
724
|
+
const flags = parseFlags(args);
|
|
725
|
+
if (!flags.ok) return flags;
|
|
726
|
+
if (flags.value.positionals.length > 0) return err("create does not accept positional arguments");
|
|
727
|
+
return ok({ reset: flags.value.switches.has("reset"), json: flags.value.json });
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// src/terminalConfirmationPrompt.ts
|
|
731
|
+
import { createInterface } from "readline/promises";
|
|
732
|
+
async function terminalConfirmationPrompt(question) {
|
|
733
|
+
const readline = createInterface({
|
|
734
|
+
input: process.stdin,
|
|
735
|
+
output: process.stderr
|
|
736
|
+
});
|
|
737
|
+
try {
|
|
738
|
+
return isAffirmativeConfirmation(await readline.question(question));
|
|
739
|
+
} catch {
|
|
740
|
+
return false;
|
|
741
|
+
} finally {
|
|
742
|
+
readline.close();
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
function isAffirmativeConfirmation(answer) {
|
|
746
|
+
const normalized = answer.trim().toLowerCase();
|
|
747
|
+
return normalized === "y" || normalized === "yes";
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// src/hooks/createConfirmationHook.ts
|
|
751
|
+
function createConfirmationHook(prompt = terminalConfirmationPrompt) {
|
|
752
|
+
return async () => {
|
|
753
|
+
const confirmed = await prompt("Continue? [y/N] ");
|
|
754
|
+
return confirmed ? ok(void 0) : err("command cancelled");
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// src/commands/reset.ts
|
|
759
|
+
var resetCommand = {
|
|
760
|
+
name: "reset",
|
|
761
|
+
beforeRun: createConfirmationHook(),
|
|
762
|
+
describe() {
|
|
763
|
+
return " macwlt reset --yes [--json]";
|
|
764
|
+
},
|
|
765
|
+
parse: parseReset,
|
|
766
|
+
async run(ctx, args) {
|
|
767
|
+
return runWithWallet(ctx.client, (wallet) => {
|
|
768
|
+
const reset = wallet.reset();
|
|
769
|
+
if (!reset.ok) return err(formatNativeError(reset.error));
|
|
770
|
+
if (!args.json) return ok("wallet reset");
|
|
771
|
+
return ok(JSON.stringify({ reset: true }, null, 2));
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
function parseReset(args) {
|
|
776
|
+
const flags = parseFlags(args);
|
|
777
|
+
if (!flags.ok) return flags;
|
|
778
|
+
if (flags.value.positionals.length > 0) return err("reset does not accept positional arguments");
|
|
779
|
+
if (!flags.value.switches.has("yes")) return err("reset requires --yes");
|
|
780
|
+
return ok({ json: flags.value.json });
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// src/commands/resetConfig.ts
|
|
784
|
+
var resetConfigCommand = {
|
|
785
|
+
name: "reset-config",
|
|
786
|
+
needsClient: false,
|
|
787
|
+
beforeRun: createConfirmationHook(),
|
|
788
|
+
describe() {
|
|
789
|
+
return " macwlt reset-config [--json]";
|
|
790
|
+
},
|
|
791
|
+
parse: parseResetConfig,
|
|
792
|
+
async run(ctx, args) {
|
|
793
|
+
const reset = await resetGlobalConfig({
|
|
794
|
+
homeDirectory: ctx.env.HOME?.length ? ctx.env.HOME : void 0,
|
|
795
|
+
storage: ctx.configStorage
|
|
796
|
+
});
|
|
797
|
+
if (!reset.ok) {
|
|
798
|
+
return err(
|
|
799
|
+
`failed to reset config ${reset.error.path}: ${messageFromUnknown2(reset.error.cause)}`
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
if (!args.json) return ok(`config reset to defaults: ${reset.value}`);
|
|
803
|
+
return ok(JSON.stringify({
|
|
804
|
+
reset: true,
|
|
805
|
+
path: reset.value
|
|
806
|
+
}, null, 2));
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
function parseResetConfig(args) {
|
|
810
|
+
const flags = parseFlags(args);
|
|
811
|
+
if (!flags.ok) return flags;
|
|
812
|
+
if (flags.value.positionals.length > 0) {
|
|
813
|
+
return err("reset-config does not accept positional arguments");
|
|
814
|
+
}
|
|
815
|
+
if (flags.value.options.size > 0 || flags.value.switches.size > 0) {
|
|
816
|
+
return err("reset-config only accepts --json");
|
|
817
|
+
}
|
|
818
|
+
return ok({ json: flags.value.json });
|
|
819
|
+
}
|
|
820
|
+
function messageFromUnknown2(value) {
|
|
821
|
+
return value instanceof Error ? value.message : String(value);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// src/commands/pubkey.ts
|
|
825
|
+
var pubkeyCommand = {
|
|
826
|
+
name: "pubkey",
|
|
827
|
+
describe() {
|
|
828
|
+
return " macwlt pubkey [path] [--json]";
|
|
829
|
+
},
|
|
830
|
+
parse: parsePubkey,
|
|
831
|
+
async run(ctx, args) {
|
|
832
|
+
return runWithWallet(ctx.client, (wallet) => {
|
|
833
|
+
const bootstrapped = wallet.bootstrap();
|
|
834
|
+
if (!bootstrapped.ok) return err(formatNativeError(bootstrapped.error));
|
|
835
|
+
const publicKey = wallet.exportPubkey(args.derivationPath);
|
|
836
|
+
if (!publicKey.ok) return err(formatNativeError(publicKey.error));
|
|
837
|
+
return ok(formatDataOutput(publicKey.value, args.json, "publicKey"));
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
function parsePubkey(args) {
|
|
842
|
+
const flags = parseFlags(args);
|
|
843
|
+
if (!flags.ok) return flags;
|
|
844
|
+
if (flags.value.positionals.length > 1) return err("pubkey accepts at most one derivation path");
|
|
845
|
+
return ok({ derivationPath: flags.value.positionals[0] ?? "m", json: flags.value.json });
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// src/commands/address.ts
|
|
849
|
+
var addressCommand = {
|
|
850
|
+
name: "address",
|
|
851
|
+
describe() {
|
|
852
|
+
return " macwlt address [path] --type bitcoin|bitcoin-testnet|bitcoin-taproot|bitcoin-taproot-testnet|ethereum [--json]";
|
|
853
|
+
},
|
|
854
|
+
parse: parseAddress,
|
|
855
|
+
async run(ctx, args) {
|
|
856
|
+
return runWithWallet(ctx.client, (wallet) => {
|
|
857
|
+
const bootstrapped = wallet.bootstrap();
|
|
858
|
+
if (!bootstrapped.ok) return err(formatNativeError(bootstrapped.error));
|
|
859
|
+
const address = wallet.exportAddress(args.derivationPath, args.addressType);
|
|
860
|
+
if (!address.ok) return err(formatNativeError(address.error));
|
|
861
|
+
if (!args.json) return ok(address.value);
|
|
862
|
+
return ok(JSON.stringify({ address: address.value, derivationPath: args.derivationPath, type: args.addressType }, null, 2));
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
function parseAddress(args) {
|
|
867
|
+
const flags = parseFlags(args);
|
|
868
|
+
if (!flags.ok) return flags;
|
|
869
|
+
if (flags.value.positionals.length > 1) return err("address accepts at most one derivation path");
|
|
870
|
+
const type = flags.value.options.get("type");
|
|
871
|
+
if (!isAddressType(type)) {
|
|
872
|
+
return err(
|
|
873
|
+
"address requires --type bitcoin|bitcoin-testnet|bitcoin-taproot|bitcoin-taproot-testnet|ethereum"
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
return ok({ derivationPath: flags.value.positionals[0] ?? "m", addressType: type, json: flags.value.json });
|
|
877
|
+
}
|
|
878
|
+
function isAddressType(value) {
|
|
879
|
+
return value === "bitcoin" || value === "bitcoin-testnet" || value === "bitcoin-taproot" || value === "bitcoin-taproot-testnet" || value === "ethereum";
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// src/bytesInput.ts
|
|
883
|
+
function parseBytesInput(options, allowed) {
|
|
884
|
+
const entries = allowed.flatMap((key) => {
|
|
885
|
+
const value = options.get(key);
|
|
886
|
+
return value ? [{ key, value }] : [];
|
|
887
|
+
});
|
|
888
|
+
if (entries.length !== 1) return err(`provide exactly one input option: ${allowed.map((key) => `--${key}`).join(", ")}`);
|
|
889
|
+
const input = entries[0];
|
|
890
|
+
if (!input) return err("missing input");
|
|
891
|
+
if (input.key === "hex") return ok({ kind: "hex", value: input.value });
|
|
892
|
+
if (input.key === "base64") return ok({ kind: "base64", value: input.value });
|
|
893
|
+
return ok({ kind: "file", path: input.value });
|
|
894
|
+
}
|
|
895
|
+
async function readInput(input) {
|
|
896
|
+
switch (input.kind) {
|
|
897
|
+
case "hex": {
|
|
898
|
+
const decoded = hexToBytes(input.value);
|
|
899
|
+
if (!decoded.ok) return err(decoded.error.kind === "empty" ? "hex input is empty" : `invalid hex input: ${decoded.error.value}`);
|
|
900
|
+
return decoded;
|
|
901
|
+
}
|
|
902
|
+
case "base64": {
|
|
903
|
+
const decoded = base64ToBytes(input.value);
|
|
904
|
+
if (!decoded.ok) return err(decoded.error.kind === "empty" ? "base64 input is empty" : "invalid base64 input");
|
|
905
|
+
return decoded;
|
|
906
|
+
}
|
|
907
|
+
case "file": {
|
|
908
|
+
try {
|
|
909
|
+
return ok(new Uint8Array(await Bun.file(input.path).arrayBuffer()));
|
|
910
|
+
} catch (caught) {
|
|
911
|
+
return err(`failed to read ${input.path}: ${caught instanceof Error ? caught.message : String(caught)}`);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// src/commands/signEth.ts
|
|
918
|
+
var signEthCommand = {
|
|
919
|
+
name: "sign-eth",
|
|
920
|
+
describe() {
|
|
921
|
+
return " macwlt sign-eth --hex <typed-transaction-preimage-hex> [--json]";
|
|
922
|
+
},
|
|
923
|
+
parse: parseSignEth,
|
|
924
|
+
async run(ctx, args) {
|
|
925
|
+
const transaction = await readInput(args.input);
|
|
926
|
+
if (!transaction.ok) return transaction;
|
|
927
|
+
return runWithWallet(ctx.client, (wallet) => {
|
|
928
|
+
const bootstrapped = wallet.bootstrap();
|
|
929
|
+
if (!bootstrapped.ok) return err(formatNativeError(bootstrapped.error));
|
|
930
|
+
const signature = wallet.signEthereumTransaction(transaction.value);
|
|
931
|
+
if (!signature.ok) return err(formatNativeError(signature.error));
|
|
932
|
+
return ok(formatDataOutput(signature.value, args.json, "signature"));
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
function parseSignEth(args) {
|
|
937
|
+
const flags = parseFlags(args);
|
|
938
|
+
if (!flags.ok) return flags;
|
|
939
|
+
if (flags.value.positionals.length > 0) return err("sign-eth does not accept positional arguments");
|
|
940
|
+
const input = parseBytesInput(flags.value.options, ["hex", "in"]);
|
|
941
|
+
if (!input.ok) return input;
|
|
942
|
+
return ok({ input: input.value, json: flags.value.json });
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// src/commands/signPsbt.ts
|
|
946
|
+
var signPsbtCommand = {
|
|
947
|
+
name: "sign-psbt",
|
|
948
|
+
describe() {
|
|
949
|
+
return [
|
|
950
|
+
" macwlt sign-psbt --base64 <psbt> [--format base64|hex] [--json]",
|
|
951
|
+
" macwlt sign-psbt --hex <psbt-hex> [--out <file>] [--format base64|hex|raw]",
|
|
952
|
+
" macwlt sign-psbt --in <file> [--out <file>] [--format base64|hex|raw]"
|
|
953
|
+
].join("\n");
|
|
954
|
+
},
|
|
955
|
+
parse: parseSignPsbt,
|
|
956
|
+
async run(ctx, args) {
|
|
957
|
+
const psbt = await readInput(args.input);
|
|
958
|
+
if (!psbt.ok) return psbt;
|
|
959
|
+
const signedResult = runWithWallet(ctx.client, (wallet) => {
|
|
960
|
+
const bootstrapped = wallet.bootstrap();
|
|
961
|
+
if (!bootstrapped.ok) return err(formatNativeError(bootstrapped.error));
|
|
962
|
+
const signedPsbt = wallet.signPsbt(psbt.value);
|
|
963
|
+
if (!signedPsbt.ok) return err(formatNativeError(signedPsbt.error));
|
|
964
|
+
return ok(signedPsbt.value);
|
|
965
|
+
});
|
|
966
|
+
if (!signedResult.ok) return signedResult;
|
|
967
|
+
if (args.output.kind === "file") {
|
|
968
|
+
const bytes = args.format === "raw" ? signedResult.value : new TextEncoder().encode(formatPsbt(signedResult.value, args.format));
|
|
969
|
+
await Bun.write(args.output.path, bytes);
|
|
970
|
+
return ok(args.json ? JSON.stringify({ output: args.output.path, format: args.format }, null, 2) : args.output.path);
|
|
971
|
+
}
|
|
972
|
+
if (args.format === "raw") return err("--format raw requires --out <file>");
|
|
973
|
+
if (args.json) return ok(JSON.stringify({ signedPsbt: formatPsbt(signedResult.value, args.format), format: args.format }, null, 2));
|
|
974
|
+
return ok(formatPsbt(signedResult.value, args.format));
|
|
975
|
+
}
|
|
976
|
+
};
|
|
977
|
+
function parseSignPsbt(args) {
|
|
978
|
+
const flags = parseFlags(args);
|
|
979
|
+
if (!flags.ok) return flags;
|
|
980
|
+
if (flags.value.positionals.length > 0) return err("sign-psbt does not accept positional arguments");
|
|
981
|
+
const input = parseBytesInput(flags.value.options, ["base64", "hex", "in"]);
|
|
982
|
+
if (!input.ok) return input;
|
|
983
|
+
const format = flags.value.options.get("format") ?? "base64";
|
|
984
|
+
if (!isPsbtOutputFormat(format)) return err("--format must be base64, hex, or raw");
|
|
985
|
+
const out = flags.value.options.get("out");
|
|
986
|
+
return ok({
|
|
987
|
+
input: input.value,
|
|
988
|
+
output: out ? { kind: "file", path: out } : { kind: "stdout" },
|
|
989
|
+
format,
|
|
990
|
+
json: flags.value.json
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
function isPsbtOutputFormat(value) {
|
|
994
|
+
return value === "base64" || value === "hex" || value === "raw";
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// src/commands/help.ts
|
|
998
|
+
var helpCommand = {
|
|
999
|
+
name: "help",
|
|
1000
|
+
aliases: ["--help", "-h"],
|
|
1001
|
+
needsClient: false,
|
|
1002
|
+
describe() {
|
|
1003
|
+
return "";
|
|
1004
|
+
},
|
|
1005
|
+
parse(_args) {
|
|
1006
|
+
return ok({ kind: "help" });
|
|
1007
|
+
},
|
|
1008
|
+
async run(ctx, _args) {
|
|
1009
|
+
return ok(helpText(ctx.registry));
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
|
|
1013
|
+
// src/commands/version.ts
|
|
1014
|
+
var versionCommand = {
|
|
1015
|
+
name: "version",
|
|
1016
|
+
aliases: ["--version", "-v"],
|
|
1017
|
+
needsClient: false,
|
|
1018
|
+
describe() {
|
|
1019
|
+
return " macwlt version";
|
|
1020
|
+
},
|
|
1021
|
+
parse(_args) {
|
|
1022
|
+
return ok({ kind: "version" });
|
|
1023
|
+
},
|
|
1024
|
+
async run(_ctx, _args) {
|
|
1025
|
+
return ok(`macwlt ${cliVersion}`);
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
|
|
1029
|
+
// src/commands/send.ts
|
|
1030
|
+
import { isAddress as isAddress4 } from "viem";
|
|
1031
|
+
|
|
1032
|
+
// src/ethereum/EthereumConfig.ts
|
|
1033
|
+
import { z as z3 } from "zod";
|
|
1034
|
+
var rpcUrlSchema = z3.string().superRefine((value, context) => {
|
|
1035
|
+
try {
|
|
1036
|
+
const protocol = new URL(value).protocol;
|
|
1037
|
+
if (protocol === "http:" || protocol === "https:") return;
|
|
1038
|
+
} catch {
|
|
1039
|
+
}
|
|
1040
|
+
context.addIssue({ code: z3.ZodIssueCode.custom, message: "Invalid RPC URL" });
|
|
1041
|
+
});
|
|
1042
|
+
var chainIdSchema = z3.number().int().positive().safe();
|
|
1043
|
+
function parseEthereumConfig(input) {
|
|
1044
|
+
const rpcUrl = rpcUrlSchema.safeParse(input.rpcUrl);
|
|
1045
|
+
if (!rpcUrl.success) {
|
|
1046
|
+
return err({ kind: "invalid-rpc-url", value: input.rpcUrl });
|
|
1047
|
+
}
|
|
1048
|
+
const chainId = chainIdSchema.safeParse(input.chainId);
|
|
1049
|
+
if (!chainId.success) {
|
|
1050
|
+
return err({ kind: "invalid-chain-id", value: input.chainId });
|
|
1051
|
+
}
|
|
1052
|
+
return ok({
|
|
1053
|
+
rpcUrl: rpcUrl.data,
|
|
1054
|
+
chainId: chainId.data
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// src/ethereum/EvmCall.ts
|
|
1059
|
+
import { z as z4 } from "zod";
|
|
1060
|
+
var addressSchema = z4.string().regex(/^0x[0-9a-fA-F]{40}$/);
|
|
1061
|
+
var hexSchema = z4.string().regex(/^0x(?:[0-9a-fA-F]{2})*$/);
|
|
1062
|
+
var quantitySchema = z4.bigint().nonnegative();
|
|
1063
|
+
var blockSchema = z4.union([
|
|
1064
|
+
z4.enum(["earliest", "finalized", "latest", "pending", "safe"]),
|
|
1065
|
+
quantitySchema
|
|
1066
|
+
]);
|
|
1067
|
+
var evmCallRequestSchema = z4.object({
|
|
1068
|
+
to: addressSchema,
|
|
1069
|
+
data: hexSchema.optional(),
|
|
1070
|
+
from: addressSchema.optional(),
|
|
1071
|
+
gas: quantitySchema.optional(),
|
|
1072
|
+
gasPrice: quantitySchema.optional(),
|
|
1073
|
+
value: quantitySchema.optional(),
|
|
1074
|
+
block: blockSchema.optional()
|
|
1075
|
+
}).strict();
|
|
1076
|
+
var evmCallResultSchema = z4.object({
|
|
1077
|
+
data: hexSchema.optional()
|
|
1078
|
+
});
|
|
1079
|
+
function parseEvmCallRequest(input) {
|
|
1080
|
+
const parsed = evmCallRequestSchema.safeParse(input);
|
|
1081
|
+
if (!parsed.success) {
|
|
1082
|
+
return err({ message: parsed.error.issues.map((issue) => issue.message).join("; ") });
|
|
1083
|
+
}
|
|
1084
|
+
return ok(parsed.data);
|
|
1085
|
+
}
|
|
1086
|
+
function parseEvmCallResult(input) {
|
|
1087
|
+
const parsed = evmCallResultSchema.safeParse(input);
|
|
1088
|
+
if (!parsed.success) {
|
|
1089
|
+
return err({ message: parsed.error.issues.map((issue) => issue.message).join("; ") });
|
|
1090
|
+
}
|
|
1091
|
+
return ok(parsed.data);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// src/ethereum/EthereumClient.ts
|
|
1095
|
+
import { z as z5 } from "zod";
|
|
1096
|
+
var addressSchema2 = z5.string().regex(/^0x[0-9a-fA-F]{40}$/);
|
|
1097
|
+
var transactionRequestSchema = z5.object({
|
|
1098
|
+
from: addressSchema2,
|
|
1099
|
+
to: addressSchema2,
|
|
1100
|
+
data: z5.string().regex(/^0x(?:[0-9a-fA-F]{2})*$/).optional(),
|
|
1101
|
+
value: z5.bigint().nonnegative().optional()
|
|
1102
|
+
}).strict();
|
|
1103
|
+
var chainIdSchema2 = z5.number().int().positive().safe();
|
|
1104
|
+
var transactionCountSchema = z5.number().int().nonnegative().safe();
|
|
1105
|
+
var quantitySchema2 = z5.bigint().nonnegative();
|
|
1106
|
+
var transactionHashSchema = z5.custom(
|
|
1107
|
+
(value) => typeof value === "string" && /^0x[0-9a-fA-F]{64}$/.test(value)
|
|
1108
|
+
);
|
|
1109
|
+
var EthereumClient = class _EthereumClient {
|
|
1110
|
+
#config;
|
|
1111
|
+
#transport;
|
|
1112
|
+
#chainVerification;
|
|
1113
|
+
constructor(config, transport) {
|
|
1114
|
+
this.#config = config;
|
|
1115
|
+
this.#transport = transport;
|
|
1116
|
+
}
|
|
1117
|
+
static create(input, createTransport) {
|
|
1118
|
+
const config = parseEthereumConfig(input);
|
|
1119
|
+
if (!config.ok) return config;
|
|
1120
|
+
try {
|
|
1121
|
+
return ok(new _EthereumClient(config.value, createTransport(config.value)));
|
|
1122
|
+
} catch (cause) {
|
|
1123
|
+
return err({ kind: "transport-creation-failed", cause });
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
get config() {
|
|
1127
|
+
return this.#config;
|
|
1128
|
+
}
|
|
1129
|
+
async call(request) {
|
|
1130
|
+
const parsedRequest = parseEvmCallRequest(request);
|
|
1131
|
+
if (!parsedRequest.ok) {
|
|
1132
|
+
return err({ kind: "invalid-request", message: parsedRequest.error.message });
|
|
1133
|
+
}
|
|
1134
|
+
let response;
|
|
1135
|
+
try {
|
|
1136
|
+
response = await this.#transport.call(parsedRequest.value);
|
|
1137
|
+
} catch (cause) {
|
|
1138
|
+
return err({ kind: "transport-failed", cause });
|
|
1139
|
+
}
|
|
1140
|
+
const parsedResponse = parseEvmCallResult(response);
|
|
1141
|
+
if (!parsedResponse.ok) {
|
|
1142
|
+
return err({ kind: "invalid-response", message: parsedResponse.error.message });
|
|
1143
|
+
}
|
|
1144
|
+
return ok(parsedResponse.value);
|
|
1145
|
+
}
|
|
1146
|
+
async verifyChain() {
|
|
1147
|
+
this.#chainVerification ??= this.#verifyConfiguredChain();
|
|
1148
|
+
return await this.#chainVerification;
|
|
1149
|
+
}
|
|
1150
|
+
async #verifyConfiguredChain() {
|
|
1151
|
+
const chainId = await this.#performTransactionOperation(
|
|
1152
|
+
"get-chain-id",
|
|
1153
|
+
(transport) => transport.getChainId(),
|
|
1154
|
+
chainIdSchema2
|
|
1155
|
+
);
|
|
1156
|
+
if (!chainId.ok) return chainId;
|
|
1157
|
+
if (chainId.value !== this.#config.chainId) {
|
|
1158
|
+
return err({
|
|
1159
|
+
kind: "chain-mismatch",
|
|
1160
|
+
expected: this.#config.chainId,
|
|
1161
|
+
actual: chainId.value
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
return ok(void 0);
|
|
1165
|
+
}
|
|
1166
|
+
async getTransactionCount(address) {
|
|
1167
|
+
if (!addressSchema2.safeParse(address).success) {
|
|
1168
|
+
return err({ kind: "invalid-request", message: "invalid Ethereum address" });
|
|
1169
|
+
}
|
|
1170
|
+
return await this.#performTransactionOperation(
|
|
1171
|
+
"get-transaction-count",
|
|
1172
|
+
(transport) => transport.getTransactionCount(address),
|
|
1173
|
+
transactionCountSchema
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
async estimateGas(request) {
|
|
1177
|
+
const parsed = transactionRequestSchema.safeParse(request);
|
|
1178
|
+
if (!parsed.success) {
|
|
1179
|
+
return err({
|
|
1180
|
+
kind: "invalid-request",
|
|
1181
|
+
message: parsed.error.issues.map((issue) => issue.message).join("; ")
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
return await this.#performTransactionOperation(
|
|
1185
|
+
"estimate-gas",
|
|
1186
|
+
(transport) => transport.estimateGas(parsed.data),
|
|
1187
|
+
quantitySchema2
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
async getBalance(address) {
|
|
1191
|
+
if (!addressSchema2.safeParse(address).success) {
|
|
1192
|
+
return err({ kind: "invalid-request", message: "invalid Ethereum address" });
|
|
1193
|
+
}
|
|
1194
|
+
return await this.#performTransactionOperation(
|
|
1195
|
+
"get-balance",
|
|
1196
|
+
(transport) => transport.getBalance(address),
|
|
1197
|
+
quantitySchema2
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
async getGasPrice() {
|
|
1201
|
+
return await this.#performTransactionOperation(
|
|
1202
|
+
"get-gas-price",
|
|
1203
|
+
(transport) => transport.getGasPrice(),
|
|
1204
|
+
quantitySchema2
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
async sendRawTransaction(transaction) {
|
|
1208
|
+
if (!/^0x(?:[0-9a-fA-F]{2})+$/.test(transaction)) {
|
|
1209
|
+
return err({ kind: "invalid-request", message: "invalid serialized transaction" });
|
|
1210
|
+
}
|
|
1211
|
+
return await this.#performTransactionOperation(
|
|
1212
|
+
"send-raw-transaction",
|
|
1213
|
+
(transport) => transport.sendRawTransaction(transaction),
|
|
1214
|
+
transactionHashSchema
|
|
1215
|
+
);
|
|
1216
|
+
}
|
|
1217
|
+
async #performTransactionOperation(operation, perform, schema) {
|
|
1218
|
+
if (!isEvmTransport(this.#transport)) {
|
|
1219
|
+
return err({ kind: "unsupported-transport" });
|
|
1220
|
+
}
|
|
1221
|
+
let response;
|
|
1222
|
+
try {
|
|
1223
|
+
response = await perform(this.#transport);
|
|
1224
|
+
} catch (cause) {
|
|
1225
|
+
return err({ kind: "transport-failed", operation, cause });
|
|
1226
|
+
}
|
|
1227
|
+
const parsed = schema.safeParse(response);
|
|
1228
|
+
if (!parsed.success) {
|
|
1229
|
+
return err({
|
|
1230
|
+
kind: "invalid-response",
|
|
1231
|
+
operation,
|
|
1232
|
+
message: parsed.error.issues.map((issue) => issue.message).join("; ")
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
return ok(parsed.data);
|
|
1236
|
+
}
|
|
1237
|
+
};
|
|
1238
|
+
function isEvmTransport(transport) {
|
|
1239
|
+
const candidate = transport;
|
|
1240
|
+
return typeof candidate.getChainId === "function" && typeof candidate.getTransactionCount === "function" && typeof candidate.getBalance === "function" && typeof candidate.estimateGas === "function" && typeof candidate.getGasPrice === "function" && typeof candidate.sendRawTransaction === "function";
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// src/ethereum/EthereumAsset.ts
|
|
1244
|
+
import { isAddress } from "viem";
|
|
1245
|
+
function parseEthereumAsset(input) {
|
|
1246
|
+
if (input.toUpperCase() === "ETH") return ok({ kind: "native-eth" });
|
|
1247
|
+
if (!isAddress(input)) return err("invalid-ethereum-asset");
|
|
1248
|
+
return ok({ kind: "erc20", tokenAddress: input });
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// src/ethereum/decodeErc20Decimals.ts
|
|
1252
|
+
import { decodeFunctionResult } from "viem";
|
|
1253
|
+
var decimalsAbi = [{
|
|
1254
|
+
type: "function",
|
|
1255
|
+
name: "decimals",
|
|
1256
|
+
stateMutability: "view",
|
|
1257
|
+
inputs: [],
|
|
1258
|
+
outputs: [{ name: "", type: "uint8" }]
|
|
1259
|
+
}];
|
|
1260
|
+
function decodeErc20Decimals(data) {
|
|
1261
|
+
try {
|
|
1262
|
+
const decimals = decodeFunctionResult({
|
|
1263
|
+
abi: decimalsAbi,
|
|
1264
|
+
functionName: "decimals",
|
|
1265
|
+
data
|
|
1266
|
+
});
|
|
1267
|
+
if (!Number.isSafeInteger(decimals) || decimals < 0 || decimals > 255) {
|
|
1268
|
+
return err({
|
|
1269
|
+
kind: "invalid-token-decimals",
|
|
1270
|
+
message: "token returned decimals outside the uint8 range"
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
return ok(decimals);
|
|
1274
|
+
} catch (caught) {
|
|
1275
|
+
return err({
|
|
1276
|
+
kind: "invalid-token-decimals",
|
|
1277
|
+
message: caught instanceof Error ? caught.message : String(caught)
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// src/ethereum/decodeErc20Balance.ts
|
|
1283
|
+
import { decodeFunctionResult as decodeFunctionResult2 } from "viem";
|
|
1284
|
+
var balanceOfAbi = [{
|
|
1285
|
+
type: "function",
|
|
1286
|
+
name: "balanceOf",
|
|
1287
|
+
stateMutability: "view",
|
|
1288
|
+
inputs: [{ name: "account", type: "address" }],
|
|
1289
|
+
outputs: [{ name: "", type: "uint256" }]
|
|
1290
|
+
}];
|
|
1291
|
+
function decodeErc20Balance(data) {
|
|
1292
|
+
try {
|
|
1293
|
+
return ok(decodeFunctionResult2({
|
|
1294
|
+
abi: balanceOfAbi,
|
|
1295
|
+
functionName: "balanceOf",
|
|
1296
|
+
data
|
|
1297
|
+
}));
|
|
1298
|
+
} catch (caught) {
|
|
1299
|
+
return err({
|
|
1300
|
+
kind: "invalid-token-balance",
|
|
1301
|
+
message: caught instanceof Error ? caught.message : String(caught)
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// src/ethereum/encodeErc20BalanceOf.ts
|
|
1307
|
+
import { encodeFunctionData } from "viem";
|
|
1308
|
+
var balanceOfAbi2 = [{
|
|
1309
|
+
type: "function",
|
|
1310
|
+
name: "balanceOf",
|
|
1311
|
+
stateMutability: "view",
|
|
1312
|
+
inputs: [{ name: "account", type: "address" }],
|
|
1313
|
+
outputs: [{ name: "", type: "uint256" }]
|
|
1314
|
+
}];
|
|
1315
|
+
function encodeErc20BalanceOf(account) {
|
|
1316
|
+
return encodeFunctionData({
|
|
1317
|
+
abi: balanceOfAbi2,
|
|
1318
|
+
functionName: "balanceOf",
|
|
1319
|
+
args: [account]
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// src/ethereum/encodeErc20DecimalsCall.ts
|
|
1324
|
+
import { encodeFunctionData as encodeFunctionData2 } from "viem";
|
|
1325
|
+
var decimalsAbi2 = [{
|
|
1326
|
+
type: "function",
|
|
1327
|
+
name: "decimals",
|
|
1328
|
+
stateMutability: "view",
|
|
1329
|
+
inputs: [],
|
|
1330
|
+
outputs: [{ name: "", type: "uint8" }]
|
|
1331
|
+
}];
|
|
1332
|
+
function encodeErc20DecimalsCall() {
|
|
1333
|
+
return encodeFunctionData2({
|
|
1334
|
+
abi: decimalsAbi2,
|
|
1335
|
+
functionName: "decimals"
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// src/ethereum/encodeErc20Transfer.ts
|
|
1340
|
+
import { encodeFunctionData as encodeFunctionData3 } from "viem";
|
|
1341
|
+
var transferAbi = [{
|
|
1342
|
+
type: "function",
|
|
1343
|
+
name: "transfer",
|
|
1344
|
+
stateMutability: "nonpayable",
|
|
1345
|
+
inputs: [
|
|
1346
|
+
{ name: "to", type: "address" },
|
|
1347
|
+
{ name: "amount", type: "uint256" }
|
|
1348
|
+
],
|
|
1349
|
+
outputs: [{ name: "", type: "bool" }]
|
|
1350
|
+
}];
|
|
1351
|
+
function encodeErc20Transfer(recipient, amount) {
|
|
1352
|
+
return encodeFunctionData3({
|
|
1353
|
+
abi: transferAbi,
|
|
1354
|
+
functionName: "transfer",
|
|
1355
|
+
args: [recipient, amount]
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// src/ethereum/getEthereumAssetBalance.ts
|
|
1360
|
+
import { formatUnits } from "viem";
|
|
1361
|
+
async function getEthereumAssetBalance(client, address, asset) {
|
|
1362
|
+
const verified = await client.verifyChain();
|
|
1363
|
+
if (!verified.ok) {
|
|
1364
|
+
return err({ kind: "chain-client", stage: "verify-chain", error: verified.error });
|
|
1365
|
+
}
|
|
1366
|
+
if (asset.kind === "native-eth") {
|
|
1367
|
+
const balance2 = await client.getBalance(address);
|
|
1368
|
+
if (!balance2.ok) {
|
|
1369
|
+
return err({
|
|
1370
|
+
kind: "chain-client",
|
|
1371
|
+
stage: "get-native-balance",
|
|
1372
|
+
error: balance2.error
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
return ok({
|
|
1376
|
+
kind: "native-eth",
|
|
1377
|
+
address,
|
|
1378
|
+
balance: formatUnits(balance2.value, 18),
|
|
1379
|
+
balanceBaseUnits: balance2.value,
|
|
1380
|
+
decimals: 18
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
const [balanceCall, decimalsCall] = await Promise.all([
|
|
1384
|
+
client.call({
|
|
1385
|
+
to: asset.tokenAddress,
|
|
1386
|
+
data: encodeErc20BalanceOf(address)
|
|
1387
|
+
}),
|
|
1388
|
+
client.call({
|
|
1389
|
+
to: asset.tokenAddress,
|
|
1390
|
+
data: encodeErc20DecimalsCall()
|
|
1391
|
+
})
|
|
1392
|
+
]);
|
|
1393
|
+
if (!balanceCall.ok) {
|
|
1394
|
+
return err({
|
|
1395
|
+
kind: "chain-client",
|
|
1396
|
+
stage: "read-token-balance",
|
|
1397
|
+
error: balanceCall.error
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
if (!decimalsCall.ok) {
|
|
1401
|
+
return err({
|
|
1402
|
+
kind: "chain-client",
|
|
1403
|
+
stage: "read-token-decimals",
|
|
1404
|
+
error: decimalsCall.error
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
if (balanceCall.value.data === void 0) {
|
|
1408
|
+
return err({ kind: "missing-token-balance" });
|
|
1409
|
+
}
|
|
1410
|
+
if (decimalsCall.value.data === void 0) {
|
|
1411
|
+
return err({ kind: "missing-token-decimals" });
|
|
1412
|
+
}
|
|
1413
|
+
const balance = decodeErc20Balance(balanceCall.value.data);
|
|
1414
|
+
if (!balance.ok) return err({ kind: "token-balance", error: balance.error });
|
|
1415
|
+
const decimals = decodeErc20Decimals(decimalsCall.value.data);
|
|
1416
|
+
if (!decimals.ok) return err({ kind: "token-decimals", error: decimals.error });
|
|
1417
|
+
return ok({
|
|
1418
|
+
kind: "erc20",
|
|
1419
|
+
address,
|
|
1420
|
+
tokenAddress: asset.tokenAddress,
|
|
1421
|
+
balance: formatUnits(balance.value, decimals.value),
|
|
1422
|
+
balanceBaseUnits: balance.value,
|
|
1423
|
+
decimals: decimals.value
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
// src/ethereum/getEthereumPortfolioBalances.ts
|
|
1428
|
+
async function getEthereumPortfolioBalances(chains, address, createTransport) {
|
|
1429
|
+
return await Promise.all(
|
|
1430
|
+
chains.map(async (chain) => {
|
|
1431
|
+
const client = EthereumClient.create(
|
|
1432
|
+
{ chainId: chain.chainId, rpcUrl: chain.rpcUrl },
|
|
1433
|
+
createTransport
|
|
1434
|
+
);
|
|
1435
|
+
if (!client.ok) {
|
|
1436
|
+
return { status: "failed", chain, error: client.error };
|
|
1437
|
+
}
|
|
1438
|
+
const assets = [
|
|
1439
|
+
{
|
|
1440
|
+
symbol: chain.nativeSymbol,
|
|
1441
|
+
asset: { kind: "native-eth" }
|
|
1442
|
+
},
|
|
1443
|
+
...chain.assets.map((asset) => ({
|
|
1444
|
+
symbol: asset.symbol,
|
|
1445
|
+
asset: {
|
|
1446
|
+
kind: "erc20",
|
|
1447
|
+
tokenAddress: asset.address
|
|
1448
|
+
}
|
|
1449
|
+
}))
|
|
1450
|
+
];
|
|
1451
|
+
const balances = await Promise.all(
|
|
1452
|
+
assets.map(async ({ symbol, asset }) => {
|
|
1453
|
+
const balance = await getEthereumAssetBalance(
|
|
1454
|
+
client.value,
|
|
1455
|
+
address,
|
|
1456
|
+
asset
|
|
1457
|
+
);
|
|
1458
|
+
return balance.ok ? { status: "fulfilled", symbol, balance: balance.value } : { status: "failed", symbol, asset, error: balance.error };
|
|
1459
|
+
})
|
|
1460
|
+
);
|
|
1461
|
+
return { status: "fulfilled", chain, assets: balances };
|
|
1462
|
+
})
|
|
1463
|
+
);
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// src/ethereum/parseEthereumPortfolioConfig.ts
|
|
1467
|
+
import { isAddress as isAddress2 } from "viem";
|
|
1468
|
+
import { z as z6 } from "zod";
|
|
1469
|
+
var symbolSchema = z6.string().trim().min(1).max(16).regex(/^[A-Za-z0-9._-]+$/);
|
|
1470
|
+
var assetSchema = z6.object({
|
|
1471
|
+
symbol: symbolSchema,
|
|
1472
|
+
address: z6.string().refine(isAddress2, "invalid Ethereum address")
|
|
1473
|
+
}).strict();
|
|
1474
|
+
var chainSchema = z6.object({
|
|
1475
|
+
name: z6.string().trim().min(1),
|
|
1476
|
+
rpcUrl: z6.string(),
|
|
1477
|
+
nativeAsset: z6.object({
|
|
1478
|
+
symbol: symbolSchema
|
|
1479
|
+
}).strict(),
|
|
1480
|
+
assets: z6.array(assetSchema)
|
|
1481
|
+
}).strict();
|
|
1482
|
+
var portfolioSchema = z6.object({
|
|
1483
|
+
ethereum: z6.object({
|
|
1484
|
+
chains: z6.record(chainSchema)
|
|
1485
|
+
}).passthrough()
|
|
1486
|
+
}).passthrough();
|
|
1487
|
+
function parseEthereumPortfolioConfig(input) {
|
|
1488
|
+
const parsed = portfolioSchema.safeParse(input);
|
|
1489
|
+
if (!parsed.success) {
|
|
1490
|
+
return err({
|
|
1491
|
+
kind: "invalid-portfolio-config",
|
|
1492
|
+
message: parsed.error.issues.map((issue) => `${issue.path.join(".") || "config"}: ${issue.message}`).join("; ")
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
const chains = [];
|
|
1496
|
+
for (const [chainIdValue, chain] of Object.entries(parsed.data.ethereum.chains)) {
|
|
1497
|
+
const chainId = Number(chainIdValue);
|
|
1498
|
+
if (!Number.isSafeInteger(chainId) || chainId <= 0) {
|
|
1499
|
+
return invalid(`ethereum.chains.${chainIdValue}: invalid chain ID`);
|
|
1500
|
+
}
|
|
1501
|
+
const ethereumConfig = parseEthereumConfig({
|
|
1502
|
+
chainId,
|
|
1503
|
+
rpcUrl: chain.rpcUrl
|
|
1504
|
+
});
|
|
1505
|
+
if (!ethereumConfig.ok) {
|
|
1506
|
+
return invalid(`ethereum.chains.${chainIdValue}.rpcUrl: invalid HTTP(S) URL`);
|
|
1507
|
+
}
|
|
1508
|
+
const symbols = /* @__PURE__ */ new Set();
|
|
1509
|
+
const addresses = /* @__PURE__ */ new Set();
|
|
1510
|
+
for (const asset of chain.assets) {
|
|
1511
|
+
const symbol = asset.symbol.toUpperCase();
|
|
1512
|
+
const address = asset.address.toLowerCase();
|
|
1513
|
+
if (symbol === chain.nativeAsset.symbol.toUpperCase()) {
|
|
1514
|
+
return invalid(
|
|
1515
|
+
`ethereum.chains.${chainIdValue}.assets: ${asset.symbol} duplicates the native symbol`
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
if (symbols.has(symbol)) {
|
|
1519
|
+
return invalid(
|
|
1520
|
+
`ethereum.chains.${chainIdValue}.assets: duplicate symbol ${asset.symbol}`
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
1523
|
+
if (addresses.has(address)) {
|
|
1524
|
+
return invalid(
|
|
1525
|
+
`ethereum.chains.${chainIdValue}.assets: duplicate address ${asset.address}`
|
|
1526
|
+
);
|
|
1527
|
+
}
|
|
1528
|
+
symbols.add(symbol);
|
|
1529
|
+
addresses.add(address);
|
|
1530
|
+
}
|
|
1531
|
+
chains.push({
|
|
1532
|
+
chainId,
|
|
1533
|
+
name: chain.name,
|
|
1534
|
+
rpcUrl: ethereumConfig.value.rpcUrl,
|
|
1535
|
+
nativeSymbol: chain.nativeAsset.symbol,
|
|
1536
|
+
assets: chain.assets.map((asset) => ({
|
|
1537
|
+
symbol: asset.symbol,
|
|
1538
|
+
address: asset.address
|
|
1539
|
+
}))
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
return ok(chains);
|
|
1543
|
+
}
|
|
1544
|
+
function invalid(message) {
|
|
1545
|
+
return err({ kind: "invalid-portfolio-config", message });
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
// src/ethereum/parseTokenAmount.ts
|
|
1549
|
+
import { parseUnits } from "viem";
|
|
1550
|
+
function parseTokenAmount(value, decimals) {
|
|
1551
|
+
if (!Number.isInteger(decimals) || decimals < 0 || decimals > 255) {
|
|
1552
|
+
return err({ kind: "invalid-token-amount", value });
|
|
1553
|
+
}
|
|
1554
|
+
if (!/^(?:0|[1-9]\d*)(?:\.\d+)?$/.test(value)) {
|
|
1555
|
+
return err({ kind: "invalid-token-amount", value });
|
|
1556
|
+
}
|
|
1557
|
+
const fractionalDigits = value.split(".")[1]?.length ?? 0;
|
|
1558
|
+
if (fractionalDigits > decimals) {
|
|
1559
|
+
return err({ kind: "invalid-token-amount", value });
|
|
1560
|
+
}
|
|
1561
|
+
try {
|
|
1562
|
+
const amount = parseUnits(value, decimals);
|
|
1563
|
+
if (amount <= 0n) return err({ kind: "zero-token-amount" });
|
|
1564
|
+
if (amount >= 1n << 256n) {
|
|
1565
|
+
return err({ kind: "invalid-token-amount", value });
|
|
1566
|
+
}
|
|
1567
|
+
return ok(amount);
|
|
1568
|
+
} catch {
|
|
1569
|
+
return err({ kind: "invalid-token-amount", value });
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
// src/ethereum/resolveEthereumRpcUrl.ts
|
|
1574
|
+
import { z as z7 } from "zod";
|
|
1575
|
+
var ethereumChainsSchema = z7.object({
|
|
1576
|
+
ethereum: z7.object({
|
|
1577
|
+
chains: z7.record(z7.object({
|
|
1578
|
+
rpcUrl: z7.string()
|
|
1579
|
+
}).passthrough())
|
|
1580
|
+
}).passthrough()
|
|
1581
|
+
}).passthrough();
|
|
1582
|
+
function resolveEthereumRpcUrl(config, chainId) {
|
|
1583
|
+
const parsed = ethereumChainsSchema.safeParse(config);
|
|
1584
|
+
if (!parsed.success) {
|
|
1585
|
+
return err({ kind: "missing-chain-rpc", chainId });
|
|
1586
|
+
}
|
|
1587
|
+
const rpcUrl = parsed.data.ethereum.chains[String(chainId)]?.rpcUrl;
|
|
1588
|
+
if (!rpcUrl) return err({ kind: "missing-chain-rpc", chainId });
|
|
1589
|
+
const ethereumConfig = parseEthereumConfig({ rpcUrl, chainId });
|
|
1590
|
+
if (!ethereumConfig.ok) {
|
|
1591
|
+
return err({ kind: "invalid-chain-rpc", chainId });
|
|
1592
|
+
}
|
|
1593
|
+
return ok(ethereumConfig.value.rpcUrl);
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
// src/ethereum/serializeSignedTransaction.ts
|
|
1597
|
+
import { serializeTransaction } from "viem";
|
|
1598
|
+
function serializeSignedTransaction(transaction, signature) {
|
|
1599
|
+
if (signature.length !== 65) {
|
|
1600
|
+
return err({
|
|
1601
|
+
kind: "invalid-signature-length",
|
|
1602
|
+
actual: signature.length
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
const parity = signature[64];
|
|
1606
|
+
if (parity !== 0 && parity !== 1) {
|
|
1607
|
+
return err({
|
|
1608
|
+
kind: "invalid-signature-parity",
|
|
1609
|
+
actual: parity ?? -1
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
const parsedSignature = {
|
|
1613
|
+
r: `0x${bytesToHex(signature.slice(0, 32))}`,
|
|
1614
|
+
s: `0x${bytesToHex(signature.slice(32, 64))}`,
|
|
1615
|
+
v: 27n + BigInt(parity)
|
|
1616
|
+
};
|
|
1617
|
+
return ok(serializeTransaction(transaction, parsedSignature));
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// src/ethereum/serializeUnsignedTransaction.ts
|
|
1621
|
+
import { serializeTransaction as serializeTransaction2 } from "viem";
|
|
1622
|
+
function serializeUnsignedTransaction(transaction) {
|
|
1623
|
+
return serializeTransaction2(transaction);
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
// src/ethereum/sendErc20Token.ts
|
|
1627
|
+
async function sendErc20Token(client, signer, input) {
|
|
1628
|
+
const verified = await client.verifyChain();
|
|
1629
|
+
if (!verified.ok) {
|
|
1630
|
+
return err({ kind: "chain-client", stage: "verify-chain", error: verified.error });
|
|
1631
|
+
}
|
|
1632
|
+
const decimalsCall = await client.call({
|
|
1633
|
+
to: input.tokenAddress,
|
|
1634
|
+
data: encodeErc20DecimalsCall()
|
|
1635
|
+
});
|
|
1636
|
+
if (!decimalsCall.ok) {
|
|
1637
|
+
return err({
|
|
1638
|
+
kind: "chain-client",
|
|
1639
|
+
stage: "read-token-decimals",
|
|
1640
|
+
error: decimalsCall.error
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
if (decimalsCall.value.data === void 0) {
|
|
1644
|
+
return err({ kind: "missing-token-decimals" });
|
|
1645
|
+
}
|
|
1646
|
+
const decimals = decodeErc20Decimals(decimalsCall.value.data);
|
|
1647
|
+
if (!decimals.ok) return err({ kind: "token-decimals", error: decimals.error });
|
|
1648
|
+
const amount = parseTokenAmount(input.amount, decimals.value);
|
|
1649
|
+
if (!amount.ok) return err({ kind: "token-amount", error: amount.error });
|
|
1650
|
+
const data = encodeErc20Transfer(input.recipient, amount.value);
|
|
1651
|
+
const request = {
|
|
1652
|
+
from: signer.address,
|
|
1653
|
+
to: input.tokenAddress,
|
|
1654
|
+
data,
|
|
1655
|
+
value: 0n
|
|
1656
|
+
};
|
|
1657
|
+
const [nonce, gas, gasPrice] = await Promise.all([
|
|
1658
|
+
client.getTransactionCount(signer.address),
|
|
1659
|
+
client.estimateGas(request),
|
|
1660
|
+
client.getGasPrice()
|
|
1661
|
+
]);
|
|
1662
|
+
if (!nonce.ok) {
|
|
1663
|
+
return err({
|
|
1664
|
+
kind: "chain-client",
|
|
1665
|
+
stage: "get-transaction-count",
|
|
1666
|
+
error: nonce.error
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
if (!gas.ok) {
|
|
1670
|
+
return err({ kind: "chain-client", stage: "estimate-gas", error: gas.error });
|
|
1671
|
+
}
|
|
1672
|
+
if (!gasPrice.ok) {
|
|
1673
|
+
return err({
|
|
1674
|
+
kind: "chain-client",
|
|
1675
|
+
stage: "get-gas-price",
|
|
1676
|
+
error: gasPrice.error
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
const transaction = {
|
|
1680
|
+
type: "legacy",
|
|
1681
|
+
chainId: client.config.chainId,
|
|
1682
|
+
nonce: nonce.value,
|
|
1683
|
+
gas: gas.value,
|
|
1684
|
+
gasPrice: gasPrice.value,
|
|
1685
|
+
to: input.tokenAddress,
|
|
1686
|
+
value: 0n,
|
|
1687
|
+
data
|
|
1688
|
+
};
|
|
1689
|
+
const unsignedTransaction = serializeUnsignedTransaction(transaction);
|
|
1690
|
+
const preimage = hexToBytes(unsignedTransaction);
|
|
1691
|
+
if (!preimage.ok) {
|
|
1692
|
+
return err({
|
|
1693
|
+
kind: "signing-failed",
|
|
1694
|
+
message: "could not serialize the transaction signing preimage"
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1697
|
+
const signature = signer.sign(preimage.value);
|
|
1698
|
+
if (!signature.ok) {
|
|
1699
|
+
return err({ kind: "signing-failed", message: signature.error });
|
|
1700
|
+
}
|
|
1701
|
+
const signedTransaction = serializeSignedTransaction(
|
|
1702
|
+
transaction,
|
|
1703
|
+
signature.value
|
|
1704
|
+
);
|
|
1705
|
+
if (!signedTransaction.ok) {
|
|
1706
|
+
return err({ kind: "invalid-signature", error: signedTransaction.error });
|
|
1707
|
+
}
|
|
1708
|
+
const transactionHash = await client.sendRawTransaction(
|
|
1709
|
+
signedTransaction.value
|
|
1710
|
+
);
|
|
1711
|
+
if (!transactionHash.ok) {
|
|
1712
|
+
return err({
|
|
1713
|
+
kind: "chain-client",
|
|
1714
|
+
stage: "broadcast",
|
|
1715
|
+
error: transactionHash.error
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
return ok({
|
|
1719
|
+
transactionHash: transactionHash.value,
|
|
1720
|
+
from: signer.address,
|
|
1721
|
+
amountBaseUnits: amount.value,
|
|
1722
|
+
decimals: decimals.value
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
// src/ethereum/sendNativeEth.ts
|
|
1727
|
+
async function sendNativeEth(client, signer, input) {
|
|
1728
|
+
const verified = await client.verifyChain();
|
|
1729
|
+
if (!verified.ok) {
|
|
1730
|
+
return err({ kind: "chain-client", stage: "verify-chain", error: verified.error });
|
|
1731
|
+
}
|
|
1732
|
+
const amount = parseTokenAmount(input.amount, 18);
|
|
1733
|
+
if (!amount.ok) return err({ kind: "amount", error: amount.error });
|
|
1734
|
+
const request = {
|
|
1735
|
+
from: signer.address,
|
|
1736
|
+
to: input.recipient,
|
|
1737
|
+
value: amount.value
|
|
1738
|
+
};
|
|
1739
|
+
const [nonce, balance, gas, gasPrice] = await Promise.all([
|
|
1740
|
+
client.getTransactionCount(signer.address),
|
|
1741
|
+
client.getBalance(signer.address),
|
|
1742
|
+
client.estimateGas(request),
|
|
1743
|
+
client.getGasPrice()
|
|
1744
|
+
]);
|
|
1745
|
+
if (!nonce.ok) {
|
|
1746
|
+
return err({
|
|
1747
|
+
kind: "chain-client",
|
|
1748
|
+
stage: "get-transaction-count",
|
|
1749
|
+
error: nonce.error
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
if (!balance.ok) {
|
|
1753
|
+
return err({ kind: "chain-client", stage: "get-balance", error: balance.error });
|
|
1754
|
+
}
|
|
1755
|
+
if (!gas.ok) {
|
|
1756
|
+
return err({ kind: "chain-client", stage: "estimate-gas", error: gas.error });
|
|
1757
|
+
}
|
|
1758
|
+
if (!gasPrice.ok) {
|
|
1759
|
+
return err({
|
|
1760
|
+
kind: "chain-client",
|
|
1761
|
+
stage: "get-gas-price",
|
|
1762
|
+
error: gasPrice.error
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
const feeWei = gas.value * gasPrice.value;
|
|
1766
|
+
const requiredWei = amount.value + feeWei;
|
|
1767
|
+
if (balance.value < requiredWei) {
|
|
1768
|
+
return err({
|
|
1769
|
+
kind: "insufficient-funds",
|
|
1770
|
+
balanceWei: balance.value,
|
|
1771
|
+
requiredWei
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
const transaction = {
|
|
1775
|
+
type: "legacy",
|
|
1776
|
+
chainId: client.config.chainId,
|
|
1777
|
+
nonce: nonce.value,
|
|
1778
|
+
gas: gas.value,
|
|
1779
|
+
gasPrice: gasPrice.value,
|
|
1780
|
+
to: input.recipient,
|
|
1781
|
+
value: amount.value,
|
|
1782
|
+
data: "0x"
|
|
1783
|
+
};
|
|
1784
|
+
const preimage = hexToBytes(serializeUnsignedTransaction(transaction));
|
|
1785
|
+
if (!preimage.ok) {
|
|
1786
|
+
return err({
|
|
1787
|
+
kind: "signing-failed",
|
|
1788
|
+
message: "could not serialize the transaction signing preimage"
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
const signature = signer.sign(preimage.value);
|
|
1792
|
+
if (!signature.ok) {
|
|
1793
|
+
return err({ kind: "signing-failed", message: signature.error });
|
|
1794
|
+
}
|
|
1795
|
+
const signed = serializeSignedTransaction(transaction, signature.value);
|
|
1796
|
+
if (!signed.ok) {
|
|
1797
|
+
return err({ kind: "invalid-signature", error: signed.error });
|
|
1798
|
+
}
|
|
1799
|
+
const transactionHash = await client.sendRawTransaction(signed.value);
|
|
1800
|
+
if (!transactionHash.ok) {
|
|
1801
|
+
return err({
|
|
1802
|
+
kind: "chain-client",
|
|
1803
|
+
stage: "broadcast",
|
|
1804
|
+
error: transactionHash.error
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
return ok({
|
|
1808
|
+
transactionHash: transactionHash.value,
|
|
1809
|
+
from: signer.address,
|
|
1810
|
+
amountWei: amount.value,
|
|
1811
|
+
feeWei,
|
|
1812
|
+
balanceWei: balance.value
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
// src/ethereum/createViemTransport.ts
|
|
1817
|
+
import {
|
|
1818
|
+
createPublicClient,
|
|
1819
|
+
defineChain,
|
|
1820
|
+
http
|
|
1821
|
+
} from "viem";
|
|
1822
|
+
function createViemTransport(config, options = {}) {
|
|
1823
|
+
const chain = defineChain({
|
|
1824
|
+
id: config.chainId,
|
|
1825
|
+
name: `Chain ${config.chainId}`,
|
|
1826
|
+
nativeCurrency: { name: "Native Currency", symbol: "NATIVE", decimals: 18 },
|
|
1827
|
+
rpcUrls: { default: { http: [config.rpcUrl] } }
|
|
1828
|
+
});
|
|
1829
|
+
const client = createPublicClient({
|
|
1830
|
+
chain,
|
|
1831
|
+
transport: http(config.rpcUrl, { fetchFn: options.fetchFn })
|
|
1832
|
+
});
|
|
1833
|
+
return {
|
|
1834
|
+
async call(request) {
|
|
1835
|
+
const response = await client.call(toViemCallParameters(request));
|
|
1836
|
+
return { data: response.data };
|
|
1837
|
+
},
|
|
1838
|
+
async getChainId() {
|
|
1839
|
+
return await client.getChainId();
|
|
1840
|
+
},
|
|
1841
|
+
async getTransactionCount(address) {
|
|
1842
|
+
return await client.getTransactionCount({
|
|
1843
|
+
address,
|
|
1844
|
+
blockTag: "pending"
|
|
1845
|
+
});
|
|
1846
|
+
},
|
|
1847
|
+
async getBalance(address) {
|
|
1848
|
+
return await client.getBalance({
|
|
1849
|
+
address,
|
|
1850
|
+
blockTag: "pending"
|
|
1851
|
+
});
|
|
1852
|
+
},
|
|
1853
|
+
async estimateGas(request) {
|
|
1854
|
+
return await client.estimateGas({
|
|
1855
|
+
account: request.from,
|
|
1856
|
+
to: request.to,
|
|
1857
|
+
data: request.data,
|
|
1858
|
+
value: request.value
|
|
1859
|
+
});
|
|
1860
|
+
},
|
|
1861
|
+
async getGasPrice() {
|
|
1862
|
+
return await client.getGasPrice();
|
|
1863
|
+
},
|
|
1864
|
+
async sendRawTransaction(transaction) {
|
|
1865
|
+
return await client.sendRawTransaction({
|
|
1866
|
+
serializedTransaction: transaction
|
|
1867
|
+
});
|
|
1868
|
+
}
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
function toViemCallParameters(request) {
|
|
1872
|
+
const block = typeof request.block === "bigint" ? { blockNumber: request.block } : typeof request.block === "string" ? { blockTag: request.block } : {};
|
|
1873
|
+
const parameters = {
|
|
1874
|
+
to: request.to,
|
|
1875
|
+
data: request.data,
|
|
1876
|
+
account: request.from,
|
|
1877
|
+
gas: request.gas,
|
|
1878
|
+
value: request.value,
|
|
1879
|
+
...block
|
|
1880
|
+
};
|
|
1881
|
+
return request.gasPrice === void 0 ? parameters : { ...parameters, type: "legacy", gasPrice: request.gasPrice };
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
// src/commands/ethereumWalletAddress.ts
|
|
1885
|
+
import { isAddress as isAddress3 } from "viem";
|
|
1886
|
+
function ethereumWalletAddress(client, derivationPath) {
|
|
1887
|
+
const address = runWithWallet(client, (wallet) => {
|
|
1888
|
+
const bootstrapped = wallet.bootstrap();
|
|
1889
|
+
if (!bootstrapped.ok) return err(formatNativeError(bootstrapped.error));
|
|
1890
|
+
const exported = wallet.exportAddress(derivationPath, "ethereum");
|
|
1891
|
+
if (!exported.ok) return err(formatNativeError(exported.error));
|
|
1892
|
+
return ok(exported.value);
|
|
1893
|
+
});
|
|
1894
|
+
if (!address.ok) return address;
|
|
1895
|
+
if (!isAddress3(address.value)) {
|
|
1896
|
+
return err("native wallet returned an invalid Ethereum address");
|
|
1897
|
+
}
|
|
1898
|
+
return ok(address.value);
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
// src/commands/formatEthereumClientError.ts
|
|
1902
|
+
function formatEthereumClientError(error) {
|
|
1903
|
+
switch (error.kind) {
|
|
1904
|
+
case "unsupported-transport":
|
|
1905
|
+
return "configured Ethereum transport does not support transactions";
|
|
1906
|
+
case "invalid-request":
|
|
1907
|
+
return error.message;
|
|
1908
|
+
case "transport-failed":
|
|
1909
|
+
return messageFromUnknown3(error.cause);
|
|
1910
|
+
case "invalid-response":
|
|
1911
|
+
return error.message;
|
|
1912
|
+
case "chain-mismatch":
|
|
1913
|
+
return `RPC node reports chain ${error.actual}, expected ${error.expected}`;
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
function messageFromUnknown3(value) {
|
|
1917
|
+
return value instanceof Error ? value.message : String(value);
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
// src/commands/resolveEthereumCommandRpcUrl.ts
|
|
1921
|
+
async function resolveEthereumCommandRpcUrl(env, chainId, explicitRpcUrl) {
|
|
1922
|
+
if (explicitRpcUrl !== void 0) return ok(explicitRpcUrl);
|
|
1923
|
+
const homeDirectory = env.HOME?.length ? env.HOME : void 0;
|
|
1924
|
+
const loaded = await GlobalConfig.load({ homeDirectory });
|
|
1925
|
+
if (!loaded.ok) return err(formatConfigLoadError(loaded.error));
|
|
1926
|
+
const rpcUrl = resolveEthereumRpcUrl(loaded.value.data, chainId);
|
|
1927
|
+
if (!rpcUrl.ok) {
|
|
1928
|
+
const configPath = loaded.value.path;
|
|
1929
|
+
return err(
|
|
1930
|
+
rpcUrl.error.kind === "missing-chain-rpc" ? `no RPC configured for chain ${chainId} in ${configPath}; set ethereum.chains.${chainId}.rpcUrl or pass --rpc` : `invalid RPC configured for chain ${chainId} in ${configPath}`
|
|
1931
|
+
);
|
|
1932
|
+
}
|
|
1933
|
+
return ok(rpcUrl.value);
|
|
1934
|
+
}
|
|
1935
|
+
function formatConfigLoadError(error) {
|
|
1936
|
+
switch (error.kind) {
|
|
1937
|
+
case "read-failed":
|
|
1938
|
+
return `failed to read global config ${error.path}: ${messageFromUnknown4(error.cause)}`;
|
|
1939
|
+
case "invalid-json":
|
|
1940
|
+
return `invalid JSON in global config ${error.path}: ${error.message}`;
|
|
1941
|
+
case "invalid-config":
|
|
1942
|
+
return `invalid global config ${error.path}: ${error.message}`;
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
function messageFromUnknown4(value) {
|
|
1946
|
+
return value instanceof Error ? value.message : String(value);
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
// src/commands/send.ts
|
|
1950
|
+
var sendCommand = {
|
|
1951
|
+
name: "send",
|
|
1952
|
+
describe() {
|
|
1953
|
+
return " macwlt send <amount> <token-address|ETH> <chain-id> <recipient> [--rpc <url>] [--path <derivation-path>] [--json]";
|
|
1954
|
+
},
|
|
1955
|
+
parse: parseSend,
|
|
1956
|
+
async run(ctx, args) {
|
|
1957
|
+
const rpcUrl = await resolveEthereumCommandRpcUrl(
|
|
1958
|
+
ctx.env,
|
|
1959
|
+
args.chainId,
|
|
1960
|
+
args.rpcUrl
|
|
1961
|
+
);
|
|
1962
|
+
if (!rpcUrl.ok) return rpcUrl;
|
|
1963
|
+
const ethereumClient = EthereumClient.create(
|
|
1964
|
+
{ rpcUrl: rpcUrl.value, chainId: args.chainId },
|
|
1965
|
+
createViemTransport
|
|
1966
|
+
);
|
|
1967
|
+
if (!ethereumClient.ok) {
|
|
1968
|
+
return err(`invalid Ethereum client configuration: ${ethereumClient.error.kind}`);
|
|
1969
|
+
}
|
|
1970
|
+
const signer = ethereumSigner(ctx, args.derivationPath);
|
|
1971
|
+
if (!signer.ok) return signer;
|
|
1972
|
+
if (args.asset.kind === "native-eth") {
|
|
1973
|
+
const sent = await sendNativeEth(ethereumClient.value, signer.value, {
|
|
1974
|
+
recipient: args.recipient,
|
|
1975
|
+
amount: args.amount
|
|
1976
|
+
});
|
|
1977
|
+
if (!sent.ok) return err(formatNativeSendError(sent.error));
|
|
1978
|
+
if (!args.json) return ok(sent.value.transactionHash);
|
|
1979
|
+
return ok(JSON.stringify({
|
|
1980
|
+
transactionHash: sent.value.transactionHash,
|
|
1981
|
+
chainId: args.chainId,
|
|
1982
|
+
asset: "ETH",
|
|
1983
|
+
recipient: args.recipient,
|
|
1984
|
+
from: sent.value.from,
|
|
1985
|
+
amount: args.amount,
|
|
1986
|
+
amountWei: sent.value.amountWei.toString(),
|
|
1987
|
+
feeWei: sent.value.feeWei.toString()
|
|
1988
|
+
}, null, 2));
|
|
1989
|
+
} else {
|
|
1990
|
+
const sent = await sendErc20Token(ethereumClient.value, signer.value, {
|
|
1991
|
+
tokenAddress: args.asset.tokenAddress,
|
|
1992
|
+
recipient: args.recipient,
|
|
1993
|
+
amount: args.amount
|
|
1994
|
+
});
|
|
1995
|
+
if (!sent.ok) return err(formatSendError(sent.error));
|
|
1996
|
+
if (!args.json) return ok(sent.value.transactionHash);
|
|
1997
|
+
return ok(JSON.stringify({
|
|
1998
|
+
transactionHash: sent.value.transactionHash,
|
|
1999
|
+
chainId: args.chainId,
|
|
2000
|
+
tokenAddress: args.asset.tokenAddress,
|
|
2001
|
+
recipient: args.recipient,
|
|
2002
|
+
from: sent.value.from,
|
|
2003
|
+
amount: args.amount,
|
|
2004
|
+
amountBaseUnits: sent.value.amountBaseUnits.toString(),
|
|
2005
|
+
decimals: sent.value.decimals
|
|
2006
|
+
}, null, 2));
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
};
|
|
2010
|
+
function parseSend(args) {
|
|
2011
|
+
const flags = parseFlags(args);
|
|
2012
|
+
if (!flags.ok) return flags;
|
|
2013
|
+
if (flags.value.positionals.length !== 4) {
|
|
2014
|
+
return err(
|
|
2015
|
+
"send requires <amount> <token-address> <chain-id> <recipient>"
|
|
2016
|
+
);
|
|
2017
|
+
}
|
|
2018
|
+
if (flags.value.switches.size > 0) {
|
|
2019
|
+
return err("send does not accept switch options");
|
|
2020
|
+
}
|
|
2021
|
+
for (const option of flags.value.options.keys()) {
|
|
2022
|
+
if (option !== "rpc" && option !== "path") {
|
|
2023
|
+
return err(`unknown send option: --${option}`);
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
const [amount, assetValue, chainIdValue, recipient] = flags.value.positionals;
|
|
2027
|
+
if (!amount || !/^(?:0|[1-9]\d*)(?:\.\d+)?$/.test(amount)) {
|
|
2028
|
+
return err("send amount must be a non-negative decimal number");
|
|
2029
|
+
}
|
|
2030
|
+
if (/^0(?:\.0+)?$/.test(amount)) {
|
|
2031
|
+
return err("send amount must be greater than zero");
|
|
2032
|
+
}
|
|
2033
|
+
if (!assetValue) {
|
|
2034
|
+
return err("send token-address must be ETH or a valid Ethereum address");
|
|
2035
|
+
}
|
|
2036
|
+
const asset = parseEthereumAsset(assetValue);
|
|
2037
|
+
if (!asset.ok) {
|
|
2038
|
+
return err("send token-address must be ETH or a valid Ethereum address");
|
|
2039
|
+
}
|
|
2040
|
+
if (!recipient || !isAddress4(recipient)) {
|
|
2041
|
+
return err("send recipient must be a valid Ethereum address");
|
|
2042
|
+
}
|
|
2043
|
+
const chainId = Number(chainIdValue);
|
|
2044
|
+
if (!Number.isSafeInteger(chainId) || chainId <= 0) {
|
|
2045
|
+
return err("send chain-id must be a positive integer");
|
|
2046
|
+
}
|
|
2047
|
+
const rpcUrl = flags.value.options.get("rpc");
|
|
2048
|
+
if (rpcUrl !== void 0) {
|
|
2049
|
+
const config = parseEthereumConfig({ rpcUrl, chainId });
|
|
2050
|
+
if (!config.ok) return err("send --rpc must be a valid HTTP(S) URL");
|
|
2051
|
+
}
|
|
2052
|
+
return ok({
|
|
2053
|
+
amount,
|
|
2054
|
+
asset: asset.value,
|
|
2055
|
+
chainId,
|
|
2056
|
+
recipient,
|
|
2057
|
+
rpcUrl,
|
|
2058
|
+
derivationPath: flags.value.options.get("path") ?? "m",
|
|
2059
|
+
json: flags.value.json
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
2062
|
+
function ethereumSigner(ctx, derivationPath) {
|
|
2063
|
+
const address = ethereumWalletAddress(ctx.client, derivationPath);
|
|
2064
|
+
if (!address.ok) return address;
|
|
2065
|
+
return ok({
|
|
2066
|
+
address: address.value,
|
|
2067
|
+
sign(transaction) {
|
|
2068
|
+
return runWithWallet(ctx.client, (wallet) => {
|
|
2069
|
+
const bootstrapped = wallet.bootstrap();
|
|
2070
|
+
if (!bootstrapped.ok) return err(formatNativeError(bootstrapped.error));
|
|
2071
|
+
const signature = wallet.signEthereumTransaction(transaction);
|
|
2072
|
+
if (!signature.ok) return err(formatNativeError(signature.error));
|
|
2073
|
+
return ok(signature.value);
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
});
|
|
2077
|
+
}
|
|
2078
|
+
function formatSendError(error) {
|
|
2079
|
+
switch (error.kind) {
|
|
2080
|
+
case "chain-client":
|
|
2081
|
+
return `${error.stage} failed: ${formatEthereumClientError(error.error)}`;
|
|
2082
|
+
case "missing-token-decimals":
|
|
2083
|
+
return "token decimals call returned no data";
|
|
2084
|
+
case "token-decimals":
|
|
2085
|
+
return `invalid token decimals response: ${error.error.message}`;
|
|
2086
|
+
case "token-amount":
|
|
2087
|
+
return error.error.kind === "zero-token-amount" ? "send amount must be greater than zero" : `send amount ${error.error.value} cannot be represented by this token`;
|
|
2088
|
+
case "signing-failed":
|
|
2089
|
+
return `transaction signing failed: ${error.message}`;
|
|
2090
|
+
case "invalid-signature":
|
|
2091
|
+
return error.error.kind === "invalid-signature-length" ? `native wallet returned a ${error.error.actual}-byte Ethereum signature; expected 65` : `native wallet returned invalid Ethereum recovery parity ${error.error.actual}`;
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
function formatNativeSendError(error) {
|
|
2095
|
+
switch (error.kind) {
|
|
2096
|
+
case "chain-client":
|
|
2097
|
+
return `${error.stage} failed: ${formatEthereumClientError(error.error)}`;
|
|
2098
|
+
case "amount":
|
|
2099
|
+
return error.error.kind === "zero-token-amount" ? "send amount must be greater than zero" : "send ETH amount is invalid";
|
|
2100
|
+
case "insufficient-funds":
|
|
2101
|
+
return `insufficient ETH balance: have ${error.balanceWei} wei, require ${error.requiredWei} wei including gas`;
|
|
2102
|
+
case "signing-failed":
|
|
2103
|
+
return `transaction signing failed: ${error.message}`;
|
|
2104
|
+
case "invalid-signature":
|
|
2105
|
+
return error.error.kind === "invalid-signature-length" ? `native wallet returned a ${error.error.actual}-byte Ethereum signature; expected 65` : `native wallet returned invalid Ethereum recovery parity ${error.error.actual}`;
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
// src/commands/balance.ts
|
|
2110
|
+
var balanceCommand = {
|
|
2111
|
+
name: "balance",
|
|
2112
|
+
describe() {
|
|
2113
|
+
return [
|
|
2114
|
+
" macwlt balance [--path <derivation-path>] [--json]",
|
|
2115
|
+
" macwlt balance <ETH|token-address> <chain-id> [--rpc <url>] [--path <derivation-path>] [--json]"
|
|
2116
|
+
].join("\n");
|
|
2117
|
+
},
|
|
2118
|
+
parse: parseBalance,
|
|
2119
|
+
async run(ctx, args) {
|
|
2120
|
+
switch (args.kind) {
|
|
2121
|
+
case "portfolio":
|
|
2122
|
+
return await runPortfolioBalance(ctx, args);
|
|
2123
|
+
case "asset":
|
|
2124
|
+
return await runAssetBalance(ctx, args);
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
};
|
|
2128
|
+
function parseBalance(args) {
|
|
2129
|
+
const flags = parseFlags(args);
|
|
2130
|
+
if (!flags.ok) return flags;
|
|
2131
|
+
if (flags.value.switches.size > 0) {
|
|
2132
|
+
return err("balance does not accept switch options");
|
|
2133
|
+
}
|
|
2134
|
+
for (const option of flags.value.options.keys()) {
|
|
2135
|
+
if (option !== "rpc" && option !== "path") {
|
|
2136
|
+
return err(`unknown balance option: --${option}`);
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
const derivationPath = flags.value.options.get("path") ?? "m";
|
|
2140
|
+
if (flags.value.positionals.length === 0) {
|
|
2141
|
+
if (flags.value.options.has("rpc")) {
|
|
2142
|
+
return err("balance --rpc requires <ETH|token-address> <chain-id>");
|
|
2143
|
+
}
|
|
2144
|
+
return ok({
|
|
2145
|
+
kind: "portfolio",
|
|
2146
|
+
derivationPath,
|
|
2147
|
+
json: flags.value.json
|
|
2148
|
+
});
|
|
2149
|
+
}
|
|
2150
|
+
if (flags.value.positionals.length !== 2) {
|
|
2151
|
+
return err("balance accepts no positionals or requires <ETH|token-address> <chain-id>");
|
|
2152
|
+
}
|
|
2153
|
+
const [assetValue, chainIdValue] = flags.value.positionals;
|
|
2154
|
+
if (!assetValue) {
|
|
2155
|
+
return err("balance asset must be ETH or a valid Ethereum token address");
|
|
2156
|
+
}
|
|
2157
|
+
const asset = parseEthereumAsset(assetValue);
|
|
2158
|
+
if (!asset.ok) {
|
|
2159
|
+
return err("balance asset must be ETH or a valid Ethereum token address");
|
|
2160
|
+
}
|
|
2161
|
+
const chainId = Number(chainIdValue);
|
|
2162
|
+
if (!Number.isSafeInteger(chainId) || chainId <= 0) {
|
|
2163
|
+
return err("balance chain-id must be a positive integer");
|
|
2164
|
+
}
|
|
2165
|
+
const rpcUrl = flags.value.options.get("rpc");
|
|
2166
|
+
if (rpcUrl !== void 0) {
|
|
2167
|
+
const config = parseEthereumConfig({ rpcUrl, chainId });
|
|
2168
|
+
if (!config.ok) return err("balance --rpc must be a valid HTTP(S) URL");
|
|
2169
|
+
}
|
|
2170
|
+
return ok({
|
|
2171
|
+
kind: "asset",
|
|
2172
|
+
asset: asset.value,
|
|
2173
|
+
chainId,
|
|
2174
|
+
rpcUrl,
|
|
2175
|
+
derivationPath,
|
|
2176
|
+
json: flags.value.json
|
|
2177
|
+
});
|
|
2178
|
+
}
|
|
2179
|
+
async function runAssetBalance(ctx, args) {
|
|
2180
|
+
const rpcUrl = await resolveEthereumCommandRpcUrl(
|
|
2181
|
+
ctx.env,
|
|
2182
|
+
args.chainId,
|
|
2183
|
+
args.rpcUrl
|
|
2184
|
+
);
|
|
2185
|
+
if (!rpcUrl.ok) return rpcUrl;
|
|
2186
|
+
const client = EthereumClient.create(
|
|
2187
|
+
{ rpcUrl: rpcUrl.value, chainId: args.chainId },
|
|
2188
|
+
createViemTransport
|
|
2189
|
+
);
|
|
2190
|
+
if (!client.ok) {
|
|
2191
|
+
return err(`invalid Ethereum client configuration: ${client.error.kind}`);
|
|
2192
|
+
}
|
|
2193
|
+
const address = ethereumWalletAddress(ctx.client, args.derivationPath);
|
|
2194
|
+
if (!address.ok) return address;
|
|
2195
|
+
const balance = await getEthereumAssetBalance(
|
|
2196
|
+
client.value,
|
|
2197
|
+
address.value,
|
|
2198
|
+
args.asset
|
|
2199
|
+
);
|
|
2200
|
+
if (!balance.ok) return err(formatBalanceError(balance.error));
|
|
2201
|
+
if (!args.json) {
|
|
2202
|
+
return ok(
|
|
2203
|
+
balance.value.kind === "native-eth" ? `${balance.value.balance} ETH` : balance.value.balance
|
|
2204
|
+
);
|
|
2205
|
+
}
|
|
2206
|
+
if (balance.value.kind === "native-eth") {
|
|
2207
|
+
return ok(JSON.stringify({
|
|
2208
|
+
address: balance.value.address,
|
|
2209
|
+
chainId: args.chainId,
|
|
2210
|
+
asset: "ETH",
|
|
2211
|
+
balance: balance.value.balance,
|
|
2212
|
+
balanceWei: balance.value.balanceBaseUnits.toString(),
|
|
2213
|
+
decimals: balance.value.decimals
|
|
2214
|
+
}, null, 2));
|
|
2215
|
+
}
|
|
2216
|
+
return ok(JSON.stringify({
|
|
2217
|
+
address: balance.value.address,
|
|
2218
|
+
chainId: args.chainId,
|
|
2219
|
+
tokenAddress: balance.value.tokenAddress,
|
|
2220
|
+
balance: balance.value.balance,
|
|
2221
|
+
balanceBaseUnits: balance.value.balanceBaseUnits.toString(),
|
|
2222
|
+
decimals: balance.value.decimals
|
|
2223
|
+
}, null, 2));
|
|
2224
|
+
}
|
|
2225
|
+
async function runPortfolioBalance(ctx, args) {
|
|
2226
|
+
const homeDirectory = ctx.env.HOME?.length ? ctx.env.HOME : void 0;
|
|
2227
|
+
const loaded = await GlobalConfig.load({
|
|
2228
|
+
homeDirectory,
|
|
2229
|
+
storage: ctx.configStorage
|
|
2230
|
+
});
|
|
2231
|
+
if (!loaded.ok) return err(formatConfigLoadError2(loaded.error));
|
|
2232
|
+
const config = parseEthereumPortfolioConfig(loaded.value.data);
|
|
2233
|
+
if (!config.ok) {
|
|
2234
|
+
return err(`invalid EVM portfolio config ${loaded.value.path}: ${config.error.message}`);
|
|
2235
|
+
}
|
|
2236
|
+
if (config.value.length === 0) {
|
|
2237
|
+
return err(`no EVM chains configured in ${loaded.value.path}`);
|
|
2238
|
+
}
|
|
2239
|
+
const address = ethereumWalletAddress(ctx.client, args.derivationPath);
|
|
2240
|
+
if (!address.ok) return address;
|
|
2241
|
+
const portfolio = await getEthereumPortfolioBalances(
|
|
2242
|
+
config.value,
|
|
2243
|
+
address.value,
|
|
2244
|
+
createViemTransport
|
|
2245
|
+
);
|
|
2246
|
+
return ok(
|
|
2247
|
+
args.json ? formatPortfolioJson(address.value, portfolio) : formatPortfolioText(address.value, portfolio)
|
|
2248
|
+
);
|
|
2249
|
+
}
|
|
2250
|
+
function formatPortfolioJson(address, portfolio) {
|
|
2251
|
+
return JSON.stringify({
|
|
2252
|
+
address,
|
|
2253
|
+
chains: portfolio.map((chain) => {
|
|
2254
|
+
if (chain.status === "failed") {
|
|
2255
|
+
return {
|
|
2256
|
+
chainId: chain.chain.chainId,
|
|
2257
|
+
name: chain.chain.name,
|
|
2258
|
+
status: "failed",
|
|
2259
|
+
error: formatClientCreationError(chain.error)
|
|
2260
|
+
};
|
|
2261
|
+
}
|
|
2262
|
+
return {
|
|
2263
|
+
chainId: chain.chain.chainId,
|
|
2264
|
+
name: chain.chain.name,
|
|
2265
|
+
status: "ok",
|
|
2266
|
+
assets: chain.assets.filter(isHeldAsset).map(formatPortfolioAssetJson),
|
|
2267
|
+
errors: chain.assets.filter(isFailedAsset).map((asset) => ({
|
|
2268
|
+
symbol: asset.symbol,
|
|
2269
|
+
kind: asset.asset.kind === "native-eth" ? "native" : "erc20",
|
|
2270
|
+
...asset.asset.kind === "erc20" ? { tokenAddress: asset.asset.tokenAddress } : {},
|
|
2271
|
+
error: formatBalanceError(asset.error)
|
|
2272
|
+
}))
|
|
2273
|
+
};
|
|
2274
|
+
})
|
|
2275
|
+
}, null, 2);
|
|
2276
|
+
}
|
|
2277
|
+
function formatPortfolioText(address, portfolio) {
|
|
2278
|
+
const lines = [`Wallet ${address}`];
|
|
2279
|
+
let heldAssetCount = 0;
|
|
2280
|
+
for (const chain of portfolio) {
|
|
2281
|
+
if (chain.status === "failed") {
|
|
2282
|
+
lines.push(
|
|
2283
|
+
`${chain.chain.name} (${chain.chain.chainId}): unavailable: ${formatClientCreationError(chain.error)}`
|
|
2284
|
+
);
|
|
2285
|
+
continue;
|
|
2286
|
+
}
|
|
2287
|
+
const heldAssets = chain.assets.filter(isHeldAsset);
|
|
2288
|
+
const failedAssets = chain.assets.filter(isFailedAsset);
|
|
2289
|
+
if (heldAssets.length === 0 && failedAssets.length === 0) continue;
|
|
2290
|
+
lines.push(`${chain.chain.name} (${chain.chain.chainId})`);
|
|
2291
|
+
for (const asset of heldAssets) {
|
|
2292
|
+
heldAssetCount++;
|
|
2293
|
+
lines.push(` ${asset.symbol}: ${asset.balance.balance}`);
|
|
2294
|
+
}
|
|
2295
|
+
for (const asset of failedAssets) {
|
|
2296
|
+
lines.push(` ${asset.symbol}: unavailable: ${formatBalanceError(asset.error)}`);
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
if (heldAssetCount === 0) lines.push("No configured assets held.");
|
|
2300
|
+
return lines.join("\n");
|
|
2301
|
+
}
|
|
2302
|
+
function formatPortfolioAssetJson(asset) {
|
|
2303
|
+
const balance = asset.balance;
|
|
2304
|
+
return {
|
|
2305
|
+
symbol: asset.symbol,
|
|
2306
|
+
kind: balance.kind === "native-eth" ? "native" : "erc20",
|
|
2307
|
+
...balance.kind === "erc20" ? { tokenAddress: balance.tokenAddress } : {},
|
|
2308
|
+
balance: balance.balance,
|
|
2309
|
+
balanceBaseUnits: balance.balanceBaseUnits.toString(),
|
|
2310
|
+
decimals: balance.decimals
|
|
2311
|
+
};
|
|
2312
|
+
}
|
|
2313
|
+
function isHeldAsset(asset) {
|
|
2314
|
+
return asset.status === "fulfilled" && asset.balance.balanceBaseUnits > 0n;
|
|
2315
|
+
}
|
|
2316
|
+
function isFailedAsset(asset) {
|
|
2317
|
+
return asset.status === "failed";
|
|
2318
|
+
}
|
|
2319
|
+
function formatBalanceError(error) {
|
|
2320
|
+
switch (error.kind) {
|
|
2321
|
+
case "chain-client":
|
|
2322
|
+
return `${error.stage} failed: ${formatEthereumClientError(error.error)}`;
|
|
2323
|
+
case "missing-token-balance":
|
|
2324
|
+
return "token balance call returned no data";
|
|
2325
|
+
case "missing-token-decimals":
|
|
2326
|
+
return "token decimals call returned no data";
|
|
2327
|
+
case "token-balance":
|
|
2328
|
+
return `invalid token balance response: ${error.error.message}`;
|
|
2329
|
+
case "token-decimals":
|
|
2330
|
+
return `invalid token decimals response: ${error.error.message}`;
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
function formatClientCreationError(error) {
|
|
2334
|
+
switch (error.kind) {
|
|
2335
|
+
case "invalid-rpc-url":
|
|
2336
|
+
return `invalid RPC URL ${error.value}`;
|
|
2337
|
+
case "invalid-chain-id":
|
|
2338
|
+
return `invalid chain ID ${error.value}`;
|
|
2339
|
+
case "transport-creation-failed":
|
|
2340
|
+
return messageFromUnknown5(error.cause);
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
function formatConfigLoadError2(error) {
|
|
2344
|
+
switch (error.kind) {
|
|
2345
|
+
case "read-failed":
|
|
2346
|
+
return `failed to read global config ${error.path}: ${messageFromUnknown5(error.cause)}`;
|
|
2347
|
+
case "invalid-json":
|
|
2348
|
+
return `invalid JSON in global config ${error.path}: ${error.message}`;
|
|
2349
|
+
case "invalid-config":
|
|
2350
|
+
return `invalid global config ${error.path}: ${error.message}`;
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
function messageFromUnknown5(value) {
|
|
2354
|
+
return value instanceof Error ? value.message : String(value);
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
// src/commands.ts
|
|
2358
|
+
var commands = [
|
|
2359
|
+
createCommand,
|
|
2360
|
+
resetCommand,
|
|
2361
|
+
resetConfigCommand,
|
|
2362
|
+
pubkeyCommand,
|
|
2363
|
+
addressCommand,
|
|
2364
|
+
balanceCommand,
|
|
2365
|
+
signEthCommand,
|
|
2366
|
+
signPsbtCommand,
|
|
2367
|
+
sendCommand,
|
|
2368
|
+
helpCommand,
|
|
2369
|
+
versionCommand
|
|
2370
|
+
];
|
|
2371
|
+
|
|
2372
|
+
// src/command.ts
|
|
2373
|
+
function parsedEnv(value) {
|
|
2374
|
+
return value;
|
|
2375
|
+
}
|
|
2376
|
+
var cliInputSchema = z8.object({
|
|
2377
|
+
args: z8.array(z8.string()),
|
|
2378
|
+
env: z8.object({
|
|
2379
|
+
HOME: z8.string().optional(),
|
|
2380
|
+
MACWLT_LIB: z8.string().optional()
|
|
2381
|
+
}).passthrough()
|
|
2382
|
+
});
|
|
2383
|
+
var cliVersion = "0.1.0";
|
|
2384
|
+
async function runCli(args, env = process.env, registry = commands, options = {}) {
|
|
2385
|
+
const input = cliInputSchema.safeParse({ args: [...args], env });
|
|
2386
|
+
if (!input.success) return failure("invalid process input");
|
|
2387
|
+
const processEnv = parsedEnv(input.data.env);
|
|
2388
|
+
const configStorage = options.configStorage ?? fileConfigStorage;
|
|
2389
|
+
const initialized = await ensureGlobalConfig({
|
|
2390
|
+
homeDirectory: processEnv.HOME?.length ? processEnv.HOME : void 0,
|
|
2391
|
+
createFile: options.createConfigFile
|
|
2392
|
+
});
|
|
2393
|
+
if (!initialized.ok) {
|
|
2394
|
+
return failure(
|
|
2395
|
+
`failed to initialize config ${initialized.error.path}: ${messageFromUnknown6(initialized.error.cause)}`
|
|
2396
|
+
);
|
|
2397
|
+
}
|
|
2398
|
+
const [name, ...rest] = input.data.args;
|
|
2399
|
+
if (!name) return success(helpText(registry));
|
|
2400
|
+
const command = registry.find((c) => c.name === name || c.aliases?.includes(name));
|
|
2401
|
+
if (!command) return failure(`unknown command: ${name}
|
|
2402
|
+
|
|
2403
|
+
${helpText(registry)}`);
|
|
2404
|
+
const parsed = command.parse(rest);
|
|
2405
|
+
if (!parsed.ok) return failure(parsed.error);
|
|
2406
|
+
if (command.needsClient === false) {
|
|
2407
|
+
const ctx = {
|
|
2408
|
+
env: processEnv,
|
|
2409
|
+
client: neverClient(),
|
|
2410
|
+
registry,
|
|
2411
|
+
configStorage
|
|
2412
|
+
};
|
|
2413
|
+
return await runCommand(command, ctx, parsed.value);
|
|
2414
|
+
}
|
|
2415
|
+
const clientResult = openNativeClient(defaultLibraryPath(input.data.env.MACWLT_LIB));
|
|
2416
|
+
if (!clientResult.ok) return failure(formatNativeError(clientResult.error));
|
|
2417
|
+
const client = clientResult.value;
|
|
2418
|
+
try {
|
|
2419
|
+
const ctx = {
|
|
2420
|
+
env: processEnv,
|
|
2421
|
+
client,
|
|
2422
|
+
registry,
|
|
2423
|
+
configStorage
|
|
2424
|
+
};
|
|
2425
|
+
return await runCommand(command, ctx, parsed.value);
|
|
2426
|
+
} finally {
|
|
2427
|
+
client.close();
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
async function runCommand(command, ctx, parsed) {
|
|
2431
|
+
if (command.beforeRun !== void 0) {
|
|
2432
|
+
const beforeRun = await command.beforeRun();
|
|
2433
|
+
if (!beforeRun.ok) return failure(beforeRun.error);
|
|
2434
|
+
}
|
|
2435
|
+
const result = await command.run(ctx, parsed);
|
|
2436
|
+
if (!result.ok) return failure(result.error);
|
|
2437
|
+
return success(result.value);
|
|
2438
|
+
}
|
|
2439
|
+
function helpText(registry) {
|
|
2440
|
+
return [
|
|
2441
|
+
"Usage:",
|
|
2442
|
+
...registry.map((c) => c.describe()).filter((line) => line.length > 0),
|
|
2443
|
+
"",
|
|
2444
|
+
"Environment:",
|
|
2445
|
+
" MACWLT_LIB Override the bundled or checkout libmacwlt.dylib path"
|
|
2446
|
+
].join("\n");
|
|
2447
|
+
}
|
|
2448
|
+
function neverClient() {
|
|
2449
|
+
return {
|
|
2450
|
+
libraryPath: "",
|
|
2451
|
+
close: () => {
|
|
2452
|
+
},
|
|
2453
|
+
withWallet: (_body) => err({ kind: "missing-library", libraryPath: "" })
|
|
2454
|
+
};
|
|
2455
|
+
}
|
|
2456
|
+
function success(stdout) {
|
|
2457
|
+
return { exitCode: 0, stdout: `${stdout}
|
|
2458
|
+
`, stderr: "" };
|
|
2459
|
+
}
|
|
2460
|
+
function failure(stderr) {
|
|
2461
|
+
return { exitCode: 1, stdout: "", stderr: `${stderr}
|
|
2462
|
+
` };
|
|
2463
|
+
}
|
|
2464
|
+
function messageFromUnknown6(value) {
|
|
2465
|
+
return value instanceof Error ? value.message : String(value);
|
|
2466
|
+
}
|
|
2467
|
+
export {
|
|
2468
|
+
EthereumClient,
|
|
2469
|
+
GlobalConfig,
|
|
2470
|
+
base64ToBytes,
|
|
2471
|
+
bytesToBase64,
|
|
2472
|
+
bytesToHex,
|
|
2473
|
+
cliVersion,
|
|
2474
|
+
commands,
|
|
2475
|
+
createConfirmationHook,
|
|
2476
|
+
createFileIfAbsent,
|
|
2477
|
+
createViemTransport,
|
|
2478
|
+
decodeErc20Balance,
|
|
2479
|
+
decodeErc20Decimals,
|
|
2480
|
+
defaultGlobalConfigContents,
|
|
2481
|
+
defaultLibraryPath,
|
|
2482
|
+
encodeErc20BalanceOf,
|
|
2483
|
+
encodeErc20DecimalsCall,
|
|
2484
|
+
encodeErc20Transfer,
|
|
2485
|
+
ensureGlobalConfig,
|
|
2486
|
+
err,
|
|
2487
|
+
fileConfigStorage,
|
|
2488
|
+
formatDataOutput,
|
|
2489
|
+
formatExecutionError,
|
|
2490
|
+
formatNativeError,
|
|
2491
|
+
formatPsbt,
|
|
2492
|
+
getEthereumAssetBalance,
|
|
2493
|
+
getEthereumPortfolioBalances,
|
|
2494
|
+
globalConfigPath,
|
|
2495
|
+
helpText,
|
|
2496
|
+
hexToBytes,
|
|
2497
|
+
ok,
|
|
2498
|
+
openNativeClient,
|
|
2499
|
+
parseBytesInput,
|
|
2500
|
+
parseEthereumAsset,
|
|
2501
|
+
parseEthereumConfig,
|
|
2502
|
+
parseEthereumPortfolioConfig,
|
|
2503
|
+
parseEvmCallRequest,
|
|
2504
|
+
parseEvmCallResult,
|
|
2505
|
+
parseFlags,
|
|
2506
|
+
parseGlobalConfigData,
|
|
2507
|
+
parseJsonValue,
|
|
2508
|
+
parseTokenAmount,
|
|
2509
|
+
readInput,
|
|
2510
|
+
resetGlobalConfig,
|
|
2511
|
+
resolveEthereumRpcUrl,
|
|
2512
|
+
runCli,
|
|
2513
|
+
runWithWallet,
|
|
2514
|
+
sendErc20Token,
|
|
2515
|
+
sendNativeEth,
|
|
2516
|
+
serializeSignedTransaction,
|
|
2517
|
+
serializeUnsignedTransaction,
|
|
2518
|
+
terminalConfirmationPrompt
|
|
2519
|
+
};
|
|
2520
|
+
//# sourceMappingURL=index.js.map
|