nodpay 0.1.0 → 0.2.1
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 +27 -215
- package/SKILL.md +22 -29
- package/package.json +16 -5
- package/scripts/propose.mjs +7 -3
- package/AGENT-NOTES.md +0 -53
- package/references/networks.json +0 -91
- package/scripts/predict-passkey.mjs +0 -167
package/README.md
CHANGED
|
@@ -1,236 +1,48 @@
|
|
|
1
|
-
|
|
2
|
-
name: nodpay
|
|
3
|
-
description: Propose on-chain payments from a shared wallet. Use when user asks to send crypto, make a payment, or create a shared wallet.
|
|
4
|
-
metadata:
|
|
5
|
-
{ "openclaw": { "requires": { "env": ["NODPAY_AGENT_KEY"] }, "primaryEnv": "NODPAY_AGENT_KEY" } }
|
|
6
|
-
---
|
|
1
|
+
# nodpay
|
|
7
2
|
|
|
8
|
-
|
|
3
|
+
Agent toolkit for [NodPay](https://nodpay.ai) — propose on-chain payments from a shared wallet.
|
|
9
4
|
|
|
10
|
-
>
|
|
5
|
+
> **This package is for AI agents.** If you're a human, visit [nodpay.ai](https://nodpay.ai).
|
|
11
6
|
|
|
12
|
-
|
|
7
|
+
## What NodPay does
|
|
13
8
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
## How It Works
|
|
17
|
-
|
|
18
|
-
```
|
|
19
|
-
You (agent) NodPay User (human)
|
|
20
|
-
│ │ │
|
|
21
|
-
├─ generate key ──►│ │
|
|
22
|
-
├─ send link ──────┼──────────────────►│
|
|
23
|
-
│ │◄── create wallet ─┤
|
|
24
|
-
│◄── wallet info ──┤ │
|
|
25
|
-
│ │ │
|
|
26
|
-
├─ propose tx ────►│ │
|
|
27
|
-
│ ├── approve link ───►│
|
|
28
|
-
│ │◄── sign + submit ─┤
|
|
29
|
-
│ │ │
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
The wallet is a **Safe 2-of-3 multisig**. You hold 1 key, the user holds 2 (passkey + recovery). You cannot move funds alone.
|
|
33
|
-
|
|
34
|
-
---
|
|
35
|
-
|
|
36
|
-
## Setup (one-time)
|
|
37
|
-
|
|
38
|
-
### 1. Install
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
git clone https://github.com/xhyumiracle/nodpay.git
|
|
42
|
-
cd nodpay/skill && npm install
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
### 2. Set `NODPAY_AGENT_KEY` in your environment
|
|
46
|
-
|
|
47
|
-
Generate one if you don't have it:
|
|
48
|
-
|
|
49
|
-
```bash
|
|
50
|
-
node -e "const w=require('ethers').Wallet.createRandom();console.log('Address:',w.address,'\nKey:',w.privateKey)"
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
Store the private key wherever your agent framework manages secrets (e.g. `.env`, system env vars). The address is your public agent identity.
|
|
54
|
-
|
|
55
|
-
No bundler key needed — NodPay proxies bundler calls for you.
|
|
56
|
-
|
|
57
|
-
---
|
|
9
|
+
You (an AI agent) and your human share a 2-of-3 multisig wallet. You propose transactions; the human approves or rejects each one. You cannot spend without their approval.
|
|
58
10
|
|
|
59
11
|
## Usage
|
|
60
12
|
|
|
61
|
-
### Create a wallet
|
|
62
|
-
|
|
63
|
-
Send the user this link:
|
|
64
|
-
|
|
65
|
-
```
|
|
66
|
-
https://nodpay.ai/?agent=YOUR_AGENT_ADDRESS
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
User opens → creates passkey → saves recovery phrase → gets a wallet. Takes 30 seconds.
|
|
70
|
-
|
|
71
|
-
The user will copy wallet info back to you:
|
|
72
|
-
|
|
73
|
-
```
|
|
74
|
-
NodPay Wallet (Safe): 0x...
|
|
75
|
-
Passkey X: 0x...
|
|
76
|
-
Passkey Y: 0x...
|
|
77
|
-
Agent: 0x...
|
|
78
|
-
Recovery Signer: 0x...
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
**Store all fields** — you need them for proposing transactions.
|
|
82
|
-
|
|
83
|
-
#### Wallet file management
|
|
84
|
-
|
|
85
|
-
Store wallet info in `.nodpay/wallets/` in your workspace root (separate from skill code):
|
|
86
|
-
|
|
87
|
-
```
|
|
88
|
-
.nodpay/wallets/
|
|
89
|
-
0xAbC...123.json # one file per wallet, named by Safe address
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
Each wallet file:
|
|
93
|
-
|
|
94
|
-
```json
|
|
95
|
-
{
|
|
96
|
-
"safe": "0x...",
|
|
97
|
-
"signerType": "passkey",
|
|
98
|
-
"passkeyX": "0x...",
|
|
99
|
-
"passkeyY": "0x...",
|
|
100
|
-
"recovery": "0x...",
|
|
101
|
-
"chain": "sepolia",
|
|
102
|
-
"createdAt": "2025-01-01"
|
|
103
|
-
}
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
For EOA wallets, replace passkey fields with `"userSigner": "0x..."`.
|
|
107
|
-
|
|
108
|
-
One agent key serves all wallets — multi-wallet is handled user-side (different passkeys/recovery keys → different Safe addresses, same agent).
|
|
109
|
-
|
|
110
|
-
**⚠️ Verify the Agent address matches yours.** If it doesn't, the wallet is bound to someone else's key — alert the user and send a fresh link.
|
|
111
|
-
|
|
112
|
-
**Multiple wallets**: A user may have multiple wallets (different devices, different passkeys). Before proposing, confirm which wallet to use — especially if you manage more than one.
|
|
113
|
-
|
|
114
|
-
### Propose a transaction
|
|
115
|
-
|
|
116
|
-
```bash
|
|
117
|
-
NODPAY_AGENT_KEY=0x... \
|
|
118
|
-
node scripts/propose.mjs \
|
|
119
|
-
--safe <WALLET_ADDRESS> \
|
|
120
|
-
--to <RECIPIENT> \
|
|
121
|
-
--value-eth <AMOUNT> \
|
|
122
|
-
--passkey-x <PASSKEY_X> \
|
|
123
|
-
--passkey-y <PASSKEY_Y> \
|
|
124
|
-
--recovery <RECOVERY_SIGNER> \
|
|
125
|
-
--signer-type passkey
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
The script outputs JSON with an `approveUrl`. Send it to the user:
|
|
129
|
-
|
|
130
|
-
> 💰 Payment: 0.01 ETH → 0xRecipient...
|
|
131
|
-
> 👉 Approve: https://nodpay.ai/approve?safeOpHash=0x...
|
|
132
|
-
|
|
133
|
-
**First transaction deploys the wallet on-chain.** Pass `--passkey-x`, `--passkey-y`, and `--recovery` for the first tx. After deployment, `--safe` alone is sufficient (but passing all params is always safe).
|
|
134
|
-
|
|
135
|
-
### Check balance
|
|
136
|
-
|
|
137
|
-
```bash
|
|
138
|
-
curl -s -X POST https://ethereum-sepolia-rpc.publicnode.com \
|
|
139
|
-
-H "Content-Type: application/json" \
|
|
140
|
-
-d '{"jsonrpc":"2.0","method":"eth_getBalance","params":["<WALLET_ADDRESS>","latest"],"id":1}'
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
If balance is 0, remind the user to deposit before proposing.
|
|
144
|
-
|
|
145
|
-
### Check pending transactions
|
|
146
|
-
|
|
147
13
|
```bash
|
|
148
|
-
|
|
14
|
+
# No install needed
|
|
15
|
+
NODPAY_AGENT_KEY=0x... npx nodpay propose \
|
|
16
|
+
--safe 0xWALLET --to 0xRECIPIENT --value-eth 0.01 \
|
|
17
|
+
--signer-type passkey --passkey-x 0x... --passkey-y 0x...
|
|
149
18
|
```
|
|
150
19
|
|
|
151
|
-
|
|
20
|
+
## Full guide
|
|
152
21
|
|
|
153
|
-
|
|
22
|
+
**[nodpay.ai/skill.md](https://nodpay.ai/skill.md)** — complete setup + integration guide for agents (key generation, wallet creation, proposing, error handling).
|
|
154
23
|
|
|
155
|
-
##
|
|
24
|
+
## How it works
|
|
156
25
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
| `--safe` | ✅ | Wallet (Safe) address |
|
|
162
|
-
| `--to` | ✅ | Recipient address |
|
|
163
|
-
| `--value-eth` | ✅ | Amount in ETH |
|
|
164
|
-
| `--signer-type` | ✅ | `passkey` or `eoa` |
|
|
165
|
-
| `--passkey-x` | passkey wallets | Passkey public key X |
|
|
166
|
-
| `--passkey-y` | passkey wallets | Passkey public key Y |
|
|
167
|
-
| `--user-signer` | eoa wallets | User's EOA address |
|
|
168
|
-
| `--recovery` | first tx | Recovery signer address |
|
|
169
|
-
| `--nonce` | ❌ | Force nonce (for replacements) |
|
|
170
|
-
| `--purpose` | ❌ | Human-readable label |
|
|
171
|
-
|
|
172
|
-
### Environment Variables
|
|
173
|
-
|
|
174
|
-
| Var | Required | Description |
|
|
175
|
-
|-----|----------|-------------|
|
|
176
|
-
| `NODPAY_AGENT_KEY` | ✅ | Agent signing key |
|
|
177
|
-
| `RPC_URL` | ❌ | RPC endpoint (default: Sepolia) |
|
|
178
|
-
| `CHAIN_ID` | ❌ | Chain ID (default: 11155111) |
|
|
179
|
-
| `PIMLICO_API_KEY` | ❌ | Own bundler key (optional, NodPay proxy is default) |
|
|
180
|
-
|
|
181
|
-
### Supported Chains
|
|
182
|
-
|
|
183
|
-
`sepolia`, `ethereum`, `base`, `base_sepolia`, `arbitrum`, `optimism`, `polygon`
|
|
184
|
-
|
|
185
|
-
Set `RPC_URL` and `CHAIN_ID` for non-Sepolia chains. See `references/networks.json` for chain configs.
|
|
186
|
-
|
|
187
|
-
---
|
|
188
|
-
|
|
189
|
-
## Transaction Patterns
|
|
190
|
-
|
|
191
|
-
**Sequential**: Just call propose multiple times. Nonces auto-increment.
|
|
192
|
-
|
|
193
|
-
**Replace**: Propose with `--nonce N` (same nonce as the pending tx). User picks which to approve.
|
|
194
|
-
|
|
195
|
-
**Cascade**: Rejecting tx #N auto-invalidates all tx with nonce > N. This is irreversible.
|
|
196
|
-
|
|
197
|
-
⚠️ **Never propose a new nonce then reject the old one** — the cascade will destroy your new tx too.
|
|
198
|
-
|
|
199
|
-
---
|
|
200
|
-
|
|
201
|
-
## Reconnect (Wallet Recovery)
|
|
202
|
-
|
|
203
|
-
If the user cleared their browser data, the wallet still exists on-chain. Build a reconnect link:
|
|
204
|
-
|
|
205
|
-
```
|
|
206
|
-
https://nodpay.ai/?agent=YOUR_AGENT_ADDRESS&safe=WALLET_ADDRESS&recovery=RECOVERY_SIGNER&x=PASSKEY_X&y=PASSKEY_Y
|
|
207
|
-
```
|
|
26
|
+
1. Agent generates a key → sends user a wallet creation link
|
|
27
|
+
2. User creates a passkey wallet at nodpay.ai (30 seconds)
|
|
28
|
+
3. Agent proposes transactions with `npx nodpay propose`
|
|
29
|
+
4. User approves/rejects on their phone
|
|
208
30
|
|
|
209
|
-
|
|
31
|
+
## Env
|
|
210
32
|
|
|
211
|
-
|
|
33
|
+
| Variable | Required | Description |
|
|
34
|
+
|----------|----------|-------------|
|
|
35
|
+
| `NODPAY_AGENT_KEY` | ✅ | Agent's private key (hex) |
|
|
212
36
|
|
|
213
|
-
##
|
|
37
|
+
## Supported chains
|
|
214
38
|
|
|
215
|
-
|
|
216
|
-
|-------|--------|--------|
|
|
217
|
-
| Agent EOA | You | Propose only |
|
|
218
|
-
| Passkey | User's device | Approve or reject |
|
|
219
|
-
| Recovery | User's 12-word phrase | Backup access |
|
|
39
|
+
Ethereum · Base · Arbitrum · Optimism · Polygon · Sepolia · Base Sepolia
|
|
220
40
|
|
|
221
|
-
|
|
222
|
-
- Passkey X/Y are public key material, safe to store
|
|
223
|
-
- No private keys stored on NodPay's server
|
|
224
|
-
- Funds are safe on-chain even if NodPay goes offline
|
|
41
|
+
## Related
|
|
225
42
|
|
|
226
|
-
|
|
43
|
+
- [`@nodpay/core`](https://www.npmjs.com/package/@nodpay/core) — Protocol primitives (hash, decode, verify)
|
|
44
|
+
- [nodpay.ai](https://nodpay.ai) — Web app
|
|
227
45
|
|
|
228
|
-
##
|
|
46
|
+
## License
|
|
229
47
|
|
|
230
|
-
|
|
231
|
-
|-----------|--------|
|
|
232
|
-
| "create a wallet" | Send wallet creation link |
|
|
233
|
-
| "send 0.1 ETH to 0x..." | Propose transaction |
|
|
234
|
-
| "balance" | RPC `eth_getBalance` on Safe address |
|
|
235
|
-
| "pending?" | `GET /api/txs?safe=...` |
|
|
236
|
-
| "wallet disappeared" | Send reconnect link |
|
|
48
|
+
MIT
|
package/SKILL.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: nodpay
|
|
3
3
|
description: Propose on-chain payments from a shared wallet. Use when user asks to send crypto, make a payment, or create a shared wallet.
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
openclaw:
|
|
5
|
+
requires:
|
|
6
|
+
env: [NODPAY_AGENT_KEY]
|
|
7
|
+
primaryEnv: NODPAY_AGENT_KEY
|
|
6
8
|
---
|
|
7
9
|
|
|
8
10
|
# NodPay — Agent Wallet
|
|
@@ -16,16 +18,16 @@ NodPay lets you (an AI agent) propose on-chain payments from a shared wallet. Yo
|
|
|
16
18
|
## How It Works
|
|
17
19
|
|
|
18
20
|
```
|
|
19
|
-
You (agent)
|
|
20
|
-
│
|
|
21
|
-
|
|
22
|
-
│
|
|
23
|
-
│◄── wallet info
|
|
24
|
-
│
|
|
25
|
-
|
|
26
|
-
│
|
|
27
|
-
│
|
|
28
|
-
│
|
|
21
|
+
You (agent) NodPay server User (human)
|
|
22
|
+
│ │ │
|
|
23
|
+
├── send setup link ──────┼─────────────────────►│
|
|
24
|
+
│ │ ├── create wallet
|
|
25
|
+
│◄── wallet info ─────────┼───────────────────────┤
|
|
26
|
+
│ │ │
|
|
27
|
+
├── propose tx ──────────►│ │
|
|
28
|
+
│ ├── approve link ──────►│
|
|
29
|
+
│ │◄── sign + submit ─────┤
|
|
30
|
+
│ │ │
|
|
29
31
|
```
|
|
30
32
|
|
|
31
33
|
The wallet is a **Safe 2-of-3 multisig**. You hold 1 key, the user holds 2 (passkey + recovery). You cannot move funds alone.
|
|
@@ -54,7 +56,7 @@ Replace `YOUR_AGENT_ADDRESS` with your EOA address and send:
|
|
|
54
56
|
|
|
55
57
|
**Use the URL above verbatim.** Do not search for NodPay or guess the URL — `nodpay.ai` is the only correct domain.
|
|
56
58
|
|
|
57
|
-
|
|
59
|
+
The user will create the wallet and copy info back to you:
|
|
58
60
|
|
|
59
61
|
```
|
|
60
62
|
NodPay Wallet (Safe): 0x...
|
|
@@ -66,9 +68,11 @@ Recovery Signer: 0x...
|
|
|
66
68
|
|
|
67
69
|
**Store all fields** — you need them for proposing transactions.
|
|
68
70
|
|
|
71
|
+
**After wallet creation**, tell the user their wallet is ready and the address works on any supported chain. Briefly offer: *"Want to try a test transaction on a testnet first?"* — only guide to testnets (Sepolia, etc.) if the user says yes.
|
|
72
|
+
|
|
69
73
|
#### Wallet file management
|
|
70
74
|
|
|
71
|
-
Store wallet info in `.nodpay/wallets/` in your workspace root
|
|
75
|
+
Store wallet info in `.nodpay/wallets/` in your workspace root:
|
|
72
76
|
|
|
73
77
|
```
|
|
74
78
|
.nodpay/wallets/
|
|
@@ -85,7 +89,6 @@ Each wallet file:
|
|
|
85
89
|
"passkeyX": "0x...",
|
|
86
90
|
"passkeyY": "0x...",
|
|
87
91
|
"recovery": "0x...",
|
|
88
|
-
"chain": "sepolia",
|
|
89
92
|
"createdAt": "2025-01-01"
|
|
90
93
|
}
|
|
91
94
|
```
|
|
@@ -98,23 +101,11 @@ One agent key serves all wallets — multi-wallet is handled user-side (differen
|
|
|
98
101
|
|
|
99
102
|
**⚠️ Verify the Agent address matches yours.** If it doesn't, the wallet is bound to someone else's key — alert the user and send a fresh link.
|
|
100
103
|
|
|
101
|
-
**Multiple wallets**: A user may have multiple wallets (different devices, different passkeys). Before proposing, confirm which wallet to use — especially if you manage more than one.
|
|
102
|
-
|
|
103
104
|
### Propose a transaction
|
|
104
105
|
|
|
105
|
-
First time only — install the propose script:
|
|
106
|
-
|
|
107
|
-
```bash
|
|
108
|
-
git clone https://github.com/xhyumiracle/nodpay.git
|
|
109
|
-
cd nodpay/skill && npm install
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
Then propose:
|
|
113
|
-
|
|
114
106
|
```bash
|
|
115
|
-
cd nodpay/skill && \
|
|
116
107
|
NODPAY_AGENT_KEY=0x... \
|
|
117
|
-
|
|
108
|
+
npx nodpay propose \
|
|
118
109
|
--safe <WALLET_ADDRESS> \
|
|
119
110
|
--to <RECIPIENT> \
|
|
120
111
|
--value-eth <AMOUNT> \
|
|
@@ -182,7 +173,9 @@ Chain config (RPC, bundler, explorer) is auto-resolved from `references/networks
|
|
|
182
173
|
|
|
183
174
|
### Supported Chains
|
|
184
175
|
|
|
185
|
-
`
|
|
176
|
+
`ethereum`, `base`, `arbitrum`, `optimism`, `polygon`, `sepolia`, `base_sepolia`
|
|
177
|
+
|
|
178
|
+
The wallet address is the same across all chains (counterfactual). Chain is only relevant at transaction time. **Do not assume a default chain.** When the user asks to send, ask which chain if not specified.
|
|
186
179
|
|
|
187
180
|
---
|
|
188
181
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodpay",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "NodPay CLI — propose on-chain payments from agent-human shared wallets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,11 +9,22 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"bin/",
|
|
11
11
|
"scripts/",
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
"SKILL.md"
|
|
13
|
+
],
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/xhyumiracle/nodpay-cli.git"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://nodpay.ai",
|
|
19
|
+
"keywords": [
|
|
20
|
+
"crypto",
|
|
21
|
+
"wallet",
|
|
22
|
+
"safe",
|
|
23
|
+
"multisig",
|
|
24
|
+
"agent",
|
|
25
|
+
"erc-4337",
|
|
26
|
+
"payment"
|
|
15
27
|
],
|
|
16
|
-
"keywords": ["crypto", "wallet", "safe", "multisig", "agent", "erc-4337", "payment"],
|
|
17
28
|
"author": "xhyumiracle",
|
|
18
29
|
"license": "MIT",
|
|
19
30
|
"dependencies": {
|
package/scripts/propose.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* NODPAY_AGENT_KEY - Agent signer private key
|
|
11
11
|
* SAFE_ADDRESS - Deployed Safe address (can be overridden with --safe)
|
|
12
12
|
* RPC_URL - RPC endpoint
|
|
13
|
-
* CHAIN_ID - Chain ID (default
|
|
13
|
+
* CHAIN_ID - Chain ID (required, no default)
|
|
14
14
|
*
|
|
15
15
|
* Args:
|
|
16
16
|
* --to <address> - Recipient address
|
|
@@ -37,8 +37,12 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
37
37
|
const PENDING_DIR = join(__dirname, '..', '.pending-txs');
|
|
38
38
|
mkdirSync(PENDING_DIR, { recursive: true });
|
|
39
39
|
|
|
40
|
-
const RPC_URL = process.env.RPC_URL
|
|
41
|
-
const CHAIN_ID = process.env.CHAIN_ID
|
|
40
|
+
const RPC_URL = process.env.RPC_URL;
|
|
41
|
+
const CHAIN_ID = process.env.CHAIN_ID;
|
|
42
|
+
if (!RPC_URL || !CHAIN_ID) {
|
|
43
|
+
console.error('Error: RPC_URL and CHAIN_ID environment variables are required.\nSet them for your target chain. See references/networks.json for supported chains.');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
42
46
|
const ENTRYPOINT_ADDRESS = ENTRYPOINT;
|
|
43
47
|
const NODPAY_AGENT_KEY = process.env.NODPAY_AGENT_KEY;
|
|
44
48
|
const DEFAULT_SAFE = process.env.SAFE_ADDRESS;
|
package/AGENT-NOTES.md
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
# Agent Operational Notes
|
|
2
|
-
|
|
3
|
-
Advanced patterns and edge cases. Read SKILL.md first.
|
|
4
|
-
|
|
5
|
-
## Nonce Management
|
|
6
|
-
|
|
7
|
-
- **Auto-increment**: propose without `--nonce` → script fetches on-chain nonce, auto-increments past pending ops
|
|
8
|
-
- **Replace**: use `--nonce N` with same nonce as pending tx → both appear in dashboard, user picks one
|
|
9
|
-
- **Gas reuse**: when nonce > on-chain nonce, bundler rejects gas estimation (`AA25`). Script auto-reuses gas from the first pending op.
|
|
10
|
-
|
|
11
|
-
## Status Lifecycle
|
|
12
|
-
|
|
13
|
-
```
|
|
14
|
-
pending → submitted → executed
|
|
15
|
-
↘ rejected
|
|
16
|
-
↘ replaced (another tx at same nonce was executed)
|
|
17
|
-
↘ invalidated (cascade from lower nonce rejection)
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
## Multi-Wallet
|
|
21
|
-
|
|
22
|
-
One agent can serve multiple wallets. Different user devices → different passkeys → different Safe addresses. Always confirm which wallet before proposing.
|
|
23
|
-
|
|
24
|
-
## EOA Wallets
|
|
25
|
-
|
|
26
|
-
For users who prefer MetaMask/browser wallet over passkey:
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
node scripts/propose.mjs \
|
|
30
|
-
--safe <WALLET_ADDRESS> \
|
|
31
|
-
--to <RECIPIENT> \
|
|
32
|
-
--value-eth <AMOUNT> \
|
|
33
|
-
--user-signer <USER_EOA_ADDRESS> \
|
|
34
|
-
--recovery <RECOVERY_SIGNER> \
|
|
35
|
-
--signer-type eoa
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
## Owner Ordering (CREATE2)
|
|
39
|
-
|
|
40
|
-
Safe address is deterministically derived. Owner order is fixed:
|
|
41
|
-
|
|
42
|
-
- **Passkey**: `[agent, recovery, SharedSigner]`
|
|
43
|
-
- **EOA**: `[userSigner, agent, recovery]`
|
|
44
|
-
|
|
45
|
-
Wrong order → wrong address. The propose script handles this automatically.
|
|
46
|
-
|
|
47
|
-
## API Reference
|
|
48
|
-
|
|
49
|
-
```
|
|
50
|
-
GET /api/txs?safe=<addr> # list all ops for a wallet
|
|
51
|
-
GET /api/txs?safe=<addr>&status=pending # filter by status
|
|
52
|
-
GET /api/tx/<shortHash> # get single op by hash prefix
|
|
53
|
-
```
|
package/references/networks.json
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"mainnet": {
|
|
3
|
-
"ethereum": {
|
|
4
|
-
"name": "Ethereum",
|
|
5
|
-
"chainId": 1,
|
|
6
|
-
"rpcUrl": "https://ethereum-rpc.publicnode.com",
|
|
7
|
-
"txServiceUrl": "https://safe-transaction-mainnet.safe.global",
|
|
8
|
-
"safePrefix": "eth",
|
|
9
|
-
"explorerUrl": "https://etherscan.io",
|
|
10
|
-
"explorerTxPath": "/tx/",
|
|
11
|
-
"explorerAddrPath": "/address/",
|
|
12
|
-
"nativeCurrency": "ETH",
|
|
13
|
-
"bundlerUrlTemplate": "https://api.pimlico.io/v2/1/rpc?apikey={PIMLICO_API_KEY}"
|
|
14
|
-
},
|
|
15
|
-
"base": {
|
|
16
|
-
"name": "Base",
|
|
17
|
-
"chainId": 8453,
|
|
18
|
-
"rpcUrl": "https://base-rpc.publicnode.com",
|
|
19
|
-
"txServiceUrl": "https://safe-transaction-base.safe.global",
|
|
20
|
-
"safePrefix": "base",
|
|
21
|
-
"explorerUrl": "https://basescan.org",
|
|
22
|
-
"explorerTxPath": "/tx/",
|
|
23
|
-
"explorerAddrPath": "/address/",
|
|
24
|
-
"nativeCurrency": "ETH",
|
|
25
|
-
"gasSponsored": true,
|
|
26
|
-
"bundlerUrlTemplate": "https://api.pimlico.io/v2/8453/rpc?apikey={PIMLICO_API_KEY}"
|
|
27
|
-
},
|
|
28
|
-
"arbitrum": {
|
|
29
|
-
"name": "Arbitrum",
|
|
30
|
-
"chainId": 42161,
|
|
31
|
-
"rpcUrl": "https://arbitrum-one-rpc.publicnode.com",
|
|
32
|
-
"txServiceUrl": "https://safe-transaction-arbitrum.safe.global",
|
|
33
|
-
"safePrefix": "arb1",
|
|
34
|
-
"explorerUrl": "https://arbiscan.io",
|
|
35
|
-
"explorerTxPath": "/tx/",
|
|
36
|
-
"explorerAddrPath": "/address/",
|
|
37
|
-
"nativeCurrency": "ETH",
|
|
38
|
-
"bundlerUrlTemplate": "https://api.pimlico.io/v2/42161/rpc?apikey={PIMLICO_API_KEY}"
|
|
39
|
-
},
|
|
40
|
-
"polygon": {
|
|
41
|
-
"name": "Polygon",
|
|
42
|
-
"chainId": 137,
|
|
43
|
-
"rpcUrl": "https://polygon-bor-rpc.publicnode.com",
|
|
44
|
-
"txServiceUrl": "https://safe-transaction-polygon.safe.global",
|
|
45
|
-
"safePrefix": "matic",
|
|
46
|
-
"explorerUrl": "https://polygonscan.com",
|
|
47
|
-
"explorerTxPath": "/tx/",
|
|
48
|
-
"explorerAddrPath": "/address/",
|
|
49
|
-
"nativeCurrency": "MATIC",
|
|
50
|
-
"bundlerUrlTemplate": "https://api.pimlico.io/v2/137/rpc?apikey={PIMLICO_API_KEY}"
|
|
51
|
-
},
|
|
52
|
-
"optimism": {
|
|
53
|
-
"name": "Optimism",
|
|
54
|
-
"chainId": 10,
|
|
55
|
-
"rpcUrl": "https://optimism-rpc.publicnode.com",
|
|
56
|
-
"txServiceUrl": "https://safe-transaction-optimism.safe.global",
|
|
57
|
-
"safePrefix": "oeth",
|
|
58
|
-
"explorerUrl": "https://optimistic.etherscan.io",
|
|
59
|
-
"explorerTxPath": "/tx/",
|
|
60
|
-
"explorerAddrPath": "/address/",
|
|
61
|
-
"nativeCurrency": "ETH",
|
|
62
|
-
"bundlerUrlTemplate": "https://api.pimlico.io/v2/10/rpc?apikey={PIMLICO_API_KEY}"
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
"testnet": {
|
|
66
|
-
"sepolia": {
|
|
67
|
-
"name": "Sepolia (Ethereum)",
|
|
68
|
-
"chainId": 11155111,
|
|
69
|
-
"rpcUrl": "https://ethereum-sepolia-rpc.publicnode.com",
|
|
70
|
-
"txServiceUrl": "https://safe-transaction-sepolia.safe.global",
|
|
71
|
-
"safePrefix": "sep",
|
|
72
|
-
"explorerUrl": "https://sepolia.etherscan.io",
|
|
73
|
-
"explorerTxPath": "/tx/",
|
|
74
|
-
"explorerAddrPath": "/address/",
|
|
75
|
-
"nativeCurrency": "ETH",
|
|
76
|
-
"bundlerUrlTemplate": "https://api.pimlico.io/v2/11155111/rpc?apikey={PIMLICO_API_KEY}"
|
|
77
|
-
},
|
|
78
|
-
"base_sepolia": {
|
|
79
|
-
"name": "Base Sepolia",
|
|
80
|
-
"chainId": 84532,
|
|
81
|
-
"rpcUrl": "https://base-sepolia-rpc.publicnode.com",
|
|
82
|
-
"txServiceUrl": "https://safe-transaction-base-sepolia.safe.global",
|
|
83
|
-
"safePrefix": "basesep",
|
|
84
|
-
"explorerUrl": "https://sepolia.basescan.org",
|
|
85
|
-
"explorerTxPath": "/tx/",
|
|
86
|
-
"explorerAddrPath": "/address/",
|
|
87
|
-
"nativeCurrency": "ETH",
|
|
88
|
-
"bundlerUrlTemplate": "https://api.pimlico.io/v2/84532/rpc?apikey={PIMLICO_API_KEY}"
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Predict a Safe address for a passkey-based wallet (counterfactual).
|
|
4
|
-
*
|
|
5
|
-
* Uses Safe4337Pack with passkey signer support to ensure the Safe setup
|
|
6
|
-
* includes SharedSigner.configure() for passkey coordinates.
|
|
7
|
-
*
|
|
8
|
-
* Env vars:
|
|
9
|
-
* NODPAY_AGENT_KEY - Agent signer private key
|
|
10
|
-
* RPC_URL - RPC endpoint
|
|
11
|
-
* CHAIN_ID - Chain ID (default: 11155111)
|
|
12
|
-
* PIMLICO_API_KEY - Pimlico bundler API key
|
|
13
|
-
*
|
|
14
|
-
* Args:
|
|
15
|
-
* --raw-id <base64> - Passkey rawId (base64 encoded)
|
|
16
|
-
* --x <hex> - Passkey public key x coordinate
|
|
17
|
-
* --y <hex> - Passkey public key y coordinate
|
|
18
|
-
* --salt <nonce> - Salt nonce (optional, default: random)
|
|
19
|
-
* --chain-id <number> - Override chain ID
|
|
20
|
-
*
|
|
21
|
-
* Output: JSON { predictedAddress, owners, threshold, chainId, salt, deployed, passkeyCoordinates }
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import { Safe4337Pack } from '@safe-global/relay-kit';
|
|
25
|
-
import Safe, { getMultiSendContract, encodeMultiSendData, SafeProvider } from '@safe-global/protocol-kit';
|
|
26
|
-
import { OperationType } from '@safe-global/types-kit';
|
|
27
|
-
import { ethers } from 'ethers';
|
|
28
|
-
|
|
29
|
-
const SHARED_SIGNER_ADDRESS = '0x94a4F6affBd8975951142c3999aEAB7ecee555c2';
|
|
30
|
-
|
|
31
|
-
// Default P-256 verifier (FCLP256Verifier) on major chains
|
|
32
|
-
// From safe-modules-deployments v0.2.1
|
|
33
|
-
const FCLP256_VERIFIERS = {
|
|
34
|
-
'11155111': '0x445a0683e494ea0c5AF3E83c5159fBE47Cf9e765', // Sepolia
|
|
35
|
-
'1': '0x445a0683e494ea0c5AF3E83c5159fBE47Cf9e765', // Mainnet
|
|
36
|
-
'8453': '0x445a0683e494ea0c5AF3E83c5159fBE47Cf9e765', // Base
|
|
37
|
-
'84532': '0x445a0683e494ea0c5AF3E83c5159fBE47Cf9e765', // Base Sepolia
|
|
38
|
-
'42161': '0x445a0683e494ea0c5AF3E83c5159fBE47Cf9e765', // Arbitrum
|
|
39
|
-
'10': '0x445a0683e494ea0c5AF3E83c5159fBE47Cf9e765', // Optimism
|
|
40
|
-
'137': '0x445a0683e494ea0c5AF3E83c5159fBE47Cf9e765', // Polygon
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const RPC_URL = process.env.RPC_URL || 'https://ethereum-sepolia-rpc.publicnode.com';
|
|
44
|
-
const NODPAY_AGENT_KEY = process.env.NODPAY_AGENT_KEY;
|
|
45
|
-
const PIMLICO_API_KEY = process.env.PIMLICO_API_KEY;
|
|
46
|
-
|
|
47
|
-
if (!NODPAY_AGENT_KEY) {
|
|
48
|
-
console.error(JSON.stringify({ error: 'Missing NODPAY_AGENT_KEY env var' }));
|
|
49
|
-
process.exit(1);
|
|
50
|
-
}
|
|
51
|
-
if (!PIMLICO_API_KEY) {
|
|
52
|
-
console.error(JSON.stringify({ error: 'Missing PIMLICO_API_KEY env var' }));
|
|
53
|
-
process.exit(1);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const agentWallet = new ethers.Wallet(NODPAY_AGENT_KEY);
|
|
57
|
-
const DEFAULT_AGENT_ADDRESS = agentWallet.address;
|
|
58
|
-
|
|
59
|
-
const args = process.argv.slice(2);
|
|
60
|
-
function getArg(name) {
|
|
61
|
-
const idx = args.indexOf(name);
|
|
62
|
-
return idx !== -1 ? args[idx + 1] : undefined;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const rawId = getArg('--raw-id');
|
|
66
|
-
const x = getArg('--x');
|
|
67
|
-
const y = getArg('--y');
|
|
68
|
-
const CHAIN_ID = parseInt(getArg('--chain-id') || process.env.CHAIN_ID || '11155111', 10);
|
|
69
|
-
const salt = getArg('--salt') || Math.floor(Math.random() * 1_000_000_000).toString();
|
|
70
|
-
// Agent address can be overridden via --agent flag (for multi-agent support)
|
|
71
|
-
const AGENT_ADDRESS = getArg('--agent') || DEFAULT_AGENT_ADDRESS;
|
|
72
|
-
const RECOVERY_ADDRESS = getArg('--recovery'); // Optional 3rd owner for 2-of-3
|
|
73
|
-
|
|
74
|
-
if (!x || !y) {
|
|
75
|
-
console.error(JSON.stringify({ error: 'Missing --x <hex> and --y <hex> passkey coordinates' }));
|
|
76
|
-
process.exit(1);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
try {
|
|
80
|
-
const owners = RECOVERY_ADDRESS
|
|
81
|
-
? [AGENT_ADDRESS, SHARED_SIGNER_ADDRESS, RECOVERY_ADDRESS]
|
|
82
|
-
: [AGENT_ADDRESS, SHARED_SIGNER_ADDRESS];
|
|
83
|
-
const threshold = 2;
|
|
84
|
-
const bundlerUrl = `https://api.pimlico.io/v2/${CHAIN_ID}/rpc?apikey=${PIMLICO_API_KEY}`;
|
|
85
|
-
const verifier = FCLP256_VERIFIERS[String(CHAIN_ID)] || FCLP256_VERIFIERS['11155111'];
|
|
86
|
-
|
|
87
|
-
// Encode SharedSigner.configure() — stores passkey coordinates in Safe's storage
|
|
88
|
-
const sharedSignerIface = new ethers.Interface([
|
|
89
|
-
'function configure((uint256 x, uint256 y, uint176 verifiers) signer)'
|
|
90
|
-
]);
|
|
91
|
-
const configureData = sharedSignerIface.encodeFunctionData('configure', [{
|
|
92
|
-
x: BigInt(x),
|
|
93
|
-
y: BigInt(y),
|
|
94
|
-
verifiers: BigInt(verifier)
|
|
95
|
-
}]);
|
|
96
|
-
|
|
97
|
-
// We need Safe4337Pack to use our custom setup that includes:
|
|
98
|
-
// 1. enableModules([Safe4337Module]) — done by Safe4337Pack
|
|
99
|
-
// 2. SharedSigner.configure({x, y, verifiers}) — we need to inject this
|
|
100
|
-
//
|
|
101
|
-
// Strategy: Initialize Safe4337Pack normally, then monkey-patch protocolKit
|
|
102
|
-
// to include our configure call in the predicted Safe config.
|
|
103
|
-
|
|
104
|
-
// First, init Safe4337Pack to get module addresses and standard config
|
|
105
|
-
const safe4337Pack = await Safe4337Pack.init({
|
|
106
|
-
provider: RPC_URL,
|
|
107
|
-
signer: NODPAY_AGENT_KEY,
|
|
108
|
-
bundlerUrl,
|
|
109
|
-
options: {
|
|
110
|
-
owners,
|
|
111
|
-
threshold,
|
|
112
|
-
saltNonce: salt,
|
|
113
|
-
},
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// Get the address WITHOUT passkey config (this is what we had before — wrong)
|
|
117
|
-
const addressWithoutPasskey = await safe4337Pack.protocolKit.getAddress();
|
|
118
|
-
|
|
119
|
-
// Now create a new Safe4337Pack with passkey signer to get the CORRECT address
|
|
120
|
-
// We pass a fake passkey signer object that Safe4337Pack recognizes
|
|
121
|
-
const passkeySignerObj = {
|
|
122
|
-
rawId: rawId || 'passkey-' + salt,
|
|
123
|
-
coordinates: { x, y },
|
|
124
|
-
customVerifierAddress: verifier,
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
const ownersForPasskeyPack = RECOVERY_ADDRESS
|
|
128
|
-
? [AGENT_ADDRESS, RECOVERY_ADDRESS] // SharedSigner will be auto-added by SDK
|
|
129
|
-
: [AGENT_ADDRESS]; // SharedSigner will be auto-added by SDK
|
|
130
|
-
const safe4337PackWithPasskey = await Safe4337Pack.init({
|
|
131
|
-
provider: RPC_URL,
|
|
132
|
-
signer: passkeySignerObj,
|
|
133
|
-
bundlerUrl,
|
|
134
|
-
options: {
|
|
135
|
-
owners: ownersForPasskeyPack,
|
|
136
|
-
threshold,
|
|
137
|
-
saltNonce: salt,
|
|
138
|
-
},
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
const predictedAddress = await safe4337PackWithPasskey.protocolKit.getAddress();
|
|
142
|
-
|
|
143
|
-
// Check if already deployed
|
|
144
|
-
const provider = new ethers.JsonRpcProvider(RPC_URL);
|
|
145
|
-
const code = await provider.getCode(predictedAddress);
|
|
146
|
-
const deployed = code !== '0x';
|
|
147
|
-
|
|
148
|
-
const result = {
|
|
149
|
-
predictedAddress,
|
|
150
|
-
owners,
|
|
151
|
-
threshold,
|
|
152
|
-
chainId: CHAIN_ID,
|
|
153
|
-
salt,
|
|
154
|
-
deployed,
|
|
155
|
-
sharedSigner: SHARED_SIGNER_ADDRESS,
|
|
156
|
-
verifier,
|
|
157
|
-
passkeyCoordinates: { x, y },
|
|
158
|
-
rawId: rawId || null,
|
|
159
|
-
// For debugging: show old address without passkey config
|
|
160
|
-
_addressWithoutPasskeyConfig: addressWithoutPasskey,
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
console.log(JSON.stringify(result, null, 2));
|
|
164
|
-
} catch (error) {
|
|
165
|
-
console.error(JSON.stringify({ error: error.message || String(error) }));
|
|
166
|
-
process.exit(1);
|
|
167
|
-
}
|