clanker-wallet 0.4.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 +27 -17
- package/SKILL.md +37 -20
- package/bin/cli.js +38 -18
- package/dist/client.js +9 -6
- 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
|
@@ -22,8 +22,9 @@ bunx clanker-wallet
|
|
|
22
22
|
# 2. Pair (one-time) — extract the JSON from the prompt
|
|
23
23
|
clanker-wallet pair '{"version":1,"pubkey":"ab12...","relay":"wss://...","wallet":"0x..."}'
|
|
24
24
|
|
|
25
|
-
# 3. Send a transaction (blocks until human approves or rejects)
|
|
26
|
-
|
|
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"
|
|
27
28
|
|
|
28
29
|
# 4. Check status anytime
|
|
29
30
|
clanker-wallet status
|
|
@@ -42,33 +43,36 @@ clanker-wallet pair '<pairing JSON from web app>'
|
|
|
42
43
|
|
|
43
44
|
### `tx`
|
|
44
45
|
|
|
45
|
-
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.
|
|
46
47
|
|
|
47
48
|
```bash
|
|
48
49
|
clanker-wallet tx \
|
|
49
|
-
--chain
|
|
50
|
-
--to
|
|
51
|
-
--value
|
|
50
|
+
--chain 84532 \
|
|
51
|
+
--to 0x000000000000000000000000000000000000dEaD \
|
|
52
|
+
--value 1000000000000000 \
|
|
52
53
|
--data 0x \
|
|
53
|
-
--reason "
|
|
54
|
+
--reason "send 0.001 ETH on base sepolia" \
|
|
54
55
|
--urgency medium \
|
|
55
|
-
--timeout
|
|
56
|
+
--timeout 600000
|
|
56
57
|
# approved → {"tx_hash":"0x...","status":"approved"}
|
|
57
58
|
# rejected → {"status":"rejected","error":"..."} (exit code 1)
|
|
58
59
|
# timeout → {"status":"timeout","error":"..."} (exit code 2)
|
|
60
|
+
# bad flag → "invalid --to: ..." on stderr, nothing sent (exit code 5)
|
|
59
61
|
```
|
|
60
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
|
+
|
|
61
65
|
**Flags:**
|
|
62
66
|
| Flag | Required | Default | Description |
|
|
63
67
|
|------|----------|---------|-------------|
|
|
64
|
-
| `--chain <id>` | yes | — | Chain ID (1=Ethereum,
|
|
68
|
+
| `--chain <id>` | yes | — | Chain ID (1=Ethereum, 8453=Base, 84532=Base Sepolia) |
|
|
65
69
|
| `--to <address>` | yes | — | Recipient address |
|
|
66
70
|
| `--value <wei>` | no | `0` | Value in wei (1 ETH = `1000000000000000000`) |
|
|
67
71
|
| `--data <hex>` | no | `0x` | Calldata hex |
|
|
68
72
|
| `--reason <text>` | no | — | Shown to human |
|
|
69
73
|
| `--urgency <level>` | no | — | `low`, `medium`, or `high` |
|
|
70
74
|
| `--expected-outcome <text>` | no | — | What will happen |
|
|
71
|
-
| `--timeout <ms>` | no | `3600000` | How long
|
|
75
|
+
| `--timeout <ms>` | no | `3600000` (1 h) | How long `tx` blocks waiting for the human |
|
|
72
76
|
|
|
73
77
|
### `status`
|
|
74
78
|
|
|
@@ -97,7 +101,7 @@ clanker-wallet whoami
|
|
|
97
101
|
| 2 | Timeout |
|
|
98
102
|
| 3 | Connection/relay error |
|
|
99
103
|
| 4 | Not paired |
|
|
100
|
-
| 5 | Invalid arguments |
|
|
104
|
+
| 5 | Invalid arguments (bad or missing flag; nothing sent) |
|
|
101
105
|
|
|
102
106
|
## State
|
|
103
107
|
|
|
@@ -119,6 +123,7 @@ Identity and pairing are persisted in `~/.clanker-wallet/`:
|
|
|
119
123
|
| Base | 8453 |
|
|
120
124
|
| Optimism | 10 |
|
|
121
125
|
| Sepolia (testnet) | 11155111 |
|
|
126
|
+
| Base Sepolia (testnet) | 84532 |
|
|
122
127
|
|
|
123
128
|
---
|
|
124
129
|
|
|
@@ -136,17 +141,22 @@ import { ClankerWallet } from 'clanker-wallet'
|
|
|
136
141
|
const wallet = new ClankerWallet({ agentName: 'my-bot' })
|
|
137
142
|
wallet.pair(pairingJson)
|
|
138
143
|
|
|
144
|
+
// 0.001 ETH on Base Sepolia; value is wei (string or bigint)
|
|
139
145
|
const txHash = await wallet.requestTx({
|
|
140
|
-
chain_id:
|
|
141
|
-
transaction: { to: '
|
|
142
|
-
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' },
|
|
143
149
|
})
|
|
144
150
|
```
|
|
145
151
|
|
|
152
|
+
`requestTx` throws `InvalidTxError` (with `field` and `reason`) before anything is sent if `chain_id`, `to`, `value` or `data` is malformed.
|
|
153
|
+
|
|
146
154
|
See full API: [`src/`](./src/)
|
|
147
155
|
|
|
148
156
|
## Python SDK
|
|
149
157
|
|
|
158
|
+
Requires Python 3.10+.
|
|
159
|
+
|
|
150
160
|
```bash
|
|
151
161
|
pip install clanker-wallet
|
|
152
162
|
```
|
|
@@ -158,9 +168,9 @@ agent = ClankerWallet()
|
|
|
158
168
|
agent.pair(pairing_json)
|
|
159
169
|
|
|
160
170
|
tx_hash = await agent.request_tx(RequestTxOptions(
|
|
161
|
-
chain_id=
|
|
162
|
-
transaction=Transaction(to="
|
|
163
|
-
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"),
|
|
164
174
|
))
|
|
165
175
|
```
|
|
166
176
|
|
package/SKILL.md
CHANGED
|
@@ -18,6 +18,14 @@ 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, check if you're already paired:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
clanker-wallet status
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
If `paired: true`, skip pairing.
|
|
28
|
+
|
|
21
29
|
The human will paste a prompt block from https://clanker-wallet.xyz that looks like this:
|
|
22
30
|
|
|
23
31
|
> read the setup guide first: https://clanker-wallet.xyz/llms.txt
|
|
@@ -53,7 +61,7 @@ If they agree, **every time you send a `tx` command**, immediately send a messag
|
|
|
53
61
|
|
|
54
62
|
Example notification:
|
|
55
63
|
> Transaction awaiting your approval
|
|
56
|
-
> 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.
|
|
57
65
|
> Urgency: normal
|
|
58
66
|
> [Approve or reject here](https://clanker-wallet.xyz)
|
|
59
67
|
|
|
@@ -61,37 +69,41 @@ Example notification:
|
|
|
61
69
|
|
|
62
70
|
```bash
|
|
63
71
|
clanker-wallet tx \
|
|
64
|
-
--chain
|
|
65
|
-
--to
|
|
72
|
+
--chain 84532 \
|
|
73
|
+
--to 0x000000000000000000000000000000000000dEaD \
|
|
66
74
|
--value 1000000000000000 \
|
|
67
|
-
--reason "Send 0.001 ETH to
|
|
75
|
+
--reason "Send 0.001 ETH on Base Sepolia to a test address" \
|
|
68
76
|
--urgency medium \
|
|
69
|
-
--timeout
|
|
77
|
+
--timeout 600000
|
|
70
78
|
```
|
|
71
79
|
|
|
72
|
-
|
|
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:
|
|
73
83
|
- Approved: `{"tx_hash":"0x...","status":"approved"}` (exit code 0)
|
|
74
84
|
- Rejected: `{"status":"rejected","error":"..."}` (exit code 1)
|
|
75
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)
|
|
76
87
|
|
|
77
88
|
## Critical Details
|
|
78
89
|
|
|
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.
|
|
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.
|
|
81
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).
|
|
82
94
|
|
|
83
95
|
## tx Flags
|
|
84
96
|
|
|
85
97
|
| Flag | Required | Default | Description |
|
|
86
98
|
|------|----------|---------|-------------|
|
|
87
|
-
| `--chain <id>` | yes | — | Chain ID (1=Ethereum,
|
|
99
|
+
| `--chain <id>` | yes | — | Chain ID (1=Ethereum, 8453=Base, 84532=Base Sepolia) |
|
|
88
100
|
| `--to <address>` | yes | — | Recipient address |
|
|
89
101
|
| `--value <wei>` | no | `0` | Value in wei |
|
|
90
102
|
| `--data <hex>` | no | `0x` | Calldata hex |
|
|
91
103
|
| `--reason <text>` | no | — | Shown to human |
|
|
92
104
|
| `--urgency <level>` | no | — | `low`, `medium`, or `high` |
|
|
93
105
|
| `--expected-outcome <text>` | no | — | What will happen |
|
|
94
|
-
| `--timeout <ms>` | no | `3600000` | How long
|
|
106
|
+
| `--timeout <ms>` | no | `3600000` (1 h) | How long `tx` blocks waiting for the human |
|
|
95
107
|
|
|
96
108
|
## Common Chain IDs
|
|
97
109
|
|
|
@@ -103,33 +115,37 @@ The command **blocks until the human approves or rejects**. Output is JSON:
|
|
|
103
115
|
| Base | 8453 |
|
|
104
116
|
| Optimism | 10 |
|
|
105
117
|
| Sepolia (testnet) | 11155111 |
|
|
118
|
+
| Base Sepolia (testnet) | 84532 |
|
|
106
119
|
|
|
107
120
|
## Common Patterns
|
|
108
121
|
|
|
109
122
|
### Simple ETH transfer
|
|
110
123
|
```bash
|
|
111
|
-
clanker-wallet tx --chain
|
|
124
|
+
clanker-wallet tx --chain 84532 --to 0x000000000000000000000000000000000000dEaD --value 1000000000000000 --reason "Send 0.001 ETH"
|
|
112
125
|
```
|
|
113
126
|
|
|
114
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
|
+
|
|
115
130
|
```bash
|
|
116
131
|
# transfer(address to, uint256 amount) — selector 0xa9059cbb
|
|
117
|
-
# encode to + amount as 32-byte padded hex
|
|
118
132
|
clanker-wallet tx \
|
|
119
|
-
--chain
|
|
120
|
-
--to
|
|
121
|
-
--data
|
|
122
|
-
--reason "Transfer
|
|
133
|
+
--chain 84532 \
|
|
134
|
+
--to 0x4200000000000000000000000000000000000006 \
|
|
135
|
+
--data 0xa9059cbb000000000000000000000000000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000038d7ea4c68000 \
|
|
136
|
+
--reason "Transfer 0.001 WETH to a test address"
|
|
123
137
|
```
|
|
124
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
|
+
|
|
125
141
|
### ERC-20 approval
|
|
126
142
|
```bash
|
|
127
143
|
# approve(address spender, uint256 amount) — selector 0x095ea7b3
|
|
128
144
|
clanker-wallet tx \
|
|
129
|
-
--chain
|
|
130
|
-
--to
|
|
131
|
-
--data
|
|
132
|
-
--reason "Approve
|
|
145
|
+
--chain 84532 \
|
|
146
|
+
--to 0x4200000000000000000000000000000000000006 \
|
|
147
|
+
--data 0x095ea7b3000000000000000000000000000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000038d7ea4c68000 \
|
|
148
|
+
--reason "Approve a test address to spend 0.001 WETH"
|
|
133
149
|
```
|
|
134
150
|
|
|
135
151
|
## Other Commands
|
|
@@ -146,6 +162,7 @@ clanker-wallet whoami
|
|
|
146
162
|
|
|
147
163
|
## Troubleshooting
|
|
148
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
|
|
149
166
|
- **exit code 4 / "not paired"** — run `clanker-wallet pair '<JSON>'` first
|
|
150
167
|
- **exit code 2 / timeout** — human didn't respond in time, increase `--timeout` or ask them to check the web app
|
|
151
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,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,14 +228,14 @@ 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
|
|
@@ -231,11 +247,11 @@ exit codes:
|
|
|
231
247
|
|
|
232
248
|
examples:
|
|
233
249
|
clanker-wallet pair '{"version":1,"pubkey":"ab12...","relay":"wss://...","wallet":"0x..."}'
|
|
234
|
-
clanker-wallet tx --chain
|
|
250
|
+
clanker-wallet tx --chain 84532 --to 0x000000000000000000000000000000000000dEaD --value 1000000000000000 --reason "send 0.001 ETH on base sepolia"
|
|
235
251
|
clanker-wallet status
|
|
236
252
|
clanker-wallet whoami
|
|
237
253
|
|
|
238
|
-
docs: https://
|
|
254
|
+
docs: https://clanker-wallet.xyz/SKILL.md
|
|
239
255
|
web: https://clanker-wallet.xyz
|
|
240
256
|
`)
|
|
241
257
|
}
|
|
@@ -256,6 +272,10 @@ switch (cmd) {
|
|
|
256
272
|
case 'whoami':
|
|
257
273
|
cmdWhoami()
|
|
258
274
|
break
|
|
275
|
+
case '--version':
|
|
276
|
+
case '-v':
|
|
277
|
+
cmdVersion()
|
|
278
|
+
break
|
|
259
279
|
case 'help':
|
|
260
280
|
case '--help':
|
|
261
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;
|
|
@@ -57,6 +58,8 @@ export class ClankerWallet extends TypedEmitter {
|
|
|
57
58
|
if (!this.pairing) {
|
|
58
59
|
throw new Error('not paired — call pair() first');
|
|
59
60
|
}
|
|
61
|
+
// reject malformed requests before anything reaches the relay
|
|
62
|
+
const { chain_id, transaction } = validateTx(options.chain_id, options.transaction);
|
|
60
63
|
const { humanPubkey, humanPubkeyHex, relayUrl, humanWallet, mode } = this.pairing;
|
|
61
64
|
const timeoutMs = options.timeout_ms ?? DEFAULT_TIMEOUT_MS;
|
|
62
65
|
const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -64,8 +67,8 @@ export class ClankerWallet extends TypedEmitter {
|
|
|
64
67
|
request_id: requestId,
|
|
65
68
|
agent_id: this.agentId,
|
|
66
69
|
timestamp: new Date().toISOString(),
|
|
67
|
-
chain_id
|
|
68
|
-
transaction
|
|
70
|
+
chain_id,
|
|
71
|
+
transaction,
|
|
69
72
|
context: options.context,
|
|
70
73
|
signature: '', // set below after signing
|
|
71
74
|
};
|
|
@@ -140,10 +143,10 @@ export class ClankerWallet extends TypedEmitter {
|
|
|
140
143
|
if (mode === 'managed') {
|
|
141
144
|
wsMsg.simulate = {
|
|
142
145
|
from: humanWallet || '',
|
|
143
|
-
to:
|
|
144
|
-
value:
|
|
145
|
-
data:
|
|
146
|
-
chainId:
|
|
146
|
+
to: transaction.to,
|
|
147
|
+
value: transaction.value,
|
|
148
|
+
data: transaction.data,
|
|
149
|
+
chainId: chain_id,
|
|
147
150
|
};
|
|
148
151
|
}
|
|
149
152
|
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
|
+
}
|