@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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
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
|
|
3064
|
-
await saveConfig(
|
|
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
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
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();
|
|
Binary file
|