clanker-wallet 0.4.0 → 0.4.2
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 +32 -18
- package/SKILL.md +51 -22
- package/bin/cli.js +40 -19
- package/dist/client.js +21 -7
- 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 +8 -1
- package/dist/validate.d.ts +19 -0
- package/dist/validate.js +65 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,6 +14,8 @@ npx clanker-wallet
|
|
|
14
14
|
bunx clanker-wallet
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
Running in a sandbox (e.g. Claude Code)? Allow a WebSocket to the relay, `wss://relay.clanker-wallet.xyz/ws` (the `relay` URL in the pairing JSON), or `tx` fails with exit code 3. If the sandbox only lets traffic out through `HTTPS_PROXY`, run the CLI as `NODE_USE_ENV_PROXY=1 clanker-wallet ...` (Node 24+); Node ignores the proxy otherwise. `pair` writes `~/.clanker-wallet/` and the other commands write there too, so allow writes (see [State](#state)).
|
|
18
|
+
|
|
17
19
|
## Quick Start
|
|
18
20
|
|
|
19
21
|
```bash
|
|
@@ -22,8 +24,9 @@ bunx clanker-wallet
|
|
|
22
24
|
# 2. Pair (one-time) — extract the JSON from the prompt
|
|
23
25
|
clanker-wallet pair '{"version":1,"pubkey":"ab12...","relay":"wss://...","wallet":"0x..."}'
|
|
24
26
|
|
|
25
|
-
# 3. Send a transaction (blocks until human approves or rejects)
|
|
26
|
-
|
|
27
|
+
# 3. Send a transaction (blocks until human approves or rejects; 1 h by default)
|
|
28
|
+
# 0.001 ETH on Base Sepolia (84532) to the burn address. --value is in wei.
|
|
29
|
+
clanker-wallet tx --chain 84532 --to 0x000000000000000000000000000000000000dEaD --value 1000000000000000 --reason "send 0.001 ETH on base sepolia"
|
|
27
30
|
|
|
28
31
|
# 4. Check status anytime
|
|
29
32
|
clanker-wallet status
|
|
@@ -33,7 +36,9 @@ clanker-wallet status
|
|
|
33
36
|
|
|
34
37
|
### `pair`
|
|
35
38
|
|
|
36
|
-
Pair with a human's web app. Run once — identity and pairing
|
|
39
|
+
Pair with a human's web app. Run once — `pair` writes identity and pairing to `~/.clanker-wallet/`.
|
|
40
|
+
|
|
41
|
+
If the pairing JSON contains `"mode":"managed"`, the relay simulates each transaction for the human's review screen, so `tx` also sends the plain transaction fields to it. Otherwise the relay only sees ciphertext. Pair with the JSON as given.
|
|
37
42
|
|
|
38
43
|
```bash
|
|
39
44
|
clanker-wallet pair '<pairing JSON from web app>'
|
|
@@ -42,33 +47,36 @@ clanker-wallet pair '<pairing JSON from web app>'
|
|
|
42
47
|
|
|
43
48
|
### `tx`
|
|
44
49
|
|
|
45
|
-
Send a transaction request. Blocks until the human approves or rejects.
|
|
50
|
+
Send a transaction request. Blocks until the human approves or rejects, for up to 1 hour by default; `--timeout <ms>` changes that. If your shell tool kills long commands, run `tx` in the background and read its output when it exits.
|
|
46
51
|
|
|
47
52
|
```bash
|
|
48
53
|
clanker-wallet tx \
|
|
49
|
-
--chain
|
|
50
|
-
--to
|
|
51
|
-
--value
|
|
54
|
+
--chain 84532 \
|
|
55
|
+
--to 0x000000000000000000000000000000000000dEaD \
|
|
56
|
+
--value 1000000000000000 \
|
|
52
57
|
--data 0x \
|
|
53
|
-
--reason "
|
|
58
|
+
--reason "send 0.001 ETH on base sepolia" \
|
|
54
59
|
--urgency medium \
|
|
55
|
-
--timeout
|
|
60
|
+
--timeout 600000
|
|
56
61
|
# approved → {"tx_hash":"0x...","status":"approved"}
|
|
57
62
|
# rejected → {"status":"rejected","error":"..."} (exit code 1)
|
|
58
63
|
# timeout → {"status":"timeout","error":"..."} (exit code 2)
|
|
64
|
+
# bad flag → "invalid --to: ..." on stderr, nothing sent (exit code 5)
|
|
59
65
|
```
|
|
60
66
|
|
|
67
|
+
`--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.
|
|
68
|
+
|
|
61
69
|
**Flags:**
|
|
62
70
|
| Flag | Required | Default | Description |
|
|
63
71
|
|------|----------|---------|-------------|
|
|
64
|
-
| `--chain <id>` | yes | — | Chain ID (1=Ethereum,
|
|
72
|
+
| `--chain <id>` | yes | — | Chain ID (1=Ethereum, 8453=Base, 84532=Base Sepolia) |
|
|
65
73
|
| `--to <address>` | yes | — | Recipient address |
|
|
66
74
|
| `--value <wei>` | no | `0` | Value in wei (1 ETH = `1000000000000000000`) |
|
|
67
75
|
| `--data <hex>` | no | `0x` | Calldata hex |
|
|
68
76
|
| `--reason <text>` | no | — | Shown to human |
|
|
69
77
|
| `--urgency <level>` | no | — | `low`, `medium`, or `high` |
|
|
70
78
|
| `--expected-outcome <text>` | no | — | What will happen |
|
|
71
|
-
| `--timeout <ms>` | no | `3600000` | How long
|
|
79
|
+
| `--timeout <ms>` | no | `3600000` (1 h) | How long `tx` blocks waiting for the human |
|
|
72
80
|
|
|
73
81
|
### `status`
|
|
74
82
|
|
|
@@ -97,7 +105,7 @@ clanker-wallet whoami
|
|
|
97
105
|
| 2 | Timeout |
|
|
98
106
|
| 3 | Connection/relay error |
|
|
99
107
|
| 4 | Not paired |
|
|
100
|
-
| 5 | Invalid arguments |
|
|
108
|
+
| 5 | Invalid arguments (bad or missing flag; nothing sent) |
|
|
101
109
|
|
|
102
110
|
## State
|
|
103
111
|
|
|
@@ -119,6 +127,7 @@ Identity and pairing are persisted in `~/.clanker-wallet/`:
|
|
|
119
127
|
| Base | 8453 |
|
|
120
128
|
| Optimism | 10 |
|
|
121
129
|
| Sepolia (testnet) | 11155111 |
|
|
130
|
+
| Base Sepolia (testnet) | 84532 |
|
|
122
131
|
|
|
123
132
|
---
|
|
124
133
|
|
|
@@ -136,17 +145,22 @@ import { ClankerWallet } from 'clanker-wallet'
|
|
|
136
145
|
const wallet = new ClankerWallet({ agentName: 'my-bot' })
|
|
137
146
|
wallet.pair(pairingJson)
|
|
138
147
|
|
|
148
|
+
// 0.001 ETH on Base Sepolia; value is wei (string or bigint)
|
|
139
149
|
const txHash = await wallet.requestTx({
|
|
140
|
-
chain_id:
|
|
141
|
-
transaction: { to: '
|
|
142
|
-
context: { reason: '
|
|
150
|
+
chain_id: 84532,
|
|
151
|
+
transaction: { to: '0x000000000000000000000000000000000000dEaD', value: '1000000000000000', data: '0x' },
|
|
152
|
+
context: { reason: 'send 0.001 ETH on base sepolia', urgency: 'medium' },
|
|
143
153
|
})
|
|
144
154
|
```
|
|
145
155
|
|
|
156
|
+
`requestTx` throws `InvalidTxError` (with `field` and `reason`) before anything is sent if `chain_id`, `to`, `value` or `data` is malformed.
|
|
157
|
+
|
|
146
158
|
See full API: [`src/`](./src/)
|
|
147
159
|
|
|
148
160
|
## Python SDK
|
|
149
161
|
|
|
162
|
+
Requires Python 3.10+.
|
|
163
|
+
|
|
150
164
|
```bash
|
|
151
165
|
pip install clanker-wallet
|
|
152
166
|
```
|
|
@@ -158,9 +172,9 @@ agent = ClankerWallet()
|
|
|
158
172
|
agent.pair(pairing_json)
|
|
159
173
|
|
|
160
174
|
tx_hash = await agent.request_tx(RequestTxOptions(
|
|
161
|
-
chain_id=
|
|
162
|
-
transaction=Transaction(to="
|
|
163
|
-
context=TxContext(reason="
|
|
175
|
+
chain_id=84532, # base sepolia
|
|
176
|
+
transaction=Transaction(to="0x000000000000000000000000000000000000dEaD", value="1000000000000000", data="0x"),
|
|
177
|
+
context=TxContext(reason="send 0.001 ETH on base sepolia"),
|
|
164
178
|
))
|
|
165
179
|
```
|
|
166
180
|
|
package/SKILL.md
CHANGED
|
@@ -16,8 +16,26 @@ npm install -g clanker-wallet
|
|
|
16
16
|
|
|
17
17
|
Or use without installing: `npx clanker-wallet` / `bunx clanker-wallet`
|
|
18
18
|
|
|
19
|
+
### Running in a sandbox
|
|
20
|
+
|
|
21
|
+
If your commands run in a sandbox (e.g. Claude Code), the CLI needs:
|
|
22
|
+
|
|
23
|
+
- **network:** a WebSocket to the relay, `wss://relay.clanker-wallet.xyz/ws` (the `relay` URL in the pairing JSON). Without it `tx` fails with exit code 3.
|
|
24
|
+
- **the sandbox proxy:** if the sandbox only lets traffic out through `HTTPS_PROXY` (Claude Code does), Node ignores it unless you run the CLI as `NODE_USE_ENV_PROXY=1 clanker-wallet ...` (Node 24+). The exit-3 error says so when this applies.
|
|
25
|
+
- **files:** `pair` writes your identity and the pairing to `~/.clanker-wallet/`, and the other commands write there too (they fix file permissions and create the identity if it is missing). Allow writes, not just reads.
|
|
26
|
+
|
|
27
|
+
Ask the human to allow the host and the directory before you pair.
|
|
28
|
+
|
|
19
29
|
## Pairing (one-time)
|
|
20
30
|
|
|
31
|
+
Before you can send transactions, check if you're already paired:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
clanker-wallet status
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
If `paired: true`, skip pairing.
|
|
38
|
+
|
|
21
39
|
The human will paste a prompt block from https://clanker-wallet.xyz that looks like this:
|
|
22
40
|
|
|
23
41
|
> read the setup guide first: https://clanker-wallet.xyz/llms.txt
|
|
@@ -33,7 +51,9 @@ clanker-wallet pair '{"version":1,"pubkey":"ab12...","relay":"wss://...","wallet
|
|
|
33
51
|
|
|
34
52
|
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.
|
|
35
53
|
|
|
36
|
-
|
|
54
|
+
`pair` writes your identity and the pairing to `~/.clanker-wallet/`. You only need to do this once.
|
|
55
|
+
|
|
56
|
+
If the JSON contains `"mode":"managed"`, the human chose managed mode: the relay simulates each transaction for the review screen, so `tx` also sends the plain transaction fields (from, to, value, data, chainId) to the relay, which shares them with its simulation provider. Without it (or with `"mode":"encrypted"`) the relay only sees ciphertext. Pair with the JSON exactly as given either way.
|
|
37
57
|
|
|
38
58
|
### Trust (first request)
|
|
39
59
|
|
|
@@ -53,7 +73,7 @@ If they agree, **every time you send a `tx` command**, immediately send a messag
|
|
|
53
73
|
|
|
54
74
|
Example notification:
|
|
55
75
|
> Transaction awaiting your approval
|
|
56
|
-
> I'm requesting to send 0.001 ETH on Sepolia to a test address.
|
|
76
|
+
> I'm requesting to send 0.001 ETH on Base Sepolia to a test address.
|
|
57
77
|
> Urgency: normal
|
|
58
78
|
> [Approve or reject here](https://clanker-wallet.xyz)
|
|
59
79
|
|
|
@@ -61,37 +81,41 @@ Example notification:
|
|
|
61
81
|
|
|
62
82
|
```bash
|
|
63
83
|
clanker-wallet tx \
|
|
64
|
-
--chain
|
|
65
|
-
--to
|
|
84
|
+
--chain 84532 \
|
|
85
|
+
--to 0x000000000000000000000000000000000000dEaD \
|
|
66
86
|
--value 1000000000000000 \
|
|
67
|
-
--reason "Send 0.001 ETH to
|
|
87
|
+
--reason "Send 0.001 ETH on Base Sepolia to a test address" \
|
|
68
88
|
--urgency medium \
|
|
69
|
-
--timeout
|
|
89
|
+
--timeout 600000
|
|
70
90
|
```
|
|
71
91
|
|
|
72
|
-
|
|
92
|
+
This sends 0.001 ETH (`1000000000000000` wei) on Base Sepolia (chain `84532`) to the burn address `0x000000000000000000000000000000000000dEaD`.
|
|
93
|
+
|
|
94
|
+
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). If your shell tool kills commands after a few minutes, run `tx` in the background and read its output when it exits. Output is JSON:
|
|
73
95
|
- Approved: `{"tx_hash":"0x...","status":"approved"}` (exit code 0)
|
|
74
96
|
- Rejected: `{"status":"rejected","error":"..."}` (exit code 1)
|
|
75
97
|
- Timeout: `{"status":"timeout","error":"..."}` (exit code 2)
|
|
98
|
+
- Invalid `--chain`, `--to`, `--value`, `--data` or `--timeout`: a one-line `invalid --<flag>: ...` message on stderr and nothing is sent (exit code 5)
|
|
76
99
|
|
|
77
100
|
## Critical Details
|
|
78
101
|
|
|
79
|
-
- **value is in WEI, not ETH.** 1 ETH = `1000000000000000000`.
|
|
80
|
-
- **data is hex-encoded calldata.** Use `0x` for simple ETH transfers. For contract calls, encode the function call.
|
|
102
|
+
- **value is in WEI, not ETH.** 1 ETH = `1000000000000000000`. A whole number: no decimals, no `1e18`.
|
|
103
|
+
- **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.
|
|
104
|
+
- **to must be a real address**: `0x` + 40 hex characters. Mixed-case addresses must carry a valid EIP-55 checksum.
|
|
81
105
|
- **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).
|
|
82
106
|
|
|
83
107
|
## tx Flags
|
|
84
108
|
|
|
85
109
|
| Flag | Required | Default | Description |
|
|
86
110
|
|------|----------|---------|-------------|
|
|
87
|
-
| `--chain <id>` | yes | — | Chain ID (1=Ethereum,
|
|
111
|
+
| `--chain <id>` | yes | — | Chain ID (1=Ethereum, 8453=Base, 84532=Base Sepolia) |
|
|
88
112
|
| `--to <address>` | yes | — | Recipient address |
|
|
89
113
|
| `--value <wei>` | no | `0` | Value in wei |
|
|
90
114
|
| `--data <hex>` | no | `0x` | Calldata hex |
|
|
91
115
|
| `--reason <text>` | no | — | Shown to human |
|
|
92
116
|
| `--urgency <level>` | no | — | `low`, `medium`, or `high` |
|
|
93
117
|
| `--expected-outcome <text>` | no | — | What will happen |
|
|
94
|
-
| `--timeout <ms>` | no | `3600000` | How long
|
|
118
|
+
| `--timeout <ms>` | no | `3600000` (1 h) | How long `tx` blocks waiting for the human |
|
|
95
119
|
|
|
96
120
|
## Common Chain IDs
|
|
97
121
|
|
|
@@ -103,33 +127,37 @@ The command **blocks until the human approves or rejects**. Output is JSON:
|
|
|
103
127
|
| Base | 8453 |
|
|
104
128
|
| Optimism | 10 |
|
|
105
129
|
| Sepolia (testnet) | 11155111 |
|
|
130
|
+
| Base Sepolia (testnet) | 84532 |
|
|
106
131
|
|
|
107
132
|
## Common Patterns
|
|
108
133
|
|
|
109
134
|
### Simple ETH transfer
|
|
110
135
|
```bash
|
|
111
|
-
clanker-wallet tx --chain
|
|
136
|
+
clanker-wallet tx --chain 84532 --to 0x000000000000000000000000000000000000dEaD --value 1000000000000000 --reason "Send 0.001 ETH"
|
|
112
137
|
```
|
|
113
138
|
|
|
114
139
|
### ERC-20 token transfer
|
|
140
|
+
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`:
|
|
141
|
+
|
|
115
142
|
```bash
|
|
116
143
|
# transfer(address to, uint256 amount) — selector 0xa9059cbb
|
|
117
|
-
# encode to + amount as 32-byte padded hex
|
|
118
144
|
clanker-wallet tx \
|
|
119
|
-
--chain
|
|
120
|
-
--to
|
|
121
|
-
--data
|
|
122
|
-
--reason "Transfer
|
|
145
|
+
--chain 84532 \
|
|
146
|
+
--to 0x4200000000000000000000000000000000000006 \
|
|
147
|
+
--data 0xa9059cbb000000000000000000000000000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000038d7ea4c68000 \
|
|
148
|
+
--reason "Transfer 0.001 WETH to a test address"
|
|
123
149
|
```
|
|
124
150
|
|
|
151
|
+
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] })`.
|
|
152
|
+
|
|
125
153
|
### ERC-20 approval
|
|
126
154
|
```bash
|
|
127
155
|
# approve(address spender, uint256 amount) — selector 0x095ea7b3
|
|
128
156
|
clanker-wallet tx \
|
|
129
|
-
--chain
|
|
130
|
-
--to
|
|
131
|
-
--data
|
|
132
|
-
--reason "Approve
|
|
157
|
+
--chain 84532 \
|
|
158
|
+
--to 0x4200000000000000000000000000000000000006 \
|
|
159
|
+
--data 0x095ea7b3000000000000000000000000000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000038d7ea4c68000 \
|
|
160
|
+
--reason "Approve a test address to spend 0.001 WETH"
|
|
133
161
|
```
|
|
134
162
|
|
|
135
163
|
## Other Commands
|
|
@@ -146,7 +174,8 @@ clanker-wallet whoami
|
|
|
146
174
|
|
|
147
175
|
## Troubleshooting
|
|
148
176
|
|
|
177
|
+
- **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
|
|
149
178
|
- **exit code 4 / "not paired"** — run `clanker-wallet pair '<JSON>'` first
|
|
150
179
|
- **exit code 2 / timeout** — human didn't respond in time, increase `--timeout` or ask them to check the web app
|
|
151
180
|
- **exit code 1 / "rejected"** — human clicked reject. Explain why the tx is needed and try again if appropriate.
|
|
152
|
-
- **exit code 3 / connection error** — relay may be down, try again in a few seconds
|
|
181
|
+
- **exit code 3 / "relay connection error: could not reach wss://..."** — if you run in a sandbox or behind a firewall, allow that host (`relay.clanker-wallet.xyz` by default) and, if the error mentions it, rerun with `NODE_USE_ENV_PROXY=1` (see "Running in a sandbox"); otherwise the relay may be down or rate limiting, try again in a few seconds
|
package/bin/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { ClankerWallet } from '../dist/index.js'
|
|
2
|
+
import { ClankerWallet, InvalidTxError, validateTx } from '../dist/index.js'
|
|
3
3
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from 'fs'
|
|
4
4
|
import { join } from 'path'
|
|
5
5
|
import { homedir } from 'os'
|
|
@@ -10,6 +10,9 @@ 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('')
|
|
@@ -135,12 +138,20 @@ async function cmdTx(flags) {
|
|
|
135
138
|
if (!chain) die('missing --chain', EXIT.BAD_ARGS)
|
|
136
139
|
if (!to) die('missing --to', EXIT.BAD_ARGS)
|
|
137
140
|
|
|
138
|
-
|
|
139
|
-
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
|
|
140
146
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
+
}
|
|
144
155
|
|
|
145
156
|
const context = {}
|
|
146
157
|
if (flags.reason) context.reason = flags.reason
|
|
@@ -159,8 +170,8 @@ async function cmdTx(flags) {
|
|
|
159
170
|
|
|
160
171
|
try {
|
|
161
172
|
const txHash = await wallet.requestTx({
|
|
162
|
-
chain_id:
|
|
163
|
-
transaction:
|
|
173
|
+
chain_id: request.chain_id,
|
|
174
|
+
transaction: request.transaction,
|
|
164
175
|
context: Object.keys(context).length > 0 ? context : undefined,
|
|
165
176
|
timeout_ms: timeout,
|
|
166
177
|
})
|
|
@@ -202,6 +213,11 @@ function cmdWhoami() {
|
|
|
202
213
|
})
|
|
203
214
|
}
|
|
204
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
|
+
|
|
205
221
|
function showHelp() {
|
|
206
222
|
process.stderr.write(`clanker-wallet — human-approved transactions for AI agents
|
|
207
223
|
|
|
@@ -212,30 +228,31 @@ commands:
|
|
|
212
228
|
whoami show agent identity
|
|
213
229
|
|
|
214
230
|
tx flags:
|
|
215
|
-
--chain <id>
|
|
216
|
-
--to <address>
|
|
217
|
-
--value <wei>
|
|
218
|
-
--data <hex>
|
|
219
|
-
--reason <text>
|
|
220
|
-
--urgency <low|
|
|
221
|
-
--expected-outcome <text>
|
|
222
|
-
--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)
|
|
223
239
|
|
|
224
240
|
exit codes:
|
|
225
241
|
0 success
|
|
226
242
|
1 rejected by human
|
|
227
243
|
2 timeout
|
|
228
|
-
3 connection error
|
|
244
|
+
3 connection error (in a sandbox or behind a firewall, allow the relay host, wss://relay.clanker-wallet.xyz/ws by default;
|
|
245
|
+
behind HTTPS_PROXY run with NODE_USE_ENV_PROXY=1)
|
|
229
246
|
4 not paired
|
|
230
247
|
5 invalid arguments
|
|
231
248
|
|
|
232
249
|
examples:
|
|
233
250
|
clanker-wallet pair '{"version":1,"pubkey":"ab12...","relay":"wss://...","wallet":"0x..."}'
|
|
234
|
-
clanker-wallet tx --chain
|
|
251
|
+
clanker-wallet tx --chain 84532 --to 0x000000000000000000000000000000000000dEaD --value 1000000000000000 --reason "send 0.001 ETH on base sepolia"
|
|
235
252
|
clanker-wallet status
|
|
236
253
|
clanker-wallet whoami
|
|
237
254
|
|
|
238
|
-
docs: https://
|
|
255
|
+
docs: https://clanker-wallet.xyz/SKILL.md
|
|
239
256
|
web: https://clanker-wallet.xyz
|
|
240
257
|
`)
|
|
241
258
|
}
|
|
@@ -256,6 +273,10 @@ switch (cmd) {
|
|
|
256
273
|
case 'whoami':
|
|
257
274
|
cmdWhoami()
|
|
258
275
|
break
|
|
276
|
+
case '--version':
|
|
277
|
+
case '-v':
|
|
278
|
+
cmdVersion()
|
|
279
|
+
break
|
|
259
280
|
case 'help':
|
|
260
281
|
case '--help':
|
|
261
282
|
case '-h':
|
package/dist/client.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
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
|
|
5
|
+
const UNREACHABLE_HINT = 'if you run in a sandbox or behind a firewall, allow this host; otherwise the relay may be down or rate limiting, retry in a few seconds';
|
|
6
|
+
/** node's WebSocket ignores HTTPS_PROXY unless NODE_USE_ENV_PROXY is set at startup, and sandboxes (claude code) only let traffic out through that proxy */
|
|
7
|
+
function proxyHint() {
|
|
8
|
+
const env = globalThis.process?.env;
|
|
9
|
+
if (!env || env.NODE_USE_ENV_PROXY)
|
|
10
|
+
return '';
|
|
11
|
+
return env.HTTPS_PROXY || env.https_proxy || env.HTTP_PROXY || env.http_proxy
|
|
12
|
+
? '; HTTPS_PROXY is set but node ignores it, rerun with NODE_USE_ENV_PROXY=1 (node 24+)'
|
|
13
|
+
: '';
|
|
14
|
+
}
|
|
4
15
|
export class ClankerWallet extends TypedEmitter {
|
|
5
16
|
publicKey;
|
|
6
17
|
secretKey;
|
|
@@ -57,6 +68,8 @@ export class ClankerWallet extends TypedEmitter {
|
|
|
57
68
|
if (!this.pairing) {
|
|
58
69
|
throw new Error('not paired — call pair() first');
|
|
59
70
|
}
|
|
71
|
+
// reject malformed requests before anything reaches the relay
|
|
72
|
+
const { chain_id, transaction } = validateTx(options.chain_id, options.transaction);
|
|
60
73
|
const { humanPubkey, humanPubkeyHex, relayUrl, humanWallet, mode } = this.pairing;
|
|
61
74
|
const timeoutMs = options.timeout_ms ?? DEFAULT_TIMEOUT_MS;
|
|
62
75
|
const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -64,8 +77,8 @@ export class ClankerWallet extends TypedEmitter {
|
|
|
64
77
|
request_id: requestId,
|
|
65
78
|
agent_id: this.agentId,
|
|
66
79
|
timestamp: new Date().toISOString(),
|
|
67
|
-
chain_id
|
|
68
|
-
transaction
|
|
80
|
+
chain_id,
|
|
81
|
+
transaction,
|
|
69
82
|
context: options.context,
|
|
70
83
|
signature: '', // set below after signing
|
|
71
84
|
};
|
|
@@ -100,8 +113,9 @@ export class ClankerWallet extends TypedEmitter {
|
|
|
100
113
|
return;
|
|
101
114
|
}
|
|
102
115
|
ws.addEventListener('error', () => {
|
|
116
|
+
// node's WebSocket error event carries no cause, so name the url: a sandbox or firewall blocking it looks the same as an outage
|
|
103
117
|
if (!settled)
|
|
104
|
-
fail(
|
|
118
|
+
fail(`relay connection error: could not reach ${relayUrl} (${UNREACHABLE_HINT}${proxyHint()})`);
|
|
105
119
|
});
|
|
106
120
|
ws.addEventListener('close', () => {
|
|
107
121
|
this.emit('disconnected', {});
|
|
@@ -140,10 +154,10 @@ export class ClankerWallet extends TypedEmitter {
|
|
|
140
154
|
if (mode === 'managed') {
|
|
141
155
|
wsMsg.simulate = {
|
|
142
156
|
from: humanWallet || '',
|
|
143
|
-
to:
|
|
144
|
-
value:
|
|
145
|
-
data:
|
|
146
|
-
chainId:
|
|
157
|
+
to: transaction.to,
|
|
158
|
+
value: transaction.value,
|
|
159
|
+
data: transaction.data,
|
|
160
|
+
chainId: chain_id,
|
|
147
161
|
};
|
|
148
162
|
}
|
|
149
163
|
ws.send(JSON.stringify(wsMsg));
|
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;
|
|
@@ -46,7 +52,8 @@ export interface PairingData {
|
|
|
46
52
|
/** options for requestTx */
|
|
47
53
|
export interface RequestTxOptions {
|
|
48
54
|
chain_id: number;
|
|
49
|
-
|
|
55
|
+
/** checked before sending; an invalid field throws InvalidTxError */
|
|
56
|
+
transaction: TxInput;
|
|
50
57
|
context?: TxContext;
|
|
51
58
|
/** timeout in ms (default: 3_600_000 = 1hr) */
|
|
52
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
|
+
}
|