@orbinum/sdk 0.4.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +15 -724
- package/dist/index.d.mts +1310 -550
- package/dist/index.d.ts +1310 -550
- package/dist/index.js +1593 -496
- package/dist/index.mjs +1580 -496
- package/package.json +23 -21
package/dist/index.mjs
CHANGED
|
@@ -41,6 +41,7 @@ var SubstrateClient = class _SubstrateClient {
|
|
|
41
41
|
constructor(_papi) {
|
|
42
42
|
this._papi = _papi;
|
|
43
43
|
}
|
|
44
|
+
_papi;
|
|
44
45
|
_dynamicBuilder = null;
|
|
45
46
|
_extDecoder = null;
|
|
46
47
|
/**
|
|
@@ -240,6 +241,14 @@ var SubstrateClient = class _SubstrateClient {
|
|
|
240
241
|
submitAndWatch(signedHex) {
|
|
241
242
|
return this._papi.submitAndWatch(signedHex);
|
|
242
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* Submits a bare (unsigned) extrinsic hex and waits for finalization.
|
|
246
|
+
* Used for gasless private_transfer and unshield transactions.
|
|
247
|
+
* The bare tx hex is produced by `tx.getBareTx()` from polkadot-api.
|
|
248
|
+
*/
|
|
249
|
+
async submitUnsignedAndWatch(bareTxHex) {
|
|
250
|
+
return this._papi.submit(bareTxHex);
|
|
251
|
+
}
|
|
243
252
|
/**
|
|
244
253
|
* Convenience: wrap raw call bytes and sign+submit in one step.
|
|
245
254
|
*/
|
|
@@ -291,15 +300,14 @@ var SubstrateClient = class _SubstrateClient {
|
|
|
291
300
|
}
|
|
292
301
|
static _buildDataProxy(value) {
|
|
293
302
|
const formatValue = (v) => {
|
|
294
|
-
if (v instanceof Uint8Array) return
|
|
303
|
+
if (v instanceof Uint8Array) return toHex(v);
|
|
295
304
|
if (typeof v === "bigint") return v.toString();
|
|
296
305
|
return String(v);
|
|
297
306
|
};
|
|
298
307
|
const jsonifyValue = (v) => {
|
|
299
308
|
if (v === null || v === void 0) return v;
|
|
300
309
|
if (typeof v === "bigint") return v.toString();
|
|
301
|
-
if (v instanceof Uint8Array)
|
|
302
|
-
return `0x${Array.from(v).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
310
|
+
if (v instanceof Uint8Array) return toHex(v);
|
|
303
311
|
if (Array.isArray(v)) return v.map(jsonifyValue);
|
|
304
312
|
if (typeof v === "object") {
|
|
305
313
|
const obj = v;
|
|
@@ -403,11 +411,14 @@ var SubstrateClient = class _SubstrateClient {
|
|
|
403
411
|
|
|
404
412
|
// src/evm/EvmClient.ts
|
|
405
413
|
var EvmClient = class {
|
|
414
|
+
/** @param rpcUrl - HTTP URL of the EVM JSON-RPC endpoint (e.g. `"http://localhost:9933"`). */
|
|
406
415
|
constructor(rpcUrl) {
|
|
407
416
|
this.rpcUrl = rpcUrl;
|
|
408
417
|
}
|
|
418
|
+
rpcUrl;
|
|
409
419
|
/**
|
|
410
|
-
* Performs a single JSON-RPC call.
|
|
420
|
+
* Performs a single JSON-RPC call and returns the typed result.
|
|
421
|
+
* Throws on HTTP errors, RPC-level errors, or a `null` result.
|
|
411
422
|
*/
|
|
412
423
|
async request(method, params = []) {
|
|
413
424
|
const res = await fetch(this.rpcUrl, {
|
|
@@ -427,6 +438,7 @@ var EvmClient = class {
|
|
|
427
438
|
}
|
|
428
439
|
/**
|
|
429
440
|
* Performs multiple JSON-RPC calls in a single HTTP request (batch).
|
|
441
|
+
* Results are returned in the same order as `calls`, as a typed tuple.
|
|
430
442
|
*/
|
|
431
443
|
async batchRequest(calls) {
|
|
432
444
|
const body = calls.map((c, i) => ({
|
|
@@ -471,30 +483,22 @@ var EvmClient = class {
|
|
|
471
483
|
const hex = await this.request("eth_gasPrice", []);
|
|
472
484
|
return hexToBigint(hex);
|
|
473
485
|
}
|
|
474
|
-
/**
|
|
475
|
-
* Submits a signed raw transaction. Returns the transaction hash.
|
|
476
|
-
*/
|
|
486
|
+
/** Submits a signed raw transaction. Returns the transaction hash. */
|
|
477
487
|
async sendRawTransaction(signedHex) {
|
|
478
488
|
return this.request("eth_sendRawTransaction", [signedHex]);
|
|
479
489
|
}
|
|
480
|
-
/**
|
|
481
|
-
* Executes a read-only call without creating a transaction.
|
|
482
|
-
*/
|
|
490
|
+
/** Executes a read-only call without creating a transaction. Returns the raw ABI-encoded response. */
|
|
483
491
|
async call(to, data, from) {
|
|
484
492
|
const txObj = { to, data };
|
|
485
493
|
if (from) txObj["from"] = from;
|
|
486
494
|
return this.request("eth_call", [txObj, "latest"]);
|
|
487
495
|
}
|
|
488
|
-
/**
|
|
489
|
-
* Estimates the gas for a transaction.
|
|
490
|
-
*/
|
|
496
|
+
/** Estimates the gas required for a transaction. Returns the estimate in wei as a `bigint`. */
|
|
491
497
|
async estimateGas(params) {
|
|
492
498
|
const hex = await this.request("eth_estimateGas", [params]);
|
|
493
499
|
return hexToBigint(hex);
|
|
494
500
|
}
|
|
495
|
-
/**
|
|
496
|
-
* Returns a transaction receipt by hash, or null if not yet mined.
|
|
497
|
-
*/
|
|
501
|
+
/** Returns a transaction receipt by hash, or `null` if the transaction has not been mined yet. */
|
|
498
502
|
async getTransactionReceipt(txHash) {
|
|
499
503
|
const res = await fetch(this.rpcUrl, {
|
|
500
504
|
method: "POST",
|
|
@@ -513,6 +517,56 @@ var EvmClient = class {
|
|
|
513
517
|
}
|
|
514
518
|
return json.result ?? null;
|
|
515
519
|
}
|
|
520
|
+
/**
|
|
521
|
+
* Polls `eth_getTransactionReceipt` until the transaction is included in a block.
|
|
522
|
+
*
|
|
523
|
+
* @param txHash - The transaction hash to wait for.
|
|
524
|
+
* @param intervalMs - Polling interval in milliseconds (default: 500).
|
|
525
|
+
* @param timeoutMs - Maximum time to wait in milliseconds (default: 60_000).
|
|
526
|
+
* @returns The transaction receipt once mined.
|
|
527
|
+
* @throws If the transaction is not mined within `timeoutMs` or if it reverted (`status == 0x0`).
|
|
528
|
+
*/
|
|
529
|
+
async waitForReceipt(txHash, intervalMs = 500, timeoutMs = 6e4) {
|
|
530
|
+
const deadline = Date.now() + timeoutMs;
|
|
531
|
+
while (Date.now() < deadline) {
|
|
532
|
+
const receipt = await this.getTransactionReceipt(txHash);
|
|
533
|
+
if (receipt !== null) {
|
|
534
|
+
if (receipt["status"] === "0x0") {
|
|
535
|
+
let revertDetail = "";
|
|
536
|
+
const nodeReason = receipt["revertReason"];
|
|
537
|
+
if (nodeReason) revertDetail = ` | revertReason: ${nodeReason}`;
|
|
538
|
+
if (!revertDetail) {
|
|
539
|
+
try {
|
|
540
|
+
const blockParam = receipt["blockNumber"] ?? "latest";
|
|
541
|
+
const rawTx = await this.request(
|
|
542
|
+
"eth_getTransactionByHash",
|
|
543
|
+
[txHash]
|
|
544
|
+
).catch(() => null);
|
|
545
|
+
if (rawTx) {
|
|
546
|
+
const calldata = rawTx["input"] ?? rawTx["data"];
|
|
547
|
+
if (calldata) {
|
|
548
|
+
const revertData = await this.request("eth_call", [
|
|
549
|
+
{ from: rawTx["from"], to: rawTx["to"], data: calldata },
|
|
550
|
+
blockParam
|
|
551
|
+
]).catch(
|
|
552
|
+
(err) => err instanceof Error ? err.message : String(err)
|
|
553
|
+
);
|
|
554
|
+
revertDetail = ` | eth_call: ${revertData}`;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
} catch {
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
throw new Error(
|
|
561
|
+
`Transaction reverted on-chain: ${txHash}${revertDetail} | receipt: ${JSON.stringify(receipt)}`
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
return receipt;
|
|
565
|
+
}
|
|
566
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
567
|
+
}
|
|
568
|
+
throw new Error(`Transaction not mined within ${timeoutMs}ms: ${txHash}`);
|
|
569
|
+
}
|
|
516
570
|
};
|
|
517
571
|
|
|
518
572
|
// src/utils/format.ts
|
|
@@ -585,10 +639,13 @@ function formatORB(raw, precision = 6) {
|
|
|
585
639
|
|
|
586
640
|
// src/evm-explorer/EvmExplorer.ts
|
|
587
641
|
var EvmExplorer = class _EvmExplorer {
|
|
642
|
+
/** @param evm - Underlying `EvmClient` used for all RPC calls. */
|
|
588
643
|
constructor(evm) {
|
|
589
644
|
this.evm = evm;
|
|
590
645
|
}
|
|
646
|
+
evm;
|
|
591
647
|
// --- Blocks ---
|
|
648
|
+
/** Returns the `count` most recent blocks in descending order (latest first). */
|
|
592
649
|
async getLatestBlocks(count = 10) {
|
|
593
650
|
const latest = await this.evm.getBlockNumber();
|
|
594
651
|
const nums = Array.from({ length: Math.min(count, latest + 1) }, (_, i) => latest - i);
|
|
@@ -602,10 +659,12 @@ var EvmExplorer = class _EvmExplorer {
|
|
|
602
659
|
);
|
|
603
660
|
return results.filter((b) => b !== null && !!b.hash).map((b) => this.parseBlock(b));
|
|
604
661
|
}
|
|
662
|
+
/** Returns a single block by number or hash, or `null` if not found. */
|
|
605
663
|
async getBlock(hashOrNumber) {
|
|
606
664
|
const b = await this.fetchBlock(hashOrNumber, false);
|
|
607
665
|
return b ? this.parseBlock(b) : null;
|
|
608
666
|
}
|
|
667
|
+
/** Returns all transactions in a block (with receipts), or `[]` if the block is not found. */
|
|
609
668
|
async getBlockTransactions(hashOrNumber) {
|
|
610
669
|
try {
|
|
611
670
|
const b = await this.fetchBlock(hashOrNumber, true);
|
|
@@ -618,6 +677,7 @@ var EvmExplorer = class _EvmExplorer {
|
|
|
618
677
|
}
|
|
619
678
|
}
|
|
620
679
|
// --- Transactions ---
|
|
680
|
+
/** Returns a single transaction with its receipt, or `null` if not found. */
|
|
621
681
|
async getTransaction(hash) {
|
|
622
682
|
try {
|
|
623
683
|
const [tx, receipt] = await Promise.all([
|
|
@@ -630,6 +690,10 @@ var EvmExplorer = class _EvmExplorer {
|
|
|
630
690
|
return null;
|
|
631
691
|
}
|
|
632
692
|
}
|
|
693
|
+
/**
|
|
694
|
+
* Returns lightweight summaries of all transactions sent from or to `address`
|
|
695
|
+
* within the last `maxBlocks` blocks, sorted by block number descending.
|
|
696
|
+
*/
|
|
633
697
|
async getTransactionsByAddress(address, maxBlocks = 300) {
|
|
634
698
|
const addr = address.toLowerCase();
|
|
635
699
|
const latest = await this.evm.getBlockNumber();
|
|
@@ -681,6 +745,10 @@ var EvmExplorer = class _EvmExplorer {
|
|
|
681
745
|
return results;
|
|
682
746
|
}
|
|
683
747
|
// --- Address ---
|
|
748
|
+
/**
|
|
749
|
+
* Returns aggregated on-chain data for an EVM address: balance, nonce,
|
|
750
|
+
* bytecode (truncated), and up to 50 recent logs from the last 5 000 blocks.
|
|
751
|
+
*/
|
|
684
752
|
async getAddressInfo(address) {
|
|
685
753
|
const latest = await this.evm.getBlockNumber().catch(() => 0);
|
|
686
754
|
const fromBlock = `0x${Math.max(0, latest - 5e3).toString(16)}`;
|
|
@@ -711,6 +779,7 @@ var EvmExplorer = class _EvmExplorer {
|
|
|
711
779
|
recentLogs
|
|
712
780
|
};
|
|
713
781
|
}
|
|
782
|
+
/** Returns the native token balance of `address`, formatted as a decimal string (no symbol). */
|
|
714
783
|
async getBalance(address) {
|
|
715
784
|
try {
|
|
716
785
|
const val = await this.evm.getBalance(address);
|
|
@@ -719,6 +788,7 @@ var EvmExplorer = class _EvmExplorer {
|
|
|
719
788
|
return "0";
|
|
720
789
|
}
|
|
721
790
|
}
|
|
791
|
+
/** Returns the current transaction count (nonce) for `address`, or `0` on error. */
|
|
722
792
|
async getNonce(address) {
|
|
723
793
|
try {
|
|
724
794
|
return await this.evm.getTransactionCount(address);
|
|
@@ -726,6 +796,7 @@ var EvmExplorer = class _EvmExplorer {
|
|
|
726
796
|
return 0;
|
|
727
797
|
}
|
|
728
798
|
}
|
|
799
|
+
/** Returns `true` when `address` has non-empty deployed bytecode. */
|
|
729
800
|
async getIsContract(address) {
|
|
730
801
|
try {
|
|
731
802
|
const code = await this.evm.request("eth_getCode", [address, "latest"]);
|
|
@@ -735,6 +806,10 @@ var EvmExplorer = class _EvmExplorer {
|
|
|
735
806
|
}
|
|
736
807
|
}
|
|
737
808
|
// --- Tokens ---
|
|
809
|
+
/**
|
|
810
|
+
* Fetches ERC-20 metadata for a token contract via ABI calls.
|
|
811
|
+
* Returns `null` when the address does not look like an ERC-20 token.
|
|
812
|
+
*/
|
|
738
813
|
async getTokenInfo(address) {
|
|
739
814
|
const addr = address.toLowerCase();
|
|
740
815
|
const [name, symbol, decimals, totalSupply] = await this.evm.batchRequest([
|
|
@@ -754,6 +829,10 @@ var EvmExplorer = class _EvmExplorer {
|
|
|
754
829
|
isErc20
|
|
755
830
|
};
|
|
756
831
|
}
|
|
832
|
+
/**
|
|
833
|
+
* Returns ERC-20 `Transfer` events for `address` from the last 5 000 blocks.
|
|
834
|
+
* When `holderAddress` is provided, restricts results to transfers sent or received by that address.
|
|
835
|
+
*/
|
|
757
836
|
async getTokenTransfers(address, holderAddress) {
|
|
758
837
|
const TRANSFER = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
759
838
|
const latest = await this.evm.getBlockNumber().catch(() => 0);
|
|
@@ -781,12 +860,14 @@ var EvmExplorer = class _EvmExplorer {
|
|
|
781
860
|
logIndex: hexToNumber(l.logIndex)
|
|
782
861
|
}));
|
|
783
862
|
}
|
|
863
|
+
/** Returns the raw ERC-20 balance of `holderAddress` for the token at `tokenAddress` (0x-prefixed hex). */
|
|
784
864
|
async getTokenBalance(tokenAddress, holderAddress) {
|
|
785
865
|
const padded = holderAddress.replace(/^0x/, "").toLowerCase().padStart(64, "0");
|
|
786
866
|
const result = await this.ethCall(tokenAddress, `0x70a08231${padded}`);
|
|
787
867
|
return result ?? "0x0";
|
|
788
868
|
}
|
|
789
869
|
// --- Private: parsers ---
|
|
870
|
+
/** Maps a raw RPC block object to the public `EvmBlock` shape. */
|
|
790
871
|
parseBlock(b) {
|
|
791
872
|
return {
|
|
792
873
|
hash: b.hash,
|
|
@@ -799,6 +880,7 @@ var EvmExplorer = class _EvmExplorer {
|
|
|
799
880
|
parentHash: b.parentHash
|
|
800
881
|
};
|
|
801
882
|
}
|
|
883
|
+
/** Maps a raw RPC transaction + optional receipt to the public `EvmTransaction` shape. */
|
|
802
884
|
parseTx(tx, receipt) {
|
|
803
885
|
const parsed = {
|
|
804
886
|
hash: tx.hash,
|
|
@@ -818,6 +900,10 @@ var EvmExplorer = class _EvmExplorer {
|
|
|
818
900
|
return parsed;
|
|
819
901
|
}
|
|
820
902
|
// --- Private: fetch helpers ---
|
|
903
|
+
/**
|
|
904
|
+
* Fetches a raw block by number or hash. Accepts a plain integer, a decimal string,
|
|
905
|
+
* or a 0x-prefixed hash. Returns `null` on any RPC error.
|
|
906
|
+
*/
|
|
821
907
|
async fetchBlock(hashOrNumber, withTxObjects) {
|
|
822
908
|
try {
|
|
823
909
|
if (typeof hashOrNumber === "number" || /^\d+$/.test(String(hashOrNumber))) {
|
|
@@ -835,6 +921,7 @@ var EvmExplorer = class _EvmExplorer {
|
|
|
835
921
|
return null;
|
|
836
922
|
}
|
|
837
923
|
}
|
|
924
|
+
/** Executes a read-only `eth_call` and returns the raw hex result, or `null` on error. */
|
|
838
925
|
async ethCall(to, data) {
|
|
839
926
|
try {
|
|
840
927
|
return await this.evm.call(to, data);
|
|
@@ -843,6 +930,7 @@ var EvmExplorer = class _EvmExplorer {
|
|
|
843
930
|
}
|
|
844
931
|
}
|
|
845
932
|
// --- Private static: ABI decoders ---
|
|
933
|
+
/** Decodes an ABI-encoded `string` return value from a raw 0x-prefixed hex string. */
|
|
846
934
|
static decodeAbiString(hex) {
|
|
847
935
|
if (!hex || hex === "0x") return "";
|
|
848
936
|
const data = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
@@ -857,11 +945,13 @@ var EvmExplorer = class _EvmExplorer {
|
|
|
857
945
|
return "";
|
|
858
946
|
}
|
|
859
947
|
}
|
|
948
|
+
/** Decodes an ABI-encoded `uint256` return value to a `bigint`. */
|
|
860
949
|
static decodeAbiUint(hex) {
|
|
861
950
|
if (!hex || hex === "0x") return 0n;
|
|
862
951
|
const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
863
952
|
return BigInt(`0x${clean || "0"}`);
|
|
864
953
|
}
|
|
954
|
+
/** Converts a 0x-prefixed hex number to its decimal string representation. Returns `'0'` on parse error. */
|
|
865
955
|
static hexToDecimalStr(hex) {
|
|
866
956
|
try {
|
|
867
957
|
return BigInt(hex).toString();
|
|
@@ -896,6 +986,24 @@ var IndexerClient = class {
|
|
|
896
986
|
}
|
|
897
987
|
return res.json();
|
|
898
988
|
}
|
|
989
|
+
async post(path, body) {
|
|
990
|
+
const controller = new AbortController();
|
|
991
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
992
|
+
try {
|
|
993
|
+
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
994
|
+
method: "POST",
|
|
995
|
+
headers: { "Content-Type": "application/json" },
|
|
996
|
+
body: JSON.stringify(body),
|
|
997
|
+
signal: controller.signal
|
|
998
|
+
});
|
|
999
|
+
if (!res.ok) {
|
|
1000
|
+
throw new Error(`IndexerClient: HTTP ${res.status} for POST ${path}`);
|
|
1001
|
+
}
|
|
1002
|
+
return res.json();
|
|
1003
|
+
} finally {
|
|
1004
|
+
clearTimeout(timer);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
899
1007
|
async getOrNull(path) {
|
|
900
1008
|
const res = await this._fetchResponse(path);
|
|
901
1009
|
if (res.status === 404) return null;
|
|
@@ -931,6 +1039,21 @@ var IndexerClient = class {
|
|
|
931
1039
|
`/shielded/commitments/${encodeURIComponent(hex)}`
|
|
932
1040
|
);
|
|
933
1041
|
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Returns a paginated list of stealth scan hints ordered ascending by leafIndex.
|
|
1044
|
+
* Each hint contains only the fields required for ECDH triage and decryption:
|
|
1045
|
+
* leafIndex, commitmentHex, assetId, ephPkHex, encryptedMemo.
|
|
1046
|
+
*
|
|
1047
|
+
* Use `sinceLeafIndex` for incremental scans (cursor = last seen leafIndex + 1).
|
|
1048
|
+
*/
|
|
1049
|
+
async getScanHints(params) {
|
|
1050
|
+
const qs = this.buildQuery({
|
|
1051
|
+
page: params?.page,
|
|
1052
|
+
limit: params?.limit,
|
|
1053
|
+
since_leaf_index: params?.sinceLeafIndex
|
|
1054
|
+
});
|
|
1055
|
+
return this.get(`/shielded/scan-hints${qs}`);
|
|
1056
|
+
}
|
|
934
1057
|
// ─── Nullifiers ────────────────────────────────────────────────────────────
|
|
935
1058
|
/** Returns a paginated list of spent nullifiers. */
|
|
936
1059
|
async getNullifiers(params) {
|
|
@@ -943,11 +1066,50 @@ var IndexerClient = class {
|
|
|
943
1066
|
`/shielded/nullifier/${encodeURIComponent(hex)}/status`
|
|
944
1067
|
);
|
|
945
1068
|
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Batch-checks which of the given nullifiers are spent.
|
|
1071
|
+
* Returns only the nullifiers that exist in the spent set.
|
|
1072
|
+
* Accepts up to 100 nullifiers (0x-prefixed hex).
|
|
1073
|
+
*/
|
|
1074
|
+
async getNullifiersBatch(nullifiers) {
|
|
1075
|
+
if (nullifiers.length === 0) return [];
|
|
1076
|
+
const res = await this.post("/shielded/nullifiers/batch", {
|
|
1077
|
+
nullifiers: nullifiers.map((n) => n.toLowerCase())
|
|
1078
|
+
});
|
|
1079
|
+
return res.data;
|
|
1080
|
+
}
|
|
946
1081
|
// ─── Private transfers ─────────────────────────────────────────────────────
|
|
947
|
-
/**
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1082
|
+
/**
|
|
1083
|
+
* Returns temporal metadata for private transfers that spent any of the given nullifiers.
|
|
1084
|
+
* Only blockNumber, extrinsicIndex, timestampMs, and hash are returned — no cross-link
|
|
1085
|
+
* between inputs and outputs to prevent graph reconstruction.
|
|
1086
|
+
* Accepts up to 50 nullifiers (0x-prefixed hex).
|
|
1087
|
+
*/
|
|
1088
|
+
async getTransfersByNullifiers(nullifiers) {
|
|
1089
|
+
if (nullifiers.length === 0) return [];
|
|
1090
|
+
const qs = this.buildQuery({
|
|
1091
|
+
nullifiers: nullifiers.map((n) => n.toLowerCase()).join(",")
|
|
1092
|
+
});
|
|
1093
|
+
const res = await this.get(
|
|
1094
|
+
`/shielded/transfers/by-nullifiers${qs}`
|
|
1095
|
+
);
|
|
1096
|
+
return res.data;
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
* Returns temporal metadata for private transfers that produced any of the given commitments.
|
|
1100
|
+
* Only blockNumber, extrinsicIndex, timestampMs, and hash are returned — no cross-link
|
|
1101
|
+
* between outputs and inputs to prevent graph reconstruction.
|
|
1102
|
+
* Accepts up to 50 commitments (0x-prefixed hex).
|
|
1103
|
+
*/
|
|
1104
|
+
async getTransfersByCommitments(commitments) {
|
|
1105
|
+
if (commitments.length === 0) return [];
|
|
1106
|
+
const qs = this.buildQuery({
|
|
1107
|
+
commitments: commitments.map((c) => c.toLowerCase()).join(",")
|
|
1108
|
+
});
|
|
1109
|
+
const res = await this.get(
|
|
1110
|
+
`/shielded/transfers/by-commitments${qs}`
|
|
1111
|
+
);
|
|
1112
|
+
return res.data;
|
|
951
1113
|
}
|
|
952
1114
|
// ─── Unshields ─────────────────────────────────────────────────────────────
|
|
953
1115
|
/** Returns a paginated list of unshield events. */
|
|
@@ -1007,6 +1169,16 @@ var IndexerClient = class {
|
|
|
1007
1169
|
`/address/${encodeURIComponent(address.toLowerCase())}/shielded${qs}`
|
|
1008
1170
|
);
|
|
1009
1171
|
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Returns a paginated list of unshield events where the given address is the recipient.
|
|
1174
|
+
* Accepts a 0x-prefixed EVM address or an SS58 Substrate address.
|
|
1175
|
+
*/
|
|
1176
|
+
async getAddressUnshields(address, params) {
|
|
1177
|
+
const qs = this.buildQuery({ page: params?.page, limit: params?.limit });
|
|
1178
|
+
return this.get(
|
|
1179
|
+
`/address/${encodeURIComponent(address)}/unshields${qs}`
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1010
1182
|
/**
|
|
1011
1183
|
* Returns a paginated list of all shielded activity (commitments, unshields,
|
|
1012
1184
|
* private transfers) associated with the given address.
|
|
@@ -1018,6 +1190,46 @@ var IndexerClient = class {
|
|
|
1018
1190
|
`/shielded/address/${encodeURIComponent(address.toLowerCase())}${qs}`
|
|
1019
1191
|
);
|
|
1020
1192
|
}
|
|
1193
|
+
// ─── Relayers ──────────────────────────────────────────────────────────────
|
|
1194
|
+
/** Returns a paginated list of relayers. Filter by active status with `active`. */
|
|
1195
|
+
async getRelayers(params) {
|
|
1196
|
+
const qs = this.buildQuery({
|
|
1197
|
+
page: params?.page,
|
|
1198
|
+
limit: params?.limit,
|
|
1199
|
+
active: params?.active === void 0 ? void 0 : params.active ? "true" : "false"
|
|
1200
|
+
});
|
|
1201
|
+
return this.get(`/relayers${qs}`);
|
|
1202
|
+
}
|
|
1203
|
+
/** Returns a single relayer by EVM address, or null if not found. */
|
|
1204
|
+
async getRelayer(evmAddress) {
|
|
1205
|
+
return this.getOrNull(`/relayers/${encodeURIComponent(evmAddress.toLowerCase())}`);
|
|
1206
|
+
}
|
|
1207
|
+
/** Returns a paginated list of relay fee events. */
|
|
1208
|
+
async getRelayFees(params) {
|
|
1209
|
+
const qs = this.buildQuery({
|
|
1210
|
+
page: params?.page,
|
|
1211
|
+
limit: params?.limit,
|
|
1212
|
+
relayer: params?.relayer,
|
|
1213
|
+
type: params?.type
|
|
1214
|
+
});
|
|
1215
|
+
return this.get(`/relayers/fees${qs}`);
|
|
1216
|
+
}
|
|
1217
|
+
/** Returns aggregated relay fee balances per asset for a given relayer account. */
|
|
1218
|
+
async getRelayFeesSummary(relayer) {
|
|
1219
|
+
return this.get(
|
|
1220
|
+
`/relayers/fees/summary/${encodeURIComponent(relayer)}`
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
// ─── Registered assets ─────────────────────────────────────────────────────
|
|
1224
|
+
/** Returns a paginated list of assets registered via register_asset. */
|
|
1225
|
+
async getRegisteredAssets(params) {
|
|
1226
|
+
const qs = this.buildQuery({ page: params?.page, limit: params?.limit });
|
|
1227
|
+
return this.get(`/shielded/assets${qs}`);
|
|
1228
|
+
}
|
|
1229
|
+
/** Returns a single registered asset by its ID, or null if not found. */
|
|
1230
|
+
async getRegisteredAsset(assetId) {
|
|
1231
|
+
return this.getOrNull(`/shielded/assets/${encodeURIComponent(assetId)}`);
|
|
1232
|
+
}
|
|
1021
1233
|
// ─── Stats & Health ────────────────────────────────────────────────────────
|
|
1022
1234
|
/** Returns aggregated indexer statistics. */
|
|
1023
1235
|
async getStats() {
|
|
@@ -1042,10 +1254,24 @@ var IndexerClient = class {
|
|
|
1042
1254
|
}
|
|
1043
1255
|
};
|
|
1044
1256
|
|
|
1045
|
-
// src/shielded-pool/ShieldedPoolModule.ts
|
|
1257
|
+
// src/shielded-pool/pallet/ShieldedPoolModule.ts
|
|
1046
1258
|
import { Binary as Binary2 } from "polkadot-api";
|
|
1047
1259
|
|
|
1048
1260
|
// src/utils/tx.ts
|
|
1261
|
+
function formatDispatchError(err) {
|
|
1262
|
+
if (err.type === "Module") {
|
|
1263
|
+
const inner = err.value;
|
|
1264
|
+
if (inner?.type) {
|
|
1265
|
+
return `Module(${inner.type})`;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
try {
|
|
1269
|
+
const detail = JSON.stringify(err.value);
|
|
1270
|
+
return detail && detail !== "null" ? `${err.type}(${detail})` : err.type;
|
|
1271
|
+
} catch {
|
|
1272
|
+
return err.type;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1049
1275
|
function toTxResult(payload) {
|
|
1050
1276
|
const base = {
|
|
1051
1277
|
txHash: payload.txHash,
|
|
@@ -1054,13 +1280,18 @@ function toTxResult(payload) {
|
|
|
1054
1280
|
ok: payload.ok
|
|
1055
1281
|
};
|
|
1056
1282
|
if (!payload.ok) {
|
|
1057
|
-
return { ...base, error: payload.dispatchError
|
|
1283
|
+
return { ...base, error: formatDispatchError(payload.dispatchError) };
|
|
1058
1284
|
}
|
|
1059
1285
|
return base;
|
|
1060
1286
|
}
|
|
1061
1287
|
function callUnsafeTx(txEntry, ...args) {
|
|
1062
1288
|
return txEntry(...args);
|
|
1063
1289
|
}
|
|
1290
|
+
async function submitBareTx(tx, client) {
|
|
1291
|
+
const bareTxHex = await tx.getBareTx();
|
|
1292
|
+
const payload = await client.submitUnsignedAndWatch(bareTxHex);
|
|
1293
|
+
return toTxResult(payload);
|
|
1294
|
+
}
|
|
1064
1295
|
function resolveTx(unsafe, pallet, call) {
|
|
1065
1296
|
const u = unsafe;
|
|
1066
1297
|
const p = u["tx"]?.[pallet];
|
|
@@ -1071,9 +1302,10 @@ function resolveTx(unsafe, pallet, call) {
|
|
|
1071
1302
|
return entry;
|
|
1072
1303
|
}
|
|
1073
1304
|
|
|
1074
|
-
// src/shielded-pool/EncryptedMemo.ts
|
|
1305
|
+
// src/shielded-pool/protocol/EncryptedMemo.ts
|
|
1075
1306
|
import { chacha20poly1305 } from "@noble/ciphers/chacha.js";
|
|
1076
1307
|
import { randomBytes } from "@noble/ciphers/utils.js";
|
|
1308
|
+
import { mulPointEscalar, Base8, packPoint, unpackPoint } from "@zk-kit/baby-jubjub";
|
|
1077
1309
|
|
|
1078
1310
|
// src/utils/bytes.ts
|
|
1079
1311
|
function bigintTo32Le(n) {
|
|
@@ -1120,77 +1352,114 @@ function computePathIndices(leafIndex, depth) {
|
|
|
1120
1352
|
return indices;
|
|
1121
1353
|
}
|
|
1122
1354
|
function leHexToBigint(hex) {
|
|
1123
|
-
|
|
1124
|
-
const bytes = new Uint8Array(h.length / 2);
|
|
1125
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
1126
|
-
bytes[i] = parseInt(h.slice(i * 2, i * 2 + 2), 16);
|
|
1127
|
-
}
|
|
1128
|
-
return bytesToBigintLE(bytes);
|
|
1355
|
+
return bytesToBigintLE(fromHex(hex));
|
|
1129
1356
|
}
|
|
1130
1357
|
|
|
1131
|
-
// src/
|
|
1358
|
+
// src/utils/crypto-constants.ts
|
|
1359
|
+
var BN254_R = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
|
|
1360
|
+
var BABYJUB_SUBORDER = 2736030358979909402780800718157159386076813972158567259200215660948447373041n;
|
|
1361
|
+
|
|
1362
|
+
// src/shielded-pool/protocol/memo.ts
|
|
1132
1363
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
1133
1364
|
var KEY_DOMAIN = new TextEncoder().encode("orbinum-note-encryption-v1");
|
|
1134
|
-
var MEMO_PLAINTEXT_SIZE =
|
|
1135
|
-
function serializeMemo(value, ownerPk, blinding, assetId) {
|
|
1365
|
+
var MEMO_PLAINTEXT_SIZE = 116;
|
|
1366
|
+
function serializeMemo(value, ownerPk, blinding, assetId, counterpartyPk) {
|
|
1136
1367
|
const buf = new Uint8Array(MEMO_PLAINTEXT_SIZE);
|
|
1137
1368
|
const view = new DataView(buf.buffer);
|
|
1138
1369
|
view.setBigUint64(0, value & 0xffffffffffffffffn, true);
|
|
1139
|
-
|
|
1140
|
-
buf.set(
|
|
1141
|
-
|
|
1370
|
+
view.setBigUint64(8, value >> 64n & 0xffffffffffffffffn, true);
|
|
1371
|
+
buf.set(ownerPk.slice(0, 32), 16);
|
|
1372
|
+
buf.set(blinding.slice(0, 32), 48);
|
|
1373
|
+
view.setUint32(80, assetId >>> 0, true);
|
|
1374
|
+
buf.set(counterpartyPk.slice(0, 32), 84);
|
|
1142
1375
|
return buf;
|
|
1143
1376
|
}
|
|
1144
|
-
function deriveEncryptionKey(
|
|
1377
|
+
function deriveEncryptionKey(sharedSecret, commitment) {
|
|
1145
1378
|
const h = sha256.create();
|
|
1146
|
-
h.update(
|
|
1379
|
+
h.update(sharedSecret);
|
|
1147
1380
|
h.update(commitment);
|
|
1148
1381
|
h.update(KEY_DOMAIN);
|
|
1149
1382
|
return h.digest();
|
|
1150
1383
|
}
|
|
1151
|
-
function toBase64(buf) {
|
|
1152
|
-
const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
|
|
1153
|
-
let str = "";
|
|
1154
|
-
for (const b of bytes) str += String.fromCharCode(b);
|
|
1155
|
-
return btoa(str);
|
|
1156
|
-
}
|
|
1157
|
-
function fromBase64(b64) {
|
|
1158
|
-
const bin = atob(b64);
|
|
1159
|
-
const out = new Uint8Array(bin.length);
|
|
1160
|
-
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
1161
|
-
return out;
|
|
1162
|
-
}
|
|
1163
1384
|
|
|
1164
|
-
// src/shielded-pool/EncryptedMemo.ts
|
|
1385
|
+
// src/shielded-pool/protocol/EncryptedMemo.ts
|
|
1165
1386
|
var NONCE_SIZE = 12;
|
|
1166
|
-
var
|
|
1387
|
+
var CIPHERTEXT_SIZE = 132;
|
|
1388
|
+
var EPH_PK_SIZE = 32;
|
|
1389
|
+
var ENCRYPTED_MEMO_SIZE = NONCE_SIZE + CIPHERTEXT_SIZE + EPH_PK_SIZE;
|
|
1390
|
+
function bytesToBjjScalar(bytes) {
|
|
1391
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
1392
|
+
return BigInt("0x" + hex) % BABYJUB_SUBORDER || 1n;
|
|
1393
|
+
}
|
|
1394
|
+
function parsePlaintext(nonce, ciphertextWithMac, encKey) {
|
|
1395
|
+
try {
|
|
1396
|
+
const cipher = chacha20poly1305(encKey, nonce);
|
|
1397
|
+
const plaintext = cipher.decrypt(ciphertextWithMac);
|
|
1398
|
+
const view = new DataView(plaintext.buffer, plaintext.byteOffset, plaintext.byteLength);
|
|
1399
|
+
const valueLo = view.getBigUint64(0, true);
|
|
1400
|
+
const valueHi = view.getBigUint64(8, true);
|
|
1401
|
+
const value = valueLo | valueHi << 64n;
|
|
1402
|
+
const ownerPk = bytesToBigintLE(plaintext.slice(16, 48));
|
|
1403
|
+
const blinding = bytesToBigintLE(plaintext.slice(48, 80));
|
|
1404
|
+
const assetId = BigInt(view.getUint32(80, true));
|
|
1405
|
+
const counterpartyPk = bytesToBigintLE(plaintext.slice(84, 116));
|
|
1406
|
+
return { value, ownerPk, blinding, assetId, counterpartyPk };
|
|
1407
|
+
} catch {
|
|
1408
|
+
return null;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1167
1411
|
var EncryptedMemo = {
|
|
1168
1412
|
/**
|
|
1169
|
-
* Build and encrypt a memo for a note.
|
|
1413
|
+
* Build and encrypt a memo for a note using ECDH (v2, 168 bytes).
|
|
1170
1414
|
*
|
|
1171
|
-
* @param value
|
|
1172
|
-
* @param ownerPk
|
|
1173
|
-
* @param blinding
|
|
1174
|
-
* @param assetId
|
|
1175
|
-
* @param commitment
|
|
1176
|
-
* @param
|
|
1177
|
-
*
|
|
1178
|
-
*
|
|
1179
|
-
|
|
1180
|
-
|
|
1415
|
+
* @param value Note value in planck.
|
|
1416
|
+
* @param ownerPk 32-byte owner public key (LE).
|
|
1417
|
+
* @param blinding 32-byte blinding scalar (LE).
|
|
1418
|
+
* @param assetId Asset identifier.
|
|
1419
|
+
* @param commitment 32-byte commitment bytes (LE).
|
|
1420
|
+
* @param recipientIvkPacked 32-byte LE-encoded packed BJJ viewing public key
|
|
1421
|
+
* (from PrivacyKeyManager.getViewingPublicKeyPacked() or
|
|
1422
|
+
* decoded from a privacy address).
|
|
1423
|
+
* Pass `new Uint8Array(32)` (all zeros) for a publicly-readable memo.
|
|
1424
|
+
* @param counterpartyPk 32-byte counterparty BJJ Ax. Default: all zeros.
|
|
1425
|
+
* @returns 168-byte encrypted memo: nonce(12) || ciphertext+MAC(124) || ephPk(32).
|
|
1426
|
+
*/
|
|
1427
|
+
encrypt(value, ownerPk, blinding, assetId, commitment, recipientIvkPacked, counterpartyPk = new Uint8Array(32), ephSkOverride) {
|
|
1181
1428
|
const nonce = randomBytes(NONCE_SIZE);
|
|
1182
|
-
const
|
|
1183
|
-
const
|
|
1184
|
-
|
|
1429
|
+
const plaintext = serializeMemo(value, ownerPk, blinding, assetId, counterpartyPk);
|
|
1430
|
+
const isZeroKey = recipientIvkPacked.every((b) => b === 0);
|
|
1431
|
+
let sharedSecret;
|
|
1432
|
+
let ephPkPackedBytes;
|
|
1433
|
+
if (isZeroKey) {
|
|
1434
|
+
sharedSecret = new Uint8Array(32);
|
|
1435
|
+
ephPkPackedBytes = new Uint8Array(EPH_PK_SIZE);
|
|
1436
|
+
} else {
|
|
1437
|
+
const ephSkBytes = ephSkOverride ?? randomBytes(32);
|
|
1438
|
+
if (ephSkBytes.length !== 32)
|
|
1439
|
+
throw new Error("EncryptedMemo.encrypt: ephSkOverride must be 32 bytes");
|
|
1440
|
+
const ephSkScalar = bytesToBjjScalar(ephSkBytes);
|
|
1441
|
+
const ephPkPoint = mulPointEscalar(Base8, ephSkScalar);
|
|
1442
|
+
ephPkPackedBytes = bigintTo32Le(packPoint(ephPkPoint));
|
|
1443
|
+
const ivkPackedBigint = bytesToBigintLE(recipientIvkPacked);
|
|
1444
|
+
const ivkPoint = unpackPoint(ivkPackedBigint);
|
|
1445
|
+
if (!ivkPoint)
|
|
1446
|
+
throw new Error("EncryptedMemo.encrypt: invalid recipient viewing public key");
|
|
1447
|
+
const sharedPoint = mulPointEscalar(ivkPoint, ephSkScalar);
|
|
1448
|
+
sharedSecret = bigintTo32Le(sharedPoint[0]);
|
|
1449
|
+
}
|
|
1450
|
+
const encKey = deriveEncryptionKey(sharedSecret, commitment);
|
|
1451
|
+
const cipher = chacha20poly1305(encKey, nonce);
|
|
1185
1452
|
const ciphertext = cipher.encrypt(plaintext);
|
|
1186
|
-
const result = new Uint8Array(
|
|
1453
|
+
const result = new Uint8Array(ENCRYPTED_MEMO_SIZE);
|
|
1187
1454
|
result.set(nonce, 0);
|
|
1188
1455
|
result.set(ciphertext, NONCE_SIZE);
|
|
1456
|
+
result.set(ephPkPackedBytes, NONCE_SIZE + CIPHERTEXT_SIZE);
|
|
1189
1457
|
return result;
|
|
1190
1458
|
},
|
|
1191
1459
|
/**
|
|
1192
|
-
* Returns a
|
|
1193
|
-
*
|
|
1460
|
+
* Returns a 168-byte public memo encrypted with a zero viewing key.
|
|
1461
|
+
* Decryptable by anyone with `decrypt(memo, commitment, new Uint8Array(32))`.
|
|
1462
|
+
* Convenience alias for `encrypt(..., new Uint8Array(32))`.
|
|
1194
1463
|
*/
|
|
1195
1464
|
encryptPublic(value, ownerPk, blinding, assetId, commitment) {
|
|
1196
1465
|
return EncryptedMemo.encrypt(
|
|
@@ -1203,226 +1472,89 @@ var EncryptedMemo = {
|
|
|
1203
1472
|
);
|
|
1204
1473
|
},
|
|
1205
1474
|
/**
|
|
1206
|
-
* Returns a
|
|
1475
|
+
* Returns a 168-byte zeroed dummy memo (no information, always valid on-chain).
|
|
1207
1476
|
*/
|
|
1208
1477
|
dummy() {
|
|
1209
1478
|
return new Uint8Array(ENCRYPTED_MEMO_SIZE);
|
|
1210
1479
|
},
|
|
1211
1480
|
/**
|
|
1212
|
-
*
|
|
1481
|
+
* Validates that `bytes` is a properly-sized encrypted memo.
|
|
1482
|
+
* Throws an Error if the length is not ENCRYPTED_MEMO_SIZE (168 bytes).
|
|
1213
1483
|
*
|
|
1214
|
-
*
|
|
1215
|
-
*
|
|
1484
|
+
* Call this at system boundaries (extrinsic builders, precompile encoders)
|
|
1485
|
+
* to catch malformed memos before they reach the chain and fail on-chain.
|
|
1216
1486
|
*
|
|
1217
|
-
* @param
|
|
1218
|
-
* @param
|
|
1219
|
-
* @param recipientVk 32-byte recipient viewing key.
|
|
1487
|
+
* @param bytes The memo bytes to validate.
|
|
1488
|
+
* @param context Optional context string included in the error (e.g. 'shield', 'output[0]').
|
|
1220
1489
|
*/
|
|
1221
|
-
|
|
1222
|
-
if (
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
const cipher = chacha20poly1305(key, nonce);
|
|
1228
|
-
const plaintext = cipher.decrypt(ciphertext);
|
|
1229
|
-
const view = new DataView(plaintext.buffer, plaintext.byteOffset, plaintext.byteLength);
|
|
1230
|
-
const value = view.getBigUint64(0, true);
|
|
1231
|
-
const ownerPk = bytesToBigintLE(plaintext.slice(8, 40));
|
|
1232
|
-
const blinding = bytesToBigintLE(plaintext.slice(40, 72));
|
|
1233
|
-
const assetId = BigInt(view.getUint32(72, true));
|
|
1234
|
-
return { value, ownerPk, blinding, assetId };
|
|
1235
|
-
} catch {
|
|
1236
|
-
return null;
|
|
1490
|
+
validate(bytes, context) {
|
|
1491
|
+
if (bytes.length !== ENCRYPTED_MEMO_SIZE) {
|
|
1492
|
+
const ctx = context ? ` (${context})` : "";
|
|
1493
|
+
throw new Error(
|
|
1494
|
+
`EncryptedMemo: invalid size${ctx} \u2014 expected ${ENCRYPTED_MEMO_SIZE} bytes, got ${bytes.length}`
|
|
1495
|
+
);
|
|
1237
1496
|
}
|
|
1238
|
-
}
|
|
1239
|
-
};
|
|
1240
|
-
|
|
1241
|
-
// src/shielded-pool/NoteBuilder.ts
|
|
1242
|
-
import { poseidon2, poseidon4 } from "poseidon-lite";
|
|
1243
|
-
var NoteBuilder = class {
|
|
1497
|
+
},
|
|
1244
1498
|
/**
|
|
1245
|
-
*
|
|
1499
|
+
* Decrypt an on-chain EncryptedMemo using the recipient's viewing secret key.
|
|
1500
|
+
* Returns null if decryption fails — wrong key, bad MAC, or malformed memo.
|
|
1501
|
+
* Never throws; safe for scan loops.
|
|
1246
1502
|
*
|
|
1247
|
-
* @param
|
|
1248
|
-
* @param
|
|
1249
|
-
* @param
|
|
1250
|
-
* @param input.blinding Random scalar — defaults to BigInt(Date.now()).
|
|
1251
|
-
* @param input.spendingKey Secret key for nullifier — default 0n.
|
|
1503
|
+
* @param memoBytes 168-byte encrypted memo.
|
|
1504
|
+
* @param commitment 32-byte note commitment (LE).
|
|
1505
|
+
* @param viewingSecretKey 32-byte HKDF viewing secret key from deriveViewingSecretKey().
|
|
1252
1506
|
*/
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
const blinding = input.blinding ?? BigInt(Date.now());
|
|
1258
|
-
const spendingKey = input.spendingKey ?? 0n;
|
|
1259
|
-
const commitment = poseidon4([value, assetId, ownerPk, blinding]);
|
|
1260
|
-
const nullifier = poseidon2([commitment, spendingKey]);
|
|
1261
|
-
const commitmentBytes = bigintTo32Le(commitment);
|
|
1262
|
-
const nullifierBytes = bigintTo32Le(nullifier);
|
|
1263
|
-
const memo = input.viewingKey !== void 0 ? Array.from(
|
|
1264
|
-
EncryptedMemo.encrypt(
|
|
1265
|
-
value,
|
|
1266
|
-
bigintTo32Le(ownerPk),
|
|
1267
|
-
bigintTo32Le(blinding),
|
|
1268
|
-
Number(assetId),
|
|
1269
|
-
commitmentBytes,
|
|
1270
|
-
input.viewingKey
|
|
1271
|
-
)
|
|
1272
|
-
) : Array.from(EncryptedMemo.dummy());
|
|
1273
|
-
const note = {
|
|
1274
|
-
value,
|
|
1275
|
-
assetId,
|
|
1276
|
-
ownerPk,
|
|
1277
|
-
blinding,
|
|
1278
|
-
spendingKey,
|
|
1279
|
-
spent: false,
|
|
1280
|
-
spentAt: null,
|
|
1281
|
-
commitment,
|
|
1282
|
-
nullifier,
|
|
1283
|
-
commitmentHex: toHex(commitmentBytes),
|
|
1284
|
-
nullifierHex: toHex(nullifierBytes),
|
|
1285
|
-
memo
|
|
1286
|
-
};
|
|
1287
|
-
return note;
|
|
1288
|
-
}
|
|
1507
|
+
decrypt(memoBytes, commitment, viewingSecretKey) {
|
|
1508
|
+
if (memoBytes.length !== ENCRYPTED_MEMO_SIZE) return null;
|
|
1509
|
+
return EncryptedMemo._decrypt(memoBytes, commitment, viewingSecretKey);
|
|
1510
|
+
},
|
|
1289
1511
|
/**
|
|
1290
|
-
*
|
|
1291
|
-
*
|
|
1292
|
-
* Pure TypeScript implementation — no WASM dependency.
|
|
1293
|
-
* Uses ChaCha20-Poly1305 with SHA-256 key derivation.
|
|
1512
|
+
* Extract the ECDH shared secret from an encrypted memo using the recipient's viewing secret key.
|
|
1294
1513
|
*
|
|
1295
|
-
*
|
|
1296
|
-
*
|
|
1297
|
-
* Pass `new Uint8Array(32)` (default) for a public/dummy memo.
|
|
1298
|
-
*/
|
|
1299
|
-
static buildMemo(note, recipientVk) {
|
|
1300
|
-
return EncryptedMemo.encrypt(
|
|
1301
|
-
note.value,
|
|
1302
|
-
bigintTo32Le(note.ownerPk),
|
|
1303
|
-
bigintTo32Le(note.blinding),
|
|
1304
|
-
Number(note.assetId),
|
|
1305
|
-
bigintTo32Le(note.commitment),
|
|
1306
|
-
recipientVk ?? new Uint8Array(32)
|
|
1307
|
-
);
|
|
1308
|
-
}
|
|
1309
|
-
};
|
|
1310
|
-
|
|
1311
|
-
// src/shielded-pool/ShieldedPoolModule.ts
|
|
1312
|
-
var ShieldedPoolModule = class {
|
|
1313
|
-
constructor(substrate) {
|
|
1314
|
-
this.substrate = substrate;
|
|
1315
|
-
}
|
|
1316
|
-
// ─── Extrinsics ────────────────────────────────────────────────────────────
|
|
1317
|
-
/**
|
|
1318
|
-
* Deposits tokens into the shielded pool.
|
|
1319
|
-
* Extrinsic: shieldedPool.shield(assetId, amount, commitment, encryptedMemo)
|
|
1320
|
-
*/
|
|
1321
|
-
async shield(params, signer) {
|
|
1322
|
-
const memo = params.encryptedMemo ?? EncryptedMemo.dummy();
|
|
1323
|
-
const entry = resolveTx(this.substrate.unsafe, "shieldedPool", "shield");
|
|
1324
|
-
const tx = callUnsafeTx(
|
|
1325
|
-
entry,
|
|
1326
|
-
params.assetId,
|
|
1327
|
-
params.amount.toString(),
|
|
1328
|
-
Binary2.fromHex(params.commitment),
|
|
1329
|
-
Binary2.fromBytes(memo)
|
|
1330
|
-
);
|
|
1331
|
-
return toTxResult(await tx.signAndSubmit(signer));
|
|
1332
|
-
}
|
|
1333
|
-
/**
|
|
1334
|
-
* Build a ZkNote locally and submit shieldedPool.shield in one call.
|
|
1514
|
+
* Used by NoteDecryptor to obtain the shared secret needed for stealth address derivation
|
|
1515
|
+
* without re-running the full decrypt path. Safe to call on any 168-byte memo.
|
|
1335
1516
|
*
|
|
1336
|
-
* Returns
|
|
1337
|
-
*
|
|
1517
|
+
* Returns `new Uint8Array(32)` (all zeros) for public/dummy memos (zero ephPk).
|
|
1518
|
+
* Returns `null` if the memo is malformed or the ephPk is not a valid BJJ point.
|
|
1519
|
+
* Never throws; safe for scan loops.
|
|
1338
1520
|
*
|
|
1339
|
-
* @param
|
|
1340
|
-
* @param
|
|
1341
|
-
* @param params.ownerPk BabyJubJub Ax (default 0n).
|
|
1342
|
-
* @param params.blinding Random blinding scalar (default BigInt(Date.now())).
|
|
1343
|
-
* @param params.spendingKey Secret spending key (default 0n).
|
|
1344
|
-
*/
|
|
1345
|
-
async buildAndShield(params, signer) {
|
|
1346
|
-
const noteInput = {
|
|
1347
|
-
value: params.value,
|
|
1348
|
-
...params.assetId !== void 0 && { assetId: BigInt(params.assetId) },
|
|
1349
|
-
...params.ownerPk !== void 0 && { ownerPk: params.ownerPk },
|
|
1350
|
-
...params.blinding !== void 0 && { blinding: params.blinding },
|
|
1351
|
-
...params.spendingKey !== void 0 && { spendingKey: params.spendingKey }
|
|
1352
|
-
};
|
|
1353
|
-
const note = await NoteBuilder.build(noteInput);
|
|
1354
|
-
const memo = NoteBuilder.buildMemo(note);
|
|
1355
|
-
const txResult = await this.shield(
|
|
1356
|
-
{
|
|
1357
|
-
assetId: Number(note.assetId),
|
|
1358
|
-
amount: note.value,
|
|
1359
|
-
commitment: note.commitmentHex,
|
|
1360
|
-
encryptedMemo: memo
|
|
1361
|
-
},
|
|
1362
|
-
signer
|
|
1363
|
-
);
|
|
1364
|
-
return { txResult, note };
|
|
1365
|
-
}
|
|
1366
|
-
/**
|
|
1367
|
-
* Withdraws tokens from the shielded pool to a public address.
|
|
1368
|
-
* Extrinsic: shieldedPool.unshield(proof, merkleRoot, nullifier, assetId, amount, recipient)
|
|
1369
|
-
*/
|
|
1370
|
-
async unshield(params, signer) {
|
|
1371
|
-
const entry = resolveTx(this.substrate.unsafe, "shieldedPool", "unshield");
|
|
1372
|
-
const tx = callUnsafeTx(
|
|
1373
|
-
entry,
|
|
1374
|
-
Binary2.fromBytes(params.proof),
|
|
1375
|
-
Binary2.fromHex(params.merkleRoot),
|
|
1376
|
-
Binary2.fromHex(params.nullifier),
|
|
1377
|
-
params.assetId,
|
|
1378
|
-
params.amount.toString(),
|
|
1379
|
-
Binary2.fromHex(params.recipientAddress)
|
|
1380
|
-
);
|
|
1381
|
-
return toTxResult(await tx.signAndSubmit(signer));
|
|
1382
|
-
}
|
|
1383
|
-
/**
|
|
1384
|
-
* Performs a private (shielded) transfer between two notes.
|
|
1385
|
-
* Extrinsic: shieldedPool.privateTransfer(inputs, outputs, proof, merkleRoot)
|
|
1386
|
-
*/
|
|
1387
|
-
async privateTransfer(params, signer) {
|
|
1388
|
-
const inputs = params.inputs.map((inp) => ({
|
|
1389
|
-
nullifier: Binary2.fromHex(inp.nullifier),
|
|
1390
|
-
commitment: Binary2.fromHex(inp.commitment)
|
|
1391
|
-
}));
|
|
1392
|
-
const outputs = params.outputs.map((out) => ({
|
|
1393
|
-
commitment: Binary2.fromHex(out.commitment),
|
|
1394
|
-
memo: Binary2.fromBytes(out.encryptedMemo ?? EncryptedMemo.dummy())
|
|
1395
|
-
}));
|
|
1396
|
-
const entry = resolveTx(this.substrate.unsafe, "shieldedPool", "privateTransfer");
|
|
1397
|
-
const tx = callUnsafeTx(
|
|
1398
|
-
entry,
|
|
1399
|
-
inputs,
|
|
1400
|
-
outputs,
|
|
1401
|
-
Binary2.fromBytes(params.proof),
|
|
1402
|
-
Binary2.fromHex(params.merkleRoot)
|
|
1403
|
-
);
|
|
1404
|
-
return toTxResult(await tx.signAndSubmit(signer));
|
|
1405
|
-
}
|
|
1406
|
-
/**
|
|
1407
|
-
* Deposits multiple notes into the shielded pool in a single extrinsic.
|
|
1408
|
-
* Extrinsic: shieldedPool.shieldBatch(operations) — max 20 items.
|
|
1521
|
+
* @param memoBytes 168-byte encrypted memo.
|
|
1522
|
+
* @param viewingSecretKey 32-byte HKDF viewing secret key from deriveViewingSecretKey().
|
|
1409
1523
|
*/
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
}
|
|
1417
|
-
const
|
|
1418
|
-
|
|
1419
|
-
|
|
1524
|
+
extractSharedSecret(memoBytes, viewingSecretKey) {
|
|
1525
|
+
if (memoBytes.length !== ENCRYPTED_MEMO_SIZE) return null;
|
|
1526
|
+
const ephPkPackedBytes = memoBytes.slice(NONCE_SIZE + CIPHERTEXT_SIZE);
|
|
1527
|
+
const ephPkPackedBigint = bytesToBigintLE(ephPkPackedBytes);
|
|
1528
|
+
if (ephPkPackedBigint === 0n) {
|
|
1529
|
+
return new Uint8Array(32);
|
|
1530
|
+
}
|
|
1531
|
+
const ephPkPoint = unpackPoint(ephPkPackedBigint);
|
|
1532
|
+
if (!ephPkPoint) return null;
|
|
1533
|
+
const ivskScalar = bytesToBjjScalar(viewingSecretKey);
|
|
1534
|
+
const sharedPoint = mulPointEscalar(ephPkPoint, ivskScalar);
|
|
1535
|
+
return bigintTo32Le(sharedPoint[0]);
|
|
1536
|
+
},
|
|
1537
|
+
/** @internal */
|
|
1538
|
+
_decrypt(memoBytes, commitment, viewingSecretKey) {
|
|
1539
|
+
const nonce = memoBytes.slice(0, NONCE_SIZE);
|
|
1540
|
+
const ciphertextWithMac = memoBytes.slice(NONCE_SIZE, NONCE_SIZE + CIPHERTEXT_SIZE);
|
|
1541
|
+
const ephPkPackedBytes = memoBytes.slice(NONCE_SIZE + CIPHERTEXT_SIZE);
|
|
1542
|
+
const ephPkPackedBigint = bytesToBigintLE(ephPkPackedBytes);
|
|
1543
|
+
let sharedSecret;
|
|
1544
|
+
if (ephPkPackedBigint === 0n) {
|
|
1545
|
+
sharedSecret = new Uint8Array(32);
|
|
1546
|
+
} else {
|
|
1547
|
+
const ephPkPoint = unpackPoint(ephPkPackedBigint);
|
|
1548
|
+
if (!ephPkPoint) return null;
|
|
1549
|
+
const ivskScalar = bytesToBjjScalar(viewingSecretKey);
|
|
1550
|
+
const sharedPoint = mulPointEscalar(ephPkPoint, ivskScalar);
|
|
1551
|
+
sharedSecret = bigintTo32Le(sharedPoint[0]);
|
|
1552
|
+
}
|
|
1553
|
+
const encKey = deriveEncryptionKey(sharedSecret, commitment);
|
|
1554
|
+
return parsePlaintext(nonce, ciphertextWithMac, encKey);
|
|
1420
1555
|
}
|
|
1421
1556
|
};
|
|
1422
1557
|
|
|
1423
|
-
// src/account-mapping/AccountMappingModule.ts
|
|
1424
|
-
import { Binary as Binary3 } from "polkadot-api";
|
|
1425
|
-
|
|
1426
1558
|
// src/utils/address.ts
|
|
1427
1559
|
import { decodeAddress, encodeAddress } from "@polkadot/util-crypto";
|
|
1428
1560
|
function normalizeEvmAddress(addr) {
|
|
@@ -1559,25 +1691,173 @@ function addressToAccountIdHex(addr) {
|
|
|
1559
1691
|
return substrateSs58ToAccountIdHex(addr);
|
|
1560
1692
|
}
|
|
1561
1693
|
|
|
1562
|
-
// src/
|
|
1563
|
-
|
|
1564
|
-
if (raw === "Eip191" || raw === "eip191") return "Eip191";
|
|
1565
|
-
if (raw === "Ed25519" || raw === "ed25519") return "Ed25519";
|
|
1566
|
-
return raw;
|
|
1567
|
-
}
|
|
1568
|
-
|
|
1569
|
-
// src/account-mapping/AccountMappingModule.ts
|
|
1570
|
-
var AccountMappingModule = class {
|
|
1694
|
+
// src/shielded-pool/pallet/ShieldedPoolModule.ts
|
|
1695
|
+
var ShieldedPoolModule = class {
|
|
1571
1696
|
constructor(substrate) {
|
|
1572
1697
|
this.substrate = substrate;
|
|
1573
1698
|
}
|
|
1574
|
-
|
|
1699
|
+
substrate;
|
|
1700
|
+
// ─── Extrinsics ────────────────────────────────────────────────────────────
|
|
1575
1701
|
/**
|
|
1576
|
-
*
|
|
1577
|
-
*
|
|
1578
|
-
*
|
|
1702
|
+
* Deposits tokens into the shielded pool.
|
|
1703
|
+
* Extrinsic: shieldedPool.shield(assetId, amount, commitment, encryptedMemo)
|
|
1704
|
+
*
|
|
1705
|
+
* Shield is always a signed (public) transaction — the caller's address
|
|
1706
|
+
* appears on-chain as the depositor.
|
|
1579
1707
|
*/
|
|
1580
|
-
async
|
|
1708
|
+
async shield(params, signer, txOptions) {
|
|
1709
|
+
EncryptedMemo.validate(params.encryptedMemo, "shield.encryptedMemo");
|
|
1710
|
+
const entry = resolveTx(this.substrate.unsafe, "ShieldedPool", "shield");
|
|
1711
|
+
const tx = callUnsafeTx(entry, {
|
|
1712
|
+
asset_id: params.assetId,
|
|
1713
|
+
amount: params.amount,
|
|
1714
|
+
commitment: Binary2.fromHex(params.commitment),
|
|
1715
|
+
encrypted_memo: Binary2.fromBytes(params.encryptedMemo)
|
|
1716
|
+
});
|
|
1717
|
+
return toTxResult(
|
|
1718
|
+
await (txOptions !== void 0 ? tx.signAndSubmit(signer, txOptions) : tx.signAndSubmit(signer))
|
|
1719
|
+
);
|
|
1720
|
+
}
|
|
1721
|
+
/**
|
|
1722
|
+
* Withdraws tokens from the shielded pool to a public address.
|
|
1723
|
+
* Submits as an UNSIGNED (gasless) transaction — fee is embedded in the ZK proof.
|
|
1724
|
+
* Pass a `signer` to fall back to signed submission (e.g. for testing).
|
|
1725
|
+
* Extrinsic: shieldedPool.unshield(proof, merkleRoot, nullifier, assetId, amount, recipient, fee)
|
|
1726
|
+
*/
|
|
1727
|
+
async unshield(params, signer, txOptions) {
|
|
1728
|
+
const entry = resolveTx(this.substrate.unsafe, "ShieldedPool", "unshield");
|
|
1729
|
+
const recipientSs58 = accountIdHexToSs58(params.recipientAddress);
|
|
1730
|
+
if (!recipientSs58) throw new Error(`Invalid recipientAddress: ${params.recipientAddress}`);
|
|
1731
|
+
const changeCommitment = params.changeCommitment ?? "0x" + "00".repeat(32);
|
|
1732
|
+
let changeEncryptedMemo;
|
|
1733
|
+
if (params.changeEncryptedMemo && params.changeEncryptedMemo.length > 0) {
|
|
1734
|
+
EncryptedMemo.validate(params.changeEncryptedMemo, "changeEncryptedMemo");
|
|
1735
|
+
changeEncryptedMemo = Binary2.fromBytes(params.changeEncryptedMemo);
|
|
1736
|
+
} else {
|
|
1737
|
+
changeEncryptedMemo = Binary2.fromBytes(new Uint8Array(0));
|
|
1738
|
+
}
|
|
1739
|
+
const tx = callUnsafeTx(entry, {
|
|
1740
|
+
proof: Binary2.fromBytes(params.proof),
|
|
1741
|
+
merkle_root: Binary2.fromHex(params.merkleRoot),
|
|
1742
|
+
nullifier: Binary2.fromHex(params.nullifier),
|
|
1743
|
+
asset_id: params.assetId,
|
|
1744
|
+
amount: params.amount,
|
|
1745
|
+
recipient: recipientSs58,
|
|
1746
|
+
fee: params.fee ?? 0n,
|
|
1747
|
+
change_commitment: Binary2.fromHex(changeCommitment),
|
|
1748
|
+
change_encrypted_memo: changeEncryptedMemo,
|
|
1749
|
+
relayer: void 0
|
|
1750
|
+
// Option<H160> — None for direct Substrate submissions
|
|
1751
|
+
});
|
|
1752
|
+
if (signer) {
|
|
1753
|
+
return toTxResult(
|
|
1754
|
+
await (txOptions !== void 0 ? tx.signAndSubmit(signer, txOptions) : tx.signAndSubmit(signer))
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1757
|
+
return submitBareTx(tx, this.substrate);
|
|
1758
|
+
}
|
|
1759
|
+
/**
|
|
1760
|
+
* Performs a private (shielded) transfer between two notes.
|
|
1761
|
+
* Submits as an UNSIGNED (gasless) transaction — fee is embedded in the ZK proof.
|
|
1762
|
+
* Pass a `signer` to fall back to signed submission (e.g. for testing).
|
|
1763
|
+
* Extrinsic: shieldedPool.privateTransfer(proof, merkleRoot, nullifiers, commitments, memos, assetId, fee)
|
|
1764
|
+
*/
|
|
1765
|
+
async privateTransfer(params, signer, txOptions) {
|
|
1766
|
+
const nullifiers = params.inputs.map((inp) => Binary2.fromHex(inp.nullifier));
|
|
1767
|
+
const commitments = params.outputs.map((out) => Binary2.fromHex(out.commitment));
|
|
1768
|
+
const memos = params.outputs.map((out, i) => {
|
|
1769
|
+
EncryptedMemo.validate(
|
|
1770
|
+
out.encryptedMemo,
|
|
1771
|
+
`privateTransfer.outputs[${i}].encryptedMemo`
|
|
1772
|
+
);
|
|
1773
|
+
return Binary2.fromBytes(out.encryptedMemo);
|
|
1774
|
+
});
|
|
1775
|
+
const entry = resolveTx(this.substrate.unsafe, "ShieldedPool", "private_transfer");
|
|
1776
|
+
const tx = callUnsafeTx(entry, {
|
|
1777
|
+
proof: Binary2.fromBytes(params.proof),
|
|
1778
|
+
merkle_root: Binary2.fromHex(params.merkleRoot),
|
|
1779
|
+
nullifiers,
|
|
1780
|
+
commitments,
|
|
1781
|
+
encrypted_memos: memos,
|
|
1782
|
+
asset_id: params.assetId,
|
|
1783
|
+
fee: params.fee ?? 0n,
|
|
1784
|
+
relayer: void 0
|
|
1785
|
+
// Option<H160> — None for direct Substrate submissions
|
|
1786
|
+
});
|
|
1787
|
+
if (signer) {
|
|
1788
|
+
return toTxResult(
|
|
1789
|
+
await (txOptions !== void 0 ? tx.signAndSubmit(signer, txOptions) : tx.signAndSubmit(signer))
|
|
1790
|
+
);
|
|
1791
|
+
}
|
|
1792
|
+
return submitBareTx(tx, this.substrate);
|
|
1793
|
+
}
|
|
1794
|
+
/**
|
|
1795
|
+
* Deposits multiple notes into the shielded pool in a single extrinsic.
|
|
1796
|
+
* Extrinsic: shieldedPool.shieldBatch(operations) — max 20 items.
|
|
1797
|
+
*/
|
|
1798
|
+
async shieldBatch(params, signer, txOptions) {
|
|
1799
|
+
const operations = params.items.map((item, i) => {
|
|
1800
|
+
EncryptedMemo.validate(item.encryptedMemo, `shieldBatch.items[${i}].encryptedMemo`);
|
|
1801
|
+
return {
|
|
1802
|
+
assetId: item.assetId,
|
|
1803
|
+
amount: item.amount.toString(),
|
|
1804
|
+
commitment: Binary2.fromHex(item.commitment),
|
|
1805
|
+
encryptedMemo: Binary2.fromBytes(item.encryptedMemo)
|
|
1806
|
+
};
|
|
1807
|
+
});
|
|
1808
|
+
const entry = resolveTx(this.substrate.unsafe, "ShieldedPool", "shield_batch");
|
|
1809
|
+
const tx = callUnsafeTx(entry, operations);
|
|
1810
|
+
return toTxResult(
|
|
1811
|
+
await (txOptions !== void 0 ? tx.signAndSubmit(signer, txOptions) : tx.signAndSubmit(signer))
|
|
1812
|
+
);
|
|
1813
|
+
}
|
|
1814
|
+
/**
|
|
1815
|
+
* Claims accrued relay fees into the shielded pool.
|
|
1816
|
+
* This is a SIGNED transaction — the relayer must sign it with their wallet.
|
|
1817
|
+
* Before calling this, generate a ZK value proof with generateFeeClaimProof() (not yet implemented).
|
|
1818
|
+
*
|
|
1819
|
+
* Extrinsic: shieldedPool.claim_shielded_fees(commitment, amount, asset_id, memo, proof, public_signals)
|
|
1820
|
+
*/
|
|
1821
|
+
async claimShieldedFees(params, signer, txOptions) {
|
|
1822
|
+
EncryptedMemo.validate(params.encryptedMemo, "claimShieldedFees.encryptedMemo");
|
|
1823
|
+
const entry = resolveTx(this.substrate.unsafe, "ShieldedPool", "claim_shielded_fees");
|
|
1824
|
+
const tx = callUnsafeTx(entry, {
|
|
1825
|
+
commitment: Binary2.fromHex(params.commitment),
|
|
1826
|
+
amount: params.amount,
|
|
1827
|
+
asset_id: params.assetId,
|
|
1828
|
+
encrypted_memo: Binary2.fromBytes(params.encryptedMemo),
|
|
1829
|
+
proof: Binary2.fromBytes(params.proof),
|
|
1830
|
+
public_signals: Binary2.fromBytes(params.publicSignals)
|
|
1831
|
+
});
|
|
1832
|
+
return toTxResult(
|
|
1833
|
+
await (txOptions !== void 0 ? tx.signAndSubmit(signer, txOptions) : tx.signAndSubmit(signer))
|
|
1834
|
+
);
|
|
1835
|
+
}
|
|
1836
|
+
};
|
|
1837
|
+
|
|
1838
|
+
// src/account-mapping/AccountMappingModule.ts
|
|
1839
|
+
import { Binary as Binary3 } from "polkadot-api";
|
|
1840
|
+
|
|
1841
|
+
// src/account-mapping/helpers.ts
|
|
1842
|
+
function mapRawScheme(raw) {
|
|
1843
|
+
if (raw === "Eip191" || raw === "eip191") return "Eip191";
|
|
1844
|
+
if (raw === "Ed25519" || raw === "ed25519") return "Ed25519";
|
|
1845
|
+
return raw;
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
// src/account-mapping/AccountMappingModule.ts
|
|
1849
|
+
var AccountMappingModule = class {
|
|
1850
|
+
constructor(substrate) {
|
|
1851
|
+
this.substrate = substrate;
|
|
1852
|
+
}
|
|
1853
|
+
substrate;
|
|
1854
|
+
// ─── Address resolution ─────────────────────────────────────────────────────
|
|
1855
|
+
/**
|
|
1856
|
+
* Returns the explicitly mapped (or fallback) Substrate AccountId32 hex for
|
|
1857
|
+
* an EVM address. `mapped` is set only when `map_account` was called.
|
|
1858
|
+
* `fallback` is always the EeSuffix rule: `H160 ++ [0x00; 12]`.
|
|
1859
|
+
*/
|
|
1860
|
+
async getAccountAddresses(accountId) {
|
|
1581
1861
|
try {
|
|
1582
1862
|
const raw = await this.substrate.request(
|
|
1583
1863
|
"accountMapping_getAccountAddresses",
|
|
@@ -1956,6 +2236,7 @@ var PrivacyModule = class {
|
|
|
1956
2236
|
constructor(substrate) {
|
|
1957
2237
|
this.substrate = substrate;
|
|
1958
2238
|
}
|
|
2239
|
+
substrate;
|
|
1959
2240
|
/** Returns the current Merkle tree root. */
|
|
1960
2241
|
async getMerkleRoot() {
|
|
1961
2242
|
return this.substrate.request("privacy_getMerkleRoot", []);
|
|
@@ -1973,14 +2254,23 @@ var PrivacyModule = class {
|
|
|
1973
2254
|
}
|
|
1974
2255
|
/**
|
|
1975
2256
|
* Returns the Merkle inclusion proof for a given commitment hex,
|
|
1976
|
-
* bundled with the
|
|
2257
|
+
* bundled with the Merkle root.
|
|
2258
|
+
*
|
|
2259
|
+
* Uses `privacy_getMerkleProofByCommitment` which resolves root and proof
|
|
2260
|
+
* under the **same block hash**, guaranteeing that the returned path is
|
|
2261
|
+
* consistent with the returned root.
|
|
1977
2262
|
*/
|
|
1978
2263
|
async getMerkleProofByCommitment(commitmentHex) {
|
|
1979
|
-
const
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
return {
|
|
2264
|
+
const raw = await this.substrate.request(
|
|
2265
|
+
"privacy_getMerkleProofByCommitment",
|
|
2266
|
+
[commitmentHex]
|
|
2267
|
+
);
|
|
2268
|
+
return {
|
|
2269
|
+
path: raw.path,
|
|
2270
|
+
leafIndex: raw.leaf_index,
|
|
2271
|
+
treeDepth: raw.tree_depth,
|
|
2272
|
+
root: raw.root
|
|
2273
|
+
};
|
|
1984
2274
|
}
|
|
1985
2275
|
/** Returns the spend status of a nullifier. */
|
|
1986
2276
|
async getNullifierStatus(nullifier) {
|
|
@@ -1999,6 +2289,7 @@ var PrivacyModule = class {
|
|
|
1999
2289
|
return {
|
|
2000
2290
|
merkleRoot: raw.merkle_root,
|
|
2001
2291
|
commitmentCount: raw.commitment_count,
|
|
2292
|
+
nullifierCount: raw.nullifier_count,
|
|
2002
2293
|
totalBalance: raw.total_balance.toString(),
|
|
2003
2294
|
assetBalances: raw.asset_balances.map(mapAssetBalance),
|
|
2004
2295
|
treeDepth: raw.tree_depth
|
|
@@ -2024,6 +2315,7 @@ var ZkVerifierModule = class {
|
|
|
2024
2315
|
constructor(substrate) {
|
|
2025
2316
|
this.substrate = substrate;
|
|
2026
2317
|
}
|
|
2318
|
+
substrate;
|
|
2027
2319
|
/** Returns basic version info for all registered circuits. */
|
|
2028
2320
|
async getAllCircuitVersions() {
|
|
2029
2321
|
const raw = await this.substrate.request(
|
|
@@ -2042,6 +2334,48 @@ var ZkVerifierModule = class {
|
|
|
2042
2334
|
}
|
|
2043
2335
|
};
|
|
2044
2336
|
|
|
2337
|
+
// src/relayer/RelayerStatusModule.ts
|
|
2338
|
+
var RelayerStatusModule = class {
|
|
2339
|
+
constructor(substrate) {
|
|
2340
|
+
this.substrate = substrate;
|
|
2341
|
+
}
|
|
2342
|
+
substrate;
|
|
2343
|
+
/**
|
|
2344
|
+
* Returns true if the given SS58 address is a registered relayer.
|
|
2345
|
+
*/
|
|
2346
|
+
async isRelayer(ss58Address) {
|
|
2347
|
+
return this.substrate.request("relayer_isRelayer", [ss58Address]);
|
|
2348
|
+
}
|
|
2349
|
+
/**
|
|
2350
|
+
* Returns the pending fees (in planck) for the given account and asset.
|
|
2351
|
+
* The node returns the value as a decimal string to avoid u128 overflow.
|
|
2352
|
+
*/
|
|
2353
|
+
async pendingFees(ss58Address, assetId) {
|
|
2354
|
+
const raw = await this.substrate.request("relayer_pendingFees", [
|
|
2355
|
+
ss58Address,
|
|
2356
|
+
assetId
|
|
2357
|
+
]);
|
|
2358
|
+
return BigInt(raw);
|
|
2359
|
+
}
|
|
2360
|
+
/**
|
|
2361
|
+
* Returns the registered EVM address (0x-prefixed) for the given account,
|
|
2362
|
+
* or null if the account is not a registered relayer.
|
|
2363
|
+
*/
|
|
2364
|
+
async registeredEvmAddress(ss58Address) {
|
|
2365
|
+
return this.substrate.request("relayer_registeredEvmAddress", [ss58Address]);
|
|
2366
|
+
}
|
|
2367
|
+
/**
|
|
2368
|
+
* Convenience method: returns relayer registry info for an account.
|
|
2369
|
+
*/
|
|
2370
|
+
async getRelayerInfo(ss58Address) {
|
|
2371
|
+
const [isRelayer, evmAddress] = await Promise.all([
|
|
2372
|
+
this.isRelayer(ss58Address),
|
|
2373
|
+
this.registeredEvmAddress(ss58Address)
|
|
2374
|
+
]);
|
|
2375
|
+
return { isRelayer, evmAddress };
|
|
2376
|
+
}
|
|
2377
|
+
};
|
|
2378
|
+
|
|
2045
2379
|
// src/precompiles/helpers.ts
|
|
2046
2380
|
var STATIC_TYPES = /* @__PURE__ */ new Set(["uint", "bytes32", "address", "bool"]);
|
|
2047
2381
|
function concat(arrays) {
|
|
@@ -2185,10 +2519,6 @@ function decodeBytes(data, slotOffset = 0) {
|
|
|
2185
2519
|
function decodeString(data, slotOffset = 0) {
|
|
2186
2520
|
return new TextDecoder().decode(decodeBytes(data, slotOffset));
|
|
2187
2521
|
}
|
|
2188
|
-
function hexToBytes(hex) {
|
|
2189
|
-
if (hex === "0x" || hex === "") return new Uint8Array(0);
|
|
2190
|
-
return fromHex(hex.startsWith("0x") ? hex : "0x" + hex);
|
|
2191
|
-
}
|
|
2192
2522
|
|
|
2193
2523
|
// src/precompiles/addresses.ts
|
|
2194
2524
|
var PRECOMPILE_ADDR = {
|
|
@@ -2247,12 +2577,14 @@ var AM_SEL = {
|
|
|
2247
2577
|
SET_ACCOUNT_METADATA: new Uint8Array([119, 108, 249, 255])
|
|
2248
2578
|
};
|
|
2249
2579
|
var SP_SEL = {
|
|
2250
|
-
// shield(uint32,
|
|
2251
|
-
SHIELD: new Uint8Array([
|
|
2252
|
-
// privateTransfer(bytes,bytes32,bytes32[],bytes32[],bytes[])
|
|
2253
|
-
PRIVATE_TRANSFER: new Uint8Array([
|
|
2254
|
-
// unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32)
|
|
2255
|
-
UNSHIELD: new Uint8Array([
|
|
2580
|
+
// shield(uint32,bytes32,bytes) → 0x9feb22ea (payable, amount = msg.value)
|
|
2581
|
+
SHIELD: new Uint8Array([159, 235, 34, 234]),
|
|
2582
|
+
// privateTransfer(bytes,bytes32,bytes32[],bytes32[],bytes[],uint32,uint256) → 0x8c0f5d24
|
|
2583
|
+
PRIVATE_TRANSFER: new Uint8Array([140, 15, 93, 36]),
|
|
2584
|
+
// unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32,uint256,bytes32) → 0xd21d9a79
|
|
2585
|
+
UNSHIELD: new Uint8Array([210, 29, 154, 121]),
|
|
2586
|
+
// claimShieldedFees(bytes32,uint256,uint32,bytes,bytes,bytes) → 0x42e1e74c
|
|
2587
|
+
CLAIM_SHIELDED_FEES: new Uint8Array([66, 225, 231, 76])
|
|
2256
2588
|
};
|
|
2257
2589
|
var KNOWN_PRECOMPILES = {
|
|
2258
2590
|
// ── Ethereum standard (EIP) ─────────────────────────────────────────────
|
|
@@ -2295,9 +2627,10 @@ var KNOWN_PRECOMPILES = {
|
|
|
2295
2627
|
"0x0000000000000000000000000000000000000801": {
|
|
2296
2628
|
name: "ShieldedPool",
|
|
2297
2629
|
functions: {
|
|
2298
|
-
"
|
|
2299
|
-
|
|
2300
|
-
|
|
2630
|
+
"9feb22ea": "shield(uint32,bytes32,bytes)",
|
|
2631
|
+
"8c0f5d24": "privateTransfer(bytes,bytes32,bytes32[],bytes32[],bytes[],uint32,uint256)",
|
|
2632
|
+
d21d9a79: "unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32,uint256,bytes32)",
|
|
2633
|
+
"42e1e74c": "claimShieldedFees(bytes32,uint256,uint32,bytes,bytes,bytes)"
|
|
2301
2634
|
}
|
|
2302
2635
|
}
|
|
2303
2636
|
};
|
|
@@ -2311,43 +2644,58 @@ var ShieldedPoolPrecompile = class {
|
|
|
2311
2644
|
constructor(evm) {
|
|
2312
2645
|
this.evm = evm;
|
|
2313
2646
|
}
|
|
2647
|
+
evm;
|
|
2314
2648
|
addr = PRECOMPILE_ADDR.SHIELDED_POOL;
|
|
2315
2649
|
// ─── shield ────────────────────────────────────────────────────────────────
|
|
2316
2650
|
/**
|
|
2317
|
-
* Returns the ABI-encoded calldata for `shield(uint32,
|
|
2318
|
-
*
|
|
2651
|
+
* Returns the ABI-encoded calldata for `shield(uint32, bytes32, bytes)`.
|
|
2652
|
+
* The token amount must be sent as `msg.value` (the `value` field of the EVM
|
|
2653
|
+
* transaction) — this is what MetaMask and other wallets display to the user.
|
|
2319
2654
|
*/
|
|
2320
2655
|
buildShieldCalldata(params) {
|
|
2321
|
-
|
|
2656
|
+
EncryptedMemo.validate(params.encryptedMemo, "buildShieldCalldata.encryptedMemo");
|
|
2322
2657
|
const commitment = fromHex(params.commitment);
|
|
2323
2658
|
return encodeHex(
|
|
2324
2659
|
SP_SEL.SHIELD,
|
|
2325
2660
|
{ type: "uint", value: BigInt(params.assetId) },
|
|
2326
|
-
{ type: "uint", value: params.amount },
|
|
2327
2661
|
{ type: "bytes32", value: commitment },
|
|
2328
|
-
{ type: "bytes", value:
|
|
2662
|
+
{ type: "bytes", value: params.encryptedMemo }
|
|
2329
2663
|
);
|
|
2330
2664
|
}
|
|
2331
2665
|
/**
|
|
2332
|
-
* Deposits tokens into the shielded pool from
|
|
2666
|
+
* Deposits tokens into the shielded pool from a payable EVM transaction.
|
|
2333
2667
|
*
|
|
2334
|
-
* The
|
|
2335
|
-
*
|
|
2668
|
+
* The token amount is sent as `msg.value` so EVM wallets (MetaMask, etc.) display
|
|
2669
|
+
* the correct amount on the confirmation screen. The precompile dispatches
|
|
2670
|
+
* `shieldedPool.shield` with its own address as origin, so the funds flow:
|
|
2671
|
+
* caller → precompile (via msg.value, handled by EVM)
|
|
2672
|
+
* precompile → pool (via pallet transfer)
|
|
2673
|
+
* This avoids double-deduction while keeping the displayed amount accurate.
|
|
2336
2674
|
*
|
|
2337
2675
|
* Extrinsic: `shieldedPool.shield(assetId, amount, commitment, encryptedMemo)`
|
|
2338
2676
|
*/
|
|
2339
2677
|
async shield(params, signer) {
|
|
2340
|
-
return signer({
|
|
2678
|
+
return signer({
|
|
2679
|
+
to: this.addr,
|
|
2680
|
+
data: this.buildShieldCalldata(params),
|
|
2681
|
+
value: params.amount
|
|
2682
|
+
});
|
|
2341
2683
|
}
|
|
2342
2684
|
// ─── privateTransfer ───────────────────────────────────────────────────────
|
|
2343
2685
|
/**
|
|
2344
2686
|
* Returns the ABI-encoded calldata for
|
|
2345
|
-
* `privateTransfer(bytes, bytes32, bytes32[], bytes32[], bytes[])`.
|
|
2687
|
+
* `privateTransfer(bytes, bytes32, bytes32[], bytes32[], bytes[], uint32, uint256)`.
|
|
2346
2688
|
*/
|
|
2347
2689
|
buildPrivateTransferCalldata(params) {
|
|
2348
2690
|
const nullifiers = params.inputs.map((i) => fromHex(i.nullifier));
|
|
2349
2691
|
const commitments = params.outputs.map((o) => fromHex(o.commitment));
|
|
2350
|
-
const memos = params.outputs.map((o) =>
|
|
2692
|
+
const memos = params.outputs.map((o, i) => {
|
|
2693
|
+
EncryptedMemo.validate(
|
|
2694
|
+
o.encryptedMemo,
|
|
2695
|
+
`buildPrivateTransferCalldata.outputs[${i}].encryptedMemo`
|
|
2696
|
+
);
|
|
2697
|
+
return o.encryptedMemo;
|
|
2698
|
+
});
|
|
2351
2699
|
const root = fromHex(params.merkleRoot);
|
|
2352
2700
|
return encodeHex(
|
|
2353
2701
|
SP_SEL.PRIVATE_TRANSFER,
|
|
@@ -2355,7 +2703,9 @@ var ShieldedPoolPrecompile = class {
|
|
|
2355
2703
|
{ type: "bytes32", value: root },
|
|
2356
2704
|
{ type: "bytes32[]", value: nullifiers },
|
|
2357
2705
|
{ type: "bytes32[]", value: commitments },
|
|
2358
|
-
{ type: "bytes[]", value: memos }
|
|
2706
|
+
{ type: "bytes[]", value: memos },
|
|
2707
|
+
{ type: "uint", value: BigInt(params.assetId) },
|
|
2708
|
+
{ type: "uint", value: params.fee ?? 0n }
|
|
2359
2709
|
);
|
|
2360
2710
|
}
|
|
2361
2711
|
/**
|
|
@@ -2383,6 +2733,9 @@ var ShieldedPoolPrecompile = class {
|
|
|
2383
2733
|
const recipientBytes = fromHex(
|
|
2384
2734
|
"0x" + (recipientRaw.length === 64 ? recipientRaw : recipientRaw.padEnd(64, "0"))
|
|
2385
2735
|
);
|
|
2736
|
+
const changeCommitmentHex = params.changeCommitment ?? "0x" + "00".repeat(32);
|
|
2737
|
+
const changeCommitment = fromHex(changeCommitmentHex);
|
|
2738
|
+
const changeEncryptedMemo = params.changeEncryptedMemo ?? new Uint8Array();
|
|
2386
2739
|
return encodeHex(
|
|
2387
2740
|
SP_SEL.UNSHIELD,
|
|
2388
2741
|
{ type: "bytes", value: proof },
|
|
@@ -2390,7 +2743,10 @@ var ShieldedPoolPrecompile = class {
|
|
|
2390
2743
|
{ type: "bytes32", value: nullifier },
|
|
2391
2744
|
{ type: "uint", value: BigInt(params.assetId) },
|
|
2392
2745
|
{ type: "uint", value: params.amount },
|
|
2393
|
-
{ type: "bytes32", value: recipientBytes }
|
|
2746
|
+
{ type: "bytes32", value: recipientBytes },
|
|
2747
|
+
{ type: "uint", value: params.fee ?? 0n },
|
|
2748
|
+
{ type: "bytes32", value: changeCommitment },
|
|
2749
|
+
{ type: "bytes", value: changeEncryptedMemo }
|
|
2394
2750
|
);
|
|
2395
2751
|
}
|
|
2396
2752
|
/**
|
|
@@ -2437,6 +2793,78 @@ var ShieldedPoolPrecompile = class {
|
|
|
2437
2793
|
data: this.buildUnshieldCalldata(params)
|
|
2438
2794
|
});
|
|
2439
2795
|
}
|
|
2796
|
+
// ─── claimShieldedFees ───────────────────────────────────────────────────────────────────
|
|
2797
|
+
/**
|
|
2798
|
+
* Returns the ABI-encoded calldata for
|
|
2799
|
+
* `claimShieldedFees(bytes32,uint256,uint32,bytes,bytes,bytes)`.
|
|
2800
|
+
*
|
|
2801
|
+
* ABI layout (params after selector):
|
|
2802
|
+
* - `commitment` — bytes32 (fixed)
|
|
2803
|
+
* - `amount` — uint256 (fixed)
|
|
2804
|
+
* - `asset_id` — uint32 (fixed, right-aligned)
|
|
2805
|
+
* - `memo` — bytes (dynamic)
|
|
2806
|
+
* - `proof` — bytes (dynamic, 128 bytes Groth16)
|
|
2807
|
+
* - `publicSignals` — bytes (dynamic, 76 bytes)
|
|
2808
|
+
*
|
|
2809
|
+
* The validator identity is derived from `msg.sender` in the precompile —
|
|
2810
|
+
* do NOT include it in the calldata.
|
|
2811
|
+
*/
|
|
2812
|
+
buildClaimShieldedFeesCalldata(params) {
|
|
2813
|
+
EncryptedMemo.validate(
|
|
2814
|
+
params.encryptedMemo,
|
|
2815
|
+
"buildClaimShieldedFeesCalldata.encryptedMemo"
|
|
2816
|
+
);
|
|
2817
|
+
if (params.proof.length === 0) {
|
|
2818
|
+
throw new Error("claimShieldedFees: proof must not be empty");
|
|
2819
|
+
}
|
|
2820
|
+
if (params.publicSignals.length !== 76) {
|
|
2821
|
+
throw new Error(
|
|
2822
|
+
`claimShieldedFees: publicSignals must be 76 bytes, got ${params.publicSignals.length}`
|
|
2823
|
+
);
|
|
2824
|
+
}
|
|
2825
|
+
const commitment = fromHex(params.commitment);
|
|
2826
|
+
return encodeHex(
|
|
2827
|
+
SP_SEL.CLAIM_SHIELDED_FEES,
|
|
2828
|
+
{ type: "bytes32", value: commitment },
|
|
2829
|
+
{ type: "uint", value: params.amount },
|
|
2830
|
+
{ type: "uint", value: BigInt(params.assetId) },
|
|
2831
|
+
{ type: "bytes", value: params.encryptedMemo },
|
|
2832
|
+
{ type: "bytes", value: params.proof },
|
|
2833
|
+
{ type: "bytes", value: params.publicSignals }
|
|
2834
|
+
);
|
|
2835
|
+
}
|
|
2836
|
+
/**
|
|
2837
|
+
* Claims accumulated relay fees as a private shielded note.
|
|
2838
|
+
*
|
|
2839
|
+
* This extrinsic is for **validators/relayers** who have accrued fees in
|
|
2840
|
+
* `pallet-relayer` and want to receive them privately inside the shielded pool
|
|
2841
|
+
* instead of as a public balance credit.
|
|
2842
|
+
*
|
|
2843
|
+
* The ZK `value_proof` binds `commitment` to `(amount, assetId, ownerPk, blinding)`
|
|
2844
|
+
* so the runtime can verify the note encodes exactly the claimed fee amount,
|
|
2845
|
+
* preventing a malicious relayer from inflating the withdrawal.
|
|
2846
|
+
*
|
|
2847
|
+
* The `msg.sender` EVM address is used as the validator identity; it must match
|
|
2848
|
+
* the address that has pending relay fees in `pallet-relayer`.
|
|
2849
|
+
*
|
|
2850
|
+
* Extrinsic: `shieldedPool.claim_shielded_fees(commitment, amount, assetId, memo, proof, publicSignals)`
|
|
2851
|
+
*/
|
|
2852
|
+
async claimShieldedFees(params, signer) {
|
|
2853
|
+
return signer({
|
|
2854
|
+
to: this.addr,
|
|
2855
|
+
data: this.buildClaimShieldedFeesCalldata(params)
|
|
2856
|
+
});
|
|
2857
|
+
}
|
|
2858
|
+
/**
|
|
2859
|
+
* Estimates the EVM gas for a `claimShieldedFees` call.
|
|
2860
|
+
*/
|
|
2861
|
+
async estimateClaimShieldedFeesGas(params, from) {
|
|
2862
|
+
return this.evm.estimateGas({
|
|
2863
|
+
from,
|
|
2864
|
+
to: this.addr,
|
|
2865
|
+
data: this.buildClaimShieldedFeesCalldata(params)
|
|
2866
|
+
});
|
|
2867
|
+
}
|
|
2440
2868
|
};
|
|
2441
2869
|
|
|
2442
2870
|
// src/precompiles/AccountMappingPrecompile.ts
|
|
@@ -2444,6 +2872,7 @@ var AccountMappingPrecompile = class {
|
|
|
2444
2872
|
constructor(evm) {
|
|
2445
2873
|
this.evm = evm;
|
|
2446
2874
|
}
|
|
2875
|
+
evm;
|
|
2447
2876
|
addr = PRECOMPILE_ADDR.ACCOUNT_MAPPING;
|
|
2448
2877
|
// ─── Read-only ─────────────────────────────────────────────────────────────
|
|
2449
2878
|
/**
|
|
@@ -2455,7 +2884,7 @@ var AccountMappingPrecompile = class {
|
|
|
2455
2884
|
async resolveAlias(alias) {
|
|
2456
2885
|
try {
|
|
2457
2886
|
const data = encodeHex(AM_SEL.RESOLVE_ALIAS, { type: "string", value: alias });
|
|
2458
|
-
const raw =
|
|
2887
|
+
const raw = fromHex(await this.evm.call(this.addr, data));
|
|
2459
2888
|
if (raw.length < 64) return null;
|
|
2460
2889
|
const owner = decodeAddress2(raw, 0);
|
|
2461
2890
|
const evm = decodeAddress2(raw, 32);
|
|
@@ -2478,7 +2907,7 @@ var AccountMappingPrecompile = class {
|
|
|
2478
2907
|
type: "address",
|
|
2479
2908
|
value: normalizeEvmAddress(evmAddress)
|
|
2480
2909
|
});
|
|
2481
|
-
const raw =
|
|
2910
|
+
const raw = fromHex(await this.evm.call(this.addr, data));
|
|
2482
2911
|
if (raw.length === 0) return null;
|
|
2483
2912
|
const alias = decodeString(raw, 0);
|
|
2484
2913
|
return alias.length > 0 ? alias : null;
|
|
@@ -2498,7 +2927,7 @@ var AccountMappingPrecompile = class {
|
|
|
2498
2927
|
{ type: "string", value: alias },
|
|
2499
2928
|
{ type: "bytes32", value: commitmentBytes }
|
|
2500
2929
|
);
|
|
2501
|
-
const raw =
|
|
2930
|
+
const raw = fromHex(await this.evm.call(this.addr, data));
|
|
2502
2931
|
if (raw.length < 32) return false;
|
|
2503
2932
|
return decodeBool(raw, 0);
|
|
2504
2933
|
} catch {
|
|
@@ -2703,6 +3132,7 @@ var CryptoPrecompiles = class {
|
|
|
2703
3132
|
constructor(evm) {
|
|
2704
3133
|
this.evm = evm;
|
|
2705
3134
|
}
|
|
3135
|
+
evm;
|
|
2706
3136
|
// ─── ECRecover (0x0001) ───────────────────────────────────────────────────
|
|
2707
3137
|
/**
|
|
2708
3138
|
* Recovers the Ethereum address from an ECDSA signature.
|
|
@@ -2718,7 +3148,7 @@ var CryptoPrecompiles = class {
|
|
|
2718
3148
|
input[63] = v;
|
|
2719
3149
|
input.set(r.slice(0, 32), 64);
|
|
2720
3150
|
input.set(s.slice(0, 32), 96);
|
|
2721
|
-
const raw =
|
|
3151
|
+
const raw = fromHex(await this.evm.call(PRECOMPILE_ADDR.EC_RECOVER, toHex(input)));
|
|
2722
3152
|
if (raw.length < 32) return "0x" + "00".repeat(20);
|
|
2723
3153
|
return "0x" + toHex(raw.slice(12, 32)).slice(2);
|
|
2724
3154
|
}
|
|
@@ -2734,7 +3164,7 @@ var CryptoPrecompiles = class {
|
|
|
2734
3164
|
input[63] = v;
|
|
2735
3165
|
input.set(r.slice(0, 32), 64);
|
|
2736
3166
|
input.set(s.slice(0, 32), 96);
|
|
2737
|
-
return
|
|
3167
|
+
return fromHex(await this.evm.call(PRECOMPILE_ADDR.EC_RECOVER_PUBKEY, toHex(input)));
|
|
2738
3168
|
}
|
|
2739
3169
|
// ─── SHA-256 (0x0002) ─────────────────────────────────────────────────────
|
|
2740
3170
|
/**
|
|
@@ -2742,7 +3172,7 @@ var CryptoPrecompiles = class {
|
|
|
2742
3172
|
* Returns a 32-byte digest.
|
|
2743
3173
|
*/
|
|
2744
3174
|
async sha256(data) {
|
|
2745
|
-
return
|
|
3175
|
+
return fromHex(await this.evm.call(PRECOMPILE_ADDR.SHA256, toHex(data)));
|
|
2746
3176
|
}
|
|
2747
3177
|
// ─── RIPEMD-160 (0x0003) ──────────────────────────────────────────────────
|
|
2748
3178
|
/**
|
|
@@ -2750,7 +3180,7 @@ var CryptoPrecompiles = class {
|
|
|
2750
3180
|
* Returns the 20-byte digest right-padded to 32 bytes (standard ABI output).
|
|
2751
3181
|
*/
|
|
2752
3182
|
async ripemd160(data) {
|
|
2753
|
-
const raw =
|
|
3183
|
+
const raw = fromHex(await this.evm.call(PRECOMPILE_ADDR.RIPEMD160, toHex(data)));
|
|
2754
3184
|
return raw.length >= 32 ? raw.slice(12, 32) : raw;
|
|
2755
3185
|
}
|
|
2756
3186
|
// ─── Identity (0x0004) ────────────────────────────────────────────────────
|
|
@@ -2759,7 +3189,7 @@ var CryptoPrecompiles = class {
|
|
|
2759
3189
|
* Mainly useful for gas benchmarking.
|
|
2760
3190
|
*/
|
|
2761
3191
|
async identity(data) {
|
|
2762
|
-
return
|
|
3192
|
+
return fromHex(await this.evm.call(PRECOMPILE_ADDR.IDENTITY, toHex(data)));
|
|
2763
3193
|
}
|
|
2764
3194
|
// ─── SHA3-FIPS-256 / Keccak-256 (0x0400) ─────────────────────────────────
|
|
2765
3195
|
/**
|
|
@@ -2767,7 +3197,7 @@ var CryptoPrecompiles = class {
|
|
|
2767
3197
|
* Returns a 32-byte digest.
|
|
2768
3198
|
*/
|
|
2769
3199
|
async keccak256(data) {
|
|
2770
|
-
return
|
|
3200
|
+
return fromHex(await this.evm.call(PRECOMPILE_ADDR.SHA3_FIPS256, toHex(data)));
|
|
2771
3201
|
}
|
|
2772
3202
|
// ─── Curve25519 / Ristretto (0x0402, 0x0403) ─────────────────────────────
|
|
2773
3203
|
/**
|
|
@@ -2790,7 +3220,7 @@ var CryptoPrecompiles = class {
|
|
|
2790
3220
|
}
|
|
2791
3221
|
input.set(pt, i * 32);
|
|
2792
3222
|
}
|
|
2793
|
-
return
|
|
3223
|
+
return fromHex(await this.evm.call(PRECOMPILE_ADDR.CURVE25519_ADD, toHex(input)));
|
|
2794
3224
|
}
|
|
2795
3225
|
/**
|
|
2796
3226
|
* Multiplies a Ristretto compressed point by a scalar via EVM precompile.
|
|
@@ -2806,39 +3236,44 @@ var CryptoPrecompiles = class {
|
|
|
2806
3236
|
const input = new Uint8Array(64);
|
|
2807
3237
|
input.set(scalar, 0);
|
|
2808
3238
|
input.set(point, 32);
|
|
2809
|
-
return
|
|
3239
|
+
return fromHex(await this.evm.call(PRECOMPILE_ADDR.CURVE25519_SCALAR_MUL, toHex(input)));
|
|
2810
3240
|
}
|
|
2811
3241
|
};
|
|
2812
3242
|
|
|
2813
3243
|
// src/client/OrbinumClient.ts
|
|
2814
3244
|
var OrbinumClient = class _OrbinumClient {
|
|
2815
|
-
/** Raw
|
|
3245
|
+
/** Raw Substrate WebSocket connection — use for custom RPC calls or low-level access. */
|
|
2816
3246
|
substrate;
|
|
2817
|
-
/** Raw
|
|
3247
|
+
/** Raw EVM HTTP JSON-RPC client. `null` when `evmRpc` is not configured. */
|
|
2818
3248
|
evm;
|
|
2819
3249
|
/**
|
|
2820
|
-
* High-level EVM block and transaction explorer
|
|
3250
|
+
* High-level EVM block and transaction explorer.
|
|
2821
3251
|
* Provides enriched queries for blocks, transactions, addresses, and token transfers.
|
|
3252
|
+
* `null` when `evmRpc` is not configured.
|
|
2822
3253
|
*/
|
|
2823
3254
|
evmExplorer;
|
|
2824
3255
|
/**
|
|
2825
|
-
* HTTP client for the Orbinum indexer REST API
|
|
3256
|
+
* HTTP client for the Orbinum indexer REST API.
|
|
2826
3257
|
* Provides paginated access to indexed blocks, extrinsics, shielded events, and nullifiers.
|
|
3258
|
+
* `null` when `indexerUrl` is not configured.
|
|
2827
3259
|
*/
|
|
2828
3260
|
indexer;
|
|
2829
|
-
/** Shielded-pool
|
|
3261
|
+
/** Shielded-pool extrinsics and Merkle tree queries (`shield`, `unshield`, `privateTransfer`, …). */
|
|
2830
3262
|
shieldedPool;
|
|
2831
|
-
/** Account
|
|
3263
|
+
/** Account-mapping extrinsics: aliases, chain links, metadata, marketplace, and identity. */
|
|
2832
3264
|
accountMapping;
|
|
2833
|
-
/** Typed access to
|
|
3265
|
+
/** Typed access to `privacy_*` custom RPC endpoints. */
|
|
2834
3266
|
privacy;
|
|
2835
|
-
/** Typed access to zkVerifier_
|
|
3267
|
+
/** Typed access to `zkVerifier_*` custom RPC endpoints. */
|
|
2836
3268
|
zkVerifier;
|
|
3269
|
+
/** Typed access to `relayer_*` RPC endpoints (registry lookup and pending fee queries). */
|
|
3270
|
+
relayerStatus;
|
|
2837
3271
|
/**
|
|
2838
|
-
*
|
|
2839
|
-
*
|
|
3272
|
+
* Precompile modules for interacting with Orbinum contracts from an EVM wallet.
|
|
3273
|
+
* `null` when `evmRpc` is not configured. Methods on each sub-module throw if `evm` is `null`.
|
|
2840
3274
|
*/
|
|
2841
3275
|
precompiles;
|
|
3276
|
+
/** @internal Use `OrbinumClient.connect()` to obtain an instance. */
|
|
2842
3277
|
constructor(substrate, evm, indexer) {
|
|
2843
3278
|
this.substrate = substrate;
|
|
2844
3279
|
this.evm = evm;
|
|
@@ -2848,6 +3283,7 @@ var OrbinumClient = class _OrbinumClient {
|
|
|
2848
3283
|
this.accountMapping = new AccountMappingModule(substrate);
|
|
2849
3284
|
this.privacy = new PrivacyModule(substrate);
|
|
2850
3285
|
this.zkVerifier = new ZkVerifierModule(substrate);
|
|
3286
|
+
this.relayerStatus = new RelayerStatusModule(substrate);
|
|
2851
3287
|
this.precompiles = evm ? {
|
|
2852
3288
|
shieldedPool: new ShieldedPoolPrecompile(evm),
|
|
2853
3289
|
accountMapping: new AccountMappingPrecompile(evm),
|
|
@@ -2855,8 +3291,10 @@ var OrbinumClient = class _OrbinumClient {
|
|
|
2855
3291
|
} : null;
|
|
2856
3292
|
}
|
|
2857
3293
|
/**
|
|
2858
|
-
*
|
|
2859
|
-
*
|
|
3294
|
+
* Creates and connects an `OrbinumClient` from the given configuration.
|
|
3295
|
+
*
|
|
3296
|
+
* Establishes the Substrate WebSocket connection and, if configured, instantiates
|
|
3297
|
+
* the EVM and indexer clients. Throws if the node is unreachable within `connectTimeoutMs`.
|
|
2860
3298
|
*/
|
|
2861
3299
|
static async connect(config) {
|
|
2862
3300
|
const substrate = await SubstrateClient.connect(
|
|
@@ -2867,7 +3305,7 @@ var OrbinumClient = class _OrbinumClient {
|
|
|
2867
3305
|
const indexer = config.indexerUrl ? new IndexerClient({ baseUrl: config.indexerUrl }) : null;
|
|
2868
3306
|
return new _OrbinumClient(substrate, evm, indexer);
|
|
2869
3307
|
}
|
|
2870
|
-
/** Closes the WebSocket connection
|
|
3308
|
+
/** Closes the underlying Substrate WebSocket connection and releases all resources. */
|
|
2871
3309
|
destroy() {
|
|
2872
3310
|
this.substrate.destroy();
|
|
2873
3311
|
}
|
|
@@ -2896,6 +3334,7 @@ var OrbinumClientProvider = class {
|
|
|
2896
3334
|
_reconnectAttempt = 0;
|
|
2897
3335
|
// ─── Events ─────────────────────────────────────────────────────────────
|
|
2898
3336
|
_listeners = /* @__PURE__ */ new Set();
|
|
3337
|
+
/** Creates a new provider with the given configuration. Does not connect automatically — call `connect()` to initiate. */
|
|
2899
3338
|
constructor(config) {
|
|
2900
3339
|
this.config = config;
|
|
2901
3340
|
this.connectTimeoutMs = config.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
@@ -2905,9 +3344,11 @@ var OrbinumClientProvider = class {
|
|
|
2905
3344
|
this.reconnectMaxMs = config.reconnectMaxMs ?? DEFAULT_RECONNECT_MAX_MS;
|
|
2906
3345
|
}
|
|
2907
3346
|
// ─── Status ─────────────────────────────────────────────────────────────
|
|
3347
|
+
/** Current connection status. Reflects the last state set by the provider internals. */
|
|
2908
3348
|
get status() {
|
|
2909
3349
|
return this._status;
|
|
2910
3350
|
}
|
|
3351
|
+
/** Updates internal status and notifies all registered listeners. Swallows listener exceptions to avoid cascading failures. */
|
|
2911
3352
|
setStatus(status, error) {
|
|
2912
3353
|
this._status = status;
|
|
2913
3354
|
const event = { status, ...error ? { error } : {} };
|
|
@@ -2918,6 +3359,10 @@ var OrbinumClientProvider = class {
|
|
|
2918
3359
|
}
|
|
2919
3360
|
});
|
|
2920
3361
|
}
|
|
3362
|
+
/**
|
|
3363
|
+
* Registers a listener that is called on every status transition.
|
|
3364
|
+
* Returns an unsubscribe function — call it to stop receiving events.
|
|
3365
|
+
*/
|
|
2921
3366
|
onStatusChange(listener) {
|
|
2922
3367
|
this._listeners.add(listener);
|
|
2923
3368
|
return () => {
|
|
@@ -2925,10 +3370,18 @@ var OrbinumClientProvider = class {
|
|
|
2925
3370
|
};
|
|
2926
3371
|
}
|
|
2927
3372
|
// ─── Lifecycle ──────────────────────────────────────────────────────────
|
|
3373
|
+
/**
|
|
3374
|
+
* Initiates the first connection attempt. No-op if the provider is not in `'idle'` state.
|
|
3375
|
+
* Call this once after constructing the provider.
|
|
3376
|
+
*/
|
|
2928
3377
|
connect() {
|
|
2929
3378
|
if (this._status !== "idle") return;
|
|
2930
3379
|
this.startConnectAttempt();
|
|
2931
3380
|
}
|
|
3381
|
+
/**
|
|
3382
|
+
* Tears down the active client and any pending reconnect timers,
|
|
3383
|
+
* then resets the provider back to `'idle'` so `connect()` can be called again.
|
|
3384
|
+
*/
|
|
2932
3385
|
reset() {
|
|
2933
3386
|
this.cancelReconnect();
|
|
2934
3387
|
this.teardownClient();
|
|
@@ -2936,6 +3389,7 @@ var OrbinumClientProvider = class {
|
|
|
2936
3389
|
this.setStatus("idle");
|
|
2937
3390
|
}
|
|
2938
3391
|
// ─── Internal connection flow ───────────────────────────────────────────
|
|
3392
|
+
/** Transitions to `'connecting'`, kicks off `attemptConnect`, and schedules a reconnect if it fails. */
|
|
2939
3393
|
startConnectAttempt() {
|
|
2940
3394
|
this.setStatus("connecting");
|
|
2941
3395
|
this._connectingPromise = this.attemptConnect();
|
|
@@ -2943,6 +3397,11 @@ var OrbinumClientProvider = class {
|
|
|
2943
3397
|
if (this._status !== "idle") this.scheduleReconnect();
|
|
2944
3398
|
});
|
|
2945
3399
|
}
|
|
3400
|
+
/**
|
|
3401
|
+
* Performs a single connection attempt race against `connectTimeoutMs`.
|
|
3402
|
+
* On success: stores the client, starts the heartbeat, and returns it.
|
|
3403
|
+
* On failure: destroys any orphaned client and transitions to `'disconnected'`.
|
|
3404
|
+
*/
|
|
2946
3405
|
async attemptConnect() {
|
|
2947
3406
|
let timeoutId = null;
|
|
2948
3407
|
let orphanClient = null;
|
|
@@ -2990,6 +3449,7 @@ var OrbinumClientProvider = class {
|
|
|
2990
3449
|
}
|
|
2991
3450
|
}
|
|
2992
3451
|
// ─── Heartbeat ──────────────────────────────────────────────────────────
|
|
3452
|
+
/** Starts the periodic heartbeat loop. Replaces any existing timer. */
|
|
2993
3453
|
startHeartbeat() {
|
|
2994
3454
|
this.stopHeartbeat();
|
|
2995
3455
|
this._heartbeatTimer = setInterval(async () => {
|
|
@@ -3002,12 +3462,17 @@ var OrbinumClientProvider = class {
|
|
|
3002
3462
|
}
|
|
3003
3463
|
}, this.heartbeatIntervalMs);
|
|
3004
3464
|
}
|
|
3465
|
+
/** Clears the heartbeat interval timer if active. */
|
|
3005
3466
|
stopHeartbeat() {
|
|
3006
3467
|
if (this._heartbeatTimer) {
|
|
3007
3468
|
clearInterval(this._heartbeatTimer);
|
|
3008
3469
|
this._heartbeatTimer = null;
|
|
3009
3470
|
}
|
|
3010
3471
|
}
|
|
3472
|
+
/**
|
|
3473
|
+
* Sends a `system_health` RPC ping and waits up to `heartbeatTimeoutMs`.
|
|
3474
|
+
* Returns `true` if the node responds in time, `false` otherwise.
|
|
3475
|
+
*/
|
|
3011
3476
|
async probe() {
|
|
3012
3477
|
if (!this._orbinumClient) return false;
|
|
3013
3478
|
try {
|
|
@@ -3023,6 +3488,10 @@ var OrbinumClientProvider = class {
|
|
|
3023
3488
|
}
|
|
3024
3489
|
}
|
|
3025
3490
|
// ─── Reconnection ───────────────────────────────────────────────────────
|
|
3491
|
+
/**
|
|
3492
|
+
* Schedules the next connection attempt using exponential backoff
|
|
3493
|
+
* (capped at `reconnectMaxMs`), then transitions to `'reconnecting'`.
|
|
3494
|
+
*/
|
|
3026
3495
|
scheduleReconnect() {
|
|
3027
3496
|
if (this._reconnectTimer) clearTimeout(this._reconnectTimer);
|
|
3028
3497
|
const delay = Math.min(
|
|
@@ -3036,6 +3505,7 @@ var OrbinumClientProvider = class {
|
|
|
3036
3505
|
if (this._status !== "idle") this.startConnectAttempt();
|
|
3037
3506
|
}, delay);
|
|
3038
3507
|
}
|
|
3508
|
+
/** Clears any pending reconnect timer without triggering a new attempt. */
|
|
3039
3509
|
cancelReconnect() {
|
|
3040
3510
|
if (this._reconnectTimer) {
|
|
3041
3511
|
clearTimeout(this._reconnectTimer);
|
|
@@ -3043,6 +3513,7 @@ var OrbinumClientProvider = class {
|
|
|
3043
3513
|
}
|
|
3044
3514
|
}
|
|
3045
3515
|
// ─── Client teardown ────────────────────────────────────────────────────
|
|
3516
|
+
/** Stops the heartbeat, destroys the active client, and clears all in-progress promises. */
|
|
3046
3517
|
teardownClient() {
|
|
3047
3518
|
this.stopHeartbeat();
|
|
3048
3519
|
try {
|
|
@@ -3053,11 +3524,19 @@ var OrbinumClientProvider = class {
|
|
|
3053
3524
|
this._connectingPromise = null;
|
|
3054
3525
|
}
|
|
3055
3526
|
// ─── Client access ──────────────────────────────────────────────────────
|
|
3527
|
+
/**
|
|
3528
|
+
* Returns the active `OrbinumClient`, or awaits the in-progress connection attempt.
|
|
3529
|
+
* Throws if the provider is `'idle'`, `'disconnected'`, or `'reconnecting'`.
|
|
3530
|
+
*/
|
|
3056
3531
|
async getOrbinumClient() {
|
|
3057
3532
|
if (this._orbinumClient) return this._orbinumClient;
|
|
3058
3533
|
if (this._connectingPromise) return this._connectingPromise;
|
|
3059
3534
|
throw new Error(`OrbinumClientProvider: cannot get client in status '${this._status}'`);
|
|
3060
3535
|
}
|
|
3536
|
+
/**
|
|
3537
|
+
* Same as `getOrbinumClient()` but returns `null` instead of throwing.
|
|
3538
|
+
* Useful in contexts where a missing client is an acceptable no-op.
|
|
3539
|
+
*/
|
|
3061
3540
|
async tryGetOrbinumClient() {
|
|
3062
3541
|
try {
|
|
3063
3542
|
return await this.getOrbinumClient();
|
|
@@ -3066,15 +3545,28 @@ var OrbinumClientProvider = class {
|
|
|
3066
3545
|
}
|
|
3067
3546
|
}
|
|
3068
3547
|
// ─── Convenience RPC helpers ────────────────────────────────────────────
|
|
3548
|
+
/**
|
|
3549
|
+
* Sends a single Substrate JSON-RPC request and returns the typed result.
|
|
3550
|
+
* Waits for the client to be ready before dispatching.
|
|
3551
|
+
*/
|
|
3069
3552
|
async rpcSend(method, params = []) {
|
|
3070
3553
|
const client = await this.getOrbinumClient();
|
|
3071
3554
|
return client.substrate.request(method, params);
|
|
3072
3555
|
}
|
|
3556
|
+
/**
|
|
3557
|
+
* Sends a single EVM JSON-RPC request and returns the typed result.
|
|
3558
|
+
* Throws if `evmRpc` was not configured.
|
|
3559
|
+
*/
|
|
3073
3560
|
async evmRpc(method, params = []) {
|
|
3074
3561
|
const client = await this.getOrbinumClient();
|
|
3075
3562
|
if (!client.evm) throw new Error("EVM RPC not configured");
|
|
3076
3563
|
return client.evm.request(method, params);
|
|
3077
3564
|
}
|
|
3565
|
+
/**
|
|
3566
|
+
* Sends multiple EVM JSON-RPC calls as a single batch request.
|
|
3567
|
+
* Returns a tuple of typed results in the same order as `calls`.
|
|
3568
|
+
* Throws if `evmRpc` was not configured.
|
|
3569
|
+
*/
|
|
3078
3570
|
async evmRpcBatch(calls) {
|
|
3079
3571
|
const client = await this.getOrbinumClient();
|
|
3080
3572
|
if (!client.evm) throw new Error("EVM RPC not configured");
|
|
@@ -3082,102 +3574,476 @@ var OrbinumClientProvider = class {
|
|
|
3082
3574
|
}
|
|
3083
3575
|
};
|
|
3084
3576
|
|
|
3085
|
-
// src/
|
|
3577
|
+
// src/utils/stealth.ts
|
|
3578
|
+
import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
|
|
3579
|
+
import { hkdf } from "@noble/hashes/hkdf.js";
|
|
3580
|
+
import { mulPointEscalar as mulPointEscalar2, Base8 as Base82, addPoint } from "@zk-kit/baby-jubjub";
|
|
3581
|
+
var STEALTH_INFO = new TextEncoder().encode("orbinum-stealth-v1");
|
|
3582
|
+
function deriveStealthScalar(sharedSecret, ownerPkBigint) {
|
|
3583
|
+
const salt = bigintTo32Le(ownerPkBigint);
|
|
3584
|
+
const stealthBytes = hkdf(sha2562, sharedSecret, salt, STEALTH_INFO, 32);
|
|
3585
|
+
return bytesToBigintLE(stealthBytes) % BABYJUB_SUBORDER || 1n;
|
|
3586
|
+
}
|
|
3587
|
+
function deriveStealthOwnerPk(sharedSecret, ownerPkBigint, ownerPkPoint) {
|
|
3588
|
+
const stealthScalar = deriveStealthScalar(sharedSecret, ownerPkBigint);
|
|
3589
|
+
const stealthPt = addPoint(mulPointEscalar2(Base82, stealthScalar), ownerPkPoint);
|
|
3590
|
+
return stealthPt[0];
|
|
3591
|
+
}
|
|
3592
|
+
function deriveStealthSk(sharedSecret, ownerPkBigint, spendingKey) {
|
|
3593
|
+
const stealthScalar = deriveStealthScalar(sharedSecret, ownerPkBigint);
|
|
3594
|
+
return (stealthScalar + spendingKey) % BABYJUB_SUBORDER || 1n;
|
|
3595
|
+
}
|
|
3596
|
+
|
|
3597
|
+
// src/utils/bjj.ts
|
|
3598
|
+
import { mulPointEscalar as mulPointEscalar3 } from "@zk-kit/baby-jubjub";
|
|
3599
|
+
var BJJ_A = 168700n;
|
|
3600
|
+
var BJJ_D = 168696n;
|
|
3601
|
+
function _modpow(base, exp, mod) {
|
|
3602
|
+
let result = 1n;
|
|
3603
|
+
base = base % mod;
|
|
3604
|
+
while (exp > 0n) {
|
|
3605
|
+
if (exp & 1n) result = result * base % mod;
|
|
3606
|
+
exp >>= 1n;
|
|
3607
|
+
base = base * base % mod;
|
|
3608
|
+
}
|
|
3609
|
+
return result;
|
|
3610
|
+
}
|
|
3611
|
+
function _sqrtModP(y2) {
|
|
3612
|
+
if (y2 === 0n) return 0n;
|
|
3613
|
+
if (_modpow(y2, (BN254_R - 1n) / 2n, BN254_R) !== 1n) return null;
|
|
3614
|
+
let s = 0n;
|
|
3615
|
+
let q = BN254_R - 1n;
|
|
3616
|
+
while ((q & 1n) === 0n) {
|
|
3617
|
+
q >>= 1n;
|
|
3618
|
+
s++;
|
|
3619
|
+
}
|
|
3620
|
+
if (s === 1n) return _modpow(y2, (BN254_R + 1n) / 4n, BN254_R);
|
|
3621
|
+
let z = 2n;
|
|
3622
|
+
while (_modpow(z, (BN254_R - 1n) / 2n, BN254_R) === 1n) z++;
|
|
3623
|
+
let m = s;
|
|
3624
|
+
let c = _modpow(z, q, BN254_R);
|
|
3625
|
+
let t = _modpow(y2, q, BN254_R);
|
|
3626
|
+
let r = _modpow(y2, (q + 1n) / 2n, BN254_R);
|
|
3627
|
+
for (; ; ) {
|
|
3628
|
+
if (t === 1n) return r;
|
|
3629
|
+
let i = 1n;
|
|
3630
|
+
let tmp = t * t % BN254_R;
|
|
3631
|
+
while (tmp !== 1n) {
|
|
3632
|
+
tmp = tmp * tmp % BN254_R;
|
|
3633
|
+
i++;
|
|
3634
|
+
}
|
|
3635
|
+
const b = _modpow(c, 1n << m - i - 1n, BN254_R);
|
|
3636
|
+
m = i;
|
|
3637
|
+
c = b * b % BN254_R;
|
|
3638
|
+
t = t * c % BN254_R;
|
|
3639
|
+
r = r * b % BN254_R;
|
|
3640
|
+
}
|
|
3641
|
+
}
|
|
3642
|
+
function recoverOwnerPkPoint(ax) {
|
|
3643
|
+
const x2 = ax * ax % BN254_R;
|
|
3644
|
+
const num = ((1n - BJJ_A * x2) % BN254_R + BN254_R) % BN254_R;
|
|
3645
|
+
const den = ((1n - BJJ_D * x2) % BN254_R + BN254_R) % BN254_R;
|
|
3646
|
+
if (den === 0n) return null;
|
|
3647
|
+
const denInv = _modpow(den, BN254_R - 2n, BN254_R);
|
|
3648
|
+
const y2 = num * denInv % BN254_R;
|
|
3649
|
+
const y = _sqrtModP(y2);
|
|
3650
|
+
if (y === null) return null;
|
|
3651
|
+
const yAlt = BN254_R - y;
|
|
3652
|
+
try {
|
|
3653
|
+
const check = mulPointEscalar3([ax, y], BABYJUB_SUBORDER);
|
|
3654
|
+
return check[0] === 0n && check[1] === 1n ? [ax, y] : [ax, yAlt];
|
|
3655
|
+
} catch {
|
|
3656
|
+
return [ax, yAlt];
|
|
3657
|
+
}
|
|
3658
|
+
}
|
|
3659
|
+
|
|
3660
|
+
// src/shielded-pool/protocol/NoteBuilder.ts
|
|
3661
|
+
import { mulPointEscalar as mulPointEscalar4, unpackPoint as unpackPoint2 } from "@zk-kit/baby-jubjub";
|
|
3662
|
+
import { randomBytes as randomBytes2 } from "@noble/ciphers/utils.js";
|
|
3663
|
+
import { poseidon2, poseidon4 } from "poseidon-lite";
|
|
3664
|
+
var NoteBuilder = class {
|
|
3665
|
+
/**
|
|
3666
|
+
* Build a ZkNote from the given inputs.
|
|
3667
|
+
*
|
|
3668
|
+
* @param input.value Amount in planck (required).
|
|
3669
|
+
* @param input.assetId Asset ID — default 0n (native ORB-Privacy).
|
|
3670
|
+
* @param input.ownerPk Sender's or recipient's global BabyJubJub Ax — default 0n.
|
|
3671
|
+
* @param input.blinding Random scalar — defaults to BigInt(Date.now()).
|
|
3672
|
+
* @param input.spendingKey Secret key for nullifier — default 0n.
|
|
3673
|
+
* @param input.viewingPublicKey Recipient's 32-byte LE packed BJJ ivk. Triggers memo encryption.
|
|
3674
|
+
* @param input.recipientOwnerPk Recipient's global ownerPk. Required with viewingPublicKey
|
|
3675
|
+
* to enable stealth address derivation. Without it, the
|
|
3676
|
+
* commitment uses ownerPk directly (no stealth).
|
|
3677
|
+
*/
|
|
3678
|
+
static async build(input) {
|
|
3679
|
+
const value = input.value;
|
|
3680
|
+
const assetId = input.assetId ?? 0n;
|
|
3681
|
+
const ownerPk = input.ownerPk ?? 0n;
|
|
3682
|
+
const blinding = input.blinding ?? BigInt(Date.now());
|
|
3683
|
+
const spendingKey = input.spendingKey ?? 0n;
|
|
3684
|
+
const counterpartyPk = input.counterpartyPk ?? 0n;
|
|
3685
|
+
const useStealth = input.viewingPublicKey !== void 0 && input.recipientOwnerPk !== void 0;
|
|
3686
|
+
let memo;
|
|
3687
|
+
if (useStealth) {
|
|
3688
|
+
const recipientOwnerPk = input.recipientOwnerPk;
|
|
3689
|
+
const recipientIvkPacked = input.viewingPublicKey;
|
|
3690
|
+
const ephSk = randomBytes2(32);
|
|
3691
|
+
const ivkPackedBigint = bytesToBigintLE(recipientIvkPacked);
|
|
3692
|
+
const ivkPoint = unpackPoint2(ivkPackedBigint);
|
|
3693
|
+
if (!ivkPoint)
|
|
3694
|
+
throw new Error("NoteBuilder.build: invalid recipient viewing public key");
|
|
3695
|
+
const ephSkScalar = BigInt(toHex(ephSk)) % BABYJUB_SUBORDER || 1n;
|
|
3696
|
+
const sharedPoint = mulPointEscalar4(ivkPoint, ephSkScalar);
|
|
3697
|
+
const sharedSecret = bigintTo32Le(sharedPoint[0]);
|
|
3698
|
+
const recipientPkPoint = recoverOwnerPkPoint(recipientOwnerPk);
|
|
3699
|
+
if (!recipientPkPoint)
|
|
3700
|
+
throw new Error(
|
|
3701
|
+
"NoteBuilder.build: recipientOwnerPk is not a valid BJJ x-coordinate"
|
|
3702
|
+
);
|
|
3703
|
+
const effectiveOwnerPk = deriveStealthOwnerPk(
|
|
3704
|
+
sharedSecret,
|
|
3705
|
+
recipientOwnerPk,
|
|
3706
|
+
recipientPkPoint
|
|
3707
|
+
);
|
|
3708
|
+
const stealthCommitment = poseidon4([value, assetId, effectiveOwnerPk, blinding]);
|
|
3709
|
+
const stealthCommitmentBytes = bigintTo32Le(stealthCommitment);
|
|
3710
|
+
memo = Array.from(
|
|
3711
|
+
EncryptedMemo.encrypt(
|
|
3712
|
+
value,
|
|
3713
|
+
bigintTo32Le(effectiveOwnerPk),
|
|
3714
|
+
bigintTo32Le(blinding),
|
|
3715
|
+
Number(assetId),
|
|
3716
|
+
stealthCommitmentBytes,
|
|
3717
|
+
recipientIvkPacked,
|
|
3718
|
+
bigintTo32Le(counterpartyPk),
|
|
3719
|
+
ephSk
|
|
3720
|
+
)
|
|
3721
|
+
);
|
|
3722
|
+
const commitment2 = stealthCommitment;
|
|
3723
|
+
const nullifier2 = poseidon2([commitment2, spendingKey]);
|
|
3724
|
+
const commitmentBytes2 = stealthCommitmentBytes;
|
|
3725
|
+
const nullifierBytes2 = bigintTo32Le(nullifier2);
|
|
3726
|
+
if (memo.length !== ENCRYPTED_MEMO_SIZE)
|
|
3727
|
+
throw new Error(
|
|
3728
|
+
`NoteBuilder.build: invariant violated \u2014 memo must be ${ENCRYPTED_MEMO_SIZE} bytes, got ${memo.length}`
|
|
3729
|
+
);
|
|
3730
|
+
return {
|
|
3731
|
+
value,
|
|
3732
|
+
assetId,
|
|
3733
|
+
ownerPk: effectiveOwnerPk,
|
|
3734
|
+
blinding,
|
|
3735
|
+
spendingKey,
|
|
3736
|
+
spent: false,
|
|
3737
|
+
spentAt: null,
|
|
3738
|
+
commitment: commitment2,
|
|
3739
|
+
nullifier: nullifier2,
|
|
3740
|
+
commitmentHex: toHex(commitmentBytes2),
|
|
3741
|
+
nullifierHex: toHex(nullifierBytes2),
|
|
3742
|
+
memo,
|
|
3743
|
+
counterpartyPk
|
|
3744
|
+
};
|
|
3745
|
+
}
|
|
3746
|
+
const commitment = poseidon4([value, assetId, ownerPk, blinding]);
|
|
3747
|
+
const nullifier = poseidon2([commitment, spendingKey]);
|
|
3748
|
+
const commitmentBytes = bigintTo32Le(commitment);
|
|
3749
|
+
const nullifierBytes = bigintTo32Le(nullifier);
|
|
3750
|
+
memo = input.viewingPublicKey !== void 0 ? Array.from(
|
|
3751
|
+
EncryptedMemo.encrypt(
|
|
3752
|
+
value,
|
|
3753
|
+
bigintTo32Le(ownerPk),
|
|
3754
|
+
bigintTo32Le(blinding),
|
|
3755
|
+
Number(assetId),
|
|
3756
|
+
commitmentBytes,
|
|
3757
|
+
input.viewingPublicKey,
|
|
3758
|
+
bigintTo32Le(counterpartyPk)
|
|
3759
|
+
)
|
|
3760
|
+
) : Array.from(EncryptedMemo.dummy());
|
|
3761
|
+
if (memo.length !== ENCRYPTED_MEMO_SIZE)
|
|
3762
|
+
throw new Error(
|
|
3763
|
+
`NoteBuilder.build: invariant violated \u2014 memo must be ${ENCRYPTED_MEMO_SIZE} bytes, got ${memo.length}`
|
|
3764
|
+
);
|
|
3765
|
+
return {
|
|
3766
|
+
value,
|
|
3767
|
+
assetId,
|
|
3768
|
+
ownerPk,
|
|
3769
|
+
blinding,
|
|
3770
|
+
spendingKey,
|
|
3771
|
+
spent: false,
|
|
3772
|
+
spentAt: null,
|
|
3773
|
+
commitment,
|
|
3774
|
+
nullifier,
|
|
3775
|
+
commitmentHex: toHex(commitmentBytes),
|
|
3776
|
+
nullifierHex: toHex(nullifierBytes),
|
|
3777
|
+
memo,
|
|
3778
|
+
counterpartyPk
|
|
3779
|
+
};
|
|
3780
|
+
}
|
|
3781
|
+
/**
|
|
3782
|
+
* Build the 168-byte ECDH-encrypted memo for a note.
|
|
3783
|
+
*
|
|
3784
|
+
* Pure TypeScript implementation — no WASM dependency.
|
|
3785
|
+
* Uses ChaCha20-Poly1305 with ECDH key agreement (BabyJubJub ephemeral keypair).
|
|
3786
|
+
*
|
|
3787
|
+
* @param note The ZkNote whose fields populate the plaintext.
|
|
3788
|
+
* @param recipientIvkPacked 32-byte LE packed BJJ viewing public key of the recipient.
|
|
3789
|
+
* Pass `new Uint8Array(32)` (default) for a public/dummy memo.
|
|
3790
|
+
* @param counterpartyPk 32-byte counterparty BabyJubJub Ax.
|
|
3791
|
+
* Pass `new Uint8Array(32)` (default) for no counterparty.
|
|
3792
|
+
*/
|
|
3793
|
+
static buildMemo(note, recipientIvkPacked, counterpartyPk) {
|
|
3794
|
+
return EncryptedMemo.encrypt(
|
|
3795
|
+
note.value,
|
|
3796
|
+
bigintTo32Le(note.ownerPk),
|
|
3797
|
+
bigintTo32Le(note.blinding),
|
|
3798
|
+
Number(note.assetId),
|
|
3799
|
+
bigintTo32Le(note.commitment),
|
|
3800
|
+
recipientIvkPacked ?? new Uint8Array(32),
|
|
3801
|
+
counterpartyPk ?? bigintTo32Le(note.counterpartyPk ?? 0n)
|
|
3802
|
+
);
|
|
3803
|
+
}
|
|
3804
|
+
};
|
|
3805
|
+
|
|
3806
|
+
// src/shielded-pool/protocol/NoteDecryptor.ts
|
|
3086
3807
|
import { poseidon2 as poseidon22, poseidon4 as poseidon42 } from "poseidon-lite";
|
|
3087
|
-
function
|
|
3088
|
-
|
|
3808
|
+
function computeNullifier(commitment, spendingKey) {
|
|
3809
|
+
return poseidon22([commitment, spendingKey]);
|
|
3810
|
+
}
|
|
3811
|
+
function tryDecryptNote(commitment, viewingSecretKey, spendingKey, ownOwnerPk = 0n) {
|
|
3812
|
+
return tryDecryptNoteVerbose(commitment, viewingSecretKey, spendingKey, ownOwnerPk).note;
|
|
3813
|
+
}
|
|
3814
|
+
function tryDecryptNoteVerbose(commitment, viewingSecretKey, spendingKey, ownOwnerPk = 0n) {
|
|
3815
|
+
if (!commitment.encryptedMemo) return { note: null, reason: "no_memo" };
|
|
3089
3816
|
let commitmentBytes;
|
|
3090
3817
|
let memoBytes;
|
|
3091
3818
|
try {
|
|
3092
3819
|
commitmentBytes = fromHex(commitment.commitmentHex);
|
|
3093
3820
|
memoBytes = fromHex(commitment.encryptedMemo);
|
|
3094
3821
|
} catch {
|
|
3095
|
-
return null;
|
|
3822
|
+
return { note: null, reason: "hex_parse_error" };
|
|
3823
|
+
}
|
|
3824
|
+
if (memoBytes.length !== ENCRYPTED_MEMO_SIZE) {
|
|
3825
|
+
return {
|
|
3826
|
+
note: null,
|
|
3827
|
+
reason: `memo_size_mismatch:got_${memoBytes.length}_expected_${ENCRYPTED_MEMO_SIZE}`
|
|
3828
|
+
};
|
|
3829
|
+
}
|
|
3830
|
+
const plaintext = EncryptedMemo.decrypt(memoBytes, commitmentBytes, viewingSecretKey);
|
|
3831
|
+
if (!plaintext) return { note: null, reason: "decrypt_failed:wrong_key_or_corrupt_mac" };
|
|
3832
|
+
let effectiveOwnerPk = plaintext.ownerPk;
|
|
3833
|
+
let effectiveSpendingKey = spendingKey;
|
|
3834
|
+
if (ownOwnerPk !== 0n && plaintext.ownerPk !== ownOwnerPk) {
|
|
3835
|
+
const sharedSecret = EncryptedMemo.extractSharedSecret(memoBytes, viewingSecretKey);
|
|
3836
|
+
if (!sharedSecret) return { note: null, reason: "stealth_shared_secret_failed" };
|
|
3837
|
+
const ownPkPoint = recoverOwnerPkPoint(ownOwnerPk);
|
|
3838
|
+
if (!ownPkPoint) return { note: null, reason: "stealth_invalid_own_owner_pk" };
|
|
3839
|
+
const stealthOwnerPk = deriveStealthOwnerPk(sharedSecret, ownOwnerPk, ownPkPoint);
|
|
3840
|
+
if (stealthOwnerPk !== plaintext.ownerPk) {
|
|
3841
|
+
return { note: null, reason: "commitment_mismatch" };
|
|
3842
|
+
}
|
|
3843
|
+
effectiveOwnerPk = stealthOwnerPk;
|
|
3844
|
+
effectiveSpendingKey = deriveStealthSk(sharedSecret, ownOwnerPk, spendingKey);
|
|
3096
3845
|
}
|
|
3097
|
-
const plaintext = EncryptedMemo.decrypt(memoBytes, commitmentBytes, viewingKey);
|
|
3098
|
-
if (!plaintext) return null;
|
|
3099
3846
|
const recomputed = poseidon42([
|
|
3100
3847
|
plaintext.value,
|
|
3101
3848
|
plaintext.assetId,
|
|
3102
|
-
|
|
3849
|
+
effectiveOwnerPk,
|
|
3103
3850
|
plaintext.blinding
|
|
3104
3851
|
]);
|
|
3105
|
-
if (recomputed !== bytesToBigintLE(commitmentBytes))
|
|
3106
|
-
|
|
3852
|
+
if (recomputed !== bytesToBigintLE(commitmentBytes)) {
|
|
3853
|
+
return { note: null, reason: "commitment_mismatch" };
|
|
3854
|
+
}
|
|
3855
|
+
const nullifier = poseidon22([recomputed, effectiveSpendingKey]);
|
|
3107
3856
|
return {
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3857
|
+
note: {
|
|
3858
|
+
value: plaintext.value,
|
|
3859
|
+
assetId: plaintext.assetId,
|
|
3860
|
+
ownerPk: effectiveOwnerPk,
|
|
3861
|
+
blinding: plaintext.blinding,
|
|
3862
|
+
spendingKey: effectiveSpendingKey,
|
|
3863
|
+
spent: false,
|
|
3864
|
+
spentAt: null,
|
|
3865
|
+
commitment: recomputed,
|
|
3866
|
+
nullifier,
|
|
3867
|
+
commitmentHex: toHex(bigintTo32Le(recomputed)),
|
|
3868
|
+
nullifierHex: toHex(bigintTo32Le(nullifier)),
|
|
3869
|
+
memo: Array.from(memoBytes),
|
|
3870
|
+
counterpartyPk: plaintext.counterpartyPk
|
|
3871
|
+
}
|
|
3120
3872
|
};
|
|
3121
3873
|
}
|
|
3122
3874
|
|
|
3123
|
-
// src/shielded-pool/
|
|
3124
|
-
import {
|
|
3125
|
-
|
|
3126
|
-
|
|
3875
|
+
// src/shielded-pool/protocol/NoteDisclosure.ts
|
|
3876
|
+
import { poseidon4 as poseidon43 } from "poseidon-lite";
|
|
3877
|
+
var PREFIX = "orbdisc:";
|
|
3878
|
+
var VERSION = 1;
|
|
3879
|
+
function toHex2(n) {
|
|
3880
|
+
return "0x" + n.toString(16);
|
|
3881
|
+
}
|
|
3882
|
+
function fromHex2(s) {
|
|
3883
|
+
return BigInt(s);
|
|
3884
|
+
}
|
|
3885
|
+
function createNoteDisclosureKey(note) {
|
|
3886
|
+
const payload = {
|
|
3887
|
+
v: VERSION,
|
|
3888
|
+
c: toHex2(note.commitment),
|
|
3889
|
+
val: toHex2(note.value),
|
|
3890
|
+
aid: toHex2(note.assetId),
|
|
3891
|
+
opk: toHex2(note.ownerPk),
|
|
3892
|
+
bld: toHex2(note.blinding)
|
|
3893
|
+
};
|
|
3894
|
+
const json = JSON.stringify(payload);
|
|
3895
|
+
const b64 = btoa(json).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
3896
|
+
return PREFIX + b64;
|
|
3897
|
+
}
|
|
3898
|
+
function decodeNoteDisclosureKey(key) {
|
|
3899
|
+
try {
|
|
3900
|
+
if (!key.startsWith(PREFIX)) return null;
|
|
3901
|
+
const b64 = key.slice(PREFIX.length).replace(/-/g, "+").replace(/_/g, "/");
|
|
3902
|
+
const json = atob(b64);
|
|
3903
|
+
const payload = JSON.parse(json);
|
|
3904
|
+
if (payload.v !== VERSION) return null;
|
|
3905
|
+
const disclosure = {
|
|
3906
|
+
commitment: fromHex2(payload.c),
|
|
3907
|
+
value: fromHex2(payload.val),
|
|
3908
|
+
assetId: fromHex2(payload.aid),
|
|
3909
|
+
ownerPk: fromHex2(payload.opk),
|
|
3910
|
+
blinding: fromHex2(payload.bld)
|
|
3911
|
+
};
|
|
3912
|
+
const recomputed = poseidon43([
|
|
3913
|
+
disclosure.value,
|
|
3914
|
+
disclosure.assetId,
|
|
3915
|
+
disclosure.ownerPk,
|
|
3916
|
+
disclosure.blinding
|
|
3917
|
+
]);
|
|
3918
|
+
if (recomputed !== disclosure.commitment) return null;
|
|
3919
|
+
return disclosure;
|
|
3920
|
+
} catch {
|
|
3921
|
+
return null;
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3127
3924
|
|
|
3128
|
-
// src/shielded-pool/
|
|
3129
|
-
var
|
|
3925
|
+
// src/shielded-pool/protocol/coinSelection.ts
|
|
3926
|
+
var TRANSFER_TREE_DEPTH = 20;
|
|
3927
|
+
function selectNotes(notes, needed) {
|
|
3928
|
+
const unspent = notes.filter((n) => !n.spent && n.value > 0n);
|
|
3929
|
+
const sorted = [...unspent].sort((a, b) => a.value < b.value ? -1 : 1);
|
|
3930
|
+
const single = sorted.find((n) => n.value >= needed);
|
|
3931
|
+
if (single) return [single, null];
|
|
3932
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
3933
|
+
for (let j = i + 1; j < sorted.length; j++) {
|
|
3934
|
+
const a = sorted[i];
|
|
3935
|
+
const b = sorted[j];
|
|
3936
|
+
if (a !== void 0 && b !== void 0 && a.value + b.value >= needed) {
|
|
3937
|
+
return [a, b];
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3940
|
+
}
|
|
3941
|
+
return null;
|
|
3942
|
+
}
|
|
3943
|
+
function buildDummyTransferInput(assetId) {
|
|
3944
|
+
const zeroSibling = "0x" + "00".repeat(32);
|
|
3945
|
+
return {
|
|
3946
|
+
nullifier: 0n,
|
|
3947
|
+
// Constraint 9: nullifier * is_dummy.out === 0 → must be 0
|
|
3948
|
+
value: 0n,
|
|
3949
|
+
// triggers is_dummy[i].out = 1 in the circuit
|
|
3950
|
+
assetId,
|
|
3951
|
+
// must match real note (Constraint 7)
|
|
3952
|
+
ownerPk: 0n,
|
|
3953
|
+
blinding: 0n,
|
|
3954
|
+
spendingKey: 1n,
|
|
3955
|
+
// arbitrary; EdDSA is disabled (enabled = 0) for dummy inputs
|
|
3956
|
+
pathSiblings: Array(TRANSFER_TREE_DEPTH).fill(zeroSibling),
|
|
3957
|
+
leafIndex: 0
|
|
3958
|
+
};
|
|
3959
|
+
}
|
|
3130
3960
|
|
|
3131
|
-
// src/
|
|
3961
|
+
// src/utils/blinding.ts
|
|
3962
|
+
function randomBlinding() {
|
|
3963
|
+
const buf = new Uint8Array(32);
|
|
3964
|
+
crypto.getRandomValues(buf);
|
|
3965
|
+
const n = bytesToBigintLE(buf);
|
|
3966
|
+
return n === 0n ? 1n : n % BN254_R;
|
|
3967
|
+
}
|
|
3968
|
+
|
|
3969
|
+
// src/privacy-keys/PrivacyKeys.ts
|
|
3970
|
+
import { hkdf as hkdf2 } from "@noble/hashes/hkdf.js";
|
|
3971
|
+
import { sha256 as sha2563 } from "@noble/hashes/sha2.js";
|
|
3972
|
+
import { mulPointEscalar as mulPointEscalar5, Base8 as Base83, packPoint as packPoint2 } from "@zk-kit/baby-jubjub";
|
|
3132
3973
|
var IVK_DOMAIN = new TextEncoder().encode("orbinum-ivk-v1");
|
|
3133
3974
|
function deriveSpendingKeyMessage(chainId, address) {
|
|
3134
3975
|
return `orbinum-spending-key-v1
|
|
3135
3976
|
${chainId}
|
|
3136
3977
|
${address.toLowerCase()}`;
|
|
3137
3978
|
}
|
|
3138
|
-
async function
|
|
3139
|
-
const
|
|
3140
|
-
const sigBytes = new Uint8Array((hex.match(/.{2}/g) ?? []).map((b) => parseInt(b, 16)));
|
|
3979
|
+
async function deriveMasterKeyBytes(signatureHex, chainId, address) {
|
|
3980
|
+
const sigBytes = fromHex(signatureHex);
|
|
3141
3981
|
const info = new TextEncoder().encode(`orbinum-sk-v1:${chainId}:${address.toLowerCase()}`);
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3982
|
+
return hkdf2(sha2563, sigBytes, new Uint8Array(0), info, 32);
|
|
3983
|
+
}
|
|
3984
|
+
async function deriveSpendingKeyFromSignature(signatureHex, chainId, address) {
|
|
3985
|
+
const masterBytes = await deriveMasterKeyBytes(signatureHex, chainId, address);
|
|
3986
|
+
const skBigint = BigInt(toHex(masterBytes)) % BABYJUB_SUBORDER;
|
|
3146
3987
|
return skBigint === 0n ? 1n : skBigint;
|
|
3147
3988
|
}
|
|
3148
|
-
function
|
|
3989
|
+
function deriveViewingSecretKey(spendingKey) {
|
|
3149
3990
|
const ikm = bigintTo32Le(spendingKey);
|
|
3150
|
-
return
|
|
3991
|
+
return hkdf2(sha2563, ikm, void 0, IVK_DOMAIN, 32);
|
|
3992
|
+
}
|
|
3993
|
+
function deriveViewingPublicKey(ivsk) {
|
|
3994
|
+
const ivskScalar = BigInt(toHex(ivsk)) % BABYJUB_SUBORDER || 1n;
|
|
3995
|
+
const ivkPoint = mulPointEscalar5(Base83, ivskScalar);
|
|
3996
|
+
const packed = packPoint2(ivkPoint);
|
|
3997
|
+
return bigintTo32Le(packed);
|
|
3151
3998
|
}
|
|
3152
3999
|
function deriveOwnerPk(spendingKey) {
|
|
3153
4000
|
try {
|
|
3154
|
-
const pubPoint =
|
|
4001
|
+
const pubPoint = mulPointEscalar5(Base83, spendingKey);
|
|
3155
4002
|
return pubPoint[0];
|
|
3156
4003
|
} catch {
|
|
3157
4004
|
return 0n;
|
|
3158
4005
|
}
|
|
3159
4006
|
}
|
|
3160
4007
|
|
|
3161
|
-
// src/
|
|
4008
|
+
// src/privacy-keys/PrivacyKeyManager.ts
|
|
3162
4009
|
var PrivacyKeyManager = class {
|
|
3163
4010
|
_state = {
|
|
3164
4011
|
spendingKey: null,
|
|
3165
|
-
|
|
4012
|
+
masterBytes: null,
|
|
4013
|
+
viewingSecretKey: null,
|
|
4014
|
+
viewingPublicKeyPacked: null,
|
|
3166
4015
|
ownerPk: null
|
|
3167
4016
|
};
|
|
3168
4017
|
/**
|
|
3169
|
-
* Load a spending key into the in-memory session.
|
|
3170
|
-
* Derives
|
|
4018
|
+
* Load a spending key and its corresponding master bytes into the in-memory session.
|
|
4019
|
+
* Derives viewingSecretKey, viewingPublicKeyPacked, and ownerPk immediately.
|
|
3171
4020
|
* Replaces any previously loaded key.
|
|
4021
|
+
*
|
|
4022
|
+
* @param spendingKey Circuit scalar: BigInt(masterBytes) % BABYJUB_SUBORDER, clamped to [1, ∞).
|
|
4023
|
+
* @param masterBytes Raw 32-byte HKDF output before modular reduction. Used to derive
|
|
4024
|
+
* the stable vault key (HKDF(masterBytes, info="orbinum-vault-key-v1")).
|
|
3172
4025
|
*/
|
|
3173
|
-
async load(spendingKey) {
|
|
3174
|
-
const
|
|
4026
|
+
async load(spendingKey, masterBytes) {
|
|
4027
|
+
const viewingSecretKey = deriveViewingSecretKey(spendingKey);
|
|
4028
|
+
const viewingPublicKeyPacked = deriveViewingPublicKey(viewingSecretKey);
|
|
3175
4029
|
const ownerPk = deriveOwnerPk(spendingKey);
|
|
3176
|
-
this._state = {
|
|
4030
|
+
this._state = {
|
|
4031
|
+
spendingKey,
|
|
4032
|
+
masterBytes,
|
|
4033
|
+
viewingSecretKey,
|
|
4034
|
+
viewingPublicKeyPacked,
|
|
4035
|
+
ownerPk
|
|
4036
|
+
};
|
|
3177
4037
|
}
|
|
3178
4038
|
/** Clear all key material from memory. Call on vault lock / sign-out. */
|
|
3179
4039
|
clear() {
|
|
3180
|
-
this._state = {
|
|
4040
|
+
this._state = {
|
|
4041
|
+
spendingKey: null,
|
|
4042
|
+
masterBytes: null,
|
|
4043
|
+
viewingSecretKey: null,
|
|
4044
|
+
viewingPublicKeyPacked: null,
|
|
4045
|
+
ownerPk: null
|
|
4046
|
+
};
|
|
3181
4047
|
}
|
|
3182
4048
|
/** Returns true if a spending key has been loaded. */
|
|
3183
4049
|
isLoaded() {
|
|
@@ -3190,12 +4056,29 @@ var PrivacyKeyManager = class {
|
|
|
3190
4056
|
}
|
|
3191
4057
|
return this._state.spendingKey;
|
|
3192
4058
|
}
|
|
3193
|
-
/**
|
|
3194
|
-
|
|
3195
|
-
|
|
4059
|
+
/**
|
|
4060
|
+
* Returns the 32-byte viewing secret key (ivsk).
|
|
4061
|
+
* Used internally for decrypting received notes during rescan.
|
|
4062
|
+
* SECURITY: never expose this in addresses or network requests.
|
|
4063
|
+
* Throws if not loaded.
|
|
4064
|
+
*/
|
|
4065
|
+
getViewingSecretKey() {
|
|
4066
|
+
if (this._state.viewingSecretKey === null) {
|
|
3196
4067
|
throw new Error("PrivacyKeyManager: no key loaded. Call load() first.");
|
|
3197
4068
|
}
|
|
3198
|
-
return this._state.
|
|
4069
|
+
return this._state.viewingSecretKey;
|
|
4070
|
+
}
|
|
4071
|
+
/**
|
|
4072
|
+
* Returns the 32-byte LE-encoded packed BJJ viewing public key (ivk).
|
|
4073
|
+
* This is the component embedded in the privacy address and passed to senders
|
|
4074
|
+
* so they can encrypt memos only the recipient can decrypt.
|
|
4075
|
+
* Throws if not loaded.
|
|
4076
|
+
*/
|
|
4077
|
+
getViewingPublicKeyPacked() {
|
|
4078
|
+
if (this._state.viewingPublicKeyPacked === null) {
|
|
4079
|
+
throw new Error("PrivacyKeyManager: no key loaded. Call load() first.");
|
|
4080
|
+
}
|
|
4081
|
+
return this._state.viewingPublicKeyPacked;
|
|
3199
4082
|
}
|
|
3200
4083
|
/** Returns the BabyJubJub owner public key (x-coordinate). Throws if not loaded. */
|
|
3201
4084
|
getOwnerPk() {
|
|
@@ -3208,24 +4091,86 @@ var PrivacyKeyManager = class {
|
|
|
3208
4091
|
getSpendingKeyBytes() {
|
|
3209
4092
|
return bigintTo32Le(this.getSpendingKey());
|
|
3210
4093
|
}
|
|
3211
|
-
/**
|
|
4094
|
+
/**
|
|
4095
|
+
* Returns the 32-byte master key bytes (pre-modulus HKDF output).
|
|
4096
|
+
* Used to derive the stable vault AES key. Throws if not loaded.
|
|
4097
|
+
*/
|
|
4098
|
+
getMasterBytes() {
|
|
4099
|
+
if (this._state.masterBytes === null) {
|
|
4100
|
+
throw new Error("PrivacyKeyManager: no key loaded. Call load() first.");
|
|
4101
|
+
}
|
|
4102
|
+
return this._state.masterBytes;
|
|
4103
|
+
}
|
|
4104
|
+
/**
|
|
4105
|
+
* Exports the master key bytes as a "mk:0x{hex}" string.
|
|
4106
|
+
* Storing masterBytes (not the sk scalar) ensures the vault key and
|
|
4107
|
+
* rescan can always be reconstructed regardless of any future modulus change.
|
|
4108
|
+
* Throws if not loaded.
|
|
4109
|
+
*/
|
|
3212
4110
|
exportHex() {
|
|
3213
|
-
|
|
4111
|
+
const mb = this.getMasterBytes();
|
|
4112
|
+
return "mk:0x" + Array.from(mb, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
4113
|
+
}
|
|
4114
|
+
/**
|
|
4115
|
+
* Exports a shareable privacy address encoding the owner public key and
|
|
4116
|
+
* viewing PUBLIC key of the currently loaded identity.
|
|
4117
|
+
*
|
|
4118
|
+
* Format: `orbpriv1:{ownerPk_hex}:{viewingPublicKey_hex}`
|
|
4119
|
+
*
|
|
4120
|
+
* The recipient uses this address so the sender can:
|
|
4121
|
+
* 1. Embed `ownerPk` in the note commitment (Poseidon4 input).
|
|
4122
|
+
* 2. Encrypt the memo via ECDH with the recipient's `viewingPublicKey`.
|
|
4123
|
+
*
|
|
4124
|
+
* SECURITY: Only the viewing PUBLIC key is embedded — the viewing secret key
|
|
4125
|
+
* (used for decryption) is never exported. Holders of this address cannot
|
|
4126
|
+
* decrypt the recipient's notes.
|
|
4127
|
+
*
|
|
4128
|
+
* Throws if no key is loaded.
|
|
4129
|
+
*/
|
|
4130
|
+
encodePrivacyAddress() {
|
|
4131
|
+
const ownerPk = this.getOwnerPk();
|
|
4132
|
+
const ivkPacked = this.getViewingPublicKeyPacked();
|
|
4133
|
+
const ownerPkHex = "0x" + ownerPk.toString(16).padStart(64, "0");
|
|
4134
|
+
const ivkHex = "0x" + Array.from(ivkPacked, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
4135
|
+
return `orbpriv1:${ownerPkHex}:${ivkHex}`;
|
|
3214
4136
|
}
|
|
3215
4137
|
/**
|
|
3216
|
-
*
|
|
3217
|
-
*
|
|
4138
|
+
* Decode a privacy address of the form `orbpriv1:{ownerPk_hex}:{viewingPublicKey_hex}`.
|
|
4139
|
+
* Returns `{ ownerPkHex, viewingPublicKeyHex }` on success, or `null` if the input
|
|
4140
|
+
* does not match the expected format.
|
|
4141
|
+
*/
|
|
4142
|
+
static decodePrivacyAddress(address) {
|
|
4143
|
+
if (!address.startsWith("orbpriv1:")) return null;
|
|
4144
|
+
const parts = address.split(":");
|
|
4145
|
+
if (parts.length !== 3) return null;
|
|
4146
|
+
const ownerPkHex = parts[1];
|
|
4147
|
+
const viewingPublicKeyHex = parts[2];
|
|
4148
|
+
if (!ownerPkHex || !viewingPublicKeyHex) return null;
|
|
4149
|
+
return { ownerPkHex, viewingPublicKeyHex };
|
|
4150
|
+
}
|
|
4151
|
+
/**
|
|
4152
|
+
* Load keys from a cached "mk:0x{masterBytes_hex}" string produced by exportHex().
|
|
4153
|
+
* Throws if the format is invalid or masterBytes length is not 32 bytes.
|
|
3218
4154
|
*/
|
|
3219
4155
|
async importFromHex(hex) {
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
4156
|
+
if (!hex.startsWith("mk:")) {
|
|
4157
|
+
throw new Error(
|
|
4158
|
+
'PrivacyKeyManager: invalid cache format. Expected "mk:0x{masterBytes_hex}".'
|
|
4159
|
+
);
|
|
4160
|
+
}
|
|
4161
|
+
const raw = hex.slice(3);
|
|
4162
|
+
const h = raw.startsWith("0x") ? raw.slice(2) : raw;
|
|
4163
|
+
const masterBytes = new Uint8Array((h.match(/.{2}/g) ?? []).map((b) => parseInt(b, 16)));
|
|
4164
|
+
if (masterBytes.length !== 32) {
|
|
4165
|
+
throw new Error("PrivacyKeyManager: invalid master bytes \u2014 expected 32 bytes.");
|
|
3223
4166
|
}
|
|
3224
|
-
|
|
4167
|
+
const masterBigint = BigInt("0x" + h);
|
|
4168
|
+
const sk = masterBigint % BABYJUB_SUBORDER || 1n;
|
|
4169
|
+
await this.load(sk, masterBytes);
|
|
3225
4170
|
}
|
|
3226
4171
|
};
|
|
3227
4172
|
|
|
3228
|
-
// src/
|
|
4173
|
+
// src/vault/VaultJson.ts
|
|
3229
4174
|
function vaultReplacer(_key, value) {
|
|
3230
4175
|
if (typeof value === "bigint") return { __bigint: value.toString() };
|
|
3231
4176
|
return value;
|
|
@@ -3236,16 +4181,28 @@ function vaultReviver(_key, value) {
|
|
|
3236
4181
|
}
|
|
3237
4182
|
return value;
|
|
3238
4183
|
}
|
|
4184
|
+
|
|
4185
|
+
// src/utils/encoding.ts
|
|
4186
|
+
function toBase64(buf) {
|
|
4187
|
+
const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
|
|
4188
|
+
let str = "";
|
|
4189
|
+
for (const b of bytes) str += String.fromCharCode(b);
|
|
4190
|
+
return btoa(str);
|
|
4191
|
+
}
|
|
4192
|
+
function fromBase64(b64) {
|
|
4193
|
+
const bin = atob(b64);
|
|
4194
|
+
const out = new Uint8Array(bin.length);
|
|
4195
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
4196
|
+
return out;
|
|
4197
|
+
}
|
|
4198
|
+
|
|
4199
|
+
// src/vault/VaultCrypto.ts
|
|
3239
4200
|
var VAULT_KEY_INFO = new TextEncoder().encode("orbinum-vault-key-v1");
|
|
3240
4201
|
var IV_BYTES = 12;
|
|
3241
|
-
async function deriveVaultKey(
|
|
3242
|
-
const keyMaterial = await crypto.subtle.importKey(
|
|
3243
|
-
"
|
|
3244
|
-
|
|
3245
|
-
"HKDF",
|
|
3246
|
-
false,
|
|
3247
|
-
["deriveKey"]
|
|
3248
|
-
);
|
|
4202
|
+
async function deriveVaultKey(masterBytes) {
|
|
4203
|
+
const keyMaterial = await crypto.subtle.importKey("raw", masterBytes.slice(0), "HKDF", false, [
|
|
4204
|
+
"deriveKey"
|
|
4205
|
+
]);
|
|
3249
4206
|
return crypto.subtle.deriveKey(
|
|
3250
4207
|
{
|
|
3251
4208
|
name: "HKDF",
|
|
@@ -3274,6 +4231,170 @@ async function decryptJson(key, iv, ciphertext) {
|
|
|
3274
4231
|
return JSON.parse(new TextDecoder().decode(plainBuf), vaultReviver);
|
|
3275
4232
|
}
|
|
3276
4233
|
|
|
4234
|
+
// src/vault/errors.ts
|
|
4235
|
+
var VaultLockedError = class extends Error {
|
|
4236
|
+
constructor(message = "Vault is locked. Connect your wallet to unlock it.") {
|
|
4237
|
+
super(message);
|
|
4238
|
+
this.name = "VaultLockedError";
|
|
4239
|
+
}
|
|
4240
|
+
};
|
|
4241
|
+
|
|
4242
|
+
// src/vault/noteOps.ts
|
|
4243
|
+
function applyNoteStatus(note, status) {
|
|
4244
|
+
return {
|
|
4245
|
+
...note,
|
|
4246
|
+
spent: status?.spent ?? note.spent ?? false,
|
|
4247
|
+
spentAt: status?.spentAt ?? note.spentAt ?? null
|
|
4248
|
+
};
|
|
4249
|
+
}
|
|
4250
|
+
async function encryptNote(key, note) {
|
|
4251
|
+
const { iv, ciphertext } = await encryptJson(key, note);
|
|
4252
|
+
return {
|
|
4253
|
+
commitmentHex: note.commitmentHex,
|
|
4254
|
+
iv,
|
|
4255
|
+
ciphertext,
|
|
4256
|
+
nullifierHex: note.nullifierHex,
|
|
4257
|
+
assetId: note.assetId.toString(),
|
|
4258
|
+
spent: note.spent,
|
|
4259
|
+
spentAt: note.spentAt,
|
|
4260
|
+
updatedAt: Date.now()
|
|
4261
|
+
};
|
|
4262
|
+
}
|
|
4263
|
+
async function decryptNoteRecord(key, rec) {
|
|
4264
|
+
const note = await decryptJson(key, rec.iv, rec.ciphertext);
|
|
4265
|
+
return applyNoteStatus(note, {
|
|
4266
|
+
...rec.spent !== void 0 && { spent: rec.spent },
|
|
4267
|
+
spentAt: rec.spentAt ?? null
|
|
4268
|
+
});
|
|
4269
|
+
}
|
|
4270
|
+
|
|
4271
|
+
// src/proof-generator/unshield.ts
|
|
4272
|
+
import {
|
|
4273
|
+
CircuitType,
|
|
4274
|
+
generateProof,
|
|
4275
|
+
WebArtifactProvider
|
|
4276
|
+
} from "@orbinum/proof-generator";
|
|
4277
|
+
import { randomBytes as randomBytes3 } from "@noble/ciphers/utils.js";
|
|
4278
|
+
import { mulPointEscalar as mulPointEscalar6, Base8 as Base84 } from "@zk-kit/baby-jubjub";
|
|
4279
|
+
import { poseidon4 as poseidon44 } from "poseidon-lite";
|
|
4280
|
+
|
|
4281
|
+
// src/proof-generator/merkle.ts
|
|
4282
|
+
function merkleProofToCircuit(siblings, leafIndex) {
|
|
4283
|
+
const elements = siblings.map((h) => leHexToBigint(h).toString());
|
|
4284
|
+
const depth = siblings.length;
|
|
4285
|
+
const indices = computePathIndices(leafIndex, depth).map(String);
|
|
4286
|
+
return { elements, indices };
|
|
4287
|
+
}
|
|
4288
|
+
|
|
4289
|
+
// src/proof-generator/unshield.ts
|
|
4290
|
+
async function generateUnshieldProof(inputs, options = {}) {
|
|
4291
|
+
const { elements, indices } = merkleProofToCircuit(inputs.pathSiblings, inputs.leafIndex);
|
|
4292
|
+
const fee = inputs.fee ?? 0n;
|
|
4293
|
+
const changeValue = inputs.changeValue ?? 0n;
|
|
4294
|
+
const noteValue = inputs.amount + fee + changeValue;
|
|
4295
|
+
if (inputs.amount <= 0n) {
|
|
4296
|
+
throw new Error("Unshield amount must be greater than zero.");
|
|
4297
|
+
}
|
|
4298
|
+
if (changeValue < 0n) {
|
|
4299
|
+
throw new Error("changeValue must be >= 0.");
|
|
4300
|
+
}
|
|
4301
|
+
const changeOwnerPubkey = inputs.changeOwnerPubkey ?? mulPointEscalar6(Base84, inputs.spendingKey)[0];
|
|
4302
|
+
const changeBlinding = inputs.changeBlinding ?? (changeValue > 0n ? bytesToBigintLE(randomBytes3(32)) : 0n);
|
|
4303
|
+
const changeCommitment = changeValue > 0n ? poseidon44([changeValue, inputs.assetId, changeOwnerPubkey, changeBlinding]) : 0n;
|
|
4304
|
+
const circuitInputs = {
|
|
4305
|
+
merkle_root: leHexToBigint(inputs.merkleRoot).toString(),
|
|
4306
|
+
nullifier: inputs.nullifier.toString(),
|
|
4307
|
+
amount: inputs.amount.toString(),
|
|
4308
|
+
recipient: inputs.recipient.toString(),
|
|
4309
|
+
asset_id: inputs.assetId.toString(),
|
|
4310
|
+
fee: fee.toString(),
|
|
4311
|
+
change_commitment: changeCommitment.toString(),
|
|
4312
|
+
note_value: noteValue.toString(),
|
|
4313
|
+
note_asset_id: inputs.assetId.toString(),
|
|
4314
|
+
note_blinding: inputs.blinding.toString(),
|
|
4315
|
+
spending_key: inputs.spendingKey.toString(),
|
|
4316
|
+
path_elements: elements,
|
|
4317
|
+
path_indices: indices,
|
|
4318
|
+
change_value: changeValue.toString(),
|
|
4319
|
+
change_blinding: changeBlinding.toString(),
|
|
4320
|
+
change_owner_pubkey: changeOwnerPubkey.toString()
|
|
4321
|
+
};
|
|
4322
|
+
const provider = options.provider ?? new WebArtifactProvider();
|
|
4323
|
+
const opts = { provider };
|
|
4324
|
+
if (options.verbose !== void 0) opts.verbose = options.verbose;
|
|
4325
|
+
const proofResult = await generateProof(CircuitType.Unshield, circuitInputs, opts);
|
|
4326
|
+
return { ...proofResult, changeCommitment, changeValue, changeBlinding, changeOwnerPubkey };
|
|
4327
|
+
}
|
|
4328
|
+
|
|
4329
|
+
// src/proof-generator/transfer.ts
|
|
4330
|
+
import {
|
|
4331
|
+
CircuitType as CircuitType2,
|
|
4332
|
+
generateProof as generateProof2,
|
|
4333
|
+
WebArtifactProvider as WebArtifactProvider2
|
|
4334
|
+
} from "@orbinum/proof-generator";
|
|
4335
|
+
async function generateTransferProof(params, options = {}) {
|
|
4336
|
+
const root = leHexToBigint(params.merkleRoot).toString();
|
|
4337
|
+
const [i0, i1] = params.inputs;
|
|
4338
|
+
const [o0, o1] = params.outputs;
|
|
4339
|
+
const fee = params.fee ?? 0n;
|
|
4340
|
+
const path0 = merkleProofToCircuit(i0.pathSiblings, i0.leafIndex);
|
|
4341
|
+
const path1 = merkleProofToCircuit(i1.pathSiblings, i1.leafIndex);
|
|
4342
|
+
const circuitInputs = {
|
|
4343
|
+
merkle_root: root,
|
|
4344
|
+
nullifiers: [i0.nullifier.toString(), i1.nullifier.toString()],
|
|
4345
|
+
commitments: [o0.commitment.toString(), o1.commitment.toString()],
|
|
4346
|
+
asset_id: i0.assetId.toString(),
|
|
4347
|
+
fee: fee.toString(),
|
|
4348
|
+
input_values: [i0.value.toString(), i1.value.toString()],
|
|
4349
|
+
input_asset_ids: [i0.assetId.toString(), i1.assetId.toString()],
|
|
4350
|
+
input_blindings: [i0.blinding.toString(), i1.blinding.toString()],
|
|
4351
|
+
spending_keys: [i0.spendingKey.toString(), i1.spendingKey.toString()],
|
|
4352
|
+
input_path_elements: [path0.elements, path1.elements],
|
|
4353
|
+
input_path_indices: [path0.indices, path1.indices],
|
|
4354
|
+
output_values: [o0.value.toString(), o1.value.toString()],
|
|
4355
|
+
output_asset_ids: [o0.assetId.toString(), o1.assetId.toString()],
|
|
4356
|
+
output_owner_pubkeys: [o0.ownerPk.toString(), o1.ownerPk.toString()],
|
|
4357
|
+
output_blindings: [o0.blinding.toString(), o1.blinding.toString()]
|
|
4358
|
+
};
|
|
4359
|
+
const provider = options.provider ?? new WebArtifactProvider2();
|
|
4360
|
+
const opts = { provider };
|
|
4361
|
+
if (options.verbose !== void 0) opts.verbose = options.verbose;
|
|
4362
|
+
return generateProof2(CircuitType2.Transfer, circuitInputs, opts);
|
|
4363
|
+
}
|
|
4364
|
+
|
|
4365
|
+
// src/proof-generator/fee-claim.ts
|
|
4366
|
+
import {
|
|
4367
|
+
CircuitType as CircuitType3,
|
|
4368
|
+
generateProof as generateProof3,
|
|
4369
|
+
WebArtifactProvider as WebArtifactProvider3
|
|
4370
|
+
} from "@orbinum/proof-generator";
|
|
4371
|
+
async function generateFeeClaimProof(inputs, options = {}) {
|
|
4372
|
+
if (inputs.amount <= 0n) {
|
|
4373
|
+
throw new Error("Fee claim amount must be greater than zero.");
|
|
4374
|
+
}
|
|
4375
|
+
const circuitInputs = {
|
|
4376
|
+
commitment: inputs.commitment.toString(),
|
|
4377
|
+
value: inputs.amount.toString(),
|
|
4378
|
+
asset_id: inputs.assetId.toString(),
|
|
4379
|
+
owner_pubkey: inputs.ownerPubkey.toString(),
|
|
4380
|
+
blinding: inputs.blinding.toString()
|
|
4381
|
+
};
|
|
4382
|
+
const provider = options.provider ?? new WebArtifactProvider3();
|
|
4383
|
+
const opts = { provider };
|
|
4384
|
+
if (options.verbose !== void 0) opts.verbose = options.verbose;
|
|
4385
|
+
const proofResult = await generateProof3(CircuitType3.ValueProof, circuitInputs, opts);
|
|
4386
|
+
const [sigCommitment, sigValue, sigAssetId, sigOwnerHash] = proofResult.publicSignals.map(BigInt);
|
|
4387
|
+
const buf = new Uint8Array(76);
|
|
4388
|
+
buf.set(bigintTo32Le(sigCommitment), 0);
|
|
4389
|
+
buf.set(bigintTo32Le(sigValue).subarray(0, 8), 32);
|
|
4390
|
+
buf.set(bigintTo32Le(sigAssetId).subarray(0, 4), 40);
|
|
4391
|
+
buf.set(bigintTo32Le(sigOwnerHash), 44);
|
|
4392
|
+
return {
|
|
4393
|
+
proof: proofResult.proof,
|
|
4394
|
+
publicSignals: Array.from(buf)
|
|
4395
|
+
};
|
|
4396
|
+
}
|
|
4397
|
+
|
|
3277
4398
|
// src/account-mapping/types/index.ts
|
|
3278
4399
|
var SignatureScheme = {
|
|
3279
4400
|
Eip191: "Eip191",
|
|
@@ -3290,7 +4411,7 @@ function decodePrecompileCalldata(address, input) {
|
|
|
3290
4411
|
if (!fnSig) return null;
|
|
3291
4412
|
if (fnSig.startsWith("registerAlias")) {
|
|
3292
4413
|
try {
|
|
3293
|
-
const data =
|
|
4414
|
+
const data = fromHex(input.slice(10));
|
|
3294
4415
|
const alias = decodeString(data, 0);
|
|
3295
4416
|
return { fnSig, args: { alias } };
|
|
3296
4417
|
} catch {
|
|
@@ -3299,37 +4420,54 @@ function decodePrecompileCalldata(address, input) {
|
|
|
3299
4420
|
}
|
|
3300
4421
|
if (fnSig.startsWith("shield(")) {
|
|
3301
4422
|
try {
|
|
3302
|
-
const data =
|
|
4423
|
+
const data = fromHex(input.slice(10));
|
|
3303
4424
|
const assetId = decodeUint(data, 0);
|
|
3304
|
-
const
|
|
3305
|
-
|
|
3306
|
-
return { fnSig, args: { assetId, amount, commitment } };
|
|
4425
|
+
const commitment = toHex(data.slice(32, 64));
|
|
4426
|
+
return { fnSig, args: { assetId, commitment } };
|
|
3307
4427
|
} catch {
|
|
3308
4428
|
return { fnSig, args: {} };
|
|
3309
4429
|
}
|
|
3310
4430
|
}
|
|
3311
4431
|
if (fnSig.startsWith("unshield(")) {
|
|
3312
4432
|
try {
|
|
3313
|
-
const data =
|
|
4433
|
+
const data = fromHex(input.slice(10));
|
|
3314
4434
|
const root = toHex(data.slice(32, 64));
|
|
3315
4435
|
const nullifier = toHex(data.slice(64, 96));
|
|
3316
4436
|
const assetId = decodeUint(data, 96);
|
|
3317
4437
|
const amount = decodeUint(data, 128);
|
|
3318
4438
|
const recipient = toHex(data.slice(160, 192));
|
|
3319
|
-
|
|
4439
|
+
const fee = decodeUint(data, 192);
|
|
4440
|
+
const changeCommitment = toHex(data.slice(224, 256));
|
|
4441
|
+
return {
|
|
4442
|
+
fnSig,
|
|
4443
|
+
args: { root, nullifier, assetId, amount, recipient, fee, changeCommitment }
|
|
4444
|
+
};
|
|
3320
4445
|
} catch {
|
|
3321
4446
|
return { fnSig, args: {} };
|
|
3322
4447
|
}
|
|
3323
4448
|
}
|
|
3324
4449
|
if (fnSig.startsWith("privateTransfer(")) {
|
|
3325
4450
|
try {
|
|
3326
|
-
const data =
|
|
4451
|
+
const data = fromHex(input.slice(10));
|
|
3327
4452
|
const root = toHex(data.slice(32, 64));
|
|
3328
4453
|
const nullOffset = Number(decodeUint(data, 64));
|
|
3329
4454
|
const commOffset = Number(decodeUint(data, 96));
|
|
3330
4455
|
const nullifiers = Number(decodeUint(data, nullOffset));
|
|
3331
4456
|
const commitments = Number(decodeUint(data, commOffset));
|
|
3332
|
-
|
|
4457
|
+
const assetId = decodeUint(data, 160);
|
|
4458
|
+
const fee = decodeUint(data, 192);
|
|
4459
|
+
return { fnSig, args: { root, nullifiers, commitments, assetId, fee } };
|
|
4460
|
+
} catch {
|
|
4461
|
+
return { fnSig, args: {} };
|
|
4462
|
+
}
|
|
4463
|
+
}
|
|
4464
|
+
if (fnSig.startsWith("claimShieldedFees(")) {
|
|
4465
|
+
try {
|
|
4466
|
+
const data = fromHex(input.slice(10));
|
|
4467
|
+
const commitment = toHex(data.slice(0, 32));
|
|
4468
|
+
const amount = decodeUint(data, 32);
|
|
4469
|
+
const assetId = decodeUint(data, 64);
|
|
4470
|
+
return { fnSig, args: { commitment, amount, assetId } };
|
|
3333
4471
|
} catch {
|
|
3334
4472
|
return { fnSig, args: {} };
|
|
3335
4473
|
}
|
|
@@ -3341,8 +4479,8 @@ function decodePrecompileCalldata(address, input) {
|
|
|
3341
4479
|
var CircuitId = {
|
|
3342
4480
|
Transfer: 1,
|
|
3343
4481
|
Unshield: 2,
|
|
3344
|
-
|
|
3345
|
-
PrivateLink:
|
|
4482
|
+
ValueProof: 4,
|
|
4483
|
+
PrivateLink: 5
|
|
3346
4484
|
};
|
|
3347
4485
|
|
|
3348
4486
|
// src/utils/string.ts
|
|
@@ -3531,34 +4669,6 @@ function mapExtrinsicArgs(section, method, args) {
|
|
|
3531
4669
|
recipient: get(5, "recipient")
|
|
3532
4670
|
};
|
|
3533
4671
|
}
|
|
3534
|
-
if (m_norm === "setauditpolicy") {
|
|
3535
|
-
return {
|
|
3536
|
-
auditors: get(0, "auditors"),
|
|
3537
|
-
conditions: get(1, "conditions"),
|
|
3538
|
-
max_frequency: get(2, "max_frequency"),
|
|
3539
|
-
valid_until: get(3, "valid_until")
|
|
3540
|
-
};
|
|
3541
|
-
}
|
|
3542
|
-
if (m_norm === "requestdisclosure") {
|
|
3543
|
-
return {
|
|
3544
|
-
target: get(0, "target"),
|
|
3545
|
-
reason: get(1, "reason")
|
|
3546
|
-
};
|
|
3547
|
-
}
|
|
3548
|
-
if (m_norm === "disclose") {
|
|
3549
|
-
return {
|
|
3550
|
-
commitment: get(0, "commitment"),
|
|
3551
|
-
proof_bytes: get(1, "proof_bytes"),
|
|
3552
|
-
public_signals: get(2, "public_signals"),
|
|
3553
|
-
auditor: get(3, "auditor")
|
|
3554
|
-
};
|
|
3555
|
-
}
|
|
3556
|
-
if (m_norm === "rejectdisclosure") {
|
|
3557
|
-
return {
|
|
3558
|
-
auditor: get(0, "auditor"),
|
|
3559
|
-
reason: get(1, "reason")
|
|
3560
|
-
};
|
|
3561
|
-
}
|
|
3562
4672
|
if (m_norm === "registerasset") {
|
|
3563
4673
|
return {
|
|
3564
4674
|
name: get(0, "name"),
|
|
@@ -3573,18 +4683,6 @@ function mapExtrinsicArgs(section, method, args) {
|
|
|
3573
4683
|
if (m_norm === "unverifyasset") {
|
|
3574
4684
|
return { asset_id: get(0, "asset_id") };
|
|
3575
4685
|
}
|
|
3576
|
-
if (m_norm === "batchsubmitdisclosureproofs") {
|
|
3577
|
-
return { submissions: get(0, "submissions") };
|
|
3578
|
-
}
|
|
3579
|
-
if (m_norm === "pruneexpiredrequest") {
|
|
3580
|
-
return {
|
|
3581
|
-
target: get(0, "target"),
|
|
3582
|
-
auditor: get(1, "auditor")
|
|
3583
|
-
};
|
|
3584
|
-
}
|
|
3585
|
-
if (m_norm === "revokedisclosurerecord") {
|
|
3586
|
-
return { commitment: get(0, "commitment") };
|
|
3587
|
-
}
|
|
3588
4686
|
}
|
|
3589
4687
|
if (s === "ethereum" && m === "transact") {
|
|
3590
4688
|
return { transaction: get(0, "transaction") };
|
|
@@ -3808,45 +4906,6 @@ function mapZkEventData(method, data) {
|
|
|
3808
4906
|
};
|
|
3809
4907
|
}
|
|
3810
4908
|
const m_norm = m.replace(/_/g, "");
|
|
3811
|
-
if (m_norm === "auditpolicyset") {
|
|
3812
|
-
return {
|
|
3813
|
-
account: get(0, "account"),
|
|
3814
|
-
version: get(1, "version")
|
|
3815
|
-
};
|
|
3816
|
-
}
|
|
3817
|
-
if (m_norm === "disclosed") {
|
|
3818
|
-
return {
|
|
3819
|
-
who: get(0, "who"),
|
|
3820
|
-
commitment: get(1, "commitment"),
|
|
3821
|
-
auditor: get(2, "auditor")
|
|
3822
|
-
};
|
|
3823
|
-
}
|
|
3824
|
-
if (m_norm === "disclosurerequested") {
|
|
3825
|
-
return {
|
|
3826
|
-
target: get(0, "target"),
|
|
3827
|
-
auditor: get(1, "auditor"),
|
|
3828
|
-
reason: get(2, "reason")
|
|
3829
|
-
};
|
|
3830
|
-
}
|
|
3831
|
-
if (m_norm === "disclosurerejected") {
|
|
3832
|
-
return {
|
|
3833
|
-
target: get(0, "target"),
|
|
3834
|
-
auditor: get(1, "auditor"),
|
|
3835
|
-
reason: get(2, "reason")
|
|
3836
|
-
};
|
|
3837
|
-
}
|
|
3838
|
-
if (m_norm === "disclosurerequestexpired") {
|
|
3839
|
-
return {
|
|
3840
|
-
target: get(0, "target"),
|
|
3841
|
-
auditor: get(1, "auditor")
|
|
3842
|
-
};
|
|
3843
|
-
}
|
|
3844
|
-
if (m_norm === "disclosurerecordrevoked") {
|
|
3845
|
-
return {
|
|
3846
|
-
who: get(0, "who"),
|
|
3847
|
-
commitment: get(1, "commitment")
|
|
3848
|
-
};
|
|
3849
|
-
}
|
|
3850
4909
|
if (m_norm === "assetregistered") {
|
|
3851
4910
|
return { asset_id: get(0, "asset_id") };
|
|
3852
4911
|
}
|
|
@@ -4100,9 +5159,13 @@ export {
|
|
|
4100
5159
|
AccountId2 as AccountId,
|
|
4101
5160
|
AccountMappingModule,
|
|
4102
5161
|
AccountMappingPrecompile,
|
|
5162
|
+
BABYJUB_SUBORDER,
|
|
5163
|
+
BN254_R,
|
|
4103
5164
|
Blake2256,
|
|
4104
5165
|
CircuitId,
|
|
5166
|
+
CircuitType,
|
|
4105
5167
|
CryptoPrecompiles,
|
|
5168
|
+
ENCRYPTED_MEMO_SIZE,
|
|
4106
5169
|
EncryptedMemo,
|
|
4107
5170
|
EvmClient,
|
|
4108
5171
|
EvmExplorer,
|
|
@@ -4115,30 +5178,44 @@ export {
|
|
|
4115
5178
|
PRECOMPILE_ADDR,
|
|
4116
5179
|
PrivacyKeyManager,
|
|
4117
5180
|
PrivacyModule,
|
|
5181
|
+
RelayerStatusModule,
|
|
4118
5182
|
SLIP0044_NAMESPACE,
|
|
4119
5183
|
ShieldedPoolModule,
|
|
4120
5184
|
ShieldedPoolPrecompile,
|
|
4121
5185
|
SignatureScheme,
|
|
4122
5186
|
Storage,
|
|
4123
5187
|
SubstrateClient,
|
|
5188
|
+
VaultLockedError,
|
|
5189
|
+
WebArtifactProvider,
|
|
4124
5190
|
ZkVerifierModule,
|
|
4125
5191
|
accountIdHexToSs58,
|
|
4126
5192
|
addressToAccountIdHex,
|
|
5193
|
+
applyNoteStatus,
|
|
4127
5194
|
base58,
|
|
4128
5195
|
bigintTo32Be,
|
|
4129
5196
|
bigintTo32Le,
|
|
4130
5197
|
bigintTo32LeArr,
|
|
5198
|
+
buildDummyTransferInput,
|
|
4131
5199
|
bytesToBigintLE,
|
|
5200
|
+
computeNullifier,
|
|
4132
5201
|
computePathIndices,
|
|
4133
5202
|
connectInjectedExtension,
|
|
5203
|
+
createNoteDisclosureKey,
|
|
5204
|
+
decodeNoteDisclosureKey,
|
|
4134
5205
|
decodePrecompileCalldata,
|
|
4135
5206
|
decryptJson,
|
|
5207
|
+
decryptNoteRecord,
|
|
5208
|
+
deriveMasterKeyBytes,
|
|
4136
5209
|
deriveOwnerPk,
|
|
4137
5210
|
deriveSpendingKeyFromSignature,
|
|
4138
5211
|
deriveSpendingKeyMessage,
|
|
5212
|
+
deriveStealthOwnerPk,
|
|
5213
|
+
deriveStealthSk,
|
|
4139
5214
|
deriveVaultKey,
|
|
4140
|
-
|
|
5215
|
+
deriveViewingPublicKey,
|
|
5216
|
+
deriveViewingSecretKey,
|
|
4141
5217
|
encryptJson,
|
|
5218
|
+
encryptNote,
|
|
4142
5219
|
ensureHexPrefix,
|
|
4143
5220
|
evmAddressToAccountId,
|
|
4144
5221
|
evmToImplicitSubstrate,
|
|
@@ -4148,6 +5225,9 @@ export {
|
|
|
4148
5225
|
formatORB,
|
|
4149
5226
|
fromBase64,
|
|
4150
5227
|
fromHex,
|
|
5228
|
+
generateFeeClaimProof,
|
|
5229
|
+
generateTransferProof,
|
|
5230
|
+
generateUnshieldProof,
|
|
4151
5231
|
getInjectedExtensions,
|
|
4152
5232
|
getPolkadotSigner,
|
|
4153
5233
|
getPolkadotSignerFromPjs,
|
|
@@ -4165,6 +5245,9 @@ export {
|
|
|
4165
5245
|
mapExtrinsicArgs,
|
|
4166
5246
|
mapZkEventData,
|
|
4167
5247
|
normalizeEvmAddress,
|
|
5248
|
+
randomBlinding,
|
|
5249
|
+
recoverOwnerPkPoint,
|
|
5250
|
+
selectNotes,
|
|
4168
5251
|
shortHash,
|
|
4169
5252
|
substrateSs58ToAccountIdHex,
|
|
4170
5253
|
substrateToEvm,
|
|
@@ -4173,6 +5256,7 @@ export {
|
|
|
4173
5256
|
toTxResult,
|
|
4174
5257
|
truncateMiddle,
|
|
4175
5258
|
tryDecryptNote,
|
|
5259
|
+
tryDecryptNoteVerbose,
|
|
4176
5260
|
u128,
|
|
4177
5261
|
u64,
|
|
4178
5262
|
vaultReplacer,
|