clanker-wallet 0.3.0 → 0.4.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 +36 -25
- package/SKILL.md +50 -28
- package/bin/cli.js +51 -21
- package/dist/client.js +24 -6
- package/dist/crypto.js +19 -3
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/keccak.d.ts +5 -0
- package/dist/keccak.js +82 -0
- package/dist/types.d.ts +10 -1
- package/dist/validate.d.ts +19 -0
- package/dist/validate.js +65 -0
- package/package.json +29 -3
package/README.md
CHANGED
|
@@ -17,12 +17,14 @@ bunx clanker-wallet
|
|
|
17
17
|
## Quick Start
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
# 1. Human goes to https://clanker-wallet.xyz, connects wallet,
|
|
21
|
-
#
|
|
20
|
+
# 1. Human goes to https://clanker-wallet.xyz, connects wallet, clicks "copy prompt for agent",
|
|
21
|
+
# pastes the prompt to you. The prompt contains a JSON pairing code.
|
|
22
|
+
# 2. Pair (one-time) — extract the JSON from the prompt
|
|
22
23
|
clanker-wallet pair '{"version":1,"pubkey":"ab12...","relay":"wss://...","wallet":"0x..."}'
|
|
23
24
|
|
|
24
|
-
# 3. Send a transaction (blocks until human approves or rejects)
|
|
25
|
-
|
|
25
|
+
# 3. Send a transaction (blocks until human approves or rejects; 1 h by default)
|
|
26
|
+
# 0.001 ETH on Base Sepolia (84532) to the burn address. --value is in wei.
|
|
27
|
+
clanker-wallet tx --chain 84532 --to 0x000000000000000000000000000000000000dEaD --value 1000000000000000 --reason "send 0.001 ETH on base sepolia"
|
|
26
28
|
|
|
27
29
|
# 4. Check status anytime
|
|
28
30
|
clanker-wallet status
|
|
@@ -41,33 +43,36 @@ clanker-wallet pair '<pairing JSON from web app>'
|
|
|
41
43
|
|
|
42
44
|
### `tx`
|
|
43
45
|
|
|
44
|
-
Send a transaction request. Blocks until the human approves or rejects.
|
|
46
|
+
Send a transaction request. Blocks until the human approves or rejects, for up to 1 hour by default; `--timeout <ms>` changes that.
|
|
45
47
|
|
|
46
48
|
```bash
|
|
47
49
|
clanker-wallet tx \
|
|
48
|
-
--chain
|
|
49
|
-
--to
|
|
50
|
-
--value
|
|
50
|
+
--chain 84532 \
|
|
51
|
+
--to 0x000000000000000000000000000000000000dEaD \
|
|
52
|
+
--value 1000000000000000 \
|
|
51
53
|
--data 0x \
|
|
52
|
-
--reason "
|
|
54
|
+
--reason "send 0.001 ETH on base sepolia" \
|
|
53
55
|
--urgency medium \
|
|
54
|
-
--timeout
|
|
56
|
+
--timeout 600000
|
|
55
57
|
# approved → {"tx_hash":"0x...","status":"approved"}
|
|
56
58
|
# rejected → {"status":"rejected","error":"..."} (exit code 1)
|
|
57
59
|
# timeout → {"status":"timeout","error":"..."} (exit code 2)
|
|
60
|
+
# bad flag → "invalid --to: ..." on stderr, nothing sent (exit code 5)
|
|
58
61
|
```
|
|
59
62
|
|
|
63
|
+
`--to` must be `0x` + 40 hex characters (mixed case must pass the EIP-55 checksum), `--value` a whole number of wei, `--data` even-length hex. See [SKILL.md](./SKILL.md) for ERC-20 examples.
|
|
64
|
+
|
|
60
65
|
**Flags:**
|
|
61
66
|
| Flag | Required | Default | Description |
|
|
62
67
|
|------|----------|---------|-------------|
|
|
63
|
-
| `--chain <id>` | yes | — | Chain ID (1=Ethereum,
|
|
68
|
+
| `--chain <id>` | yes | — | Chain ID (1=Ethereum, 8453=Base, 84532=Base Sepolia) |
|
|
64
69
|
| `--to <address>` | yes | — | Recipient address |
|
|
65
70
|
| `--value <wei>` | no | `0` | Value in wei (1 ETH = `1000000000000000000`) |
|
|
66
71
|
| `--data <hex>` | no | `0x` | Calldata hex |
|
|
67
72
|
| `--reason <text>` | no | — | Shown to human |
|
|
68
73
|
| `--urgency <level>` | no | — | `low`, `medium`, or `high` |
|
|
69
74
|
| `--expected-outcome <text>` | no | — | What will happen |
|
|
70
|
-
| `--timeout <ms>` | no | `3600000` | How long
|
|
75
|
+
| `--timeout <ms>` | no | `3600000` (1 h) | How long `tx` blocks waiting for the human |
|
|
71
76
|
|
|
72
77
|
### `status`
|
|
73
78
|
|
|
@@ -80,11 +85,11 @@ clanker-wallet status
|
|
|
80
85
|
|
|
81
86
|
### `whoami`
|
|
82
87
|
|
|
83
|
-
Show agent identity.
|
|
88
|
+
Show agent identity. `fingerprint` (first 16 hex chars of the public key) is what the web app shows for this agent; the human compares it before trusting the agent.
|
|
84
89
|
|
|
85
90
|
```bash
|
|
86
91
|
clanker-wallet whoami
|
|
87
|
-
# → {"agent_id":"agent_abc123","public_key":"
|
|
92
|
+
# → {"agent_id":"agent_abc123","public_key":"ab12cd34ef567890...","fingerprint":"ab12 cd34 ef56 7890"}
|
|
88
93
|
```
|
|
89
94
|
|
|
90
95
|
## Exit Codes
|
|
@@ -96,7 +101,7 @@ clanker-wallet whoami
|
|
|
96
101
|
| 2 | Timeout |
|
|
97
102
|
| 3 | Connection/relay error |
|
|
98
103
|
| 4 | Not paired |
|
|
99
|
-
| 5 | Invalid arguments |
|
|
104
|
+
| 5 | Invalid arguments (bad or missing flag; nothing sent) |
|
|
100
105
|
|
|
101
106
|
## State
|
|
102
107
|
|
|
@@ -118,6 +123,7 @@ Identity and pairing are persisted in `~/.clanker-wallet/`:
|
|
|
118
123
|
| Base | 8453 |
|
|
119
124
|
| Optimism | 10 |
|
|
120
125
|
| Sepolia (testnet) | 11155111 |
|
|
126
|
+
| Base Sepolia (testnet) | 84532 |
|
|
121
127
|
|
|
122
128
|
---
|
|
123
129
|
|
|
@@ -135,17 +141,22 @@ import { ClankerWallet } from 'clanker-wallet'
|
|
|
135
141
|
const wallet = new ClankerWallet({ agentName: 'my-bot' })
|
|
136
142
|
wallet.pair(pairingJson)
|
|
137
143
|
|
|
144
|
+
// 0.001 ETH on Base Sepolia; value is wei (string or bigint)
|
|
138
145
|
const txHash = await wallet.requestTx({
|
|
139
|
-
chain_id:
|
|
140
|
-
transaction: { to: '
|
|
141
|
-
context: { reason: '
|
|
146
|
+
chain_id: 84532,
|
|
147
|
+
transaction: { to: '0x000000000000000000000000000000000000dEaD', value: '1000000000000000', data: '0x' },
|
|
148
|
+
context: { reason: 'send 0.001 ETH on base sepolia', urgency: 'medium' },
|
|
142
149
|
})
|
|
143
150
|
```
|
|
144
151
|
|
|
152
|
+
`requestTx` throws `InvalidTxError` (with `field` and `reason`) before anything is sent if `chain_id`, `to`, `value` or `data` is malformed.
|
|
153
|
+
|
|
145
154
|
See full API: [`src/`](./src/)
|
|
146
155
|
|
|
147
156
|
## Python SDK
|
|
148
157
|
|
|
158
|
+
Requires Python 3.10+.
|
|
159
|
+
|
|
149
160
|
```bash
|
|
150
161
|
pip install clanker-wallet
|
|
151
162
|
```
|
|
@@ -157,21 +168,21 @@ agent = ClankerWallet()
|
|
|
157
168
|
agent.pair(pairing_json)
|
|
158
169
|
|
|
159
170
|
tx_hash = await agent.request_tx(RequestTxOptions(
|
|
160
|
-
chain_id=
|
|
161
|
-
transaction=Transaction(to="
|
|
162
|
-
context=TxContext(reason="
|
|
171
|
+
chain_id=84532, # base sepolia
|
|
172
|
+
transaction=Transaction(to="0x000000000000000000000000000000000000dEaD", value="1000000000000000", data="0x"),
|
|
173
|
+
context=TxContext(reason="send 0.001 ETH on base sepolia"),
|
|
163
174
|
))
|
|
164
175
|
```
|
|
165
176
|
|
|
166
177
|
## How It Works
|
|
167
178
|
|
|
168
|
-
1. Human opens [clanker-wallet.xyz](https://clanker-wallet.xyz), connects wallet, gets a pairing
|
|
169
|
-
2. Agent runs `clanker-wallet pair` — establishes E2E encrypted channel
|
|
179
|
+
1. Human opens [clanker-wallet.xyz](https://clanker-wallet.xyz), connects wallet, clicks "copy prompt for agent" — gets a paste-ready prompt that bundles intent, the setup guide URL, and the pairing JSON
|
|
180
|
+
2. Human pastes the prompt to their AI agent. Agent extracts the JSON and runs `clanker-wallet pair` — establishes E2E encrypted channel
|
|
170
181
|
3. Agent runs `clanker-wallet tx` — encrypts tx with nacl.box, sends via WebSocket relay
|
|
171
|
-
4. Human reviews tx details, simulation, risk flags in the web app
|
|
182
|
+
4. Human reviews tx details, simulation, risk flags in the web app. A new agent shows up as unverified with its fingerprint; auto-approve rules only apply after the human trusts it. Contract rules (address + methods) don't limit token amounts or recipients
|
|
172
183
|
5. Human approves → wallet signs + broadcasts → agent gets the tx hash
|
|
173
184
|
|
|
174
|
-
|
|
185
|
+
Request and response payloads are encrypted end-to-end; the relay only forwards them. Exception: when paired in managed mode, the agent also sends the transaction fields (from, to, value, data, chainId) to the relay for simulation.
|
|
175
186
|
|
|
176
187
|
## License
|
|
177
188
|
|
package/SKILL.md
CHANGED
|
@@ -18,22 +18,35 @@ Or use without installing: `npx clanker-wallet` / `bunx clanker-wallet`
|
|
|
18
18
|
|
|
19
19
|
## Pairing (one-time)
|
|
20
20
|
|
|
21
|
-
Before you can send transactions,
|
|
21
|
+
Before you can send transactions, check if you're already paired:
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
The human will give you a JSON string like:
|
|
26
|
-
```json
|
|
27
|
-
{"version":1,"pubkey":"ab12cd...","relay":"wss://clanker-wallet-relay-625052130878.us-east1.run.app/ws","wallet":"0x..."}
|
|
23
|
+
```bash
|
|
24
|
+
clanker-wallet status
|
|
28
25
|
```
|
|
29
26
|
|
|
30
|
-
|
|
27
|
+
If `paired: true`, skip pairing.
|
|
28
|
+
|
|
29
|
+
The human will paste a prompt block from https://clanker-wallet.xyz that looks like this:
|
|
30
|
+
|
|
31
|
+
> read the setup guide first: https://clanker-wallet.xyz/llms.txt
|
|
32
|
+
>
|
|
33
|
+
> then pair using this code:
|
|
34
|
+
> `{"version":1,"pubkey":"ab12cd...","relay":"wss://...","wallet":"0x..."}`
|
|
35
|
+
|
|
36
|
+
Extract the JSON object (the line starting with `{`) and pair:
|
|
37
|
+
|
|
31
38
|
```bash
|
|
32
39
|
clanker-wallet pair '{"version":1,"pubkey":"ab12...","relay":"wss://...","wallet":"0x..."}'
|
|
33
40
|
```
|
|
34
41
|
|
|
42
|
+
If the human only pastes raw JSON (no prompt prose), pair with it directly. If they only paste the URL, ask them to click "copy prompt for agent" on the pairing screen.
|
|
43
|
+
|
|
35
44
|
This saves identity and pairing to `~/.clanker-wallet/`. You only need to do this once.
|
|
36
45
|
|
|
46
|
+
### Trust (first request)
|
|
47
|
+
|
|
48
|
+
The web app lists a new agent as unverified and shows its fingerprint (e.g. `ab12 cd34 ef56 7890`). Until the human compares it with the `fingerprint` from `clanker-wallet whoami` and clicks "trust this agent", none of their auto-approve rules apply: every tx waits for manual approval. Contract rules (contract address + allowed methods) do not limit token amounts or recipients: a rule allowing `transfer` on a token auto-approves any amount to any recipient. If asked, run `clanker-wallet whoami` and share the fingerprint.
|
|
49
|
+
|
|
37
50
|
### Transaction notifications
|
|
38
51
|
|
|
39
52
|
After pairing, ask the human:
|
|
@@ -48,7 +61,7 @@ If they agree, **every time you send a `tx` command**, immediately send a messag
|
|
|
48
61
|
|
|
49
62
|
Example notification:
|
|
50
63
|
> Transaction awaiting your approval
|
|
51
|
-
> I'm requesting to send 0.001 ETH on Sepolia to a test address.
|
|
64
|
+
> I'm requesting to send 0.001 ETH on Base Sepolia to a test address.
|
|
52
65
|
> Urgency: normal
|
|
53
66
|
> [Approve or reject here](https://clanker-wallet.xyz)
|
|
54
67
|
|
|
@@ -56,37 +69,41 @@ Example notification:
|
|
|
56
69
|
|
|
57
70
|
```bash
|
|
58
71
|
clanker-wallet tx \
|
|
59
|
-
--chain
|
|
60
|
-
--to
|
|
72
|
+
--chain 84532 \
|
|
73
|
+
--to 0x000000000000000000000000000000000000dEaD \
|
|
61
74
|
--value 1000000000000000 \
|
|
62
|
-
--reason "Send 0.001 ETH to
|
|
75
|
+
--reason "Send 0.001 ETH on Base Sepolia to a test address" \
|
|
63
76
|
--urgency medium \
|
|
64
|
-
--timeout
|
|
77
|
+
--timeout 600000
|
|
65
78
|
```
|
|
66
79
|
|
|
67
|
-
|
|
80
|
+
This sends 0.001 ETH (`1000000000000000` wei) on Base Sepolia (chain `84532`) to the burn address `0x000000000000000000000000000000000000dEaD`.
|
|
81
|
+
|
|
82
|
+
The command **blocks until the human approves or rejects**, for up to 1 hour by default. Pass `--timeout <ms>` to wait a different time (`600000` = 10 minutes). Output is JSON:
|
|
68
83
|
- Approved: `{"tx_hash":"0x...","status":"approved"}` (exit code 0)
|
|
69
84
|
- Rejected: `{"status":"rejected","error":"..."}` (exit code 1)
|
|
70
85
|
- Timeout: `{"status":"timeout","error":"..."}` (exit code 2)
|
|
86
|
+
- Invalid `--chain`, `--to`, `--value`, `--data` or `--timeout`: a one-line `invalid --<flag>: ...` message on stderr and nothing is sent (exit code 5)
|
|
71
87
|
|
|
72
88
|
## Critical Details
|
|
73
89
|
|
|
74
|
-
- **value is in WEI, not ETH.** 1 ETH = `1000000000000000000`.
|
|
75
|
-
- **data is hex-encoded calldata.** Use `0x` for simple ETH transfers. For contract calls, encode the function call.
|
|
90
|
+
- **value is in WEI, not ETH.** 1 ETH = `1000000000000000000`. A whole number: no decimals, no `1e18`.
|
|
91
|
+
- **data is hex-encoded calldata.** Use `0x` for simple ETH transfers. For contract calls, encode the function call (see ERC-20 transfer below). Never put placeholder text inside the hex.
|
|
92
|
+
- **to must be a real address**: `0x` + 40 hex characters. Mixed-case addresses must carry a valid EIP-55 checksum.
|
|
76
93
|
- **The human must be online to approve.** Set a reasonable `--timeout`. Always notify the human through your communication channel when you send a request (if they opted in).
|
|
77
94
|
|
|
78
95
|
## tx Flags
|
|
79
96
|
|
|
80
97
|
| Flag | Required | Default | Description |
|
|
81
98
|
|------|----------|---------|-------------|
|
|
82
|
-
| `--chain <id>` | yes | — | Chain ID (1=Ethereum,
|
|
99
|
+
| `--chain <id>` | yes | — | Chain ID (1=Ethereum, 8453=Base, 84532=Base Sepolia) |
|
|
83
100
|
| `--to <address>` | yes | — | Recipient address |
|
|
84
101
|
| `--value <wei>` | no | `0` | Value in wei |
|
|
85
102
|
| `--data <hex>` | no | `0x` | Calldata hex |
|
|
86
103
|
| `--reason <text>` | no | — | Shown to human |
|
|
87
104
|
| `--urgency <level>` | no | — | `low`, `medium`, or `high` |
|
|
88
105
|
| `--expected-outcome <text>` | no | — | What will happen |
|
|
89
|
-
| `--timeout <ms>` | no | `3600000` | How long
|
|
106
|
+
| `--timeout <ms>` | no | `3600000` (1 h) | How long `tx` blocks waiting for the human |
|
|
90
107
|
|
|
91
108
|
## Common Chain IDs
|
|
92
109
|
|
|
@@ -98,33 +115,37 @@ The command **blocks until the human approves or rejects**. Output is JSON:
|
|
|
98
115
|
| Base | 8453 |
|
|
99
116
|
| Optimism | 10 |
|
|
100
117
|
| Sepolia (testnet) | 11155111 |
|
|
118
|
+
| Base Sepolia (testnet) | 84532 |
|
|
101
119
|
|
|
102
120
|
## Common Patterns
|
|
103
121
|
|
|
104
122
|
### Simple ETH transfer
|
|
105
123
|
```bash
|
|
106
|
-
clanker-wallet tx --chain
|
|
124
|
+
clanker-wallet tx --chain 84532 --to 0x000000000000000000000000000000000000dEaD --value 1000000000000000 --reason "Send 0.001 ETH"
|
|
107
125
|
```
|
|
108
126
|
|
|
109
127
|
### ERC-20 token transfer
|
|
128
|
+
Calldata is the 4-byte selector followed by each argument as a 32-byte word: the address left-padded with zeros, the amount as big-endian hex in the token's smallest unit. This sends 0.001 WETH (`1000000000000000` = `0x38d7ea4c68000`, 18 decimals) on Base Sepolia, where WETH is `0x4200000000000000000000000000000000000006`:
|
|
129
|
+
|
|
110
130
|
```bash
|
|
111
131
|
# transfer(address to, uint256 amount) — selector 0xa9059cbb
|
|
112
|
-
# encode to + amount as 32-byte padded hex
|
|
113
132
|
clanker-wallet tx \
|
|
114
|
-
--chain
|
|
115
|
-
--to
|
|
116
|
-
--data
|
|
117
|
-
--reason "Transfer
|
|
133
|
+
--chain 84532 \
|
|
134
|
+
--to 0x4200000000000000000000000000000000000006 \
|
|
135
|
+
--data 0xa9059cbb000000000000000000000000000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000038d7ea4c68000 \
|
|
136
|
+
--reason "Transfer 0.001 WETH to a test address"
|
|
118
137
|
```
|
|
119
138
|
|
|
139
|
+
Encode it with a library rather than by hand, e.g. `cast calldata "transfer(address,uint256)" 0x000000000000000000000000000000000000dEaD 1000000000000000` (foundry) or viem `encodeFunctionData({ abi: erc20Abi, functionName: 'transfer', args: [to, amount] })`.
|
|
140
|
+
|
|
120
141
|
### ERC-20 approval
|
|
121
142
|
```bash
|
|
122
143
|
# approve(address spender, uint256 amount) — selector 0x095ea7b3
|
|
123
144
|
clanker-wallet tx \
|
|
124
|
-
--chain
|
|
125
|
-
--to
|
|
126
|
-
--data
|
|
127
|
-
--reason "Approve
|
|
145
|
+
--chain 84532 \
|
|
146
|
+
--to 0x4200000000000000000000000000000000000006 \
|
|
147
|
+
--data 0x095ea7b3000000000000000000000000000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000038d7ea4c68000 \
|
|
148
|
+
--reason "Approve a test address to spend 0.001 WETH"
|
|
128
149
|
```
|
|
129
150
|
|
|
130
151
|
## Other Commands
|
|
@@ -136,11 +157,12 @@ clanker-wallet status
|
|
|
136
157
|
|
|
137
158
|
# Show agent identity
|
|
138
159
|
clanker-wallet whoami
|
|
139
|
-
# → {"agent_id":"agent_abc123","public_key":"
|
|
160
|
+
# → {"agent_id":"agent_abc123","public_key":"ab12cd34ef567890...","fingerprint":"ab12 cd34 ef56 7890"}
|
|
140
161
|
```
|
|
141
162
|
|
|
142
163
|
## Troubleshooting
|
|
143
164
|
|
|
165
|
+
- **exit code 5 / "invalid --flag: ..."** — fix the named flag; nothing was sent. Usually a placeholder `--to`, a `--value` in ETH instead of wei, or `--data` that isn't even-length hex
|
|
144
166
|
- **exit code 4 / "not paired"** — run `clanker-wallet pair '<JSON>'` first
|
|
145
167
|
- **exit code 2 / timeout** — human didn't respond in time, increase `--timeout` or ask them to check the web app
|
|
146
168
|
- **exit code 1 / "rejected"** — human clicked reject. Explain why the tx is needed and try again if appropriate.
|
package/bin/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { ClankerWallet } from '../dist/index.js'
|
|
3
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'
|
|
2
|
+
import { ClankerWallet, InvalidTxError, validateTx } from '../dist/index.js'
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from 'fs'
|
|
4
4
|
import { join } from 'path'
|
|
5
5
|
import { homedir } from 'os'
|
|
6
6
|
|
|
@@ -10,10 +10,15 @@ const IDENTITY_FILE = join(STATE_DIR, 'identity.json')
|
|
|
10
10
|
const PAIRING_FILE = join(STATE_DIR, 'pairing.json')
|
|
11
11
|
|
|
12
12
|
const EXIT = { OK: 0, REJECTED: 1, TIMEOUT: 2, CONNECTION: 3, NOT_PAIRED: 4, BAD_ARGS: 5 }
|
|
13
|
+
const DEFAULT_TIMEOUT_MS = 3_600_000
|
|
14
|
+
/** validateTx field -> the tx flag that sets it */
|
|
15
|
+
const FIELD_FLAGS = { chain_id: '--chain', to: '--to', value: '--value', data: '--data' }
|
|
13
16
|
|
|
14
17
|
// ── helpers ──────────────────────────────────────────────────────────
|
|
15
18
|
const hex = (bytes) => Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('')
|
|
16
19
|
const fromHexStr = (h) => new Uint8Array(h.match(/.{2}/g).map(b => parseInt(b, 16)))
|
|
20
|
+
/** short id the human compares in the web app: first 16 hex (64 bits) of the x25519 pubkey, "ab12 cd34 ef56 7890" */
|
|
21
|
+
const fingerprint = (pubkeyHex) => pubkeyHex.slice(0, 16).toLowerCase().match(/.{1,4}/g).join(' ')
|
|
17
22
|
|
|
18
23
|
function die(msg, code) {
|
|
19
24
|
process.stderr.write(msg + '\n')
|
|
@@ -24,19 +29,25 @@ function out(obj) {
|
|
|
24
29
|
process.stdout.write(JSON.stringify(obj) + '\n')
|
|
25
30
|
}
|
|
26
31
|
|
|
32
|
+
// owner-only; an existing dir from an older version is tightened too
|
|
27
33
|
function ensureDir() {
|
|
28
|
-
if (!existsSync(STATE_DIR)) mkdirSync(STATE_DIR, { recursive: true })
|
|
34
|
+
if (!existsSync(STATE_DIR)) mkdirSync(STATE_DIR, { recursive: true, mode: 0o700 })
|
|
35
|
+
else chmodSync(STATE_DIR, 0o700)
|
|
29
36
|
}
|
|
30
37
|
|
|
38
|
+
// identity.json holds the agent secret key: owner-only, tightened if an older version left it readable
|
|
31
39
|
function loadIdentity() {
|
|
32
40
|
if (!existsSync(IDENTITY_FILE)) return null
|
|
41
|
+
ensureDir()
|
|
42
|
+
chmodSync(IDENTITY_FILE, 0o600)
|
|
33
43
|
try { return JSON.parse(readFileSync(IDENTITY_FILE, 'utf8')) }
|
|
34
44
|
catch { return null }
|
|
35
45
|
}
|
|
36
46
|
|
|
37
47
|
function saveIdentity(data) {
|
|
38
48
|
ensureDir()
|
|
39
|
-
writeFileSync(IDENTITY_FILE, JSON.stringify(data, null, 2) + '\n')
|
|
49
|
+
writeFileSync(IDENTITY_FILE, JSON.stringify(data, null, 2) + '\n', { mode: 0o600 })
|
|
50
|
+
chmodSync(IDENTITY_FILE, 0o600)
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
function loadPairing() {
|
|
@@ -127,12 +138,20 @@ async function cmdTx(flags) {
|
|
|
127
138
|
if (!chain) die('missing --chain', EXIT.BAD_ARGS)
|
|
128
139
|
if (!to) die('missing --to', EXIT.BAD_ARGS)
|
|
129
140
|
|
|
130
|
-
|
|
131
|
-
if (
|
|
141
|
+
if (!/^[0-9]+$/.test(chain)) die(`invalid --chain: must be a positive integer chain id, got ${JSON.stringify(chain)}`, EXIT.BAD_ARGS)
|
|
142
|
+
if (flags.timeout !== undefined && !/^[1-9][0-9]*$/.test(flags.timeout)) {
|
|
143
|
+
die(`invalid --timeout: must be a positive number of milliseconds, got ${JSON.stringify(flags.timeout)}`, EXIT.BAD_ARGS)
|
|
144
|
+
}
|
|
145
|
+
const timeout = flags.timeout ? Number(flags.timeout) : DEFAULT_TIMEOUT_MS
|
|
132
146
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
147
|
+
// same checks requestTx runs, done here so a bad flag exits 5 instead of surfacing as a connection error
|
|
148
|
+
let request
|
|
149
|
+
try {
|
|
150
|
+
request = validateTx(Number(chain), { to, value: flags.value ?? '0', data: flags.data ?? '0x' })
|
|
151
|
+
} catch (err) {
|
|
152
|
+
if (!(err instanceof InvalidTxError)) throw err
|
|
153
|
+
die(`invalid ${FIELD_FLAGS[err.field]}: ${err.reason}`, EXIT.BAD_ARGS)
|
|
154
|
+
}
|
|
136
155
|
|
|
137
156
|
const context = {}
|
|
138
157
|
if (flags.reason) context.reason = flags.reason
|
|
@@ -145,13 +164,14 @@ async function cmdTx(flags) {
|
|
|
145
164
|
pubkey: pairing.pubkey,
|
|
146
165
|
relay: pairing.relay,
|
|
147
166
|
wallet: pairing.wallet,
|
|
167
|
+
mode: pairing.mode || undefined,
|
|
148
168
|
})
|
|
149
169
|
wallet.pair(pairingJson)
|
|
150
170
|
|
|
151
171
|
try {
|
|
152
172
|
const txHash = await wallet.requestTx({
|
|
153
|
-
chain_id:
|
|
154
|
-
transaction:
|
|
173
|
+
chain_id: request.chain_id,
|
|
174
|
+
transaction: request.transaction,
|
|
155
175
|
context: Object.keys(context).length > 0 ? context : undefined,
|
|
156
176
|
timeout_ms: timeout,
|
|
157
177
|
})
|
|
@@ -189,9 +209,15 @@ function cmdWhoami() {
|
|
|
189
209
|
out({
|
|
190
210
|
agent_id: wallet.agentId,
|
|
191
211
|
public_key: hex(wallet.publicKey),
|
|
212
|
+
fingerprint: fingerprint(hex(wallet.publicKey)),
|
|
192
213
|
})
|
|
193
214
|
}
|
|
194
215
|
|
|
216
|
+
function cmdVersion() {
|
|
217
|
+
const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
|
|
218
|
+
process.stdout.write(version + '\n')
|
|
219
|
+
}
|
|
220
|
+
|
|
195
221
|
function showHelp() {
|
|
196
222
|
process.stderr.write(`clanker-wallet — human-approved transactions for AI agents
|
|
197
223
|
|
|
@@ -202,14 +228,14 @@ commands:
|
|
|
202
228
|
whoami show agent identity
|
|
203
229
|
|
|
204
230
|
tx flags:
|
|
205
|
-
--chain <id>
|
|
206
|
-
--to <address>
|
|
207
|
-
--value <wei>
|
|
208
|
-
--data <hex>
|
|
209
|
-
--reason <text>
|
|
210
|
-
--urgency <low|
|
|
211
|
-
--expected-outcome <text>
|
|
212
|
-
--timeout <ms>
|
|
231
|
+
--chain <id> chain ID (required) — 1, 8453, 84532 (base sepolia), etc.
|
|
232
|
+
--to <address> recipient (required)
|
|
233
|
+
--value <wei> value in wei, not ETH (default: 0; 1 ETH = 1000000000000000000)
|
|
234
|
+
--data <hex> calldata (default: 0x)
|
|
235
|
+
--reason <text> shown to human
|
|
236
|
+
--urgency <low|medium|high> priority hint
|
|
237
|
+
--expected-outcome <text> what will happen
|
|
238
|
+
--timeout <ms> how long tx blocks waiting for the human (default: 3600000 = 1 h)
|
|
213
239
|
|
|
214
240
|
exit codes:
|
|
215
241
|
0 success
|
|
@@ -221,11 +247,11 @@ exit codes:
|
|
|
221
247
|
|
|
222
248
|
examples:
|
|
223
249
|
clanker-wallet pair '{"version":1,"pubkey":"ab12...","relay":"wss://...","wallet":"0x..."}'
|
|
224
|
-
clanker-wallet tx --chain
|
|
250
|
+
clanker-wallet tx --chain 84532 --to 0x000000000000000000000000000000000000dEaD --value 1000000000000000 --reason "send 0.001 ETH on base sepolia"
|
|
225
251
|
clanker-wallet status
|
|
226
252
|
clanker-wallet whoami
|
|
227
253
|
|
|
228
|
-
docs: https://
|
|
254
|
+
docs: https://clanker-wallet.xyz/SKILL.md
|
|
229
255
|
web: https://clanker-wallet.xyz
|
|
230
256
|
`)
|
|
231
257
|
}
|
|
@@ -246,6 +272,10 @@ switch (cmd) {
|
|
|
246
272
|
case 'whoami':
|
|
247
273
|
cmdWhoami()
|
|
248
274
|
break
|
|
275
|
+
case '--version':
|
|
276
|
+
case '-v':
|
|
277
|
+
cmdVersion()
|
|
278
|
+
break
|
|
249
279
|
case 'help':
|
|
250
280
|
case '--help':
|
|
251
281
|
case '-h':
|
package/dist/client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { decryptMessage, encryptMessage, fromHex, generateKeypair, keypairFromSecret, signMessage, signingKeypairFromSeed, toHex } from './crypto.js';
|
|
2
2
|
import { TypedEmitter } from './events.js';
|
|
3
|
+
import { validateTx } from './validate.js';
|
|
3
4
|
const DEFAULT_TIMEOUT_MS = 3_600_000; // 1 hour
|
|
4
5
|
export class ClankerWallet extends TypedEmitter {
|
|
5
6
|
publicKey;
|
|
@@ -43,6 +44,7 @@ export class ClankerWallet extends TypedEmitter {
|
|
|
43
44
|
humanPubkeyHex: data.pubkey,
|
|
44
45
|
relayUrl: data.relay,
|
|
45
46
|
humanWallet: data.wallet || null,
|
|
47
|
+
mode: data.mode || 'encrypted',
|
|
46
48
|
};
|
|
47
49
|
this.emit('paired', { relay: data.relay, wallet: data.wallet || '' });
|
|
48
50
|
return data;
|
|
@@ -56,15 +58,17 @@ export class ClankerWallet extends TypedEmitter {
|
|
|
56
58
|
if (!this.pairing) {
|
|
57
59
|
throw new Error('not paired — call pair() first');
|
|
58
60
|
}
|
|
59
|
-
|
|
61
|
+
// reject malformed requests before anything reaches the relay
|
|
62
|
+
const { chain_id, transaction } = validateTx(options.chain_id, options.transaction);
|
|
63
|
+
const { humanPubkey, humanPubkeyHex, relayUrl, humanWallet, mode } = this.pairing;
|
|
60
64
|
const timeoutMs = options.timeout_ms ?? DEFAULT_TIMEOUT_MS;
|
|
61
65
|
const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
62
66
|
const txRequest = {
|
|
63
67
|
request_id: requestId,
|
|
64
68
|
agent_id: this.agentId,
|
|
65
69
|
timestamp: new Date().toISOString(),
|
|
66
|
-
chain_id
|
|
67
|
-
transaction
|
|
70
|
+
chain_id,
|
|
71
|
+
transaction,
|
|
68
72
|
context: options.context,
|
|
69
73
|
signature: '', // set below after signing
|
|
70
74
|
};
|
|
@@ -130,18 +134,32 @@ export class ClankerWallet extends TypedEmitter {
|
|
|
130
134
|
ciphertext,
|
|
131
135
|
signing_pubkey: this.signingPubkeyHex,
|
|
132
136
|
};
|
|
133
|
-
|
|
137
|
+
const wsMsg = {
|
|
134
138
|
type: 'message',
|
|
135
139
|
room: humanPubkeyHex,
|
|
136
140
|
payload: JSON.stringify(envelope),
|
|
137
|
-
}
|
|
141
|
+
};
|
|
142
|
+
// managed mode: relay gets plaintext tx params so it can simulate
|
|
143
|
+
if (mode === 'managed') {
|
|
144
|
+
wsMsg.simulate = {
|
|
145
|
+
from: humanWallet || '',
|
|
146
|
+
to: transaction.to,
|
|
147
|
+
value: transaction.value,
|
|
148
|
+
data: transaction.data,
|
|
149
|
+
chainId: chain_id,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
ws.send(JSON.stringify(wsMsg));
|
|
138
153
|
this.emit('request_sent', { request_id: requestId });
|
|
139
154
|
return;
|
|
140
155
|
}
|
|
141
156
|
if (msg.type === 'message') {
|
|
142
157
|
try {
|
|
143
158
|
const envelope = JSON.parse(msg.payload);
|
|
144
|
-
|
|
159
|
+
// only the paired human may answer
|
|
160
|
+
if (envelope.sender_pubkey?.toLowerCase() !== humanPubkeyHex.toLowerCase())
|
|
161
|
+
return;
|
|
162
|
+
const plaintext = decryptMessage(envelope.ciphertext, humanPubkey, this.secretKey);
|
|
145
163
|
if (!plaintext)
|
|
146
164
|
return;
|
|
147
165
|
const response = JSON.parse(plaintext);
|
package/dist/crypto.js
CHANGED
|
@@ -20,7 +20,13 @@ export function encryptMessage(message, recipientPublicKey, senderSecretKey) {
|
|
|
20
20
|
}
|
|
21
21
|
/** decrypt a nacl.box message. payload = base64(nonce + ciphertext). */
|
|
22
22
|
export function decryptMessage(base64Payload, senderPublicKey, recipientSecretKey) {
|
|
23
|
-
|
|
23
|
+
let raw;
|
|
24
|
+
try {
|
|
25
|
+
raw = base64ToBytes(base64Payload);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
24
30
|
if (raw.length <= nacl.box.nonceLength)
|
|
25
31
|
return null;
|
|
26
32
|
const nonce = raw.slice(0, nacl.box.nonceLength);
|
|
@@ -38,7 +44,13 @@ export function toHex(bytes) {
|
|
|
38
44
|
}
|
|
39
45
|
/** hex string → Uint8Array (strips 0x prefix if present) */
|
|
40
46
|
export function fromHex(hex) {
|
|
41
|
-
const clean = hex.startsWith('0x') ? hex.slice(2) : hex;
|
|
47
|
+
const clean = hex.startsWith('0x') || hex.startsWith('0X') ? hex.slice(2) : hex;
|
|
48
|
+
if (clean.length % 2 !== 0) {
|
|
49
|
+
throw new Error('invalid hex string: length must be even');
|
|
50
|
+
}
|
|
51
|
+
if (!/^[0-9a-fA-F]*$/.test(clean)) {
|
|
52
|
+
throw new Error('invalid hex string: contains non-hex characters');
|
|
53
|
+
}
|
|
42
54
|
const bytes = new Uint8Array(clean.length / 2);
|
|
43
55
|
for (let i = 0; i < bytes.length; i++) {
|
|
44
56
|
bytes[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
|
|
@@ -47,7 +59,11 @@ export function fromHex(hex) {
|
|
|
47
59
|
}
|
|
48
60
|
/** derive an Ed25519 signing keypair from a 32-byte seed */
|
|
49
61
|
export function signingKeypairFromSeed(seed) {
|
|
50
|
-
|
|
62
|
+
if (seed.length !== nacl.sign.seedLength) {
|
|
63
|
+
throw new Error(`invalid seed length: expected ${nacl.sign.seedLength} bytes, got ${seed.length}`);
|
|
64
|
+
}
|
|
65
|
+
// copy to keep derivation independent of caller-owned mutable buffers
|
|
66
|
+
return nacl.sign.keyPair.fromSeed(new Uint8Array(seed));
|
|
51
67
|
}
|
|
52
68
|
/** sign a message string with an Ed25519 secret key. returns base64(signature). */
|
|
53
69
|
export function signMessage(message, signingSecretKey) {
|
package/dist/index.d.ts
CHANGED
|
@@ -3,4 +3,6 @@ export type { ClankerWalletOptions } from './client.js';
|
|
|
3
3
|
export { base64ToBytes, bytesToBase64, decryptMessage, encryptMessage, fromHex, generateKeypair, keypairFromSecret, signMessage, signingKeypairFromSeed, toHex, verifySignature } from './crypto.js';
|
|
4
4
|
export { TypedEmitter } from './events.js';
|
|
5
5
|
export type { EventMap } from './events.js';
|
|
6
|
-
export
|
|
6
|
+
export { InvalidTxError, isChecksumAddress, validateTx } from './validate.js';
|
|
7
|
+
export type { TxField } from './validate.js';
|
|
8
|
+
export type { AgentEnvelope, PairingData, RequestTxOptions, Transaction, TxContext, TxInput, TxRequest, TxResponse, } from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { ClankerWallet } from './client.js';
|
|
2
2
|
export { base64ToBytes, bytesToBase64, decryptMessage, encryptMessage, fromHex, generateKeypair, keypairFromSecret, signMessage, signingKeypairFromSeed, toHex, verifySignature } from './crypto.js';
|
|
3
3
|
export { TypedEmitter } from './events.js';
|
|
4
|
+
export { InvalidTxError, isChecksumAddress, validateTx } from './validate.js';
|
package/dist/keccak.d.ts
ADDED
package/dist/keccak.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* keccak-256 (the pre-NIST padding ethereum uses; NIST sha3-256 differs), only for
|
|
3
|
+
* EIP-55 address checksums. inputs are a few dozen bytes, so lanes are plain bigints.
|
|
4
|
+
*/
|
|
5
|
+
const MASK64 = (1n << 64n) - 1n;
|
|
6
|
+
const RATE_BYTES = 136;
|
|
7
|
+
const rotl = (x, n) => n === 0 ? x : ((x << BigInt(n)) | (x >> BigInt(64 - n))) & MASK64;
|
|
8
|
+
// round constants from the degree-8 LFSR in the keccak reference
|
|
9
|
+
const ROUND_CONSTANTS = (() => {
|
|
10
|
+
const out = [];
|
|
11
|
+
let r = 1;
|
|
12
|
+
for (let round = 0; round < 24; round++) {
|
|
13
|
+
let rc = 0n;
|
|
14
|
+
for (let j = 0; j < 7; j++) {
|
|
15
|
+
if (r & 1)
|
|
16
|
+
rc |= 1n << BigInt((1 << j) - 1);
|
|
17
|
+
r = r & 0x80 ? ((r << 1) ^ 0x71) & 0xff : (r << 1) & 0xff;
|
|
18
|
+
}
|
|
19
|
+
out.push(rc);
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
})();
|
|
23
|
+
// rho offsets, indexed by x + 5y
|
|
24
|
+
const ROTATIONS = (() => {
|
|
25
|
+
const out = new Array(25).fill(0);
|
|
26
|
+
let x = 1;
|
|
27
|
+
let y = 0;
|
|
28
|
+
for (let t = 0; t < 24; t++) {
|
|
29
|
+
out[x + 5 * y] = (((t + 1) * (t + 2)) / 2) % 64;
|
|
30
|
+
[x, y] = [y, (2 * x + 3 * y) % 5];
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
})();
|
|
34
|
+
function keccakF(a) {
|
|
35
|
+
const b = new Array(25);
|
|
36
|
+
const c = new Array(5);
|
|
37
|
+
for (let round = 0; round < 24; round++) {
|
|
38
|
+
// theta
|
|
39
|
+
for (let x = 0; x < 5; x++)
|
|
40
|
+
c[x] = a[x] ^ a[x + 5] ^ a[x + 10] ^ a[x + 15] ^ a[x + 20];
|
|
41
|
+
for (let x = 0; x < 5; x++) {
|
|
42
|
+
const d = c[(x + 4) % 5] ^ rotl(c[(x + 1) % 5], 1);
|
|
43
|
+
for (let y = 0; y < 25; y += 5)
|
|
44
|
+
a[x + y] ^= d;
|
|
45
|
+
}
|
|
46
|
+
// rho + pi
|
|
47
|
+
for (let x = 0; x < 5; x++) {
|
|
48
|
+
for (let y = 0; y < 5; y++) {
|
|
49
|
+
b[y + 5 * ((2 * x + 3 * y) % 5)] = rotl(a[x + 5 * y], ROTATIONS[x + 5 * y]);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// chi
|
|
53
|
+
for (let y = 0; y < 25; y += 5) {
|
|
54
|
+
for (let x = 0; x < 5; x++) {
|
|
55
|
+
a[x + y] = b[x + y] ^ (~b[((x + 1) % 5) + y] & MASK64 & b[((x + 2) % 5) + y]);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// iota
|
|
59
|
+
a[0] ^= ROUND_CONSTANTS[round];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function keccak256(input) {
|
|
63
|
+
// pad10*1 with keccak's 0x01 domain byte
|
|
64
|
+
const padded = new Uint8Array(Math.floor(input.length / RATE_BYTES + 1) * RATE_BYTES);
|
|
65
|
+
padded.set(input);
|
|
66
|
+
padded[input.length] ^= 0x01;
|
|
67
|
+
padded[padded.length - 1] ^= 0x80;
|
|
68
|
+
const state = new Array(25).fill(0n);
|
|
69
|
+
for (let off = 0; off < padded.length; off += RATE_BYTES) {
|
|
70
|
+
for (let i = 0; i < RATE_BYTES / 8; i++) {
|
|
71
|
+
let lane = 0n;
|
|
72
|
+
for (let k = 7; k >= 0; k--)
|
|
73
|
+
lane = (lane << 8n) | BigInt(padded[off + i * 8 + k]);
|
|
74
|
+
state[i] ^= lane;
|
|
75
|
+
}
|
|
76
|
+
keccakF(state);
|
|
77
|
+
}
|
|
78
|
+
const out = new Uint8Array(32);
|
|
79
|
+
for (let i = 0; i < 32; i++)
|
|
80
|
+
out[i] = Number((state[i >> 3] >> BigInt(8 * (i & 7))) & 0xffn);
|
|
81
|
+
return out;
|
|
82
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -4,6 +4,12 @@ export interface Transaction {
|
|
|
4
4
|
value: string;
|
|
5
5
|
data: string;
|
|
6
6
|
}
|
|
7
|
+
/** transaction as passed to requestTx: value in wei, data defaults to "0x" */
|
|
8
|
+
export interface TxInput {
|
|
9
|
+
to: string;
|
|
10
|
+
value: string | bigint;
|
|
11
|
+
data?: string;
|
|
12
|
+
}
|
|
7
13
|
/** optional context about the transaction */
|
|
8
14
|
export interface TxContext {
|
|
9
15
|
reason?: string;
|
|
@@ -40,11 +46,14 @@ export interface PairingData {
|
|
|
40
46
|
pubkey: string;
|
|
41
47
|
relay: string;
|
|
42
48
|
wallet: string;
|
|
49
|
+
/** "encrypted" (default): relay sees only ciphertext. "managed": relay also receives tx params for simulation. */
|
|
50
|
+
mode?: 'encrypted' | 'managed';
|
|
43
51
|
}
|
|
44
52
|
/** options for requestTx */
|
|
45
53
|
export interface RequestTxOptions {
|
|
46
54
|
chain_id: number;
|
|
47
|
-
|
|
55
|
+
/** checked before sending; an invalid field throws InvalidTxError */
|
|
56
|
+
transaction: TxInput;
|
|
48
57
|
context?: TxContext;
|
|
49
58
|
/** timeout in ms (default: 3_600_000 = 1hr) */
|
|
50
59
|
timeout_ms?: number;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Transaction, TxInput } from './types.js';
|
|
2
|
+
export type TxField = 'chain_id' | 'to' | 'value' | 'data';
|
|
3
|
+
/** a tx request field failed validation; nothing was sent */
|
|
4
|
+
export declare class InvalidTxError extends Error {
|
|
5
|
+
readonly field: TxField;
|
|
6
|
+
readonly reason: string;
|
|
7
|
+
constructor(field: TxField, reason: string);
|
|
8
|
+
}
|
|
9
|
+
/** EIP-55: a mixed-case address must carry the checksum; single-case addresses are unchecksummed */
|
|
10
|
+
export declare function isChecksumAddress(address: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* check a tx request before anything is signed or sent, and normalize it to the wire
|
|
13
|
+
* format (value as a decimal wei string, data defaulting to "0x").
|
|
14
|
+
* throws InvalidTxError naming the first bad field.
|
|
15
|
+
*/
|
|
16
|
+
export declare function validateTx(chainId: unknown, tx: TxInput | Record<string, unknown>): {
|
|
17
|
+
chain_id: number;
|
|
18
|
+
transaction: Transaction;
|
|
19
|
+
};
|
package/dist/validate.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { keccak256 } from './keccak.js';
|
|
2
|
+
/** a tx request field failed validation; nothing was sent */
|
|
3
|
+
export class InvalidTxError extends Error {
|
|
4
|
+
field;
|
|
5
|
+
reason;
|
|
6
|
+
constructor(field, reason) {
|
|
7
|
+
super(`invalid ${field}: ${reason}`);
|
|
8
|
+
this.field = field;
|
|
9
|
+
this.reason = reason;
|
|
10
|
+
this.name = 'InvalidTxError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
14
|
+
const DECIMAL_RE = /^[0-9]+$/;
|
|
15
|
+
const CALLDATA_RE = /^0x(?:[0-9a-fA-F]{2})*$/;
|
|
16
|
+
/** short, quoted rendering of a rejected input for error messages */
|
|
17
|
+
function shown(v) {
|
|
18
|
+
const s = typeof v === 'bigint' ? `${v}n` : JSON.stringify(v) ?? String(v);
|
|
19
|
+
return s.length > 70 ? `${s.slice(0, 67)}...` : s;
|
|
20
|
+
}
|
|
21
|
+
/** EIP-55: a mixed-case address must carry the checksum; single-case addresses are unchecksummed */
|
|
22
|
+
export function isChecksumAddress(address) {
|
|
23
|
+
if (!ADDRESS_RE.test(address))
|
|
24
|
+
return false;
|
|
25
|
+
const body = address.slice(2);
|
|
26
|
+
if (body === body.toLowerCase() || body === body.toUpperCase())
|
|
27
|
+
return true;
|
|
28
|
+
const lower = body.toLowerCase();
|
|
29
|
+
const hash = keccak256(new TextEncoder().encode(lower));
|
|
30
|
+
for (let i = 0; i < 40; i++) {
|
|
31
|
+
const nibble = (hash[i >> 1] >> (i % 2 === 0 ? 4 : 0)) & 0xf;
|
|
32
|
+
const expected = nibble >= 8 ? lower[i].toUpperCase() : lower[i];
|
|
33
|
+
if (body[i] !== expected)
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* check a tx request before anything is signed or sent, and normalize it to the wire
|
|
40
|
+
* format (value as a decimal wei string, data defaulting to "0x").
|
|
41
|
+
* throws InvalidTxError naming the first bad field.
|
|
42
|
+
*/
|
|
43
|
+
export function validateTx(chainId, tx) {
|
|
44
|
+
if (typeof chainId !== 'number' || !Number.isSafeInteger(chainId) || chainId <= 0) {
|
|
45
|
+
throw new InvalidTxError('chain_id', `must be a positive integer, got ${shown(chainId)}`);
|
|
46
|
+
}
|
|
47
|
+
const { to, value, data = '0x' } = tx;
|
|
48
|
+
if (typeof to !== 'string' || !ADDRESS_RE.test(to)) {
|
|
49
|
+
throw new InvalidTxError('to', `must be 0x followed by 40 hex characters, got ${shown(to)}`);
|
|
50
|
+
}
|
|
51
|
+
if (!isChecksumAddress(to)) {
|
|
52
|
+
throw new InvalidTxError('to', `mixed-case address fails its EIP-55 checksum, got ${shown(to)}`);
|
|
53
|
+
}
|
|
54
|
+
let wei;
|
|
55
|
+
if (typeof value === 'bigint' && value >= 0n)
|
|
56
|
+
wei = value.toString();
|
|
57
|
+
else if (typeof value === 'string' && DECIMAL_RE.test(value))
|
|
58
|
+
wei = value;
|
|
59
|
+
else
|
|
60
|
+
throw new InvalidTxError('value', `must be a non-negative integer amount of wei (decimal string or bigint), got ${shown(value)}`);
|
|
61
|
+
if (typeof data !== 'string' || !CALLDATA_RE.test(data)) {
|
|
62
|
+
throw new InvalidTxError('data', `must be 0x followed by an even number of hex characters, got ${shown(data)}`);
|
|
63
|
+
}
|
|
64
|
+
return { chain_id: chainId, transaction: { to, value: wei, data } };
|
|
65
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clanker-wallet",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -13,7 +13,13 @@
|
|
|
13
13
|
"bin": {
|
|
14
14
|
"clanker-wallet": "bin/cli.js"
|
|
15
15
|
},
|
|
16
|
-
"files": [
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"bin",
|
|
19
|
+
"SKILL.md",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
17
23
|
"license": "MIT",
|
|
18
24
|
"author": "Clanker Wallet Contributors",
|
|
19
25
|
"repository": {
|
|
@@ -21,7 +27,27 @@
|
|
|
21
27
|
"url": "git+https://github.com/almogdepaz/clanker-wallet.git"
|
|
22
28
|
},
|
|
23
29
|
"homepage": "https://github.com/almogdepaz/clanker-wallet#readme",
|
|
24
|
-
"keywords": [
|
|
30
|
+
"keywords": [
|
|
31
|
+
"wallet",
|
|
32
|
+
"ai-agent",
|
|
33
|
+
"ethereum",
|
|
34
|
+
"e2e-encrypted",
|
|
35
|
+
"transaction-approval",
|
|
36
|
+
"blockchain",
|
|
37
|
+
"web3",
|
|
38
|
+
"langchain",
|
|
39
|
+
"human-in-the-loop",
|
|
40
|
+
"evm",
|
|
41
|
+
"agent-wallet",
|
|
42
|
+
"llm",
|
|
43
|
+
"typescript",
|
|
44
|
+
"base",
|
|
45
|
+
"polygon",
|
|
46
|
+
"transaction",
|
|
47
|
+
"approval",
|
|
48
|
+
"defi",
|
|
49
|
+
"vercel-ai-sdk"
|
|
50
|
+
],
|
|
25
51
|
"scripts": {
|
|
26
52
|
"build": "tsc -p tsconfig.build.json",
|
|
27
53
|
"dev": "tsc --watch",
|