nodpay 0.2.35 → 0.2.37
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/bin/nodpay.mjs +30 -0
- package/package.json +1 -1
- package/scripts/config.mjs +20 -0
- package/scripts/env.mjs +59 -0
- package/scripts/keygen.mjs +10 -19
- package/scripts/propose.mjs +31 -41
- package/scripts/remote.mjs +58 -0
- package/scripts/sign.mjs +51 -0
- package/scripts/wallets.mjs +38 -0
package/bin/nodpay.mjs
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
const command = process.argv[2];
|
|
4
4
|
|
|
5
|
+
// Remote wallet routing: if config.json has remote_wallet and command is sign/wallets,
|
|
6
|
+
// delegate to SafeClaw vault proxy (HTTP passthrough) instead of local execution.
|
|
7
|
+
const REMOTE_COMMANDS = new Set(['sign', 'wallets']);
|
|
8
|
+
if (command && REMOTE_COMMANDS.has(command)) {
|
|
9
|
+
const { loadConfig } = await import('../scripts/config.mjs');
|
|
10
|
+
const config = loadConfig();
|
|
11
|
+
if (config.remote_wallet) {
|
|
12
|
+
const { remoteSign, remoteWallets } = await import('../scripts/remote.mjs');
|
|
13
|
+
if (command === 'sign') await remoteSign(config.remote_wallet);
|
|
14
|
+
else if (command === 'wallets') await remoteWallets(config.remote_wallet);
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
5
19
|
if (command === 'propose') {
|
|
6
20
|
// Forward all args after 'propose' to the propose script
|
|
7
21
|
const scriptPath = new URL('../scripts/propose.mjs', import.meta.url).pathname;
|
|
@@ -23,6 +37,14 @@ if (command === 'propose') {
|
|
|
23
37
|
const scriptPath = new URL('../scripts/gasprice.mjs', import.meta.url).pathname;
|
|
24
38
|
process.argv = [process.argv[0], scriptPath, ...process.argv.slice(3)];
|
|
25
39
|
await import(scriptPath);
|
|
40
|
+
} else if (command === 'sign') {
|
|
41
|
+
const scriptPath = new URL('../scripts/sign.mjs', import.meta.url).pathname;
|
|
42
|
+
process.argv = [process.argv[0], scriptPath, ...process.argv.slice(3)];
|
|
43
|
+
await import(scriptPath);
|
|
44
|
+
} else if (command === 'wallets') {
|
|
45
|
+
const scriptPath = new URL('../scripts/wallets.mjs', import.meta.url).pathname;
|
|
46
|
+
process.argv = [process.argv[0], scriptPath, ...process.argv.slice(3)];
|
|
47
|
+
await import(scriptPath);
|
|
26
48
|
} else if (command === 'version' || command === '--version' || command === '-v') {
|
|
27
49
|
const { readFileSync } = await import('fs');
|
|
28
50
|
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
@@ -36,6 +58,8 @@ Commands:
|
|
|
36
58
|
txs List pending and completed transactions
|
|
37
59
|
nonce Query next nonce from on-chain EntryPoint
|
|
38
60
|
gasprice Get current gas price and estimated gas cost per chain
|
|
61
|
+
sign Sign a hash with agent key (stdin JSON)
|
|
62
|
+
wallets List all local wallets (JSON array)
|
|
39
63
|
|
|
40
64
|
Examples:
|
|
41
65
|
nodpay keygen
|
|
@@ -43,6 +67,12 @@ Examples:
|
|
|
43
67
|
nodpay txs --safe 0x...
|
|
44
68
|
nodpay nonce --safe 0x... --chain base
|
|
45
69
|
nodpay gasprice --chain base
|
|
70
|
+
echo '{"hash":"0x..."}' | nodpay sign
|
|
71
|
+
nodpay wallets
|
|
72
|
+
|
|
73
|
+
Remote wallet (SafeClaw):
|
|
74
|
+
Set ~/.nodpay/config.json: {"remote_wallet": "http://localhost:23295/nodpay"}
|
|
75
|
+
sign/wallets will delegate to SafeClaw vault proxy automatically.
|
|
46
76
|
|
|
47
77
|
Docs: https://nodpay.ai/skill.md`);
|
|
48
78
|
process.exit(command ? 1 : 0);
|
package/package.json
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load ~/.nodpay/config.json (optional).
|
|
3
|
+
*
|
|
4
|
+
* Returns parsed config or {} if file doesn't exist / is invalid.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync, existsSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { HOME } from './env.mjs';
|
|
10
|
+
|
|
11
|
+
const CONFIG_PATH = join(HOME, '.nodpay', 'config.json');
|
|
12
|
+
|
|
13
|
+
export function loadConfig(path = CONFIG_PATH) {
|
|
14
|
+
if (!existsSync(path)) return {};
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
17
|
+
} catch {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
}
|
package/scripts/env.mjs
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified environment variable loader for NodPay CLI.
|
|
3
|
+
*
|
|
4
|
+
* Priority (highest wins):
|
|
5
|
+
* 1. process.env (real env — set by caller, SafeClaw injection, container, etc.)
|
|
6
|
+
* 2. ~/.nodpay/.env file (convenience layer for local dev)
|
|
7
|
+
*
|
|
8
|
+
* The .env file is loaded into process.env WITHOUT overriding existing values.
|
|
9
|
+
* This matches standard dotenv behavior and Unix conventions:
|
|
10
|
+
* NODPAY_AGENT_KEY=0xabc npx nodpay propose → uses 0xabc, ignores file
|
|
11
|
+
*
|
|
12
|
+
* After loadDotEnv(), all code reads from process.env uniformly.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readFileSync, existsSync } from 'fs';
|
|
16
|
+
import { join } from 'path';
|
|
17
|
+
|
|
18
|
+
const HOME = process.env.HOME || process.env.USERPROFILE || '/tmp';
|
|
19
|
+
const DEFAULT_ENV_PATH = join(HOME, '.nodpay', '.env');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parse a .env file and load values into process.env.
|
|
23
|
+
* Does NOT override existing env vars (real env takes priority).
|
|
24
|
+
*
|
|
25
|
+
* @param {string} [envPath] - Path to .env file. Defaults to ~/.nodpay/.env
|
|
26
|
+
*/
|
|
27
|
+
export function loadDotEnv(envPath = DEFAULT_ENV_PATH) {
|
|
28
|
+
if (!existsSync(envPath)) return;
|
|
29
|
+
try {
|
|
30
|
+
const lines = readFileSync(envPath, 'utf8').split('\n');
|
|
31
|
+
for (const line of lines) {
|
|
32
|
+
const trimmed = line.trim();
|
|
33
|
+
if (!trimmed || trimmed.startsWith('#') || !trimmed.includes('=')) continue;
|
|
34
|
+
const eqIdx = trimmed.indexOf('=');
|
|
35
|
+
const name = trimmed.slice(0, eqIdx).trim();
|
|
36
|
+
const value = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, '');
|
|
37
|
+
// Only set if not already in process.env (real env wins)
|
|
38
|
+
if (!(name in process.env)) {
|
|
39
|
+
process.env[name] = value;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
// Non-fatal: .env file may not exist or be unreadable
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get an env var after .env has been loaded.
|
|
49
|
+
* Convenience wrapper — just reads process.env.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} name - Environment variable name
|
|
52
|
+
* @param {string} [fallback] - Default value if not set
|
|
53
|
+
* @returns {string|undefined}
|
|
54
|
+
*/
|
|
55
|
+
export function env(name, fallback) {
|
|
56
|
+
return process.env[name] || fallback;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { HOME, DEFAULT_ENV_PATH };
|
package/scripts/keygen.mjs
CHANGED
|
@@ -14,41 +14,32 @@
|
|
|
14
14
|
import { Wallet } from 'ethers';
|
|
15
15
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from 'fs';
|
|
16
16
|
import { resolve, dirname, join } from 'path';
|
|
17
|
+
import { loadDotEnv, env, HOME } from './env.mjs';
|
|
17
18
|
|
|
18
19
|
const args = process.argv.slice(2);
|
|
19
20
|
const envFileIdx = args.indexOf('--env-file');
|
|
20
|
-
const HOME = process.env.HOME || process.env.USERPROFILE || '/tmp';
|
|
21
21
|
const envFile = envFileIdx !== -1
|
|
22
22
|
? resolve(args[envFileIdx + 1])
|
|
23
23
|
: join(HOME, '.nodpay', '.env');
|
|
24
24
|
|
|
25
25
|
const ENV_VAR = 'NODPAY_AGENT_KEY';
|
|
26
26
|
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
if (!existsSync(envFile)) return null;
|
|
30
|
-
const lines = readFileSync(envFile, 'utf8').split('\n');
|
|
31
|
-
for (const line of lines) {
|
|
32
|
-
const trimmed = line.trim();
|
|
33
|
-
if (trimmed.startsWith('#') || !trimmed.includes('=')) continue;
|
|
34
|
-
const [name, ...rest] = trimmed.split('=');
|
|
35
|
-
if (name.trim() === ENV_VAR) {
|
|
36
|
-
const value = rest.join('=').trim().replace(/^["']|["']$/g, '');
|
|
37
|
-
if (value) return value;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
27
|
+
// Load .env file into process.env (real env takes priority)
|
|
28
|
+
loadDotEnv(envFile);
|
|
42
29
|
|
|
43
|
-
|
|
30
|
+
// Check if key already exists (process.env — from real env or .env file)
|
|
31
|
+
const existing = env(ENV_VAR);
|
|
44
32
|
|
|
45
33
|
if (existing) {
|
|
46
34
|
try {
|
|
47
35
|
const wallet = new Wallet(existing);
|
|
48
36
|
console.log(wallet.address);
|
|
49
|
-
|
|
37
|
+
const source = process.env[ENV_VAR] === existing && !existsSync(envFile)
|
|
38
|
+
? 'environment variable'
|
|
39
|
+
: envFile;
|
|
40
|
+
console.error(`${ENV_VAR} already configured (${source})`);
|
|
50
41
|
} catch {
|
|
51
|
-
console.error(`${ENV_VAR}
|
|
42
|
+
console.error(`${ENV_VAR} is set but invalid. Check your env or ${envFile}.`);
|
|
52
43
|
process.exit(1);
|
|
53
44
|
}
|
|
54
45
|
} else {
|
package/scripts/propose.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* The agent signs first (1 of 2). The serialized SafeOperation is
|
|
7
7
|
* output so the web app can have the user co-sign and submit.
|
|
8
8
|
*
|
|
9
|
-
* Agent key:
|
|
9
|
+
* Agent key: from process.env.NODPAY_AGENT_KEY (real env > ~/.nodpay/.env file > --remote-signer).
|
|
10
10
|
* Chain config: resolved via --chain from @nodpay/core networks registry.
|
|
11
11
|
* Bundler: NodPay public proxy (override with OP_STORE_URL for self-hosted).
|
|
12
12
|
*
|
|
@@ -27,15 +27,22 @@
|
|
|
27
27
|
|
|
28
28
|
import { Safe4337Pack } from '@safe-global/relay-kit';
|
|
29
29
|
import { ethers } from 'ethers';
|
|
30
|
-
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
30
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
31
31
|
import { join, dirname } from 'path';
|
|
32
32
|
import { fileURLToPath } from 'url';
|
|
33
33
|
import { computeUserOpHash, ENTRYPOINT } from '@nodpay/core';
|
|
34
|
+
import { loadDotEnv, env, HOME } from './env.mjs';
|
|
34
35
|
|
|
35
36
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
36
37
|
const PENDING_DIR = join(__dirname, '..', '.pending-txs');
|
|
37
38
|
mkdirSync(PENDING_DIR, { recursive: true });
|
|
38
39
|
|
|
40
|
+
// Load ~/.nodpay/.env into process.env (real env takes priority, file is fallback)
|
|
41
|
+
const _envFileArg = process.argv.includes('--env-file')
|
|
42
|
+
? process.argv[process.argv.indexOf('--env-file') + 1]
|
|
43
|
+
: undefined;
|
|
44
|
+
loadDotEnv(_envFileArg);
|
|
45
|
+
|
|
39
46
|
// Resolve chain config: --chain flag auto-resolves from networks.json, env vars as fallback
|
|
40
47
|
import { createRequire } from 'module';
|
|
41
48
|
const require = createRequire(import.meta.url);
|
|
@@ -64,26 +71,10 @@ if (chainArg) {
|
|
|
64
71
|
}
|
|
65
72
|
const ENTRYPOINT_ADDRESS = ENTRYPOINT;
|
|
66
73
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
// so it never passes through the LLM agent's context or conversation history.
|
|
72
|
-
function loadAgentKey() {
|
|
73
|
-
try {
|
|
74
|
-
const envPath = join(HOME, '.nodpay', '.env');
|
|
75
|
-
const lines = readFileSync(envPath, 'utf8').split('\n');
|
|
76
|
-
for (const line of lines) {
|
|
77
|
-
const trimmed = line.trim();
|
|
78
|
-
if (trimmed.startsWith('#') || !trimmed.includes('=')) continue;
|
|
79
|
-
const [name, ...rest] = trimmed.split('=');
|
|
80
|
-
if (name.trim() === 'NODPAY_AGENT_KEY') {
|
|
81
|
-
return rest.join('=').trim().replace(/^["']|["']$/g, '');
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
} catch {}
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
74
|
+
// SECURITY: Agent key is read from process.env.NODPAY_AGENT_KEY.
|
|
75
|
+
// The .env file was loaded above (loadDotEnv) without overriding real env vars.
|
|
76
|
+
// Priority: real env (SafeClaw injection, container, explicit) > .env file.
|
|
77
|
+
// The key never appears in stdout or agent conversation history.
|
|
87
78
|
|
|
88
79
|
const DEFAULT_SAFE = null; // always use --safe flag
|
|
89
80
|
|
|
@@ -91,21 +82,7 @@ const DEFAULT_SAFE = null; // always use --safe flag
|
|
|
91
82
|
// agents don't need their own bundler API key. This is a thin relay — it
|
|
92
83
|
// forwards the UserOp to a bundler service and returns the result. The proxy
|
|
93
84
|
// only sees the already-signed (partial) UserOp; it cannot modify or execute it.
|
|
94
|
-
|
|
95
|
-
function loadDotEnvVar(name, fallback) {
|
|
96
|
-
try {
|
|
97
|
-
const envPath = join(HOME, '.nodpay', '.env');
|
|
98
|
-
const lines = readFileSync(envPath, 'utf8').split('\n');
|
|
99
|
-
for (const line of lines) {
|
|
100
|
-
const trimmed = line.trim();
|
|
101
|
-
if (trimmed.startsWith('#') || !trimmed.includes('=')) continue;
|
|
102
|
-
const [k, ...rest] = trimmed.split('=');
|
|
103
|
-
if (k.trim() === name) return rest.join('=').trim().replace(/^["']|["']$/g, '');
|
|
104
|
-
}
|
|
105
|
-
} catch {}
|
|
106
|
-
return fallback;
|
|
107
|
-
}
|
|
108
|
-
const opStoreBase = loadDotEnvVar('OP_STORE_URL', 'https://nodpay.ai/api');
|
|
85
|
+
const opStoreBase = env('OP_STORE_URL', 'https://nodpay.ai/api');
|
|
109
86
|
const BUNDLER_URL = `${opStoreBase}/bundler/${CHAIN_ID}`;
|
|
110
87
|
|
|
111
88
|
// --- Agent key: remote signer (SafeClaw proxy) or local key (~/.nodpay/.env) ---
|
|
@@ -143,10 +120,10 @@ if (_remoteSignerUrl) {
|
|
|
143
120
|
return (await signRes.json()).signature;
|
|
144
121
|
};
|
|
145
122
|
} else {
|
|
146
|
-
// Local mode: read key from ~/.nodpay/.env
|
|
147
|
-
const NODPAY_AGENT_KEY =
|
|
123
|
+
// Local mode: read key from process.env (loaded from real env or ~/.nodpay/.env)
|
|
124
|
+
const NODPAY_AGENT_KEY = env('NODPAY_AGENT_KEY');
|
|
148
125
|
if (!NODPAY_AGENT_KEY) {
|
|
149
|
-
console.error(JSON.stringify({ error: 'Missing NODPAY_AGENT_KEY
|
|
126
|
+
console.error(JSON.stringify({ error: 'Missing NODPAY_AGENT_KEY — set env var, add to ~/.nodpay/.env, or use --remote-signer' }));
|
|
150
127
|
process.exit(1);
|
|
151
128
|
}
|
|
152
129
|
_localAgentKey = NODPAY_AGENT_KEY;
|
|
@@ -201,6 +178,18 @@ if (!ethers.isAddress(to)) {
|
|
|
201
178
|
|
|
202
179
|
const SAFE_ADDRESS = safeOverride || DEFAULT_SAFE;
|
|
203
180
|
|
|
181
|
+
// Read optional wallet JSON for rpId (SafeClaw cross-origin passkey support)
|
|
182
|
+
let walletRpId = null;
|
|
183
|
+
if (SAFE_ADDRESS) {
|
|
184
|
+
const walletJsonPath = join(HOME, '.nodpay', 'wallets', `${SAFE_ADDRESS}.json`);
|
|
185
|
+
if (existsSync(walletJsonPath)) {
|
|
186
|
+
try {
|
|
187
|
+
const walletData = JSON.parse(readFileSync(walletJsonPath, 'utf8'));
|
|
188
|
+
if (walletData.rpId) walletRpId = walletData.rpId;
|
|
189
|
+
} catch {}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
204
193
|
if (!isCounterfactual && !SAFE_ADDRESS) {
|
|
205
194
|
console.error(JSON.stringify({ error: 'Missing SAFE_ADDRESS. Use --safe <address> or set SAFE_ADDRESS env, or use --counterfactual.' }));
|
|
206
195
|
process.exit(1);
|
|
@@ -588,12 +577,13 @@ try {
|
|
|
588
577
|
result.opStoreError = storeData.error || `HTTP ${storeRes.status}`;
|
|
589
578
|
}
|
|
590
579
|
if (storeData.shortHash) {
|
|
591
|
-
const webBase =
|
|
580
|
+
const webBase = env('WEB_APP_URL', 'https://nodpay.ai/');
|
|
592
581
|
// Build approve URL with passkey params for SafeClaw users (no localStorage)
|
|
593
582
|
const approveParams = new URLSearchParams({ safeOpHash: storeData.safeOpHash });
|
|
594
583
|
if (passkeyX) approveParams.set('px', passkeyX);
|
|
595
584
|
if (passkeyY) approveParams.set('py', passkeyY);
|
|
596
585
|
if (recoverySigner) approveParams.set('recovery', recoverySigner);
|
|
586
|
+
if (walletRpId) approveParams.set('rpId', walletRpId);
|
|
597
587
|
approveUrl = `${webBase}approve?${approveParams.toString()}`;
|
|
598
588
|
result.approveUrl = approveUrl;
|
|
599
589
|
result.opStoreSafeOpHash = storeData.safeOpHash;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote wallet proxy — delegates sign/wallets to SafeClaw vault proxy.
|
|
3
|
+
*
|
|
4
|
+
* Used when ~/.nodpay/config.json has "remote_wallet" set.
|
|
5
|
+
* Transparent: stdin/stdout/stderr/exitCode passthrough.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* POST {base}/sign — pipe stdin as body, stdout the response.
|
|
10
|
+
* @param {string} base - remote_wallet URL (e.g. http://localhost:23295/nodpay)
|
|
11
|
+
*/
|
|
12
|
+
export async function remoteSign(base) {
|
|
13
|
+
// Read stdin
|
|
14
|
+
const chunks = [];
|
|
15
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
16
|
+
const body = Buffer.concat(chunks).toString('utf8').trim();
|
|
17
|
+
|
|
18
|
+
if (!body) {
|
|
19
|
+
console.error(JSON.stringify({ error: 'Empty stdin. Expected JSON: {"hash":"0x..."}' }));
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const res = await fetch(`${base.replace(/\/$/, '')}/sign`, {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: { 'Content-Type': 'application/json' },
|
|
27
|
+
body,
|
|
28
|
+
});
|
|
29
|
+
const text = await res.text();
|
|
30
|
+
if (!res.ok) {
|
|
31
|
+
console.error(text);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
console.log(text);
|
|
35
|
+
} catch (e) {
|
|
36
|
+
console.error(JSON.stringify({ error: `Remote sign failed: ${e.message}` }));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* GET {base}/wallets — stdout the response.
|
|
43
|
+
* @param {string} base - remote_wallet URL
|
|
44
|
+
*/
|
|
45
|
+
export async function remoteWallets(base) {
|
|
46
|
+
try {
|
|
47
|
+
const res = await fetch(`${base.replace(/\/$/, '')}/wallets`);
|
|
48
|
+
const text = await res.text();
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
console.error(text);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
console.log(text);
|
|
54
|
+
} catch (e) {
|
|
55
|
+
console.error(JSON.stringify({ error: `Remote wallets failed: ${e.message}` }));
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
}
|
package/scripts/sign.mjs
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Sign a hash with the agent's private key.
|
|
4
|
+
*
|
|
5
|
+
* Reads JSON from stdin: {"hash": "0x..."}
|
|
6
|
+
* Outputs JSON to stdout: {"signature": "0x..."}
|
|
7
|
+
*
|
|
8
|
+
* Key source: process.env.NODPAY_AGENT_KEY (real env > ~/.nodpay/.env file)
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* echo '{"hash":"0xabc..."}' | npx nodpay sign
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { ethers } from 'ethers';
|
|
15
|
+
import { loadDotEnv, env } from './env.mjs';
|
|
16
|
+
|
|
17
|
+
// Load .env file (real env takes priority)
|
|
18
|
+
loadDotEnv();
|
|
19
|
+
|
|
20
|
+
const NODPAY_AGENT_KEY = env('NODPAY_AGENT_KEY');
|
|
21
|
+
if (!NODPAY_AGENT_KEY) {
|
|
22
|
+
console.error(JSON.stringify({ error: 'Missing NODPAY_AGENT_KEY — set env var or add to ~/.nodpay/.env' }));
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Read stdin
|
|
27
|
+
const chunks = [];
|
|
28
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
29
|
+
const input = Buffer.concat(chunks).toString('utf8').trim();
|
|
30
|
+
|
|
31
|
+
if (!input) {
|
|
32
|
+
console.error(JSON.stringify({ error: 'Empty stdin. Expected JSON: {"hash":"0x..."}' }));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const { hash } = JSON.parse(input);
|
|
38
|
+
if (!hash || !hash.startsWith('0x')) {
|
|
39
|
+
console.error(JSON.stringify({ error: 'Invalid input. Expected {"hash":"0x..."}' }));
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const wallet = new ethers.Wallet(NODPAY_AGENT_KEY);
|
|
44
|
+
const sig = wallet.signingKey.sign(hash);
|
|
45
|
+
const signature = ethers.Signature.from(sig).serialized;
|
|
46
|
+
|
|
47
|
+
console.log(JSON.stringify({ signature }));
|
|
48
|
+
} catch (e) {
|
|
49
|
+
console.error(JSON.stringify({ error: e.message || String(e) }));
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* List all local NodPay wallets.
|
|
4
|
+
*
|
|
5
|
+
* Reads ~/.nodpay/wallets/*.json and outputs a JSON array to stdout.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* npx nodpay wallets
|
|
9
|
+
* npx nodpay wallets --json (same, explicit)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readdirSync, readFileSync, existsSync } from 'fs';
|
|
13
|
+
import { join } from 'path';
|
|
14
|
+
import { HOME } from './env.mjs';
|
|
15
|
+
|
|
16
|
+
const WALLETS_DIR = join(HOME, '.nodpay', 'wallets');
|
|
17
|
+
|
|
18
|
+
if (!existsSync(WALLETS_DIR)) {
|
|
19
|
+
console.log(JSON.stringify([]));
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const files = readdirSync(WALLETS_DIR).filter(f => f.endsWith('.json'));
|
|
25
|
+
const wallets = [];
|
|
26
|
+
for (const file of files) {
|
|
27
|
+
try {
|
|
28
|
+
const data = JSON.parse(readFileSync(join(WALLETS_DIR, file), 'utf8'));
|
|
29
|
+
wallets.push(data);
|
|
30
|
+
} catch {
|
|
31
|
+
// Skip invalid JSON files
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
console.log(JSON.stringify(wallets, null, 2));
|
|
35
|
+
} catch (e) {
|
|
36
|
+
console.error(JSON.stringify({ error: e.message || String(e) }));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|