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 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, copies pairing JSON
21
- # 2. Pair (one-time)
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":"ab12cd..."}
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 code
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
- All messages are encrypted end-to-end. The relay server never sees plaintext.
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
- Before you can send transactions, you must pair with the human. Ask them:
21
+ The human will paste a prompt block from https://clanker-wallet.xyz that looks like this:
22
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."
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
- 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
- ```
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":"ab12cd..."}
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
- ws.send(JSON.stringify({
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
- const plaintext = decryptMessage(envelope.ciphertext, fromHex(envelope.sender_pubkey), this.secretKey);
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
- const raw = base64ToBytes(base64Payload);
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
- return nacl.sign.keyPair.fromSeed(seed);
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.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": ["dist", "bin", "SKILL.md", "README.md", "LICENSE"],
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": ["wallet", "ai-agent", "ethereum", "e2e-encrypted", "transaction-approval"],
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",