nodpay 0.2.36 → 0.2.38
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/propose.mjs +48 -8
- 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/propose.mjs
CHANGED
|
@@ -32,6 +32,7 @@ import { join, dirname } from 'path';
|
|
|
32
32
|
import { fileURLToPath } from 'url';
|
|
33
33
|
import { computeUserOpHash, ENTRYPOINT } from '@nodpay/core';
|
|
34
34
|
import { loadDotEnv, env, HOME } from './env.mjs';
|
|
35
|
+
import { loadConfig } from './config.mjs';
|
|
35
36
|
|
|
36
37
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
37
38
|
const PENDING_DIR = join(__dirname, '..', '.pending-txs');
|
|
@@ -85,7 +86,9 @@ const DEFAULT_SAFE = null; // always use --safe flag
|
|
|
85
86
|
const opStoreBase = env('OP_STORE_URL', 'https://nodpay.ai/api');
|
|
86
87
|
const BUNDLER_URL = `${opStoreBase}/bundler/${CHAIN_ID}`;
|
|
87
88
|
|
|
88
|
-
// --- Agent key:
|
|
89
|
+
// --- Agent key resolution: remote_wallet (config.json) > --remote-signer > local key ---
|
|
90
|
+
const _config = loadConfig();
|
|
91
|
+
const _remoteWalletUrl = _config.remote_wallet || null;
|
|
89
92
|
const _remoteSignerUrl = process.argv.includes('--remote-signer')
|
|
90
93
|
? process.argv[process.argv.indexOf('--remote-signer') + 1]
|
|
91
94
|
: undefined;
|
|
@@ -93,9 +96,41 @@ const _remoteSignerUrl = process.argv.includes('--remote-signer')
|
|
|
93
96
|
let AGENT_ADDRESS;
|
|
94
97
|
let signHash;
|
|
95
98
|
let _localAgentKey; // only set in local mode, used for SDK init (EOA signer)
|
|
99
|
+
let _remoteWalletsCache = null; // cached wallets from remote_wallet fetch
|
|
96
100
|
|
|
97
|
-
if (
|
|
98
|
-
//
|
|
101
|
+
if (_remoteWalletUrl) {
|
|
102
|
+
// remote_wallet mode (config.json): get agent address from wallets, sign via proxy
|
|
103
|
+
const walletsRes = await fetch(`${_remoteWalletUrl.replace(/\/$/, '')}/wallets`);
|
|
104
|
+
if (!walletsRes.ok) {
|
|
105
|
+
const err = await walletsRes.json().catch(() => ({}));
|
|
106
|
+
console.error(JSON.stringify({
|
|
107
|
+
error: err.error || 'Failed to get wallets from remote_wallet',
|
|
108
|
+
code: err.code || 'REMOTE_WALLET_ERROR'
|
|
109
|
+
}));
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
const wallets = await walletsRes.json();
|
|
113
|
+
_remoteWalletsCache = wallets;
|
|
114
|
+
if (!wallets.length || !wallets[0].agentSigner) {
|
|
115
|
+
console.error(JSON.stringify({ error: 'No wallets found on remote_wallet' }));
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
AGENT_ADDRESS = wallets[0].agentSigner;
|
|
119
|
+
|
|
120
|
+
signHash = async (hash) => {
|
|
121
|
+
const signRes = await fetch(`${_remoteWalletUrl.replace(/\/$/, '')}/sign`, {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
headers: { 'Content-Type': 'application/json' },
|
|
124
|
+
body: JSON.stringify({ hash })
|
|
125
|
+
});
|
|
126
|
+
if (!signRes.ok) {
|
|
127
|
+
const err = await signRes.json().catch(() => ({}));
|
|
128
|
+
throw new Error(err.error || 'Remote signing failed');
|
|
129
|
+
}
|
|
130
|
+
return (await signRes.json()).signature;
|
|
131
|
+
};
|
|
132
|
+
} else if (_remoteSignerUrl) {
|
|
133
|
+
// Legacy --remote-signer mode (deprecated, use config.json remote_wallet)
|
|
99
134
|
const addrRes = await fetch(`${_remoteSignerUrl}/address`);
|
|
100
135
|
if (!addrRes.ok) {
|
|
101
136
|
const err = await addrRes.json().catch(() => ({}));
|
|
@@ -123,7 +158,7 @@ if (_remoteSignerUrl) {
|
|
|
123
158
|
// Local mode: read key from process.env (loaded from real env or ~/.nodpay/.env)
|
|
124
159
|
const NODPAY_AGENT_KEY = env('NODPAY_AGENT_KEY');
|
|
125
160
|
if (!NODPAY_AGENT_KEY) {
|
|
126
|
-
console.error(JSON.stringify({ error: 'Missing NODPAY_AGENT_KEY — set env var, add to ~/.nodpay/.env, or
|
|
161
|
+
console.error(JSON.stringify({ error: 'Missing NODPAY_AGENT_KEY — set env var, add to ~/.nodpay/.env, or set remote_wallet in ~/.nodpay/config.json' }));
|
|
127
162
|
process.exit(1);
|
|
128
163
|
}
|
|
129
164
|
_localAgentKey = NODPAY_AGENT_KEY;
|
|
@@ -160,9 +195,9 @@ const passkeyVerifier = getArg('--passkey-verifier') || '0x445a0683e494ea0c5AF3E
|
|
|
160
195
|
const recoverySigner = getArg('--recovery-signer');
|
|
161
196
|
const isPasskey = !!(passkeyX && passkeyY);
|
|
162
197
|
|
|
163
|
-
// Phase 1:
|
|
164
|
-
if (_remoteSignerUrl && humanSigner) {
|
|
165
|
-
console.error(JSON.stringify({ error: '
|
|
198
|
+
// Phase 1: remote modes + --human-signer-eoa is not supported
|
|
199
|
+
if ((_remoteSignerUrl || _remoteWalletUrl) && humanSigner) {
|
|
200
|
+
console.error(JSON.stringify({ error: 'Remote signer/wallet and --human-signer-eoa cannot be combined. Remote mode only supports passkey wallets.' }));
|
|
166
201
|
process.exit(1);
|
|
167
202
|
}
|
|
168
203
|
|
|
@@ -179,8 +214,13 @@ if (!ethers.isAddress(to)) {
|
|
|
179
214
|
const SAFE_ADDRESS = safeOverride || DEFAULT_SAFE;
|
|
180
215
|
|
|
181
216
|
// Read optional wallet JSON for rpId (SafeClaw cross-origin passkey support)
|
|
217
|
+
// In remote_wallet mode, wallets were already fetched — use that data.
|
|
182
218
|
let walletRpId = null;
|
|
183
|
-
if (SAFE_ADDRESS) {
|
|
219
|
+
if (SAFE_ADDRESS && _remoteWalletUrl && _remoteWalletsCache) {
|
|
220
|
+
const match = _remoteWalletsCache.find(w => w.safe?.toLowerCase() === SAFE_ADDRESS.toLowerCase());
|
|
221
|
+
if (match?.rpId) walletRpId = match.rpId;
|
|
222
|
+
}
|
|
223
|
+
if (!walletRpId && SAFE_ADDRESS) {
|
|
184
224
|
const walletJsonPath = join(HOME, '.nodpay', 'wallets', `${SAFE_ADDRESS}.json`);
|
|
185
225
|
if (existsSync(walletJsonPath)) {
|
|
186
226
|
try {
|
|
@@ -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
|
+
}
|