shell-sdk 0.1.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 ADDED
@@ -0,0 +1,773 @@
1
+ # shell-sdk
2
+
3
+ **TypeScript / JavaScript SDK for Shell Chain** — a post-quantum blockchain with native account abstraction.
4
+
5
+ [![Node ≥ 18](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org/)
6
+ [![ESM only](https://img.shields.io/badge/module-ESM-blue)](https://nodejs.org/api/esm.html)
7
+
8
+ ---
9
+
10
+ ## Table of Contents
11
+
12
+ - [Features](#features)
13
+ - [Installation](#installation)
14
+ - [Quick start](#quick-start)
15
+ - [Architecture overview](#architecture-overview)
16
+ - [Module reference](#module-reference)
17
+ - [Types](#types)
18
+ - [Addresses](#addresses)
19
+ - [Provider / RPC](#provider--rpc)
20
+ - [Signer & Adapters](#signer--adapters)
21
+ - [Transaction builders](#transaction-builders)
22
+ - [System contracts](#system-contracts)
23
+ - [Keystore](#keystore)
24
+ - [End-to-end examples](#end-to-end-examples)
25
+ - [Key rotation](#key-rotation)
26
+ - [Error handling](#error-handling)
27
+ - [TypeScript types reference](#typescript-types-reference)
28
+ - [Compatibility](#compatibility)
29
+ - [Chain reference](#chain-reference)
30
+
31
+ ---
32
+
33
+ ## Features
34
+
35
+ - **Post-quantum signing** — ML-DSA-65 (FIPS 204) and SLH-DSA-SHA2-256f (FIPS 205)
36
+ - **PQ addresses** — bech32m-encoded `pq1…` addresses derived from PQ public keys via BLAKE3
37
+ - **Native account abstraction** — key rotation and custom validation code via system contracts
38
+ - **viem integration** — standard Ethereum JSON-RPC methods via a typed `PublicClient`
39
+ - **Shell-specific RPC** — `shell_getPqPubkey`, `shell_sendTransaction`, `shell_getTransactionsByAddress`
40
+ - **Encrypted keystore** — argon2id KDF + xchacha20-poly1305 cipher; compatible with the Shell CLI
41
+
42
+ ---
43
+
44
+ ## Installation
45
+
46
+ ```bash
47
+ # npm
48
+ npm install shell-sdk
49
+
50
+ # yarn
51
+ yarn add shell-sdk
52
+
53
+ # pnpm
54
+ pnpm add shell-sdk
55
+ ```
56
+
57
+ > **Requires Node.js ≥ 18** for the built-in `fetch` API and `WebCrypto` (`crypto.getRandomValues`).
58
+
59
+ ---
60
+
61
+ ## Quick start
62
+
63
+ Send a SHELL transfer in ~10 lines:
64
+
65
+ ```typescript
66
+ import { MlDsa65Adapter } from "shell-sdk/adapters";
67
+ import { createShellProvider } from "shell-sdk/provider";
68
+ import { ShellSigner } from "shell-sdk/signer";
69
+ import { buildTransferTransaction, hashTransaction } from "shell-sdk/transactions";
70
+ import { parseEther } from "viem";
71
+
72
+ const adapter = MlDsa65Adapter.generate();
73
+ const signer = new ShellSigner("MlDsa65", adapter);
74
+ const from = signer.getAddress(); // pq1…
75
+
76
+ const provider = createShellProvider();
77
+ const nonce = await provider.client.getTransactionCount({ address: signer.getHexAddress() });
78
+
79
+ const tx = buildTransferTransaction({ chainId: 424242, nonce, to: "pq1recipient…", value: parseEther("1") });
80
+ const txHash = hashTransaction(tx);
81
+ const signed = await signer.buildSignedTransaction({ tx, txHash });
82
+ const hash = await provider.sendTransaction(signed);
83
+ console.log("tx hash:", hash);
84
+ ```
85
+
86
+ ---
87
+
88
+ ## Architecture overview
89
+
90
+ ### PQ addresses
91
+
92
+ Shell Chain uses **bech32m**-encoded addresses (prefix `pq`) instead of Ethereum's hex checksummed format. A `pq1…` address encodes:
93
+
94
+ ```
95
+ bech32m( hrp="pq", payload = [ version_byte(0x01) | address_bytes(20) ] )
96
+ ```
97
+
98
+ The 20 address bytes are derived deterministically:
99
+
100
+ ```
101
+ blake3( version(1) || algo_id(1) || public_key )[0..20]
102
+ ```
103
+
104
+ Algorithm IDs: `Dilithium3=0`, `MlDsa65=1`, `SphincsSha2256f=2`.
105
+
106
+ Internally the chain also recognises the hex representation (`0x…`) of the same 20 bytes, so both forms are interchangeable in SDK APIs.
107
+
108
+ ### Native account abstraction (AA)
109
+
110
+ Every account on Shell Chain supports two system-contract operations via the **AccountManager** (`0x…0002`):
111
+
112
+ | Operation | Description |
113
+ |---|---|
114
+ | `rotateKey` | Replace the signing key associated with an address |
115
+ | `setValidationCode` | Attach a custom EVM validation contract (smart account) |
116
+ | `clearValidationCode` | Revert to the default PQ key validation |
117
+
118
+ These are sent as ordinary transactions whose `to` field is the AccountManager address.
119
+
120
+ ### System contracts
121
+
122
+ | Name | Hex address | PQ address |
123
+ |---|---|---|
124
+ | ValidatorRegistry | `0x0000000000000000000000000000000000000001` | derived pq1 form |
125
+ | AccountManager | `0x0000000000000000000000000000000000000002` | derived pq1 form |
126
+
127
+ ---
128
+
129
+ ## Module reference
130
+
131
+ ### Types
132
+
133
+ Defined in `src/types.ts`. All types are re-exported from the package root.
134
+
135
+ | Type | Description |
136
+ |---|---|
137
+ | `HexString` | Template-literal type `0x${string}` |
138
+ | `AddressLike` | Any string accepted as an address |
139
+ | `SignatureTypeName` | `"Dilithium3" \| "MlDsa65" \| "SphincsSha2256f"` |
140
+ | `ShellTransactionRequest` | Wire format for a Shell transaction |
141
+ | `ShellSignature` | `{ sig_type, data: number[] }` |
142
+ | `SignedShellTransaction` | Complete signed transaction ready to broadcast |
143
+ | `ShellAccessListItem` | EIP-2930 access list entry |
144
+ | `ShellTxByAddressPage` | Paginated address history response |
145
+ | `ShellKdfParams` | argon2id parameters inside a keystore |
146
+ | `ShellCipherParams` | xchacha20-poly1305 nonce inside a keystore |
147
+ | `ShellEncryptedKey` | Full encrypted keystore file structure |
148
+
149
+ ---
150
+
151
+ ### Addresses
152
+
153
+ `import { … } from "shell-sdk/address"`
154
+
155
+ #### Constants
156
+
157
+ | Export | Value | Description |
158
+ |---|---|---|
159
+ | `PQ_ADDRESS_HRP` | `"pq"` | Human-readable part for bech32m encoding |
160
+ | `PQ_ADDRESS_LENGTH` | `20` | Address bytes (excluding version byte) |
161
+ | `PQ_ADDRESS_VERSION_V1` | `0x01` | Current address version |
162
+
163
+ #### Functions
164
+
165
+ | Function | Signature | Description |
166
+ |---|---|---|
167
+ | `bytesToPqAddress` | `(bytes: Uint8Array, version?) → string` | Encode 20 raw bytes as a `pq1…` bech32m address |
168
+ | `pqAddressToBytes` | `(address: string) → Uint8Array` | Decode a `pq1…` address to its 20 raw bytes |
169
+ | `pqAddressVersion` | `(address: string) → number` | Extract the version byte from a `pq1…` address |
170
+ | `hexAddressToBytes` | `(address: string) → Uint8Array` | Parse a `0x…` address to 20 bytes |
171
+ | `bytesToHexAddress` | `(bytes: Uint8Array) → HexString` | Encode 20 bytes as a `0x…` hex address |
172
+ | `normalizePqAddress` | `(address: string) → string` | Accept either pq1 or 0x form, always return pq1 |
173
+ | `normalizeHexAddress` | `(address: string) → HexString` | Accept either pq1 or 0x form, always return 0x |
174
+ | `derivePqAddressFromPublicKey` | `(pk, algoId, version?) → string` | Derive pq1 address from a raw public key |
175
+ | `isPqAddress` | `(address: string) → boolean` | Return `true` if the string is a valid pq1 address |
176
+
177
+ **Examples:**
178
+
179
+ ```typescript
180
+ import {
181
+ derivePqAddressFromPublicKey,
182
+ isPqAddress,
183
+ normalizePqAddress,
184
+ } from "shell-sdk/address";
185
+
186
+ const address = derivePqAddressFromPublicKey(publicKey, 1 /* MlDsa65 */);
187
+ // → "pq1qx3f…"
188
+
189
+ console.log(isPqAddress(address)); // true
190
+
191
+ // Round-trip normalisation
192
+ normalizePqAddress("0xabcdef…"); // → "pq1…"
193
+ normalizePqAddress("pq1qx3f…"); // → "pq1qx3f…" (unchanged)
194
+ ```
195
+
196
+ ---
197
+
198
+ ### Provider / RPC
199
+
200
+ `import { … } from "shell-sdk/provider"`
201
+
202
+ #### Chain config
203
+
204
+ ```typescript
205
+ import { shellDevnet } from "shell-sdk/provider";
206
+ // shellDevnet = { id: 424242, name: "Shell Devnet", nativeCurrency: { symbol: "SHELL", decimals: 18 }, … }
207
+ ```
208
+
209
+ #### Factory functions
210
+
211
+ | Function | Description |
212
+ |---|---|
213
+ | `createShellProvider(options?)` | Create a `ShellProvider` (recommended entry point) |
214
+ | `createShellPublicClient(options?)` | Create a viem `PublicClient` over HTTP |
215
+ | `createShellWsClient(options?)` | Create a viem `PublicClient` over WebSocket |
216
+
217
+ `options: CreateShellPublicClientOptions`:
218
+
219
+ | Field | Type | Default |
220
+ |---|---|---|
221
+ | `chain` | `Chain` | `shellDevnet` |
222
+ | `rpcHttpUrl` | `string` | `http://127.0.0.1:8545` |
223
+ | `rpcWsUrl` | `string` | `ws://127.0.0.1:8546` |
224
+
225
+ #### `ShellProvider` class
226
+
227
+ | Member | Description |
228
+ |---|---|
229
+ | `.client` | Underlying viem `PublicClient` for all standard `eth_*` methods |
230
+ | `.rpcHttpUrl` | HTTP RPC URL in use |
231
+ | `getPqPubkey(address)` | `shell_getPqPubkey` → hex public key or `null` |
232
+ | `sendTransaction(signed)` | `shell_sendTransaction` → tx hash string |
233
+ | `getTransactionsByAddress(address, opts)` | `shell_getTransactionsByAddress` with optional `fromBlock/toBlock/page/limit` |
234
+ | `getBlockReceipts(block)` | `eth_getBlockReceipts` → array of receipts |
235
+
236
+ **Examples:**
237
+
238
+ ```typescript
239
+ import { createShellProvider } from "shell-sdk/provider";
240
+
241
+ const provider = createShellProvider();
242
+
243
+ // Standard eth methods via viem
244
+ const block = await provider.client.getBlockNumber();
245
+ const balance = await provider.client.getBalance({ address: "0x…" });
246
+
247
+ // Shell-specific methods
248
+ const pubkeyHex = await provider.getPqPubkey("pq1…");
249
+ const txHash = await provider.sendTransaction(signedTx);
250
+
251
+ const history = await provider.getTransactionsByAddress("pq1…", { page: 0, limit: 20 });
252
+ ```
253
+
254
+ **Custom endpoint:**
255
+
256
+ ```typescript
257
+ const provider = createShellProvider({ rpcHttpUrl: "https://rpc.shellchain.example" });
258
+ ```
259
+
260
+ ---
261
+
262
+ ### Signer & Adapters
263
+
264
+ `import { … } from "shell-sdk/signer"`
265
+ `import { … } from "shell-sdk/adapters"`
266
+
267
+ #### `SignerAdapter` interface
268
+
269
+ Any object satisfying this interface can be plugged into `ShellSigner`:
270
+
271
+ ```typescript
272
+ interface SignerAdapter {
273
+ sign(message: Uint8Array): Promise<Uint8Array>;
274
+ getPublicKey(): Uint8Array;
275
+ }
276
+ ```
277
+
278
+ #### Concrete adapters
279
+
280
+ | Class | Algorithm | Key sizes |
281
+ |---|---|---|
282
+ | `MlDsa65Adapter` | ML-DSA-65 (FIPS 204); also used as Dilithium3 stand-in | pk: 1952 B, sk: 4032 B |
283
+ | `SlhDsaAdapter` | SLH-DSA-SHA2-256f (FIPS 205) | pk: 64 B, sk: 128 B |
284
+
285
+ Both adapters expose the same API:
286
+
287
+ ```typescript
288
+ // Generate a fresh key pair (optionally from a deterministic seed)
289
+ const adapter = MlDsa65Adapter.generate();
290
+ const adapter = MlDsa65Adapter.generate(seed /* Uint8Array(32) */);
291
+
292
+ // Load from an existing key pair (e.g. from a keystore)
293
+ const adapter = MlDsa65Adapter.fromKeyPair(publicKey, secretKey);
294
+ ```
295
+
296
+ #### Key-pair generators
297
+
298
+ ```typescript
299
+ import { generateMlDsa65KeyPair, generateSlhDsaKeyPair } from "shell-sdk/adapters";
300
+
301
+ const { publicKey, secretKey } = generateMlDsa65KeyPair();
302
+ const { publicKey, secretKey } = generateSlhDsaKeyPair(seed /* Uint8Array(96) */);
303
+ ```
304
+
305
+ #### `generateAdapter` / `adapterFromKeyPair`
306
+
307
+ ```typescript
308
+ import { generateAdapter, adapterFromKeyPair } from "shell-sdk/adapters";
309
+
310
+ const adapter = generateAdapter("MlDsa65");
311
+ const adapter = adapterFromKeyPair("SphincsSha2256f", pk, sk);
312
+ ```
313
+
314
+ #### `ShellSigner` class
315
+
316
+ ```typescript
317
+ import { ShellSigner } from "shell-sdk/signer";
318
+ import { MlDsa65Adapter } from "shell-sdk/adapters";
319
+
320
+ const signer = new ShellSigner("MlDsa65", MlDsa65Adapter.generate());
321
+ ```
322
+
323
+ | Member | Description |
324
+ |---|---|
325
+ | `algorithmId` | Numeric algorithm ID (`0`, `1`, or `2`) |
326
+ | `getPublicKey()` | Raw public key bytes |
327
+ | `getAddress()` | `pq1…` bech32m address |
328
+ | `getHexAddress()` | `0x…` hex address |
329
+ | `sign(message)` | Sign an arbitrary byte message → signature bytes |
330
+ | `buildSignedTransaction(options)` | Sign `txHash` and assemble a `SignedShellTransaction` |
331
+
332
+ `buildSignedTransaction` options:
333
+
334
+ | Field | Type | Description |
335
+ |---|---|---|
336
+ | `tx` | `ShellTransactionRequest` | The unsigned transaction |
337
+ | `txHash` | `Uint8Array` | RLP-encoded hash to sign |
338
+ | `includePublicKey?` | `boolean` | Embed `sender_pubkey` for first-time senders |
339
+
340
+ #### Helper functions
341
+
342
+ | Function | Description |
343
+ |---|---|
344
+ | `signatureTypeFromKeyType(keyType)` | Convert keystore `key_type` string to `SignatureTypeName` |
345
+ | `publicKeyFromHex(hex)` | Hex string → `Uint8Array` |
346
+ | `buildShellSignature(type, bytes)` | Build a `ShellSignature` object |
347
+
348
+ ---
349
+
350
+ ### Transaction builders
351
+
352
+ `import { … } from "shell-sdk/transactions"`
353
+
354
+ #### Constants
355
+
356
+ | Constant | Value |
357
+ |---|---|
358
+ | `DEFAULT_TX_TYPE` | `2` (EIP-1559) |
359
+ | `DEFAULT_TRANSFER_GAS_LIMIT` | `21_000` |
360
+ | `DEFAULT_SYSTEM_GAS_LIMIT` | `100_000` |
361
+ | `DEFAULT_MAX_FEE_PER_GAS` | `1_000_000_000` (1 Gwei) |
362
+ | `DEFAULT_MAX_PRIORITY_FEE_PER_GAS` | `100_000_000` (0.1 Gwei) |
363
+
364
+ #### `buildTransferTransaction`
365
+
366
+ Build a SHELL token transfer (type-2 EIP-1559 transaction):
367
+
368
+ ```typescript
369
+ import { buildTransferTransaction, hashTransaction } from "shell-sdk/transactions";
370
+ import { parseEther } from "viem";
371
+
372
+ const tx = buildTransferTransaction({
373
+ chainId: 424242,
374
+ nonce: 0,
375
+ to: "pq1recipient…",
376
+ value: parseEther("1.5"),
377
+ });
378
+ ```
379
+
380
+ #### `buildSystemTransaction`
381
+
382
+ Low-level builder for any call to the AccountManager system contract:
383
+
384
+ ```typescript
385
+ const tx = buildSystemTransaction({
386
+ chainId: 424242,
387
+ nonce: 1,
388
+ data: "0xdeadbeef…",
389
+ });
390
+ ```
391
+
392
+ #### `buildRotateKeyTransaction`
393
+
394
+ Rotate the signing key for the sender's account:
395
+
396
+ ```typescript
397
+ import { buildRotateKeyTransaction, hashTransaction } from "shell-sdk/transactions";
398
+
399
+ const tx = buildRotateKeyTransaction({
400
+ chainId: 424242,
401
+ nonce: 2,
402
+ publicKey: newAdapter.getPublicKey(),
403
+ algorithmId: 1, // MlDsa65
404
+ });
405
+ ```
406
+
407
+ #### `buildSetValidationCodeTransaction` / `buildClearValidationCodeTransaction`
408
+
409
+ ```typescript
410
+ const tx = buildSetValidationCodeTransaction({
411
+ chainId: 424242,
412
+ nonce: 3,
413
+ codeHash: "0xabc123…", // bytes32 hash of custom validation contract
414
+ });
415
+
416
+ const tx = buildClearValidationCodeTransaction({ chainId: 424242, nonce: 4 });
417
+ ```
418
+
419
+ #### `buildSignedTransaction`
420
+
421
+ Assemble a `SignedShellTransaction` directly (use `ShellSigner.buildSignedTransaction` in practice):
422
+
423
+ ```typescript
424
+ import { buildSignedTransaction } from "shell-sdk/transactions";
425
+
426
+ const signed = buildSignedTransaction({
427
+ from: "pq1sender…",
428
+ tx,
429
+ signature: sigBytes,
430
+ signatureType: "MlDsa65",
431
+ senderPubkey: publicKey, // optional
432
+ });
433
+ ```
434
+
435
+ #### `hashTransaction`
436
+
437
+ RLP-encode a `ShellTransactionRequest` and return its **keccak256** hash as a `Uint8Array`. This is the value you must pass as `txHash` to `signer.buildSignedTransaction`.
438
+
439
+ Shell Chain computes the signing hash identically to Ethereum EIP-1559: `keccak256(RLP([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, accessList]))`.
440
+
441
+ ```typescript
442
+ import { buildTransferTransaction, hashTransaction } from "shell-sdk/transactions";
443
+
444
+ const tx = buildTransferTransaction({ chainId: 424242, nonce: 0, to: "pq1…", value: 1n });
445
+ const txHash = hashTransaction(tx); // Uint8Array (32 bytes)
446
+ const signed = await signer.buildSignedTransaction({ tx, txHash });
447
+ ```
448
+
449
+ ---
450
+
451
+ ### System contracts
452
+
453
+ `import { … } from "shell-sdk/system-contracts"`
454
+
455
+ #### Addresses
456
+
457
+ | Export | Value |
458
+ |---|---|
459
+ | `validatorRegistryHexAddress` | `0x0000000000000000000000000000000000000001` |
460
+ | `accountManagerHexAddress` | `0x0000000000000000000000000000000000000002` |
461
+ | `validatorRegistryAddress` | pq1 bech32m form of above |
462
+ | `accountManagerAddress` | pq1 bech32m form of above |
463
+
464
+ #### Function selectors
465
+
466
+ | Export | Selector for |
467
+ |---|---|
468
+ | `rotateKeySelector` | `rotateKey(bytes,uint8)` |
469
+ | `setValidationCodeSelector` | `setValidationCode(bytes32)` |
470
+ | `clearValidationCodeSelector` | `clearValidationCode()` |
471
+
472
+ #### Calldata encoders
473
+
474
+ ```typescript
475
+ import {
476
+ encodeRotateKeyCalldata,
477
+ encodeSetValidationCodeCalldata,
478
+ encodeClearValidationCodeCalldata,
479
+ isSystemContractAddress,
480
+ } from "shell-sdk/system-contracts";
481
+
482
+ const data = encodeRotateKeyCalldata(newPublicKey, 1 /* MlDsa65 */);
483
+ const data = encodeSetValidationCodeCalldata("0xcodehash…");
484
+ const data = encodeClearValidationCodeCalldata(); // selector only
485
+
486
+ isSystemContractAddress("0x0000000000000000000000000000000000000002"); // true
487
+ ```
488
+
489
+ ---
490
+
491
+ ### Keystore
492
+
493
+ `import { … } from "shell-sdk/keystore"`
494
+
495
+ Shell keystore files are JSON objects encrypted with **argon2id** (KDF) and **xchacha20-poly1305** (cipher). The `shell` CLI generates compatible files.
496
+
497
+ #### Keystore format
498
+
499
+ ```jsonc
500
+ {
501
+ "version": 1,
502
+ "address": "pq1…",
503
+ "key_type": "mldsa65",
504
+ "kdf": "argon2id",
505
+ "kdf_params": { "m_cost": 65536, "t_cost": 3, "p_cost": 1, "salt": "hex…" },
506
+ "cipher": "xchacha20-poly1305",
507
+ "cipher_params": { "nonce": "hex…" },
508
+ "ciphertext": "hex…",
509
+ "public_key": "hex…"
510
+ }
511
+ ```
512
+
513
+ Plaintext layout after decryption: `[secret_key_bytes][public_key_bytes]`.
514
+
515
+ #### API
516
+
517
+ | Function | Description |
518
+ |---|---|
519
+ | `parseEncryptedKey(input)` | Parse keystore JSON → `ParsedShellKeystore` (no decryption) |
520
+ | `validateEncryptedKeyAddress(input)` | Parse + verify `address` matches derived public-key address |
521
+ | `exportEncryptedKeyJson(input)` | Pretty-print keystore JSON string |
522
+ | `assertSignerMatchesKeystore(signer, keystore)` | Throw if signer algorithm or address doesn't match |
523
+ | `decryptKeystore(input, password)` | Full decryption → `Promise<ShellSigner>` |
524
+
525
+ **Examples:**
526
+
527
+ ```typescript
528
+ import { decryptKeystore, parseEncryptedKey } from "shell-sdk/keystore";
529
+ import { readFileSync } from "fs";
530
+
531
+ const json = readFileSync("./my-key.json", "utf8");
532
+
533
+ // Inspect without decrypting
534
+ const parsed = parseEncryptedKey(json);
535
+ console.log(parsed.canonicalAddress); // pq1…
536
+ console.log(parsed.signatureType); // "MlDsa65"
537
+
538
+ // Decrypt
539
+ const signer = await decryptKeystore(json, "my-passphrase");
540
+ console.log(signer.getAddress()); // pq1…
541
+ ```
542
+
543
+ ---
544
+
545
+ ## End-to-end examples
546
+
547
+ ### Key generation → address → transfer → submit
548
+
549
+ ```typescript
550
+ import { MlDsa65Adapter } from "shell-sdk/adapters";
551
+ import { createShellProvider } from "shell-sdk/provider";
552
+ import { ShellSigner } from "shell-sdk/signer";
553
+ import { buildTransferTransaction, hashTransaction } from "shell-sdk/transactions";
554
+
555
+
556
+ // 1. Generate keys
557
+ const adapter = MlDsa65Adapter.generate();
558
+ const signer = new ShellSigner("MlDsa65", adapter);
559
+ const from = signer.getAddress(); // pq1…
560
+ const fromHex = signer.getHexAddress(); // 0x…
561
+
562
+ console.log("Address:", from);
563
+
564
+ // 2. Connect to devnet
565
+ const provider = createShellProvider(); // defaults to http://127.0.0.1:8545
566
+
567
+ // 3. Get current nonce
568
+ const nonce = await provider.client.getTransactionCount({ address: fromHex });
569
+
570
+ // 4. Build the transaction
571
+ const tx = buildTransferTransaction({
572
+ chainId: 424242,
573
+ nonce,
574
+ to: "pq1recipientaddress…",
575
+ value: parseEther("0.5"),
576
+ });
577
+
578
+ // 5. RLP-encode and hash for signing
579
+ // (Shell uses the same EIP-1559 signing hash as Ethereum)
580
+ const txHash = hashTransaction(tx);
581
+
582
+ // 6. Sign and build the complete signed transaction
583
+ // includePublicKey=true is required for accounts that haven't been seen on-chain yet
584
+ const signed = await signer.buildSignedTransaction({
585
+ tx,
586
+ txHash: txHash,
587
+ includePublicKey: true,
588
+ });
589
+
590
+ // 7. Broadcast
591
+ const hash = await provider.sendTransaction(signed);
592
+ console.log("Transaction hash:", hash);
593
+ ```
594
+
595
+ ### Load from keystore and send
596
+
597
+ ```typescript
598
+ import { decryptKeystore } from "shell-sdk/keystore";
599
+ import { createShellProvider } from "shell-sdk/provider";
600
+ import { buildTransferTransaction, hashTransaction } from "shell-sdk/transactions";
601
+ import { readFileSync } from "fs";
602
+ import { parseEther } from "viem";
603
+
604
+ const signer = await decryptKeystore(readFileSync("./key.json", "utf8"), process.env.PASSPHRASE!);
605
+ const provider = createShellProvider();
606
+ const nonce = await provider.client.getTransactionCount({ address: signer.getHexAddress() });
607
+
608
+ const tx = buildTransferTransaction({
609
+ chainId: 424242,
610
+ nonce,
611
+ to: "pq1dest…",
612
+ value: parseEther("10"),
613
+ });
614
+
615
+ const txHash = hashTransaction(tx);
616
+ const signed = await signer.buildSignedTransaction({ tx, txHash });
617
+ const hash = await provider.sendTransaction(signed);
618
+ console.log(hash);
619
+ ```
620
+
621
+ ---
622
+
623
+ ## Key rotation
624
+
625
+ Shell Chain accounts support **key rotation** — replacing the signing key without changing the account address. This is a critical security feature for post-quantum safety.
626
+
627
+ ```typescript
628
+ import { MlDsa65Adapter } from "shell-sdk/adapters";
629
+ import { ShellSigner } from "shell-sdk/signer";
630
+ import { createShellProvider } from "shell-sdk/provider";
631
+ import { buildRotateKeyTransaction, hashTransaction } from "shell-sdk/transactions";
632
+
633
+ const provider = createShellProvider();
634
+
635
+ // Current signer (must sign the rotation transaction)
636
+ const currentSigner = await decryptKeystore(readFileSync("old-key.json", "utf8"), passphrase);
637
+
638
+ // New key pair to rotate to
639
+ const newAdapter = MlDsa65Adapter.generate();
640
+ const newSigner = new ShellSigner("MlDsa65", newAdapter);
641
+
642
+ const nonce = await provider.client.getTransactionCount({ address: currentSigner.getHexAddress() });
643
+
644
+ // Build the rotateKey system transaction
645
+ const tx = buildRotateKeyTransaction({
646
+ chainId: 424242,
647
+ nonce,
648
+ publicKey: newAdapter.getPublicKey(),
649
+ algorithmId: newSigner.algorithmId, // 1 for MlDsa65
650
+ });
651
+
652
+ const txHash = hashTransaction(tx);
653
+
654
+ // Sign with the CURRENT key
655
+ const signed = await currentSigner.buildSignedTransaction({ tx, txHash });
656
+ const hash = await provider.sendTransaction(signed);
657
+ console.log("Key rotated! tx:", hash);
658
+ // From the next transaction onwards, use newSigner
659
+ ```
660
+
661
+ ---
662
+
663
+ ## Error handling
664
+
665
+ All SDK functions throw standard `Error` instances. Common error messages:
666
+
667
+ | Error message | Cause |
668
+ |---|---|
669
+ | `expected 20 address bytes, got N` | Wrong-length bytes passed to address helpers |
670
+ | `expected pq address prefix, got X` | bech32m prefix is not `pq` |
671
+ | `invalid hex address` | String does not start with `0x` |
672
+ | `invalid bech32m address` | String is not a valid bech32m address |
673
+ | `unsupported key type: X` | Keystore `key_type` not recognised |
674
+ | `unsupported kdf: X` | Only `argon2id` is supported |
675
+ | `unsupported cipher: X` | Only `xchacha20-poly1305` is supported |
676
+ | `keystore address mismatch` | Declared address ≠ derived address in the keystore |
677
+ | `decrypted public key mismatch` | Wrong password or corrupt keystore |
678
+ | `rpc request failed: 4XX/5XX` | HTTP-level RPC error |
679
+ | `[code] message` | JSON-RPC error returned by the node |
680
+
681
+ ```typescript
682
+ try {
683
+ const signer = await decryptKeystore(json, "wrong-password");
684
+ } catch (err) {
685
+ if (err instanceof Error && err.message.includes("mismatch")) {
686
+ console.error("Wrong password or corrupt keystore file");
687
+ }
688
+ }
689
+ ```
690
+
691
+ ---
692
+
693
+ ## TypeScript types reference
694
+
695
+ ```typescript
696
+ // Branded hex string: "0x" + arbitrary hex chars
697
+ type HexString = `0x${string}`;
698
+
699
+ // Any value accepted as an address (pq1… or 0x…)
700
+ type AddressLike = string;
701
+
702
+ // Post-quantum signature algorithm names
703
+ type SignatureTypeName = "Dilithium3" | "MlDsa65" | "SphincsSha2256f";
704
+
705
+ // Wire format sent to shell_sendTransaction
706
+ interface SignedShellTransaction {
707
+ from: AddressLike;
708
+ tx: ShellTransactionRequest;
709
+ signature: ShellSignature;
710
+ sender_pubkey?: number[] | null;
711
+ }
712
+
713
+ interface ShellTransactionRequest {
714
+ chain_id: number;
715
+ nonce: number;
716
+ to: AddressLike | null;
717
+ value: string; // hex-encoded bigint, e.g. "0xde0b6b3a7640000"
718
+ data: HexString;
719
+ gas_limit: number;
720
+ max_fee_per_gas: number;
721
+ max_priority_fee_per_gas: number;
722
+ access_list?: ShellAccessListItem[] | null;
723
+ tx_type?: number;
724
+ max_fee_per_blob_gas?: number | null;
725
+ blob_versioned_hashes?: HexString[] | null;
726
+ }
727
+
728
+ interface ShellSignature {
729
+ sig_type: SignatureTypeName;
730
+ data: number[]; // raw signature bytes as a JS number array
731
+ }
732
+ ```
733
+
734
+ ---
735
+
736
+ ## Compatibility
737
+
738
+ | Requirement | Version |
739
+ |---|---|
740
+ | Node.js | ≥ 18 (for `fetch` and `crypto.getRandomValues`) |
741
+ | TypeScript | ≥ 5.0 recommended |
742
+ | Module format | ESM only (`"type": "module"`) |
743
+ | Browser | Any modern browser with WebCrypto; bundler required (Vite/webpack/esbuild) |
744
+
745
+ **Key dependencies:**
746
+
747
+ | Package | Purpose |
748
+ |---|---|
749
+ | [`viem`](https://viem.sh) | Ethereum JSON-RPC client, ABI encoding |
750
+ | [`@noble/post-quantum`](https://github.com/paulmillr/noble-post-quantum) | ML-DSA-65 and SLH-DSA-SHA2-256f |
751
+ | [`@noble/hashes`](https://github.com/paulmillr/noble-hashes) | BLAKE3 |
752
+ | [`@noble/ciphers`](https://github.com/paulmillr/noble-ciphers) | xchacha20-poly1305 |
753
+ | [`@scure/base`](https://github.com/paulmillr/scure-base) | bech32m encoding |
754
+ | [`hash-wasm`](https://github.com/nicowillis/hash-wasm) | argon2id (WASM) |
755
+
756
+ ---
757
+
758
+ ## Chain reference
759
+
760
+ | Parameter | Value |
761
+ |---|---|
762
+ | Chain ID | `424242` |
763
+ | Network name | Shell Devnet |
764
+ | Native currency | SHELL (18 decimals) |
765
+ | HTTP RPC | `http://127.0.0.1:8545` |
766
+ | WebSocket RPC | `ws://127.0.0.1:8546` |
767
+ | Address format | bech32m, prefix `pq`, version byte `0x01` |
768
+ | Default tx type | 2 (EIP-1559) |
769
+ | Default gas limit (transfer) | 21 000 |
770
+ | Default gas limit (system) | 100 000 |
771
+ | Default max fee per gas | 1 Gwei |
772
+
773
+ For full chain documentation, validator setup, and the Shell CLI reference, see the project wiki or official docs site.