nodpay 0.2.29 → 0.2.31

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/SKILL.md CHANGED
@@ -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.29",
3
+ "version": "0.2.31",
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
package/scripts/txs.mjs CHANGED
@@ -124,7 +124,8 @@ try {
124
124
  }
125
125
  }
126
126
 
127
- console.log(JSON.stringify(data, null, 2));
127
+ // BigInt values from @nodpay/core (e.g. decoded value) must be serialized as strings
128
+ console.log(JSON.stringify(data, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2));
128
129
  } catch (err) {
129
130
  console.error(JSON.stringify({ error: err.message }));
130
131
  process.exit(1);