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 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 three commands:
19
+ The CLI provides five commands:
20
20
 
21
21
  ```
22
22
  nodpay keygen # Generate agent keypair (~/.nodpay/.env, chmod 600)
23
- nodpay propose # Propose a transaction for human approval
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. Propose a payment
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
- # 3. Check pending transactions (with verification)
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 directly to `~/.nodpay/.env` (chmod 600), strictly excluded from stdout and agent context.
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, never exposed to agent context)",
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 internally at runtime — only the public address appears in stdout. |
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 run `txs` before proposing.** Do not assume a previous transaction is still pending — the human may have approved or rejected it without telling you. Check actual on-chain state first.
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` | optional | Force nonce (replacements) |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodpay",
3
- "version": "0.2.30",
3
+ "version": "0.2.32",
4
4
  "description": "NodPay CLI — propose on-chain payments from agent-human shared wallets",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
+ }
@@ -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> - Override nonce
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
- // Determine nonce: on-chain nonce is the source of truth.
340
- // For queued ops, find the highest pending nonce and increment.
341
- if (customNonceArg !== undefined) {
342
- txOptions.customNonce = BigInt(customNonceArg);
343
- } else {
344
- try {
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