@orbinum/sdk 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Official TypeScript SDK for Orbinum — a privacy-focused blockchain built on Substrate with an EVM compatibility layer.
4
4
 
5
- The SDK provides typed modules for interacting with the Orbinum protocol: Substrate RPC, EVM JSON-RPC, shielded pool operations, account identity and mapping, `rpc-v2`, ZK verifier, precompiles, and the indexer REST API.
5
+ The SDK provides typed modules for interacting with the Orbinum protocol: Substrate RPC, EVM JSON-RPC, shielded pool operations, ZK proof generation, encrypted note vault, account identity and mapping, relayer status, `rpc-v2`, ZK verifier, precompiles, and the indexer REST API.
6
6
 
7
7
  ## Installation
8
8
 
@@ -14,9 +14,9 @@ pnpm add @orbinum/sdk
14
14
 
15
15
  ## Requirements
16
16
 
17
- - Node.js 18 or later
17
+ - Node.js 18 or later (WebCrypto API required for vault operations)
18
18
  - A running Orbinum node (Substrate WebSocket endpoint)
19
- - An EVM JSON-RPC endpoint (optional, required for EVM and precompile operations)
19
+ - An EVM JSON-RPC endpoint (optional required for EVM and precompile operations)
20
20
 
21
21
  ## Quick Start
22
22
 
@@ -29,8 +29,8 @@ const client = await OrbinumClient.connect({
29
29
  });
30
30
 
31
31
  const chainInfo = await client.substrate.getChainInfo();
32
- const root = await client.rpcV2.privacy.getMerkleRoot();
33
- const events = await client.substrate.queryBlockEvents('0xabc...');
32
+ const root = await client.rpcV2.privacy.getMerkleRoot();
33
+ const events = await client.substrate.queryBlockEvents('0xabc...');
34
34
  ```
35
35
 
36
36
  `OrbinumClient` wires together the SDK surface under a single interface.
@@ -64,42 +64,6 @@ await provider.connect();
64
64
  const client = provider.client; // OrbinumClient once connected
65
65
  ```
66
66
 
67
- ## rpc-v2
68
-
69
- Orbinum's `rpc-v2` is organized by namespace.
70
-
71
- - Standard Frontier namespaces: `eth_*`, `net_*`, `web3_*`, `txpool_*`, `debug_*`
72
- - Orbinum-specific namespace: `privacy_*`
73
-
74
- In the SDK, `rpc-v2` is modeled as a dedicated top-level module, exposed through `client.rpcV2`. Each namespace lives under `src/rpc-v2/`.
75
-
76
- Current typed coverage:
77
-
78
- - `client.rpcV2.privacy.getMerkleRoot()`
79
- - `client.rpcV2.privacy.getMerkleProof(leafIndex)`
80
- - `client.rpcV2.privacy.getNullifierStatus(nullifier)`
81
- - `client.rpcV2.privacy.getPoolStats()`
82
-
83
- Example:
84
-
85
- ```ts
86
- const root = await client.rpcV2.privacy.getMerkleRoot();
87
-
88
- const proof = await client.rpcV2.privacy.getMerkleProof(12);
89
- // { path: string[]; leafIndex: number; treeDepth: number; }
90
-
91
- const status = await client.rpcV2.privacy.getNullifierStatus('0x...');
92
- // { nullifier: string; isSpent: boolean; }
93
-
94
- const stats = await client.rpcV2.privacy.getPoolStats();
95
- // { merkleRoot: string; commitmentCount: number; totalBalance: string; ... }
96
- ```
97
-
98
- Notes:
99
-
100
- - `rpc-v2` responses use `snake_case` from the node; the SDK normalizes them to `camelCase`.
101
- - `u128` values are exposed as decimal strings to avoid precision loss.
102
-
103
67
  ## Modules
104
68
 
105
69
  ### SubstrateClient
@@ -131,8 +95,8 @@ substrate.blocks$.subscribe(block => console.log(block.number));
131
95
  const result = await substrate.request('system_name', []);
132
96
 
133
97
  // Transaction helpers
134
- const tx = await substrate.txFromCallData(callBytes);
135
- const finalized = await tx.signAndSubmit(signer);
98
+ const tx = await substrate.txFromCallData(callBytes);
99
+ const finalized = await tx.signAndSubmit(signer);
136
100
  const finalized2 = await substrate.submit(signedHex);
137
101
  substrate.submitAndWatch(signedHex).subscribe(event => console.log(event));
138
102
  ```
@@ -173,6 +137,26 @@ const logs = await explorer.getLogs({ fromBlock: 100, toBlock: 200, address: '0
173
137
 
174
138
  Types: `EvmBlock`, `EvmTransaction`, `EvmAddressInfo`, `EvmTxSummary`, `EvmLog`, `TokenInfo`, `TokenTransfer`.
175
139
 
140
+ ### rpc-v2 / PrivacyModule
141
+
142
+ Orbinum's `rpc-v2` is organized by namespace. In the SDK, it is exposed through `client.rpcV2`.
143
+
144
+ ```ts
145
+ const root = await client.rpcV2.privacy.getMerkleRoot();
146
+ const proof = await client.rpcV2.privacy.getMerkleProof(12);
147
+ // { path: string[]; leafIndex: number; treeDepth: number; }
148
+
149
+ const status = await client.rpcV2.privacy.getNullifierStatus('0x...');
150
+ // { nullifier: string; isSpent: boolean; }
151
+
152
+ const stats = await client.rpcV2.privacy.getPoolStats();
153
+ // { merkleRoot: string; commitmentCount: number; totalBalance: string; ... }
154
+ ```
155
+
156
+ Notes:
157
+ - `rpc-v2` responses use `snake_case` from the node; the SDK normalizes them to `camelCase`.
158
+ - `u128` values are exposed as decimal strings to avoid precision loss.
159
+
176
160
  ### ZkVerifierModule
177
161
 
178
162
  Typed access to the on-chain ZK verifier — circuit version info, VK hashes, and version history.
@@ -180,8 +164,7 @@ Typed access to the on-chain ZK verifier — circuit version info, VK hashes, an
180
164
  ```ts
181
165
  import { ZkVerifierModule } from '@orbinum/sdk';
182
166
 
183
- const zkv = new ZkVerifierModule(substrate);
184
-
167
+ const zkv = new ZkVerifierModule(substrate);
185
168
  const info = await zkv.getCircuitVersionInfo('circuit-id');
186
169
  const vkHash = await zkv.getVkHash('circuit-id', 1);
187
170
  const stats = await zkv.getVersionStats('circuit-id');
@@ -190,401 +173,198 @@ const history = await zkv.getHistoricalVersions('circuit-id');
190
173
 
191
174
  ### ShieldedPoolModule
192
175
 
193
- High-level interface for shielded pool extrinsics and note workflows. Requires a loaded `PrivacyKeyManager` and ZK proofs for unshield and private transfer.
176
+ High-level interface for shielded pool extrinsics. Transactions are built via polkadot-api's UnsafeApi (metadata-driven). Unshield and private transfer are submitted as **unsigned (gasless)** transactions by default — the fee is embedded in the ZK proof.
194
177
 
195
178
  ```ts
196
- // Shield (deposit) tokens into the shielded pool
197
- const { txResult, note } = await client.shieldedPool.buildAndShield({
198
- amount: 1_000_000n,
199
- assetId: 1,
200
- tokenAddress: '0xTokenAddress',
201
- }, signer);
179
+ import { ShieldedPoolModule } from '@orbinum/sdk';
202
180
 
203
- // Private transfer between notes
204
- await client.shieldedPool.privateTransfer({ ...params }, signer);
181
+ const pool = new ShieldedPoolModule(substrate);
205
182
 
206
- // Unshield (withdraw) tokens to a public address
207
- await client.shieldedPool.unshield({ ...params }, signer);
208
- ```
183
+ // Deposit tokens into the shielded pool (signed)
184
+ await pool.shield(params, signer);
209
185
 
210
- ### PrivacyModule (`rpc-v2`)
186
+ // Withdraw tokens via ZK proof (unsigned, gasless)
187
+ await pool.unshield(params);
211
188
 
212
- Typed wrapper for Orbinum `privacy_*` endpoints from `rpc-v2`.
189
+ // Private transfer between shielded addresses (unsigned, gasless)
190
+ await pool.privateTransfer(params);
213
191
 
214
- ```ts
215
- import { PrivacyModule } from '@orbinum/sdk';
192
+ // Batch shield
193
+ await pool.shieldBatch(params, signer);
216
194
 
217
- const privacy = new PrivacyModule(substrate);
195
+ // Claim accumulated relayer fees
196
+ await pool.claimShieldedFees(params, signer);
218
197
 
219
- const merkleRoot = await privacy.getMerkleRoot();
220
- const merkleProof = await privacy.getMerkleProof(4);
221
- const nullifier = await privacy.getNullifierStatus('0xNullifier');
222
- const stats = await privacy.getPoolStats();
198
+ // Selective disclosure
199
+ await pool.requestDisclosure(args, signer);
200
+ await pool.disclose(args, signer);
201
+ await pool.rejectDisclosure(args, signer);
223
202
  ```
224
203
 
225
- ### Privacy Keys
226
-
227
- Key derivation follows a deterministic chain from the user's wallet signature. No key material is ever stored by the SDK.
228
-
229
- ```ts
230
- import {
231
- PrivacyKeyManager,
232
- deriveOwnerPk,
233
- deriveSpendingKeyFromSignature,
234
- deriveSpendingKeyMessage,
235
- deriveViewingKey,
236
- } from '@orbinum/sdk';
237
-
238
- const message = deriveSpendingKeyMessage(chainId, evmAddress);
239
- const spendingKey = deriveSpendingKeyFromSignature(sigHex, chainId, evmAddress);
204
+ ### NoteBuilder
240
205
 
241
- const keyManager = new PrivacyKeyManager();
242
- keyManager.load(spendingKey);
206
+ Builds ZK notes (commitment + nullifier + encrypted memo) off-chain, with optional stealth address support. No network calls are made.
243
207
 
244
- const viewingKey = deriveViewingKey(spendingKey);
245
- const ownerPk = deriveOwnerPk(spendingKey);
208
+ Hash scheme:
246
209
  ```
247
-
248
- ### AccountMappingModule
249
-
250
- Manage on-chain identity by linking Substrate and EVM accounts, registering aliases, and interacting with the alias marketplace.
251
-
252
- ```ts
253
- await client.accountMapping.registerAlias({ alias: 'myalias' }, signer);
254
- await client.accountMapping.addChainLink({ chainId: 1, address: '0xEvmAddr' }, signer);
255
-
256
- const alias = await client.accountMapping.getAliasOf(substrateHex);
257
- const linked = await client.accountMapping.getChainLinks(substrateHex);
210
+ commitment = Poseidon4(value, assetId, ownerPk, blinding)
211
+ nullifier = Poseidon2(commitment, spendingKey)
258
212
  ```
259
213
 
260
- ### IndexerClient
261
-
262
- HTTP client for the Orbinum indexer REST API. All list endpoints are paginated.
263
-
264
214
  ```ts
265
- import { IndexerClient } from '@orbinum/sdk';
266
-
267
- const indexer = new IndexerClient({ baseUrl: 'https://indexer.orbinum.io' });
268
-
269
- const blocks = await indexer.getBlocks({ page: 1, limit: 20 });
270
- const extrinsics = await indexer.getExtrinsics({ address: '5F...' });
271
- const commitments = await indexer.getCommitments({ page: 1, limit: 20 });
272
- const nullifier = await indexer.getNullifierStatus('0xNullifier');
273
- const roots = await indexer.getMerkleRoots({ limit: 10 });
274
- const evmTxs = await indexer.getEvmTransactions({ address: '0xabc...' });
275
- const stats = await indexer.getStats();
215
+ import { NoteBuilder } from '@orbinum/sdk';
216
+
217
+ const note = await NoteBuilder.build({
218
+ value: 1_000_000n,
219
+ assetId: 0n,
220
+ ownerPk: myOwnerPk,
221
+ viewingPublicKey: recipientViewingPk,
222
+ // Optional: provide recipientOwnerPk to derive a per-note stealthOwnerPk
223
+ recipientOwnerPk: recipientOwnerPk,
224
+ });
225
+ // note.commitmentHex, note.nullifierHex, note.encryptedMemo
276
226
  ```
277
227
 
278
- ### Precompiles
228
+ When `recipientOwnerPk` is provided, the commitment uses a fresh `stealthOwnerPk` derived from an ephemeral ECDH key, making each transfer unlinkable even when the same privacy address is reused.
279
229
 
280
- Typed wrappers for Orbinum's EVM precompile contracts.
230
+ ### NoteDecryptor
281
231
 
282
- | Precompile | Address | Description |
283
- |---|---|---|
284
- | `ShieldedPoolPrecompile` | `0x0801` | Shield, unshield, and private transfer from EVM |
285
- | `AccountMappingPrecompile` | `0x0800` | Identity and alias operations from EVM |
286
- | `CryptoPrecompiles` | `0x0400–0x0403` | Frontier crypto utilities |
232
+ Scans on-chain commitments and attempts to decrypt them using the viewer's viewing key.
287
233
 
288
234
  ```ts
289
- import { ShieldedPoolPrecompile, decodePrecompileCalldata } from '@orbinum/sdk';
290
-
291
- const precompile = new ShieldedPoolPrecompile(evmClient);
292
- await precompile.shield(amount, tokenAddress, commitment, walletSigner);
293
-
294
- // Decode raw EVM calldata for a known precompile
295
- const decoded = decodePrecompileCalldata('0x0800', calldata);
296
- ```
297
-
298
- ## Extrinsic & Event Decoders
299
-
300
- The `mapExtrinsicArgs` and `mapZkEventData` helpers decode raw pallet call data from the indexer or block scanner into typed objects.
235
+ import { tryDecryptNote, tryDecryptNoteVerbose, computeNullifier } from '@orbinum/sdk';
301
236
 
302
- ```ts
303
- import { mapExtrinsicArgs, mapZkEventData } from '@orbinum/sdk';
237
+ // Returns a ZkNote on success, null on key mismatch or commitment failure
238
+ const note = tryDecryptNote(commitment, viewingSecretKey, spendingKey, ownOwnerPk);
304
239
 
305
- const decoded = mapExtrinsicArgs('shieldedPool', 'shield', rawArgs);
306
- // DecodedShieldArgs | DecodedUnshieldArgs | ...
240
+ // Returns the note plus a human-readable failure reason (useful for debugging)
241
+ const { note, reason } = tryDecryptNoteVerbose(commitment, viewingSecretKey, spendingKey, ownOwnerPk);
307
242
 
308
- const eventData = mapZkEventData('ProofVerified', rawEventData);
243
+ // Compute nullifier directly
244
+ const nullifier = computeNullifier(commitmentBigint, spendingKey);
309
245
  ```
310
246
 
311
- ## Substrate SCALE Primitives
312
-
313
- SCALE codec primitives from `@polkadot-api/substrate-bindings` are re-exported directly from the SDK. There is no need to install that package separately.
247
+ `ScanCommitment` shape: `{ commitmentHex: string; leafIndex: number; encryptedMemo: string | null }`.
314
248
 
315
- ```ts
316
- import { Blake2256, AccountId, u128, u64, Storage, Keccak256 } from '@orbinum/sdk';
317
- import { base58, getSs58AddressInfo } from '@orbinum/sdk';
318
- ```
319
-
320
- ## Key Derivation Chain
321
-
322
- ```
323
- wallet.sign(message)
324
- |
325
- v
326
- deriveSpendingKeyFromSignature() -- HKDF-SHA256 over signature bytes, mod BN254_R
327
- |
328
- +-- deriveViewingKey() -- HKDF-SHA256 -> 32-byte ChaCha20 symmetric key
329
- | (used to encrypt/decrypt note memos)
330
- |
331
- +-- deriveOwnerPk() -- BabyJubJub scalar multiplication -> Ax
332
- (embedded in shielded note commitments)
333
- ```
334
-
335
- ## Development
336
-
337
- ### Typing Layout
338
-
339
- The SDK organizes types by feature ownership.
340
-
341
- - `types/index.ts`: public shared types of the feature
342
- - `types/pallet-events.ts`: event payloads and discriminated unions for that pallet
343
- - `types/pallet-extrinsics.ts`: extrinsic argument types and call unions for that pallet
344
- - `types/raw.ts`: internal node or RPC response shapes used only for transport mapping
345
-
346
- Examples:
347
-
348
- - `src/shielded-pool/types/index.ts`
349
- - `src/shielded-pool/types/pallet-events.ts`
350
- - `src/shielded-pool/types/pallet-extrinsics.ts`
351
- - `src/rpc-v2/types/raw.ts`
352
- - `src/indexer/types/index.ts`
353
- - `src/account-mapping/types/index.ts`
354
-
355
- Rules:
356
-
357
- 1. If a type is part of the public API of a feature, keep it in that feature's `types/index.ts`.
358
- 2. If a type only mirrors a transport payload from RPC, keep it in `types/raw.ts`.
359
- 3. Do not reintroduce a global `src/types.ts` or `src/types/` directory.
360
- 4. Export public types from `src/index.ts`, but keep ownership at the feature level.
361
-
362
- ### Extending `rpc-v2`
363
-
364
- The intended extension pattern is namespace-oriented and centralized under `RpcV2Module`.
365
-
366
- For example, to add `net_*` support:
367
-
368
- 1. Create `src/rpc-v2/NetModule.ts`
369
- 2. Define raw response types that match the node's JSON exactly
370
- 3. Map raw `snake_case` or hex-shaped values into stable SDK-facing TypeScript types
371
- 4. Export the module from `src/rpc-v2/index.ts`
372
- 5. Attach it in `RpcV2Module`, for example as `client.rpcV2.net`
373
- 6. Add unit tests that verify RPC method names, params, and response mapping
374
-
375
- ```ts
376
- export class NetModule {
377
- constructor(private readonly substrate: SubstrateClient) {}
378
-
379
- async version(): Promise<string> {
380
- return this.substrate.request<string>('net_version', []);
381
- }
382
- }
383
- ```
249
+ ### EncryptedMemo
384
250
 
385
- ### Commands
251
+ All memos attached to shielded notes are exactly **168 bytes** (ChaCha20-Poly1305 with ECDH ephemeral key):
386
252
 
387
- ```bash
388
- pnpm install
389
- pnpm build # compile to dist/ (ESM + CJS + types)
390
- pnpm test # run test suite (769 tests)
391
- pnpm typecheck:all # typecheck src and tests
392
- pnpm lint # eslint
393
- pnpm format # prettier
394
253
  ```
395
-
396
- ## License
397
-
398
- ISC
399
-
400
-
401
- ## Installation
402
-
403
- ```bash
404
- npm install @orbinum/sdk
405
- # or
406
- pnpm add @orbinum/sdk
254
+ nonce(12) || ciphertext+MAC(124) || ephPk(32) = 168 bytes
407
255
  ```
408
256
 
409
- ## Requirements
410
-
411
- - Node.js 18 or later
412
- - A running Orbinum node (Substrate WebSocket endpoint)
413
- - An EVM JSON-RPC endpoint (optional, required for EVM and precompile operations)
414
-
415
- ## Quick Start
416
-
417
257
  ```ts
418
- import { OrbinumClient } from '@orbinum/sdk';
258
+ import { EncryptedMemo, ENCRYPTED_MEMO_SIZE } from '@orbinum/sdk';
259
+ // ENCRYPTED_MEMO_SIZE === 168
419
260
 
420
- const client = await OrbinumClient.connect({
421
- substrateWs: 'ws://localhost:9944',
422
- evmRpc: 'http://localhost:9933',
423
- });
261
+ const memo = await EncryptedMemo.encrypt(payload, recipientViewingPk, commitment);
262
+ const decrypted = await EncryptedMemo.decrypt(memoBytes, viewingSecretKey, commitment);
424
263
 
425
- const chainInfo = await client.substrate.getChainInfo();
426
- const root = await client.rpcV2.privacy.getMerkleRoot();
427
- const tree = await client.shieldedPool.merkle.getTreeInfo();
264
+ // Validation is called automatically inside ShieldedPoolModule and ShieldedPoolPrecompile.
265
+ // It can also be called explicitly:
266
+ EncryptedMemo.validate(memoBytes, 'my-context'); // throws if length !== 168
428
267
  ```
429
268
 
430
- `OrbinumClient` wires together the current SDK surface under a single interface.
431
-
432
- - `client.substrate`: Substrate WebSocket RPC and transaction helpers
433
- - `client.evm`: EVM JSON-RPC client, or `null` if `evmRpc` was not configured
434
- - `client.rpcV2`: typed Orbinum `rpc-v2` namespaces
435
- - `client.shieldedPool`: shielded-pool extrinsics, note helpers, and Merkle queries
436
- - `client.accountMapping`: alias, chain-link, metadata, and marketplace operations
437
- - `client.precompiles`: EVM precompile wrappers, or `null` if `evmRpc` was not configured
438
-
439
- Each module can also be instantiated independently.
440
-
441
- ## rpc-v2
442
-
443
- Orbinum's `rpc-v2` is organized by namespace.
444
-
445
- - Standard Frontier namespaces: `eth_*`, `net_*`, `web3_*`, `txpool_*`, `debug_*`
446
- - Orbinum-specific namespace: `privacy_*`
447
-
448
- In the SDK, `rpc-v2` is modeled as a dedicated top-level module, exposed through `client.rpcV2`. Each namespace lives under `src/rpc-v2/` and is grouped there instead of being mixed into legacy protocol modules.
449
-
450
- Current typed coverage:
451
-
452
- - `client.rpcV2.privacy.getMerkleRoot()`
453
- - `client.rpcV2.privacy.getMerkleProof(leafIndex)`
454
- - `client.rpcV2.privacy.getNullifierStatus(nullifier)`
455
- - `client.rpcV2.privacy.getPoolStats()`
456
-
457
- Example:
269
+ ### Coin Selection
458
270
 
459
271
  ```ts
460
- const root = await client.rpcV2.privacy.getMerkleRoot();
272
+ import { selectNotes, buildDummyTransferInput } from '@orbinum/sdk';
461
273
 
462
- const proof = await client.rpcV2.privacy.getMerkleProof(12);
463
- // {
464
- // path: string[];
465
- // leafIndex: number;
466
- // treeDepth: number;
467
- // }
274
+ // Select up to 2 unspent notes covering `needed` planck.
275
+ // Priority: single note first, then smallest qualifying pair.
276
+ const selected = selectNotes(myNotes, needed);
277
+ // [ZkNote, ZkNote | null] | null
468
278
 
469
- const status = await client.rpcV2.privacy.getNullifierStatus('0x...');
470
- // {
471
- // nullifier: string;
472
- // isSpent: boolean;
473
- // }
474
-
475
- const stats = await client.rpcV2.privacy.getPoolStats();
476
- // {
477
- // merkleRoot: string;
478
- // commitmentCount: number;
479
- // totalBalance: string;
480
- // assetBalances: { assetId: number; balance: string }[];
481
- // treeDepth: number;
482
- // }
279
+ // Build a zero-value dummy second input for single-note private transfers.
280
+ // The circuit exempts inputs with value == 0 from Merkle membership and EdDSA checks.
281
+ const dummy = buildDummyTransferInput(assetId);
483
282
  ```
484
283
 
485
- Notes:
284
+ ### Proof Generator
486
285
 
487
- - `rpc-v2` responses from the node use `snake_case`; the SDK maps them to `camelCase`.
488
- - `u128` values are exposed as decimal strings to avoid precision loss in JavaScript.
489
- - Privacy queries belong to `client.rpcV2.privacy.*`; they are no longer described as part of `ShieldedPoolModule`.
490
-
491
- ## Modules
492
-
493
- ### SubstrateClient
494
-
495
- Thin wrapper over [polkadot-api](https://github.com/polkadot-api/polkadot-api) (PAPI) for Substrate WebSocket communication.
286
+ Wrappers around `@orbinum/proof-generator` for generating Groth16 ZK proofs. The `ArtifactProvider` controls where circuit `.wasm` and `.zkey` files are loaded from use `WebArtifactProvider` in browser environments.
496
287
 
497
288
  ```ts
498
- import { SubstrateClient } from '@orbinum/sdk';
499
-
500
- const substrate = await SubstrateClient.connect('ws://localhost:9944');
501
-
502
- const info = await substrate.getChainInfo();
503
- const health = await substrate.getHealth();
504
- const version = await substrate.getNodeVersion();
505
- const genesis = await substrate.getGenesisHash();
289
+ import {
290
+ generateUnshieldProof,
291
+ generateTransferProof,
292
+ generateFeeClaimProof,
293
+ WebArtifactProvider,
294
+ } from '@orbinum/sdk';
506
295
 
507
- // Raw JSON-RPC call
508
- const result = await substrate.request('system_name', []);
296
+ const provider = new WebArtifactProvider('/circuits');
509
297
 
510
- // Build a transaction from pre-encoded SCALE call bytes
511
- const tx = await substrate.txFromCallData(callBytes);
512
- const finalized = await tx.signAndSubmit(signer);
298
+ // Unshield proof
299
+ const result = await generateUnshieldProof({
300
+ merkleRoot, nullifier, amount, assetId, recipient,
301
+ blinding, spendingKey, pathSiblings, leafIndex,
302
+ fee: 0n, // optional gasless fee
303
+ changeValue: 0n, // optional change note value
304
+ }, provider);
305
+ // result.proof, result.publicSignals, result.changeCommitment
513
306
 
514
- // Submit a pre-signed extrinsic
515
- const finalized = await substrate.submit(signedHex);
307
+ // Private transfer proof (exactly 2 inputs and 2 outputs)
308
+ const txProof = await generateTransferProof({
309
+ merkleRoot,
310
+ inputs: [inputNote1, inputNote2], // use buildDummyTransferInput for single-note transfers
311
+ outputs: [outputNote1, outputNote2],
312
+ fee: 0n,
313
+ }, provider);
516
314
 
517
- // Submit and observe lifecycle events
518
- substrate.submitAndWatch(signedHex).subscribe(event => console.log(event));
315
+ // Fee-claim proof
316
+ const feeProof = await generateFeeClaimProof({
317
+ amount, assetId, ownerPubkey, blinding, commitment,
318
+ }, provider);
319
+ // feeProof.proof (128-byte 0x-prefixed hex), feeProof.publicSignals (76-byte number[])
519
320
  ```
520
321
 
521
- ### EvmClient
322
+ ### RelayerStatusModule
522
323
 
523
- Stateless HTTP client following the Ethereum JSON-RPC specification.
324
+ Typed client for `relayer_*` JSON-RPC endpoints.
524
325
 
525
326
  ```ts
526
- import { EvmClient } from '@orbinum/sdk';
327
+ import { RelayerStatusModule } from '@orbinum/sdk';
527
328
 
528
- const evm = new EvmClient('http://localhost:9933');
529
-
530
- const balance = await evm.getBalance('0xYourAddress');
531
- const chainId = await evm.getChainId();
532
- const txHash = await evm.sendRawTransaction(signedHex);
329
+ const relayer = new RelayerStatusModule(substrate);
533
330
 
534
- // Batch multiple calls in a single HTTP request
535
- const [balance, nonce] = await evm.batchRequest([
536
- { method: 'eth_getBalance', params: ['0xAddr', 'latest'] },
537
- { method: 'eth_getTransactionCount', params: ['0xAddr', 'latest'] },
538
- ]);
331
+ const isRelayer = await relayer.isRelayer(ss58Address);
332
+ const pending = await relayer.pendingFees(ss58Address, assetId); // bigint
333
+ const evmAddress = await relayer.registeredEvmAddress(ss58Address); // string | null
334
+ const info = await relayer.getRelayerInfo(ss58Address);
335
+ // { isRelayer: boolean; evmAddress: string | null }
539
336
  ```
540
337
 
541
- ### RpcV2Module
338
+ ### Vault
542
339
 
543
- Top-level typed entry point for Orbinum `rpc-v2` namespaces.
340
+ AES-GCM-256 encrypted local note storage. Works in browser (WebCrypto) and Node.js 18+.
544
341
 
545
- ```ts
546
- const root = await client.rpcV2.privacy.getMerkleRoot();
547
- const proof = await client.rpcV2.privacy.getMerkleProof(4);
548
- const nullifier = await client.rpcV2.privacy.getNullifierStatus('0xNullifier');
549
- const stats = await client.rpcV2.privacy.getPoolStats();
550
- ```
551
-
552
- ### ShieldedPoolModule
553
-
554
- High-level interface for shielded pool extrinsics and note workflows. Requires a loaded `PrivacyKeyManager` and ZK proofs for unshield and private transfer.
342
+ Key derivation uses HKDF-SHA-256 over 32-byte master material derived **before** modular reduction, so the vault key remains stable across circuit field changes.
555
343
 
556
344
  ```ts
557
- // Shield (deposit) tokens into the shielded pool
558
- const { txResult, note } = await client.shieldedPool.buildAndShield({
559
- amount: 1_000_000n,
560
- assetId: 1,
561
- tokenAddress: '0xTokenAddress',
562
- }, signer);
345
+ import {
346
+ deriveVaultKey,
347
+ encryptNote,
348
+ decryptNoteRecord,
349
+ applyNoteStatus,
350
+ VaultLockedError,
351
+ } from '@orbinum/sdk';
563
352
 
564
- const treeInfo = await client.shieldedPool.merkle.getTreeInfo();
353
+ // Derive the vault encryption key from master key bytes (pre-modulus)
354
+ const vaultKey = await deriveVaultKey(masterBytes);
565
355
 
566
- // Private transfer between notes
567
- await client.shieldedPool.privateTransfer({ ...params }, signer);
356
+ // Encrypt a ZkNote into an EncryptedNoteRecord
357
+ const record = await encryptNote(vaultKey, zkNote);
358
+ // { commitmentHex, iv, ciphertext, nullifierHex, assetId, spent?, updatedAt }
568
359
 
569
- // Unshield (withdraw) tokens to a public address
570
- await client.shieldedPool.unshield({ ...params }, signer);
360
+ // Decrypt
361
+ const note = await decryptNoteRecord(vaultKey, record); // ZkNote
571
362
 
363
+ // Update spent status without re-encrypting the full ciphertext
364
+ const updated = applyNoteStatus(record, { spent: true, spentAt: Date.now() });
572
365
  ```
573
366
 
574
- ### PrivacyModule (`rpc-v2`)
575
-
576
- Typed wrapper for Orbinum `privacy_*` endpoints from `rpc-v2`.
577
-
578
- ```ts
579
- import { PrivacyModule } from '@orbinum/sdk';
580
-
581
- const privacy = new PrivacyModule(client.substrate);
582
-
583
- const merkleRoot = await privacy.getMerkleRoot();
584
- const merkleProof = await privacy.getMerkleProof(4);
585
- const nullifier = await privacy.getNullifierStatus('0xNullifier');
586
- const stats = await privacy.getPoolStats();
587
- ```
367
+ The `nullifierHex` and `assetId` fields are stored unencrypted so the host application can perform spent-checks and asset filtering without unlocking the vault.
588
368
 
589
369
  ### Privacy Keys
590
370
 
@@ -599,20 +379,14 @@ import {
599
379
  deriveViewingKey,
600
380
  } from '@orbinum/sdk';
601
381
 
602
- // 1. Get the message the user must sign
603
- const message = deriveSpendingKeyMessage(chainId, evmAddress);
604
-
605
- // 2. Derive the spending key from the wallet signature
382
+ const message = deriveSpendingKeyMessage(chainId, evmAddress);
606
383
  const spendingKey = deriveSpendingKeyFromSignature(sigHex, chainId, evmAddress);
607
384
 
608
- // 3. Load into the key manager
609
385
  const keyManager = new PrivacyKeyManager();
610
386
  keyManager.load(spendingKey);
611
387
 
612
- // The manager derives the viewing key (ChaCha20 memo decryption)
613
- // and owner public key (BabyJubJub, used in note commitments)
614
388
  const viewingKey = deriveViewingKey(spendingKey);
615
- const ownerPk = deriveOwnerPk(spendingKey);
389
+ const ownerPk = deriveOwnerPk(spendingKey);
616
390
  ```
617
391
 
618
392
  ### AccountMappingModule
@@ -620,50 +394,67 @@ const ownerPk = deriveOwnerPk(spendingKey);
620
394
  Manage on-chain identity by linking Substrate and EVM accounts, registering aliases, and interacting with the alias marketplace.
621
395
 
622
396
  ```ts
623
- // Register an alias
624
397
  await client.accountMapping.registerAlias({ alias: 'myalias' }, signer);
625
-
626
- // Link an EVM address to a Substrate account
627
398
  await client.accountMapping.addChainLink({ chainId: 1, address: '0xEvmAddr' }, signer);
628
399
 
629
- // Query
630
- const alias = await client.accountMapping.getAliasOf(substrateHex);
400
+ const alias = await client.accountMapping.getAliasOf(substrateHex);
631
401
  const linked = await client.accountMapping.getChainLinks(substrateHex);
632
402
  ```
633
403
 
634
404
  ### IndexerClient
635
405
 
636
- HTTP client for the Orbinum indexer REST API. The indexer runs as a separate service indexed from node events.
406
+ HTTP client for the Orbinum indexer REST API. All list endpoints are paginated.
637
407
 
638
408
  ```ts
639
409
  import { IndexerClient } from '@orbinum/sdk';
640
410
 
641
- const indexer = new IndexerClient({
642
- baseUrl: 'https://indexer.orbinum.io',
643
- });
411
+ const indexer = new IndexerClient({ baseUrl: 'https://indexer.orbinum.io' });
644
412
 
413
+ const blocks = await indexer.getBlocks({ page: 1, limit: 20 });
414
+ const extrinsics = await indexer.getExtrinsics({ address: '5F...' });
645
415
  const commitments = await indexer.getCommitments({ page: 1, limit: 20 });
646
- const status = await indexer.getNullifierStatus('0xNullifier');
416
+ const nullifier = await indexer.getNullifierStatus('0xNullifier');
647
417
  const roots = await indexer.getMerkleRoots({ limit: 10 });
648
- const extrinsics = await indexer.getAddressExtrinsics('5F...');
649
418
  const evmTxs = await indexer.getEvmTransactions({ address: '0xabc...' });
419
+ const stats = await indexer.getStats();
650
420
  ```
651
421
 
652
422
  ### Precompiles
653
423
 
654
- Typed wrappers for Orbinum's EVM precompile contracts, callable from any EVM wallet or signer.
424
+ Typed wrappers for Orbinum's EVM precompile contracts.
655
425
 
656
426
  | Precompile | Address | Description |
657
427
  |---|---|---|
658
428
  | `ShieldedPoolPrecompile` | `0x0801` | Shield, unshield, and private transfer from EVM |
659
429
  | `AccountMappingPrecompile` | `0x0800` | Identity and alias operations from EVM |
660
- | `CryptoPrecompiles` | `0x0400–0x0403` | Frontier crypto utilities (SHA3, EC recover, Curve25519) |
430
+ | `CryptoPrecompiles` | `0x0400–0x0403` | Frontier crypto utilities |
661
431
 
662
432
  ```ts
663
- import { ShieldedPoolPrecompile } from '@orbinum/sdk';
433
+ import { ShieldedPoolPrecompile, decodePrecompileCalldata } from '@orbinum/sdk';
664
434
 
665
435
  const precompile = new ShieldedPoolPrecompile(evmClient);
666
436
  await precompile.shield(amount, tokenAddress, commitment, walletSigner);
437
+
438
+ // Decode raw EVM calldata for a known precompile
439
+ const decoded = decodePrecompileCalldata('0x0800', calldata);
440
+ ```
441
+
442
+ ### Extrinsic & Event Decoders
443
+
444
+ ```ts
445
+ import { mapExtrinsicArgs, mapZkEventData } from '@orbinum/sdk';
446
+
447
+ const decoded = mapExtrinsicArgs('shieldedPool', 'shield', rawArgs);
448
+ const eventData = mapZkEventData('ProofVerified', rawEventData);
449
+ ```
450
+
451
+ ### Substrate SCALE Primitives
452
+
453
+ SCALE codec primitives from `@polkadot-api/substrate-bindings` are re-exported directly. There is no need to install that package separately.
454
+
455
+ ```ts
456
+ import { Blake2256, AccountId, u128, u64, Storage, Keccak256 } from '@orbinum/sdk';
457
+ import { base58, getSs58AddressInfo } from '@orbinum/sdk';
667
458
  ```
668
459
 
669
460
  ## Key Derivation Chain
@@ -672,76 +463,59 @@ await precompile.shield(amount, tokenAddress, commitment, walletSigner);
672
463
  wallet.sign(message)
673
464
  |
674
465
  v
675
- deriveSpendingKeyFromSignature() -- HKDF-SHA256 over signature bytes, mod BN254_R
466
+ deriveSpendingKeyFromSignature() HKDF-SHA256 over signature bytes, mod BN254_R
467
+ |
468
+ +-- deriveViewingKey() HKDF-SHA256 → 32-byte ChaCha20 symmetric key
469
+ | Used to encrypt/decrypt note memos (EncryptedMemo)
676
470
  |
677
- +-- deriveViewingKey() -- HKDF-SHA256 -> 32-byte ChaCha20 symmetric key
678
- | (used to encrypt/decrypt note memos)
471
+ +-- deriveOwnerPk() BabyJubJub scalar multiplication Ax
472
+ | Embedded in note commitments: Poseidon4(v, a, ownerPk, b)
679
473
  |
680
- +-- deriveOwnerPk() -- BabyJubJub scalar multiplication -> Ax
681
- (embedded in shielded note commitments)
474
+ +-- deriveVaultKey() HKDF-SHA256 (pre-modulus bytes) AES-GCM-256 key
475
+ Stable across circuit field changes
682
476
  ```
683
477
 
478
+ ### Stealth Addresses
479
+
480
+ When a sender builds a note for a recipient, a per-note stealth `ownerPk` is derived:
481
+
482
+ ```
483
+ ephSk ← random scalar
484
+ sharedSecret ← ECDH(ephSk, recipientViewingPk)
485
+ stealthScalar ← HKDF-SHA256(sharedSecret, salt=ownerPk_LE, info="orbinum-stealth-v1") % suborder
486
+ stealthOwnerPk ← stealthScalar × Base8 + ownerPkPoint
487
+ ```
488
+
489
+ The recipient recovers the stealth spending key:
490
+
491
+ ```
492
+ stealthSk ← (stealthScalar + spendingKey) % BABYJUB_SUBORDER
493
+ ```
494
+
495
+ Each received note has a unique `stealthOwnerPk`, making transfers unlinkable even when the same privacy address is reused. The ZK circuit validates ownership without modification because `BabyPbk(stealthSk).Ax == stealthOwnerPk`.
496
+
684
497
  ## Development
685
498
 
686
- ### Typing Layout
499
+ ### Type Layout
687
500
 
688
501
  The SDK organizes types by feature ownership.
689
502
 
690
503
  - `types/index.ts`: public shared types of the feature
691
- - `types/pallet-events.ts`: event payloads and discriminated unions for that pallet
692
- - `types/pallet-extrinsics.ts`: extrinsic argument types and call unions for that pallet
504
+ - `types/pallet-events.ts`: event payloads and discriminated unions
505
+ - `types/pallet-extrinsics.ts`: extrinsic argument types and call unions
693
506
  - `types/raw.ts`: internal node or RPC response shapes used only for transport mapping
694
507
 
695
- Examples:
696
-
697
- - `src/shielded-pool/types/index.ts`
698
- - `src/shielded-pool/types/pallet-events.ts`
699
- - `src/shielded-pool/types/pallet-extrinsics.ts`
700
- - `src/rpc-v2/types/raw.ts`
701
- - `src/indexer/types/index.ts`
702
- - `src/precompiles/types/index.ts`
703
-
704
508
  Rules:
705
-
706
- 1. If a type is part of the public API of a feature, keep it in that feature's `types/index.ts`.
707
- 2. If a type only mirrors a transport payload from RPC, keep it in `types/raw.ts`.
509
+ 1. Public types belong in the feature's `types/index.ts`.
510
+ 2. Transport-only types belong in `types/raw.ts`.
708
511
  3. Do not reintroduce a global `src/types.ts` or `src/types/` directory.
709
- 4. Export public types from `src/index.ts`, but keep ownership at the feature level.
512
+ 4. Export public types from `src/index.ts`, keeping ownership at the feature level.
710
513
 
711
514
  ### Extending `rpc-v2`
712
515
 
713
- The intended extension pattern is namespace-oriented and centralized under `RpcV2Module`.
714
-
715
- For example, to add `net_*` support:
716
-
717
- 1. Create `src/rpc-v2/NetModule.ts`
718
- 2. Define raw response types that match the node's JSON exactly
719
- 3. Map raw `snake_case` or hex-shaped values into stable SDK-facing TypeScript types
720
- 4. Export the module from `src/rpc-v2/index.ts`
721
- 5. Attach it in `RpcV2Module`, for example as `client.rpcV2.net`
722
- 6. Add unit tests that verify RPC method names, params, and response mapping
516
+ Add a new namespace by creating `src/rpc-v2/NetModule.ts`, defining raw response types, mapping them to SDK-facing types, and attaching the module in `RpcV2Module`. Add unit tests verifying RPC method names, params, and response mapping.
723
517
 
724
- Minimal shape example:
725
-
726
- ```ts
727
- export class NetModule {
728
- constructor(private readonly substrate: SubstrateClient) {}
729
-
730
- async version(): Promise<string> {
731
- return this.substrate.request<string>('net_version', []);
732
- }
733
-
734
- async peerCount(): Promise<string> {
735
- return this.substrate.request<string>('net_peerCount', []);
736
- }
737
-
738
- async listening(): Promise<boolean> {
739
- return this.substrate.request<boolean>('net_listening', []);
740
- }
741
- }
742
- ```
743
-
744
- The same approach applies to `web3_*`, `txpool_*`, `eth_*`, and any future Orbinum-specific namespaces.
518
+ ### Commands
745
519
 
746
520
  ```bash
747
521
  pnpm install