@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 +217 -443
- package/dist/index.d.mts +2005 -845
- package/dist/index.d.ts +2005 -845
- package/dist/index.js +1729 -426
- package/dist/index.mjs +1713 -425
- package/package.json +22 -20
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
|
|
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
|
|
33
|
-
const events
|
|
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
|
|
135
|
-
const finalized
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
204
|
-
await client.shieldedPool.privateTransfer({ ...params }, signer);
|
|
181
|
+
const pool = new ShieldedPoolModule(substrate);
|
|
205
182
|
|
|
206
|
-
//
|
|
207
|
-
await
|
|
208
|
-
```
|
|
183
|
+
// Deposit tokens into the shielded pool (signed)
|
|
184
|
+
await pool.shield(params, signer);
|
|
209
185
|
|
|
210
|
-
|
|
186
|
+
// Withdraw tokens via ZK proof (unsigned, gasless)
|
|
187
|
+
await pool.unshield(params);
|
|
211
188
|
|
|
212
|
-
|
|
189
|
+
// Private transfer between shielded addresses (unsigned, gasless)
|
|
190
|
+
await pool.privateTransfer(params);
|
|
213
191
|
|
|
214
|
-
|
|
215
|
-
|
|
192
|
+
// Batch shield
|
|
193
|
+
await pool.shieldBatch(params, signer);
|
|
216
194
|
|
|
217
|
-
|
|
195
|
+
// Claim accumulated relayer fees
|
|
196
|
+
await pool.claimShieldedFees(params, signer);
|
|
218
197
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
245
|
-
const ownerPk = deriveOwnerPk(spendingKey);
|
|
208
|
+
Hash scheme:
|
|
246
209
|
```
|
|
247
|
-
|
|
248
|
-
|
|
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 {
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
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
|
-
|
|
230
|
+
### NoteDecryptor
|
|
281
231
|
|
|
282
|
-
|
|
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 {
|
|
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
|
-
|
|
303
|
-
|
|
237
|
+
// Returns a ZkNote on success, null on key mismatch or commitment failure
|
|
238
|
+
const note = tryDecryptNote(commitment, viewingSecretKey, spendingKey, ownOwnerPk);
|
|
304
239
|
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
243
|
+
// Compute nullifier directly
|
|
244
|
+
const nullifier = computeNullifier(commitmentBigint, spendingKey);
|
|
309
245
|
```
|
|
310
246
|
|
|
311
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
258
|
+
import { EncryptedMemo, ENCRYPTED_MEMO_SIZE } from '@orbinum/sdk';
|
|
259
|
+
// ENCRYPTED_MEMO_SIZE === 168
|
|
419
260
|
|
|
420
|
-
const
|
|
421
|
-
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
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
|
-
|
|
272
|
+
import { selectNotes, buildDummyTransferInput } from '@orbinum/sdk';
|
|
461
273
|
|
|
462
|
-
|
|
463
|
-
//
|
|
464
|
-
|
|
465
|
-
//
|
|
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
|
-
|
|
470
|
-
//
|
|
471
|
-
|
|
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
|
-
|
|
284
|
+
### Proof Generator
|
|
486
285
|
|
|
487
|
-
- `
|
|
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 {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
-
|
|
508
|
-
const result = await substrate.request('system_name', []);
|
|
296
|
+
const provider = new WebArtifactProvider('/circuits');
|
|
509
297
|
|
|
510
|
-
//
|
|
511
|
-
const
|
|
512
|
-
|
|
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
|
-
//
|
|
515
|
-
const
|
|
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
|
-
//
|
|
518
|
-
|
|
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
|
-
###
|
|
322
|
+
### RelayerStatusModule
|
|
522
323
|
|
|
523
|
-
|
|
324
|
+
Typed client for `relayer_*` JSON-RPC endpoints.
|
|
524
325
|
|
|
525
326
|
```ts
|
|
526
|
-
import {
|
|
327
|
+
import { RelayerStatusModule } from '@orbinum/sdk';
|
|
527
328
|
|
|
528
|
-
const
|
|
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
|
-
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
###
|
|
338
|
+
### Vault
|
|
542
339
|
|
|
543
|
-
|
|
340
|
+
AES-GCM-256 encrypted local note storage. Works in browser (WebCrypto) and Node.js 18+.
|
|
544
341
|
|
|
545
|
-
|
|
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
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
345
|
+
import {
|
|
346
|
+
deriveVaultKey,
|
|
347
|
+
encryptNote,
|
|
348
|
+
decryptNoteRecord,
|
|
349
|
+
applyNoteStatus,
|
|
350
|
+
VaultLockedError,
|
|
351
|
+
} from '@orbinum/sdk';
|
|
563
352
|
|
|
564
|
-
|
|
353
|
+
// Derive the vault encryption key from master key bytes (pre-modulus)
|
|
354
|
+
const vaultKey = await deriveVaultKey(masterBytes);
|
|
565
355
|
|
|
566
|
-
//
|
|
567
|
-
await
|
|
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
|
-
//
|
|
570
|
-
await
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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()
|
|
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
|
-
+--
|
|
678
|
-
|
|
|
471
|
+
+-- deriveOwnerPk() BabyJubJub scalar multiplication → Ax
|
|
472
|
+
| Embedded in note commitments: Poseidon4(v, a, ownerPk, b)
|
|
679
473
|
|
|
|
680
|
-
+--
|
|
681
|
-
|
|
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
|
-
###
|
|
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
|
|
692
|
-
- `types/pallet-extrinsics.ts`: extrinsic argument types and call unions
|
|
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
|
-
|
|
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`,
|
|
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
|
-
|
|
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
|
-
|
|
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
|