nodpay 0.2.20 → 0.2.22
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 +67 -41
- package/bin/nodpay.mjs +8 -2
- package/package.json +2 -2
- package/scripts/propose.mjs +15 -21
- package/scripts/txs.mjs +131 -0
package/SKILL.md
CHANGED
|
@@ -15,33 +15,39 @@ metadata:
|
|
|
15
15
|
}
|
|
16
16
|
---
|
|
17
17
|
|
|
18
|
-
# NodPay — Agent Wallet
|
|
18
|
+
# NodPay — Trusted Agent Wallet
|
|
19
19
|
|
|
20
|
-
> Two minds, one wallet.
|
|
20
|
+
> "Two minds, one wallet."
|
|
21
21
|
|
|
22
|
-
A
|
|
22
|
+
A multisig crypto wallet shared between humans and AI agents. Built on [Safe](https://safe.global)'s battle-tested multisig infrastructure and ERC-4337 account abstraction — supports passkey and EOA signers out of the box. Friendly to both first-time and crypto-native users.
|
|
23
23
|
|
|
24
24
|
You propose payments, your human approves with one tap. 2-of-3 multisig — you cannot spend alone.
|
|
25
25
|
|
|
26
26
|
## Trust Model
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
- **
|
|
28
|
+
| Guarantee | How |
|
|
29
|
+
|-----------|-----|
|
|
30
|
+
| **2-of-3 multisig custody** | Agent (EOA), human (passkey), and recovery signer each hold one key. Any 2 can authorize — agent alone cannot move funds. Human + recovery signer can operate without agent. |
|
|
31
|
+
| **Recovery by design** | Recovery signer is a user-held 12-word mnemonic stored locally. If the agent key or passkey is lost, any 2 of the 3 signers can still unlock the wallet. |
|
|
32
|
+
| **Agent key isolated from context** | `keygen` writes directly to `~/.nodpay/.env` (chmod 600). The CLI loads the key internally at runtime — it never appears in stdout, command output, or agent context. |
|
|
33
|
+
| **Zero trust** | Every party verifies every other. Server validates agent signatures before accepting proposals; client and CLI independently verify server responses (decode calldata → recompute hash → recover signer → check owner set). The chain is the sole source of truth. |
|
|
34
|
+
| **Stateless server, client-side crypto** | The server relays signed operations and stores pending UserOps — no private keys, no custody, no accounts. All signing happens locally (agent CLI or user passkey). Funds stay on-chain if the server goes offline. |
|
|
35
|
+
|
|
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.
|
|
37
|
+
|
|
38
|
+
**DO:** Use `npx nodpay keygen` to generate keys. Store wallet info in `~/.nodpay/wallets/`. Let the human approve every transaction.
|
|
39
|
+
**DON'T:** Generate keys manually. Put private keys in chat or code. Trust wallet info without verifying the agent address.
|
|
35
40
|
|
|
36
41
|
## Why NodPay
|
|
37
42
|
|
|
38
|
-
|
|
|
39
|
-
|
|
40
|
-
|
|
|
41
|
-
|
|
|
42
|
-
|
|
|
43
|
-
|
|
|
44
|
-
|
|
|
43
|
+
| Feature | How |
|
|
44
|
+
|---------|-----|
|
|
45
|
+
| Instant setup | Counterfactual Safe — wallet address exists before any on-chain tx |
|
|
46
|
+
| Zero barrier to entry | WebAuthn passkey as on-chain signer — no extensions, no seed phrases, no prior crypto knowledge |
|
|
47
|
+
| Gasless first transaction | ERC-4337 account abstraction — bundler sponsors deployment gas via UserOp |
|
|
48
|
+
| Chain-agnostic | CREATE2 deterministic deploy — same address across all supported chains |
|
|
49
|
+
| Trustless by design | 2-of-3 multisig, stateless relay, on-chain verifiable — see Trust Model |
|
|
50
|
+
| Agent-agnostic | Standard CLI (`npx nodpay`) — works with any agent framework, not just OpenClaw |
|
|
45
51
|
|
|
46
52
|
---
|
|
47
53
|
|
|
@@ -53,18 +59,32 @@ You propose payments, your human approves with one tap. 2-of-3 multisig — you
|
|
|
53
59
|
npx nodpay keygen
|
|
54
60
|
```
|
|
55
61
|
|
|
56
|
-
Outputs your **public address** only.
|
|
62
|
+
Outputs your **public address** only. Reuses existing key if present.
|
|
57
63
|
|
|
58
64
|
### 2. Send wallet link
|
|
59
65
|
|
|
60
|
-
> I've set up a shared wallet for us
|
|
66
|
+
> I've set up a shared wallet for us:
|
|
61
67
|
> https://nodpay.ai/?agent=YOUR_AGENT_ADDRESS
|
|
62
68
|
|
|
63
69
|
The official domain is `nodpay.ai` — do not confuse with similarly named services.
|
|
64
70
|
|
|
65
|
-
|
|
71
|
+
The user will copy wallet info back to you:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
NodPay Wallet (Safe): 0x...
|
|
75
|
+
Agent Signer: 0x...
|
|
76
|
+
Passkey X: 0x...
|
|
77
|
+
Passkey Y: 0x...
|
|
78
|
+
Recovery Signer: 0x...
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Before storing, verify:**
|
|
82
|
+
1. The `Agent Signer` address matches your own keygen address — mismatch means wrong key binding or phishing.
|
|
83
|
+
2. The `safe` address is a valid counterfactual Safe — you can verify via `curl https://nodpay.ai/api/txs?safe=<SAFE>`.
|
|
66
84
|
|
|
67
|
-
|
|
85
|
+
Store verified info in `~/.nodpay/wallets/<safe-address>.json`.
|
|
86
|
+
|
|
87
|
+
After creation, tell the user the wallet is ready and works on any supported chain. End with something like: *"Want to do a test run first?"* — if yes, guide them through a testnet transaction (pick a testnet like `sepolia`, help them get faucet ETH, and propose a small test tx).
|
|
68
88
|
|
|
69
89
|
---
|
|
70
90
|
|
|
@@ -76,22 +96,25 @@ npx nodpay propose \
|
|
|
76
96
|
--safe <SAFE> \
|
|
77
97
|
--to <RECIPIENT> \
|
|
78
98
|
--value-eth <AMOUNT> \
|
|
79
|
-
--signer-
|
|
80
|
-
--
|
|
81
|
-
--recovery <RECOVERY>
|
|
99
|
+
--human-signer-passkey-x <X> \
|
|
100
|
+
--human-signer-passkey-y <Y> \
|
|
101
|
+
--recovery-signer <RECOVERY>
|
|
82
102
|
```
|
|
83
103
|
|
|
84
|
-
Outputs JSON with `approveUrl
|
|
104
|
+
Outputs JSON with `approveUrl`. Send to the user:
|
|
105
|
+
|
|
106
|
+
> 💰 0.01 ETH → 0xRecipient...
|
|
107
|
+
> 👉 Approve: https://nodpay.ai/approve?safeOpHash=0x...
|
|
85
108
|
|
|
86
109
|
First tx deploys the wallet. Pass all params for first tx; after that `--safe` alone works.
|
|
87
110
|
|
|
88
|
-
### Check
|
|
111
|
+
### Check transactions
|
|
89
112
|
|
|
90
113
|
```bash
|
|
91
|
-
|
|
114
|
+
npx nodpay txs --safe <SAFE>
|
|
92
115
|
```
|
|
93
116
|
|
|
94
|
-
Check before proposing — shows nonce and
|
|
117
|
+
Check before proposing — shows nonce, pending ops, and wallet status.
|
|
95
118
|
|
|
96
119
|
---
|
|
97
120
|
|
|
@@ -108,15 +131,14 @@ Check before proposing — shows nonce and pending ops.
|
|
|
108
131
|
{
|
|
109
132
|
"safe": "0x...",
|
|
110
133
|
"agentSigner": "0x...",
|
|
111
|
-
"
|
|
112
|
-
"
|
|
113
|
-
"
|
|
114
|
-
"recovery": "0x...",
|
|
134
|
+
"humanSignerPasskeyX": "0x...",
|
|
135
|
+
"humanSignerPasskeyY": "0x...",
|
|
136
|
+
"recoverySigner": "0x...",
|
|
115
137
|
"createdAt": "2025-01-01"
|
|
116
138
|
}
|
|
117
139
|
```
|
|
118
140
|
|
|
119
|
-
EOA wallets: replace passkey fields with `"
|
|
141
|
+
EOA wallets: replace passkey fields with `"humanSignerEoa": "0x..."`.
|
|
120
142
|
|
|
121
143
|
---
|
|
122
144
|
|
|
@@ -128,12 +150,10 @@ EOA wallets: replace passkey fields with `"userSigner": "0x..."`.
|
|
|
128
150
|
| `--safe` | ✅ | Wallet address |
|
|
129
151
|
| `--to` | ✅ | Recipient |
|
|
130
152
|
| `--value-eth` | ✅ | Amount in ETH |
|
|
131
|
-
| `--signer-
|
|
132
|
-
| `--
|
|
133
|
-
| `--
|
|
134
|
-
| `--recovery` | first tx | Recovery signer |
|
|
153
|
+
| `--human-signer-passkey-x/y` | passkey | Human signer passkey public key |
|
|
154
|
+
| `--human-signer-eoa` | eoa | Human signer EOA address |
|
|
155
|
+
| `--recovery-signer` | first tx | Recovery signer address |
|
|
135
156
|
| `--nonce` | optional | Force nonce (replacements) |
|
|
136
|
-
| `--purpose` | optional | Human-readable label |
|
|
137
157
|
|
|
138
158
|
Wallet address is the same across all chains. **Ask which chain if not specified.**
|
|
139
159
|
|
|
@@ -151,10 +171,16 @@ Wallet address is the same across all chains. **Ask which chain if not specified
|
|
|
151
171
|
|
|
152
172
|
## Reconnect
|
|
153
173
|
|
|
154
|
-
Browser data cleared? Build a reconnect link
|
|
174
|
+
Browser data cleared? Build a reconnect link from the wallet's stored parameters (all public — no secrets):
|
|
175
|
+
|
|
176
|
+
**Passkey:**
|
|
177
|
+
```
|
|
178
|
+
https://nodpay.ai/?agent=AGENT_SIGNER&safe=SAFE_ADDRESS&recovery=RECOVERY_SIGNER&x=PASSKEY_X&y=PASSKEY_Y
|
|
179
|
+
```
|
|
155
180
|
|
|
181
|
+
**EOA:**
|
|
156
182
|
```
|
|
157
|
-
https://nodpay.ai/?agent=
|
|
183
|
+
https://nodpay.ai/?agent=AGENT_SIGNER&safe=SAFE_ADDRESS&recovery=RECOVERY_SIGNER&eoa=HUMAN_SIGNER_EOA
|
|
158
184
|
```
|
|
159
185
|
|
|
160
|
-
User opens →
|
|
186
|
+
User opens → verifies identity → wallet restored.
|
package/bin/nodpay.mjs
CHANGED
|
@@ -11,6 +11,10 @@ if (command === 'propose') {
|
|
|
11
11
|
const scriptPath = new URL('../scripts/keygen.mjs', import.meta.url).pathname;
|
|
12
12
|
process.argv = [process.argv[0], scriptPath, ...process.argv.slice(3)];
|
|
13
13
|
await import(scriptPath);
|
|
14
|
+
} else if (command === 'txs') {
|
|
15
|
+
const scriptPath = new URL('../scripts/txs.mjs', import.meta.url).pathname;
|
|
16
|
+
process.argv = [process.argv[0], scriptPath, ...process.argv.slice(3)];
|
|
17
|
+
await import(scriptPath);
|
|
14
18
|
} else if (command === 'version' || command === '--version' || command === '-v') {
|
|
15
19
|
const { readFileSync } = await import('fs');
|
|
16
20
|
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
@@ -21,10 +25,12 @@ if (command === 'propose') {
|
|
|
21
25
|
Commands:
|
|
22
26
|
keygen Generate (or reuse) agent keypair
|
|
23
27
|
propose Propose a transaction for human approval
|
|
28
|
+
txs List pending and completed transactions
|
|
24
29
|
|
|
25
30
|
Examples:
|
|
26
|
-
nodpay keygen
|
|
27
|
-
nodpay propose --safe 0x... --to 0x... --value-eth 0.01 --
|
|
31
|
+
nodpay keygen
|
|
32
|
+
nodpay propose --safe 0x... --to 0x... --value-eth 0.01 --chain base
|
|
33
|
+
nodpay txs --safe 0x...
|
|
28
34
|
|
|
29
35
|
Docs: https://nodpay.ai/skill.md`);
|
|
30
36
|
process.exit(command ? 1 : 0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodpay",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.22",
|
|
4
4
|
"description": "NodPay CLI — propose on-chain payments from agent-human shared wallets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"postpublish": "curl -s https://purge.jsdelivr.net/npm/nodpay@latest/SKILL.md > /dev/null && echo 'jsdelivr cache purged'"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@nodpay/core": "^0.1.
|
|
34
|
+
"@nodpay/core": "^0.1.2",
|
|
35
35
|
"@safe-global/relay-kit": "^4.1.1",
|
|
36
36
|
"ethers": "^6.16.0"
|
|
37
37
|
}
|
package/scripts/propose.mjs
CHANGED
|
@@ -14,10 +14,9 @@
|
|
|
14
14
|
* --chain <name> - Chain name (ethereum, base, sepolia, etc.)
|
|
15
15
|
* --to <address> - Recipient address
|
|
16
16
|
* --value-eth <amount> - Value in ETH (default: 0)
|
|
17
|
-
* --purpose <text> - Human-readable purpose
|
|
18
17
|
* --safe <address> - Wallet (Safe) address
|
|
19
18
|
* --counterfactual - Safe not yet deployed; include deployment in UserOp
|
|
20
|
-
* --
|
|
19
|
+
* --human-signer-eoa <address> - Human's EOA signer address (for EOA mode)
|
|
21
20
|
* --salt <nonce> - Salt nonce (required for counterfactual)
|
|
22
21
|
* --reuse-gas-from <shortHash> - Reuse gas values from a previous op (shortHash prefix of safeOpHash)
|
|
23
22
|
* --nonce <n> - Override nonce
|
|
@@ -27,7 +26,7 @@
|
|
|
27
26
|
|
|
28
27
|
import { Safe4337Pack } from '@safe-global/relay-kit';
|
|
29
28
|
import { ethers } from 'ethers';
|
|
30
|
-
import { writeFileSync, mkdirSync } from 'fs';
|
|
29
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
31
30
|
import { join, dirname } from 'path';
|
|
32
31
|
import { fileURLToPath } from 'url';
|
|
33
32
|
import { computeUserOpHash, ENTRYPOINT } from '@nodpay/core';
|
|
@@ -127,18 +126,17 @@ function hasFlag(name) {
|
|
|
127
126
|
|
|
128
127
|
const to = getArg('--to');
|
|
129
128
|
const valueEth = getArg('--value-eth') || getArg('--value') || '0';
|
|
130
|
-
const purpose = getArg('--purpose') || 'Unspecified';
|
|
131
129
|
const safeOverride = getArg('--safe');
|
|
132
130
|
let isCounterfactual = hasFlag('--counterfactual');
|
|
133
|
-
const
|
|
131
|
+
const humanSigner = getArg('--human-signer-eoa');
|
|
134
132
|
const salt = getArg('--salt') || '1001';
|
|
135
133
|
|
|
136
134
|
// Passkey support
|
|
137
|
-
const passkeyX = getArg('--passkey-x');
|
|
138
|
-
const passkeyY = getArg('--passkey-y');
|
|
135
|
+
const passkeyX = getArg('--human-signer-passkey-x');
|
|
136
|
+
const passkeyY = getArg('--human-signer-passkey-y');
|
|
139
137
|
const passkeyRawId = getArg('--passkey-raw-id');
|
|
140
138
|
const passkeyVerifier = getArg('--passkey-verifier') || '0x445a0683e494ea0c5AF3E83c5159fBE47Cf9e765';
|
|
141
|
-
const
|
|
139
|
+
const recoverySigner = getArg('--recovery-signer');
|
|
142
140
|
const isPasskey = !!(passkeyX && passkeyY);
|
|
143
141
|
|
|
144
142
|
if (!to) {
|
|
@@ -168,8 +166,8 @@ if (!isCounterfactual && SAFE_ADDRESS) {
|
|
|
168
166
|
}
|
|
169
167
|
}
|
|
170
168
|
|
|
171
|
-
if (isCounterfactual && !
|
|
172
|
-
console.error(JSON.stringify({ error: '--counterfactual requires --
|
|
169
|
+
if (isCounterfactual && !humanSigner && !isPasskey) {
|
|
170
|
+
console.error(JSON.stringify({ error: '--counterfactual requires --human-signer-eoa <address> (or use passkey mode)' }));
|
|
173
171
|
process.exit(1);
|
|
174
172
|
}
|
|
175
173
|
|
|
@@ -280,8 +278,8 @@ try {
|
|
|
280
278
|
customVerifierAddress: passkeyVerifier,
|
|
281
279
|
};
|
|
282
280
|
if (isCounterfactual) {
|
|
283
|
-
const passkeyOwners =
|
|
284
|
-
? [AGENT_ADDRESS,
|
|
281
|
+
const passkeyOwners = recoverySigner
|
|
282
|
+
? [AGENT_ADDRESS, recoverySigner] // SharedSigner auto-added by SDK
|
|
285
283
|
: [AGENT_ADDRESS]; // SharedSigner auto-added by SDK
|
|
286
284
|
initOptions.options = {
|
|
287
285
|
owners: passkeyOwners,
|
|
@@ -295,10 +293,10 @@ try {
|
|
|
295
293
|
// EOA signer: agent key as primary signer
|
|
296
294
|
initOptions.signer = NODPAY_AGENT_KEY;
|
|
297
295
|
if (isCounterfactual) {
|
|
298
|
-
// Canonical owner order: [
|
|
299
|
-
const eoaOwners =
|
|
300
|
-
? [
|
|
301
|
-
: [
|
|
296
|
+
// Canonical owner order: [humanSigner, agentSigner, recoverySigner] — must match frontend
|
|
297
|
+
const eoaOwners = recoverySigner
|
|
298
|
+
? [humanSigner, AGENT_ADDRESS, recoverySigner]
|
|
299
|
+
: [humanSigner, AGENT_ADDRESS];
|
|
302
300
|
initOptions.options = {
|
|
303
301
|
owners: eoaOwners,
|
|
304
302
|
threshold: 2,
|
|
@@ -470,7 +468,6 @@ try {
|
|
|
470
468
|
to,
|
|
471
469
|
value,
|
|
472
470
|
valueEth,
|
|
473
|
-
purpose,
|
|
474
471
|
safeAddress,
|
|
475
472
|
counterfactual: isCounterfactual,
|
|
476
473
|
status: 'pending_user_signature',
|
|
@@ -491,8 +488,6 @@ try {
|
|
|
491
488
|
to,
|
|
492
489
|
value,
|
|
493
490
|
valueEth,
|
|
494
|
-
// purpose intentionally NOT stored server-side — privacy by design
|
|
495
|
-
// purpose is passed via URL param in the approve link (private Telegram channel)
|
|
496
491
|
safeAddress,
|
|
497
492
|
chainId: parseInt(CHAIN_ID, 10),
|
|
498
493
|
counterfactual: isCounterfactual,
|
|
@@ -522,8 +517,7 @@ try {
|
|
|
522
517
|
}
|
|
523
518
|
if (storeData.shortHash) {
|
|
524
519
|
const webBase = loadDotEnvVar('WEB_APP_URL', 'https://nodpay.ai/');
|
|
525
|
-
|
|
526
|
-
approveUrl = `${webBase}approve?safeOpHash=${storeData.safeOpHash}${purposeParam}`;
|
|
520
|
+
approveUrl = `${webBase}approve?safeOpHash=${storeData.safeOpHash}`;
|
|
527
521
|
result.approveUrl = approveUrl;
|
|
528
522
|
result.opStoreSafeOpHash = storeData.safeOpHash;
|
|
529
523
|
result.opStoreShortHash = storeData.shortHash;
|
package/scripts/txs.mjs
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* List and verify transactions for a wallet.
|
|
4
|
+
*
|
|
5
|
+
* Fetches pending/completed ops from the NodPay API, then independently
|
|
6
|
+
* verifies each one using @nodpay/core: decode callData, recompute hashes,
|
|
7
|
+
* recover signers, and check owner membership.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* npx nodpay txs --safe <SAFE_ADDRESS> --chain <CHAIN>
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { ethers } from 'ethers';
|
|
14
|
+
import {
|
|
15
|
+
computeSafeOpHash,
|
|
16
|
+
computeUserOpHash,
|
|
17
|
+
decodeCallData,
|
|
18
|
+
recoverEcdsaSigner,
|
|
19
|
+
recoverRejectSigner,
|
|
20
|
+
} from '@nodpay/core';
|
|
21
|
+
|
|
22
|
+
const args = process.argv.slice(2);
|
|
23
|
+
function getArg(name) {
|
|
24
|
+
const idx = args.indexOf(name);
|
|
25
|
+
return idx !== -1 ? args[idx + 1] : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const safe = getArg('--safe');
|
|
29
|
+
const chain = getArg('--chain');
|
|
30
|
+
|
|
31
|
+
if (!safe) {
|
|
32
|
+
console.error(JSON.stringify({ error: '--safe <address> is required' }));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const baseUrl = 'https://nodpay.ai/api';
|
|
37
|
+
const params = new URLSearchParams({ safe });
|
|
38
|
+
if (chain) params.set('chain', chain);
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const res = await fetch(`${baseUrl}/txs?${params}`);
|
|
42
|
+
const data = await res.json();
|
|
43
|
+
|
|
44
|
+
// Verify each operation
|
|
45
|
+
if (data.txs && data.txs.length > 0) {
|
|
46
|
+
for (const op of data.txs) {
|
|
47
|
+
op._verified = {};
|
|
48
|
+
|
|
49
|
+
// 1. Decode callData → actual to/value
|
|
50
|
+
if (op.callData) {
|
|
51
|
+
try {
|
|
52
|
+
const decoded = decodeCallData(op.callData);
|
|
53
|
+
if (decoded) {
|
|
54
|
+
op._verified.decodedTo = decoded.to;
|
|
55
|
+
op._verified.decodedValue = decoded.value;
|
|
56
|
+
// Cross-check: does decoded match claimed?
|
|
57
|
+
if (op.to && decoded.to.toLowerCase() !== op.to.toLowerCase()) {
|
|
58
|
+
op._verified.toMismatch = true;
|
|
59
|
+
op._verified.warning = `Claimed to=${op.to} but callData decodes to=${decoded.to}`;
|
|
60
|
+
}
|
|
61
|
+
if (op.value && decoded.value !== op.value) {
|
|
62
|
+
op._verified.valueMismatch = true;
|
|
63
|
+
op._verified.warning = (op._verified.warning || '') +
|
|
64
|
+
` Claimed value=${op.value} but callData decodes value=${decoded.value}`;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch (e) {
|
|
68
|
+
op._verified.decodeError = e.message;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 2. Recompute safeOpHash from UserOp fields
|
|
73
|
+
if (op.userOp && op.chainId) {
|
|
74
|
+
try {
|
|
75
|
+
const recomputedSafeOpHash = computeSafeOpHash(op.userOp, op.chainId);
|
|
76
|
+
op._verified.recomputedSafeOpHash = recomputedSafeOpHash;
|
|
77
|
+
if (op.safeOpHash && recomputedSafeOpHash !== op.safeOpHash) {
|
|
78
|
+
op._verified.hashMismatch = true;
|
|
79
|
+
op._verified.warning = (op._verified.warning || '') +
|
|
80
|
+
` safeOpHash mismatch: claimed=${op.safeOpHash} computed=${recomputedSafeOpHash}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const recomputedUserOpHash = computeUserOpHash(op.userOp, op.chainId);
|
|
84
|
+
op._verified.recomputedUserOpHash = recomputedUserOpHash;
|
|
85
|
+
} catch (e) {
|
|
86
|
+
op._verified.hashError = e.message;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 3. Recover propose signature signer
|
|
91
|
+
if (op.safeOpHash && op.signatures) {
|
|
92
|
+
try {
|
|
93
|
+
const sigs = typeof op.signatures === 'object' ? Object.values(op.signatures) : [];
|
|
94
|
+
for (const sig of sigs) {
|
|
95
|
+
if (sig && sig.data) {
|
|
96
|
+
const recovered = recoverEcdsaSigner(op.safeOpHash, sig.data);
|
|
97
|
+
op._verified.proposeSigner = recovered;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} catch (e) {
|
|
101
|
+
op._verified.sigRecoverError = e.message;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 4. Recover reject signature signer
|
|
106
|
+
if (op.safeOpHash && op.rejectSignature) {
|
|
107
|
+
try {
|
|
108
|
+
const recovered = recoverRejectSigner(op.safeOpHash, op.rejectSignature);
|
|
109
|
+
op._verified.rejectSigner = recovered;
|
|
110
|
+
} catch (e) {
|
|
111
|
+
op._verified.rejectSigError = e.message;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Summary
|
|
116
|
+
const warnings = op._verified.warning;
|
|
117
|
+
if (warnings) {
|
|
118
|
+
op._verified.status = 'WARNING';
|
|
119
|
+
} else if (op._verified.recomputedSafeOpHash) {
|
|
120
|
+
op._verified.status = 'VERIFIED';
|
|
121
|
+
} else {
|
|
122
|
+
op._verified.status = 'UNVERIFIED';
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
console.log(JSON.stringify(data, null, 2));
|
|
128
|
+
} catch (err) {
|
|
129
|
+
console.error(JSON.stringify({ error: err.message }));
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|