@walletconnect/companion-wallet 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,953 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { readFileSync as readFileSync3 } from "fs";
5
+ import { join as join3, dirname as dirname3 } from "path";
6
+ import { fileURLToPath } from "url";
7
+
8
+ // src/keystore.ts
9
+ import { mkdirSync, writeFileSync, readFileSync, readdirSync, renameSync } from "fs";
10
+ import { join, dirname } from "path";
11
+ import { homedir } from "os";
12
+ import { generateMnemonic, mnemonicToAccount, english } from "viem/accounts";
13
+ import { getAddress } from "viem";
14
+ var KEYS_DIR = join(homedir(), ".config", "wallet", "keys");
15
+ function keyFilePath(address) {
16
+ return join(KEYS_DIR, `${getAddress(address)}.json`);
17
+ }
18
+ function generateAndStore() {
19
+ const mnemonic = generateMnemonic(english);
20
+ const account = mnemonicToAccount(mnemonic);
21
+ const address = getAddress(account.address);
22
+ const filePath = keyFilePath(address);
23
+ mkdirSync(dirname(filePath), { recursive: true, mode: 448 });
24
+ const walletFile = {
25
+ version: 2,
26
+ address,
27
+ mnemonic
28
+ };
29
+ atomicWrite(filePath, JSON.stringify(walletFile, null, 2));
30
+ return { address, mnemonic };
31
+ }
32
+ function loadKey(address) {
33
+ const filePath = keyFilePath(getAddress(address));
34
+ const raw = readFileSync(filePath, "utf-8");
35
+ const data = JSON.parse(raw);
36
+ const account = mnemonicToAccount(data.mnemonic);
37
+ return account.getHdKey().privateKey ? `0x${Buffer.from(account.getHdKey().privateKey).toString("hex")}` : (() => {
38
+ throw new Error("Failed to derive private key from mnemonic");
39
+ })();
40
+ }
41
+ function listAddresses() {
42
+ try {
43
+ const files = readdirSync(KEYS_DIR);
44
+ return files.filter((f) => f.endsWith(".json")).map((f) => f.replace(".json", ""));
45
+ } catch {
46
+ return [];
47
+ }
48
+ }
49
+ function atomicWrite(filePath, content) {
50
+ const tmpPath = filePath + ".tmp";
51
+ writeFileSync(tmpPath, content, { mode: 384 });
52
+ renameSync(tmpPath, filePath);
53
+ }
54
+
55
+ // src/signer.ts
56
+ import { privateKeyToAccount } from "viem/accounts";
57
+
58
+ // src/chains.ts
59
+ import {
60
+ mainnet,
61
+ base,
62
+ optimism
63
+ } from "viem/chains";
64
+ import { http } from "viem";
65
+ var CHAIN_REGISTRY = {
66
+ "eip155:1": { chain: mainnet, name: "Ethereum", defaultRpcUrl: "https://eth.drpc.org" },
67
+ "eip155:8453": { chain: base, name: "Base", defaultRpcUrl: "https://mainnet.base.org" },
68
+ "eip155:10": {
69
+ chain: optimism,
70
+ name: "Optimism",
71
+ defaultRpcUrl: "https://mainnet.optimism.io"
72
+ }
73
+ };
74
+ var SUPPORTED_CHAINS = Object.keys(CHAIN_REGISTRY);
75
+ function resolveChain(caip2) {
76
+ const entry = CHAIN_REGISTRY[caip2];
77
+ if (!entry) {
78
+ throw new Error(
79
+ `Unsupported chain: ${caip2}. Supported: ${SUPPORTED_CHAINS.join(", ")}`
80
+ );
81
+ }
82
+ return entry.chain;
83
+ }
84
+ function getTransport(caip2) {
85
+ const entry = CHAIN_REGISTRY[caip2];
86
+ if (!entry) {
87
+ throw new Error(`Unsupported chain: ${caip2}`);
88
+ }
89
+ const chainId = caip2.split(":")[1];
90
+ const envUrl = process.env[`WALLET_RPC_URL_${chainId}`];
91
+ return http(envUrl || entry.defaultRpcUrl);
92
+ }
93
+ function parseChainId(caip2) {
94
+ const parts = caip2.split(":");
95
+ if (parts.length !== 2 || parts[0] !== "eip155") {
96
+ throw new Error(`Invalid CAIP-2 chain ID: ${caip2}`);
97
+ }
98
+ return parseInt(parts[1], 10);
99
+ }
100
+ function getChainName(caip2) {
101
+ const entry = CHAIN_REGISTRY[caip2];
102
+ if (!entry) {
103
+ throw new Error(`Unsupported chain: ${caip2}`);
104
+ }
105
+ return entry.name;
106
+ }
107
+
108
+ // src/signer.ts
109
+ async function signMessage(privateKey, message) {
110
+ const account = privateKeyToAccount(privateKey);
111
+ return account.signMessage({ message });
112
+ }
113
+ async function signTypedData(privateKey, typedData) {
114
+ const account = privateKeyToAccount(privateKey);
115
+ return account.signTypedData(typedData);
116
+ }
117
+ async function signTransaction(privateKey, transaction, caip2Chain) {
118
+ const account = privateKeyToAccount(privateKey);
119
+ const chainId = parseChainId(caip2Chain);
120
+ const normalized = normalizeTransaction(transaction);
121
+ const tx = {
122
+ ...normalized,
123
+ chainId
124
+ };
125
+ if (!transaction.type && !transaction.gasPrice && !transaction.maxFeePerGas && !transaction.accessList && !transaction.blobs && !transaction.authorizationList) {
126
+ tx.type = "eip1559";
127
+ }
128
+ return account.signTransaction(tx);
129
+ }
130
+ function normalizeTransaction(tx) {
131
+ return {
132
+ to: tx.to,
133
+ value: tx.value !== void 0 ? BigInt(tx.value) : void 0,
134
+ data: tx.data,
135
+ nonce: tx.nonce !== void 0 ? Number(tx.nonce) : void 0,
136
+ gas: tx.gas !== void 0 ? BigInt(tx.gas) : void 0,
137
+ gasPrice: tx.gasPrice !== void 0 ? BigInt(tx.gasPrice) : void 0,
138
+ maxFeePerGas: tx.maxFeePerGas !== void 0 ? BigInt(tx.maxFeePerGas) : void 0,
139
+ maxPriorityFeePerGas: tx.maxPriorityFeePerGas !== void 0 ? BigInt(tx.maxPriorityFeePerGas) : void 0
140
+ };
141
+ }
142
+
143
+ // src/rpc.ts
144
+ import {
145
+ createWalletClient,
146
+ createPublicClient
147
+ } from "viem";
148
+ import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
149
+ async function sendTransaction(privateKey, transaction, caip2Chain) {
150
+ const chain = resolveChain(caip2Chain);
151
+ const transport = getTransport(caip2Chain);
152
+ const account = privateKeyToAccount2(privateKey);
153
+ const publicClient = createPublicClient({ chain, transport });
154
+ const walletClient = createWalletClient({ account, chain, transport });
155
+ const normalized = normalizeTransaction(transaction);
156
+ const request = {
157
+ to: normalized.to,
158
+ value: normalized.value,
159
+ data: normalized.data,
160
+ nonce: normalized.nonce,
161
+ gas: normalized.gas,
162
+ account,
163
+ chain
164
+ };
165
+ if (!request.gas) {
166
+ request.gas = await publicClient.estimateGas({
167
+ account: account.address,
168
+ to: request.to,
169
+ value: request.value,
170
+ data: request.data
171
+ });
172
+ }
173
+ const hash = await walletClient.sendTransaction(request);
174
+ return hash;
175
+ }
176
+
177
+ // src/fund.ts
178
+ import { parseEther } from "viem";
179
+ import { withWallet, resolveProjectId } from "@walletconnect/cli-sdk";
180
+
181
+ // src/tokens.ts
182
+ import { encodeFunctionData } from "viem";
183
+ var USDC_ADDRESSES = {
184
+ "eip155:1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
185
+ "eip155:8453": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
186
+ "eip155:10": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85"
187
+ };
188
+ var TOKEN_REGISTRY = {
189
+ "eip155:1": ["eth", "usdc"],
190
+ "eip155:8453": ["eth", "usdc"],
191
+ "eip155:10": ["eth", "usdc"]
192
+ };
193
+ function getTokenSymbols(chain) {
194
+ return TOKEN_REGISTRY[chain] ?? ["eth"];
195
+ }
196
+ function getToken(symbol, chain) {
197
+ const s = symbol.toLowerCase();
198
+ if (s === "eth") {
199
+ return { symbol: "ETH", decimals: 18 };
200
+ }
201
+ if (s === "usdc") {
202
+ const address = USDC_ADDRESSES[chain];
203
+ if (!address) {
204
+ throw new Error(`USDC not supported on chain ${chain}`);
205
+ }
206
+ return { symbol: "USDC", decimals: 6, address };
207
+ }
208
+ throw new Error(`Unknown token: ${symbol}`);
209
+ }
210
+ function parseTokenAmount(amount, decimals) {
211
+ const [whole = "0", frac = ""] = amount.split(".");
212
+ const paddedFrac = frac.padEnd(decimals, "0").slice(0, decimals);
213
+ return BigInt(whole) * 10n ** BigInt(decimals) + BigInt(paddedFrac);
214
+ }
215
+ var ERC20_TRANSFER_ABI = [
216
+ {
217
+ name: "transfer",
218
+ type: "function",
219
+ stateMutability: "nonpayable",
220
+ inputs: [
221
+ { name: "to", type: "address" },
222
+ { name: "amount", type: "uint256" }
223
+ ],
224
+ outputs: [{ name: "", type: "bool" }]
225
+ }
226
+ ];
227
+ function buildErc20Transfer(tokenAddress, to, amount) {
228
+ const data = encodeFunctionData({
229
+ abi: ERC20_TRANSFER_ABI,
230
+ functionName: "transfer",
231
+ args: [to, amount]
232
+ });
233
+ return { to: tokenAddress, data, value: "0x0" };
234
+ }
235
+
236
+ // src/fund.ts
237
+ async function fund(options) {
238
+ const { amount, chain, token: tokenSymbol = "eth" } = options;
239
+ const embeddedAddress = resolveAccount(options.account);
240
+ const tokenInfo = getToken(tokenSymbol, chain);
241
+ const projectId = resolveProjectId();
242
+ if (!projectId) {
243
+ throw new Error(
244
+ "WalletConnect project ID not found. Set WALLETCONNECT_PROJECT_ID env var."
245
+ );
246
+ }
247
+ let result;
248
+ await withWallet(
249
+ {
250
+ projectId,
251
+ metadata: {
252
+ name: "companion-wallet",
253
+ description: "Fund companion wallet",
254
+ url: "https://walletconnect.com",
255
+ icons: []
256
+ },
257
+ chains: [chain],
258
+ methods: ["eth_sendTransaction"],
259
+ events: []
260
+ },
261
+ async (wallet, { accounts }) => {
262
+ const caip10Account = accounts.find((a) => a.startsWith(`${chain}:`));
263
+ if (!caip10Account) {
264
+ throw new Error(
265
+ `No account found on chain ${chain}. Connected accounts: ${accounts.join(", ")}`
266
+ );
267
+ }
268
+ const externalAddress = caip10Account.split(":").slice(2).join(":");
269
+ let tx;
270
+ if (tokenInfo.address) {
271
+ const rawAmount = parseTokenAmount(amount, tokenInfo.decimals);
272
+ const erc20Tx = buildErc20Transfer(
273
+ tokenInfo.address,
274
+ embeddedAddress,
275
+ rawAmount
276
+ );
277
+ tx = { from: externalAddress, ...erc20Tx };
278
+ } else {
279
+ const amountWei = parseEther(amount);
280
+ const valueHex = `0x${amountWei.toString(16)}`;
281
+ tx = {
282
+ from: externalAddress,
283
+ to: embeddedAddress,
284
+ value: valueHex,
285
+ gas: `0x${21e3.toString(16)}`
286
+ };
287
+ }
288
+ const transactionHash = await wallet.request({
289
+ chainId: chain,
290
+ request: {
291
+ method: "eth_sendTransaction",
292
+ params: [tx]
293
+ }
294
+ });
295
+ result = {
296
+ transactionHash,
297
+ from: externalAddress,
298
+ to: embeddedAddress,
299
+ amount,
300
+ token: tokenInfo.symbol
301
+ };
302
+ }
303
+ );
304
+ if (!result) {
305
+ throw new Error("Fund operation completed without a result");
306
+ }
307
+ return result;
308
+ }
309
+ function resolveAccount(account) {
310
+ if (account) return account;
311
+ const addresses = listAddresses();
312
+ if (addresses.length === 0) {
313
+ throw new Error("No wallet found. Run 'companion-wallet generate' first.");
314
+ }
315
+ return addresses[0];
316
+ }
317
+
318
+ // src/drain.ts
319
+ import {
320
+ createWalletClient as createWalletClient2,
321
+ createPublicClient as createPublicClient2,
322
+ formatEther,
323
+ encodeFunctionData as encodeFunctionData2
324
+ } from "viem";
325
+ import { privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
326
+ var NATIVE_TRANSFER_GAS = 21000n;
327
+ var ERC20_ABI = [
328
+ {
329
+ name: "balanceOf",
330
+ type: "function",
331
+ stateMutability: "view",
332
+ inputs: [{ name: "account", type: "address" }],
333
+ outputs: [{ name: "", type: "uint256" }]
334
+ },
335
+ {
336
+ name: "transfer",
337
+ type: "function",
338
+ stateMutability: "nonpayable",
339
+ inputs: [
340
+ { name: "to", type: "address" },
341
+ { name: "amount", type: "uint256" }
342
+ ],
343
+ outputs: [{ name: "", type: "bool" }]
344
+ }
345
+ ];
346
+ async function drain(options) {
347
+ const { to, chain, token: tokenSymbol = "eth" } = options;
348
+ const tokenInfo = getToken(tokenSymbol, chain);
349
+ const account = resolveAccount2(options.account);
350
+ const privateKey = loadKey(account);
351
+ const viemAccount = privateKeyToAccount3(privateKey);
352
+ const viemChain = resolveChain(chain);
353
+ const transport = getTransport(chain);
354
+ const publicClient = createPublicClient2({ chain: viemChain, transport });
355
+ const walletClient = createWalletClient2({
356
+ account: viemAccount,
357
+ chain: viemChain,
358
+ transport
359
+ });
360
+ let transactionHash;
361
+ let amount;
362
+ if (tokenInfo.address) {
363
+ const balance = await publicClient.readContract({
364
+ address: tokenInfo.address,
365
+ abi: ERC20_ABI,
366
+ functionName: "balanceOf",
367
+ args: [viemAccount.address]
368
+ });
369
+ if (balance === 0n) {
370
+ throw new Error(`No ${tokenInfo.symbol} balance to drain`);
371
+ }
372
+ const data = encodeFunctionData2({
373
+ abi: ERC20_ABI,
374
+ functionName: "transfer",
375
+ args: [to, balance]
376
+ });
377
+ transactionHash = await walletClient.sendTransaction({
378
+ to: tokenInfo.address,
379
+ data,
380
+ value: 0n
381
+ });
382
+ amount = balance.toString();
383
+ } else {
384
+ const balance = await publicClient.getBalance({ address: viemAccount.address });
385
+ const gasPrice = await publicClient.getGasPrice();
386
+ const gasCost = NATIVE_TRANSFER_GAS * gasPrice;
387
+ const maxSend = balance - gasCost;
388
+ if (maxSend <= 0n) {
389
+ throw new Error(
390
+ `Insufficient balance to cover gas. Balance: ${formatEther(balance)} ETH, gas cost: ${formatEther(gasCost)} ETH`
391
+ );
392
+ }
393
+ transactionHash = await walletClient.sendTransaction({
394
+ to,
395
+ value: maxSend,
396
+ gas: NATIVE_TRANSFER_GAS
397
+ });
398
+ amount = maxSend.toString();
399
+ }
400
+ return {
401
+ transactionHash,
402
+ from: viemAccount.address,
403
+ to,
404
+ amount,
405
+ token: tokenInfo.symbol
406
+ };
407
+ }
408
+ function resolveAccount2(account) {
409
+ if (account) return account;
410
+ const addresses = listAddresses();
411
+ if (addresses.length === 0) {
412
+ throw new Error("No wallet found. Run 'companion-wallet generate' first.");
413
+ }
414
+ return addresses[0];
415
+ }
416
+
417
+ // src/prompt.ts
418
+ import { createInterface } from "readline/promises";
419
+ import { stdin, stdout } from "process";
420
+ async function ask(question) {
421
+ const rl = createInterface({ input: stdin, output: stdout });
422
+ try {
423
+ const answer = await rl.question(question);
424
+ return answer.trim();
425
+ } finally {
426
+ rl.close();
427
+ }
428
+ }
429
+ function printOptions(items) {
430
+ for (let i = 0; i < items.length; i++) {
431
+ process.stderr.write(` ${i + 1}. ${items[i].label}
432
+ `);
433
+ }
434
+ }
435
+ async function selectChain() {
436
+ const options = SUPPORTED_CHAINS.map((caip2) => ({
437
+ label: getChainName(caip2),
438
+ value: caip2
439
+ }));
440
+ process.stderr.write("\nSelect chain:\n");
441
+ printOptions(options);
442
+ const input = await ask("> ");
443
+ const idx = parseInt(input, 10) - 1;
444
+ if (isNaN(idx) || idx < 0 || idx >= options.length) {
445
+ throw new Error(`Invalid selection: ${input}`);
446
+ }
447
+ return options[idx].value;
448
+ }
449
+ async function selectToken(chain) {
450
+ const symbols = getTokenSymbols(chain);
451
+ const options = symbols.map((s) => ({
452
+ label: s.toUpperCase(),
453
+ value: s
454
+ }));
455
+ process.stderr.write("\nSelect token:\n");
456
+ printOptions(options);
457
+ const input = await ask("> ");
458
+ const idx = parseInt(input, 10) - 1;
459
+ if (isNaN(idx) || idx < 0 || idx >= options.length) {
460
+ throw new Error(`Invalid selection: ${input}`);
461
+ }
462
+ return options[idx].value;
463
+ }
464
+ async function inputAmount(symbol) {
465
+ const input = await ask(`
466
+ Amount (${symbol.toUpperCase()}): `);
467
+ const num = parseFloat(input);
468
+ if (isNaN(num) || num <= 0) {
469
+ throw new Error(`Invalid amount: ${input}`);
470
+ }
471
+ return input;
472
+ }
473
+ async function inputAddress(label) {
474
+ const input = await ask(`
475
+ ${label}: `);
476
+ if (!/^0x[0-9a-fA-F]{40}$/.test(input)) {
477
+ throw new Error(`Invalid address: ${input}`);
478
+ }
479
+ return input;
480
+ }
481
+
482
+ // src/sessions.ts
483
+ import { randomBytes } from "crypto";
484
+ import {
485
+ mkdirSync as mkdirSync2,
486
+ writeFileSync as writeFileSync2,
487
+ readFileSync as readFileSync2,
488
+ renameSync as renameSync2
489
+ } from "fs";
490
+ import { join as join2, dirname as dirname2 } from "path";
491
+ import { homedir as homedir2 } from "os";
492
+ var SESSIONS_DIR = join2(homedir2(), ".config", "wallet", "sessions");
493
+ function sessionFilePath(sessionId) {
494
+ return join2(SESSIONS_DIR, `${sessionId}.json`);
495
+ }
496
+ function atomicWrite2(filePath, content) {
497
+ const tmpPath = filePath + ".tmp";
498
+ mkdirSync2(dirname2(filePath), { recursive: true, mode: 448 });
499
+ writeFileSync2(tmpPath, content, { mode: 384 });
500
+ renameSync2(tmpPath, filePath);
501
+ }
502
+ function grantSession(input) {
503
+ const sessionId = randomBytes(16).toString("hex");
504
+ const session = {
505
+ sessionId,
506
+ account: input.account,
507
+ chain: input.chain,
508
+ permissions: input.permissions,
509
+ expiry: input.expiry,
510
+ revoked: false,
511
+ callCounts: {},
512
+ totalValue: {}
513
+ };
514
+ atomicWrite2(sessionFilePath(sessionId), JSON.stringify(session, null, 2));
515
+ return session;
516
+ }
517
+ function revokeSession(sessionId) {
518
+ const session = loadSession(sessionId);
519
+ session.revoked = true;
520
+ atomicWrite2(
521
+ sessionFilePath(sessionId),
522
+ JSON.stringify(session, null, 2)
523
+ );
524
+ }
525
+ function loadSession(sessionId) {
526
+ const filePath = sessionFilePath(sessionId);
527
+ try {
528
+ const raw = readFileSync2(filePath, "utf-8");
529
+ return JSON.parse(raw);
530
+ } catch {
531
+ throw new Error(`Session not found: ${sessionId}`);
532
+ }
533
+ }
534
+ function validateSession(sessionId, operation, input) {
535
+ const session = loadSession(sessionId);
536
+ if (session.revoked) {
537
+ throw new SessionError("Session has been revoked");
538
+ }
539
+ if (Date.now() > session.expiry) {
540
+ throw new SessionError("Session has expired");
541
+ }
542
+ const permission = session.permissions.find(
543
+ (p) => p.operation === operation
544
+ );
545
+ if (!permission) {
546
+ throw new SessionError(
547
+ `Session does not permit operation: ${operation}`
548
+ );
549
+ }
550
+ if (permission.policies) {
551
+ for (const policy of permission.policies) {
552
+ validatePolicy(policy, session, input);
553
+ }
554
+ }
555
+ return session;
556
+ }
557
+ function recordSessionUsage(sessionId, operation, input) {
558
+ const session = loadSession(sessionId);
559
+ const key = operation;
560
+ session.callCounts[key] = (session.callCounts[key] || 0) + 1;
561
+ if (input?.transaction?.value) {
562
+ const chain = input.chain || "unknown";
563
+ const currentTotal = BigInt(session.totalValue[chain] || "0");
564
+ const txValue = BigInt(input.transaction.value);
565
+ session.totalValue[chain] = (currentTotal + txValue).toString();
566
+ }
567
+ atomicWrite2(
568
+ sessionFilePath(sessionId),
569
+ JSON.stringify(session, null, 2)
570
+ );
571
+ }
572
+ function validatePolicy(policy, session, input) {
573
+ switch (policy.type) {
574
+ case "value-limit": {
575
+ if (!input) break;
576
+ const tx = input.transaction;
577
+ if (!tx?.value) break;
578
+ const chain = input.chain || "unknown";
579
+ const maxValue = BigInt(policy.params.maxValue);
580
+ const currentTotal = BigInt(session.totalValue[chain] || "0");
581
+ const txValue = BigInt(tx.value);
582
+ if (currentTotal + txValue > maxValue) {
583
+ throw new SessionError(
584
+ `Value limit exceeded: ${(currentTotal + txValue).toString()} > ${maxValue.toString()}`
585
+ );
586
+ }
587
+ break;
588
+ }
589
+ case "recipient-allowlist": {
590
+ if (!input) break;
591
+ const tx = input.transaction;
592
+ if (!tx?.to) break;
593
+ const allowlist = policy.params.addresses.map(
594
+ (a) => a.toLowerCase()
595
+ );
596
+ const to = tx.to.toLowerCase();
597
+ if (!allowlist.includes(to)) {
598
+ throw new SessionError(
599
+ `Recipient ${tx.to} not in allowlist`
600
+ );
601
+ }
602
+ break;
603
+ }
604
+ case "call-limit": {
605
+ const maxCalls = policy.params.maxCalls;
606
+ const operation = policy.params.operation;
607
+ const currentCalls = session.callCounts[operation] || 0;
608
+ if (currentCalls >= maxCalls) {
609
+ throw new SessionError(
610
+ `Call limit reached for ${operation}: ${currentCalls} >= ${maxCalls}`
611
+ );
612
+ }
613
+ break;
614
+ }
615
+ }
616
+ }
617
+ var SessionError = class extends Error {
618
+ constructor(message) {
619
+ super(message);
620
+ this.name = "SessionError";
621
+ }
622
+ };
623
+
624
+ // src/types.ts
625
+ var ExitCode = {
626
+ SUCCESS: 0,
627
+ ERROR: 1,
628
+ UNSUPPORTED: 2,
629
+ REJECTED: 3,
630
+ TIMEOUT: 4,
631
+ NOT_CONNECTED: 5,
632
+ SESSION_ERROR: 6
633
+ };
634
+
635
+ // src/cli.ts
636
+ function getVersion() {
637
+ try {
638
+ const __dirname = dirname3(fileURLToPath(import.meta.url));
639
+ const pkg = JSON.parse(
640
+ readFileSync3(join3(__dirname, "..", "package.json"), "utf-8")
641
+ );
642
+ return pkg.version;
643
+ } catch {
644
+ return "0.0.0";
645
+ }
646
+ }
647
+ async function readStdin() {
648
+ if (process.stdin.isTTY) return "";
649
+ const chunks = [];
650
+ for await (const chunk of process.stdin) {
651
+ chunks.push(chunk);
652
+ }
653
+ return Buffer.concat(chunks).toString("utf-8").trim();
654
+ }
655
+ function respond(data) {
656
+ process.stdout.write(JSON.stringify(data) + "\n");
657
+ }
658
+ function respondError(error, code) {
659
+ const resp = { error, code };
660
+ process.stdout.write(JSON.stringify(resp) + "\n");
661
+ }
662
+ async function parseInput() {
663
+ const raw = await readStdin();
664
+ if (!raw) return null;
665
+ try {
666
+ return JSON.parse(raw);
667
+ } catch {
668
+ respondError("Invalid JSON input", "INVALID_INPUT");
669
+ process.exit(ExitCode.ERROR);
670
+ }
671
+ }
672
+ async function handleInfo() {
673
+ const info = {
674
+ name: "companion-wallet",
675
+ version: getVersion(),
676
+ rdns: "com.walletconnect.companion-wallet",
677
+ capabilities: [
678
+ "accounts",
679
+ "sign-message",
680
+ "sign-typed-data",
681
+ "sign-transaction",
682
+ "send-transaction",
683
+ "grant-session",
684
+ "revoke-session",
685
+ "get-session",
686
+ "fund",
687
+ "drain"
688
+ ],
689
+ chains: SUPPORTED_CHAINS
690
+ };
691
+ respond(info);
692
+ }
693
+ async function handleGenerate() {
694
+ const existing = listAddresses();
695
+ if (existing.length > 0) {
696
+ respond({ address: existing[0] });
697
+ return;
698
+ }
699
+ const { address, mnemonic } = generateAndStore();
700
+ respond({ address, mnemonic });
701
+ process.stderr.write(
702
+ "\n\u26A0\uFE0F BACKUP YOUR SEED PHRASE \u2014 it will not be shown again.\n Anyone with this phrase can access your funds.\n\n"
703
+ );
704
+ }
705
+ async function handleAccounts() {
706
+ const addresses = listAddresses();
707
+ const accounts = {
708
+ accounts: addresses.flatMap(
709
+ (address) => SUPPORTED_CHAINS.map((chain) => ({ chain, address }))
710
+ )
711
+ };
712
+ respond(accounts);
713
+ }
714
+ async function handleSignMessage() {
715
+ const input = await parseInput();
716
+ if (!input?.account || !input?.message) {
717
+ respondError("Missing account or message", "INVALID_INPUT");
718
+ process.exit(ExitCode.ERROR);
719
+ }
720
+ if (input.sessionId) {
721
+ validateSessionOrExit(input.sessionId, "sign-message", input);
722
+ }
723
+ const privateKey = loadKey(input.account);
724
+ const signature = await signMessage(privateKey, input.message);
725
+ if (input.sessionId) {
726
+ recordSessionUsage(input.sessionId, "sign-message");
727
+ }
728
+ respond({ signature });
729
+ }
730
+ async function handleSignTypedData() {
731
+ const input = await parseInput();
732
+ if (!input?.account || !input?.typedData) {
733
+ respondError("Missing account or typedData", "INVALID_INPUT");
734
+ process.exit(ExitCode.ERROR);
735
+ }
736
+ if (input.sessionId) {
737
+ validateSessionOrExit(input.sessionId, "sign-typed-data", input);
738
+ }
739
+ const privateKey = loadKey(input.account);
740
+ const signature = await signTypedData(privateKey, input.typedData);
741
+ if (input.sessionId) {
742
+ recordSessionUsage(input.sessionId, "sign-typed-data");
743
+ }
744
+ respond({ signature });
745
+ }
746
+ async function handleSignTransaction() {
747
+ const input = await parseInput();
748
+ if (!input?.account || !input?.transaction || !input?.chain) {
749
+ respondError("Missing account, transaction, or chain", "INVALID_INPUT");
750
+ process.exit(ExitCode.ERROR);
751
+ }
752
+ if (input.sessionId) {
753
+ validateSessionOrExit(input.sessionId, "sign-transaction", input);
754
+ }
755
+ const privateKey = loadKey(input.account);
756
+ const signedTransaction = await signTransaction(
757
+ privateKey,
758
+ input.transaction,
759
+ input.chain
760
+ );
761
+ if (input.sessionId) {
762
+ recordSessionUsage(input.sessionId, "sign-transaction");
763
+ }
764
+ respond({ signedTransaction });
765
+ }
766
+ async function handleSendTransaction() {
767
+ const input = await parseInput();
768
+ if (!input?.account || !input?.transaction || !input?.chain) {
769
+ respondError("Missing account, transaction, or chain", "INVALID_INPUT");
770
+ process.exit(ExitCode.ERROR);
771
+ }
772
+ if (input.sessionId) {
773
+ validateSessionOrExit(input.sessionId, "send-transaction", input);
774
+ }
775
+ const privateKey = loadKey(input.account);
776
+ const transactionHash = await sendTransaction(
777
+ privateKey,
778
+ input.transaction,
779
+ input.chain
780
+ );
781
+ if (input.sessionId) {
782
+ recordSessionUsage(input.sessionId, "send-transaction", input);
783
+ }
784
+ respond({ transactionHash });
785
+ }
786
+ async function handleGrantSession() {
787
+ const input = await parseInput();
788
+ if (!input?.account || !input?.chain || !input?.permissions || !input?.expiry) {
789
+ respondError(
790
+ "Missing account, chain, permissions, or expiry",
791
+ "INVALID_INPUT"
792
+ );
793
+ process.exit(ExitCode.ERROR);
794
+ }
795
+ const session = grantSession(input);
796
+ respond({
797
+ sessionId: session.sessionId,
798
+ permissions: session.permissions,
799
+ expiry: session.expiry
800
+ });
801
+ }
802
+ async function handleRevokeSession() {
803
+ const input = await parseInput();
804
+ if (!input?.sessionId) {
805
+ respondError("Missing sessionId", "INVALID_INPUT");
806
+ process.exit(ExitCode.ERROR);
807
+ }
808
+ try {
809
+ revokeSession(input.sessionId);
810
+ respond({ revoked: true });
811
+ } catch (err) {
812
+ respondError(
813
+ err instanceof Error ? err.message : "Failed to revoke session",
814
+ "SESSION_ERROR"
815
+ );
816
+ process.exit(ExitCode.SESSION_ERROR);
817
+ }
818
+ }
819
+ async function handleGetSession() {
820
+ const input = await parseInput();
821
+ if (!input?.sessionId) {
822
+ respondError("Missing sessionId", "INVALID_INPUT");
823
+ process.exit(ExitCode.ERROR);
824
+ }
825
+ try {
826
+ const session = loadSession(input.sessionId);
827
+ respond(session);
828
+ } catch (err) {
829
+ respondError(
830
+ err instanceof Error ? err.message : "Session not found",
831
+ "SESSION_ERROR"
832
+ );
833
+ process.exit(ExitCode.SESSION_ERROR);
834
+ }
835
+ }
836
+ function parseCliArg(name) {
837
+ const flag = `--${name}`;
838
+ const idx = process.argv.indexOf(flag);
839
+ if (idx === -1 || idx + 1 >= process.argv.length) return void 0;
840
+ return process.argv[idx + 1];
841
+ }
842
+ function warnBeta() {
843
+ process.stderr.write(
844
+ "WARNING: companion-wallet is beta software. Do not use with real funds on mainnet.\n"
845
+ );
846
+ }
847
+ async function handleFund() {
848
+ warnBeta();
849
+ const account = parseCliArg("account");
850
+ let amount = parseCliArg("amount");
851
+ let chain = parseCliArg("chain");
852
+ let token = parseCliArg("token");
853
+ try {
854
+ if (process.stdin.isTTY) {
855
+ if (!chain) chain = await selectChain();
856
+ if (!token) token = await selectToken(chain);
857
+ if (!amount) amount = await inputAmount(token);
858
+ }
859
+ if (!amount) {
860
+ respondError("Missing --amount", "INVALID_INPUT");
861
+ process.exit(ExitCode.ERROR);
862
+ }
863
+ chain = chain || "eip155:1";
864
+ token = token || "eth";
865
+ const result = await fund({ account, amount, chain, token });
866
+ respond(result);
867
+ } catch (err) {
868
+ respondError(
869
+ err instanceof Error ? err.message : "Fund failed",
870
+ "FUND_ERROR"
871
+ );
872
+ process.exit(ExitCode.ERROR);
873
+ }
874
+ }
875
+ async function handleDrain() {
876
+ warnBeta();
877
+ const account = parseCliArg("account");
878
+ let to = parseCliArg("to");
879
+ let chain = parseCliArg("chain");
880
+ let token = parseCliArg("token");
881
+ try {
882
+ if (process.stdin.isTTY) {
883
+ if (!chain) chain = await selectChain();
884
+ if (!token) token = await selectToken(chain);
885
+ if (!to) to = await inputAddress("Recipient address");
886
+ }
887
+ if (!to) {
888
+ respondError("Missing --to", "INVALID_INPUT");
889
+ process.exit(ExitCode.ERROR);
890
+ }
891
+ chain = chain || "eip155:1";
892
+ token = token || "eth";
893
+ const result = await drain({ account, to, chain, token });
894
+ respond(result);
895
+ } catch (err) {
896
+ respondError(
897
+ err instanceof Error ? err.message : "Drain failed",
898
+ "DRAIN_ERROR"
899
+ );
900
+ process.exit(ExitCode.ERROR);
901
+ }
902
+ }
903
+ function validateSessionOrExit(sessionId, operation, input) {
904
+ try {
905
+ validateSession(sessionId, operation, input);
906
+ } catch (err) {
907
+ respondError(
908
+ err instanceof Error ? err.message : "Session validation failed",
909
+ "SESSION_ERROR"
910
+ );
911
+ process.exit(
912
+ err instanceof SessionError ? ExitCode.SESSION_ERROR : ExitCode.ERROR
913
+ );
914
+ }
915
+ }
916
+ var HANDLERS = {
917
+ info: handleInfo,
918
+ generate: handleGenerate,
919
+ accounts: handleAccounts,
920
+ "sign-message": handleSignMessage,
921
+ "sign-typed-data": handleSignTypedData,
922
+ "sign-transaction": handleSignTransaction,
923
+ "send-transaction": handleSendTransaction,
924
+ "grant-session": handleGrantSession,
925
+ "revoke-session": handleRevokeSession,
926
+ "get-session": handleGetSession,
927
+ fund: handleFund,
928
+ drain: handleDrain
929
+ };
930
+ async function main() {
931
+ const operation = process.argv[2];
932
+ if (!operation || !(operation in HANDLERS)) {
933
+ respondError(
934
+ `Unsupported operation: ${operation || "(none)"}`,
935
+ "UNSUPPORTED_OPERATION"
936
+ );
937
+ process.exit(ExitCode.UNSUPPORTED);
938
+ }
939
+ try {
940
+ await HANDLERS[operation]();
941
+ } catch (err) {
942
+ if (err instanceof SessionError) {
943
+ respondError(err.message, "SESSION_ERROR");
944
+ process.exit(ExitCode.SESSION_ERROR);
945
+ }
946
+ respondError(
947
+ err instanceof Error ? err.message : "Internal error",
948
+ "INTERNAL_ERROR"
949
+ );
950
+ process.exit(ExitCode.ERROR);
951
+ }
952
+ }
953
+ main();