clanker-wallet 0.2.0 → 0.4.0
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 +139 -54
- package/SKILL.md +152 -0
- package/bin/cli.js +267 -0
- package/dist/client.d.ts +3 -5
- package/dist/client.js +39 -50
- package/dist/crypto.d.ts +3 -0
- package/dist/crypto.js +28 -11
- package/dist/events.js +6 -5
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/types.d.ts +1 -1
- package/package.json +33 -6
package/README.md
CHANGED
|
@@ -1,93 +1,178 @@
|
|
|
1
1
|
# clanker-wallet
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Human-approved blockchain transactions for AI agents. Agent proposes a tx, human reviews and signs it from the [web app](https://clanker-wallet.xyz). All communication is end-to-end encrypted.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install clanker-wallet
|
|
8
|
+
npm install -g clanker-wallet
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Or run without installing:
|
|
12
|
+
```bash
|
|
13
|
+
npx clanker-wallet
|
|
14
|
+
bunx clanker-wallet
|
|
15
|
+
```
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
import { ClankerWallet } from 'clanker-wallet'
|
|
17
|
+
## Quick Start
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
```bash
|
|
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
|
|
23
|
+
clanker-wallet pair '{"version":1,"pubkey":"ab12...","relay":"wss://...","wallet":"0x..."}'
|
|
17
24
|
|
|
18
|
-
|
|
19
|
-
wallet
|
|
25
|
+
# 3. Send a transaction (blocks until human approves or rejects)
|
|
26
|
+
clanker-wallet tx --chain 1 --to 0xRecipient --value 1000000000000000000 --reason "swap ETH for USDC"
|
|
20
27
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
})
|
|
28
|
+
# 4. Check status anytime
|
|
29
|
+
clanker-wallet status
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Commands
|
|
27
33
|
|
|
28
|
-
|
|
34
|
+
### `pair`
|
|
35
|
+
|
|
36
|
+
Pair with a human's web app. Run once — identity and pairing are saved to `~/.clanker-wallet/`.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
clanker-wallet pair '<pairing JSON from web app>'
|
|
40
|
+
# → {"agent_id":"agent_abc123","status":"paired","wallet":"0x...","relay":"wss://..."}
|
|
29
41
|
```
|
|
30
42
|
|
|
31
|
-
|
|
43
|
+
### `tx`
|
|
32
44
|
|
|
33
|
-
|
|
34
|
-
- **Relay transport** — lightweight WebSocket relay forwards encrypted messages between agent and human. No private keys touch the server.
|
|
35
|
-
- **Human approval** — the human reviews transaction details in the Clanker Wallet web app and signs with their own wallet (MetaMask, WalletConnect, etc).
|
|
45
|
+
Send a transaction request. Blocks until the human approves or rejects.
|
|
36
46
|
|
|
37
|
-
|
|
47
|
+
```bash
|
|
48
|
+
clanker-wallet tx \
|
|
49
|
+
--chain 1 \
|
|
50
|
+
--to 0xRecipient \
|
|
51
|
+
--value 1000000000000000000 \
|
|
52
|
+
--data 0x \
|
|
53
|
+
--reason "swap ETH for USDC" \
|
|
54
|
+
--urgency medium \
|
|
55
|
+
--timeout 120000
|
|
56
|
+
# approved → {"tx_hash":"0x...","status":"approved"}
|
|
57
|
+
# rejected → {"status":"rejected","error":"..."} (exit code 1)
|
|
58
|
+
# timeout → {"status":"timeout","error":"..."} (exit code 2)
|
|
59
|
+
```
|
|
38
60
|
|
|
39
|
-
|
|
61
|
+
**Flags:**
|
|
62
|
+
| Flag | Required | Default | Description |
|
|
63
|
+
|------|----------|---------|-------------|
|
|
64
|
+
| `--chain <id>` | yes | — | Chain ID (1=Ethereum, 137=Polygon, 8453=Base) |
|
|
65
|
+
| `--to <address>` | yes | — | Recipient address |
|
|
66
|
+
| `--value <wei>` | no | `0` | Value in wei (1 ETH = `1000000000000000000`) |
|
|
67
|
+
| `--data <hex>` | no | `0x` | Calldata hex |
|
|
68
|
+
| `--reason <text>` | no | — | Shown to human |
|
|
69
|
+
| `--urgency <level>` | no | — | `low`, `medium`, or `high` |
|
|
70
|
+
| `--expected-outcome <text>` | no | — | What will happen |
|
|
71
|
+
| `--timeout <ms>` | no | `3600000` | How long to wait |
|
|
40
72
|
|
|
41
|
-
|
|
42
|
-
|--------|------|-------------|
|
|
43
|
-
| `agentName` | `string` | Display name shown to human (optional) |
|
|
44
|
-
| `secretKey` | `Uint8Array` | 32-byte secret key to restore identity. Omit to generate new keypair. |
|
|
73
|
+
### `status`
|
|
45
74
|
|
|
46
|
-
|
|
75
|
+
Check current pairing status.
|
|
47
76
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
77
|
+
```bash
|
|
78
|
+
clanker-wallet status
|
|
79
|
+
# → {"agent_id":"agent_abc123","paired":true,"wallet":"0x...","relay":"wss://..."}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### `whoami`
|
|
54
83
|
|
|
55
|
-
|
|
84
|
+
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.
|
|
56
85
|
|
|
57
|
-
|
|
86
|
+
```bash
|
|
87
|
+
clanker-wallet whoami
|
|
88
|
+
# → {"agent_id":"agent_abc123","public_key":"ab12cd34ef567890...","fingerprint":"ab12 cd34 ef56 7890"}
|
|
89
|
+
```
|
|
58
90
|
|
|
59
|
-
|
|
91
|
+
## Exit Codes
|
|
60
92
|
|
|
61
|
-
|
|
93
|
+
| Code | Meaning |
|
|
94
|
+
|------|---------|
|
|
95
|
+
| 0 | Success |
|
|
96
|
+
| 1 | Rejected by human |
|
|
97
|
+
| 2 | Timeout |
|
|
98
|
+
| 3 | Connection/relay error |
|
|
99
|
+
| 4 | Not paired |
|
|
100
|
+
| 5 | Invalid arguments |
|
|
62
101
|
|
|
63
|
-
|
|
64
|
-
|--------|------|-------------|
|
|
65
|
-
| `chain_id` | `number` | Target chain (1 = mainnet, 137 = polygon, etc) |
|
|
66
|
-
| `transaction` | `{ to, value, data }` | Transaction parameters (`value` in wei) |
|
|
67
|
-
| `context` | `{ reason?, urgency?, expected_outcome? }` | Optional context shown to human |
|
|
68
|
-
| `timeout_ms` | `number` | Timeout in ms (default: 1 hour) |
|
|
102
|
+
## State
|
|
69
103
|
|
|
70
|
-
|
|
104
|
+
Identity and pairing are persisted in `~/.clanker-wallet/`:
|
|
71
105
|
|
|
72
|
-
|
|
106
|
+
```
|
|
107
|
+
~/.clanker-wallet/
|
|
108
|
+
identity.json # agent keypair (auto-generated on first use)
|
|
109
|
+
pairing.json # human's public key + relay URL
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Common Chain IDs
|
|
113
|
+
|
|
114
|
+
| Chain | ID |
|
|
115
|
+
|-------|----|
|
|
116
|
+
| Ethereum | 1 |
|
|
117
|
+
| Polygon | 137 |
|
|
118
|
+
| Arbitrum | 42161 |
|
|
119
|
+
| Base | 8453 |
|
|
120
|
+
| Optimism | 10 |
|
|
121
|
+
| Sepolia (testnet) | 11155111 |
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Library Usage (Advanced)
|
|
126
|
+
|
|
127
|
+
The CLI wraps a TypeScript library. For programmatic use:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
npm install clanker-wallet
|
|
131
|
+
```
|
|
73
132
|
|
|
74
133
|
```typescript
|
|
75
|
-
import {
|
|
134
|
+
import { ClankerWallet } from 'clanker-wallet'
|
|
135
|
+
|
|
136
|
+
const wallet = new ClankerWallet({ agentName: 'my-bot' })
|
|
137
|
+
wallet.pair(pairingJson)
|
|
138
|
+
|
|
139
|
+
const txHash = await wallet.requestTx({
|
|
140
|
+
chain_id: 1,
|
|
141
|
+
transaction: { to: '0x...', value: '1000000000000000000', data: '0x' },
|
|
142
|
+
context: { reason: 'Buy tokens', urgency: 'medium' },
|
|
143
|
+
})
|
|
144
|
+
```
|
|
76
145
|
|
|
77
|
-
|
|
78
|
-
const secretKey = existsSync('.agent-key')
|
|
79
|
-
? new Uint8Array(readFileSync('.agent-key'))
|
|
80
|
-
: undefined
|
|
146
|
+
See full API: [`src/`](./src/)
|
|
81
147
|
|
|
82
|
-
|
|
148
|
+
## Python SDK
|
|
83
149
|
|
|
84
|
-
|
|
85
|
-
|
|
150
|
+
```bash
|
|
151
|
+
pip install clanker-wallet
|
|
86
152
|
```
|
|
87
153
|
|
|
88
|
-
|
|
154
|
+
```python
|
|
155
|
+
from clanker_wallet import ClankerWallet, Transaction, RequestTxOptions, TxContext
|
|
156
|
+
|
|
157
|
+
agent = ClankerWallet()
|
|
158
|
+
agent.pair(pairing_json)
|
|
159
|
+
|
|
160
|
+
tx_hash = await agent.request_tx(RequestTxOptions(
|
|
161
|
+
chain_id=1,
|
|
162
|
+
transaction=Transaction(to="0x...", value="1000000000000000000", data="0x"),
|
|
163
|
+
context=TxContext(reason="Buy tokens"),
|
|
164
|
+
))
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## How It Works
|
|
168
|
+
|
|
169
|
+
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
|
|
170
|
+
2. Human pastes the prompt to their AI agent. Agent extracts the JSON and runs `clanker-wallet pair` — establishes E2E encrypted channel
|
|
171
|
+
3. Agent runs `clanker-wallet tx` — encrypts tx with nacl.box, sends via WebSocket relay
|
|
172
|
+
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
|
|
173
|
+
5. Human approves → wallet signs + broadcasts → agent gets the tx hash
|
|
89
174
|
|
|
90
|
-
|
|
175
|
+
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.
|
|
91
176
|
|
|
92
177
|
## License
|
|
93
178
|
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: clanker-wallet
|
|
3
|
+
description: "Send blockchain transactions through a human-approved wallet. You propose tx, human reviews and signs. No private keys needed."
|
|
4
|
+
homepage: https://clanker-wallet.xyz
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Clanker Wallet — Human-Approved Transactions
|
|
8
|
+
|
|
9
|
+
You are an AI agent that needs to send blockchain transactions. You do NOT have a private key. Instead, you propose transactions and a human approves them from their own wallet.
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g clanker-wallet
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or use without installing: `npx clanker-wallet` / `bunx clanker-wallet`
|
|
18
|
+
|
|
19
|
+
## Pairing (one-time)
|
|
20
|
+
|
|
21
|
+
The human will paste a prompt block from https://clanker-wallet.xyz that looks like this:
|
|
22
|
+
|
|
23
|
+
> read the setup guide first: https://clanker-wallet.xyz/llms.txt
|
|
24
|
+
>
|
|
25
|
+
> then pair using this code:
|
|
26
|
+
> `{"version":1,"pubkey":"ab12cd...","relay":"wss://...","wallet":"0x..."}`
|
|
27
|
+
|
|
28
|
+
Extract the JSON object (the line starting with `{`) and pair:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
clanker-wallet pair '{"version":1,"pubkey":"ab12...","relay":"wss://...","wallet":"0x..."}'
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
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
|
+
|
|
36
|
+
This saves identity and pairing to `~/.clanker-wallet/`. You only need to do this once.
|
|
37
|
+
|
|
38
|
+
### Trust (first request)
|
|
39
|
+
|
|
40
|
+
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.
|
|
41
|
+
|
|
42
|
+
### Transaction notifications
|
|
43
|
+
|
|
44
|
+
After pairing, ask the human:
|
|
45
|
+
|
|
46
|
+
> "Would you like me to notify you here when I need a transaction approved? I'll send you a message with a direct link to approve it."
|
|
47
|
+
|
|
48
|
+
If they agree, **every time you send a `tx` command**, immediately send a message through your current communication channel with:
|
|
49
|
+
|
|
50
|
+
1. A short description of what the tx does (from the `--reason`)
|
|
51
|
+
2. A direct link to the web app: **https://clanker-wallet.xyz**
|
|
52
|
+
3. The urgency level
|
|
53
|
+
|
|
54
|
+
Example notification:
|
|
55
|
+
> Transaction awaiting your approval
|
|
56
|
+
> I'm requesting to send 0.001 ETH on Sepolia to a test address.
|
|
57
|
+
> Urgency: normal
|
|
58
|
+
> [Approve or reject here](https://clanker-wallet.xyz)
|
|
59
|
+
|
|
60
|
+
## Sending a Transaction
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
clanker-wallet tx \
|
|
64
|
+
--chain 1 \
|
|
65
|
+
--to 0xRecipientAddress \
|
|
66
|
+
--value 1000000000000000 \
|
|
67
|
+
--reason "Send 0.001 ETH to recipient" \
|
|
68
|
+
--urgency medium \
|
|
69
|
+
--timeout 120000
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The command **blocks until the human approves or rejects**. Output is JSON:
|
|
73
|
+
- Approved: `{"tx_hash":"0x...","status":"approved"}` (exit code 0)
|
|
74
|
+
- Rejected: `{"status":"rejected","error":"..."}` (exit code 1)
|
|
75
|
+
- Timeout: `{"status":"timeout","error":"..."}` (exit code 2)
|
|
76
|
+
|
|
77
|
+
## Critical Details
|
|
78
|
+
|
|
79
|
+
- **value is in WEI, not ETH.** 1 ETH = `1000000000000000000`. Use string, not number.
|
|
80
|
+
- **data is hex-encoded calldata.** Use `0x` for simple ETH transfers. For contract calls, encode the function call.
|
|
81
|
+
- **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
|
+
|
|
83
|
+
## tx Flags
|
|
84
|
+
|
|
85
|
+
| Flag | Required | Default | Description |
|
|
86
|
+
|------|----------|---------|-------------|
|
|
87
|
+
| `--chain <id>` | yes | — | Chain ID (1=Ethereum, 137=Polygon, 8453=Base) |
|
|
88
|
+
| `--to <address>` | yes | — | Recipient address |
|
|
89
|
+
| `--value <wei>` | no | `0` | Value in wei |
|
|
90
|
+
| `--data <hex>` | no | `0x` | Calldata hex |
|
|
91
|
+
| `--reason <text>` | no | — | Shown to human |
|
|
92
|
+
| `--urgency <level>` | no | — | `low`, `medium`, or `high` |
|
|
93
|
+
| `--expected-outcome <text>` | no | — | What will happen |
|
|
94
|
+
| `--timeout <ms>` | no | `3600000` | How long to wait |
|
|
95
|
+
|
|
96
|
+
## Common Chain IDs
|
|
97
|
+
|
|
98
|
+
| Chain | ID |
|
|
99
|
+
|-------|----|
|
|
100
|
+
| Ethereum | 1 |
|
|
101
|
+
| Polygon | 137 |
|
|
102
|
+
| Arbitrum | 42161 |
|
|
103
|
+
| Base | 8453 |
|
|
104
|
+
| Optimism | 10 |
|
|
105
|
+
| Sepolia (testnet) | 11155111 |
|
|
106
|
+
|
|
107
|
+
## Common Patterns
|
|
108
|
+
|
|
109
|
+
### Simple ETH transfer
|
|
110
|
+
```bash
|
|
111
|
+
clanker-wallet tx --chain 1 --to 0xRecipientAddress --value 1000000000000000 --reason "Send 0.001 ETH"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### ERC-20 token transfer
|
|
115
|
+
```bash
|
|
116
|
+
# transfer(address to, uint256 amount) — selector 0xa9059cbb
|
|
117
|
+
# encode to + amount as 32-byte padded hex
|
|
118
|
+
clanker-wallet tx \
|
|
119
|
+
--chain 1 \
|
|
120
|
+
--to 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 \
|
|
121
|
+
--data 0xa9059cbb000000000000000000000000RecipientAddr00000000000000000000000000000000000000000000000000000000000f4240 \
|
|
122
|
+
--reason "Transfer 1 USDC"
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### ERC-20 approval
|
|
126
|
+
```bash
|
|
127
|
+
# approve(address spender, uint256 amount) — selector 0x095ea7b3
|
|
128
|
+
clanker-wallet tx \
|
|
129
|
+
--chain 1 \
|
|
130
|
+
--to 0xTokenContractAddress \
|
|
131
|
+
--data 0x095ea7b3000000000000000000000000SpenderAddr0000000000000000000000000000000000000000000000000000000000000f4240 \
|
|
132
|
+
--reason "Approve 1 USDC spend for DeFi protocol"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Other Commands
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
# Check pairing status
|
|
139
|
+
clanker-wallet status
|
|
140
|
+
# → {"agent_id":"agent_abc123","paired":true,"wallet":"0x...","relay":"wss://..."}
|
|
141
|
+
|
|
142
|
+
# Show agent identity
|
|
143
|
+
clanker-wallet whoami
|
|
144
|
+
# → {"agent_id":"agent_abc123","public_key":"ab12cd34ef567890...","fingerprint":"ab12 cd34 ef56 7890"}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Troubleshooting
|
|
148
|
+
|
|
149
|
+
- **exit code 4 / "not paired"** — run `clanker-wallet pair '<JSON>'` first
|
|
150
|
+
- **exit code 2 / timeout** — human didn't respond in time, increase `--timeout` or ask them to check the web app
|
|
151
|
+
- **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
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { ClankerWallet } from '../dist/index.js'
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from 'fs'
|
|
4
|
+
import { join } from 'path'
|
|
5
|
+
import { homedir } from 'os'
|
|
6
|
+
|
|
7
|
+
// ── constants ────────────────────────────────────────────────────────
|
|
8
|
+
const STATE_DIR = join(homedir(), '.clanker-wallet')
|
|
9
|
+
const IDENTITY_FILE = join(STATE_DIR, 'identity.json')
|
|
10
|
+
const PAIRING_FILE = join(STATE_DIR, 'pairing.json')
|
|
11
|
+
|
|
12
|
+
const EXIT = { OK: 0, REJECTED: 1, TIMEOUT: 2, CONNECTION: 3, NOT_PAIRED: 4, BAD_ARGS: 5 }
|
|
13
|
+
|
|
14
|
+
// ── helpers ──────────────────────────────────────────────────────────
|
|
15
|
+
const hex = (bytes) => Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('')
|
|
16
|
+
const fromHexStr = (h) => new Uint8Array(h.match(/.{2}/g).map(b => parseInt(b, 16)))
|
|
17
|
+
/** short id the human compares in the web app: first 16 hex (64 bits) of the x25519 pubkey, "ab12 cd34 ef56 7890" */
|
|
18
|
+
const fingerprint = (pubkeyHex) => pubkeyHex.slice(0, 16).toLowerCase().match(/.{1,4}/g).join(' ')
|
|
19
|
+
|
|
20
|
+
function die(msg, code) {
|
|
21
|
+
process.stderr.write(msg + '\n')
|
|
22
|
+
process.exit(code)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function out(obj) {
|
|
26
|
+
process.stdout.write(JSON.stringify(obj) + '\n')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// owner-only; an existing dir from an older version is tightened too
|
|
30
|
+
function ensureDir() {
|
|
31
|
+
if (!existsSync(STATE_DIR)) mkdirSync(STATE_DIR, { recursive: true, mode: 0o700 })
|
|
32
|
+
else chmodSync(STATE_DIR, 0o700)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// identity.json holds the agent secret key: owner-only, tightened if an older version left it readable
|
|
36
|
+
function loadIdentity() {
|
|
37
|
+
if (!existsSync(IDENTITY_FILE)) return null
|
|
38
|
+
ensureDir()
|
|
39
|
+
chmodSync(IDENTITY_FILE, 0o600)
|
|
40
|
+
try { return JSON.parse(readFileSync(IDENTITY_FILE, 'utf8')) }
|
|
41
|
+
catch { return null }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function saveIdentity(data) {
|
|
45
|
+
ensureDir()
|
|
46
|
+
writeFileSync(IDENTITY_FILE, JSON.stringify(data, null, 2) + '\n', { mode: 0o600 })
|
|
47
|
+
chmodSync(IDENTITY_FILE, 0o600)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function loadPairing() {
|
|
51
|
+
if (!existsSync(PAIRING_FILE)) return null
|
|
52
|
+
try { return JSON.parse(readFileSync(PAIRING_FILE, 'utf8')) }
|
|
53
|
+
catch { return null }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function savePairing(data) {
|
|
57
|
+
ensureDir()
|
|
58
|
+
writeFileSync(PAIRING_FILE, JSON.stringify(data, null, 2) + '\n')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** restore or create a ClankerWallet with persisted identity */
|
|
62
|
+
function getWallet() {
|
|
63
|
+
const identity = loadIdentity()
|
|
64
|
+
const secretKey = identity ? fromHexStr(identity.secret_key) : undefined
|
|
65
|
+
const wallet = new ClankerWallet({ secretKey })
|
|
66
|
+
if (!identity) {
|
|
67
|
+
saveIdentity({ secret_key: hex(wallet.secretKey), agent_id: wallet.agentId })
|
|
68
|
+
}
|
|
69
|
+
return wallet
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── arg parsing ──────────────────────────────────────────────────────
|
|
73
|
+
function parseArgs(argv) {
|
|
74
|
+
const args = argv.slice(2)
|
|
75
|
+
const cmd = args[0]
|
|
76
|
+
const positional = []
|
|
77
|
+
const flags = {}
|
|
78
|
+
|
|
79
|
+
for (let i = 1; i < args.length; i++) {
|
|
80
|
+
const a = args[i]
|
|
81
|
+
if (a.startsWith('--')) {
|
|
82
|
+
const key = a.slice(2)
|
|
83
|
+
const next = args[i + 1]
|
|
84
|
+
if (next && !next.startsWith('--')) {
|
|
85
|
+
flags[key] = next
|
|
86
|
+
i++
|
|
87
|
+
} else {
|
|
88
|
+
flags[key] = true
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
positional.push(a)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { cmd, positional, flags }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── commands ─────────────────────────────────────────────────────────
|
|
99
|
+
function cmdPair(positional) {
|
|
100
|
+
const jsonStr = positional[0]
|
|
101
|
+
if (!jsonStr) {
|
|
102
|
+
die('usage: clanker-wallet pair \'<pairing JSON from web app>\'', EXIT.BAD_ARGS)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const wallet = getWallet()
|
|
106
|
+
|
|
107
|
+
let pairingData
|
|
108
|
+
try {
|
|
109
|
+
pairingData = wallet.pair(jsonStr)
|
|
110
|
+
} catch (err) {
|
|
111
|
+
die(`pair failed: ${err.message}`, EXIT.BAD_ARGS)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
savePairing({
|
|
115
|
+
pubkey: pairingData.pubkey,
|
|
116
|
+
relay: pairingData.relay,
|
|
117
|
+
wallet: pairingData.wallet || null,
|
|
118
|
+
mode: pairingData.mode || null,
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
out({
|
|
122
|
+
agent_id: wallet.agentId,
|
|
123
|
+
status: 'paired',
|
|
124
|
+
wallet: pairingData.wallet || null,
|
|
125
|
+
relay: pairingData.relay,
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function cmdTx(flags) {
|
|
130
|
+
const pairing = loadPairing()
|
|
131
|
+
if (!pairing) die('not paired — run: clanker-wallet pair \'<JSON>\'', EXIT.NOT_PAIRED)
|
|
132
|
+
|
|
133
|
+
const chain = flags.chain
|
|
134
|
+
const to = flags.to
|
|
135
|
+
if (!chain) die('missing --chain', EXIT.BAD_ARGS)
|
|
136
|
+
if (!to) die('missing --to', EXIT.BAD_ARGS)
|
|
137
|
+
|
|
138
|
+
const chainId = parseInt(chain, 10)
|
|
139
|
+
if (isNaN(chainId)) die('--chain must be a number', EXIT.BAD_ARGS)
|
|
140
|
+
|
|
141
|
+
const value = flags.value || '0'
|
|
142
|
+
const data = flags.data || '0x'
|
|
143
|
+
const timeout = flags.timeout ? parseInt(flags.timeout, 10) : 3_600_000
|
|
144
|
+
|
|
145
|
+
const context = {}
|
|
146
|
+
if (flags.reason) context.reason = flags.reason
|
|
147
|
+
if (flags.urgency) context.urgency = flags.urgency
|
|
148
|
+
if (flags['expected-outcome']) context.expected_outcome = flags['expected-outcome']
|
|
149
|
+
|
|
150
|
+
const wallet = getWallet()
|
|
151
|
+
const pairingJson = JSON.stringify({
|
|
152
|
+
version: 1,
|
|
153
|
+
pubkey: pairing.pubkey,
|
|
154
|
+
relay: pairing.relay,
|
|
155
|
+
wallet: pairing.wallet,
|
|
156
|
+
mode: pairing.mode || undefined,
|
|
157
|
+
})
|
|
158
|
+
wallet.pair(pairingJson)
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const txHash = await wallet.requestTx({
|
|
162
|
+
chain_id: chainId,
|
|
163
|
+
transaction: { to, value, data },
|
|
164
|
+
context: Object.keys(context).length > 0 ? context : undefined,
|
|
165
|
+
timeout_ms: timeout,
|
|
166
|
+
})
|
|
167
|
+
out({ tx_hash: txHash, status: 'approved' })
|
|
168
|
+
process.exit(EXIT.OK)
|
|
169
|
+
} catch (err) {
|
|
170
|
+
const msg = err.message || String(err)
|
|
171
|
+
if (msg.includes('rejected')) {
|
|
172
|
+
out({ status: 'rejected', error: msg })
|
|
173
|
+
process.exit(EXIT.REJECTED)
|
|
174
|
+
} else if (msg.includes('timed out')) {
|
|
175
|
+
out({ status: 'timeout', error: msg })
|
|
176
|
+
process.exit(EXIT.TIMEOUT)
|
|
177
|
+
} else {
|
|
178
|
+
out({ status: 'error', error: msg })
|
|
179
|
+
process.exit(EXIT.CONNECTION)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function cmdStatus() {
|
|
185
|
+
const identity = loadIdentity()
|
|
186
|
+
const pairing = loadPairing()
|
|
187
|
+
|
|
188
|
+
out({
|
|
189
|
+
agent_id: identity?.agent_id || null,
|
|
190
|
+
paired: !!pairing,
|
|
191
|
+
wallet: pairing?.wallet || null,
|
|
192
|
+
relay: pairing?.relay || null,
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function cmdWhoami() {
|
|
197
|
+
const wallet = getWallet()
|
|
198
|
+
out({
|
|
199
|
+
agent_id: wallet.agentId,
|
|
200
|
+
public_key: hex(wallet.publicKey),
|
|
201
|
+
fingerprint: fingerprint(hex(wallet.publicKey)),
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function showHelp() {
|
|
206
|
+
process.stderr.write(`clanker-wallet — human-approved transactions for AI agents
|
|
207
|
+
|
|
208
|
+
commands:
|
|
209
|
+
pair '<JSON>' pair with human's web app (one-time setup)
|
|
210
|
+
tx send a transaction (blocks until approved/rejected)
|
|
211
|
+
status check pairing status
|
|
212
|
+
whoami show agent identity
|
|
213
|
+
|
|
214
|
+
tx flags:
|
|
215
|
+
--chain <id> chain ID (required) — 1, 137, 8453, etc.
|
|
216
|
+
--to <address> recipient (required)
|
|
217
|
+
--value <wei> value in wei (default: 0)
|
|
218
|
+
--data <hex> calldata (default: 0x)
|
|
219
|
+
--reason <text> shown to human
|
|
220
|
+
--urgency <low|med|high> priority hint
|
|
221
|
+
--expected-outcome <text> what will happen
|
|
222
|
+
--timeout <ms> wait time (default: 3600000)
|
|
223
|
+
|
|
224
|
+
exit codes:
|
|
225
|
+
0 success
|
|
226
|
+
1 rejected by human
|
|
227
|
+
2 timeout
|
|
228
|
+
3 connection error
|
|
229
|
+
4 not paired
|
|
230
|
+
5 invalid arguments
|
|
231
|
+
|
|
232
|
+
examples:
|
|
233
|
+
clanker-wallet pair '{"version":1,"pubkey":"ab12...","relay":"wss://...","wallet":"0x..."}'
|
|
234
|
+
clanker-wallet tx --chain 1 --to 0xRecipient --value 1000000000000000000 --reason "swap ETH"
|
|
235
|
+
clanker-wallet status
|
|
236
|
+
clanker-wallet whoami
|
|
237
|
+
|
|
238
|
+
docs: https://github.com/almogdepaz/clanker-wallet
|
|
239
|
+
web: https://clanker-wallet.xyz
|
|
240
|
+
`)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ── main ─────────────────────────────────────────────────────────────
|
|
244
|
+
const { cmd, positional, flags } = parseArgs(process.argv)
|
|
245
|
+
|
|
246
|
+
switch (cmd) {
|
|
247
|
+
case 'pair':
|
|
248
|
+
cmdPair(positional)
|
|
249
|
+
break
|
|
250
|
+
case 'tx':
|
|
251
|
+
await cmdTx(flags)
|
|
252
|
+
break
|
|
253
|
+
case 'status':
|
|
254
|
+
cmdStatus()
|
|
255
|
+
break
|
|
256
|
+
case 'whoami':
|
|
257
|
+
cmdWhoami()
|
|
258
|
+
break
|
|
259
|
+
case 'help':
|
|
260
|
+
case '--help':
|
|
261
|
+
case '-h':
|
|
262
|
+
showHelp()
|
|
263
|
+
break
|
|
264
|
+
default:
|
|
265
|
+
showHelp()
|
|
266
|
+
break
|
|
267
|
+
}
|
package/dist/client.d.ts
CHANGED
|
@@ -13,11 +13,9 @@ export declare class ClankerWallet extends TypedEmitter {
|
|
|
13
13
|
/** Ed25519 public key for signing TxRequests */
|
|
14
14
|
readonly signingPublicKey: Uint8Array;
|
|
15
15
|
private readonly signingSecretKey;
|
|
16
|
-
private
|
|
17
|
-
private
|
|
18
|
-
private
|
|
19
|
-
private humanWallet;
|
|
20
|
-
private mode;
|
|
16
|
+
private readonly agentPubkeyHex;
|
|
17
|
+
private readonly signingPubkeyHex;
|
|
18
|
+
private pairing;
|
|
21
19
|
constructor(options?: ClankerWalletOptions);
|
|
22
20
|
/** whether this agent has been paired with a human */
|
|
23
21
|
get paired(): boolean;
|
package/dist/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { decryptMessage, encryptMessage, fromHex, generateKeypair, keypairFromSecret, signMessage, signingKeypairFromSeed, toHex } from './crypto.js';
|
|
2
2
|
import { TypedEmitter } from './events.js';
|
|
3
3
|
const DEFAULT_TIMEOUT_MS = 3_600_000; // 1 hour
|
|
4
4
|
export class ClankerWallet extends TypedEmitter {
|
|
@@ -8,11 +8,9 @@ export class ClankerWallet extends TypedEmitter {
|
|
|
8
8
|
/** Ed25519 public key for signing TxRequests */
|
|
9
9
|
signingPublicKey;
|
|
10
10
|
signingSecretKey;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
humanWallet = null;
|
|
15
|
-
mode = 'encrypted';
|
|
11
|
+
agentPubkeyHex;
|
|
12
|
+
signingPubkeyHex;
|
|
13
|
+
pairing = null;
|
|
16
14
|
constructor(options = {}) {
|
|
17
15
|
super();
|
|
18
16
|
const kp = options.secretKey
|
|
@@ -21,14 +19,15 @@ export class ClankerWallet extends TypedEmitter {
|
|
|
21
19
|
this.publicKey = kp.publicKey;
|
|
22
20
|
this.secretKey = kp.secretKey;
|
|
23
21
|
this.agentId = `agent_${toHex(kp.publicKey).slice(0, 12)}`;
|
|
24
|
-
|
|
22
|
+
this.agentPubkeyHex = toHex(kp.publicKey);
|
|
25
23
|
const signingKp = signingKeypairFromSeed(kp.secretKey);
|
|
26
24
|
this.signingPublicKey = signingKp.publicKey;
|
|
27
25
|
this.signingSecretKey = signingKp.secretKey;
|
|
26
|
+
this.signingPubkeyHex = toHex(signingKp.publicKey);
|
|
28
27
|
}
|
|
29
28
|
/** whether this agent has been paired with a human */
|
|
30
29
|
get paired() {
|
|
31
|
-
return this.
|
|
30
|
+
return this.pairing !== null;
|
|
32
31
|
}
|
|
33
32
|
/** parse QR/pairing data and store the human's pubkey + relay url */
|
|
34
33
|
pair(qrData) {
|
|
@@ -39,11 +38,13 @@ export class ClankerWallet extends TypedEmitter {
|
|
|
39
38
|
if (!data.pubkey || !data.relay) {
|
|
40
39
|
throw new Error('invalid pairing data: missing pubkey or relay');
|
|
41
40
|
}
|
|
42
|
-
this.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
this.pairing = {
|
|
42
|
+
humanPubkey: fromHex(data.pubkey),
|
|
43
|
+
humanPubkeyHex: data.pubkey,
|
|
44
|
+
relayUrl: data.relay,
|
|
45
|
+
humanWallet: data.wallet || null,
|
|
46
|
+
mode: data.mode || 'encrypted',
|
|
47
|
+
};
|
|
47
48
|
this.emit('paired', { relay: data.relay, wallet: data.wallet || '' });
|
|
48
49
|
return data;
|
|
49
50
|
}
|
|
@@ -53,9 +54,10 @@ export class ClankerWallet extends TypedEmitter {
|
|
|
53
54
|
* throws on rejection, timeout, or connection error.
|
|
54
55
|
*/
|
|
55
56
|
async requestTx(options) {
|
|
56
|
-
if (!this.
|
|
57
|
+
if (!this.pairing) {
|
|
57
58
|
throw new Error('not paired — call pair() first');
|
|
58
59
|
}
|
|
60
|
+
const { humanPubkey, humanPubkeyHex, relayUrl, humanWallet, mode } = this.pairing;
|
|
59
61
|
const timeoutMs = options.timeout_ms ?? DEFAULT_TIMEOUT_MS;
|
|
60
62
|
const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
61
63
|
const txRequest = {
|
|
@@ -67,17 +69,7 @@ export class ClankerWallet extends TypedEmitter {
|
|
|
67
69
|
context: options.context,
|
|
68
70
|
signature: '', // set below after signing
|
|
69
71
|
};
|
|
70
|
-
|
|
71
|
-
const sigPayload = JSON.stringify(txRequest);
|
|
72
|
-
txRequest.signature = signMessage(sigPayload, this.signingSecretKey);
|
|
73
|
-
const humanPubkey = this.humanPubkey;
|
|
74
|
-
const humanPubkeyHex = this.humanPubkeyHex;
|
|
75
|
-
const relayUrl = this.relayUrl;
|
|
76
|
-
const secretKey = this.secretKey;
|
|
77
|
-
const agentPubkeyHex = toHex(this.publicKey);
|
|
78
|
-
const signingPubkeyHex = toHex(this.signingPublicKey);
|
|
79
|
-
const mode = this.mode;
|
|
80
|
-
const humanWallet = this.humanWallet;
|
|
72
|
+
txRequest.signature = signMessage(JSON.stringify(txRequest), this.signingSecretKey);
|
|
81
73
|
return new Promise((resolve, reject) => {
|
|
82
74
|
let ws;
|
|
83
75
|
let timer;
|
|
@@ -89,11 +81,14 @@ export class ClankerWallet extends TypedEmitter {
|
|
|
89
81
|
ws.close();
|
|
90
82
|
}
|
|
91
83
|
};
|
|
84
|
+
const fail = (message) => {
|
|
85
|
+
cleanup();
|
|
86
|
+
this.emit('error', { message });
|
|
87
|
+
reject(new Error(message));
|
|
88
|
+
};
|
|
92
89
|
timer = setTimeout(() => {
|
|
93
90
|
if (!settled) {
|
|
94
|
-
|
|
95
|
-
this.emit('error', { message: `request ${requestId} timed out after ${timeoutMs}ms` });
|
|
96
|
-
reject(new Error(`request ${requestId} timed out after ${timeoutMs}ms`));
|
|
91
|
+
fail(`request ${requestId} timed out after ${timeoutMs}ms`);
|
|
97
92
|
}
|
|
98
93
|
}, timeoutMs);
|
|
99
94
|
this.emit('connecting', { relay: relayUrl });
|
|
@@ -101,17 +96,12 @@ export class ClankerWallet extends TypedEmitter {
|
|
|
101
96
|
ws = new WebSocket(relayUrl);
|
|
102
97
|
}
|
|
103
98
|
catch (err) {
|
|
104
|
-
|
|
105
|
-
this.emit('error', { message: `failed to connect to relay: ${err}` });
|
|
106
|
-
reject(new Error(`failed to connect to relay: ${err}`));
|
|
99
|
+
fail(`failed to connect to relay: ${err}`);
|
|
107
100
|
return;
|
|
108
101
|
}
|
|
109
102
|
ws.addEventListener('error', () => {
|
|
110
|
-
if (!settled)
|
|
111
|
-
|
|
112
|
-
this.emit('error', { message: 'relay connection error' });
|
|
113
|
-
reject(new Error('relay connection error'));
|
|
114
|
-
}
|
|
103
|
+
if (!settled)
|
|
104
|
+
fail('relay connection error');
|
|
115
105
|
});
|
|
116
106
|
ws.addEventListener('close', () => {
|
|
117
107
|
this.emit('disconnected', {});
|
|
@@ -121,7 +111,6 @@ export class ClankerWallet extends TypedEmitter {
|
|
|
121
111
|
}
|
|
122
112
|
});
|
|
123
113
|
ws.addEventListener('open', () => {
|
|
124
|
-
// join the human's room
|
|
125
114
|
ws.send(JSON.stringify({ type: 'join', room: humanPubkeyHex }));
|
|
126
115
|
});
|
|
127
116
|
ws.addEventListener('message', (event) => {
|
|
@@ -132,23 +121,22 @@ export class ClankerWallet extends TypedEmitter {
|
|
|
132
121
|
msg = JSON.parse(event.data);
|
|
133
122
|
}
|
|
134
123
|
catch {
|
|
135
|
-
return;
|
|
124
|
+
return;
|
|
136
125
|
}
|
|
137
126
|
if (msg.type === 'joined') {
|
|
138
127
|
this.emit('connected', { room: humanPubkeyHex });
|
|
139
|
-
|
|
140
|
-
const plaintext = JSON.stringify(txRequest);
|
|
141
|
-
const ciphertext = encryptMessage(plaintext, humanPubkey, secretKey);
|
|
128
|
+
const ciphertext = encryptMessage(JSON.stringify(txRequest), humanPubkey, this.secretKey);
|
|
142
129
|
const envelope = {
|
|
143
|
-
sender_pubkey: agentPubkeyHex,
|
|
130
|
+
sender_pubkey: this.agentPubkeyHex,
|
|
144
131
|
ciphertext,
|
|
145
|
-
signing_pubkey: signingPubkeyHex,
|
|
132
|
+
signing_pubkey: this.signingPubkeyHex,
|
|
146
133
|
};
|
|
147
134
|
const wsMsg = {
|
|
148
135
|
type: 'message',
|
|
149
136
|
room: humanPubkeyHex,
|
|
150
137
|
payload: JSON.stringify(envelope),
|
|
151
138
|
};
|
|
139
|
+
// managed mode: relay gets plaintext tx params so it can simulate
|
|
152
140
|
if (mode === 'managed') {
|
|
153
141
|
wsMsg.simulate = {
|
|
154
142
|
from: humanWallet || '',
|
|
@@ -163,15 +151,17 @@ export class ClankerWallet extends TypedEmitter {
|
|
|
163
151
|
return;
|
|
164
152
|
}
|
|
165
153
|
if (msg.type === 'message') {
|
|
166
|
-
// try to decrypt as TxResponse
|
|
167
154
|
try {
|
|
168
155
|
const envelope = JSON.parse(msg.payload);
|
|
169
|
-
|
|
156
|
+
// only the paired human may answer
|
|
157
|
+
if (envelope.sender_pubkey?.toLowerCase() !== humanPubkeyHex.toLowerCase())
|
|
158
|
+
return;
|
|
159
|
+
const plaintext = decryptMessage(envelope.ciphertext, humanPubkey, this.secretKey);
|
|
170
160
|
if (!plaintext)
|
|
171
|
-
return;
|
|
161
|
+
return;
|
|
172
162
|
const response = JSON.parse(plaintext);
|
|
173
163
|
if (response.request_id !== requestId)
|
|
174
|
-
return;
|
|
164
|
+
return;
|
|
175
165
|
this.emit('response', {
|
|
176
166
|
request_id: response.request_id,
|
|
177
167
|
status: response.status,
|
|
@@ -194,11 +184,10 @@ export class ClankerWallet extends TypedEmitter {
|
|
|
194
184
|
catch {
|
|
195
185
|
// ignore messages that aren't valid envelopes
|
|
196
186
|
}
|
|
187
|
+
return;
|
|
197
188
|
}
|
|
198
189
|
if (msg.type === 'error') {
|
|
199
|
-
|
|
200
|
-
this.emit('error', { message: `relay error: ${msg.message}` });
|
|
201
|
-
reject(new Error(`relay error: ${msg.message}`));
|
|
190
|
+
fail(`relay error: ${msg.message}`);
|
|
202
191
|
}
|
|
203
192
|
});
|
|
204
193
|
});
|
package/dist/crypto.d.ts
CHANGED
|
@@ -17,4 +17,7 @@ export declare function signingKeypairFromSeed(seed: Uint8Array): nacl.SignKeyPa
|
|
|
17
17
|
export declare function signMessage(message: string, signingSecretKey: Uint8Array): string;
|
|
18
18
|
/** verify a detached Ed25519 signature. signature is base64-encoded. */
|
|
19
19
|
export declare function verifySignature(message: string, base64Signature: string, signingPublicKey: Uint8Array): boolean;
|
|
20
|
+
/** base64 string → Uint8Array */
|
|
21
|
+
export declare function base64ToBytes(b64: string): Uint8Array;
|
|
22
|
+
/** Uint8Array → base64 string */
|
|
20
23
|
export declare function bytesToBase64(bytes: Uint8Array): string;
|
package/dist/crypto.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import nacl from 'tweetnacl';
|
|
2
|
+
const encoder = new TextEncoder();
|
|
3
|
+
const decoder = new TextDecoder();
|
|
2
4
|
/** generate a random x25519 keypair for the agent's identity */
|
|
3
5
|
export function generateKeypair() {
|
|
4
6
|
return nacl.box.keyPair();
|
|
@@ -10,8 +12,7 @@ export function keypairFromSecret(secretKey) {
|
|
|
10
12
|
/** encrypt a message using nacl.box. returns base64(nonce + ciphertext). */
|
|
11
13
|
export function encryptMessage(message, recipientPublicKey, senderSecretKey) {
|
|
12
14
|
const nonce = nacl.randomBytes(nacl.box.nonceLength);
|
|
13
|
-
const
|
|
14
|
-
const ciphertext = nacl.box(encoded, nonce, recipientPublicKey, senderSecretKey);
|
|
15
|
+
const ciphertext = nacl.box(encoder.encode(message), nonce, recipientPublicKey, senderSecretKey);
|
|
15
16
|
const combined = new Uint8Array(nonce.length + ciphertext.length);
|
|
16
17
|
combined.set(nonce);
|
|
17
18
|
combined.set(ciphertext, nonce.length);
|
|
@@ -19,7 +20,13 @@ export function encryptMessage(message, recipientPublicKey, senderSecretKey) {
|
|
|
19
20
|
}
|
|
20
21
|
/** decrypt a nacl.box message. payload = base64(nonce + ciphertext). */
|
|
21
22
|
export function decryptMessage(base64Payload, senderPublicKey, recipientSecretKey) {
|
|
22
|
-
|
|
23
|
+
let raw;
|
|
24
|
+
try {
|
|
25
|
+
raw = base64ToBytes(base64Payload);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
23
30
|
if (raw.length <= nacl.box.nonceLength)
|
|
24
31
|
return null;
|
|
25
32
|
const nonce = raw.slice(0, nacl.box.nonceLength);
|
|
@@ -27,7 +34,7 @@ export function decryptMessage(base64Payload, senderPublicKey, recipientSecretKe
|
|
|
27
34
|
const plaintext = nacl.box.open(ciphertext, nonce, senderPublicKey, recipientSecretKey);
|
|
28
35
|
if (!plaintext)
|
|
29
36
|
return null;
|
|
30
|
-
return
|
|
37
|
+
return decoder.decode(plaintext);
|
|
31
38
|
}
|
|
32
39
|
/** hex-encode bytes (no 0x prefix) */
|
|
33
40
|
export function toHex(bytes) {
|
|
@@ -37,7 +44,13 @@ export function toHex(bytes) {
|
|
|
37
44
|
}
|
|
38
45
|
/** hex string → Uint8Array (strips 0x prefix if present) */
|
|
39
46
|
export function fromHex(hex) {
|
|
40
|
-
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
|
+
}
|
|
41
54
|
const bytes = new Uint8Array(clean.length / 2);
|
|
42
55
|
for (let i = 0; i < bytes.length; i++) {
|
|
43
56
|
bytes[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
|
|
@@ -46,21 +59,24 @@ export function fromHex(hex) {
|
|
|
46
59
|
}
|
|
47
60
|
/** derive an Ed25519 signing keypair from a 32-byte seed */
|
|
48
61
|
export function signingKeypairFromSeed(seed) {
|
|
49
|
-
|
|
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));
|
|
50
67
|
}
|
|
51
68
|
/** sign a message string with an Ed25519 secret key. returns base64(signature). */
|
|
52
69
|
export function signMessage(message, signingSecretKey) {
|
|
53
|
-
const
|
|
54
|
-
const signature = nacl.sign.detached(encoded, signingSecretKey);
|
|
70
|
+
const signature = nacl.sign.detached(encoder.encode(message), signingSecretKey);
|
|
55
71
|
return bytesToBase64(signature);
|
|
56
72
|
}
|
|
57
73
|
/** verify a detached Ed25519 signature. signature is base64-encoded. */
|
|
58
74
|
export function verifySignature(message, base64Signature, signingPublicKey) {
|
|
59
|
-
const encoded = new TextEncoder().encode(message);
|
|
60
75
|
const signature = base64ToBytes(base64Signature);
|
|
61
|
-
return nacl.sign.detached.verify(
|
|
76
|
+
return nacl.sign.detached.verify(encoder.encode(message), signature, signingPublicKey);
|
|
62
77
|
}
|
|
63
|
-
|
|
78
|
+
/** base64 string → Uint8Array */
|
|
79
|
+
export function base64ToBytes(b64) {
|
|
64
80
|
const binary = atob(b64);
|
|
65
81
|
const bytes = new Uint8Array(binary.length);
|
|
66
82
|
for (let i = 0; i < binary.length; i++) {
|
|
@@ -68,6 +84,7 @@ function base64ToBytes(b64) {
|
|
|
68
84
|
}
|
|
69
85
|
return bytes;
|
|
70
86
|
}
|
|
87
|
+
/** Uint8Array → base64 string */
|
|
71
88
|
export function bytesToBase64(bytes) {
|
|
72
89
|
let binary = '';
|
|
73
90
|
for (let i = 0; i < bytes.length; i++) {
|
package/dist/events.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
export class TypedEmitter {
|
|
2
2
|
_listeners = new Map();
|
|
3
3
|
on(event, handler) {
|
|
4
|
-
|
|
5
|
-
if (
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
const existing = this._listeners.get(event);
|
|
5
|
+
if (existing) {
|
|
6
|
+
existing.add(handler);
|
|
7
|
+
}
|
|
8
|
+
else {
|
|
9
|
+
this._listeners.set(event, new Set([handler]));
|
|
8
10
|
}
|
|
9
|
-
set.add(handler);
|
|
10
11
|
return this;
|
|
11
12
|
}
|
|
12
13
|
off(event, handler) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { ClankerWallet } from './client.js';
|
|
2
2
|
export type { ClankerWalletOptions } from './client.js';
|
|
3
|
+
export { base64ToBytes, bytesToBase64, decryptMessage, encryptMessage, fromHex, generateKeypair, keypairFromSecret, signMessage, signingKeypairFromSeed, toHex, verifySignature } from './crypto.js';
|
|
3
4
|
export { TypedEmitter } from './events.js';
|
|
4
5
|
export type { EventMap } from './events.js';
|
|
5
|
-
export {
|
|
6
|
-
export type { Transaction, TxContext, TxRequest, TxResponse, AgentEnvelope, PairingData, RequestTxOptions, } from './types.js';
|
|
6
|
+
export type { AgentEnvelope, PairingData, RequestTxOptions, Transaction, TxContext, TxRequest, TxResponse, } from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { ClankerWallet } from './client.js';
|
|
2
|
+
export { base64ToBytes, bytesToBase64, decryptMessage, encryptMessage, fromHex, generateKeypair, keypairFromSecret, signMessage, signingKeypairFromSeed, toHex, verifySignature } from './crypto.js';
|
|
2
3
|
export { TypedEmitter } from './events.js';
|
|
3
|
-
export { generateKeypair, keypairFromSecret, encryptMessage, decryptMessage, toHex, fromHex, signingKeypairFromSeed, signMessage, verifySignature, bytesToBase64 } from './crypto.js';
|
package/dist/types.d.ts
CHANGED
|
@@ -40,7 +40,7 @@ export interface PairingData {
|
|
|
40
40
|
pubkey: string;
|
|
41
41
|
relay: string;
|
|
42
42
|
wallet: string;
|
|
43
|
-
/**
|
|
43
|
+
/** "encrypted" (default): relay sees only ciphertext. "managed": relay also receives tx params for simulation. */
|
|
44
44
|
mode?: 'encrypted' | 'managed';
|
|
45
45
|
}
|
|
46
46
|
/** options for requestTx */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clanker-wallet",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -10,7 +10,16 @@
|
|
|
10
10
|
"types": "./dist/index.d.ts"
|
|
11
11
|
}
|
|
12
12
|
},
|
|
13
|
-
"
|
|
13
|
+
"bin": {
|
|
14
|
+
"clanker-wallet": "bin/cli.js"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"bin",
|
|
19
|
+
"SKILL.md",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
14
23
|
"license": "MIT",
|
|
15
24
|
"author": "Clanker Wallet Contributors",
|
|
16
25
|
"repository": {
|
|
@@ -18,7 +27,27 @@
|
|
|
18
27
|
"url": "git+https://github.com/almogdepaz/clanker-wallet.git"
|
|
19
28
|
},
|
|
20
29
|
"homepage": "https://github.com/almogdepaz/clanker-wallet#readme",
|
|
21
|
-
"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
|
+
],
|
|
22
51
|
"scripts": {
|
|
23
52
|
"build": "tsc -p tsconfig.build.json",
|
|
24
53
|
"dev": "tsc --watch",
|
|
@@ -31,9 +60,7 @@
|
|
|
31
60
|
},
|
|
32
61
|
"devDependencies": {
|
|
33
62
|
"@types/bun": "^1.3.8",
|
|
34
|
-
"@types/ws": "^8.18.1",
|
|
35
63
|
"typescript": "^5.4.0",
|
|
36
|
-
"vitest": "^2.1.0"
|
|
37
|
-
"ws": "^8.19.0"
|
|
64
|
+
"vitest": "^2.1.0"
|
|
38
65
|
}
|
|
39
66
|
}
|