@coinfello/agent-cli 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -101,6 +101,23 @@ Transaction submitted successfully.
101
101
  Transaction ID: <txn_hash_>
102
102
  ```
103
103
 
104
+ ### 6. signer-daemon
105
+
106
+ Manages the Secure Enclave signing daemon. Without the daemon, each signing operation (account creation, sign-in, delegation signing) triggers a separate Touch ID / password prompt. Starting the daemon authenticates once and caches the authorization for subsequent operations.
107
+
108
+ ```bash
109
+ # Start the daemon (prompts Touch ID / password once)
110
+ node dist/index.js signer-daemon start
111
+
112
+ # Check if the daemon is running
113
+ node dist/index.js signer-daemon status
114
+
115
+ # Stop the daemon
116
+ node dist/index.js signer-daemon stop
117
+ ```
118
+
119
+ If the daemon is not running, all signing operations fall back to direct Secure Enclave binary execution (which prompts Touch ID each time).
120
+
104
121
  ### Help
105
122
 
106
123
  View all commands and options:
@@ -109,4 +126,5 @@ View all commands and options:
109
126
  node dist/index.js --help
110
127
  node dist/index.js create_account --help
111
128
  node dist/index.js send_prompt --help
129
+ node dist/index.js signer-daemon --help
112
130
  ```
package/dist/index.js CHANGED
@@ -3,26 +3,143 @@ import { Command } from "commander";
3
3
  import { toMetaMaskSmartAccount, Implementation, createDelegation } from "@metamask/smart-accounts-kit";
4
4
  import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
5
5
  import { toWebAuthnAccount } from "viem/account-abstraction";
6
- import { createPublicClient, http, serializeErc6492Signature } from "viem";
7
6
  import * as chains from "viem/chains";
8
7
  import { createHash, randomBytes } from "node:crypto";
9
- import { execFile } from "node:child_process";
8
+ import { execFile, spawn } from "node:child_process";
10
9
  import { promisify } from "node:util";
11
10
  import { dirname, join } from "node:path";
12
11
  import { fileURLToPath } from "node:url";
13
- import { platform, homedir } from "node:os";
14
- import { readFile, mkdir, writeFile } from "node:fs/promises";
12
+ import { userInfo, platform, homedir } from "node:os";
13
+ import { readFile, unlink, mkdir, writeFile } from "node:fs/promises";
14
+ import { createConnection } from "node:net";
15
+ import { http, createPublicClient as createPublicClient$1, serializeErc6492Signature } from "viem";
15
16
  import { createSiweMessage } from "viem/siwe";
16
17
  const execFileAsync = promisify(execFile);
17
18
  const __filename$1 = fileURLToPath(import.meta.url);
18
19
  const __dirname$1 = dirname(__filename$1);
19
20
  function getBinaryPath() {
20
- return join(__dirname$1, "secure-enclave-signer.app", "Contents", "MacOS", "secure-enclave-signer");
21
+ const bundlePath = join(
22
+ __dirname$1,
23
+ "secure-enclave-signer.app",
24
+ "Contents",
25
+ "MacOS",
26
+ "secure-enclave-signer"
27
+ );
28
+ if (__dirname$1.includes("/src/")) {
29
+ const projectRoot = __dirname$1.split("/src/")[0];
30
+ return join(
31
+ projectRoot,
32
+ "dist",
33
+ "secure-enclave-signer.app",
34
+ "Contents",
35
+ "MacOS",
36
+ "secure-enclave-signer"
37
+ );
38
+ }
39
+ return bundlePath;
21
40
  }
41
+ const SOCKET_PATH = `/tmp/coinfello-se-signer-${userInfo().username}.sock`;
42
+ const PID_PATH = `/tmp/coinfello-se-signer-${userInfo().username}.pid`;
22
43
  function isSecureEnclaveAvailable() {
23
44
  return platform() === "darwin";
24
45
  }
25
- async function runCommand(args) {
46
+ async function sendDaemonRequest(request) {
47
+ return new Promise((resolve, reject) => {
48
+ const client = createConnection({ path: SOCKET_PATH }, () => {
49
+ const data = JSON.stringify(request);
50
+ client.end(data);
51
+ });
52
+ let response = "";
53
+ client.on("data", (chunk) => {
54
+ response += chunk.toString();
55
+ });
56
+ client.on("end", () => {
57
+ try {
58
+ const parsed = JSON.parse(response.trim());
59
+ if (parsed.success) {
60
+ resolve(parsed.result);
61
+ } else {
62
+ reject(new Error(`SecureEnclave [${parsed.error}]: ${parsed.message}`));
63
+ }
64
+ } catch {
65
+ reject(new Error(`Invalid daemon response: ${response}`));
66
+ }
67
+ });
68
+ client.on("error", (err) => {
69
+ reject(err);
70
+ });
71
+ client.setTimeout(3e4, () => {
72
+ client.destroy();
73
+ reject(new Error("Daemon request timed out"));
74
+ });
75
+ });
76
+ }
77
+ async function isDaemonRunning() {
78
+ try {
79
+ await sendDaemonRequest({ command: "ping" });
80
+ return true;
81
+ } catch {
82
+ return false;
83
+ }
84
+ }
85
+ async function startDaemon() {
86
+ const binaryPath = getBinaryPath();
87
+ const child = spawn(binaryPath, ["daemon"], {
88
+ detached: true,
89
+ stdio: ["ignore", "pipe", "pipe"]
90
+ });
91
+ child.unref();
92
+ return new Promise((resolve, reject) => {
93
+ let stdout = "";
94
+ let stderr = "";
95
+ child.stdout.on("data", (chunk) => {
96
+ stdout += chunk.toString();
97
+ try {
98
+ const ready = JSON.parse(stdout.trim());
99
+ if (ready.status === "ready") {
100
+ child.stdout.removeAllListeners();
101
+ child.stderr.removeAllListeners();
102
+ resolve({ pid: ready.pid, socket: ready.socket });
103
+ }
104
+ } catch {
105
+ }
106
+ });
107
+ child.stderr.on("data", (chunk) => {
108
+ stderr += chunk.toString();
109
+ });
110
+ child.on("error", (err) => {
111
+ reject(new Error(`Failed to start daemon: ${err.message}`));
112
+ });
113
+ child.on("exit", (code) => {
114
+ if (code !== 0) {
115
+ try {
116
+ const parsed = JSON.parse(stderr.trim());
117
+ reject(new Error(`Daemon exited: [${parsed.error}] ${parsed.message}`));
118
+ } catch {
119
+ reject(new Error(`Daemon exited with code ${code}: ${stderr}`));
120
+ }
121
+ }
122
+ });
123
+ setTimeout(() => reject(new Error("Daemon startup timed out")), 15e3);
124
+ });
125
+ }
126
+ async function stopDaemon() {
127
+ try {
128
+ const pidStr = await readFile(PID_PATH, "utf-8");
129
+ const pid = parseInt(pidStr.trim(), 10);
130
+ process.kill(pid, "SIGTERM");
131
+ } catch {
132
+ try {
133
+ await unlink(SOCKET_PATH);
134
+ } catch {
135
+ }
136
+ try {
137
+ await unlink(PID_PATH);
138
+ } catch {
139
+ }
140
+ }
141
+ }
142
+ async function runCommandDirect(args) {
26
143
  const binaryPath = getBinaryPath();
27
144
  try {
28
145
  const { stdout } = await execFileAsync(binaryPath, args, {
@@ -45,6 +162,24 @@ async function runCommand(args) {
45
162
  throw new Error(`SecureEnclave command failed: ${error.message ?? "Unknown error"}`);
46
163
  }
47
164
  }
165
+ async function runCommand(args) {
166
+ try {
167
+ const command = args[0];
168
+ const request = { command };
169
+ if (command === "sign") {
170
+ const tagIdx = args.indexOf("--tag");
171
+ const payloadIdx = args.indexOf("--payload");
172
+ if (tagIdx >= 0 && tagIdx + 1 < args.length) request.tag = args[tagIdx + 1];
173
+ if (payloadIdx >= 0 && payloadIdx + 1 < args.length) request.payload = args[payloadIdx + 1];
174
+ } else if (command === "get-public-key") {
175
+ const tagIdx = args.indexOf("--tag");
176
+ if (tagIdx >= 0 && tagIdx + 1 < args.length) request.tag = args[tagIdx + 1];
177
+ }
178
+ return await sendDaemonRequest(request);
179
+ } catch {
180
+ return await runCommandDirect(args);
181
+ }
182
+ }
48
183
  async function generateKey() {
49
184
  const result = await runCommand(["generate"]);
50
185
  return {
@@ -115,6 +250,30 @@ function toArrayBuffer(buf) {
115
250
  view.set(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
116
251
  return ab;
117
252
  }
253
+ const INFURA_API_KEY = process.env.INFURA_API_KEY ?? "b6bf7d3508c941499b10025c0776eaf8";
254
+ const INFURA_CHAIN_NAMES = {
255
+ 1: "mainnet",
256
+ 11155111: "sepolia",
257
+ 137: "polygon-mainnet",
258
+ 80002: "polygon-amoy",
259
+ 42161: "arbitrum-mainnet",
260
+ 421614: "arbitrum-sepolia",
261
+ 10: "optimism-mainnet",
262
+ 11155420: "optimism-sepolia",
263
+ 8453: "base-mainnet",
264
+ 84532: "base-sepolia",
265
+ 59144: "linea-mainnet",
266
+ 59141: "linea-sepolia",
267
+ 43114: "avalanche-mainnet",
268
+ 43113: "avalanche-fuji",
269
+ 56: "bsc-mainnet",
270
+ 97: "bsc-testnet"
271
+ };
272
+ function createPublicClient(chain) {
273
+ const infuraName = INFURA_CHAIN_NAMES[chain.id];
274
+ const transport = infuraName ? http(`https://${infuraName}.infura.io/v3/${INFURA_API_KEY}`) : http();
275
+ return createPublicClient$1({ chain, transport });
276
+ }
118
277
  function resolveChain(chainName) {
119
278
  const chain = chains[chainName];
120
279
  if (!chain) {
@@ -141,10 +300,7 @@ function resolveChainInput(chainInput) {
141
300
  }
142
301
  async function createSmartAccount(privateKey, chainInput) {
143
302
  const chain = resolveChainInput(chainInput);
144
- const publicClient = createPublicClient({
145
- chain,
146
- transport: http()
147
- });
303
+ const publicClient = createPublicClient(chain);
148
304
  const owner = privateKeyToAccount(privateKey);
149
305
  const smartAccount = await toMetaMaskSmartAccount({
150
306
  client: publicClient,
@@ -177,7 +333,7 @@ function createSubdelegation({
177
333
  }
178
334
  async function createSmartAccountWithSecureEnclave(chainInput) {
179
335
  const chain = resolveChainInput(chainInput);
180
- const publicClient = createPublicClient({ chain, transport: http() });
336
+ const publicClient = createPublicClient(chain);
181
337
  const keyPair = await generateKey();
182
338
  const keyId = `0x${randomBytes(32).toString("hex")}`;
183
339
  const xHex = keyPair.x.toString(16).padStart(64, "0");
@@ -217,7 +373,7 @@ async function createSmartAccountWithSecureEnclave(chainInput) {
217
373
  }
218
374
  async function getSmartAccountFromSecureEnclave(keyTag, publicKeyX, publicKeyY, keyId, chainInput) {
219
375
  const chain = resolveChainInput(chainInput);
220
- const publicClient = createPublicClient({ chain, transport: http() });
376
+ const publicClient = createPublicClient(chain);
221
377
  const xBigInt = BigInt(publicKeyX);
222
378
  const yBigInt = BigInt(publicKeyY);
223
379
  const xHex = xBigInt.toString(16).padStart(64, "0");
@@ -3050,18 +3206,17 @@ program.command("create_account").description("Create a MetaMask smart account a
3050
3206
  if (useHardwareKey) {
3051
3207
  console.log(`Creating Secure Enclave-backed smart account on ${chain}...`);
3052
3208
  const { address, keyTag, publicKeyX, publicKeyY, keyId } = await createSmartAccountWithSecureEnclave(chain);
3053
- const config2 = await loadConfig();
3054
- config2.signer_type = "secureEnclave";
3055
- config2.smart_account_address = address;
3056
- config2.chain = chain;
3057
- config2.secure_enclave = {
3209
+ config.signer_type = "secureEnclave";
3210
+ config.smart_account_address = address;
3211
+ config.chain = chain;
3212
+ config.secure_enclave = {
3058
3213
  key_tag: keyTag,
3059
3214
  public_key_x: publicKeyX,
3060
3215
  public_key_y: publicKeyY,
3061
3216
  key_id: keyId
3062
3217
  };
3063
- delete config2.private_key;
3064
- await saveConfig(config2);
3218
+ delete config.private_key;
3219
+ await saveConfig(config);
3065
3220
  console.log("Secure Enclave smart account created successfully.");
3066
3221
  console.log(`Address: ${address}`);
3067
3222
  console.log(`Key tag: ${keyTag}`);
@@ -3075,12 +3230,11 @@ program.command("create_account").description("Create a MetaMask smart account a
3075
3230
  console.log(`Creating smart account on ${chain}...`);
3076
3231
  const privateKey = generatePrivateKey();
3077
3232
  const { address } = await createSmartAccount(privateKey, chain);
3078
- const config2 = await loadConfig();
3079
- config2.private_key = privateKey;
3080
- config2.signer_type = "privateKey";
3081
- config2.smart_account_address = address;
3082
- config2.chain = chain;
3083
- await saveConfig(config2);
3233
+ config.private_key = privateKey;
3234
+ config.signer_type = "privateKey";
3235
+ config.smart_account_address = address;
3236
+ config.chain = chain;
3237
+ await saveConfig(config);
3084
3238
  console.log("Smart account created successfully.");
3085
3239
  console.log(`Address: ${address}`);
3086
3240
  console.log(`Config saved to: ${CONFIG_PATH}`);
@@ -3207,10 +3361,7 @@ program.command("send_prompt").description("Send a prompt to CoinFello, creating
3207
3361
  });
3208
3362
  let sig = signature;
3209
3363
  const chain = resolveChainInput(config.chain);
3210
- const publicClient = createPublicClient({
3211
- chain,
3212
- transport: http()
3213
- });
3364
+ const publicClient = createPublicClient(chain);
3214
3365
  const code = await publicClient.getCode({ address: smartAccount.address });
3215
3366
  const isDeployed = !!(code && code !== "0x");
3216
3367
  if (!isDeployed) {
@@ -3241,4 +3392,44 @@ program.command("send_prompt").description("Send a prompt to CoinFello, creating
3241
3392
  process.exit(1);
3242
3393
  }
3243
3394
  });
3395
+ const signerDaemon = program.command("signer-daemon").description("Manage the Secure Enclave signing daemon");
3396
+ signerDaemon.command("start").description("Start the signing daemon (authenticates via Touch ID / password once)").action(async () => {
3397
+ try {
3398
+ const running = await isDaemonRunning();
3399
+ if (running) {
3400
+ console.log("Signing daemon is already running.");
3401
+ return;
3402
+ }
3403
+ console.log("Starting signing daemon (authenticate when prompted)...");
3404
+ const { pid, socket } = await startDaemon();
3405
+ console.log(`Signing daemon started.`);
3406
+ console.log(`PID: ${pid}`);
3407
+ console.log(`Socket: ${socket}`);
3408
+ } catch (err) {
3409
+ console.error(`Failed to start daemon: ${err.message}`);
3410
+ process.exit(1);
3411
+ }
3412
+ });
3413
+ signerDaemon.command("stop").description("Stop the signing daemon").action(async () => {
3414
+ try {
3415
+ const running = await isDaemonRunning();
3416
+ if (!running) {
3417
+ console.log("Signing daemon is not running.");
3418
+ return;
3419
+ }
3420
+ await stopDaemon();
3421
+ console.log("Signing daemon stopped.");
3422
+ } catch (err) {
3423
+ console.error(`Failed to stop daemon: ${err.message}`);
3424
+ process.exit(1);
3425
+ }
3426
+ });
3427
+ signerDaemon.command("status").description("Check if the signing daemon is running").action(async () => {
3428
+ const running = await isDaemonRunning();
3429
+ if (running) {
3430
+ console.log("Signing daemon is running.");
3431
+ } else {
3432
+ console.log("Signing daemon is not running.");
3433
+ }
3434
+ });
3244
3435
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coinfello/agent-cli",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",