shell-sdk 0.1.0 → 0.3.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/CHANGELOG.md ADDED
@@ -0,0 +1,26 @@
1
+ # Changelog
2
+
3
+ ## [0.3.0] — 2026-04-22
4
+
5
+ ### Added
6
+ - **API freeze**: `ShellSigner`, `ShellProvider`, and `ShellWallet` are now stable public APIs.
7
+ - ML-DSA-65 cross-validation tests confirming wire compatibility with `pqcrypto-dilithium` v0.5 (pk=1952 B, sk=4032 B, sig=3309 B).
8
+ - `examples/minimal-dapp`: Node.js (`node-demo.mjs`) and browser (`browser-demo.html`) integration examples.
9
+ - JSDoc complete for all public-facing exports.
10
+ - `MlDsa65Adapter` round-trip sign+verify test.
11
+ - `getNodeInfo()`, `getWitness()`, and `getStorageProfile()` helpers for Shell-specific node capabilities.
12
+
13
+ ### Changed
14
+ - `adapters.ts`: both `"Dilithium3"` and `"MlDsa65"` aliases now explicitly document ML-DSA-65 (FIPS 204) wire compatibility with the chain's Dilithium3 verifier.
15
+ - `hashTransaction()` canonical RLP field ordering aligned with `shell-chain` deserialiser.
16
+ - Package root export surface narrowed to stable application-facing APIs.
17
+ - NodeInfo example version string now reflects current node naming (`shell-node/0.17.0`) without tying the SDK package version to the chain version.
18
+
19
+ ## 0.2.0-rc.1
20
+
21
+ - add Browser and Node integration tests for signer, keystore, and provider flows
22
+ - add Rust compatibility vectors for address derivation and transaction hashing
23
+ - fix `hashTransaction()` to match the Rust node's canonical RLP field ordering
24
+ - fix `hashTransaction()` to accept canonical `pq1...` recipient addresses
25
+ - narrow the package root export surface to stable application-facing APIs
26
+ - document extension background flow, minimal dApp usage, and release checklist
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # shell-sdk
2
2
 
3
- **TypeScript / JavaScript SDK for Shell Chain** — a post-quantum blockchain with native account abstraction.
3
+ **TypeScript / JavaScript SDK for Shell Chain** — build quantum-safe dApps on the first EVM chain secured before Q-Day.
4
4
 
5
5
  [![Node ≥ 18](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org/)
6
6
  [![ESM only](https://img.shields.io/badge/module-ESM-blue)](https://nodejs.org/api/esm.html)
@@ -22,10 +22,13 @@
22
22
  - [System contracts](#system-contracts)
23
23
  - [Keystore](#keystore)
24
24
  - [End-to-end examples](#end-to-end-examples)
25
+ - [Wallet extension background flow](#wallet-extension-background-flow)
26
+ - [Minimal dApp flow](#minimal-dapp-flow)
25
27
  - [Key rotation](#key-rotation)
26
28
  - [Error handling](#error-handling)
27
29
  - [TypeScript types reference](#typescript-types-reference)
28
30
  - [Compatibility](#compatibility)
31
+ - [Release checklist](#release-checklist)
29
32
  - [Chain reference](#chain-reference)
30
33
 
31
34
  ---
@@ -36,7 +39,8 @@
36
39
  - **PQ addresses** — bech32m-encoded `pq1…` addresses derived from PQ public keys via BLAKE3
37
40
  - **Native account abstraction** — key rotation and custom validation code via system contracts
38
41
  - **viem integration** — standard Ethereum JSON-RPC methods via a typed `PublicClient`
39
- - **Shell-specific RPC** — `shell_getPqPubkey`, `shell_sendTransaction`, `shell_getTransactionsByAddress`
42
+ - **Shell-specific RPC** — `shell_getPqPubkey`, `shell_sendTransaction`, `shell_getTransactionsByAddress`, `shell_getNodeInfo`, `shell_getWitness`
43
+ - **Node introspection** — `getNodeInfo()` returns version, block height, peer count, and storage profile; `getWitness()` fetches raw PQ signatures for any block
40
44
  - **Encrypted keystore** — argon2id KDF + xchacha20-poly1305 cipher; compatible with the Shell CLI
41
45
 
42
46
  ---
@@ -128,6 +132,8 @@ These are sent as ordinary transactions whose `to` field is the AccountManager a
128
132
 
129
133
  ## Module reference
130
134
 
135
+ The package root (`shell-sdk`) is the **stable surface** for typical app usage. Lower-level constants and helpers that are more likely to change remain available from subpath imports such as `shell-sdk/signer` and `shell-sdk/transactions`.
136
+
131
137
  ### Types
132
138
 
133
139
  Defined in `src/types.ts`. All types are re-exported from the package root.
@@ -232,6 +238,9 @@ import { shellDevnet } from "shell-sdk/provider";
232
238
  | `sendTransaction(signed)` | `shell_sendTransaction` → tx hash string |
233
239
  | `getTransactionsByAddress(address, opts)` | `shell_getTransactionsByAddress` with optional `fromBlock/toBlock/page/limit` |
234
240
  | `getBlockReceipts(block)` | `eth_getBlockReceipts` → array of receipts |
241
+ | `getNodeInfo()` | `shell_getNodeInfo` → `ShellNodeInfo` (version, block height, peer count, storage profile) |
242
+ | `getWitness(blockNumberOrHash)` | `shell_getWitness` → `ShellWitnessBundle` or `null` if pruned |
243
+ | `getStorageProfile()` | Convenience wrapper around `getNodeInfo()` → `ShellStorageProfile \| undefined` |
235
244
 
236
245
  **Examples:**
237
246
 
@@ -434,9 +443,11 @@ const signed = buildSignedTransaction({
434
443
 
435
444
  #### `hashTransaction`
436
445
 
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`.
446
+ RLP-encode a `ShellTransactionRequest` using the Rust node's canonical field order and return its **keccak256** hash as a `Uint8Array`. This is the value you must pass as `txHash` to `signer.buildSignedTransaction`.
447
+
448
+ Shell Chain signs the full unsigned transaction payload in this order:
438
449
 
439
- Shell Chain computes the signing hash identically to Ethereum EIP-1559: `keccak256(RLP([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, accessList]))`.
450
+ `[chainId, nonce, to, value, data, gasLimit, maxFeePerGas, maxPriorityFeePerGas, accessList, txType, blobFeeFlag, maxFeePerBlobGas, blobVersionedHashes]`
440
451
 
441
452
  ```typescript
442
453
  import { buildTransferTransaction, hashTransaction } from "shell-sdk/transactions";
@@ -620,6 +631,55 @@ console.log(hash);
620
631
 
621
632
  ---
622
633
 
634
+ ### Wallet extension background flow
635
+
636
+ This is the recommended shape for a Chrome extension background worker: keep the decrypted signer only in memory, fetch the latest nonce from RPC, and use the stable root entrypoint for the common path.
637
+
638
+ ```typescript
639
+ import { createShellProvider, buildTransferTransaction, hashTransaction } from "shell-sdk";
640
+
641
+ async function submitTransfer({ signer, to, value, rpcHttpUrl }: {
642
+ signer: { getHexAddress(): `0x${string}`; buildSignedTransaction(args: { tx: unknown; txHash: Uint8Array; includePublicKey?: boolean }): Promise<unknown> };
643
+ to: string;
644
+ value: bigint;
645
+ rpcHttpUrl: string;
646
+ }) {
647
+ const provider = createShellProvider({ rpcHttpUrl });
648
+ const nonce = await provider.client.getTransactionCount({ address: signer.getHexAddress() });
649
+
650
+ const tx = buildTransferTransaction({
651
+ chainId: 424242,
652
+ nonce,
653
+ to,
654
+ value,
655
+ });
656
+ const txHash = hashTransaction(tx);
657
+ const signed = await signer.buildSignedTransaction({ tx, txHash, includePublicKey: nonce === 0 });
658
+
659
+ return provider.sendTransaction(signed);
660
+ }
661
+ ```
662
+
663
+ ### Minimal dApp flow
664
+
665
+ For a lightweight web app, keep the provider in the page and delegate signing to an injected wallet or background bridge:
666
+
667
+ ```typescript
668
+ import { createShellProvider, normalizePqAddress } from "shell-sdk";
669
+
670
+ const provider = createShellProvider({
671
+ rpcHttpUrl: "https://rpc.testnet.shell.network",
672
+ });
673
+
674
+ const account = normalizePqAddress("0x1234...abcd");
675
+ const history = await provider.getTransactionsByAddress(account, { page: 1, limit: 10 });
676
+
677
+ console.log("recent txs:", history.transactions);
678
+ console.log("total:", history.total);
679
+ ```
680
+
681
+ ---
682
+
623
683
  ## Key rotation
624
684
 
625
685
  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.
@@ -755,6 +815,19 @@ interface ShellSignature {
755
815
 
756
816
  ---
757
817
 
818
+ ## Release checklist
819
+
820
+ Before publishing a `shell-sdk` release candidate:
821
+
822
+ 1. Run `npm test` and `npm run typecheck`.
823
+ 2. Confirm the stable root surface still excludes low-level helpers such as `hexBytes` and internal signer maps.
824
+ 3. Verify Browser + Node integration tests both cover signer, keystore, and provider RPC flows.
825
+ 4. Review README examples against the current public exports (`shell-sdk`, `shell-sdk/signer`, `shell-sdk/transactions`).
826
+ 5. Check `package.json` `exports`, `files`, `version`, and repository metadata.
827
+ 6. Build once from a clean tree and smoke-import the package root plus subpaths from `dist/`.
828
+
829
+ ---
830
+
758
831
  ## Chain reference
759
832
 
760
833
  | Parameter | Value |
@@ -1,8 +1,15 @@
1
1
  /**
2
2
  * Concrete SignerAdapter implementations for each PQ algorithm.
3
3
  *
4
- * @noble/post-quantum provides ML-DSA-65 and SLH-DSA-SHA2-256f.
5
- * Dilithium3 (pre-FIPS Round-3) uses {@link MlDsa65Adapter} as a stand-in.
4
+ * `@noble/post-quantum` provides ML-DSA-65 and SLH-DSA-SHA2-256f via
5
+ * WebAssembly-accelerated pure-JS implementations.
6
+ *
7
+ * **Dilithium3 compatibility note**: `pqcrypto-dilithium` v0.5 (used by
8
+ * shell-chain) implements ML-DSA-65 (FIPS 204) under the `dilithium3` name.
9
+ * `@noble/post-quantum` `ml_dsa65` produces byte-identical keys and
10
+ * signatures (pk=1952, sk=4032, sig=3309), so `MlDsa65Adapter` is fully
11
+ * wire-compatible with the chain's Dilithium3 verifier. Both `"Dilithium3"`
12
+ * and `"MlDsa65"` algorithm names route to the same adapter.
6
13
  *
7
14
  * @module adapters
8
15
  */
@@ -45,8 +52,10 @@ export declare function generateSlhDsaKeyPair(seed?: Uint8Array): SlhDsaKeyPair;
45
52
  /**
46
53
  * {@link SignerAdapter} for ML-DSA-65 (NIST FIPS 204).
47
54
  *
48
- * Also used as the implementation for `"Dilithium3"` (the pre-standardisation
49
- * Round-3 variant) since the wire format is compatible.
55
+ * This is the primary signing adapter for Shell Chain. It is also used for
56
+ * `"Dilithium3"` keys since `pqcrypto-dilithium` v0.5 (the Rust crate used
57
+ * by shell-chain) implements FIPS 204 ML-DSA-65 — producing byte-identical
58
+ * keys and signatures (pk=1952 bytes, sk=4032 bytes, sig=3309 bytes).
50
59
  *
51
60
  * @example
52
61
  * ```typescript
package/dist/adapters.js CHANGED
@@ -1,8 +1,15 @@
1
1
  /**
2
2
  * Concrete SignerAdapter implementations for each PQ algorithm.
3
3
  *
4
- * @noble/post-quantum provides ML-DSA-65 and SLH-DSA-SHA2-256f.
5
- * Dilithium3 (pre-FIPS Round-3) uses {@link MlDsa65Adapter} as a stand-in.
4
+ * `@noble/post-quantum` provides ML-DSA-65 and SLH-DSA-SHA2-256f via
5
+ * WebAssembly-accelerated pure-JS implementations.
6
+ *
7
+ * **Dilithium3 compatibility note**: `pqcrypto-dilithium` v0.5 (used by
8
+ * shell-chain) implements ML-DSA-65 (FIPS 204) under the `dilithium3` name.
9
+ * `@noble/post-quantum` `ml_dsa65` produces byte-identical keys and
10
+ * signatures (pk=1952, sk=4032, sig=3309), so `MlDsa65Adapter` is fully
11
+ * wire-compatible with the chain's Dilithium3 verifier. Both `"Dilithium3"`
12
+ * and `"MlDsa65"` algorithm names route to the same adapter.
6
13
  *
7
14
  * @module adapters
8
15
  */
@@ -41,8 +48,10 @@ export function generateSlhDsaKeyPair(seed) {
41
48
  /**
42
49
  * {@link SignerAdapter} for ML-DSA-65 (NIST FIPS 204).
43
50
  *
44
- * Also used as the implementation for `"Dilithium3"` (the pre-standardisation
45
- * Round-3 variant) since the wire format is compatible.
51
+ * This is the primary signing adapter for Shell Chain. It is also used for
52
+ * `"Dilithium3"` keys since `pqcrypto-dilithium` v0.5 (the Rust crate used
53
+ * by shell-chain) implements FIPS 204 ML-DSA-65 — producing byte-identical
54
+ * keys and signatures (pk=1952 bytes, sk=4032 bytes, sig=3309 bytes).
46
55
  *
47
56
  * @example
48
57
  * ```typescript
@@ -86,7 +95,7 @@ export class MlDsa65Adapter {
86
95
  * @param message - The bytes to sign (typically an RLP-encoded tx hash).
87
96
  */
88
97
  async sign(message) {
89
- return ml_dsa65.sign(this._secretKey, message);
98
+ return ml_dsa65.sign(message, this._secretKey);
90
99
  }
91
100
  }
92
101
  /**
@@ -134,7 +143,7 @@ export class SlhDsaAdapter {
134
143
  * @param message - The bytes to sign (typically an RLP-encoded tx hash).
135
144
  */
136
145
  async sign(message) {
137
- return slh_dsa_sha2_256f.sign(this._secretKey, message);
146
+ return slh_dsa_sha2_256f.sign(message, this._secretKey);
138
147
  }
139
148
  }
140
149
  /**
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  export { bytesToHexAddress, PQ_ADDRESS_HRP, PQ_ADDRESS_LENGTH, PQ_ADDRESS_VERSION_V1, bytesToPqAddress, derivePqAddressFromPublicKey, isPqAddress, normalizeHexAddress, normalizePqAddress, pqAddressVersion, pqAddressToBytes, } from "./address.js";
2
2
  export { createShellProvider, createShellPublicClient, createShellWsClient, ShellProvider, shellDevnet, type CreateShellPublicClientOptions, } from "./provider.js";
3
3
  export { accountManagerAddress, accountManagerHexAddress, clearValidationCodeSelector, encodeClearValidationCodeCalldata, encodeRotateKeyCalldata, encodeSetValidationCodeCalldata, isSystemContractAddress, rotateKeySelector, setValidationCodeSelector, validatorRegistryAddress, validatorRegistryHexAddress, } from "./system-contracts.js";
4
- export { buildClearValidationCodeTransaction, buildRotateKeyTransaction, buildSetValidationCodeTransaction, buildSignature, buildSignedTransaction, buildSystemTransaction, buildTransaction, buildTransferTransaction, DEFAULT_MAX_FEE_PER_GAS, DEFAULT_MAX_PRIORITY_FEE_PER_GAS, DEFAULT_SYSTEM_GAS_LIMIT, DEFAULT_TRANSFER_GAS_LIMIT, DEFAULT_TX_TYPE, hashTransaction, hexBytes, } from "./transactions.js";
4
+ export { buildClearValidationCodeTransaction, buildRotateKeyTransaction, buildSetValidationCodeTransaction, buildSignature, buildSignedTransaction, buildSystemTransaction, buildTransaction, buildTransferTransaction, DEFAULT_MAX_FEE_PER_GAS, DEFAULT_MAX_PRIORITY_FEE_PER_GAS, DEFAULT_SYSTEM_GAS_LIMIT, DEFAULT_TRANSFER_GAS_LIMIT, DEFAULT_TX_TYPE, hashTransaction, } from "./transactions.js";
5
5
  export { assertSignerMatchesKeystore, decryptKeystore, exportEncryptedKeyJson, parseEncryptedKey, validateEncryptedKeyAddress, type ParsedShellKeystore, } from "./keystore.js";
6
6
  export { adapterFromKeyPair, generateAdapter, generateMlDsa65KeyPair, generateSlhDsaKeyPair, MlDsa65Adapter, SlhDsaAdapter, type MlDsa65KeyPair, type SlhDsaKeyPair, } from "./adapters.js";
7
- export { buildShellSignature, KEY_TYPE_TO_SIGNATURE_TYPE, publicKeyFromHex, ShellSigner, SIGNATURE_TYPE_IDS, signatureTypeFromKeyType, type SignerAdapter, } from "./signer.js";
8
- export type { AddressLike, HexString, ShellAccessListItem, ShellCipherParams, ShellEncryptedKey, ShellSendTransactionParams, ShellKdfParams, ShellSignature, ShellTransactionRequest, ShellTxByAddressPage, SignedShellTransaction, SignatureTypeName, } from "./types.js";
7
+ export { buildShellSignature, publicKeyFromHex, ShellSigner, signatureTypeFromKeyType, type SignerAdapter, } from "./signer.js";
8
+ export type { AddressLike, HexString, ShellAccessListItem, ShellCipherParams, ShellEncryptedKey, ShellNodeInfo, ShellSendTransactionParams, ShellKdfParams, ShellSignature, ShellStorageProfile, ShellTransactionRequest, ShellTxByAddressPage, ShellTxWitness, ShellWitnessBundle, SignedShellTransaction, SignatureTypeName, } from "./types.js";
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export { bytesToHexAddress, PQ_ADDRESS_HRP, PQ_ADDRESS_LENGTH, PQ_ADDRESS_VERSION_V1, bytesToPqAddress, derivePqAddressFromPublicKey, isPqAddress, normalizeHexAddress, normalizePqAddress, pqAddressVersion, pqAddressToBytes, } from "./address.js";
2
2
  export { createShellProvider, createShellPublicClient, createShellWsClient, ShellProvider, shellDevnet, } from "./provider.js";
3
3
  export { accountManagerAddress, accountManagerHexAddress, clearValidationCodeSelector, encodeClearValidationCodeCalldata, encodeRotateKeyCalldata, encodeSetValidationCodeCalldata, isSystemContractAddress, rotateKeySelector, setValidationCodeSelector, validatorRegistryAddress, validatorRegistryHexAddress, } from "./system-contracts.js";
4
- export { buildClearValidationCodeTransaction, buildRotateKeyTransaction, buildSetValidationCodeTransaction, buildSignature, buildSignedTransaction, buildSystemTransaction, buildTransaction, buildTransferTransaction, DEFAULT_MAX_FEE_PER_GAS, DEFAULT_MAX_PRIORITY_FEE_PER_GAS, DEFAULT_SYSTEM_GAS_LIMIT, DEFAULT_TRANSFER_GAS_LIMIT, DEFAULT_TX_TYPE, hashTransaction, hexBytes, } from "./transactions.js";
4
+ export { buildClearValidationCodeTransaction, buildRotateKeyTransaction, buildSetValidationCodeTransaction, buildSignature, buildSignedTransaction, buildSystemTransaction, buildTransaction, buildTransferTransaction, DEFAULT_MAX_FEE_PER_GAS, DEFAULT_MAX_PRIORITY_FEE_PER_GAS, DEFAULT_SYSTEM_GAS_LIMIT, DEFAULT_TRANSFER_GAS_LIMIT, DEFAULT_TX_TYPE, hashTransaction, } from "./transactions.js";
5
5
  export { assertSignerMatchesKeystore, decryptKeystore, exportEncryptedKeyJson, parseEncryptedKey, validateEncryptedKeyAddress, } from "./keystore.js";
6
6
  export { adapterFromKeyPair, generateAdapter, generateMlDsa65KeyPair, generateSlhDsaKeyPair, MlDsa65Adapter, SlhDsaAdapter, } from "./adapters.js";
7
- export { buildShellSignature, KEY_TYPE_TO_SIGNATURE_TYPE, publicKeyFromHex, ShellSigner, SIGNATURE_TYPE_IDS, signatureTypeFromKeyType, } from "./signer.js";
7
+ export { buildShellSignature, publicKeyFromHex, ShellSigner, signatureTypeFromKeyType, } from "./signer.js";
@@ -16,7 +16,7 @@
16
16
  * @module provider
17
17
  */
18
18
  import { type Chain, type PublicClient } from "viem";
19
- import type { SignedShellTransaction } from "./types.js";
19
+ import type { ShellNodeInfo, ShellStorageProfile, ShellWitnessBundle, SignedShellTransaction } from "./types.js";
20
20
  /**
21
21
  * Pre-configured viem chain definition for Shell Devnet.
22
22
  *
@@ -155,6 +155,34 @@ export declare class ShellProvider {
155
155
  * @returns Array of transaction receipt objects.
156
156
  */
157
157
  getBlockReceipts(block: string): Promise<unknown[]>;
158
+ /**
159
+ * Fetch metadata about the connected Shell Chain node.
160
+ *
161
+ * Calls `shell_getNodeInfo`.
162
+ *
163
+ * @returns Node info including version, block height, peer count, and storage profile.
164
+ */
165
+ getNodeInfo(): Promise<ShellNodeInfo>;
166
+ /**
167
+ * Fetch the PQ witness bundle for a block.
168
+ *
169
+ * Calls `shell_getWitness`. Returns `null` if the witness has been pruned
170
+ * (the node is running with a `full` or `light` profile and the STARK proof
171
+ * has already replaced the raw signatures).
172
+ *
173
+ * @param blockNumberOrHash - Hex block number (`"0x1a"`) or block hash.
174
+ * @returns Witness bundle, or `null` if pruned.
175
+ */
176
+ getWitness(blockNumberOrHash: string): Promise<ShellWitnessBundle | null>;
177
+ /**
178
+ * Fetch the active storage profile of the connected node.
179
+ *
180
+ * Convenience wrapper around {@link getNodeInfo}.
181
+ *
182
+ * @returns Storage profile string (`"archive"`, `"full"`, or `"light"`), or
183
+ * `undefined` if the node does not report it.
184
+ */
185
+ getStorageProfile(): Promise<ShellStorageProfile | undefined>;
158
186
  }
159
187
  /**
160
188
  * Create a viem `PublicClient` connected to Shell Chain over HTTP.
package/dist/provider.js CHANGED
@@ -137,6 +137,41 @@ export class ShellProvider {
137
137
  async getBlockReceipts(block) {
138
138
  return this.request("eth_getBlockReceipts", [block]);
139
139
  }
140
+ /**
141
+ * Fetch metadata about the connected Shell Chain node.
142
+ *
143
+ * Calls `shell_getNodeInfo`.
144
+ *
145
+ * @returns Node info including version, block height, peer count, and storage profile.
146
+ */
147
+ async getNodeInfo() {
148
+ return this.request("shell_getNodeInfo", []);
149
+ }
150
+ /**
151
+ * Fetch the PQ witness bundle for a block.
152
+ *
153
+ * Calls `shell_getWitness`. Returns `null` if the witness has been pruned
154
+ * (the node is running with a `full` or `light` profile and the STARK proof
155
+ * has already replaced the raw signatures).
156
+ *
157
+ * @param blockNumberOrHash - Hex block number (`"0x1a"`) or block hash.
158
+ * @returns Witness bundle, or `null` if pruned.
159
+ */
160
+ async getWitness(blockNumberOrHash) {
161
+ return this.request("shell_getWitness", [blockNumberOrHash]);
162
+ }
163
+ /**
164
+ * Fetch the active storage profile of the connected node.
165
+ *
166
+ * Convenience wrapper around {@link getNodeInfo}.
167
+ *
168
+ * @returns Storage profile string (`"archive"`, `"full"`, or `"light"`), or
169
+ * `undefined` if the node does not report it.
170
+ */
171
+ async getStorageProfile() {
172
+ const info = await this.getNodeInfo();
173
+ return info.storage_profile;
174
+ }
140
175
  }
141
176
  /**
142
177
  * Create a viem `PublicClient` connected to Shell Chain over HTTP.
@@ -190,8 +190,8 @@ export declare function hexBytes(bytes: Uint8Array): HexString;
190
190
  * `keccak256(RLP(tx))` — the same scheme as Ethereum EIP-1559 signing.
191
191
  *
192
192
  * **Encoding order** (EIP-2718 type-2 fields):
193
- * chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit,
194
- * to, value, data, accessList, maxFeePerBlobGas (if present), blobVersionedHashes (if present)
193
+ * chainId, nonce, to, value, data, gasLimit, maxFeePerGas, maxPriorityFeePerGas,
194
+ * accessList, txType, blobFeeFlag, maxFeePerBlobGas, blobVersionedHashes
195
195
  *
196
196
  * @example
197
197
  * ```typescript
@@ -7,8 +7,9 @@
7
7
  *
8
8
  * @module transactions
9
9
  */
10
- import { bytesToHex, keccak256, toRlp, numberToHex, hexToBytes } from "viem";
10
+ import { bytesToHex, keccak256, toRlp, hexToBytes } from "viem";
11
11
  import { accountManagerAddress, encodeClearValidationCodeCalldata, encodeRotateKeyCalldata, encodeSetValidationCodeCalldata, } from "./system-contracts.js";
12
+ import { normalizeHexAddress } from "./address.js";
12
13
  /** Default transaction type: `2` (EIP-1559). */
13
14
  export const DEFAULT_TX_TYPE = 2;
14
15
  /** Default gas limit for simple SHELL token transfers (`21_000`). */
@@ -25,6 +26,22 @@ function toByteArray(bytes) {
25
26
  function toHexData(data) {
26
27
  return data ?? "0x";
27
28
  }
29
+ function toRlpUint(value) {
30
+ const numeric = typeof value === "string" ? BigInt(value) : BigInt(value);
31
+ if (numeric === 0n) {
32
+ return "0x";
33
+ }
34
+ return `0x${numeric.toString(16)}`;
35
+ }
36
+ function toRlpAccessList(accessList) {
37
+ if (!accessList || accessList.length === 0) {
38
+ return [];
39
+ }
40
+ return accessList.map((item) => [
41
+ normalizeHexAddress(item.address),
42
+ item.storage_keys.map((key) => key),
43
+ ]);
44
+ }
28
45
  /**
29
46
  * Low-level transaction builder that maps camelCase options to the
30
47
  * snake_case wire format expected by the Shell node.
@@ -210,8 +227,8 @@ export function hexBytes(bytes) {
210
227
  * `keccak256(RLP(tx))` — the same scheme as Ethereum EIP-1559 signing.
211
228
  *
212
229
  * **Encoding order** (EIP-2718 type-2 fields):
213
- * chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit,
214
- * to, value, data, accessList, maxFeePerBlobGas (if present), blobVersionedHashes (if present)
230
+ * chainId, nonce, to, value, data, gasLimit, maxFeePerGas, maxPriorityFeePerGas,
231
+ * accessList, txType, blobFeeFlag, maxFeePerBlobGas, blobVersionedHashes
215
232
  *
216
233
  * @example
217
234
  * ```typescript
@@ -226,24 +243,21 @@ export function hexBytes(bytes) {
226
243
  * @returns 32-byte keccak256 hash as a `Uint8Array`.
227
244
  */
228
245
  export function hashTransaction(tx) {
229
- const to = tx.to ? hexToBytes(tx.to.startsWith("0x") ? tx.to : `0x${tx.to}`) : new Uint8Array(0);
230
- const data = hexToBytes(tx.data);
231
- const value = hexToBytes(tx.value);
232
246
  const fields = [
233
- numberToHex(tx.chain_id),
234
- numberToHex(tx.nonce),
235
- numberToHex(tx.max_priority_fee_per_gas),
236
- numberToHex(tx.max_fee_per_gas),
237
- numberToHex(tx.gas_limit),
238
- bytesToHex(to),
239
- bytesToHex(value),
240
- bytesToHex(data),
241
- "0x", // empty access list
247
+ toRlpUint(tx.chain_id),
248
+ toRlpUint(tx.nonce),
249
+ tx.to ? normalizeHexAddress(tx.to) : "0x",
250
+ toRlpUint(tx.value),
251
+ tx.data,
252
+ toRlpUint(tx.gas_limit),
253
+ toRlpUint(tx.max_fee_per_gas),
254
+ toRlpUint(tx.max_priority_fee_per_gas),
255
+ toRlpAccessList(tx.access_list),
256
+ toRlpUint(tx.tx_type ?? DEFAULT_TX_TYPE),
257
+ toRlpUint(tx.max_fee_per_blob_gas != null ? 1 : 0),
258
+ toRlpUint(tx.max_fee_per_blob_gas ?? 0),
259
+ (tx.blob_versioned_hashes ?? []).map((hash) => hash),
242
260
  ];
243
- if (tx.max_fee_per_blob_gas != null) {
244
- fields.push(numberToHex(tx.max_fee_per_blob_gas));
245
- fields.push("0x"); // empty blob versioned hashes
246
- }
247
261
  const rlpEncoded = toRlp(fields);
248
262
  const hash = hexToBytes(keccak256(rlpEncoded));
249
263
  return hash;
package/dist/types.d.ts CHANGED
@@ -82,6 +82,61 @@ export interface SignedShellTransaction {
82
82
  */
83
83
  sender_pubkey?: number[] | null;
84
84
  }
85
+ /**
86
+ * Node storage profile as advertised via the `StorageCapability` P2P message.
87
+ *
88
+ * - `"archive"` — all TX bodies and PQ witnesses kept forever; STARK proofs never replace witnesses.
89
+ * - `"full"` — TX bodies kept forever; PQ witnesses replaced by STARK proofs when they arrive.
90
+ * - `"light"` — rolling ~4096-block window; older data pruned.
91
+ */
92
+ export type ShellStorageProfile = "archive" | "full" | "light";
93
+ /**
94
+ * Response from `shell_getNodeInfo`.
95
+ *
96
+ * Contains runtime metadata about the connected Shell Chain node.
97
+ */
98
+ export interface ShellNodeInfo {
99
+ /** Node software version string, e.g. `"shell-node/0.17.0"`, independent of the SDK package version. */
100
+ version: string;
101
+ /** Chain ID as a decimal string. */
102
+ chain_id: string;
103
+ /** Current head block number (decimal). */
104
+ block_height: number;
105
+ /** libp2p peer ID of this node. */
106
+ peer_id: string;
107
+ /** Number of currently connected peers. */
108
+ peer_count: number;
109
+ /** Active storage profile. */
110
+ storage_profile?: ShellStorageProfile;
111
+ /** Oldest block number for which this node has full body data. */
112
+ oldest_body_block?: number;
113
+ }
114
+ /**
115
+ * A single PQ transaction witness from `shell_getBlockWitnesses` / `shell_getWitness`.
116
+ */
117
+ export interface ShellTxWitness {
118
+ /** Zero-based transaction index within the block. */
119
+ tx_index: number;
120
+ /** Signature algorithm name. */
121
+ sig_type: SignatureTypeName;
122
+ /** Raw signature bytes as hex string. */
123
+ signature: string;
124
+ /** Raw public key bytes as hex string (only present on first-use txs). */
125
+ public_key?: string;
126
+ }
127
+ /**
128
+ * Response from `shell_getWitness` for a single block.
129
+ */
130
+ export interface ShellWitnessBundle {
131
+ /** Block hash (0x-prefixed). */
132
+ block_hash: string;
133
+ /** Block number. */
134
+ block_number: number;
135
+ /** Number of witnesses in this bundle. */
136
+ witness_count: number;
137
+ /** Individual transaction witnesses. */
138
+ witnesses: ShellTxWitness[];
139
+ }
85
140
  /** Paginated response from `shell_getTransactionsByAddress`. */
86
141
  export interface ShellTxByAddressPage {
87
142
  address: AddressLike;
@@ -0,0 +1,59 @@
1
+ # Shell SDK — Minimal dApp Example
2
+
3
+ Demonstrates the core Shell SDK flow:
4
+ 1. Generate a post-quantum key pair (ML-DSA-65)
5
+ 2. Create a signer and derive address
6
+ 3. Query balance from a Shell node
7
+ 4. Build + sign a transaction (without broadcasting)
8
+
9
+ ## Node.js (server / CLI)
10
+
11
+ ```bash
12
+ # From the shell-sdk root:
13
+ npm install
14
+ npm run build
15
+
16
+ # Run the demo (points at localhost:8545 by default)
17
+ node examples/minimal-dapp/node-demo.mjs
18
+
19
+ # Point at a different RPC endpoint:
20
+ SHELL_RPC_URL=http://my-node:8545 node examples/minimal-dapp/node-demo.mjs
21
+ ```
22
+
23
+ Expected output:
24
+ ```
25
+ Address (pq1…): pq1…
26
+ Address (0x…): 0x…
27
+ Balance (wei): 0 ← 0 for a fresh account; fund it via the faucet
28
+ Signed tx type: 2
29
+ Signature length (bytes): 3309
30
+ Sender pubkey set: true
31
+
32
+ All done! Connect to a live node and call provider.sendTransaction(signed) to broadcast.
33
+ ```
34
+
35
+ ## Browser
36
+
37
+ ```bash
38
+ # Build the SDK first
39
+ cd ../../ # shell-sdk root
40
+ npm run build
41
+
42
+ # Serve this directory with any HTTP server
43
+ npx serve examples/minimal-dapp/
44
+ # or
45
+ python3 -m http.server 8080
46
+ ```
47
+
48
+ Then open `http://localhost:8080/browser-demo.html` in your browser.
49
+
50
+ ## Key sizes (ML-DSA-65 / FIPS 204)
51
+
52
+ | Field | Bytes |
53
+ |-------------|-------|
54
+ | Public key | 1952 |
55
+ | Secret key | 4032 |
56
+ | Signature | 3309 |
57
+
58
+ These match `pqcrypto-dilithium` v0.5 (used by shell-chain), confirming wire
59
+ compatibility. Both `"Dilithium3"` and `"MlDsa65"` keys use this algorithm.
@@ -0,0 +1,98 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>Shell SDK — Minimal Browser dApp</title>
6
+ <style>
7
+ body { font-family: monospace; max-width: 800px; margin: 2rem auto; padding: 0 1rem; }
8
+ pre { background: #1a1a1a; color: #d4d4d4; padding: 1rem; border-radius: 4px; overflow-x: auto; }
9
+ button { padding: .5rem 1.2rem; cursor: pointer; margin: .25rem; }
10
+ #log { white-space: pre-wrap; }
11
+ </style>
12
+ </head>
13
+ <body>
14
+ <h1>Shell SDK — Minimal Browser dApp</h1>
15
+ <p>This page runs entirely in the browser using ES modules from a CDN (or local build).</p>
16
+
17
+ <button onclick="generateKey()">1. Generate Key</button>
18
+ <button onclick="queryBalance()">2. Query Balance</button>
19
+ <button onclick="buildTx()">3. Build &amp; Sign Tx</button>
20
+
21
+ <pre id="log">Click a button to start…</pre>
22
+
23
+ <script type="module">
24
+ // Adjust the import path to your local build or CDN URL.
25
+ // e.g. import { MlDsa65Adapter, ... } from "./shell-sdk.esm.js";
26
+ // For local dev: `cd shell-sdk && npm run build` then serve this folder.
27
+ const SDK_URL = "./../../dist/index.js";
28
+
29
+ let signer = null;
30
+
31
+ function log(...args) {
32
+ const el = document.getElementById("log");
33
+ el.textContent += args.map(String).join(" ") + "\n";
34
+ }
35
+
36
+ async function loadSdk() {
37
+ try {
38
+ return await import(SDK_URL);
39
+ } catch (e) {
40
+ log("Failed to load SDK:", e.message);
41
+ log("Make sure you have run `npm run build` in the shell-sdk directory");
42
+ log("and are serving this file from an HTTP server (not file://).");
43
+ throw e;
44
+ }
45
+ }
46
+
47
+ window.generateKey = async function () {
48
+ const { MlDsa65Adapter, ShellSigner } = await loadSdk();
49
+ const adapter = MlDsa65Adapter.generate();
50
+ signer = new ShellSigner("MlDsa65", adapter);
51
+ log("=== Key Generated ===");
52
+ log("Address (pq1…):", signer.getAddress());
53
+ log("Address (0x…):", signer.getHexAddress());
54
+ log("Public key size:", adapter.getPublicKey().length, "bytes");
55
+ };
56
+
57
+ window.queryBalance = async function () {
58
+ if (!signer) { log("Generate a key first!"); return; }
59
+ const { createShellProvider } = await loadSdk();
60
+ const rpc = prompt("RPC URL?", "http://127.0.0.1:8545");
61
+ if (!rpc) return;
62
+ const provider = createShellProvider({ url: rpc });
63
+ try {
64
+ const bal = await provider.getBalance({ address: signer.getHexAddress() });
65
+ log("Balance:", bal.toString(), "wei");
66
+ } catch (e) {
67
+ log("getBalance error:", e.message);
68
+ }
69
+ };
70
+
71
+ window.buildTx = async function () {
72
+ if (!signer) { log("Generate a key first!"); return; }
73
+ const { hashTransaction } = await loadSdk();
74
+ const tx = {
75
+ from: signer.getHexAddress(),
76
+ chain_id: 1337,
77
+ nonce: 0,
78
+ to: "0x0000000000000000000000000000000000000001",
79
+ value: "0x1",
80
+ data: "0x",
81
+ gas_limit: 21000,
82
+ max_fee_per_gas: 1_000_000_000,
83
+ max_priority_fee_per_gas: 1_000_000_000,
84
+ };
85
+ const txHash = hashTransaction(tx);
86
+ const signed = await signer.buildSignedTransaction({ tx, txHash, includePublicKey: true });
87
+ log("=== Signed Transaction ===");
88
+ log("Type:", signed.type);
89
+ log("From:", signed.from);
90
+ log("To:", signed.to);
91
+ log("Value:", signed.value.toString());
92
+ log("Sig length:", signed.signature.data.length, "bytes");
93
+ log("Has pubkey:", signed.sender_pubkey != null);
94
+ log("\nCall provider.sendTransaction(signed) to broadcast to a live node.");
95
+ };
96
+ </script>
97
+ </body>
98
+ </html>
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Shell SDK minimal dApp — Node.js script
3
+ *
4
+ * Demonstrates: connect provider → query balance → sign + build transaction
5
+ *
6
+ * Usage:
7
+ * npm install shell-sdk
8
+ * node node-demo.mjs
9
+ *
10
+ * By default this points at the Shell devnet RPC. Set SHELL_RPC_URL to
11
+ * override.
12
+ */
13
+
14
+ import { MlDsa65Adapter, ShellSigner, createShellProvider } from "shell-sdk";
15
+
16
+ const RPC_URL = process.env.SHELL_RPC_URL ?? "http://127.0.0.1:8545";
17
+
18
+ async function main() {
19
+ // ── 1. Create a provider ──────────────────────────────────────────────
20
+ const provider = createShellProvider({ url: RPC_URL });
21
+
22
+ // ── 2. Generate a key pair and build a signer ─────────────────────────
23
+ const adapter = MlDsa65Adapter.generate();
24
+ const signer = new ShellSigner("MlDsa65", adapter);
25
+
26
+ console.log("Address (pq1…):", signer.getAddress());
27
+ console.log("Address (0x…):", signer.getHexAddress());
28
+
29
+ // ── 3. Query balance ───────────────────────────────────────────────────
30
+ try {
31
+ const balance = await provider.client.getBalance({ address: signer.getHexAddress() });
32
+ console.log("Balance (wei):", balance.toString());
33
+ } catch (err) {
34
+ console.warn("getBalance failed (node may not be running):", err.message);
35
+ }
36
+
37
+ // ── 4. Build and sign a transaction ───────────────────────────────────
38
+ // NOTE: sending requires a funded account + live node.
39
+ // This section shows the sign-only flow without broadcasting.
40
+ const TO = "0x0000000000000000000000000000000000000001";
41
+
42
+ /** @type {import("shell-sdk").ShellTransactionRequest} */
43
+ const tx = {
44
+ from: signer.getHexAddress(),
45
+ chain_id: 1337,
46
+ nonce: 0,
47
+ to: TO,
48
+ value: "0x1",
49
+ data: "0x",
50
+ gas_limit: 21000,
51
+ max_fee_per_gas: 1_000_000_000,
52
+ max_priority_fee_per_gas: 1_000_000_000,
53
+ };
54
+
55
+ const { hashTransaction } = await import("shell-sdk");
56
+ const txHash = hashTransaction(tx);
57
+ const signed = await signer.buildSignedTransaction({ tx, txHash, includePublicKey: true });
58
+ console.log("Signed tx type:", signed.tx.tx_type ?? 2, "(EIP-1559)");
59
+ console.log("Signature length (bytes):", signed.signature.data.length);
60
+ console.log("Sender pubkey set:", signed.sender_pubkey != null);
61
+
62
+ console.log("\nAll done! Connect to a live node and call provider.sendTransaction(signed) to broadcast.");
63
+ }
64
+
65
+ main().catch((err) => {
66
+ console.error(err);
67
+ process.exit(1);
68
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "shell-sdk",
3
- "version": "0.1.0",
4
- "description": "TypeScript SDK for Shell Chain",
3
+ "version": "0.3.0",
4
+ "description": "TypeScript SDK for Shell Chain — build quantum-safe dApps before Q-Day.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",
@@ -45,12 +45,15 @@
45
45
  }
46
46
  },
47
47
  "files": [
48
- "dist"
48
+ "dist",
49
+ "examples",
50
+ "CHANGELOG.md"
49
51
  ],
50
52
  "scripts": {
51
53
  "build": "tsc -p tsconfig.json",
52
54
  "clean": "rm -rf dist",
53
- "typecheck": "tsc -p tsconfig.json --noEmit"
55
+ "typecheck": "tsc -p tsconfig.json --noEmit",
56
+ "test": "npm run build && node --test tests/*.test.mjs"
54
57
  },
55
58
  "keywords": [
56
59
  "shell-chain",