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/LICENSE +21 -0
- package/README.md +773 -0
- package/dist/adapters.d.ts +155 -0
- package/dist/adapters.js +191 -0
- package/dist/address.d.ts +119 -0
- package/dist/address.js +220 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +7 -0
- package/dist/keystore.d.ts +99 -0
- package/dist/keystore.js +166 -0
- package/dist/provider.d.ts +204 -0
- package/dist/provider.js +208 -0
- package/dist/signer.d.ts +161 -0
- package/dist/signer.js +188 -0
- package/dist/system-contracts.d.ts +65 -0
- package/dist/system-contracts.js +105 -0
- package/dist/transactions.d.ts +208 -0
- package/dist/transactions.js +250 -0
- package/dist/types.d.ts +140 -0
- package/dist/types.js +1 -0
- package/package.json +82 -0
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
|
+
[](https://nodejs.org/)
|
|
6
|
+
[](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.
|