@orbinum/sdk 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -18
- package/dist/adapters/indexeddb/index.d.mts +10 -10
- package/dist/adapters/indexeddb/index.d.ts +10 -10
- package/dist/{chunk-VYKKBXOE.mjs → chunk-54HFH63J.mjs} +493 -256
- package/dist/{index-CLpM1984.d.mts → index-B-dbSIPN.d.mts} +209 -52
- package/dist/{index-CLpM1984.d.ts → index-B-dbSIPN.d.ts} +209 -52
- package/dist/index.d.mts +977 -498
- package/dist/index.d.ts +977 -498
- package/dist/index.js +990 -512
- package/dist/index.mjs +572 -313
- package/dist/{secretStore-CF6Nse__.d.ts → secretStore-CCYo4hna.d.mts} +15 -11
- package/dist/{secretStore-CF6Nse__.d.mts → secretStore-CCYo4hna.d.ts} +15 -11
- package/dist/wallet/worker/index.d.mts +1 -1
- package/dist/wallet/worker/index.d.ts +1 -1
- package/dist/wallet/worker/index.js +428 -111
- package/dist/wallet/worker/index.mjs +3 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -15,10 +15,11 @@ import {
|
|
|
15
15
|
LEAVES_PER_TREE,
|
|
16
16
|
MAX_WORKERS,
|
|
17
17
|
NoteBuilder,
|
|
18
|
-
|
|
18
|
+
OUTGOING_EPH_WINDOW,
|
|
19
19
|
PAIRWISE_EPH_WINDOW,
|
|
20
20
|
SELF_EPH_WINDOW,
|
|
21
21
|
WORKER_CRASHED,
|
|
22
|
+
assertSecretKeyBytes,
|
|
22
23
|
base64UrlDecode,
|
|
23
24
|
base64UrlEncode,
|
|
24
25
|
bigintTo32Be,
|
|
@@ -40,18 +41,22 @@ import {
|
|
|
40
41
|
createWorkerPool,
|
|
41
42
|
decodeNoteDisclosureKey,
|
|
42
43
|
decryptHintBatch,
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
deriveOutgoingEphPk,
|
|
45
|
+
deriveOutgoingEphSk,
|
|
46
|
+
deriveOutgoingSharedSecret,
|
|
47
|
+
deriveOutgoingViewingKeyV3,
|
|
45
48
|
deriveOwnerPk,
|
|
46
49
|
derivePairwiseEphSk,
|
|
47
50
|
derivePairwiseSharedSecret,
|
|
48
51
|
deriveSelfEphSk,
|
|
49
52
|
deriveSpendingKeyFromMaster,
|
|
53
|
+
deriveSpendingKeyV3,
|
|
50
54
|
deriveStealthOwnerPk,
|
|
51
55
|
deriveStealthSk,
|
|
52
56
|
deriveViewTag,
|
|
53
57
|
deriveViewingPublicKey,
|
|
54
58
|
deriveViewingSecretKey,
|
|
59
|
+
deriveViewingSecretKeyV3,
|
|
55
60
|
ensureHexPrefix,
|
|
56
61
|
fastMulBase,
|
|
57
62
|
fastMulPoint,
|
|
@@ -62,15 +67,20 @@ import {
|
|
|
62
67
|
isAbortError,
|
|
63
68
|
isHexOfLength,
|
|
64
69
|
isSpendable,
|
|
70
|
+
isUsableSecretKey,
|
|
65
71
|
isValidLeafIndex,
|
|
66
72
|
leHexToBigint,
|
|
67
|
-
|
|
73
|
+
openRecipientBookEntry,
|
|
74
|
+
outgoingEphWindow,
|
|
68
75
|
pairwiseEphWindow,
|
|
69
|
-
|
|
76
|
+
randomBlinding,
|
|
77
|
+
reconstructOutgoingIndex,
|
|
70
78
|
recoverOwnerPkPoint,
|
|
79
|
+
recoverSentFromSharedSecret,
|
|
80
|
+
recoverSentNote,
|
|
71
81
|
scalarToHex,
|
|
72
82
|
scanAbortError,
|
|
73
|
-
|
|
83
|
+
sealRecipientBookEntry,
|
|
74
84
|
selectNotes,
|
|
75
85
|
selfEphWindow,
|
|
76
86
|
serializeMemo,
|
|
@@ -78,8 +88,8 @@ import {
|
|
|
78
88
|
treeIdOf,
|
|
79
89
|
tryDecryptNote,
|
|
80
90
|
tryDecryptNoteVerbose,
|
|
81
|
-
|
|
82
|
-
} from "./chunk-
|
|
91
|
+
unpackUsableViewingKey
|
|
92
|
+
} from "./chunk-54HFH63J.mjs";
|
|
83
93
|
|
|
84
94
|
// src/foundation/encoding/base64.ts
|
|
85
95
|
var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
@@ -119,14 +129,6 @@ function fromBase64(b64) {
|
|
|
119
129
|
return new Uint8Array(bytes);
|
|
120
130
|
}
|
|
121
131
|
|
|
122
|
-
// src/foundation/crypto/blinding.ts
|
|
123
|
-
function randomBlinding() {
|
|
124
|
-
const buf = new Uint8Array(32);
|
|
125
|
-
crypto.getRandomValues(buf);
|
|
126
|
-
const reduced = bytesToBigintLE(buf) % BN254_R;
|
|
127
|
-
return reduced === 0n ? 1n : reduced;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
132
|
// src/foundation/text/format.ts
|
|
131
133
|
var LOCALE_DECIMAL_SEP = new Intl.NumberFormat(void 0).formatToParts(1.1).find((p) => p.type === "decimal")?.value ?? ".";
|
|
132
134
|
function formatIntegerLocale(intPart) {
|
|
@@ -234,9 +236,6 @@ function parseEvmAddress(addr) {
|
|
|
234
236
|
const withPrefix = addr.startsWith("0x") ? addr : `0x${addr}`;
|
|
235
237
|
return isEvmAddress(withPrefix) ? withPrefix.toLowerCase() : null;
|
|
236
238
|
}
|
|
237
|
-
function isSs58(addr) {
|
|
238
|
-
return !addr.startsWith("0x") && addr.length >= 46 && addr.length <= 50;
|
|
239
|
-
}
|
|
240
239
|
function isEvmAddress(addr) {
|
|
241
240
|
return /^0x[0-9a-fA-F]{40}$/.test(addr);
|
|
242
241
|
}
|
|
@@ -366,11 +365,10 @@ function addressToAccountIdHex(addr) {
|
|
|
366
365
|
function addressToFieldElement(address) {
|
|
367
366
|
const accountIdHex = addressToAccountIdHex(address);
|
|
368
367
|
if (!accountIdHex) throw new Error(`Cannot resolve address to AccountId32: ${address}`);
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
for (let i = 0; i < 32; i++) {
|
|
372
|
-
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
368
|
+
if (!isHexOfLength(accountIdHex, 32)) {
|
|
369
|
+
throw new Error(`AccountId32 must be 32 bytes of hex: ${accountIdHex}`);
|
|
373
370
|
}
|
|
371
|
+
const bytes = fromHex(accountIdHex);
|
|
374
372
|
return bytesToBigintLE(bytes) % BN254_R;
|
|
375
373
|
}
|
|
376
374
|
|
|
@@ -379,7 +377,7 @@ import { chacha20poly1305 } from "@noble/ciphers/chacha.js";
|
|
|
379
377
|
import { randomBytes } from "@noble/ciphers/utils.js";
|
|
380
378
|
import { hkdf } from "@noble/hashes/hkdf.js";
|
|
381
379
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
382
|
-
import { packPoint
|
|
380
|
+
import { packPoint } from "@zk-kit/baby-jubjub";
|
|
383
381
|
var SLIP_DOMAIN = new TextEncoder().encode("orbinum-payment-slip-v1");
|
|
384
382
|
var NONCE_PREFIX = new TextEncoder().encode("SLP1");
|
|
385
383
|
var EPH_PK_SIZE = 32;
|
|
@@ -400,7 +398,7 @@ function sealPaymentSlip(recipientIvkPacked, fields) {
|
|
|
400
398
|
`PaymentSlip: recipientIvk must be 32 bytes, got ${recipientIvkPacked.length}`
|
|
401
399
|
);
|
|
402
400
|
}
|
|
403
|
-
const ivkPoint =
|
|
401
|
+
const ivkPoint = unpackUsableViewingKey(bytesToBigintLE(recipientIvkPacked));
|
|
404
402
|
if (!ivkPoint) throw new Error("PaymentSlip: invalid recipient viewing public key");
|
|
405
403
|
const ephSkScalar = bytesToBjjScalar(randomBytes(32));
|
|
406
404
|
const ephPkPacked = bigintTo32Le(packPoint(fastMulBase(ephSkScalar)));
|
|
@@ -422,7 +420,7 @@ function openPaymentSlip(recipientIvsk, envelope) {
|
|
|
422
420
|
if (envelope.length > MAX_SLIP_ENVELOPE_SIZE) return null;
|
|
423
421
|
try {
|
|
424
422
|
const ephPkPacked = envelope.subarray(0, EPH_PK_SIZE);
|
|
425
|
-
const ephPkPoint =
|
|
423
|
+
const ephPkPoint = unpackUsableViewingKey(bytesToBigintLE(ephPkPacked));
|
|
426
424
|
if (!ephPkPoint) return null;
|
|
427
425
|
const ivskScalar = bytesToBjjScalar(recipientIvsk);
|
|
428
426
|
const sharedSecret = bigintTo32Le(fastMulPoint(ephPkPoint, ivskScalar)[0]);
|
|
@@ -466,14 +464,7 @@ function decodePaymentSlip(text) {
|
|
|
466
464
|
const checksum = rest.slice(sep + 1);
|
|
467
465
|
if (slipChecksum(payload) !== checksum) return null;
|
|
468
466
|
try {
|
|
469
|
-
|
|
470
|
-
const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
471
|
-
if (clean.length % 2 !== 0) return null;
|
|
472
|
-
const bytes = new Uint8Array(clean.length / 2);
|
|
473
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
474
|
-
bytes[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
|
|
475
|
-
}
|
|
476
|
-
return bytes;
|
|
467
|
+
return fromHex(base64UrlDecode(payload));
|
|
477
468
|
} catch {
|
|
478
469
|
return null;
|
|
479
470
|
}
|
|
@@ -578,8 +569,17 @@ ${canonicalAccountId(address)}`;
|
|
|
578
569
|
|
|
579
570
|
// src/protocol/keys/PrivacyKeyManager.ts
|
|
580
571
|
import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
|
|
581
|
-
|
|
582
|
-
|
|
572
|
+
var SCHEME_BY_VERSION = { v2: "orbpriv2", v3: "orbpriv3" };
|
|
573
|
+
function usableAddressFields(ownerPkHex, ivkHex) {
|
|
574
|
+
const ownerPk = ownerPkHex.toLowerCase();
|
|
575
|
+
const ivk = ivkHex.toLowerCase();
|
|
576
|
+
if (!isHexOfLength(ownerPk, 32) || !isHexOfLength(ivk, 32)) return null;
|
|
577
|
+
if (/^0x0+$/.test(ownerPk) || /^0x0+$/.test(ivk)) return null;
|
|
578
|
+
if (!unpackUsableViewingKey(bytesToBigintLE(fromHex(ivk)))) return null;
|
|
579
|
+
return { ownerPkHex: ownerPk, viewingPublicKeyHex: ivk };
|
|
580
|
+
}
|
|
581
|
+
function privacyAddressChecksum(scheme, ownerPkHex, ivkHex) {
|
|
582
|
+
const body = `${scheme}:${ownerPkHex}:${ivkHex}`;
|
|
583
583
|
const digest = sha2562(new TextEncoder().encode(body));
|
|
584
584
|
return toHex(digest.slice(0, 4)).slice(2);
|
|
585
585
|
}
|
|
@@ -589,30 +589,29 @@ var PrivacyKeyManager = class {
|
|
|
589
589
|
masterBytes: null,
|
|
590
590
|
viewingSecretKey: null,
|
|
591
591
|
viewingPublicKeyPacked: null,
|
|
592
|
-
ownerPk: null
|
|
593
|
-
outgoingViewingKey: null
|
|
592
|
+
ownerPk: null
|
|
594
593
|
};
|
|
595
594
|
/**
|
|
596
595
|
* Load a spending key and its corresponding master bytes into the in-memory session.
|
|
597
596
|
* Derives viewingSecretKey, viewingPublicKeyPacked, and ownerPk immediately.
|
|
598
597
|
* Replaces any previously loaded key.
|
|
599
598
|
*
|
|
600
|
-
* @param
|
|
601
|
-
*
|
|
602
|
-
*
|
|
599
|
+
* @param _legacySpendingKey IGNORED. Kept so v2-era callers still compile;
|
|
600
|
+
* the scalar is re-derived from `masterBytes`.
|
|
601
|
+
* @param masterBytes Raw 32-byte HKDF output. The root every v3 branch and
|
|
602
|
+
* the stable vault key hang off.
|
|
603
603
|
*/
|
|
604
|
-
async load(
|
|
605
|
-
const
|
|
604
|
+
async load(_legacySpendingKey, masterBytes) {
|
|
605
|
+
const spendingKey = deriveSpendingKeyV3(masterBytes);
|
|
606
|
+
const viewingSecretKey = deriveViewingSecretKeyV3(masterBytes);
|
|
606
607
|
const viewingPublicKeyPacked = deriveViewingPublicKey(viewingSecretKey);
|
|
607
608
|
const ownerPk = deriveOwnerPk(spendingKey);
|
|
608
|
-
const outgoingViewingKey = deriveOutgoingViewingKey(masterBytes);
|
|
609
609
|
this._state = {
|
|
610
610
|
spendingKey,
|
|
611
611
|
masterBytes,
|
|
612
612
|
viewingSecretKey,
|
|
613
613
|
viewingPublicKeyPacked,
|
|
614
|
-
ownerPk
|
|
615
|
-
outgoingViewingKey
|
|
614
|
+
ownerPk
|
|
616
615
|
};
|
|
617
616
|
}
|
|
618
617
|
/** Clear all key material from memory. Call on vault lock / sign-out. */
|
|
@@ -622,8 +621,7 @@ var PrivacyKeyManager = class {
|
|
|
622
621
|
masterBytes: null,
|
|
623
622
|
viewingSecretKey: null,
|
|
624
623
|
viewingPublicKeyPacked: null,
|
|
625
|
-
ownerPk: null
|
|
626
|
-
outgoingViewingKey: null
|
|
624
|
+
ownerPk: null
|
|
627
625
|
};
|
|
628
626
|
}
|
|
629
627
|
/** Returns true if a spending key has been loaded. */
|
|
@@ -661,6 +659,24 @@ var PrivacyKeyManager = class {
|
|
|
661
659
|
}
|
|
662
660
|
return this._state.viewingPublicKeyPacked;
|
|
663
661
|
}
|
|
662
|
+
/**
|
|
663
|
+
* The OUTGOING viewing key (ovk) — reads what this wallet SENT.
|
|
664
|
+
*
|
|
665
|
+
* Seeds the outgoing ephemeral sequence and opens the recipient book, so a
|
|
666
|
+
* holder can enumerate every payment the wallet made and name who received
|
|
667
|
+
* it. That makes it strictly more sensitive than the incoming viewing key,
|
|
668
|
+
* which yields amounts and no counterparties: hand it out only where the
|
|
669
|
+
* payment graph is meant to travel with it.
|
|
670
|
+
*
|
|
671
|
+
* A scan without it still finds every note the wallet OWNS and simply
|
|
672
|
+
* reconstructs no sent history — the watch-only case.
|
|
673
|
+
*
|
|
674
|
+
* SECURITY: never expose this in an address or a network request.
|
|
675
|
+
* Throws if not loaded.
|
|
676
|
+
*/
|
|
677
|
+
getOutgoingViewingKey() {
|
|
678
|
+
return deriveOutgoingViewingKeyV3(this.getMasterBytes());
|
|
679
|
+
}
|
|
664
680
|
/** Returns the BabyJubJub owner public key (x-coordinate). Throws if not loaded. */
|
|
665
681
|
getOwnerPk() {
|
|
666
682
|
if (this._state.ownerPk === null) {
|
|
@@ -668,16 +684,6 @@ var PrivacyKeyManager = class {
|
|
|
668
684
|
}
|
|
669
685
|
return this._state.ownerPk;
|
|
670
686
|
}
|
|
671
|
-
/**
|
|
672
|
-
* Returns the 32-byte outgoing viewing key (ovk). Throws if not loaded.
|
|
673
|
-
* Used to seal/open the outgoing blob that lets the sender recover a transfer.
|
|
674
|
-
*/
|
|
675
|
-
getOutgoingViewingKey() {
|
|
676
|
-
if (this._state.outgoingViewingKey === null) {
|
|
677
|
-
throw new Error("PrivacyKeyManager: no key loaded. Call load() first.");
|
|
678
|
-
}
|
|
679
|
-
return this._state.outgoingViewingKey;
|
|
680
|
-
}
|
|
681
687
|
/** Returns the spending key as a 32-byte little-endian Uint8Array. Throws if not loaded. */
|
|
682
688
|
getSpendingKeyBytes() {
|
|
683
689
|
return bigintTo32Le(this.getSpendingKey());
|
|
@@ -706,7 +712,7 @@ var PrivacyKeyManager = class {
|
|
|
706
712
|
* Exports a shareable privacy address encoding the owner public key and
|
|
707
713
|
* viewing PUBLIC key of the currently loaded identity.
|
|
708
714
|
*
|
|
709
|
-
* Format: `
|
|
715
|
+
* Format: `orbpriv3:{ownerPk_hex}:{viewingPublicKey_hex}:{checksum}`
|
|
710
716
|
*
|
|
711
717
|
* The recipient uses this address so the sender can:
|
|
712
718
|
* 1. Embed `ownerPk` in the note commitment (Poseidon4 input).
|
|
@@ -721,44 +727,64 @@ var PrivacyKeyManager = class {
|
|
|
721
727
|
*
|
|
722
728
|
* Throws if no key is loaded.
|
|
723
729
|
*/
|
|
724
|
-
encodePrivacyAddress() {
|
|
730
|
+
encodePrivacyAddress(version = "v3") {
|
|
725
731
|
const ownerPk = this.getOwnerPk();
|
|
726
732
|
const ivkPacked = this.getViewingPublicKeyPacked();
|
|
727
733
|
const ownerPkHex = scalarToHex(ownerPk);
|
|
728
734
|
const ivkHex = toHex(ivkPacked);
|
|
729
|
-
const
|
|
730
|
-
|
|
735
|
+
const scheme = SCHEME_BY_VERSION[version];
|
|
736
|
+
const checksum = privacyAddressChecksum(scheme, ownerPkHex, ivkHex);
|
|
737
|
+
return `${scheme}:${ownerPkHex}:${ivkHex}:${checksum}`;
|
|
731
738
|
}
|
|
732
739
|
/**
|
|
733
740
|
* Decode a privacy address into `{ ownerPkHex, viewingPublicKeyHex }`, or
|
|
734
741
|
* `null` if it does not parse.
|
|
735
742
|
*
|
|
736
|
-
*
|
|
737
|
-
*
|
|
738
|
-
*
|
|
739
|
-
*
|
|
740
|
-
*
|
|
743
|
+
* Reads every scheme ever emitted; only `orbpriv3` is written.
|
|
744
|
+
*
|
|
745
|
+
* - `orbpriv3` / `orbpriv2` — checksum verified; a mismatch (corrupted
|
|
746
|
+
* paste, or a relabelled prefix) returns null.
|
|
747
|
+
* - `orbpriv1` — legacy, predates the checksum entirely. The FIELDS are
|
|
748
|
+
* validated exactly as above, since this branch has no integrity check
|
|
749
|
+
* of its own.
|
|
750
|
+
*
|
|
751
|
+
* Reading an old address is not the same as supporting an old identity. An
|
|
752
|
+
* address is an owner key and a viewing key; paying one works whatever
|
|
753
|
+
* scheme derived it, and whoever shares it may not have upgraded yet.
|
|
754
|
+
* Refusing them here would break incoming payments for no gain.
|
|
741
755
|
*/
|
|
742
756
|
static decodePrivacyAddress(address) {
|
|
743
757
|
const parts = address.split(":");
|
|
744
|
-
|
|
758
|
+
for (const version of ["v2", "v3"]) {
|
|
759
|
+
if (parts[0] !== SCHEME_BY_VERSION[version]) continue;
|
|
745
760
|
if (parts.length !== 4) return null;
|
|
746
761
|
const [, ownerPkHex, viewingPublicKeyHex, checksum] = parts;
|
|
747
762
|
if (!ownerPkHex || !viewingPublicKeyHex || !checksum) return null;
|
|
748
|
-
if (privacyAddressChecksum(
|
|
749
|
-
|
|
763
|
+
if (privacyAddressChecksum(
|
|
764
|
+
SCHEME_BY_VERSION[version],
|
|
765
|
+
ownerPkHex,
|
|
766
|
+
viewingPublicKeyHex
|
|
767
|
+
) !== checksum) {
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
const fields = usableAddressFields(ownerPkHex, viewingPublicKeyHex);
|
|
771
|
+
return fields ? { ...fields, scheme: SCHEME_BY_VERSION[version] } : null;
|
|
750
772
|
}
|
|
751
773
|
if (parts[0] === "orbpriv1") {
|
|
752
774
|
if (parts.length !== 3) return null;
|
|
753
775
|
const [, ownerPkHex, viewingPublicKeyHex] = parts;
|
|
754
776
|
if (!ownerPkHex || !viewingPublicKeyHex) return null;
|
|
755
|
-
|
|
777
|
+
const fields = usableAddressFields(ownerPkHex, viewingPublicKeyHex);
|
|
778
|
+
return fields ? { ...fields, scheme: "orbpriv1" } : null;
|
|
756
779
|
}
|
|
757
780
|
return null;
|
|
758
781
|
}
|
|
759
782
|
/**
|
|
760
783
|
* Load keys from a cached "mk:0x{masterBytes_hex}" string produced by exportHex().
|
|
761
|
-
*
|
|
784
|
+
*
|
|
785
|
+
* Throws on a bad prefix, on non-hex content, and on a master that is not
|
|
786
|
+
* 32 usable bytes — an all-zero one included, since every wallet would
|
|
787
|
+
* derive the same identity from it.
|
|
762
788
|
*/
|
|
763
789
|
async importFromHex(hex) {
|
|
764
790
|
if (!hex.startsWith("mk:")) {
|
|
@@ -766,15 +792,9 @@ var PrivacyKeyManager = class {
|
|
|
766
792
|
'PrivacyKeyManager: invalid cache format. Expected "mk:0x{masterBytes_hex}".'
|
|
767
793
|
);
|
|
768
794
|
}
|
|
769
|
-
const
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
if (masterBytes.length !== 32) {
|
|
773
|
-
throw new Error("PrivacyKeyManager: invalid master bytes \u2014 expected 32 bytes.");
|
|
774
|
-
}
|
|
775
|
-
const masterBigint = BigInt("0x" + h);
|
|
776
|
-
const sk = masterBigint % BABYJUB_SUBORDER || 1n;
|
|
777
|
-
await this.load(sk, masterBytes);
|
|
795
|
+
const masterBytes = fromHex(hex.slice(3));
|
|
796
|
+
assertSecretKeyBytes(masterBytes, "PrivacyKeyManager: master");
|
|
797
|
+
await this.load(0n, masterBytes);
|
|
778
798
|
}
|
|
779
799
|
};
|
|
780
800
|
|
|
@@ -1095,6 +1115,25 @@ function wsUrlToHttp(wsUrl) {
|
|
|
1095
1115
|
}
|
|
1096
1116
|
|
|
1097
1117
|
// src/chain/substrate/SubstrateClient.ts
|
|
1118
|
+
var TIMESTAMP_PALLET = 1;
|
|
1119
|
+
function decodeCompact(bytes, offset) {
|
|
1120
|
+
const first = bytes[offset];
|
|
1121
|
+
if (first === void 0) return null;
|
|
1122
|
+
const mode = first & 3;
|
|
1123
|
+
if (mode === 0) return first >>> 2;
|
|
1124
|
+
if (mode === 1) {
|
|
1125
|
+
const b1 = bytes[offset + 1];
|
|
1126
|
+
return b1 === void 0 ? null : (first >>> 2 | b1 << 6) >>> 0;
|
|
1127
|
+
}
|
|
1128
|
+
const width = mode === 2 ? 4 : (first >>> 2) + 5;
|
|
1129
|
+
if (offset + width > bytes.length) return null;
|
|
1130
|
+
let value = 0n;
|
|
1131
|
+
const start = mode === 2 ? offset : offset + 1;
|
|
1132
|
+
const end = mode === 2 ? offset + 4 : offset + width;
|
|
1133
|
+
for (let i = end - 1; i >= start; i--) value = value << 8n | BigInt(bytes[i]);
|
|
1134
|
+
if (mode === 2) value >>= 2n;
|
|
1135
|
+
return value > BigInt(Number.MAX_SAFE_INTEGER) ? null : Number(value);
|
|
1136
|
+
}
|
|
1098
1137
|
var SubstrateClient = class _SubstrateClient {
|
|
1099
1138
|
constructor(_papi, _httpUrl, _owned) {
|
|
1100
1139
|
this._papi = _papi;
|
|
@@ -1292,25 +1331,17 @@ var SubstrateClient = class _SubstrateClient {
|
|
|
1292
1331
|
}
|
|
1293
1332
|
}
|
|
1294
1333
|
if (!timestampMs) {
|
|
1295
|
-
const
|
|
1334
|
+
const ts = extrinsics.reduce((found, hex) => {
|
|
1335
|
+
if (found !== null) return found;
|
|
1296
1336
|
try {
|
|
1297
1337
|
const b = fromHex(hex);
|
|
1298
|
-
|
|
1338
|
+
if (b.length < 5 || b[2] !== TIMESTAMP_PALLET || b[3] !== 0) return null;
|
|
1339
|
+
return decodeCompact(b, 4);
|
|
1299
1340
|
} catch {
|
|
1300
|
-
return
|
|
1341
|
+
return null;
|
|
1301
1342
|
}
|
|
1302
|
-
});
|
|
1303
|
-
if (
|
|
1304
|
-
try {
|
|
1305
|
-
const b = fromHex(tsHex);
|
|
1306
|
-
const view = new DataView(b.buffer, b.byteOffset + 6, 8);
|
|
1307
|
-
const lo = view.getUint32(0, true);
|
|
1308
|
-
const hi = view.getUint32(4, true);
|
|
1309
|
-
const ms = lo + hi * 4294967296;
|
|
1310
|
-
if (ms > 0) timestampMs = ms;
|
|
1311
|
-
} catch {
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1343
|
+
}, null);
|
|
1344
|
+
if (ts !== null && ts > 0) timestampMs = ts;
|
|
1314
1345
|
}
|
|
1315
1346
|
const author = _SubstrateClient.extractAuthorFromLogs(header.digest.logs, ss58Prefix);
|
|
1316
1347
|
return { header, extrinsics, timestampMs, author };
|
|
@@ -1580,7 +1611,10 @@ var EvmClient = class {
|
|
|
1580
1611
|
}
|
|
1581
1612
|
/**
|
|
1582
1613
|
* Performs multiple JSON-RPC calls in a single HTTP request (batch).
|
|
1583
|
-
*
|
|
1614
|
+
*
|
|
1615
|
+
* Results come back in the order of `calls`, as a typed tuple. A call the
|
|
1616
|
+
* server answered with an error, or did not answer at all, lands as `null`
|
|
1617
|
+
* in its own slot — never shifting the ones after it.
|
|
1584
1618
|
*/
|
|
1585
1619
|
async batchRequest(calls) {
|
|
1586
1620
|
const body = calls.map((c, i) => ({
|
|
@@ -1592,8 +1626,8 @@ var EvmClient = class {
|
|
|
1592
1626
|
const res = await postJsonWithRetry(this.rpcUrl, JSON.stringify(body));
|
|
1593
1627
|
if (!res.ok) throw new Error(`EVM HTTP ${res.status}: ${res.statusText}`);
|
|
1594
1628
|
const arr = await res.json();
|
|
1595
|
-
arr.
|
|
1596
|
-
return
|
|
1629
|
+
const byId = new Map(arr.map((r) => [r.id, r]));
|
|
1630
|
+
return calls.map((_, i) => byId.get(i + 1)?.result ?? null);
|
|
1597
1631
|
}
|
|
1598
1632
|
// ─── Convenience wrappers ─────────────────────────────────────────────────
|
|
1599
1633
|
/** Returns the native token balance (in wei) for an EVM address. */
|
|
@@ -1639,7 +1673,10 @@ var EvmClient = class {
|
|
|
1639
1673
|
if (from) txObj["from"] = from;
|
|
1640
1674
|
return this.request("eth_call", [txObj, "latest"]);
|
|
1641
1675
|
}
|
|
1642
|
-
/**
|
|
1676
|
+
/**
|
|
1677
|
+
* Estimates a transaction's gas, in GAS UNITS — not wei. Multiply by
|
|
1678
|
+
* `getGasPrice()` for a cost.
|
|
1679
|
+
*/
|
|
1643
1680
|
async estimateGas(params) {
|
|
1644
1681
|
const hex = await this.request("eth_estimateGas", [params]);
|
|
1645
1682
|
return hexToBigint(hex);
|
|
@@ -1890,7 +1927,7 @@ var EvmExplorer = class _EvmExplorer {
|
|
|
1890
1927
|
// --- Address ---
|
|
1891
1928
|
/**
|
|
1892
1929
|
* Returns aggregated on-chain data for an EVM address: balance, nonce,
|
|
1893
|
-
* bytecode (truncated), and up to 50
|
|
1930
|
+
* bytecode (truncated), and up to 50 logs it EMITTED in the last 1 001 blocks.
|
|
1894
1931
|
*/
|
|
1895
1932
|
async getAddressInfo(address) {
|
|
1896
1933
|
const latest = await this.evm.getBlockNumber().catch(() => 0);
|
|
@@ -1973,7 +2010,7 @@ var EvmExplorer = class _EvmExplorer {
|
|
|
1973
2010
|
};
|
|
1974
2011
|
}
|
|
1975
2012
|
/**
|
|
1976
|
-
* Returns ERC-20 `Transfer` events for `address` from the last 1
|
|
2013
|
+
* Returns ERC-20 `Transfer` events for `address` from the last 1 001 blocks.
|
|
1977
2014
|
* When `holderAddress` is provided, restricts results to transfers sent or received by that address.
|
|
1978
2015
|
*/
|
|
1979
2016
|
async getTokenTransfers(address, holderAddress) {
|
|
@@ -2200,7 +2237,11 @@ var ShieldedPoolModule = class {
|
|
|
2200
2237
|
* Withdraws tokens from the shielded pool to a public address.
|
|
2201
2238
|
* Submits as an UNSIGNED (gasless) transaction — fee is embedded in the ZK proof.
|
|
2202
2239
|
* Pass a `signer` to fall back to signed submission (e.g. for testing).
|
|
2203
|
-
* Extrinsic: shieldedPool.unshield(proof, merkleRoot, nullifier, assetId, amount, recipient, fee, changeCommitment, changeEncryptedMemo,
|
|
2240
|
+
* Extrinsic: shieldedPool.unshield(proof, merkleRoot, nullifier, assetId, amount, recipient, fee, changeCommitment, changeEncryptedMemo, circuitVersion)
|
|
2241
|
+
*
|
|
2242
|
+
* The relay fee recipient is NOT a parameter: the chain takes it from the
|
|
2243
|
+
* dispatch origin. Submitting unsigned credits the block author; submitting
|
|
2244
|
+
* through the EVM precompile credits whoever signed that transaction.
|
|
2204
2245
|
*/
|
|
2205
2246
|
async unshield(params, signer, options) {
|
|
2206
2247
|
const entry = resolveTx(this.substrate.unsafe, "ShieldedPool", "unshield");
|
|
@@ -2224,8 +2265,6 @@ var ShieldedPoolModule = class {
|
|
|
2224
2265
|
fee: params.fee ?? 0n,
|
|
2225
2266
|
change_commitment: changeCommitment,
|
|
2226
2267
|
change_encrypted_memo: changeEncryptedMemo,
|
|
2227
|
-
relayer: void 0,
|
|
2228
|
-
// Option<H160> — None for direct Substrate submissions
|
|
2229
2268
|
circuit_version: params.circuitVersion
|
|
2230
2269
|
});
|
|
2231
2270
|
if (signer) {
|
|
@@ -2237,7 +2276,11 @@ var ShieldedPoolModule = class {
|
|
|
2237
2276
|
* Performs a private (shielded) transfer between two notes.
|
|
2238
2277
|
* Submits as an UNSIGNED (gasless) transaction — fee is embedded in the ZK proof.
|
|
2239
2278
|
* Pass a `signer` to fall back to signed submission (e.g. for testing).
|
|
2240
|
-
* Extrinsic: shieldedPool.privateTransfer(proof, merkleRoot, nullifiers, commitments, memos, assetId, fee,
|
|
2279
|
+
* Extrinsic: shieldedPool.privateTransfer(proof, merkleRoot, nullifiers, commitments, memos, assetId, fee, circuitVersion)
|
|
2280
|
+
*
|
|
2281
|
+
* The relay fee recipient is NOT a parameter: the chain takes it from the
|
|
2282
|
+
* dispatch origin. Submitting unsigned credits the block author; submitting
|
|
2283
|
+
* through the EVM precompile credits whoever signed that transaction.
|
|
2241
2284
|
*/
|
|
2242
2285
|
async privateTransfer(params, signer, options) {
|
|
2243
2286
|
const nullifiers = params.inputs.map((inp) => inp.nullifier);
|
|
@@ -2258,8 +2301,6 @@ var ShieldedPoolModule = class {
|
|
|
2258
2301
|
encrypted_memos: memos,
|
|
2259
2302
|
asset_id: params.assetId,
|
|
2260
2303
|
fee: params.fee ?? 0n,
|
|
2261
|
-
relayer: void 0,
|
|
2262
|
-
// Option<H160> — None for direct Substrate submissions
|
|
2263
2304
|
circuit_version: params.circuitVersion
|
|
2264
2305
|
});
|
|
2265
2306
|
if (signer) {
|
|
@@ -2342,7 +2383,13 @@ var PrivacyModule = class {
|
|
|
2342
2383
|
async getMerkleRoot() {
|
|
2343
2384
|
return this.substrate.request("privacy_getMerkleRoot", []);
|
|
2344
2385
|
}
|
|
2345
|
-
/**
|
|
2386
|
+
/**
|
|
2387
|
+
* Returns the Merkle proof for a LEAF INDEX.
|
|
2388
|
+
*
|
|
2389
|
+
* The node declares this parameter `u32`, so a commitment hex is rejected
|
|
2390
|
+
* by deserialisation before any lookup runs — `getMerkleProofByCommitment`
|
|
2391
|
+
* is the one that takes a string.
|
|
2392
|
+
*/
|
|
2346
2393
|
async getMerkleProof(leafIndex) {
|
|
2347
2394
|
const raw = await this.substrate.request("privacy_getMerkleProof", [
|
|
2348
2395
|
leafIndex
|
|
@@ -2776,7 +2823,9 @@ var ShieldedPoolPrecompile = class {
|
|
|
2776
2823
|
* hidden by design, so any address (a relayer included) can submit a valid
|
|
2777
2824
|
* proof.
|
|
2778
2825
|
*
|
|
2779
|
-
* Extrinsic: `shieldedPool.privateTransfer(proof, merkleRoot, nullifiers,
|
|
2826
|
+
* Extrinsic: `shieldedPool.privateTransfer(proof, merkleRoot, nullifiers,
|
|
2827
|
+
* commitments, memos, assetId, fee, circuitVersion)` — eight arguments; see
|
|
2828
|
+
* `buildPrivateTransferCalldata` for the encoding order.
|
|
2780
2829
|
*/
|
|
2781
2830
|
async privateTransfer(params, signer) {
|
|
2782
2831
|
return signer({ to: this.addr, data: buildPrivateTransferCalldata(params) });
|
|
@@ -2788,7 +2837,14 @@ var ShieldedPoolPrecompile = class {
|
|
|
2788
2837
|
* `params.recipientAddress` must be a 0x-prefixed AccountId32. To send to an
|
|
2789
2838
|
* EVM address, derive it first with `evmToImplicitSubstrate(evmAddr)`.
|
|
2790
2839
|
*
|
|
2791
|
-
* Extrinsic: `shieldedPool.unshield(proof, merkleRoot, nullifier, assetId,
|
|
2840
|
+
* Extrinsic: `shieldedPool.unshield(proof, merkleRoot, nullifier, assetId,
|
|
2841
|
+
* amount, recipient, fee, changeCommitment, changeEncryptedMemo,
|
|
2842
|
+
* circuitVersion)` — ten arguments; see `buildUnshieldCalldata`.
|
|
2843
|
+
*
|
|
2844
|
+
* **The relay fee goes to whoever `signer` is.** The chain takes the recipient
|
|
2845
|
+
* from `msg.sender`, not from calldata, so the account behind this signer is
|
|
2846
|
+
* the one credited — and it is also the one paying gas. Relaying on someone
|
|
2847
|
+
* else's behalf and being paid for it is the same act here.
|
|
2792
2848
|
*/
|
|
2793
2849
|
async unshield(params, signer) {
|
|
2794
2850
|
return signer({ to: this.addr, data: buildUnshieldCalldata(params) });
|
|
@@ -2807,7 +2863,9 @@ var ShieldedPoolPrecompile = class {
|
|
|
2807
2863
|
* The `msg.sender` address is the validator identity, and must match the
|
|
2808
2864
|
* one with pending fees.
|
|
2809
2865
|
*
|
|
2810
|
-
* Extrinsic: `shieldedPool.claim_shielded_fees(commitment, amount, assetId,
|
|
2866
|
+
* Extrinsic: `shieldedPool.claim_shielded_fees(commitment, amount, assetId,
|
|
2867
|
+
* memo, proof, publicSignals, circuitVersion)` — seven arguments; see
|
|
2868
|
+
* `buildClaimShieldedFeesCalldata`.
|
|
2811
2869
|
*/
|
|
2812
2870
|
async claimShieldedFees(params, signer) {
|
|
2813
2871
|
return signer({
|
|
@@ -2821,7 +2879,12 @@ var ShieldedPoolPrecompile = class {
|
|
|
2821
2879
|
// AccountId32, so estimating from a different address measures a different
|
|
2822
2880
|
// call.
|
|
2823
2881
|
async estimateShieldGas(params, from) {
|
|
2824
|
-
return this.evm.estimateGas({
|
|
2882
|
+
return this.evm.estimateGas({
|
|
2883
|
+
from,
|
|
2884
|
+
to: this.addr,
|
|
2885
|
+
data: buildShieldCalldata(params),
|
|
2886
|
+
value: `0x${params.amount.toString(16)}`
|
|
2887
|
+
});
|
|
2825
2888
|
}
|
|
2826
2889
|
async estimatePrivateTransferGas(params, from) {
|
|
2827
2890
|
return this.evm.estimateGas({
|
|
@@ -3276,9 +3339,13 @@ var OrbinumClientProvider = class _OrbinumClientProvider {
|
|
|
3276
3339
|
// ─── Reconnection ───────────────────────────────────────────────────────
|
|
3277
3340
|
/**
|
|
3278
3341
|
* Schedules the next connection attempt using exponential backoff
|
|
3279
|
-
* (capped at `reconnectMaxMs`) with
|
|
3280
|
-
* `'reconnecting'`.
|
|
3281
|
-
*
|
|
3342
|
+
* (capped at `reconnectMaxMs`) with EQUAL jitter, then transitions to
|
|
3343
|
+
* `'reconnecting'`.
|
|
3344
|
+
*
|
|
3345
|
+
* The delay lands in `[capped/2, capped]` — half fixed, half random. That
|
|
3346
|
+
* spreads a shared outage's reconnects over a `capped/2` window instead of
|
|
3347
|
+
* having every client retry in lockstep, while keeping a floor so the first
|
|
3348
|
+
* attempts do not all pile up near zero the way full jitter allows.
|
|
3282
3349
|
*/
|
|
3283
3350
|
scheduleReconnect() {
|
|
3284
3351
|
if (this._reconnectTimer) clearTimeout(this._reconnectTimer);
|
|
@@ -3670,6 +3737,16 @@ function mapZkEventData(method, data) {
|
|
|
3670
3737
|
index: get(4, "leaf_index") || get(4, "index")
|
|
3671
3738
|
};
|
|
3672
3739
|
}
|
|
3740
|
+
if (m === "nullifiersspent") {
|
|
3741
|
+
return { nullifiers: get(0, "nullifiers") };
|
|
3742
|
+
}
|
|
3743
|
+
if (m === "commitmentsinserted") {
|
|
3744
|
+
return {
|
|
3745
|
+
commitments: get(0, "commitments"),
|
|
3746
|
+
memos: get(1, "encrypted_memos") || get(1, "memos"),
|
|
3747
|
+
indices: get(2, "leaf_indices") || get(2, "indices")
|
|
3748
|
+
};
|
|
3749
|
+
}
|
|
3673
3750
|
if (m === "privatetransfer") {
|
|
3674
3751
|
return {
|
|
3675
3752
|
nullifiers: get(0, "nullifiers"),
|
|
@@ -3711,12 +3788,12 @@ function mapZkEventData(method, data) {
|
|
|
3711
3788
|
if (m_norm === "log") {
|
|
3712
3789
|
return { log: get(0, "log") };
|
|
3713
3790
|
}
|
|
3714
|
-
if (m_norm === "created" || m_norm === "createdfailed" || m_norm === "
|
|
3791
|
+
if (m_norm === "created" || m_norm === "createdfailed" || m_norm === "executedfailed") {
|
|
3715
3792
|
return { address: get(0, "address") };
|
|
3716
3793
|
}
|
|
3717
3794
|
if (m_norm === "executed") {
|
|
3718
3795
|
return {
|
|
3719
|
-
from: get(0, "from"),
|
|
3796
|
+
from: get(0, "from") ?? get(0, "address"),
|
|
3720
3797
|
to: get(1, "to"),
|
|
3721
3798
|
transaction_hash: get(2, "transaction_hash"),
|
|
3722
3799
|
exit_reason: get(3, "exit_reason")
|
|
@@ -3863,10 +3940,10 @@ var ERROR_KINDS = {
|
|
|
3863
3940
|
CommitmentAlreadyExists: "shape",
|
|
3864
3941
|
UnknownMerkleRoot: "stale-proof",
|
|
3865
3942
|
// Amounts and fees
|
|
3866
|
-
AmountTooSmall: "amount",
|
|
3867
3943
|
InvalidAmount: "amount",
|
|
3868
3944
|
InsufficientPoolBalance: "amount",
|
|
3869
3945
|
FeeTooLow: "amount",
|
|
3946
|
+
InsufficientPendingFees: "amount",
|
|
3870
3947
|
// Proofs
|
|
3871
3948
|
InvalidProof: "proof",
|
|
3872
3949
|
ProofVerificationFailed: "proof",
|
|
@@ -3875,12 +3952,14 @@ var ERROR_KINDS = {
|
|
|
3875
3952
|
InvalidAssetId: "asset",
|
|
3876
3953
|
AssetNotVerified: "asset",
|
|
3877
3954
|
AssetIdMismatch: "asset",
|
|
3955
|
+
AssetIdAlreadyExists: "asset",
|
|
3878
3956
|
// Request shape
|
|
3879
3957
|
InvalidMemoSize: "shape",
|
|
3880
3958
|
MemoCommitmentMismatch: "shape",
|
|
3881
3959
|
TooManyInputsOrOutputs: "shape",
|
|
3882
3960
|
InvalidRecipient: "shape",
|
|
3883
3961
|
EmptyBatch: "shape",
|
|
3962
|
+
FeeRecipientUnavailable: "shape",
|
|
3884
3963
|
// Capacity
|
|
3885
3964
|
MerkleTreeFull: "capacity",
|
|
3886
3965
|
// Balances pallet, reached through the same path
|
|
@@ -3935,6 +4014,7 @@ function methodOf(fnSig) {
|
|
|
3935
4014
|
if (fnSig.startsWith("unshield(")) return "unshield";
|
|
3936
4015
|
if (fnSig.startsWith("privateTransfer(")) return "privateTransfer";
|
|
3937
4016
|
if (fnSig.startsWith("shieldBatch(")) return "shieldBatch";
|
|
4017
|
+
if (fnSig.startsWith("claimShieldedFees(")) return "claimShieldedFees";
|
|
3938
4018
|
if (fnSig.startsWith("shield(")) return "shield";
|
|
3939
4019
|
return null;
|
|
3940
4020
|
}
|
|
@@ -4238,6 +4318,7 @@ function buildConfig(existing, chainFingerprint) {
|
|
|
4238
4318
|
updatedAt: now,
|
|
4239
4319
|
...chainFingerprint ? { chainFingerprint } : {},
|
|
4240
4320
|
...existing?.selfEphCounter !== void 0 ? { selfEphCounter: existing.selfEphCounter } : {},
|
|
4321
|
+
...existing?.outgoingEphCounter !== void 0 ? { outgoingEphCounter: existing.outgoingEphCounter } : {},
|
|
4241
4322
|
...existing?.pairwiseCounterparties !== void 0 ? { pairwiseCounterparties: existing.pairwiseCounterparties } : {}
|
|
4242
4323
|
};
|
|
4243
4324
|
}
|
|
@@ -4249,6 +4330,7 @@ async function writeConfig(storage, fallback, chainFingerprint) {
|
|
|
4249
4330
|
}
|
|
4250
4331
|
function mergeCounters(current, fallback) {
|
|
4251
4332
|
const selfEphCounter = higher(current.selfEphCounter, fallback?.selfEphCounter);
|
|
4333
|
+
const outgoingEphCounter = higher(current.outgoingEphCounter, fallback?.outgoingEphCounter);
|
|
4252
4334
|
const parties = mergePairwise(current.pairwiseCounterparties, fallback?.pairwiseCounterparties);
|
|
4253
4335
|
return {
|
|
4254
4336
|
createdAt: current.createdAt ?? fallback?.createdAt,
|
|
@@ -4256,6 +4338,7 @@ function mergeCounters(current, fallback) {
|
|
|
4256
4338
|
// is exactOptionalPropertyTypes, so an explicit undefined is a type
|
|
4257
4339
|
// error AND would round-trip through structured clone as a present key.
|
|
4258
4340
|
...selfEphCounter !== void 0 ? { selfEphCounter } : {},
|
|
4341
|
+
...outgoingEphCounter !== void 0 ? { outgoingEphCounter } : {},
|
|
4259
4342
|
...parties !== void 0 ? { pairwiseCounterparties: parties } : {}
|
|
4260
4343
|
};
|
|
4261
4344
|
}
|
|
@@ -4293,6 +4376,7 @@ function mergePairwise(current, fallback) {
|
|
|
4293
4376
|
|
|
4294
4377
|
// src/wallet/vault/storage/ephemeralIndex.ts
|
|
4295
4378
|
var MAX_EPH_INDEX = 4294967294;
|
|
4379
|
+
var RESTORE_SAFETY_GAP = 8;
|
|
4296
4380
|
function usableCounter(value) {
|
|
4297
4381
|
if (typeof value !== "number" || !Number.isInteger(value)) return null;
|
|
4298
4382
|
if (value < 0 || value > MAX_EPH_INDEX) return null;
|
|
@@ -4307,27 +4391,51 @@ async function reserveSelfEphIndex(storage) {
|
|
|
4307
4391
|
if (!updated) throw new Error("vault not initialized");
|
|
4308
4392
|
return (usableCounter(updated.selfEphCounter) ?? 1) - 1;
|
|
4309
4393
|
}
|
|
4310
|
-
async function
|
|
4394
|
+
async function reserveOutgoingIndex(storage, fromChain) {
|
|
4395
|
+
const recovered = usableCounter(fromChain);
|
|
4396
|
+
let exhausted = false;
|
|
4397
|
+
const updated = await storage.updateConfig((config) => {
|
|
4398
|
+
const raw = config.outgoingEphCounter;
|
|
4399
|
+
if (typeof raw === "number" && Number.isSafeInteger(raw) && raw > MAX_EPH_INDEX) {
|
|
4400
|
+
exhausted = true;
|
|
4401
|
+
return config;
|
|
4402
|
+
}
|
|
4403
|
+
exhausted = false;
|
|
4404
|
+
const stored = usableCounter(raw) ?? 0;
|
|
4405
|
+
const floor = recovered !== null && recovered > stored ? Math.min(recovered + RESTORE_SAFETY_GAP, MAX_EPH_INDEX) : stored;
|
|
4406
|
+
return {
|
|
4407
|
+
...config,
|
|
4408
|
+
outgoingEphCounter: floor + 1,
|
|
4409
|
+
updatedAt: Date.now()
|
|
4410
|
+
};
|
|
4411
|
+
});
|
|
4412
|
+
if (!updated) throw new Error("vault not initialized");
|
|
4413
|
+
if (exhausted || typeof updated.outgoingEphCounter === "number" && Number.isSafeInteger(updated.outgoingEphCounter) && updated.outgoingEphCounter > MAX_EPH_INDEX) {
|
|
4414
|
+
throw new Error("outgoing ephemeral sequence exhausted");
|
|
4415
|
+
}
|
|
4416
|
+
const committed = usableCounter(updated.outgoingEphCounter);
|
|
4417
|
+
if (committed === null || committed - 1 >= MAX_EPH_INDEX) {
|
|
4418
|
+
throw new Error("outgoing ephemeral sequence exhausted");
|
|
4419
|
+
}
|
|
4420
|
+
return committed - 1;
|
|
4421
|
+
}
|
|
4422
|
+
async function registerPairwiseCounterparty(storage, ivkHex) {
|
|
4311
4423
|
const key = ivkHex.toLowerCase();
|
|
4312
4424
|
const updated = await storage.updateConfig((config) => {
|
|
4313
|
-
const
|
|
4314
|
-
const addedAt = config.pairwiseCounterparties?.[key]?.addedAt;
|
|
4425
|
+
const existing = config.pairwiseCounterparties?.[key];
|
|
4315
4426
|
return {
|
|
4316
4427
|
...config,
|
|
4317
4428
|
pairwiseCounterparties: {
|
|
4318
4429
|
...config.pairwiseCounterparties,
|
|
4319
4430
|
[key]: {
|
|
4320
|
-
nextIndex: (
|
|
4321
|
-
addedAt: typeof addedAt === "number" ? addedAt : Date.now()
|
|
4431
|
+
nextIndex: usableCounter(existing?.nextIndex) ?? 0,
|
|
4432
|
+
addedAt: typeof existing?.addedAt === "number" ? existing.addedAt : Date.now()
|
|
4322
4433
|
}
|
|
4323
4434
|
},
|
|
4324
4435
|
updatedAt: Date.now()
|
|
4325
4436
|
};
|
|
4326
4437
|
});
|
|
4327
4438
|
if (!updated) throw new Error("vault not initialized");
|
|
4328
|
-
const nextIndex = usableCounter(updated.pairwiseCounterparties?.[key]?.nextIndex);
|
|
4329
|
-
if (nextIndex === null || nextIndex <= 1) return null;
|
|
4330
|
-
return nextIndex - 1;
|
|
4331
4439
|
}
|
|
4332
4440
|
|
|
4333
4441
|
// src/wallet/vault/notes/record.ts
|
|
@@ -4762,19 +4870,44 @@ async function processHints(ctx, hints, total) {
|
|
|
4762
4870
|
}
|
|
4763
4871
|
}
|
|
4764
4872
|
outcome.scanned++;
|
|
4765
|
-
if (!hint.encryptedMemo
|
|
4873
|
+
if (!hint.encryptedMemo) {
|
|
4766
4874
|
outcome.noMemo++;
|
|
4767
4875
|
continue;
|
|
4768
4876
|
}
|
|
4769
4877
|
toDecrypt.push(hint);
|
|
4770
4878
|
}
|
|
4771
|
-
const {
|
|
4879
|
+
const {
|
|
4880
|
+
notes,
|
|
4881
|
+
tagFiltered,
|
|
4882
|
+
selfMatched,
|
|
4883
|
+
pairwiseMatched,
|
|
4884
|
+
maxSelfEphIndex,
|
|
4885
|
+
maxOutgoingEphIndex,
|
|
4886
|
+
sentNotes,
|
|
4887
|
+
learnedRecipients,
|
|
4888
|
+
unmatchedSent,
|
|
4889
|
+
sealedBookEntries
|
|
4890
|
+
} = await ctx.pool.decryptBatch(
|
|
4891
|
+
toDecrypt,
|
|
4892
|
+
{ ...ctx.keys, recipientCandidates: [...outcome.learnedRecipients].map(fromHex) },
|
|
4893
|
+
ctx.signal
|
|
4894
|
+
);
|
|
4772
4895
|
outcome.tagFiltered += tagFiltered;
|
|
4773
4896
|
outcome.selfDiscovered += selfMatched;
|
|
4774
4897
|
outcome.pairwiseDiscovered += pairwiseMatched;
|
|
4898
|
+
outcome.sentNotes.push(...sentNotes ?? []);
|
|
4899
|
+
outcome.unmatchedSent.push(...unmatchedSent ?? []);
|
|
4900
|
+
for (const e of sealedBookEntries ?? []) outcome.sealedBookEntries.add(e);
|
|
4901
|
+
for (const r of learnedRecipients ?? []) outcome.learnedRecipients.add(r);
|
|
4775
4902
|
if (maxSelfEphIndex !== null) {
|
|
4776
4903
|
outcome.maxSelfEphIndex = Math.max(outcome.maxSelfEphIndex ?? -1, maxSelfEphIndex);
|
|
4777
4904
|
}
|
|
4905
|
+
if (maxOutgoingEphIndex != null) {
|
|
4906
|
+
outcome.maxOutgoingEphIndex = Math.max(
|
|
4907
|
+
outcome.maxOutgoingEphIndex ?? -1,
|
|
4908
|
+
maxOutgoingEphIndex
|
|
4909
|
+
);
|
|
4910
|
+
}
|
|
4778
4911
|
const pageEntries = [];
|
|
4779
4912
|
for (const [i, decrypted] of notes.entries()) {
|
|
4780
4913
|
const hint = toDecrypt[i];
|
|
@@ -4871,7 +5004,12 @@ async function collectScanEntries(params) {
|
|
|
4871
5004
|
tagFiltered: 0,
|
|
4872
5005
|
selfDiscovered: 0,
|
|
4873
5006
|
pairwiseDiscovered: 0,
|
|
4874
|
-
|
|
5007
|
+
sentNotes: [],
|
|
5008
|
+
unmatchedSent: [],
|
|
5009
|
+
sealedBookEntries: /* @__PURE__ */ new Set(),
|
|
5010
|
+
learnedRecipients: new Set((params.keys.recipientCandidates ?? []).map((c) => toHex(c))),
|
|
5011
|
+
maxSelfEphIndex: null,
|
|
5012
|
+
maxOutgoingEphIndex: null
|
|
4875
5013
|
};
|
|
4876
5014
|
const ctx = {
|
|
4877
5015
|
outcome,
|
|
@@ -5093,7 +5231,7 @@ function gapMargin(windowSize) {
|
|
|
5093
5231
|
return Math.max(1, Math.floor(windowSize / 16));
|
|
5094
5232
|
}
|
|
5095
5233
|
function resolveSelfEphCeiling(params) {
|
|
5096
|
-
const { notes,
|
|
5234
|
+
const { notes, viewingKey, scanMaxIndex } = params;
|
|
5097
5235
|
const windowSize = params.windowSize !== void 0 && params.windowSize > 0 ? params.windowSize : SELF_EPH_WINDOW;
|
|
5098
5236
|
if (scanMaxIndex === null) return null;
|
|
5099
5237
|
if (scanMaxIndex < windowSize - gapMargin(windowSize)) return scanMaxIndex;
|
|
@@ -5106,7 +5244,7 @@ function resolveSelfEphCeiling(params) {
|
|
|
5106
5244
|
let max = scanMaxIndex;
|
|
5107
5245
|
let from = windowSize;
|
|
5108
5246
|
for (; ; ) {
|
|
5109
|
-
const slice = selfEphWindow(
|
|
5247
|
+
const slice = selfEphWindow(viewingKey, ivkPacked, from, windowSize);
|
|
5110
5248
|
let matched = false;
|
|
5111
5249
|
for (const entry of slice) {
|
|
5112
5250
|
if (ownEphPks.has(entry.ephPkHex.toLowerCase())) {
|
|
@@ -5162,6 +5300,8 @@ async function resolveSpentStatus(params) {
|
|
|
5162
5300
|
}
|
|
5163
5301
|
|
|
5164
5302
|
// src/wallet/scanner/pipeline.ts
|
|
5303
|
+
var OUTGOING_RESCAN_GAP = 8;
|
|
5304
|
+
var RETRY_BOOK_LIMIT = 64;
|
|
5165
5305
|
async function runScan(params) {
|
|
5166
5306
|
const { vault, storage, hints, nullifiers, pool, keys, sinceLeafIndex, signal, onWarning } = params;
|
|
5167
5307
|
const isIncremental = sinceLeafIndex !== void 0;
|
|
@@ -5169,6 +5309,7 @@ async function runScan(params) {
|
|
|
5169
5309
|
const preScanHexes = new Set(existingHexes);
|
|
5170
5310
|
const config = await storage.getConfig().catch(() => null);
|
|
5171
5311
|
const selfEphWindowSize = windowSizeForCounter(config?.selfEphCounter ?? 0);
|
|
5312
|
+
const outgoingEphWindowSize = windowSizeForCounter(config?.outgoingEphCounter ?? 0);
|
|
5172
5313
|
const pairwiseCounterparties = Object.keys(config?.pairwiseCounterparties ?? {}).flatMap(
|
|
5173
5314
|
(hex) => {
|
|
5174
5315
|
try {
|
|
@@ -5197,7 +5338,12 @@ async function runScan(params) {
|
|
|
5197
5338
|
// worth it on full scans and restores, waste on small incremental ticks.
|
|
5198
5339
|
selfEph: !isIncremental,
|
|
5199
5340
|
selfEphWindowSize,
|
|
5200
|
-
pairwiseCounterparties
|
|
5341
|
+
pairwiseCounterparties,
|
|
5342
|
+
// Sent-note discovery rides the same full-scan/restore budget as
|
|
5343
|
+
// self discovery: on an incremental tick the history is already
|
|
5344
|
+
// there, and the window would be paid for nothing.
|
|
5345
|
+
outgoingEph: !isIncremental,
|
|
5346
|
+
outgoingEphWindowSize
|
|
5201
5347
|
},
|
|
5202
5348
|
existingHexes,
|
|
5203
5349
|
sinceLeafIndex,
|
|
@@ -5211,6 +5357,49 @@ async function runScan(params) {
|
|
|
5211
5357
|
onPage: (entries) => checkpoint.savePageNotes(entries),
|
|
5212
5358
|
onBatchDone: (maxLeafIndex) => checkpoint.advanceCursor(maxLeafIndex)
|
|
5213
5359
|
});
|
|
5360
|
+
const ovk = keys.outgoingViewingKey;
|
|
5361
|
+
if (scan.unmatchedSent.length > 0 && ovk) {
|
|
5362
|
+
const proven = [...scan.learnedRecipients].map(fromHex);
|
|
5363
|
+
const keptEntries = [...scan.sealedBookEntries].slice(-RETRY_BOOK_LIMIT);
|
|
5364
|
+
const sealed = [];
|
|
5365
|
+
for (const entry of keptEntries) {
|
|
5366
|
+
try {
|
|
5367
|
+
sealed.push(BigInt(entry));
|
|
5368
|
+
} catch {
|
|
5369
|
+
}
|
|
5370
|
+
}
|
|
5371
|
+
const retries = scan.unmatchedSent.slice(-RETRY_BOOK_LIMIT);
|
|
5372
|
+
if (retries.length < scan.unmatchedSent.length) {
|
|
5373
|
+
onWarning?.(
|
|
5374
|
+
`sent-note retry capped at ${retries.length} of ${scan.unmatchedSent.length}; the rest resolve on the next full scan`
|
|
5375
|
+
);
|
|
5376
|
+
}
|
|
5377
|
+
if (keptEntries.length < scan.sealedBookEntries.size) {
|
|
5378
|
+
onWarning?.(
|
|
5379
|
+
`recipient book capped at ${keptEntries.length} of ${scan.sealedBookEntries.size} entries; older payments resolve on the next full scan`
|
|
5380
|
+
);
|
|
5381
|
+
}
|
|
5382
|
+
for (const { hint, ephIndex } of retries) {
|
|
5383
|
+
if (signal?.aborted) throw scanAbortError();
|
|
5384
|
+
const candidates = [
|
|
5385
|
+
...sealed.map((e) => openRecipientBookEntry(e, ovk, hint.commitmentHex)),
|
|
5386
|
+
...proven
|
|
5387
|
+
];
|
|
5388
|
+
const sent = recoverSentNote({
|
|
5389
|
+
hint,
|
|
5390
|
+
outgoingViewingKey: ovk,
|
|
5391
|
+
ephIndex,
|
|
5392
|
+
recipientCandidates: candidates
|
|
5393
|
+
});
|
|
5394
|
+
if (!sent) continue;
|
|
5395
|
+
const { recipientIvk, ...facts } = sent;
|
|
5396
|
+
scan.sentNotes.push({
|
|
5397
|
+
...facts,
|
|
5398
|
+
counterpartyIvkHex: toHex(recipientIvk),
|
|
5399
|
+
encryptedMemo: hint.encryptedMemo ?? ""
|
|
5400
|
+
});
|
|
5401
|
+
}
|
|
5402
|
+
}
|
|
5214
5403
|
const nullifiersToQuery = collectNullifiersToQuery(
|
|
5215
5404
|
scan.scanEntries,
|
|
5216
5405
|
scan.onChainHexes,
|
|
@@ -5240,7 +5429,6 @@ async function runScan(params) {
|
|
|
5240
5429
|
try {
|
|
5241
5430
|
ceiling = resolveSelfEphCeiling({
|
|
5242
5431
|
notes: vault.getAll(),
|
|
5243
|
-
spendingKey: keys.spendingKey,
|
|
5244
5432
|
viewingKey: keys.viewingKey,
|
|
5245
5433
|
scanMaxIndex: scan.maxSelfEphIndex,
|
|
5246
5434
|
windowSize: selfEphWindowSize
|
|
@@ -5250,6 +5438,13 @@ async function runScan(params) {
|
|
|
5250
5438
|
await bumpSelfEphCounterTo(storage, ceiling + 1).catch(() => {
|
|
5251
5439
|
});
|
|
5252
5440
|
}
|
|
5441
|
+
if (scan.maxOutgoingEphIndex !== null) {
|
|
5442
|
+
await bumpOutgoingCounterTo(
|
|
5443
|
+
storage,
|
|
5444
|
+
scan.maxOutgoingEphIndex + 1 + OUTGOING_RESCAN_GAP
|
|
5445
|
+
).catch(() => {
|
|
5446
|
+
});
|
|
5447
|
+
}
|
|
5253
5448
|
if (scan.decryptFailed > 0 && scan.found === 0 && scan.alreadyPresent === 0) {
|
|
5254
5449
|
onWarning?.(
|
|
5255
5450
|
`no notes recovered from ${scan.decryptFailed} hint(s) with memos; if this wallet should hold notes, check the keys it was unlocked with`
|
|
@@ -5261,7 +5456,13 @@ async function runScan(params) {
|
|
|
5261
5456
|
decryptFailed: scan.decryptFailed,
|
|
5262
5457
|
alreadyPresent: scan.alreadyPresent,
|
|
5263
5458
|
purged,
|
|
5264
|
-
incremental: isIncremental
|
|
5459
|
+
incremental: isIncremental,
|
|
5460
|
+
sentNotes: scan.sentNotes,
|
|
5461
|
+
discovery: {
|
|
5462
|
+
self: scan.selfDiscovered,
|
|
5463
|
+
pairwise: scan.pairwiseDiscovered,
|
|
5464
|
+
tagFiltered: scan.tagFiltered
|
|
5465
|
+
}
|
|
5265
5466
|
};
|
|
5266
5467
|
}
|
|
5267
5468
|
async function bumpSelfEphCounterTo(storage, next) {
|
|
@@ -5269,6 +5470,11 @@ async function bumpSelfEphCounterTo(storage, next) {
|
|
|
5269
5470
|
(config) => (config.selfEphCounter ?? 0) >= next ? config : { ...config, selfEphCounter: next, updatedAt: Date.now() }
|
|
5270
5471
|
);
|
|
5271
5472
|
}
|
|
5473
|
+
async function bumpOutgoingCounterTo(storage, next) {
|
|
5474
|
+
await storage.updateConfig(
|
|
5475
|
+
(config) => (config.outgoingEphCounter ?? 0) >= next ? config : { ...config, outgoingEphCounter: next, updatedAt: Date.now() }
|
|
5476
|
+
);
|
|
5477
|
+
}
|
|
5272
5478
|
|
|
5273
5479
|
// src/wallet/scanner/history/extrinsicFacts.ts
|
|
5274
5480
|
var UNKNOWN = { fee: null, success: true };
|
|
@@ -5407,10 +5613,8 @@ function regeneratePaymentSlip(facts, recipientIvkPacked, txHash) {
|
|
|
5407
5613
|
commitmentHex: facts.commitmentHex,
|
|
5408
5614
|
encryptedMemo: facts.encryptedMemo,
|
|
5409
5615
|
...facts.leafIndex !== void 0 ? { leafIndex: facts.leafIndex } : {},
|
|
5410
|
-
//
|
|
5616
|
+
// The recipient renders this as an explorer link, so an unconstrained
|
|
5411
5617
|
// string is a URL injection wearing the authority of a decrypted slip.
|
|
5412
|
-
// Dropped rather than fatal: the slip still rebuilds the note, which is
|
|
5413
|
-
// the part that matters.
|
|
5414
5618
|
...isHexOfLength(txHash, 32) ? { txHash } : {}
|
|
5415
5619
|
});
|
|
5416
5620
|
return encodePaymentSlip(envelope);
|
|
@@ -5435,15 +5639,37 @@ async function loadExistingRecords(vault) {
|
|
|
5435
5639
|
function recordKey(transfer) {
|
|
5436
5640
|
return transfer.hash ?? `${transfer.blockNumber}-${transfer.extrinsicIndex ?? 0}`;
|
|
5437
5641
|
}
|
|
5642
|
+
function sentByCommitment(sentNotes) {
|
|
5643
|
+
return new Map(sentNotes.map((s) => [s.commitmentHex.toLowerCase(), s]));
|
|
5644
|
+
}
|
|
5645
|
+
function reissuedSlip(sent, hash) {
|
|
5646
|
+
try {
|
|
5647
|
+
const facts = {
|
|
5648
|
+
commitmentHex: sent.commitmentHex,
|
|
5649
|
+
encryptedMemo: sent.encryptedMemo,
|
|
5650
|
+
...sent.leafIndex !== void 0 ? { leafIndex: sent.leafIndex } : {}
|
|
5651
|
+
};
|
|
5652
|
+
return {
|
|
5653
|
+
paymentSlip: regeneratePaymentSlip(
|
|
5654
|
+
facts,
|
|
5655
|
+
fromHex(sent.counterpartyIvkHex),
|
|
5656
|
+
hash ?? void 0
|
|
5657
|
+
)
|
|
5658
|
+
};
|
|
5659
|
+
} catch {
|
|
5660
|
+
return {};
|
|
5661
|
+
}
|
|
5662
|
+
}
|
|
5438
5663
|
async function reconstructOutgoingTxRecords(deps) {
|
|
5439
5664
|
const { vault, transfers } = deps;
|
|
5440
5665
|
const now = deps.now ?? Date.now;
|
|
5441
5666
|
const allNotes = vault.getAll();
|
|
5442
5667
|
const spentNotes = allNotes.filter((n) => n.spent);
|
|
5443
5668
|
if (spentNotes.length === 0) return;
|
|
5669
|
+
const sentCommitments = (deps.sentNotes ?? []).map((s) => s.commitmentHex);
|
|
5444
5670
|
const [outgoingTransfers, commitmentTransfers] = await Promise.all([
|
|
5445
5671
|
transfers.byNullifiers(spentNotes.map((n) => n.nullifierHex)),
|
|
5446
|
-
transfers.byCommitments(allNotes.map((n) => n.commitmentHex))
|
|
5672
|
+
transfers.byCommitments([...allNotes.map((n) => n.commitmentHex), ...sentCommitments])
|
|
5447
5673
|
]);
|
|
5448
5674
|
if (outgoingTransfers.length === 0) return;
|
|
5449
5675
|
const noteByNullifier = new Map(spentNotes.map((n) => [n.nullifierHex, n]));
|
|
@@ -5452,64 +5678,85 @@ async function reconstructOutgoingTxRecords(deps) {
|
|
|
5452
5678
|
commitmentTransfers.map((ct) => [extrinsicKey(ct), ct.matchedCommitments ?? []])
|
|
5453
5679
|
);
|
|
5454
5680
|
const existingByKey = await loadExistingRecords(vault);
|
|
5681
|
+
const sentIndex = sentByCommitment(deps.sentNotes ?? []);
|
|
5682
|
+
const recoveryEnabled = deps.sentNotes !== void 0;
|
|
5683
|
+
const sentFor = (row) => (commitmentsByExtrinsic.get(extrinsicKey(row)) ?? []).map((hex) => sentIndex.get(hex.toLowerCase())).find((s) => s !== void 0);
|
|
5455
5684
|
for (const transfer of outgoingTransfers) {
|
|
5456
5685
|
const existing = existingByKey.get(recordKey(transfer));
|
|
5457
|
-
|
|
5686
|
+
const known = existing?.recipientPkHex && existing.recipientPkHex !== ZERO_PK;
|
|
5687
|
+
if (known && existing?.paymentSlip) continue;
|
|
5458
5688
|
const inputNotes = (transfer.matchedNullifiers ?? []).map((h) => noteByNullifier.get(h)).filter((n) => n !== void 0);
|
|
5459
5689
|
if (inputNotes.length === 0) continue;
|
|
5460
5690
|
const changeNote = selectDescribingNoteByCommitment(
|
|
5461
5691
|
commitmentsByExtrinsic.get(extrinsicKey(transfer)) ?? [],
|
|
5462
5692
|
noteByCommitment
|
|
5463
5693
|
);
|
|
5464
|
-
const
|
|
5694
|
+
const sentForRow = sentFor(transfer);
|
|
5695
|
+
const recipientPkHex = sentForRow ? toPkHex(sentForRow.recipientStealthPk) : recoveryEnabled ? ZERO_PK : toPkHex(changeNote?.sourcePk);
|
|
5465
5696
|
if (existing) {
|
|
5466
|
-
|
|
5467
|
-
|
|
5697
|
+
const patch = {
|
|
5698
|
+
// Only when it ADDS something: this runs over the whole
|
|
5699
|
+
// outgoing history on every scan, and rewriting what is already
|
|
5700
|
+
// there would make each one a write.
|
|
5701
|
+
...!known && recipientPkHex !== ZERO_PK ? { recipientPkHex } : {},
|
|
5702
|
+
...sentForRow && !existing.paymentSlip ? reissuedSlip(sentForRow, transfer.hash) : {}
|
|
5703
|
+
};
|
|
5704
|
+
if (Object.keys(patch).length > 0) {
|
|
5705
|
+
await vault.saveTxRecord({ ...existing, ...patch });
|
|
5706
|
+
}
|
|
5468
5707
|
continue;
|
|
5469
5708
|
}
|
|
5470
5709
|
const { fee, success } = await fetchExtrinsicFacts(deps.txFacts, transfer.hash);
|
|
5471
5710
|
const totalInputValue = inputNotes.reduce((sum, n) => sum + n.value, 0n);
|
|
5472
|
-
const
|
|
5711
|
+
const derivedAmount = totalInputValue - (changeNote?.value ?? 0n) - (fee ?? 0n);
|
|
5712
|
+
const transferAmount = sentForRow?.value ?? derivedAmount;
|
|
5473
5713
|
if (transferAmount <= 0n) continue;
|
|
5474
5714
|
const record = {
|
|
5475
5715
|
id: recordKey(transfer),
|
|
5476
5716
|
type: "private_transfer",
|
|
5477
5717
|
blockNumber: transfer.blockNumber,
|
|
5478
5718
|
hash: transfer.hash ?? "",
|
|
5479
|
-
assetId: inputNotes[0].assetId.toString(),
|
|
5719
|
+
assetId: (sentForRow?.assetId ?? inputNotes[0].assetId).toString(),
|
|
5480
5720
|
amount: transferAmount.toString(),
|
|
5481
5721
|
recipientPkHex,
|
|
5482
5722
|
status: success ? "success" : "failed",
|
|
5483
|
-
...fee !== null ? { feePlanck: fee.toString() } : {
|
|
5484
|
-
|
|
5723
|
+
...fee !== null ? { feePlanck: fee.toString() } : {},
|
|
5724
|
+
...fee === null && !sentForRow ? { amountApproximate: true } : {},
|
|
5725
|
+
timestampMs: transfer.timestampMs ?? now(),
|
|
5726
|
+
// Best-effort: a slip that cannot be sealed costs the convenience of
|
|
5727
|
+
// handing it over again, never the history row.
|
|
5728
|
+
...sentForRow ? reissuedSlip(sentForRow, transfer.hash) : {}
|
|
5485
5729
|
};
|
|
5486
5730
|
await vault.saveTxRecord(record);
|
|
5487
5731
|
}
|
|
5488
5732
|
}
|
|
5489
5733
|
|
|
5490
5734
|
// src/wallet/ops/notes/buildNote.ts
|
|
5491
|
-
async function
|
|
5735
|
+
async function buildZkNoteWithIndex(params, deps) {
|
|
5492
5736
|
const { keys, storage } = deps;
|
|
5493
5737
|
const ownerPk = params.ownerPk ?? keys.ownerPk;
|
|
5494
5738
|
const spendingKey = params.spendingKey ?? keys.spendingKey;
|
|
5495
5739
|
const viewingPublicKey = params.viewingPublicKey ?? keys.viewingPublicKey;
|
|
5496
5740
|
const isSelf = params.viewingPublicKey === void 0 && params.recipientOwnerPk === void 0;
|
|
5497
5741
|
let ephSkOverride;
|
|
5498
|
-
|
|
5742
|
+
let outgoingIndex;
|
|
5743
|
+
if (isSelf && storage && keys.viewingSecretKey) {
|
|
5499
5744
|
try {
|
|
5500
|
-
ephSkOverride = deriveSelfEphSk(
|
|
5745
|
+
ephSkOverride = deriveSelfEphSk(
|
|
5746
|
+
keys.viewingSecretKey,
|
|
5747
|
+
await reserveSelfEphIndex(storage)
|
|
5748
|
+
);
|
|
5501
5749
|
} catch {
|
|
5502
5750
|
}
|
|
5503
|
-
} else if (params.viewingPublicKey && storage && keys.
|
|
5751
|
+
} else if (params.viewingPublicKey && storage && keys.outgoingViewingKey) {
|
|
5504
5752
|
try {
|
|
5505
|
-
const index = await
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
}
|
|
5753
|
+
const index = await reserveOutgoingIndex(storage, params.outgoingIndexFromChain);
|
|
5754
|
+
ephSkOverride = deriveOutgoingEphSk(keys.outgoingViewingKey, index);
|
|
5755
|
+
outgoingIndex = index;
|
|
5756
|
+
} catch {
|
|
5757
|
+
}
|
|
5758
|
+
try {
|
|
5759
|
+
await registerPairwiseCounterparty(storage, toHex(params.viewingPublicKey));
|
|
5513
5760
|
} catch {
|
|
5514
5761
|
}
|
|
5515
5762
|
}
|
|
@@ -5529,7 +5776,13 @@ async function buildZkNote(params, deps) {
|
|
|
5529
5776
|
},
|
|
5530
5777
|
...ephSkOverride !== void 0 && { ephSkOverride }
|
|
5531
5778
|
});
|
|
5532
|
-
return
|
|
5779
|
+
return {
|
|
5780
|
+
note: stampCreatedAt(note, Date.now()),
|
|
5781
|
+
...outgoingIndex !== void 0 ? { outgoingIndex } : {}
|
|
5782
|
+
};
|
|
5783
|
+
}
|
|
5784
|
+
async function buildZkNote(params, deps) {
|
|
5785
|
+
return (await buildZkNoteWithIndex(params, deps)).note;
|
|
5533
5786
|
}
|
|
5534
5787
|
|
|
5535
5788
|
// src/wallet/ops/notes/selfStealthNote.ts
|
|
@@ -5548,89 +5801,6 @@ function recoverSelfStealthNote(note, keys) {
|
|
|
5548
5801
|
return recovered;
|
|
5549
5802
|
}
|
|
5550
5803
|
|
|
5551
|
-
// src/wallet/ops/notes/noteTransfer.ts
|
|
5552
|
-
var NOTE_TRANSFER_URI_SCHEME = "orbinum://notes/v1/";
|
|
5553
|
-
var QR_PAGE_MAX_CHARS = 1800;
|
|
5554
|
-
function noteToTransferEntry(note) {
|
|
5555
|
-
return {
|
|
5556
|
-
c: note.commitmentHex,
|
|
5557
|
-
n: note.nullifierHex,
|
|
5558
|
-
val: note.value.toString(),
|
|
5559
|
-
aid: note.assetId.toString(),
|
|
5560
|
-
opk: scalarToHex(note.ownerPk),
|
|
5561
|
-
bld: scalarToHex(note.blinding),
|
|
5562
|
-
sk: scalarToHex(note.spendingKey)
|
|
5563
|
-
};
|
|
5564
|
-
}
|
|
5565
|
-
function encodeNoteTransferPages(notes, options = {}) {
|
|
5566
|
-
const now = options.now ?? Date.now;
|
|
5567
|
-
const maxChars = options.maxChars ?? QR_PAGE_MAX_CHARS;
|
|
5568
|
-
const entries = notes.map(noteToTransferEntry);
|
|
5569
|
-
const batches = [];
|
|
5570
|
-
let batch = [];
|
|
5571
|
-
for (const entry of entries) {
|
|
5572
|
-
batch.push(entry);
|
|
5573
|
-
const probe = { v: 1, ts: 0, p: 0, pt: 1, notes: batch };
|
|
5574
|
-
const size = NOTE_TRANSFER_URI_SCHEME.length + base64UrlEncode(JSON.stringify(probe)).length;
|
|
5575
|
-
if (size > maxChars && batch.length > 1) {
|
|
5576
|
-
batch.pop();
|
|
5577
|
-
batches.push(batch);
|
|
5578
|
-
batch = [entry];
|
|
5579
|
-
}
|
|
5580
|
-
}
|
|
5581
|
-
if (batch.length > 0) batches.push(batch);
|
|
5582
|
-
const ts = now();
|
|
5583
|
-
return batches.map((notesInPage, i) => {
|
|
5584
|
-
const payload = {
|
|
5585
|
-
v: 1,
|
|
5586
|
-
ts,
|
|
5587
|
-
p: i,
|
|
5588
|
-
pt: batches.length,
|
|
5589
|
-
notes: notesInPage
|
|
5590
|
-
};
|
|
5591
|
-
return NOTE_TRANSFER_URI_SCHEME + base64UrlEncode(JSON.stringify(payload));
|
|
5592
|
-
});
|
|
5593
|
-
}
|
|
5594
|
-
function decodeNoteTransferPage(uri) {
|
|
5595
|
-
if (!uri.startsWith(NOTE_TRANSFER_URI_SCHEME)) {
|
|
5596
|
-
throw new Error("Not an Orbinum note-transfer code.");
|
|
5597
|
-
}
|
|
5598
|
-
let payload;
|
|
5599
|
-
try {
|
|
5600
|
-
payload = JSON.parse(base64UrlDecode(uri.slice(NOTE_TRANSFER_URI_SCHEME.length)));
|
|
5601
|
-
} catch {
|
|
5602
|
-
throw new Error("Note-transfer code is corrupt.");
|
|
5603
|
-
}
|
|
5604
|
-
const p = payload;
|
|
5605
|
-
if (p.v !== 1) {
|
|
5606
|
-
throw new Error(`Unsupported note-transfer version: ${String(p.v)}.`);
|
|
5607
|
-
}
|
|
5608
|
-
if (typeof p.p !== "number" || typeof p.pt !== "number" || !Array.isArray(p.notes) || p.notes.some((n) => !isEntry(n))) {
|
|
5609
|
-
throw new Error("Note-transfer code is missing required fields.");
|
|
5610
|
-
}
|
|
5611
|
-
return p;
|
|
5612
|
-
}
|
|
5613
|
-
function isEntry(value) {
|
|
5614
|
-
if (typeof value !== "object" || value === null) return false;
|
|
5615
|
-
const e = value;
|
|
5616
|
-
return ["c", "n", "val", "aid", "opk", "bld", "sk"].every(
|
|
5617
|
-
(k) => typeof e[k] === "string" && e[k].length > 0
|
|
5618
|
-
);
|
|
5619
|
-
}
|
|
5620
|
-
function assembleNoteTransfer(pages) {
|
|
5621
|
-
if (pages.length === 0) throw new Error("No pages scanned.");
|
|
5622
|
-
const total = pages[0].pt;
|
|
5623
|
-
if (pages.some((p) => p.pt !== total)) {
|
|
5624
|
-
throw new Error("Pages come from different batches.");
|
|
5625
|
-
}
|
|
5626
|
-
const seen = new Set(pages.map((p) => p.p));
|
|
5627
|
-
if (seen.size !== total) {
|
|
5628
|
-
const missing = [...Array(total).keys()].filter((i) => !seen.has(i));
|
|
5629
|
-
throw new Error(`Missing page(s): ${missing.map((i) => i + 1).join(", ")} of ${total}.`);
|
|
5630
|
-
}
|
|
5631
|
-
return [...pages].sort((a, b) => a.p - b.p).flatMap((p) => p.notes);
|
|
5632
|
-
}
|
|
5633
|
-
|
|
5634
5804
|
// src/wallet/ops/notes/noteBackup.ts
|
|
5635
5805
|
var NOTE_BACKUP_VERSION = 1;
|
|
5636
5806
|
function noteToBackupEntry(note) {
|
|
@@ -5665,12 +5835,13 @@ function decodeNoteBackup(json) {
|
|
|
5665
5835
|
if (p.v !== NOTE_BACKUP_VERSION) {
|
|
5666
5836
|
throw new Error(`Unsupported note-backup version: ${String(p.v)}.`);
|
|
5667
5837
|
}
|
|
5668
|
-
if (!Array.isArray(p.notes) || p.notes.some((n) => !
|
|
5838
|
+
if (!Array.isArray(p.notes) || p.notes.some((n) => !isEntry(n))) {
|
|
5669
5839
|
throw new Error("Note backup is missing required fields.");
|
|
5670
5840
|
}
|
|
5671
5841
|
return p.notes;
|
|
5672
5842
|
}
|
|
5673
5843
|
function importNotesFromBackup(entries, keys) {
|
|
5844
|
+
assertSecretKeyBytes(keys.viewingSecretKey, "importNotesFromBackup: viewingSecretKey");
|
|
5674
5845
|
const out = [];
|
|
5675
5846
|
for (const entry of entries) {
|
|
5676
5847
|
const commitment = {
|
|
@@ -5694,12 +5865,14 @@ function importNotesFromBackup(entries, keys) {
|
|
|
5694
5865
|
}
|
|
5695
5866
|
return out;
|
|
5696
5867
|
}
|
|
5697
|
-
function
|
|
5868
|
+
function isEntry(value) {
|
|
5698
5869
|
if (typeof value !== "object" || value === null) return false;
|
|
5699
5870
|
const e = value;
|
|
5700
|
-
if (
|
|
5701
|
-
if (
|
|
5702
|
-
if (
|
|
5871
|
+
if (!isHexOfLength(e["commitmentHex"], 32)) return false;
|
|
5872
|
+
if (!isHexOfLength(e["encryptedMemo"], ENCRYPTED_MEMO_SIZE)) return false;
|
|
5873
|
+
if (e["leafIndex"] !== void 0 && !isValidLeafIndex(e["leafIndex"])) return false;
|
|
5874
|
+
if (e["spent"] !== void 0 && typeof e["spent"] !== "boolean") return false;
|
|
5875
|
+
if (e["spentAt"] !== void 0 && e["spentAt"] !== null && typeof e["spentAt"] !== "number") {
|
|
5703
5876
|
return false;
|
|
5704
5877
|
}
|
|
5705
5878
|
return true;
|
|
@@ -5707,6 +5880,7 @@ function isEntry2(value) {
|
|
|
5707
5880
|
|
|
5708
5881
|
// src/wallet/ops/notes/paymentSlipImport.ts
|
|
5709
5882
|
function importPaymentSlip(slip, keys) {
|
|
5883
|
+
assertSecretKeyBytes(keys.viewingSecretKey, "importPaymentSlip: viewingSecretKey");
|
|
5710
5884
|
const envelope = typeof slip === "string" ? decodePaymentSlip(slip) : slip;
|
|
5711
5885
|
if (!envelope) return null;
|
|
5712
5886
|
const fields = openPaymentSlip(keys.viewingSecretKey, envelope);
|
|
@@ -5835,10 +6009,7 @@ async function transferNotes(deps, params, onProgress) {
|
|
|
5835
6009
|
const merkleRoot = merkleA.root;
|
|
5836
6010
|
onProgress?.("building-output-notes");
|
|
5837
6011
|
const effectiveSenderPk = senderPk ?? noteA.ownerPk;
|
|
5838
|
-
const
|
|
5839
|
-
deriveViewingSecretKey(noteA.spendingKey)
|
|
5840
|
-
);
|
|
5841
|
-
const recipientNote = await deps.buildNote({
|
|
6012
|
+
const { note: recipientNote, outgoingIndex } = await deps.buildNote({
|
|
5842
6013
|
value: transferAmount,
|
|
5843
6014
|
assetId: noteA.assetId,
|
|
5844
6015
|
ownerPk: recipientPk,
|
|
@@ -5848,13 +6019,17 @@ async function transferNotes(deps, params, onProgress) {
|
|
|
5848
6019
|
// With a viewing key present this activates stealth derivation.
|
|
5849
6020
|
recipientOwnerPk: recipientPk
|
|
5850
6021
|
});
|
|
5851
|
-
const
|
|
6022
|
+
const changeSourcePk = outgoingIndex !== void 0 && recipientViewingPublicKey !== void 0 && deps.outgoingViewingKey !== void 0 ? bytesToBigintLE(
|
|
6023
|
+
sealRecipientBookEntry(
|
|
6024
|
+
recipientViewingPublicKey,
|
|
6025
|
+
deps.outgoingViewingKey,
|
|
6026
|
+
recipientNote.commitmentHex
|
|
6027
|
+
)
|
|
6028
|
+
) : recipientNote.ownerPk;
|
|
6029
|
+
const { note: changeNote } = await deps.buildNote({
|
|
5852
6030
|
value: changeValue,
|
|
5853
6031
|
assetId: noteA.assetId,
|
|
5854
|
-
|
|
5855
|
-
spendingKey: noteA.spendingKey,
|
|
5856
|
-
sourcePk: recipientNote.ownerPk,
|
|
5857
|
-
viewingPublicKey: senderViewingPublicKey
|
|
6032
|
+
sourcePk: changeSourcePk
|
|
5858
6033
|
});
|
|
5859
6034
|
onProgress?.("generating-zk");
|
|
5860
6035
|
const noteToInput = (note, proof) => ({
|
|
@@ -5971,7 +6146,7 @@ async function unshieldNote(deps, params, onProgress) {
|
|
|
5971
6146
|
if (changeValue > 0n) {
|
|
5972
6147
|
const { ownerPk, spendingKey } = deps.stealthKeys;
|
|
5973
6148
|
changeBlinding = randomBlinding();
|
|
5974
|
-
changeNote = await deps.buildNote({
|
|
6149
|
+
changeNote = (await deps.buildNote({
|
|
5975
6150
|
value: changeValue,
|
|
5976
6151
|
assetId: note.assetId,
|
|
5977
6152
|
ownerPk,
|
|
@@ -5979,7 +6154,7 @@ async function unshieldNote(deps, params, onProgress) {
|
|
|
5979
6154
|
spendingKey,
|
|
5980
6155
|
// Activates stealth: the commitment covers a one-time ownerPk.
|
|
5981
6156
|
recipientOwnerPk: ownerPk
|
|
5982
|
-
});
|
|
6157
|
+
})).note;
|
|
5983
6158
|
}
|
|
5984
6159
|
const { provider, version: circuitVersion } = await deps.resolver.resolve(
|
|
5985
6160
|
CircuitType6.Unshield,
|
|
@@ -6038,7 +6213,7 @@ async function unshieldNote(deps, params, onProgress) {
|
|
|
6038
6213
|
import { CircuitType as CircuitType7 } from "@orbinum/proof-generator";
|
|
6039
6214
|
async function claimFees(deps, { assetId, amount, signer }, onStep) {
|
|
6040
6215
|
onStep?.("building-note");
|
|
6041
|
-
const note = await deps.buildNote({ value: amount, assetId: BigInt(assetId) });
|
|
6216
|
+
const { note } = await deps.buildNote({ value: amount, assetId: BigInt(assetId) });
|
|
6042
6217
|
const { provider } = await deps.resolver.resolve(CircuitType7.ValueProof, note.circuitVersion);
|
|
6043
6218
|
onStep?.("generating-proof");
|
|
6044
6219
|
const proofOutput = await generateFeeClaimProof(
|
|
@@ -6265,8 +6440,9 @@ var KEY_PREFIX = "orbinum_sk_";
|
|
|
6265
6440
|
function isEnvelope(value) {
|
|
6266
6441
|
return typeof value === "object" && value !== null && value.v === 1 && typeof value.iv === "string" && typeof value.ct === "string";
|
|
6267
6442
|
}
|
|
6268
|
-
function sessionCacheKey(address, chainId) {
|
|
6269
|
-
|
|
6443
|
+
function sessionCacheKey(address, chainId, version = "v3") {
|
|
6444
|
+
const marker = `${version}_`;
|
|
6445
|
+
return `${KEY_PREFIX}${marker}${chainId}_${canonicalAccountId(address)}`;
|
|
6270
6446
|
}
|
|
6271
6447
|
async function hasCachedSession(store, address, chainId) {
|
|
6272
6448
|
return await store.get(sessionCacheKey(address, chainId)) !== null;
|
|
@@ -6302,9 +6478,55 @@ async function clearSession(store, address, chainId) {
|
|
|
6302
6478
|
|
|
6303
6479
|
// src/wallet/identity/vaultName.ts
|
|
6304
6480
|
var PREFIX = "orbinum-vault";
|
|
6305
|
-
|
|
6481
|
+
var versionMarker = (version) => `${version}-`;
|
|
6482
|
+
var VERSION_MARKERS = ["v3-"];
|
|
6483
|
+
function assertNoVersionMarker(component, label) {
|
|
6484
|
+
for (const marker of VERSION_MARKERS) {
|
|
6485
|
+
if (component.startsWith(marker)) {
|
|
6486
|
+
throw new Error(
|
|
6487
|
+
`vaultStorageName: ${label} must not start with "${marker}" \u2014 it would collide with another identity version`
|
|
6488
|
+
);
|
|
6489
|
+
}
|
|
6490
|
+
}
|
|
6491
|
+
}
|
|
6492
|
+
function vaultStorageName(address, chainFingerprint, version = "v3") {
|
|
6306
6493
|
const account = canonicalAccountId(address);
|
|
6307
|
-
|
|
6494
|
+
assertNoVersionMarker(account, "account");
|
|
6495
|
+
const marker = versionMarker(version);
|
|
6496
|
+
if (chainFingerprint) assertNoVersionMarker(chainFingerprint.toLowerCase(), "chainFingerprint");
|
|
6497
|
+
return chainFingerprint ? `${PREFIX}-${marker}${chainFingerprint.toLowerCase()}-${account}` : `${PREFIX}-${marker}${account}`;
|
|
6498
|
+
}
|
|
6499
|
+
|
|
6500
|
+
// src/wallet/identity/walletIdentity.ts
|
|
6501
|
+
function deriveIdentity(rootSecret, version) {
|
|
6502
|
+
if (rootSecret.length !== 32) {
|
|
6503
|
+
throw new Error(`deriveIdentity: rootSecret must be 32 bytes, got ${rootSecret.length}`);
|
|
6504
|
+
}
|
|
6505
|
+
if (rootSecret.every((b) => b === 0)) {
|
|
6506
|
+
throw new Error("deriveIdentity: rootSecret is all zeros \u2014 every wallet would share it");
|
|
6507
|
+
}
|
|
6508
|
+
if (version !== "v3") {
|
|
6509
|
+
throw new Error(`deriveIdentity: unknown identity version ${String(version)}`);
|
|
6510
|
+
}
|
|
6511
|
+
const spendingKey = deriveSpendingKeyV3(rootSecret);
|
|
6512
|
+
const viewingSecretKey = deriveViewingSecretKeyV3(rootSecret);
|
|
6513
|
+
return {
|
|
6514
|
+
version,
|
|
6515
|
+
spendingKey,
|
|
6516
|
+
ownerPk: deriveOwnerPk(spendingKey),
|
|
6517
|
+
viewingSecretKey,
|
|
6518
|
+
viewingPublicKey: deriveViewingPublicKey(viewingSecretKey),
|
|
6519
|
+
outgoingViewingKey: deriveOutgoingViewingKeyV3(rootSecret)
|
|
6520
|
+
};
|
|
6521
|
+
}
|
|
6522
|
+
function exportViewingCredential(identity, options = {}) {
|
|
6523
|
+
return {
|
|
6524
|
+
version: identity.version,
|
|
6525
|
+
ownerPk: identity.ownerPk,
|
|
6526
|
+
viewingSecretKey: Uint8Array.from(identity.viewingSecretKey),
|
|
6527
|
+
viewingPublicKey: Uint8Array.from(identity.viewingPublicKey),
|
|
6528
|
+
...options.includeOutgoing && identity.outgoingViewingKey ? { outgoingViewingKey: Uint8Array.from(identity.outgoingViewingKey) } : {}
|
|
6529
|
+
};
|
|
6308
6530
|
}
|
|
6309
6531
|
|
|
6310
6532
|
// src/wallet/identity/injectedExtensions.ts
|
|
@@ -6342,25 +6564,36 @@ var OrbinumWallet = class {
|
|
|
6342
6564
|
session;
|
|
6343
6565
|
notes;
|
|
6344
6566
|
vaultStore;
|
|
6345
|
-
/**
|
|
6346
|
-
|
|
6567
|
+
/**
|
|
6568
|
+
* The identity this wallet is unlocked as, or null.
|
|
6569
|
+
*
|
|
6570
|
+
* Held as a bundle rather than a single scalar because v3 keys are SIBLINGS:
|
|
6571
|
+
* the spending key no longer produces the viewing key, so there is nothing
|
|
6572
|
+
* left to derive the others from once unlock returns. Keeping the root out
|
|
6573
|
+
* of this object is deliberate — the branches are what the wallet needs, and
|
|
6574
|
+
* the root is the thing worth not retaining.
|
|
6575
|
+
*/
|
|
6576
|
+
identity = null;
|
|
6347
6577
|
/** The underlying store, for operations the facade does not cover. */
|
|
6348
6578
|
get vault() {
|
|
6349
6579
|
return this.vaultStore;
|
|
6350
6580
|
}
|
|
6351
6581
|
get unlocked() {
|
|
6352
|
-
return this.session.unlocked && this.
|
|
6582
|
+
return this.session.unlocked && this.identity !== null;
|
|
6353
6583
|
}
|
|
6354
6584
|
/**
|
|
6355
6585
|
* Opens the wallet from the master key bytes a `SpendingKeyRequest`
|
|
6356
6586
|
* signature produces — the same value `PrivacyKeyManager.getMasterBytes()`
|
|
6357
6587
|
* returns.
|
|
6358
6588
|
*
|
|
6359
|
-
* Master bytes rather than the spending-key scalar, deliberately: the
|
|
6360
|
-
* is the
|
|
6361
|
-
*
|
|
6362
|
-
*
|
|
6363
|
-
*
|
|
6589
|
+
* Master bytes rather than the spending-key scalar, deliberately: the master
|
|
6590
|
+
* is the root EVERY branch hangs off — spending key, viewing key, outgoing
|
|
6591
|
+
* viewing key and vault key are each their own HKDF of it, none derived
|
|
6592
|
+
* from another. Only `ownerPk` comes from the spending scalar, because the
|
|
6593
|
+
* circuit defines it that way.
|
|
6594
|
+
*
|
|
6595
|
+
* It is also what keeps a stored vault readable across a modulus change:
|
|
6596
|
+
* the vault key is derived pre-reduction.
|
|
6364
6597
|
*
|
|
6365
6598
|
* Reports whether the stored vault was RESET: a different chain, a different
|
|
6366
6599
|
* key, or notes the chain no longer recognises. When it was, the wallet
|
|
@@ -6374,14 +6607,12 @@ var OrbinumWallet = class {
|
|
|
6374
6607
|
* against a vault that was never read.
|
|
6375
6608
|
*/
|
|
6376
6609
|
async unlock(masterBytes, options = {}) {
|
|
6377
|
-
|
|
6378
|
-
throw new Error(`Master key must be 32 bytes, got ${masterBytes.length}.`);
|
|
6379
|
-
}
|
|
6610
|
+
assertSecretKeyBytes(masterBytes, "Master key");
|
|
6380
6611
|
this.session.open(
|
|
6381
6612
|
await deriveVaultKey(masterBytes),
|
|
6382
6613
|
await deriveVaultBlindKey(masterBytes)
|
|
6383
6614
|
);
|
|
6384
|
-
this.
|
|
6615
|
+
this.identity = deriveIdentity(masterBytes, this.config.identityVersion ?? "v3");
|
|
6385
6616
|
try {
|
|
6386
6617
|
return await this.vaultStore.unlock(this.session.cryptoKey, options);
|
|
6387
6618
|
} catch (err) {
|
|
@@ -6392,8 +6623,12 @@ var OrbinumWallet = class {
|
|
|
6392
6623
|
/** Drops the in-memory keys. The stored vault is untouched. */
|
|
6393
6624
|
lock() {
|
|
6394
6625
|
this.session.lock();
|
|
6395
|
-
this.
|
|
6626
|
+
this.identity = null;
|
|
6396
6627
|
this.notes.set([]);
|
|
6628
|
+
try {
|
|
6629
|
+
this.config.pool.terminate();
|
|
6630
|
+
} catch {
|
|
6631
|
+
}
|
|
6397
6632
|
}
|
|
6398
6633
|
/** The notes currently held. Empty while locked. */
|
|
6399
6634
|
getNotes() {
|
|
@@ -6405,11 +6640,8 @@ var OrbinumWallet = class {
|
|
|
6405
6640
|
}
|
|
6406
6641
|
/** This wallet's privacy identity: the keys a payer needs to address it. */
|
|
6407
6642
|
privacyKeys() {
|
|
6408
|
-
const
|
|
6409
|
-
return {
|
|
6410
|
-
ownerPk: deriveOwnerPk(spendingKey),
|
|
6411
|
-
viewingPublicKey: deriveViewingPublicKey(deriveViewingSecretKey(spendingKey))
|
|
6412
|
-
};
|
|
6643
|
+
const { ownerPk, viewingPublicKey } = this.requireIdentity();
|
|
6644
|
+
return { ownerPk, viewingPublicKey };
|
|
6413
6645
|
}
|
|
6414
6646
|
/**
|
|
6415
6647
|
* Walks the feed, recovers this wallet's notes, and persists them with their
|
|
@@ -6419,15 +6651,21 @@ var OrbinumWallet = class {
|
|
|
6419
6651
|
* `sinceLeafIndex` for a full scan, which is what a restore needs.
|
|
6420
6652
|
*/
|
|
6421
6653
|
async scan(options = {}) {
|
|
6422
|
-
const
|
|
6423
|
-
const viewingKey = deriveViewingSecretKey(spendingKey);
|
|
6654
|
+
const identity = this.requireIdentity();
|
|
6424
6655
|
return runScan({
|
|
6425
6656
|
vault: this.vaultStore,
|
|
6426
6657
|
storage: this.config.storage,
|
|
6427
6658
|
hints: this.config.hints,
|
|
6428
6659
|
nullifiers: this.config.nullifiers,
|
|
6429
6660
|
pool: this.config.pool,
|
|
6430
|
-
keys: {
|
|
6661
|
+
keys: {
|
|
6662
|
+
viewingKey: identity.viewingSecretKey,
|
|
6663
|
+
spendingKey: identity.spendingKey,
|
|
6664
|
+
ownerPk: identity.ownerPk,
|
|
6665
|
+
// Absent on a watch-only identity: the scan then finds every
|
|
6666
|
+
// note the wallet owns and reconstructs no payment history.
|
|
6667
|
+
...identity.outgoingViewingKey ? { outgoingViewingKey: identity.outgoingViewingKey } : {}
|
|
6668
|
+
},
|
|
6431
6669
|
sinceLeafIndex: options.sinceLeafIndex,
|
|
6432
6670
|
viewTagActivationLeaf: this.config.viewTagActivationLeaf,
|
|
6433
6671
|
onProgress: options.onProgress,
|
|
@@ -6449,13 +6687,14 @@ var OrbinumWallet = class {
|
|
|
6449
6687
|
* reusing an index would republish an ephPk and link the two notes.
|
|
6450
6688
|
*/
|
|
6451
6689
|
async buildNote(params) {
|
|
6452
|
-
const
|
|
6690
|
+
const identity = this.requireIdentity();
|
|
6453
6691
|
return buildZkNote(params, {
|
|
6454
6692
|
keys: {
|
|
6455
|
-
ownerPk:
|
|
6456
|
-
spendingKey,
|
|
6457
|
-
viewingPublicKey:
|
|
6458
|
-
viewingSecretKey:
|
|
6693
|
+
ownerPk: identity.ownerPk,
|
|
6694
|
+
spendingKey: identity.spendingKey,
|
|
6695
|
+
viewingPublicKey: identity.viewingPublicKey,
|
|
6696
|
+
viewingSecretKey: identity.viewingSecretKey,
|
|
6697
|
+
...identity.outgoingViewingKey ? { outgoingViewingKey: identity.outgoingViewingKey } : {}
|
|
6459
6698
|
},
|
|
6460
6699
|
storage: this.config.storage
|
|
6461
6700
|
});
|
|
@@ -6472,11 +6711,12 @@ var OrbinumWallet = class {
|
|
|
6472
6711
|
* Treat the result as secret: it can spend every note this wallet holds.
|
|
6473
6712
|
*/
|
|
6474
6713
|
spendKeys() {
|
|
6475
|
-
const
|
|
6714
|
+
const identity = this.requireIdentity();
|
|
6476
6715
|
return {
|
|
6477
|
-
spendingKey,
|
|
6478
|
-
viewingSecretKey:
|
|
6479
|
-
ownerPk:
|
|
6716
|
+
spendingKey: identity.spendingKey,
|
|
6717
|
+
viewingSecretKey: identity.viewingSecretKey,
|
|
6718
|
+
ownerPk: identity.ownerPk,
|
|
6719
|
+
...identity.outgoingViewingKey ? { outgoingViewingKey: identity.outgoingViewingKey } : {}
|
|
6480
6720
|
};
|
|
6481
6721
|
}
|
|
6482
6722
|
/**
|
|
@@ -6498,7 +6738,20 @@ var OrbinumWallet = class {
|
|
|
6498
6738
|
* verifying-key rotation, and the failure surfaces much later.
|
|
6499
6739
|
*/
|
|
6500
6740
|
buildOutputNote = async (params) => {
|
|
6501
|
-
|
|
6741
|
+
const identity = this.requireIdentity();
|
|
6742
|
+
return buildZkNoteWithIndex(
|
|
6743
|
+
{ ...params, circuitVersion: await this.circuitVersion() },
|
|
6744
|
+
{
|
|
6745
|
+
keys: {
|
|
6746
|
+
ownerPk: identity.ownerPk,
|
|
6747
|
+
spendingKey: identity.spendingKey,
|
|
6748
|
+
viewingPublicKey: identity.viewingPublicKey,
|
|
6749
|
+
viewingSecretKey: identity.viewingSecretKey,
|
|
6750
|
+
...identity.outgoingViewingKey ? { outgoingViewingKey: identity.outgoingViewingKey } : {}
|
|
6751
|
+
},
|
|
6752
|
+
storage: this.config.storage
|
|
6753
|
+
}
|
|
6754
|
+
);
|
|
6502
6755
|
};
|
|
6503
6756
|
/** The circuit version a new note should carry. */
|
|
6504
6757
|
async circuitVersion() {
|
|
@@ -6510,11 +6763,11 @@ var OrbinumWallet = class {
|
|
|
6510
6763
|
}
|
|
6511
6764
|
return chainActiveCircuitVersion(this.config.zkVerifier);
|
|
6512
6765
|
}
|
|
6513
|
-
|
|
6514
|
-
if (this.
|
|
6766
|
+
requireIdentity() {
|
|
6767
|
+
if (this.identity === null) {
|
|
6515
6768
|
throw new Error("Wallet is locked. Call unlock() before using it.");
|
|
6516
6769
|
}
|
|
6517
|
-
return this.
|
|
6770
|
+
return this.identity;
|
|
6518
6771
|
}
|
|
6519
6772
|
};
|
|
6520
6773
|
export {
|
|
@@ -6545,9 +6798,8 @@ export {
|
|
|
6545
6798
|
NATIVE_ASSET_ID,
|
|
6546
6799
|
NOTE_BACKUP_VERSION,
|
|
6547
6800
|
NOTE_BIGINT_FIELDS,
|
|
6548
|
-
NOTE_TRANSFER_URI_SCHEME,
|
|
6549
6801
|
NoteBuilder,
|
|
6550
|
-
|
|
6802
|
+
OUTGOING_EPH_WINDOW,
|
|
6551
6803
|
OrbinumClient,
|
|
6552
6804
|
OrbinumClientProvider,
|
|
6553
6805
|
OrbinumWallet,
|
|
@@ -6557,7 +6809,6 @@ export {
|
|
|
6557
6809
|
PRECOMPILE_ADDR,
|
|
6558
6810
|
PrivacyKeyManager,
|
|
6559
6811
|
PrivacyModule,
|
|
6560
|
-
QR_PAGE_MAX_CHARS,
|
|
6561
6812
|
RECOVERED_TX_RESULT,
|
|
6562
6813
|
RelayerStatusModule,
|
|
6563
6814
|
RpcV2Module,
|
|
@@ -6582,7 +6833,7 @@ export {
|
|
|
6582
6833
|
addressToFieldElement,
|
|
6583
6834
|
applyBatch,
|
|
6584
6835
|
applyNoteStatus,
|
|
6585
|
-
|
|
6836
|
+
assertSecretKeyBytes,
|
|
6586
6837
|
base58,
|
|
6587
6838
|
base64UrlDecode,
|
|
6588
6839
|
base64UrlEncode,
|
|
@@ -6595,6 +6846,7 @@ export {
|
|
|
6595
6846
|
buildShieldBatchOperations,
|
|
6596
6847
|
buildShieldParams,
|
|
6597
6848
|
buildZkNote,
|
|
6849
|
+
buildZkNoteWithIndex,
|
|
6598
6850
|
bytesToBigintLE,
|
|
6599
6851
|
bytesToBjjScalar,
|
|
6600
6852
|
cacheSession,
|
|
@@ -6624,15 +6876,17 @@ export {
|
|
|
6624
6876
|
createWorkerPool,
|
|
6625
6877
|
decodeNoteBackup,
|
|
6626
6878
|
decodeNoteDisclosureKey,
|
|
6627
|
-
decodeNoteTransferPage,
|
|
6628
6879
|
decodePaymentSlip,
|
|
6629
6880
|
decodePrecompileCalldata,
|
|
6630
6881
|
decryptHintBatch,
|
|
6631
6882
|
decryptJson,
|
|
6632
6883
|
decryptNoteRecord,
|
|
6884
|
+
deriveIdentity,
|
|
6633
6885
|
deriveMasterKeyBytes,
|
|
6634
|
-
|
|
6635
|
-
|
|
6886
|
+
deriveOutgoingEphPk,
|
|
6887
|
+
deriveOutgoingEphSk,
|
|
6888
|
+
deriveOutgoingSharedSecret,
|
|
6889
|
+
deriveOutgoingViewingKeyV3,
|
|
6636
6890
|
deriveOwnerPk,
|
|
6637
6891
|
derivePairwiseEphSk,
|
|
6638
6892
|
derivePairwiseSharedSecret,
|
|
@@ -6641,6 +6895,7 @@ export {
|
|
|
6641
6895
|
deriveSpendingKeyFromSignature,
|
|
6642
6896
|
deriveSpendingKeyMessageV2,
|
|
6643
6897
|
deriveSpendingKeyTypedData,
|
|
6898
|
+
deriveSpendingKeyV3,
|
|
6644
6899
|
deriveStealthOwnerPk,
|
|
6645
6900
|
deriveStealthSk,
|
|
6646
6901
|
deriveVaultBlindKey,
|
|
@@ -6648,9 +6903,9 @@ export {
|
|
|
6648
6903
|
deriveViewTag,
|
|
6649
6904
|
deriveViewingPublicKey,
|
|
6650
6905
|
deriveViewingSecretKey,
|
|
6906
|
+
deriveViewingSecretKeyV3,
|
|
6651
6907
|
detectCommitmentMismatch,
|
|
6652
6908
|
encodeNoteBackup,
|
|
6653
|
-
encodeNoteTransferPages,
|
|
6654
6909
|
encodePaymentSlip,
|
|
6655
6910
|
encryptJson,
|
|
6656
6911
|
encryptNote,
|
|
@@ -6660,6 +6915,7 @@ export {
|
|
|
6660
6915
|
evmToImplicitSubstrate,
|
|
6661
6916
|
evmToMappedAccountHex,
|
|
6662
6917
|
evmToSubstrate,
|
|
6918
|
+
exportViewingCredential,
|
|
6663
6919
|
extractPalletError,
|
|
6664
6920
|
failed,
|
|
6665
6921
|
fastMulBase,
|
|
@@ -6700,9 +6956,9 @@ export {
|
|
|
6700
6956
|
isNativeAsset,
|
|
6701
6957
|
isNoteSelfConsistent,
|
|
6702
6958
|
isSpendable,
|
|
6703
|
-
isSs58,
|
|
6704
6959
|
isSubstrateAddress,
|
|
6705
6960
|
isUnifiedAddress,
|
|
6961
|
+
isUsableSecretKey,
|
|
6706
6962
|
isValidLeafIndex,
|
|
6707
6963
|
leHexToBigint,
|
|
6708
6964
|
mapExtrinsicArgs,
|
|
@@ -6719,10 +6975,10 @@ export {
|
|
|
6719
6975
|
noteMatchesCommitment,
|
|
6720
6976
|
noteOrigin,
|
|
6721
6977
|
noteSpentTxHash,
|
|
6722
|
-
noteToTransferEntry,
|
|
6723
6978
|
noteTxKind,
|
|
6724
|
-
openOutgoingBlob,
|
|
6725
6979
|
openPaymentSlip,
|
|
6980
|
+
openRecipientBookEntry,
|
|
6981
|
+
outgoingEphWindow,
|
|
6726
6982
|
outranks,
|
|
6727
6983
|
pairwiseEphWindow,
|
|
6728
6984
|
palletErrorKind,
|
|
@@ -6733,15 +6989,18 @@ export {
|
|
|
6733
6989
|
planTransfer,
|
|
6734
6990
|
planUnshield,
|
|
6735
6991
|
randomBlinding,
|
|
6736
|
-
|
|
6992
|
+
reconstructOutgoingIndex,
|
|
6737
6993
|
reconstructOutgoingTxRecords,
|
|
6738
6994
|
recoverOwnerPkPoint,
|
|
6739
6995
|
recoverSelfStealthNote,
|
|
6996
|
+
recoverSentFromSharedSecret,
|
|
6997
|
+
recoverSentNote,
|
|
6740
6998
|
refuseIfAlreadySpent,
|
|
6741
6999
|
regeneratePaymentSlip,
|
|
7000
|
+
registerPairwiseCounterparty,
|
|
6742
7001
|
removeByCommitment,
|
|
6743
7002
|
requireSessionKeys,
|
|
6744
|
-
|
|
7003
|
+
reserveOutgoingIndex,
|
|
6745
7004
|
reserveSelfEphIndex,
|
|
6746
7005
|
resolveSelfEphCeiling,
|
|
6747
7006
|
resolveSpentSet,
|
|
@@ -6750,8 +7009,8 @@ export {
|
|
|
6750
7009
|
runScan,
|
|
6751
7010
|
scalarToHex,
|
|
6752
7011
|
scanAbortError,
|
|
6753
|
-
sealOutgoingBlob,
|
|
6754
7012
|
sealPaymentSlip,
|
|
7013
|
+
sealRecipientBookEntry,
|
|
6755
7014
|
selectDescribingNote,
|
|
6756
7015
|
selectDescribingNoteByCommitment,
|
|
6757
7016
|
selectGhosts,
|
|
@@ -6777,10 +7036,10 @@ export {
|
|
|
6777
7036
|
truncateMiddle,
|
|
6778
7037
|
tryDecryptNote,
|
|
6779
7038
|
tryDecryptNoteVerbose,
|
|
6780
|
-
tryRecoverOutgoing,
|
|
6781
7039
|
txLandedAfterError,
|
|
6782
7040
|
u128,
|
|
6783
7041
|
u64,
|
|
7042
|
+
unpackUsableViewingKey,
|
|
6784
7043
|
unshieldNote,
|
|
6785
7044
|
upsertNote,
|
|
6786
7045
|
vaultReplacer,
|