clanker-wallet 0.3.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 +9 -8
- package/SKILL.md +13 -8
- package/bin/cli.js +13 -3
- package/dist/client.js +19 -4
- package/dist/crypto.js +19 -3
- package/dist/types.d.ts +2 -0
- package/package.json +29 -3
package/README.md
CHANGED
|
@@ -17,8 +17,9 @@ bunx clanker-wallet
|
|
|
17
17
|
## Quick Start
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
# 1. Human goes to https://clanker-wallet.xyz, connects wallet,
|
|
21
|
-
#
|
|
20
|
+
# 1. Human goes to https://clanker-wallet.xyz, connects wallet, clicks "copy prompt for agent",
|
|
21
|
+
# pastes the prompt to you. The prompt contains a JSON pairing code.
|
|
22
|
+
# 2. Pair (one-time) — extract the JSON from the prompt
|
|
22
23
|
clanker-wallet pair '{"version":1,"pubkey":"ab12...","relay":"wss://...","wallet":"0x..."}'
|
|
23
24
|
|
|
24
25
|
# 3. Send a transaction (blocks until human approves or rejects)
|
|
@@ -80,11 +81,11 @@ clanker-wallet status
|
|
|
80
81
|
|
|
81
82
|
### `whoami`
|
|
82
83
|
|
|
83
|
-
Show agent identity.
|
|
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.
|
|
84
85
|
|
|
85
86
|
```bash
|
|
86
87
|
clanker-wallet whoami
|
|
87
|
-
# → {"agent_id":"agent_abc123","public_key":"
|
|
88
|
+
# → {"agent_id":"agent_abc123","public_key":"ab12cd34ef567890...","fingerprint":"ab12 cd34 ef56 7890"}
|
|
88
89
|
```
|
|
89
90
|
|
|
90
91
|
## Exit Codes
|
|
@@ -165,13 +166,13 @@ tx_hash = await agent.request_tx(RequestTxOptions(
|
|
|
165
166
|
|
|
166
167
|
## How It Works
|
|
167
168
|
|
|
168
|
-
1. Human opens [clanker-wallet.xyz](https://clanker-wallet.xyz), connects wallet, gets a pairing
|
|
169
|
-
2. Agent runs `clanker-wallet pair` — establishes E2E encrypted channel
|
|
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
|
|
170
171
|
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
|
+
4. Human reviews tx details, simulation, risk flags in the web app. A new agent shows up as unverified with its fingerprint; auto-approve rules only apply after the human trusts it. Contract rules (address + methods) don't limit token amounts or recipients
|
|
172
173
|
5. Human approves → wallet signs + broadcasts → agent gets the tx hash
|
|
173
174
|
|
|
174
|
-
|
|
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.
|
|
175
176
|
|
|
176
177
|
## License
|
|
177
178
|
|
package/SKILL.md
CHANGED
|
@@ -18,22 +18,27 @@ Or use without installing: `npx clanker-wallet` / `bunx clanker-wallet`
|
|
|
18
18
|
|
|
19
19
|
## Pairing (one-time)
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
The human will paste a prompt block from https://clanker-wallet.xyz that looks like this:
|
|
22
22
|
|
|
23
|
-
>
|
|
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..."}`
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
```json
|
|
27
|
-
{"version":1,"pubkey":"ab12cd...","relay":"wss://clanker-wallet-relay-625052130878.us-east1.run.app/ws","wallet":"0x..."}
|
|
28
|
-
```
|
|
28
|
+
Extract the JSON object (the line starting with `{`) and pair:
|
|
29
29
|
|
|
30
|
-
Pair with it:
|
|
31
30
|
```bash
|
|
32
31
|
clanker-wallet pair '{"version":1,"pubkey":"ab12...","relay":"wss://...","wallet":"0x..."}'
|
|
33
32
|
```
|
|
34
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
|
+
|
|
35
36
|
This saves identity and pairing to `~/.clanker-wallet/`. You only need to do this once.
|
|
36
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
|
+
|
|
37
42
|
### Transaction notifications
|
|
38
43
|
|
|
39
44
|
After pairing, ask the human:
|
|
@@ -136,7 +141,7 @@ clanker-wallet status
|
|
|
136
141
|
|
|
137
142
|
# Show agent identity
|
|
138
143
|
clanker-wallet whoami
|
|
139
|
-
# → {"agent_id":"agent_abc123","public_key":"
|
|
144
|
+
# → {"agent_id":"agent_abc123","public_key":"ab12cd34ef567890...","fingerprint":"ab12 cd34 ef56 7890"}
|
|
140
145
|
```
|
|
141
146
|
|
|
142
147
|
## Troubleshooting
|
package/bin/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { ClankerWallet } from '../dist/index.js'
|
|
3
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from 'fs'
|
|
4
4
|
import { join } from 'path'
|
|
5
5
|
import { homedir } from 'os'
|
|
6
6
|
|
|
@@ -14,6 +14,8 @@ const EXIT = { OK: 0, REJECTED: 1, TIMEOUT: 2, CONNECTION: 3, NOT_PAIRED: 4, BAD
|
|
|
14
14
|
// ── helpers ──────────────────────────────────────────────────────────
|
|
15
15
|
const hex = (bytes) => Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('')
|
|
16
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(' ')
|
|
17
19
|
|
|
18
20
|
function die(msg, code) {
|
|
19
21
|
process.stderr.write(msg + '\n')
|
|
@@ -24,19 +26,25 @@ function out(obj) {
|
|
|
24
26
|
process.stdout.write(JSON.stringify(obj) + '\n')
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
// owner-only; an existing dir from an older version is tightened too
|
|
27
30
|
function ensureDir() {
|
|
28
|
-
if (!existsSync(STATE_DIR)) mkdirSync(STATE_DIR, { recursive: true })
|
|
31
|
+
if (!existsSync(STATE_DIR)) mkdirSync(STATE_DIR, { recursive: true, mode: 0o700 })
|
|
32
|
+
else chmodSync(STATE_DIR, 0o700)
|
|
29
33
|
}
|
|
30
34
|
|
|
35
|
+
// identity.json holds the agent secret key: owner-only, tightened if an older version left it readable
|
|
31
36
|
function loadIdentity() {
|
|
32
37
|
if (!existsSync(IDENTITY_FILE)) return null
|
|
38
|
+
ensureDir()
|
|
39
|
+
chmodSync(IDENTITY_FILE, 0o600)
|
|
33
40
|
try { return JSON.parse(readFileSync(IDENTITY_FILE, 'utf8')) }
|
|
34
41
|
catch { return null }
|
|
35
42
|
}
|
|
36
43
|
|
|
37
44
|
function saveIdentity(data) {
|
|
38
45
|
ensureDir()
|
|
39
|
-
writeFileSync(IDENTITY_FILE, JSON.stringify(data, null, 2) + '\n')
|
|
46
|
+
writeFileSync(IDENTITY_FILE, JSON.stringify(data, null, 2) + '\n', { mode: 0o600 })
|
|
47
|
+
chmodSync(IDENTITY_FILE, 0o600)
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
function loadPairing() {
|
|
@@ -145,6 +153,7 @@ async function cmdTx(flags) {
|
|
|
145
153
|
pubkey: pairing.pubkey,
|
|
146
154
|
relay: pairing.relay,
|
|
147
155
|
wallet: pairing.wallet,
|
|
156
|
+
mode: pairing.mode || undefined,
|
|
148
157
|
})
|
|
149
158
|
wallet.pair(pairingJson)
|
|
150
159
|
|
|
@@ -189,6 +198,7 @@ function cmdWhoami() {
|
|
|
189
198
|
out({
|
|
190
199
|
agent_id: wallet.agentId,
|
|
191
200
|
public_key: hex(wallet.publicKey),
|
|
201
|
+
fingerprint: fingerprint(hex(wallet.publicKey)),
|
|
192
202
|
})
|
|
193
203
|
}
|
|
194
204
|
|
package/dist/client.js
CHANGED
|
@@ -43,6 +43,7 @@ export class ClankerWallet extends TypedEmitter {
|
|
|
43
43
|
humanPubkeyHex: data.pubkey,
|
|
44
44
|
relayUrl: data.relay,
|
|
45
45
|
humanWallet: data.wallet || null,
|
|
46
|
+
mode: data.mode || 'encrypted',
|
|
46
47
|
};
|
|
47
48
|
this.emit('paired', { relay: data.relay, wallet: data.wallet || '' });
|
|
48
49
|
return data;
|
|
@@ -56,7 +57,7 @@ export class ClankerWallet extends TypedEmitter {
|
|
|
56
57
|
if (!this.pairing) {
|
|
57
58
|
throw new Error('not paired — call pair() first');
|
|
58
59
|
}
|
|
59
|
-
const { humanPubkey, humanPubkeyHex, relayUrl } = this.pairing;
|
|
60
|
+
const { humanPubkey, humanPubkeyHex, relayUrl, humanWallet, mode } = this.pairing;
|
|
60
61
|
const timeoutMs = options.timeout_ms ?? DEFAULT_TIMEOUT_MS;
|
|
61
62
|
const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
62
63
|
const txRequest = {
|
|
@@ -130,18 +131,32 @@ export class ClankerWallet extends TypedEmitter {
|
|
|
130
131
|
ciphertext,
|
|
131
132
|
signing_pubkey: this.signingPubkeyHex,
|
|
132
133
|
};
|
|
133
|
-
|
|
134
|
+
const wsMsg = {
|
|
134
135
|
type: 'message',
|
|
135
136
|
room: humanPubkeyHex,
|
|
136
137
|
payload: JSON.stringify(envelope),
|
|
137
|
-
}
|
|
138
|
+
};
|
|
139
|
+
// managed mode: relay gets plaintext tx params so it can simulate
|
|
140
|
+
if (mode === 'managed') {
|
|
141
|
+
wsMsg.simulate = {
|
|
142
|
+
from: humanWallet || '',
|
|
143
|
+
to: options.transaction.to,
|
|
144
|
+
value: options.transaction.value,
|
|
145
|
+
data: options.transaction.data,
|
|
146
|
+
chainId: options.chain_id,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
ws.send(JSON.stringify(wsMsg));
|
|
138
150
|
this.emit('request_sent', { request_id: requestId });
|
|
139
151
|
return;
|
|
140
152
|
}
|
|
141
153
|
if (msg.type === 'message') {
|
|
142
154
|
try {
|
|
143
155
|
const envelope = JSON.parse(msg.payload);
|
|
144
|
-
|
|
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);
|
|
145
160
|
if (!plaintext)
|
|
146
161
|
return;
|
|
147
162
|
const response = JSON.parse(plaintext);
|
package/dist/crypto.js
CHANGED
|
@@ -20,7 +20,13 @@ export function encryptMessage(message, recipientPublicKey, senderSecretKey) {
|
|
|
20
20
|
}
|
|
21
21
|
/** decrypt a nacl.box message. payload = base64(nonce + ciphertext). */
|
|
22
22
|
export function decryptMessage(base64Payload, senderPublicKey, recipientSecretKey) {
|
|
23
|
-
|
|
23
|
+
let raw;
|
|
24
|
+
try {
|
|
25
|
+
raw = base64ToBytes(base64Payload);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
24
30
|
if (raw.length <= nacl.box.nonceLength)
|
|
25
31
|
return null;
|
|
26
32
|
const nonce = raw.slice(0, nacl.box.nonceLength);
|
|
@@ -38,7 +44,13 @@ export function toHex(bytes) {
|
|
|
38
44
|
}
|
|
39
45
|
/** hex string → Uint8Array (strips 0x prefix if present) */
|
|
40
46
|
export function fromHex(hex) {
|
|
41
|
-
const clean = hex.startsWith('0x') ? hex.slice(2) : hex;
|
|
47
|
+
const clean = hex.startsWith('0x') || hex.startsWith('0X') ? hex.slice(2) : hex;
|
|
48
|
+
if (clean.length % 2 !== 0) {
|
|
49
|
+
throw new Error('invalid hex string: length must be even');
|
|
50
|
+
}
|
|
51
|
+
if (!/^[0-9a-fA-F]*$/.test(clean)) {
|
|
52
|
+
throw new Error('invalid hex string: contains non-hex characters');
|
|
53
|
+
}
|
|
42
54
|
const bytes = new Uint8Array(clean.length / 2);
|
|
43
55
|
for (let i = 0; i < bytes.length; i++) {
|
|
44
56
|
bytes[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
|
|
@@ -47,7 +59,11 @@ export function fromHex(hex) {
|
|
|
47
59
|
}
|
|
48
60
|
/** derive an Ed25519 signing keypair from a 32-byte seed */
|
|
49
61
|
export function signingKeypairFromSeed(seed) {
|
|
50
|
-
|
|
62
|
+
if (seed.length !== nacl.sign.seedLength) {
|
|
63
|
+
throw new Error(`invalid seed length: expected ${nacl.sign.seedLength} bytes, got ${seed.length}`);
|
|
64
|
+
}
|
|
65
|
+
// copy to keep derivation independent of caller-owned mutable buffers
|
|
66
|
+
return nacl.sign.keyPair.fromSeed(new Uint8Array(seed));
|
|
51
67
|
}
|
|
52
68
|
/** sign a message string with an Ed25519 secret key. returns base64(signature). */
|
|
53
69
|
export function signMessage(message, signingSecretKey) {
|
package/dist/types.d.ts
CHANGED
|
@@ -40,6 +40,8 @@ export interface PairingData {
|
|
|
40
40
|
pubkey: string;
|
|
41
41
|
relay: string;
|
|
42
42
|
wallet: string;
|
|
43
|
+
/** "encrypted" (default): relay sees only ciphertext. "managed": relay also receives tx params for simulation. */
|
|
44
|
+
mode?: 'encrypted' | 'managed';
|
|
43
45
|
}
|
|
44
46
|
/** options for requestTx */
|
|
45
47
|
export interface RequestTxOptions {
|
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",
|
|
@@ -13,7 +13,13 @@
|
|
|
13
13
|
"bin": {
|
|
14
14
|
"clanker-wallet": "bin/cli.js"
|
|
15
15
|
},
|
|
16
|
-
"files": [
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"bin",
|
|
19
|
+
"SKILL.md",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
17
23
|
"license": "MIT",
|
|
18
24
|
"author": "Clanker Wallet Contributors",
|
|
19
25
|
"repository": {
|
|
@@ -21,7 +27,27 @@
|
|
|
21
27
|
"url": "git+https://github.com/almogdepaz/clanker-wallet.git"
|
|
22
28
|
},
|
|
23
29
|
"homepage": "https://github.com/almogdepaz/clanker-wallet#readme",
|
|
24
|
-
"keywords": [
|
|
30
|
+
"keywords": [
|
|
31
|
+
"wallet",
|
|
32
|
+
"ai-agent",
|
|
33
|
+
"ethereum",
|
|
34
|
+
"e2e-encrypted",
|
|
35
|
+
"transaction-approval",
|
|
36
|
+
"blockchain",
|
|
37
|
+
"web3",
|
|
38
|
+
"langchain",
|
|
39
|
+
"human-in-the-loop",
|
|
40
|
+
"evm",
|
|
41
|
+
"agent-wallet",
|
|
42
|
+
"llm",
|
|
43
|
+
"typescript",
|
|
44
|
+
"base",
|
|
45
|
+
"polygon",
|
|
46
|
+
"transaction",
|
|
47
|
+
"approval",
|
|
48
|
+
"defi",
|
|
49
|
+
"vercel-ai-sdk"
|
|
50
|
+
],
|
|
25
51
|
"scripts": {
|
|
26
52
|
"build": "tsc -p tsconfig.build.json",
|
|
27
53
|
"dev": "tsc --watch",
|