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