clanker-wallet 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,8 +22,9 @@ bunx clanker-wallet
22
22
  # 2. Pair (one-time) — extract the JSON from the prompt
23
23
  clanker-wallet pair '{"version":1,"pubkey":"ab12...","relay":"wss://...","wallet":"0x..."}'
24
24
 
25
- # 3. Send a transaction (blocks until human approves or rejects)
26
- clanker-wallet tx --chain 1 --to 0xRecipient --value 1000000000000000000 --reason "swap ETH for USDC"
25
+ # 3. Send a transaction (blocks until human approves or rejects; 1 h by default)
26
+ # 0.001 ETH on Base Sepolia (84532) to the burn address. --value is in wei.
27
+ clanker-wallet tx --chain 84532 --to 0x000000000000000000000000000000000000dEaD --value 1000000000000000 --reason "send 0.001 ETH on base sepolia"
27
28
 
28
29
  # 4. Check status anytime
29
30
  clanker-wallet status
@@ -42,33 +43,36 @@ clanker-wallet pair '<pairing JSON from web app>'
42
43
 
43
44
  ### `tx`
44
45
 
45
- Send a transaction request. Blocks until the human approves or rejects.
46
+ Send a transaction request. Blocks until the human approves or rejects, for up to 1 hour by default; `--timeout <ms>` changes that.
46
47
 
47
48
  ```bash
48
49
  clanker-wallet tx \
49
- --chain 1 \
50
- --to 0xRecipient \
51
- --value 1000000000000000000 \
50
+ --chain 84532 \
51
+ --to 0x000000000000000000000000000000000000dEaD \
52
+ --value 1000000000000000 \
52
53
  --data 0x \
53
- --reason "swap ETH for USDC" \
54
+ --reason "send 0.001 ETH on base sepolia" \
54
55
  --urgency medium \
55
- --timeout 120000
56
+ --timeout 600000
56
57
  # approved → {"tx_hash":"0x...","status":"approved"}
57
58
  # rejected → {"status":"rejected","error":"..."} (exit code 1)
58
59
  # timeout → {"status":"timeout","error":"..."} (exit code 2)
60
+ # bad flag → "invalid --to: ..." on stderr, nothing sent (exit code 5)
59
61
  ```
60
62
 
63
+ `--to` must be `0x` + 40 hex characters (mixed case must pass the EIP-55 checksum), `--value` a whole number of wei, `--data` even-length hex. See [SKILL.md](./SKILL.md) for ERC-20 examples.
64
+
61
65
  **Flags:**
62
66
  | Flag | Required | Default | Description |
63
67
  |------|----------|---------|-------------|
64
- | `--chain <id>` | yes | — | Chain ID (1=Ethereum, 137=Polygon, 8453=Base) |
68
+ | `--chain <id>` | yes | — | Chain ID (1=Ethereum, 8453=Base, 84532=Base Sepolia) |
65
69
  | `--to <address>` | yes | — | Recipient address |
66
70
  | `--value <wei>` | no | `0` | Value in wei (1 ETH = `1000000000000000000`) |
67
71
  | `--data <hex>` | no | `0x` | Calldata hex |
68
72
  | `--reason <text>` | no | — | Shown to human |
69
73
  | `--urgency <level>` | no | — | `low`, `medium`, or `high` |
70
74
  | `--expected-outcome <text>` | no | — | What will happen |
71
- | `--timeout <ms>` | no | `3600000` | How long to wait |
75
+ | `--timeout <ms>` | no | `3600000` (1 h) | How long `tx` blocks waiting for the human |
72
76
 
73
77
  ### `status`
74
78
 
@@ -97,7 +101,7 @@ clanker-wallet whoami
97
101
  | 2 | Timeout |
98
102
  | 3 | Connection/relay error |
99
103
  | 4 | Not paired |
100
- | 5 | Invalid arguments |
104
+ | 5 | Invalid arguments (bad or missing flag; nothing sent) |
101
105
 
102
106
  ## State
103
107
 
@@ -119,6 +123,7 @@ Identity and pairing are persisted in `~/.clanker-wallet/`:
119
123
  | Base | 8453 |
120
124
  | Optimism | 10 |
121
125
  | Sepolia (testnet) | 11155111 |
126
+ | Base Sepolia (testnet) | 84532 |
122
127
 
123
128
  ---
124
129
 
@@ -136,17 +141,22 @@ import { ClankerWallet } from 'clanker-wallet'
136
141
  const wallet = new ClankerWallet({ agentName: 'my-bot' })
137
142
  wallet.pair(pairingJson)
138
143
 
144
+ // 0.001 ETH on Base Sepolia; value is wei (string or bigint)
139
145
  const txHash = await wallet.requestTx({
140
- chain_id: 1,
141
- transaction: { to: '0x...', value: '1000000000000000000', data: '0x' },
142
- context: { reason: 'Buy tokens', urgency: 'medium' },
146
+ chain_id: 84532,
147
+ transaction: { to: '0x000000000000000000000000000000000000dEaD', value: '1000000000000000', data: '0x' },
148
+ context: { reason: 'send 0.001 ETH on base sepolia', urgency: 'medium' },
143
149
  })
144
150
  ```
145
151
 
152
+ `requestTx` throws `InvalidTxError` (with `field` and `reason`) before anything is sent if `chain_id`, `to`, `value` or `data` is malformed.
153
+
146
154
  See full API: [`src/`](./src/)
147
155
 
148
156
  ## Python SDK
149
157
 
158
+ Requires Python 3.10+.
159
+
150
160
  ```bash
151
161
  pip install clanker-wallet
152
162
  ```
@@ -158,9 +168,9 @@ agent = ClankerWallet()
158
168
  agent.pair(pairing_json)
159
169
 
160
170
  tx_hash = await agent.request_tx(RequestTxOptions(
161
- chain_id=1,
162
- transaction=Transaction(to="0x...", value="1000000000000000000", data="0x"),
163
- context=TxContext(reason="Buy tokens"),
171
+ chain_id=84532, # base sepolia
172
+ transaction=Transaction(to="0x000000000000000000000000000000000000dEaD", value="1000000000000000", data="0x"),
173
+ context=TxContext(reason="send 0.001 ETH on base sepolia"),
164
174
  ))
165
175
  ```
166
176
 
package/SKILL.md CHANGED
@@ -18,6 +18,14 @@ Or use without installing: `npx clanker-wallet` / `bunx clanker-wallet`
18
18
 
19
19
  ## Pairing (one-time)
20
20
 
21
+ Before you can send transactions, check if you're already paired:
22
+
23
+ ```bash
24
+ clanker-wallet status
25
+ ```
26
+
27
+ If `paired: true`, skip pairing.
28
+
21
29
  The human will paste a prompt block from https://clanker-wallet.xyz that looks like this:
22
30
 
23
31
  > read the setup guide first: https://clanker-wallet.xyz/llms.txt
@@ -53,7 +61,7 @@ If they agree, **every time you send a `tx` command**, immediately send a messag
53
61
 
54
62
  Example notification:
55
63
  > Transaction awaiting your approval
56
- > I'm requesting to send 0.001 ETH on Sepolia to a test address.
64
+ > I'm requesting to send 0.001 ETH on Base Sepolia to a test address.
57
65
  > Urgency: normal
58
66
  > [Approve or reject here](https://clanker-wallet.xyz)
59
67
 
@@ -61,37 +69,41 @@ Example notification:
61
69
 
62
70
  ```bash
63
71
  clanker-wallet tx \
64
- --chain 1 \
65
- --to 0xRecipientAddress \
72
+ --chain 84532 \
73
+ --to 0x000000000000000000000000000000000000dEaD \
66
74
  --value 1000000000000000 \
67
- --reason "Send 0.001 ETH to recipient" \
75
+ --reason "Send 0.001 ETH on Base Sepolia to a test address" \
68
76
  --urgency medium \
69
- --timeout 120000
77
+ --timeout 600000
70
78
  ```
71
79
 
72
- The command **blocks until the human approves or rejects**. Output is JSON:
80
+ This sends 0.001 ETH (`1000000000000000` wei) on Base Sepolia (chain `84532`) to the burn address `0x000000000000000000000000000000000000dEaD`.
81
+
82
+ The command **blocks until the human approves or rejects**, for up to 1 hour by default. Pass `--timeout <ms>` to wait a different time (`600000` = 10 minutes). Output is JSON:
73
83
  - Approved: `{"tx_hash":"0x...","status":"approved"}` (exit code 0)
74
84
  - Rejected: `{"status":"rejected","error":"..."}` (exit code 1)
75
85
  - Timeout: `{"status":"timeout","error":"..."}` (exit code 2)
86
+ - Invalid `--chain`, `--to`, `--value`, `--data` or `--timeout`: a one-line `invalid --<flag>: ...` message on stderr and nothing is sent (exit code 5)
76
87
 
77
88
  ## Critical Details
78
89
 
79
- - **value is in WEI, not ETH.** 1 ETH = `1000000000000000000`. Use string, not number.
80
- - **data is hex-encoded calldata.** Use `0x` for simple ETH transfers. For contract calls, encode the function call.
90
+ - **value is in WEI, not ETH.** 1 ETH = `1000000000000000000`. A whole number: no decimals, no `1e18`.
91
+ - **data is hex-encoded calldata.** Use `0x` for simple ETH transfers. For contract calls, encode the function call (see ERC-20 transfer below). Never put placeholder text inside the hex.
92
+ - **to must be a real address**: `0x` + 40 hex characters. Mixed-case addresses must carry a valid EIP-55 checksum.
81
93
  - **The human must be online to approve.** Set a reasonable `--timeout`. Always notify the human through your communication channel when you send a request (if they opted in).
82
94
 
83
95
  ## tx Flags
84
96
 
85
97
  | Flag | Required | Default | Description |
86
98
  |------|----------|---------|-------------|
87
- | `--chain <id>` | yes | — | Chain ID (1=Ethereum, 137=Polygon, 8453=Base) |
99
+ | `--chain <id>` | yes | — | Chain ID (1=Ethereum, 8453=Base, 84532=Base Sepolia) |
88
100
  | `--to <address>` | yes | — | Recipient address |
89
101
  | `--value <wei>` | no | `0` | Value in wei |
90
102
  | `--data <hex>` | no | `0x` | Calldata hex |
91
103
  | `--reason <text>` | no | — | Shown to human |
92
104
  | `--urgency <level>` | no | — | `low`, `medium`, or `high` |
93
105
  | `--expected-outcome <text>` | no | — | What will happen |
94
- | `--timeout <ms>` | no | `3600000` | How long to wait |
106
+ | `--timeout <ms>` | no | `3600000` (1 h) | How long `tx` blocks waiting for the human |
95
107
 
96
108
  ## Common Chain IDs
97
109
 
@@ -103,33 +115,37 @@ The command **blocks until the human approves or rejects**. Output is JSON:
103
115
  | Base | 8453 |
104
116
  | Optimism | 10 |
105
117
  | Sepolia (testnet) | 11155111 |
118
+ | Base Sepolia (testnet) | 84532 |
106
119
 
107
120
  ## Common Patterns
108
121
 
109
122
  ### Simple ETH transfer
110
123
  ```bash
111
- clanker-wallet tx --chain 1 --to 0xRecipientAddress --value 1000000000000000 --reason "Send 0.001 ETH"
124
+ clanker-wallet tx --chain 84532 --to 0x000000000000000000000000000000000000dEaD --value 1000000000000000 --reason "Send 0.001 ETH"
112
125
  ```
113
126
 
114
127
  ### ERC-20 token transfer
128
+ Calldata is the 4-byte selector followed by each argument as a 32-byte word: the address left-padded with zeros, the amount as big-endian hex in the token's smallest unit. This sends 0.001 WETH (`1000000000000000` = `0x38d7ea4c68000`, 18 decimals) on Base Sepolia, where WETH is `0x4200000000000000000000000000000000000006`:
129
+
115
130
  ```bash
116
131
  # transfer(address to, uint256 amount) — selector 0xa9059cbb
117
- # encode to + amount as 32-byte padded hex
118
132
  clanker-wallet tx \
119
- --chain 1 \
120
- --to 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 \
121
- --data 0xa9059cbb000000000000000000000000RecipientAddr00000000000000000000000000000000000000000000000000000000000f4240 \
122
- --reason "Transfer 1 USDC"
133
+ --chain 84532 \
134
+ --to 0x4200000000000000000000000000000000000006 \
135
+ --data 0xa9059cbb000000000000000000000000000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000038d7ea4c68000 \
136
+ --reason "Transfer 0.001 WETH to a test address"
123
137
  ```
124
138
 
139
+ Encode it with a library rather than by hand, e.g. `cast calldata "transfer(address,uint256)" 0x000000000000000000000000000000000000dEaD 1000000000000000` (foundry) or viem `encodeFunctionData({ abi: erc20Abi, functionName: 'transfer', args: [to, amount] })`.
140
+
125
141
  ### ERC-20 approval
126
142
  ```bash
127
143
  # approve(address spender, uint256 amount) — selector 0x095ea7b3
128
144
  clanker-wallet tx \
129
- --chain 1 \
130
- --to 0xTokenContractAddress \
131
- --data 0x095ea7b3000000000000000000000000SpenderAddr0000000000000000000000000000000000000000000000000000000000000f4240 \
132
- --reason "Approve 1 USDC spend for DeFi protocol"
145
+ --chain 84532 \
146
+ --to 0x4200000000000000000000000000000000000006 \
147
+ --data 0x095ea7b3000000000000000000000000000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000038d7ea4c68000 \
148
+ --reason "Approve a test address to spend 0.001 WETH"
133
149
  ```
134
150
 
135
151
  ## Other Commands
@@ -146,6 +162,7 @@ clanker-wallet whoami
146
162
 
147
163
  ## Troubleshooting
148
164
 
165
+ - **exit code 5 / "invalid --flag: ..."** — fix the named flag; nothing was sent. Usually a placeholder `--to`, a `--value` in ETH instead of wei, or `--data` that isn't even-length hex
149
166
  - **exit code 4 / "not paired"** — run `clanker-wallet pair '<JSON>'` first
150
167
  - **exit code 2 / timeout** — human didn't respond in time, increase `--timeout` or ask them to check the web app
151
168
  - **exit code 1 / "rejected"** — human clicked reject. Explain why the tx is needed and try again if appropriate.
package/bin/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { ClankerWallet } from '../dist/index.js'
2
+ import { ClankerWallet, InvalidTxError, validateTx } from '../dist/index.js'
3
3
  import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from 'fs'
4
4
  import { join } from 'path'
5
5
  import { homedir } from 'os'
@@ -10,6 +10,9 @@ const IDENTITY_FILE = join(STATE_DIR, 'identity.json')
10
10
  const PAIRING_FILE = join(STATE_DIR, 'pairing.json')
11
11
 
12
12
  const EXIT = { OK: 0, REJECTED: 1, TIMEOUT: 2, CONNECTION: 3, NOT_PAIRED: 4, BAD_ARGS: 5 }
13
+ const DEFAULT_TIMEOUT_MS = 3_600_000
14
+ /** validateTx field -> the tx flag that sets it */
15
+ const FIELD_FLAGS = { chain_id: '--chain', to: '--to', value: '--value', data: '--data' }
13
16
 
14
17
  // ── helpers ──────────────────────────────────────────────────────────
15
18
  const hex = (bytes) => Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('')
@@ -135,12 +138,20 @@ async function cmdTx(flags) {
135
138
  if (!chain) die('missing --chain', EXIT.BAD_ARGS)
136
139
  if (!to) die('missing --to', EXIT.BAD_ARGS)
137
140
 
138
- const chainId = parseInt(chain, 10)
139
- if (isNaN(chainId)) die('--chain must be a number', EXIT.BAD_ARGS)
141
+ if (!/^[0-9]+$/.test(chain)) die(`invalid --chain: must be a positive integer chain id, got ${JSON.stringify(chain)}`, EXIT.BAD_ARGS)
142
+ if (flags.timeout !== undefined && !/^[1-9][0-9]*$/.test(flags.timeout)) {
143
+ die(`invalid --timeout: must be a positive number of milliseconds, got ${JSON.stringify(flags.timeout)}`, EXIT.BAD_ARGS)
144
+ }
145
+ const timeout = flags.timeout ? Number(flags.timeout) : DEFAULT_TIMEOUT_MS
140
146
 
141
- const value = flags.value || '0'
142
- const data = flags.data || '0x'
143
- const timeout = flags.timeout ? parseInt(flags.timeout, 10) : 3_600_000
147
+ // same checks requestTx runs, done here so a bad flag exits 5 instead of surfacing as a connection error
148
+ let request
149
+ try {
150
+ request = validateTx(Number(chain), { to, value: flags.value ?? '0', data: flags.data ?? '0x' })
151
+ } catch (err) {
152
+ if (!(err instanceof InvalidTxError)) throw err
153
+ die(`invalid ${FIELD_FLAGS[err.field]}: ${err.reason}`, EXIT.BAD_ARGS)
154
+ }
144
155
 
145
156
  const context = {}
146
157
  if (flags.reason) context.reason = flags.reason
@@ -159,8 +170,8 @@ async function cmdTx(flags) {
159
170
 
160
171
  try {
161
172
  const txHash = await wallet.requestTx({
162
- chain_id: chainId,
163
- transaction: { to, value, data },
173
+ chain_id: request.chain_id,
174
+ transaction: request.transaction,
164
175
  context: Object.keys(context).length > 0 ? context : undefined,
165
176
  timeout_ms: timeout,
166
177
  })
@@ -202,6 +213,11 @@ function cmdWhoami() {
202
213
  })
203
214
  }
204
215
 
216
+ function cmdVersion() {
217
+ const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
218
+ process.stdout.write(version + '\n')
219
+ }
220
+
205
221
  function showHelp() {
206
222
  process.stderr.write(`clanker-wallet — human-approved transactions for AI agents
207
223
 
@@ -212,14 +228,14 @@ commands:
212
228
  whoami show agent identity
213
229
 
214
230
  tx flags:
215
- --chain <id> 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)
231
+ --chain <id> chain ID (required) — 1, 8453, 84532 (base sepolia), etc.
232
+ --to <address> recipient (required)
233
+ --value <wei> value in wei, not ETH (default: 0; 1 ETH = 1000000000000000000)
234
+ --data <hex> calldata (default: 0x)
235
+ --reason <text> shown to human
236
+ --urgency <low|medium|high> priority hint
237
+ --expected-outcome <text> what will happen
238
+ --timeout <ms> how long tx blocks waiting for the human (default: 3600000 = 1 h)
223
239
 
224
240
  exit codes:
225
241
  0 success
@@ -231,11 +247,11 @@ exit codes:
231
247
 
232
248
  examples:
233
249
  clanker-wallet pair '{"version":1,"pubkey":"ab12...","relay":"wss://...","wallet":"0x..."}'
234
- clanker-wallet tx --chain 1 --to 0xRecipient --value 1000000000000000000 --reason "swap ETH"
250
+ clanker-wallet tx --chain 84532 --to 0x000000000000000000000000000000000000dEaD --value 1000000000000000 --reason "send 0.001 ETH on base sepolia"
235
251
  clanker-wallet status
236
252
  clanker-wallet whoami
237
253
 
238
- docs: https://github.com/almogdepaz/clanker-wallet
254
+ docs: https://clanker-wallet.xyz/SKILL.md
239
255
  web: https://clanker-wallet.xyz
240
256
  `)
241
257
  }
@@ -256,6 +272,10 @@ switch (cmd) {
256
272
  case 'whoami':
257
273
  cmdWhoami()
258
274
  break
275
+ case '--version':
276
+ case '-v':
277
+ cmdVersion()
278
+ break
259
279
  case 'help':
260
280
  case '--help':
261
281
  case '-h':
package/dist/client.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { decryptMessage, encryptMessage, fromHex, generateKeypair, keypairFromSecret, signMessage, signingKeypairFromSeed, toHex } from './crypto.js';
2
2
  import { TypedEmitter } from './events.js';
3
+ import { validateTx } from './validate.js';
3
4
  const DEFAULT_TIMEOUT_MS = 3_600_000; // 1 hour
4
5
  export class ClankerWallet extends TypedEmitter {
5
6
  publicKey;
@@ -57,6 +58,8 @@ export class ClankerWallet extends TypedEmitter {
57
58
  if (!this.pairing) {
58
59
  throw new Error('not paired — call pair() first');
59
60
  }
61
+ // reject malformed requests before anything reaches the relay
62
+ const { chain_id, transaction } = validateTx(options.chain_id, options.transaction);
60
63
  const { humanPubkey, humanPubkeyHex, relayUrl, humanWallet, mode } = this.pairing;
61
64
  const timeoutMs = options.timeout_ms ?? DEFAULT_TIMEOUT_MS;
62
65
  const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
@@ -64,8 +67,8 @@ export class ClankerWallet extends TypedEmitter {
64
67
  request_id: requestId,
65
68
  agent_id: this.agentId,
66
69
  timestamp: new Date().toISOString(),
67
- chain_id: options.chain_id,
68
- transaction: options.transaction,
70
+ chain_id,
71
+ transaction,
69
72
  context: options.context,
70
73
  signature: '', // set below after signing
71
74
  };
@@ -140,10 +143,10 @@ export class ClankerWallet extends TypedEmitter {
140
143
  if (mode === 'managed') {
141
144
  wsMsg.simulate = {
142
145
  from: humanWallet || '',
143
- to: options.transaction.to,
144
- value: options.transaction.value,
145
- data: options.transaction.data,
146
- chainId: options.chain_id,
146
+ to: transaction.to,
147
+ value: transaction.value,
148
+ data: transaction.data,
149
+ chainId: chain_id,
147
150
  };
148
151
  }
149
152
  ws.send(JSON.stringify(wsMsg));
package/dist/index.d.ts CHANGED
@@ -3,4 +3,6 @@ export type { ClankerWalletOptions } from './client.js';
3
3
  export { base64ToBytes, bytesToBase64, decryptMessage, encryptMessage, fromHex, generateKeypair, keypairFromSecret, signMessage, signingKeypairFromSeed, toHex, verifySignature } from './crypto.js';
4
4
  export { TypedEmitter } from './events.js';
5
5
  export type { EventMap } from './events.js';
6
- export type { AgentEnvelope, PairingData, RequestTxOptions, Transaction, TxContext, TxRequest, TxResponse, } from './types.js';
6
+ export { InvalidTxError, isChecksumAddress, validateTx } from './validate.js';
7
+ export type { TxField } from './validate.js';
8
+ export type { AgentEnvelope, PairingData, RequestTxOptions, Transaction, TxContext, TxInput, TxRequest, TxResponse, } from './types.js';
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { ClankerWallet } from './client.js';
2
2
  export { base64ToBytes, bytesToBase64, decryptMessage, encryptMessage, fromHex, generateKeypair, keypairFromSecret, signMessage, signingKeypairFromSeed, toHex, verifySignature } from './crypto.js';
3
3
  export { TypedEmitter } from './events.js';
4
+ export { InvalidTxError, isChecksumAddress, validateTx } from './validate.js';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * keccak-256 (the pre-NIST padding ethereum uses; NIST sha3-256 differs), only for
3
+ * EIP-55 address checksums. inputs are a few dozen bytes, so lanes are plain bigints.
4
+ */
5
+ export declare function keccak256(input: Uint8Array): Uint8Array;
package/dist/keccak.js ADDED
@@ -0,0 +1,82 @@
1
+ /**
2
+ * keccak-256 (the pre-NIST padding ethereum uses; NIST sha3-256 differs), only for
3
+ * EIP-55 address checksums. inputs are a few dozen bytes, so lanes are plain bigints.
4
+ */
5
+ const MASK64 = (1n << 64n) - 1n;
6
+ const RATE_BYTES = 136;
7
+ const rotl = (x, n) => n === 0 ? x : ((x << BigInt(n)) | (x >> BigInt(64 - n))) & MASK64;
8
+ // round constants from the degree-8 LFSR in the keccak reference
9
+ const ROUND_CONSTANTS = (() => {
10
+ const out = [];
11
+ let r = 1;
12
+ for (let round = 0; round < 24; round++) {
13
+ let rc = 0n;
14
+ for (let j = 0; j < 7; j++) {
15
+ if (r & 1)
16
+ rc |= 1n << BigInt((1 << j) - 1);
17
+ r = r & 0x80 ? ((r << 1) ^ 0x71) & 0xff : (r << 1) & 0xff;
18
+ }
19
+ out.push(rc);
20
+ }
21
+ return out;
22
+ })();
23
+ // rho offsets, indexed by x + 5y
24
+ const ROTATIONS = (() => {
25
+ const out = new Array(25).fill(0);
26
+ let x = 1;
27
+ let y = 0;
28
+ for (let t = 0; t < 24; t++) {
29
+ out[x + 5 * y] = (((t + 1) * (t + 2)) / 2) % 64;
30
+ [x, y] = [y, (2 * x + 3 * y) % 5];
31
+ }
32
+ return out;
33
+ })();
34
+ function keccakF(a) {
35
+ const b = new Array(25);
36
+ const c = new Array(5);
37
+ for (let round = 0; round < 24; round++) {
38
+ // theta
39
+ for (let x = 0; x < 5; x++)
40
+ c[x] = a[x] ^ a[x + 5] ^ a[x + 10] ^ a[x + 15] ^ a[x + 20];
41
+ for (let x = 0; x < 5; x++) {
42
+ const d = c[(x + 4) % 5] ^ rotl(c[(x + 1) % 5], 1);
43
+ for (let y = 0; y < 25; y += 5)
44
+ a[x + y] ^= d;
45
+ }
46
+ // rho + pi
47
+ for (let x = 0; x < 5; x++) {
48
+ for (let y = 0; y < 5; y++) {
49
+ b[y + 5 * ((2 * x + 3 * y) % 5)] = rotl(a[x + 5 * y], ROTATIONS[x + 5 * y]);
50
+ }
51
+ }
52
+ // chi
53
+ for (let y = 0; y < 25; y += 5) {
54
+ for (let x = 0; x < 5; x++) {
55
+ a[x + y] = b[x + y] ^ (~b[((x + 1) % 5) + y] & MASK64 & b[((x + 2) % 5) + y]);
56
+ }
57
+ }
58
+ // iota
59
+ a[0] ^= ROUND_CONSTANTS[round];
60
+ }
61
+ }
62
+ export function keccak256(input) {
63
+ // pad10*1 with keccak's 0x01 domain byte
64
+ const padded = new Uint8Array(Math.floor(input.length / RATE_BYTES + 1) * RATE_BYTES);
65
+ padded.set(input);
66
+ padded[input.length] ^= 0x01;
67
+ padded[padded.length - 1] ^= 0x80;
68
+ const state = new Array(25).fill(0n);
69
+ for (let off = 0; off < padded.length; off += RATE_BYTES) {
70
+ for (let i = 0; i < RATE_BYTES / 8; i++) {
71
+ let lane = 0n;
72
+ for (let k = 7; k >= 0; k--)
73
+ lane = (lane << 8n) | BigInt(padded[off + i * 8 + k]);
74
+ state[i] ^= lane;
75
+ }
76
+ keccakF(state);
77
+ }
78
+ const out = new Uint8Array(32);
79
+ for (let i = 0; i < 32; i++)
80
+ out[i] = Number((state[i >> 3] >> BigInt(8 * (i & 7))) & 0xffn);
81
+ return out;
82
+ }
package/dist/types.d.ts CHANGED
@@ -4,6 +4,12 @@ export interface Transaction {
4
4
  value: string;
5
5
  data: string;
6
6
  }
7
+ /** transaction as passed to requestTx: value in wei, data defaults to "0x" */
8
+ export interface TxInput {
9
+ to: string;
10
+ value: string | bigint;
11
+ data?: string;
12
+ }
7
13
  /** optional context about the transaction */
8
14
  export interface TxContext {
9
15
  reason?: string;
@@ -46,7 +52,8 @@ export interface PairingData {
46
52
  /** options for requestTx */
47
53
  export interface RequestTxOptions {
48
54
  chain_id: number;
49
- transaction: Transaction;
55
+ /** checked before sending; an invalid field throws InvalidTxError */
56
+ transaction: TxInput;
50
57
  context?: TxContext;
51
58
  /** timeout in ms (default: 3_600_000 = 1hr) */
52
59
  timeout_ms?: number;
@@ -0,0 +1,19 @@
1
+ import type { Transaction, TxInput } from './types.js';
2
+ export type TxField = 'chain_id' | 'to' | 'value' | 'data';
3
+ /** a tx request field failed validation; nothing was sent */
4
+ export declare class InvalidTxError extends Error {
5
+ readonly field: TxField;
6
+ readonly reason: string;
7
+ constructor(field: TxField, reason: string);
8
+ }
9
+ /** EIP-55: a mixed-case address must carry the checksum; single-case addresses are unchecksummed */
10
+ export declare function isChecksumAddress(address: string): boolean;
11
+ /**
12
+ * check a tx request before anything is signed or sent, and normalize it to the wire
13
+ * format (value as a decimal wei string, data defaulting to "0x").
14
+ * throws InvalidTxError naming the first bad field.
15
+ */
16
+ export declare function validateTx(chainId: unknown, tx: TxInput | Record<string, unknown>): {
17
+ chain_id: number;
18
+ transaction: Transaction;
19
+ };
@@ -0,0 +1,65 @@
1
+ import { keccak256 } from './keccak.js';
2
+ /** a tx request field failed validation; nothing was sent */
3
+ export class InvalidTxError extends Error {
4
+ field;
5
+ reason;
6
+ constructor(field, reason) {
7
+ super(`invalid ${field}: ${reason}`);
8
+ this.field = field;
9
+ this.reason = reason;
10
+ this.name = 'InvalidTxError';
11
+ }
12
+ }
13
+ const ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
14
+ const DECIMAL_RE = /^[0-9]+$/;
15
+ const CALLDATA_RE = /^0x(?:[0-9a-fA-F]{2})*$/;
16
+ /** short, quoted rendering of a rejected input for error messages */
17
+ function shown(v) {
18
+ const s = typeof v === 'bigint' ? `${v}n` : JSON.stringify(v) ?? String(v);
19
+ return s.length > 70 ? `${s.slice(0, 67)}...` : s;
20
+ }
21
+ /** EIP-55: a mixed-case address must carry the checksum; single-case addresses are unchecksummed */
22
+ export function isChecksumAddress(address) {
23
+ if (!ADDRESS_RE.test(address))
24
+ return false;
25
+ const body = address.slice(2);
26
+ if (body === body.toLowerCase() || body === body.toUpperCase())
27
+ return true;
28
+ const lower = body.toLowerCase();
29
+ const hash = keccak256(new TextEncoder().encode(lower));
30
+ for (let i = 0; i < 40; i++) {
31
+ const nibble = (hash[i >> 1] >> (i % 2 === 0 ? 4 : 0)) & 0xf;
32
+ const expected = nibble >= 8 ? lower[i].toUpperCase() : lower[i];
33
+ if (body[i] !== expected)
34
+ return false;
35
+ }
36
+ return true;
37
+ }
38
+ /**
39
+ * check a tx request before anything is signed or sent, and normalize it to the wire
40
+ * format (value as a decimal wei string, data defaulting to "0x").
41
+ * throws InvalidTxError naming the first bad field.
42
+ */
43
+ export function validateTx(chainId, tx) {
44
+ if (typeof chainId !== 'number' || !Number.isSafeInteger(chainId) || chainId <= 0) {
45
+ throw new InvalidTxError('chain_id', `must be a positive integer, got ${shown(chainId)}`);
46
+ }
47
+ const { to, value, data = '0x' } = tx;
48
+ if (typeof to !== 'string' || !ADDRESS_RE.test(to)) {
49
+ throw new InvalidTxError('to', `must be 0x followed by 40 hex characters, got ${shown(to)}`);
50
+ }
51
+ if (!isChecksumAddress(to)) {
52
+ throw new InvalidTxError('to', `mixed-case address fails its EIP-55 checksum, got ${shown(to)}`);
53
+ }
54
+ let wei;
55
+ if (typeof value === 'bigint' && value >= 0n)
56
+ wei = value.toString();
57
+ else if (typeof value === 'string' && DECIMAL_RE.test(value))
58
+ wei = value;
59
+ else
60
+ throw new InvalidTxError('value', `must be a non-negative integer amount of wei (decimal string or bigint), got ${shown(value)}`);
61
+ if (typeof data !== 'string' || !CALLDATA_RE.test(data)) {
62
+ throw new InvalidTxError('data', `must be 0x followed by an even number of hex characters, got ${shown(data)}`);
63
+ }
64
+ return { chain_id: chainId, transaction: { to, value: wei, data } };
65
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clanker-wallet",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",