nodpay 0.2.30 → 0.2.32
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 +14 -5
- package/SKILL.md +11 -4
- package/bin/nodpay.mjs +6 -0
- package/package.json +1 -1
- package/scripts/nonce.mjs +89 -0
- package/scripts/propose.mjs +8 -32
package/README.md
CHANGED
|
@@ -16,12 +16,14 @@ This npm package (`nodpay`) is the **agent-facing CLI**. It is also published as
|
|
|
16
16
|
| **ClawHub** (`clawhub install nodpay`) | `SKILL.md` only | OpenClaw agents |
|
|
17
17
|
| **nodpay.ai/skill.md** | `SKILL.md` via CDN proxy | All agent frameworks |
|
|
18
18
|
|
|
19
|
-
The CLI provides
|
|
19
|
+
The CLI provides five commands:
|
|
20
20
|
|
|
21
21
|
```
|
|
22
22
|
nodpay keygen # Generate agent keypair (~/.nodpay/.env, chmod 600)
|
|
23
|
-
nodpay
|
|
23
|
+
nodpay nonce # Query next nonce (on-chain EntryPoint + pending proposals)
|
|
24
|
+
nodpay propose # Propose a transaction for human approval (--nonce required)
|
|
24
25
|
nodpay txs # List and verify transactions for a wallet
|
|
26
|
+
nodpay gasprice # Get current gas price + estimated cost per chain
|
|
25
27
|
```
|
|
26
28
|
|
|
27
29
|
## Quick Start
|
|
@@ -30,25 +32,32 @@ nodpay txs # List and verify transactions for a wallet
|
|
|
30
32
|
# 1. Generate key (public address only in stdout; key never exposed)
|
|
31
33
|
npx nodpay keygen
|
|
32
34
|
|
|
33
|
-
# 2.
|
|
35
|
+
# 2. Get next nonce (on-chain + pending)
|
|
36
|
+
npx nodpay nonce --safe 0xWALLET --chain base
|
|
37
|
+
|
|
38
|
+
# 3. Propose a payment
|
|
34
39
|
npx nodpay propose \
|
|
35
40
|
--chain base \
|
|
36
41
|
--safe 0xWALLET \
|
|
37
42
|
--to 0xRECIPIENT \
|
|
38
43
|
--value-eth 0.01 \
|
|
44
|
+
--nonce 0 \
|
|
39
45
|
--human-signer-passkey-x 0x... \
|
|
40
46
|
--human-signer-passkey-y 0x... \
|
|
41
47
|
--recovery-signer 0x...
|
|
42
48
|
|
|
43
|
-
#
|
|
49
|
+
# 4. Check transactions (with verification)
|
|
44
50
|
npx nodpay txs --safe 0xWALLET
|
|
51
|
+
|
|
52
|
+
# 5. Estimate gas cost for a sweep
|
|
53
|
+
npx nodpay gasprice --chain base
|
|
45
54
|
```
|
|
46
55
|
|
|
47
56
|
## Security
|
|
48
57
|
|
|
49
58
|
All config lives in `~/.nodpay/` — zero `process.env` references in code.
|
|
50
59
|
|
|
51
|
-
- **Hardened Key Isolation:** private key written
|
|
60
|
+
- **Hardened Key Isolation:** private key written to `~/.nodpay/.env` (chmod 600), read via file I/O at runtime. Not passed through CLI args, env vars, or stdout.
|
|
52
61
|
- **Zero Trust:** `txs` independently verifies every server response (decode calldata → recompute hash → recover signer → check owner set).
|
|
53
62
|
- **Threshold Security:** 2-of-3 multisig — agent cannot move funds unilaterally.
|
|
54
63
|
|
package/SKILL.md
CHANGED
|
@@ -9,7 +9,7 @@ metadata:
|
|
|
9
9
|
"homepage": "https://nodpay.ai",
|
|
10
10
|
"install": [{ "id": "node", "kind": "node", "package": "nodpay", "label": "Install NodPay CLI (npm)", "author": "xhyumiracle", "source": "https://github.com/xhyumiracle/nodpay-cli" }]
|
|
11
11
|
},
|
|
12
|
-
"credentials": "Agent signing key stored in ~/.nodpay/.env (generated by npx nodpay keygen,
|
|
12
|
+
"credentials": "Agent signing key stored in ~/.nodpay/.env (chmod 600, generated by npx nodpay keygen). Read at runtime by CLI process; not passed via CLI args, env vars, or stdout.",
|
|
13
13
|
"persistence": ["~/.nodpay/.env (agent key, chmod 600)", "~/.nodpay/wallets/*.json (wallet info, public key material)"],
|
|
14
14
|
"network": ["nodpay.ai (op-store relay + wallet creation UI)", "Public RPC endpoints via --chain"]
|
|
15
15
|
}
|
|
@@ -30,7 +30,7 @@ You propose payments, your human approves with one tap. 2-of-3 multisig — you
|
|
|
30
30
|
| **Threshold Security** | **Elimination of single point of failure:** authority keys are distributed between the agent, human, and a recovery signer (2-of-3 multisig). Ensures non-custodial control — the agent cannot move funds unilaterally. |
|
|
31
31
|
| **Zero Trust** | **End-to-end verification:** no party is implicitly trusted. Server validates signatures; client and CLI independently verify server responses (decode calldata → recompute hash → recover signer → check owner set). The blockchain serves as the canonical source of truth. |
|
|
32
32
|
| **Disaster Recovery** | **Key redundancy & continuity:** uses a locally-stored 12-word mnemonic as recovery signer. Any two of the three signers can reconstruct authority to unlock the wallet, ensuring the user is never locked out by a single lost credential. |
|
|
33
|
-
| **Hardened Key Isolation** | `keygen` writes to `~/.nodpay/.env` (chmod 600). The CLI reads the key
|
|
33
|
+
| **Hardened Key Isolation** | `keygen` writes to `~/.nodpay/.env` (chmod 600). The CLI reads the key via file I/O at runtime — not passed through CLI arguments, environment variables, or stdout. Only the public address is returned to the caller. |
|
|
34
34
|
| **Keyless & Non-Custodial Server** | **Stateless relayer:** the server stores no private keys and maintains no session state that could compromise assets. All signing happens locally. Funds stay on-chain if the server goes offline. |
|
|
35
35
|
|
|
36
36
|
All wallet parameters (Safe address, passkey X/Y, recovery signer address) are public key material — safe to store, pass in URLs, and include in CLI flags.
|
|
@@ -93,6 +93,7 @@ npx nodpay propose \
|
|
|
93
93
|
--safe <SAFE> \
|
|
94
94
|
--to <RECIPIENT> \
|
|
95
95
|
--value-eth <AMOUNT> \
|
|
96
|
+
--nonce <N> \
|
|
96
97
|
--human-signer-passkey-x <X> \
|
|
97
98
|
--human-signer-passkey-y <Y> \
|
|
98
99
|
--recovery-signer <RECOVERY>
|
|
@@ -111,7 +112,13 @@ First tx deploys the wallet. Pass all params for first tx; after that `--safe` a
|
|
|
111
112
|
npx nodpay txs --safe <SAFE>
|
|
112
113
|
```
|
|
113
114
|
|
|
114
|
-
**Always
|
|
115
|
+
**Always check nonce before proposing.** Do not assume a previous transaction is still pending — the human may have approved or rejected it without telling you.
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
npx nodpay nonce --safe <SAFE> --chain <CHAIN>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Returns `nextNonce` (from on-chain EntryPoint + pending proposals), `onChainNonce`, and `pendingCount`. Pass `nextNonce` as `--nonce` to propose.
|
|
115
122
|
|
|
116
123
|
```bash
|
|
117
124
|
npx nodpay gasprice --chain <CHAIN>
|
|
@@ -156,7 +163,7 @@ EOA wallets: replace passkey fields with `"humanSignerEoa": "0x..."`.
|
|
|
156
163
|
| `--human-signer-passkey-x/y` | passkey | Human signer passkey public key |
|
|
157
164
|
| `--human-signer-eoa` | eoa | Human signer EOA address |
|
|
158
165
|
| `--recovery-signer` | first tx | Recovery signer address |
|
|
159
|
-
| `--nonce` |
|
|
166
|
+
| `--nonce` | **required** | Nonce for this proposal. Run `txs` first to determine. |
|
|
160
167
|
|
|
161
168
|
Wallet address is the same across all chains. **Ask which chain if not specified.**
|
|
162
169
|
|
package/bin/nodpay.mjs
CHANGED
|
@@ -15,6 +15,10 @@ if (command === 'propose') {
|
|
|
15
15
|
const scriptPath = new URL('../scripts/txs.mjs', import.meta.url).pathname;
|
|
16
16
|
process.argv = [process.argv[0], scriptPath, ...process.argv.slice(3)];
|
|
17
17
|
await import(scriptPath);
|
|
18
|
+
} else if (command === 'nonce') {
|
|
19
|
+
const scriptPath = new URL('../scripts/nonce.mjs', import.meta.url).pathname;
|
|
20
|
+
process.argv = [process.argv[0], scriptPath, ...process.argv.slice(3)];
|
|
21
|
+
await import(scriptPath);
|
|
18
22
|
} else if (command === 'gasprice') {
|
|
19
23
|
const scriptPath = new URL('../scripts/gasprice.mjs', import.meta.url).pathname;
|
|
20
24
|
process.argv = [process.argv[0], scriptPath, ...process.argv.slice(3)];
|
|
@@ -30,12 +34,14 @@ Commands:
|
|
|
30
34
|
keygen Generate (or reuse) agent keypair
|
|
31
35
|
propose Propose a transaction for human approval
|
|
32
36
|
txs List pending and completed transactions
|
|
37
|
+
nonce Query next nonce from on-chain EntryPoint
|
|
33
38
|
gasprice Get current gas price and estimated gas cost per chain
|
|
34
39
|
|
|
35
40
|
Examples:
|
|
36
41
|
nodpay keygen
|
|
37
42
|
nodpay propose --safe 0x... --to 0x... --value-eth 0.01 --chain base
|
|
38
43
|
nodpay txs --safe 0x...
|
|
44
|
+
nodpay nonce --safe 0x... --chain base
|
|
39
45
|
nodpay gasprice --chain base
|
|
40
46
|
|
|
41
47
|
Docs: https://nodpay.ai/skill.md`);
|
package/package.json
CHANGED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Query the next available nonce for a Safe wallet from on-chain EntryPoint.
|
|
4
|
+
*
|
|
5
|
+
* This is the ERC-4337 standard: EntryPoint.getNonce(sender, key).
|
|
6
|
+
* Pure on-chain query — no server dependency.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* npx nodpay nonce --safe <SAFE_ADDRESS> --chain <CHAIN>
|
|
10
|
+
*
|
|
11
|
+
* Output:
|
|
12
|
+
* { "nonce": 0, "safe": "0x...", "chain": "base", "chainId": "8453" }
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { ethers } from 'ethers';
|
|
16
|
+
import { ENTRYPOINT } from '@nodpay/core';
|
|
17
|
+
import { createRequire } from 'module';
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
const NETWORKS = require('@nodpay/core/networks');
|
|
20
|
+
|
|
21
|
+
const args = process.argv.slice(2);
|
|
22
|
+
function getArg(name) {
|
|
23
|
+
const idx = args.indexOf(name);
|
|
24
|
+
return idx !== -1 ? args[idx + 1] : undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const safe = getArg('--safe');
|
|
28
|
+
const chainArg = getArg('--chain');
|
|
29
|
+
|
|
30
|
+
if (!safe) {
|
|
31
|
+
console.error(JSON.stringify({ error: '--safe <address> is required' }));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
if (!chainArg) {
|
|
35
|
+
const allChains = { ...NETWORKS.mainnet, ...NETWORKS.testnet };
|
|
36
|
+
console.error(JSON.stringify({ error: '--chain is required. Supported: ' + Object.keys(allChains).join(', ') }));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const allChains = { ...NETWORKS.mainnet, ...NETWORKS.testnet };
|
|
41
|
+
const net = allChains[chainArg];
|
|
42
|
+
if (!net) {
|
|
43
|
+
console.error(JSON.stringify({ error: `Unknown chain "${chainArg}". Supported: ${Object.keys(allChains).join(', ')}` }));
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// 1. On-chain nonce from EntryPoint (source of truth for executed txs)
|
|
49
|
+
const provider = new ethers.JsonRpcProvider(net.rpcUrl);
|
|
50
|
+
const ep = new ethers.Contract(
|
|
51
|
+
ENTRYPOINT,
|
|
52
|
+
['function getNonce(address,uint192) view returns (uint256)'],
|
|
53
|
+
provider
|
|
54
|
+
);
|
|
55
|
+
const onChainNonce = await ep.getNonce(safe, 0);
|
|
56
|
+
|
|
57
|
+
// 2. Check pending ops to find the highest queued nonce
|
|
58
|
+
// (same logic as the battle-tested propose.mjs nonce resolution)
|
|
59
|
+
let nextNonce = onChainNonce;
|
|
60
|
+
let pendingCount = 0;
|
|
61
|
+
try {
|
|
62
|
+
const baseUrl = 'https://nodpay.ai/api';
|
|
63
|
+
const listRes = await fetch(`${baseUrl}/txs?safe=${safe}&chain=${net.chainId}`);
|
|
64
|
+
if (listRes.ok) {
|
|
65
|
+
const listData = await listRes.json();
|
|
66
|
+
for (const op of (listData.txs || listData.ops || [])) {
|
|
67
|
+
const opNonce = BigInt(op.nonce ?? -1);
|
|
68
|
+
if (opNonce >= onChainNonce && opNonce >= nextNonce) {
|
|
69
|
+
pendingCount++;
|
|
70
|
+
nextNonce = opNonce + 1n;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch (e) {
|
|
75
|
+
// Non-fatal: op-store may be unavailable, fall back to on-chain only
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log(JSON.stringify({
|
|
79
|
+
nextNonce: Number(nextNonce),
|
|
80
|
+
onChainNonce: Number(onChainNonce),
|
|
81
|
+
pendingCount,
|
|
82
|
+
safe,
|
|
83
|
+
chain: chainArg,
|
|
84
|
+
chainId: String(net.chainId),
|
|
85
|
+
}, null, 2));
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.error(JSON.stringify({ error: err.message }));
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
package/scripts/propose.mjs
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* --human-signer-eoa <address> - Human's EOA signer address (for EOA mode)
|
|
20
20
|
* --salt <nonce> - Salt nonce (required for counterfactual)
|
|
21
21
|
* --reuse-gas-from <shortHash> - Reuse gas values from a previous op (shortHash prefix of safeOpHash)
|
|
22
|
-
* --nonce <n> -
|
|
22
|
+
* --nonce <n> - Required. Use `txs` to find current nonce.
|
|
23
23
|
*
|
|
24
24
|
* Output: JSON with userOpHash, safeTxHash, safeOperationJson, etc.
|
|
25
25
|
*/
|
|
@@ -336,38 +336,14 @@ try {
|
|
|
336
336
|
const opStoreUrl = opStoreBase;
|
|
337
337
|
const safeAddr = await safe4337Pack.protocolKit.getAddress();
|
|
338
338
|
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const provider = new ethers.JsonRpcProvider(RPC_URL);
|
|
346
|
-
const ep = new ethers.Contract('0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789',
|
|
347
|
-
['function getNonce(address,uint192) view returns (uint256)'], provider);
|
|
348
|
-
const onChainNonce = await ep.getNonce(safeAddr, 0);
|
|
349
|
-
|
|
350
|
-
// Check pending ops to find the highest queued nonce
|
|
351
|
-
const listRes = await fetch(`${opStoreUrl}/txs?safe=${safeAddr}&chain=${CHAIN_ID}`);
|
|
352
|
-
let nextNonce = onChainNonce;
|
|
353
|
-
if (listRes.ok) {
|
|
354
|
-
const listData = await listRes.json();
|
|
355
|
-
for (const op of (listData.txs || listData.ops || [])) {
|
|
356
|
-
const opNonce = BigInt(op.nonce ?? -1);
|
|
357
|
-
if (opNonce >= onChainNonce && opNonce >= nextNonce) {
|
|
358
|
-
nextNonce = opNonce + 1n;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
// Only set custom nonce if we need to skip ahead for queuing
|
|
363
|
-
if (nextNonce > onChainNonce) {
|
|
364
|
-
txOptions.customNonce = nextNonce;
|
|
365
|
-
}
|
|
366
|
-
// else: let SDK use on-chain nonce naturally
|
|
367
|
-
} catch (e) {
|
|
368
|
-
// Non-fatal: SDK will use its own nonce detection
|
|
369
|
-
}
|
|
339
|
+
// --nonce is required. Agent must run `txs` first to determine the correct nonce.
|
|
340
|
+
// This prevents accidental double-proposals: if agent proposes twice with the same nonce,
|
|
341
|
+
// the second is just a replacement (same slot), not a new transaction.
|
|
342
|
+
if (customNonceArg === undefined) {
|
|
343
|
+
console.error(JSON.stringify({ error: '--nonce is required. Run `npx nodpay txs --safe <SAFE>` first to check current nonce.' }));
|
|
344
|
+
process.exit(1);
|
|
370
345
|
}
|
|
346
|
+
txOptions.customNonce = BigInt(customNonceArg);
|
|
371
347
|
|
|
372
348
|
// Resolve gas values: use hardcoded defaults (from RPC gas price) always.
|
|
373
349
|
// If --reuse-gas-from is provided and fetch succeeds, use those values instead
|