@orbinum/sdk 2.1.0 → 3.0.1

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/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { Z as ZkNote, D as DecryptedMemo, N as NoteInput, S as ScanCommitment, a as NoteFacts, O as OutgoingNoteRecord, b as DecryptPool, c as ScanKeys } from './index-CLpM1984.js';
2
- export { C as CURRENT_CIRCUIT_VERSION, d as DECRYPT_YIELD_EVERY, e as DecryptBatchResult, f as DecryptRequest, E as EMPTY_BATCH_RESULT, K as KnownEphEntry, g as KnownEphWindow, M as MAX_WORKERS, h as MatchSource, i as MerkleTreeInfo, P as PAIRWISE_EPH_WINDOW, j as SELF_EPH_WINDOW, W as WORKER_CRASHED, k as WorkerFactory, l as WorkerLike, m as WorkerMessage, n as clearKnownEphWindow, o as createDecryptPool, p as createMainThreadPool, q as createWorkerPool, r as decryptHintBatch, s as getKnownEphWindow } from './index-CLpM1984.js';
1
+ import { Z as ZkNote, D as DecryptedMemo, N as NoteInput, S as ScanCommitment, a as NoteFacts, b as SentNoteMatch, c as DecryptPool, d as ScanKeys, U as UnmatchedSentHint } from './index-B-dbSIPN.js';
2
+ export { C as CURRENT_CIRCUIT_VERSION, e as DECRYPT_YIELD_EVERY, f as DecryptBatchResult, g as DecryptRequest, E as EMPTY_BATCH_RESULT, K as KnownEphEntry, h as KnownEphWindow, M as MAX_WORKERS, i as MerkleTreeInfo, O as OUTGOING_EPH_WINDOW, P as PAIRWISE_EPH_WINDOW, j as SELF_EPH_WINDOW, k as SentNoteFacts, W as WORKER_CRASHED, l as WorkerFactory, m as WorkerLike, n as WorkerMessage, o as clearKnownEphWindow, p as createDecryptPool, q as createMainThreadPool, r as createWorkerPool, s as decryptHintBatch, t as getKnownEphWindow, u as recoverSentFromSharedSecret, v as recoverSentNote } from './index-B-dbSIPN.js';
3
3
  import { ArtifactProvider, ProofResult, CircuitType } from '@orbinum/proof-generator';
4
4
  export { ArtifactProvider, CircuitType, ProofResult, WebArtifactProvider, shouldProveSingleThreaded } from '@orbinum/proof-generator';
5
5
  import * as polkadot_api from 'polkadot-api';
@@ -12,8 +12,8 @@ export { base58 } from '@scure/base';
12
12
  export { getPolkadotSigner as getSubstrateSigner } from 'polkadot-api/signer';
13
13
  import { InjectedExtension } from 'polkadot-api/pjs-signer';
14
14
  export { SignPayload, SignRaw, getPolkadotSignerFromPjs as getSubstrateSignerFromExtension } from 'polkadot-api/pjs-signer';
15
- import { V as VaultStorage, a as VaultConfigRecord, E as EncryptedNoteRecord, C as CachedNullifier, N as NullifierSyncMeta, S as SpendDetails, b as EncryptedTxRecord, d as NoteStorage, e as NoteStatusUpdate, T as TxHistoryStore, f as NullifierCache, c as SecretStore } from './secretStore-CF6Nse__.js';
16
- export { D as DeviceKeyStore, g as createDeviceKeyProvider, h as createMemorySecretStore, i as generateDeviceKey, j as importDeviceKey } from './secretStore-CF6Nse__.js';
15
+ import { V as VaultStorage, a as VaultConfigRecord, E as EncryptedNoteRecord, C as CachedNullifier, N as NullifierSyncMeta, S as SpendDetails, b as EncryptedTxRecord, d as NoteStorage, e as NoteStatusUpdate, T as TxHistoryStore, f as NullifierCache, c as SecretStore } from './secretStore-CCYo4hna.js';
16
+ export { D as DeviceKeyStore, g as createDeviceKeyProvider, h as createMemorySecretStore, i as generateDeviceKey, j as importDeviceKey } from './secretStore-CCYo4hna.js';
17
17
 
18
18
  /**
19
19
  * The two keys a vault runs on, and the AES-GCM envelope they protect.
@@ -210,7 +210,7 @@ declare const VAULT_SCHEMA_VERSION = 5;
210
210
  */
211
211
  declare function normalizeChainFingerprint(chainFingerprint?: string | null): string | undefined;
212
212
  /** The counters a config write must never drop. */
213
- type EphemeralCounters = Pick<VaultConfigRecord, 'createdAt' | 'selfEphCounter' | 'pairwiseCounterparties'>;
213
+ type EphemeralCounters = Pick<VaultConfigRecord, 'createdAt' | 'selfEphCounter' | 'outgoingEphCounter' | 'pairwiseCounterparties'>;
214
214
  /**
215
215
  * Builds a config record ready to persist.
216
216
  *
@@ -250,31 +250,43 @@ declare function buildConfig(existing: EphemeralCounters | null, chainFingerprin
250
250
  */
251
251
  declare function reserveSelfEphIndex(storage: NoteStorage): Promise<number>;
252
252
  /**
253
- * Reserves the next pairwise index for a counterparty, keyed by their packed
254
- * viewing public key. Registering the counterparty is a side effect worth
255
- * having: it makes the REVERSE direction cheap too, since their future payments
256
- * to this wallet become hash lookups instead of one trial ECDH per note.
253
+ * Reserves the next OUTGOING index the ephemeral published on a note this
254
+ * wallet sends.
257
255
  *
258
- * Returns `null` when this vault holds no history for that counterparty, and
259
- * the caller must then use a random ephemeral. The reason is that "no history"
260
- * has two causes this function cannot tell apart:
256
+ * "No history" is unambiguous here, unlike the pairwise counter: a sender can
257
+ * predict every ephPk they published, so a restored wallet recovers the counter
258
+ * by sweeping the chain. That is why this may start at 0 instead of degrading
259
+ * to a random ephemeral.
261
260
  *
262
- * - a genuine first payment, where index 0 is correct;
263
- * - a counter that was LOST a restored seed, a cleared IndexedDB, a wipe
264
- * and rescan where index 0 was already published and re-deriving it
265
- * republishes that ephPk, linking the two notes in public.
261
+ * THE COUNTER ONLY MOVES FORWARD. `fromChain` comes from a feed, and a feed
262
+ * that HID the highest published index would report a counter already used
263
+ * handing it out again republishes that ephPk and links the two notes in
264
+ * public. So it is a floor, never the truth: a stored counter always wins.
266
265
  *
267
- * Unlike `selfEphCounter`, this one cannot be recovered: the index was
268
- * published on a note encrypted toward someone else, so it never appears in
269
- * this wallet's own scan, and asking a server whether a given ephPk exists
270
- * would reveal which notes are ours. So the ambiguity is resolved the safe way
271
- * — the caller degrades to random, costing the recipient one trial scan, and
272
- * every later payment to the same counterparty takes the fast path again.
266
+ * The restore itself is the one exposure, having no stored counter to defend
267
+ * with. `RESTORE_SAFETY_GAP` covers it by resuming past a run of unused
268
+ * indexes, so hiding one top index is not enough a feed would have to hide
269
+ * the whole run. Skipped indexes are free and only widen a window.
273
270
  *
274
- * The entry is still created: the NEXT payment has a counter, and registering
275
- * the counterparty is what makes the reverse direction cheap.
271
+ * @param fromChain Next index per `reconstructOutgoingIndex`, or undefined on
272
+ * the normal path where the stored counter stands alone.
276
273
  */
277
- declare function reservePairwiseIndex(storage: NoteStorage, ivkHex: string): Promise<number | null>;
274
+ declare function reserveOutgoingIndex(storage: NoteStorage, fromChain?: number): Promise<number>;
275
+ /**
276
+ * Records a counterparty this wallet has paid, keyed by their packed viewing
277
+ * public key.
278
+ *
279
+ * Purely an optimisation for the REVERSE direction: once they are known, their
280
+ * future payments to this wallet are found by hash lookup against a precomputed
281
+ * window instead of one trial ECDH per note in the pool. Nothing this wallet
282
+ * publishes depends on it, so a failure here costs scan speed and nothing else.
283
+ *
284
+ * The stored `nextIndex` is legacy — outgoing ephemerals now come from the
285
+ * wallet-wide sequence in `outgoingEphCounter`, which is recoverable where a
286
+ * per-counterparty one was not. It is preserved rather than dropped so that a
287
+ * vault written by an older version still reads back cleanly.
288
+ */
289
+ declare function registerPairwiseCounterparty(storage: NoteStorage, ivkHex: string): Promise<void>;
278
290
 
279
291
  /**
280
292
  * Turning a note into a stored record and back.
@@ -290,7 +302,7 @@ declare function reservePairwiseIndex(storage: NoteStorage, ivkHex: string): Pro
290
302
  */
291
303
  declare function applyNoteStatus(note: ZkNote, status?: NoteStatusUpdate): ZkNote;
292
304
  /**
293
- * Encrypts a ZkNote into a v2 EncryptedNoteRecord.
305
+ * Encrypts a ZkNote into an `EncryptedNoteRecord`.
294
306
  *
295
307
  * The full note (values, secrets) is AES-GCM encrypted under `key`; the note
296
308
  * identifiers (commitment, nullifier, asset) are stored as BLIND TAGS under
@@ -664,6 +676,11 @@ declare function fromHex(hex: string): Uint8Array;
664
676
  declare function ensureHexPrefix(hex: string): string;
665
677
  /**
666
678
  * Converts a 0x-prefixed hex string (as returned by JSON-RPC) to a number.
679
+ *
680
+ * Throws rather than returning `NaN`. A bare `parseInt` accepts a valid prefix
681
+ * and drops the rest, so `"0x12zz"` used to yield `18` — a plausible block
682
+ * number that is simply wrong. It also has no upper bound: a quantity past
683
+ * 2^53 loses precision silently, and these values come from a server.
667
684
  */
668
685
  declare function hexToNumber(hex: string): number;
669
686
  /**
@@ -673,11 +690,10 @@ declare function hexToBigint(hex: string): bigint;
673
690
  /**
674
691
  * A field scalar as canonical 32-byte hex: `0x` + 64 zero-padded chars.
675
692
  *
676
- * Big-endian and fixed-width on purpose. These strings are compared as strings
677
- * — a wallet finds its own note by matching an `ownerPk`, and the note-transfer
678
- * format is a cross-client contract so one scalar must produce exactly one
679
- * spelling. Letting `toString(16)` emit its natural width would make `0x1` and
680
- * `0x0…01` two different keys for one value.
693
+ * Big-endian and fixed-width on purpose. These are compared AS STRINGS — a
694
+ * wallet finds its own note by matching an `ownerPk` that way — so one scalar
695
+ * must have exactly one spelling. `toString(16)` at its natural width would
696
+ * make `0x1` and `0x0…01` two different keys for the same value.
681
697
  *
682
698
  * Not for commitments or nullifiers: those travel LITTLE-endian and go through
683
699
  * `toHex(bigintTo32Le(...))`.
@@ -755,6 +771,78 @@ declare const BABYJUB_SUBORDER = 27360303589799094027808007181571593860768139721
755
771
  * Returns the point with the canonical (smaller) y, or null if Ax is not on the curve.
756
772
  */
757
773
  declare function recoverOwnerPkPoint(ax: bigint): [bigint, bigint] | null;
774
+ /**
775
+ * Unpack a recipient's packed viewing key, refusing keys that would make the
776
+ * ECDH secret guessable.
777
+ *
778
+ * Unpacking alone is not validation. BabyJubJub has a cofactor of 8, so the
779
+ * curve contains a small subgroup, and a point from it has order 1, 2, 4 or 8.
780
+ * `[ephSk]·P` then takes at most 8 values no matter how random `ephSk` is, and
781
+ * an interceptor simply tries them all: a memo sealed toward such a key yields
782
+ * its value, blinding and sourcePk, and a payment slip decrypts, without any
783
+ * secret at all. The all-zero packed value is one of them — an order-4 point.
784
+ *
785
+ * The all-zero byte check that guards privacy addresses does not catch this:
786
+ * the packed value `0` is not the neutral element, it is a legitimate point
787
+ * with y=0 that happens to have order 4. Nor is a low-order key necessarily
788
+ * hostile — it is also what a truncated or half-initialised buffer decodes to.
789
+ * Either way the note is world-readable, so both are refused here.
790
+ *
791
+ * Multiplying by the cofactor is the standard test: `[8]·P` is the identity for
792
+ * every point of the small subgroup and for no other point.
793
+ *
794
+ * @returns the point, or null when it is unusable as a recipient key
795
+ */
796
+ declare function unpackUsableViewingKey(packed: bigint): [bigint, bigint] | null;
797
+
798
+ /**
799
+ * Guards for SECRET key material.
800
+ *
801
+ * `unpackUsableViewingKey` covers the public side — a point someone else chose,
802
+ * where the risk is a low-order key that collapses the ECDH. This covers the
803
+ * other side: the 32-byte secrets a caller passes in, where the risk is that
804
+ * nothing checks them at all.
805
+ *
806
+ * ## Why a degenerate secret is not merely weak
807
+ *
808
+ * `bytesToBjjScalar` reduces whatever it gets modulo the suborder and clamps a
809
+ * zero result to `1n`. So an all-zero key, or a 16-byte one, does not fail —
810
+ * it becomes a VALID scalar that some other wallet could also arrive at. An
811
+ * empty array is worse: `BigInt('0x')` throws, which surfaces as an exception
812
+ * from whichever primitive happened to touch it first.
813
+ *
814
+ * The same shapes are what a missing field, a truncated buffer, or an
815
+ * uninitialised `new Uint8Array(32)` decode to, so this is the failure a
816
+ * mis-wired caller actually hits.
817
+ *
818
+ * ## Why these THROW
819
+ *
820
+ * A bad secret key is a programming error, not untrusted input. Returning null
821
+ * would fold it into "this note is not mine" / "this slip is not for me", which
822
+ * is indistinguishable from the ordinary case — the wallet would report an
823
+ * empty scan and look perfectly healthy.
824
+ *
825
+ * Data from the network is the opposite and must NOT use this: a malformed
826
+ * backup entry or scan hint is dropped, never thrown, so one bad record cannot
827
+ * take down the batch. `isUsableSecretKey` exists for the paths that have to
828
+ * make that decision without unwinding.
829
+ */
830
+ /**
831
+ * True when `key` can be used as secret key material.
832
+ *
833
+ * For callers that must not throw — a scan loop, a window builder — where the
834
+ * answer is "turn this capability off", not "abort".
835
+ */
836
+ declare function isUsableSecretKey(key: Uint8Array | undefined | null): key is Uint8Array;
837
+ /**
838
+ * Throws unless `key` is usable secret key material.
839
+ *
840
+ * @param key the secret to check
841
+ * @param label what it is, named in the error — a caller that wired two keys
842
+ * the wrong way round needs to know WHICH one is wrong, and every
843
+ * secret here is 32 opaque bytes that look alike in a debugger.
844
+ */
845
+ declare function assertSecretKeyBytes(key: Uint8Array, label: string): void;
758
846
 
759
847
  /** Affine point as zk-kit represents it. */
760
848
  type AffinePoint = [bigint, bigint];
@@ -820,6 +908,19 @@ interface CryptoKey$1 {
820
908
  readonly usages: string[];
821
909
  }
822
910
 
911
+ /**
912
+ * Rendering on-chain amounts for people.
913
+ *
914
+ * Chain amounts are integers in the smallest unit — 18 decimals for ORB — and
915
+ * every one of these keeps them that way internally: BigInt arithmetic only, no
916
+ * float ever touches a balance. A `number` intermediate would silently lose
917
+ * precision past 2^53, which for 18 decimals starts at about 0.009 ORB.
918
+ *
919
+ * Malformed input yields ZERO rather than a guess. Grouped digits, scientific
920
+ * notation and a leading `+` are all rejected: coercing them would render an
921
+ * amount that is not the one on chain, and a balance nobody can explain is
922
+ * worse than a visible zero.
923
+ */
823
924
  /**
824
925
  * Options for {@link formatBalance}.
825
926
  */
@@ -831,6 +932,7 @@ interface FormatOptions {
831
932
  /** Whether to append the symbol. Defaults to `true`. */
832
933
  showSymbol?: boolean;
833
934
  /** Maximum number of decimal digits shown in output. Defaults to `6`. */
935
+ /** MAXIMUM decimal digits shown. Trailing zeros are trimmed, never padded. */
834
936
  precision?: number;
835
937
  }
836
938
  /**
@@ -851,7 +953,7 @@ interface FormatOptions {
851
953
  *
852
954
  * @example
853
955
  * formatBalance('1000000000000000000') // '1 ORB'
854
- * formatBalance(500000000000000000n, { precision: 2 }) // '0.50 ORB'
956
+ * formatBalance(500000000000000000n, { precision: 2 }) // '0.5 ORB' (max, not pad)
855
957
  * formatBalance('0x0de0b6b3a7640000', { showSymbol: false }) // '1'
856
958
  * formatBalance(null) // '0 ORB'
857
959
  */
@@ -931,18 +1033,19 @@ declare function normalizeEvmAddress(addr: string): string;
931
1033
  * a UI renders, not an exception.
932
1034
  */
933
1035
  declare function parseEvmAddress(addr: string): string | null;
934
- /**
935
- * Returns true if the string looks like an SS58 encoded address
936
- * (not a 0x-prefixed hex).
937
- */
938
- declare function isSs58(addr: string): boolean;
939
1036
  /**
940
1037
  * Returns true if the string looks like a 20-byte EVM address.
941
1038
  */
942
1039
  declare function isEvmAddress(addr: string): boolean;
943
1040
  /**
944
- * Pads a 20-byte EVM address to a 32-byte account ID (H256)
945
- * by prepending 12 zero bytes (Ethereum-compatible mapping).
1041
+ * Pads a 20-byte EVM address to 32 bytes by PREPENDING 12 zero bytes.
1042
+ *
1043
+ * The Ethereum convention, NOT Orbinum's. This chain maps an H160 to an account
1044
+ * by appending — `evmToImplicitSubstrate` is the one that matches the runtime,
1045
+ * and the two produce different accounts for the same address.
1046
+ *
1047
+ * Kept for callers that need the Ethereum-shaped padding (an H256 topic, an ABI
1048
+ * word). Anything that has to name an Orbinum account wants the other one.
946
1049
  */
947
1050
  declare function evmAddressToAccountId(evmAddr: string): Uint8Array;
948
1051
  /**
@@ -1035,10 +1138,10 @@ declare function addressToFieldElement(address: string): bigint;
1035
1138
  * Layout (180 bytes, ECDH):
1036
1139
  * nonce(12) || ciphertext+MAC(136) || ephPk_packed(32) = 180
1037
1140
  *
1038
- * View tag (memos built by SDK ≥ 0.15): nonce[0] = deriveViewTag(sharedSecret)
1039
- * see memo.ts. Layout and size are unchanged; legacy memos carry a random
1040
- * byte there instead, so the filter is only sound at/after the wallet's
1041
- * tagActivationLeaf.
1141
+ * View tag: nonce[0] = deriveViewTag(sharedSecret). Layout and size unchanged.
1142
+ * Memos written before tags carry a RANDOM byte there, so the filter is only
1143
+ * sound at or after the leaf where they were switched on — the scanner passes
1144
+ * that boundary as `ScanKeys.viewTagActivationLeaf`.
1042
1145
  *
1043
1146
  * Plaintext layout (120 bytes):
1044
1147
  * value_lo(8 LE) || value_hi(8 LE) || owner_pk(32) || blinding(32) || asset_id(4 LE) || source_pk(32) || circuit_version(4 LE)
@@ -1046,7 +1149,9 @@ declare function addressToFieldElement(address: string): bigint;
1046
1149
  * value is stored as a 128-bit LE unsigned integer (two uint64 words), supporting
1047
1150
  * amounts up to ~3.4 × 10^38 planck — well above any realistic token supply.
1048
1151
  *
1049
- * v2 key derivation (ECDH):
1152
+ * Key derivation (ECDH). The "v2" in the memo's own wire format names THIS
1153
+ * layout, and is unrelated to the identity version — the v3 identity work
1154
+ * changed which branches derive a wallet's keys, not how a memo is sealed:
1050
1155
  * ephSk = random scalar in [1, BABYJUB_SUBORDER)
1051
1156
  * ephPk = mulPointEscalar(Base8, ephSk)
1052
1157
  * sharedPoint = mulPointEscalar(recipientIvk, ephSk) ← or mulPointEscalar(ephPk, ivsk)
@@ -1063,7 +1168,7 @@ declare const ENCRYPTED_MEMO_SIZE: number;
1063
1168
  declare function bytesToBjjScalar(bytes: Uint8Array): bigint;
1064
1169
  declare const EncryptedMemo: {
1065
1170
  /**
1066
- * Build and encrypt a memo for a note using ECDH (v2, 180 bytes).
1171
+ * Build and encrypt a memo for a note using ECDH (180 bytes).
1067
1172
  *
1068
1173
  * @param value Note value in planck.
1069
1174
  * @param ownerPk 32-byte owner public key (LE).
@@ -1129,9 +1234,9 @@ declare const EncryptedMemo: {
1129
1234
  * Cheap view-tag check: does memo nonce[0] match the tag derived from
1130
1235
  * `sharedSecret`? One SHA256 + one byte compare — no AEAD work.
1131
1236
  *
1132
- * Only meaningful for memos built with view tags (commitments at/after
1133
- * the wallet's tagActivationLeaf): a legacy memo carries a random byte
1134
- * there and would false-negative 255/256 of the time.
1237
+ * Only meaningful for memos that carry a tag at or after
1238
+ * `ScanKeys.viewTagActivationLeaf`. An older memo has a random byte there
1239
+ * and would false-negative 255/256 of the time.
1135
1240
  */
1136
1241
  checkViewTag(memoBytes: Uint8Array, sharedSecret: Uint8Array): boolean;
1137
1242
  /**
@@ -1171,76 +1276,6 @@ declare function serializeMemo(value: bigint, ownerPk: Uint8Array, blinding: Uin
1171
1276
  */
1172
1277
  declare function deriveViewTag(sharedSecret: Uint8Array): number;
1173
1278
 
1174
- /**
1175
- * OutgoingBlob — the OVK "wrap-the-key" primitive.
1176
- *
1177
- * When a wallet sends a private_transfer, the recipient's memo is encrypted ECDH
1178
- * toward the RECIPIENT's ivk; the sender cannot reopen it, so a cold restore
1179
- * loses the outgoing history (who/how much). This blob fixes that: it wraps the
1180
- * memo's 32-byte `sharedSecret` under a key derived from the SENDER's ovk. On
1181
- * restore the sender unwraps the sharedSecret and feeds it to the SAME memo
1182
- * decryption path the recipient uses — one decryption routine in the whole
1183
- * system (OVK plan requirement #7).
1184
- *
1185
- * This is Zcash Sapling's `outCiphertext` / Penumbra's `OvkWrappedKey`, adapted
1186
- * to a shared secret that is a field element (32 bytes), not a group point — so
1187
- * the blob contains no group element to decode, sidestepping Zcash's canonical-
1188
- * encoding class of bugs.
1189
- *
1190
- * Layout (56 bytes): nonce_suffix(8) || ciphertext(32) || MAC(16).
1191
- * Cipher: ChaCha20-Poly1305 (IETF), same primitive as EncryptedMemo.
1192
- *
1193
- * The outgoing cipher key binds every unique-per-output public value:
1194
- * ock = HKDF(ikm=ovk, salt=commitment_bytes||ephPk_bytes, info=OCK_DOMAIN)
1195
- * so each ock encrypts exactly one message. Uniqueness comes from the COMMITMENT
1196
- * (Poseidon4 with random blinding, consensus rejects duplicates); the ephPk adds
1197
- * the on-chain binding, not the uniqueness — two self-notes can share an ephPk
1198
- * but never a commitment, so their ock still differs.
1199
- *
1200
- * SALT IS RAW ON-CHAIN BYTES. `commitment_bytes` and `ephPk_bytes` must be the
1201
- * exact bytes that are/go on chain, never a re-serialization of a decoded point:
1202
- * two encodings of "the same point" produce different ock and the AEAD fails —
1203
- * no ambiguity to exploit.
1204
- */
1205
- /** Total on-chain blob size: nonce_suffix(8) || ciphertext(32) || MAC(16). */
1206
- declare const OVK_BLOB_SIZE: number;
1207
- /**
1208
- * Derive the 32-byte outgoing cipher key.
1209
- * ock = HKDF-SHA256(ikm=ovk, salt=commitmentBytes||ephPkBytes, info=OCK_DOMAIN)
1210
- *
1211
- * Both salt parts must be the raw 32-byte on-chain values (see file header).
1212
- * Throws if either is not exactly 32 bytes — a wrong length here would silently
1213
- * change the key and make recovery fail far from the cause.
1214
- */
1215
- declare function deriveOutgoingCipherKey(ovk: Uint8Array, commitmentBytes: Uint8Array, ephPkBytes: Uint8Array): Uint8Array;
1216
- /**
1217
- * Seal a sharedSecret into a 56-byte outgoing blob under the sender's ovk.
1218
- *
1219
- * @param ovk sender's 32-byte outgoing viewing key
1220
- * @param sharedSecret the memo's 32-byte ECDH shared secret (what we wrap)
1221
- * @param commitmentBytes raw 32-byte on-chain commitment of the recipient output
1222
- * @param ephPkBytes raw 32-byte ephemeral public key (from the memo, not recomputed)
1223
- * @returns 56 bytes: nonce_suffix(8) || ciphertext(32) || MAC(16)
1224
- */
1225
- declare function sealOutgoingBlob(ovk: Uint8Array, sharedSecret: Uint8Array, commitmentBytes: Uint8Array, ephPkBytes: Uint8Array): Uint8Array;
1226
- /**
1227
- * Open an outgoing blob, returning the wrapped sharedSecret or null.
1228
- *
1229
- * Never throws — this runs in recovery loops over hints that may be foreign,
1230
- * pre-OVK, or ovk=⊥ random. A wrong ovk, wrong commitment, wrong ephPk, or any
1231
- * corrupted byte fails the MAC and returns null.
1232
- */
1233
- declare function openOutgoingBlob(ovk: Uint8Array, blob: Uint8Array, commitmentBytes: Uint8Array, ephPkBytes: Uint8Array): Uint8Array | null;
1234
- /**
1235
- * A 56-byte random blob for the ovk=⊥ case (sender opts out of recoverability).
1236
- *
1237
- * Indistinguishable on-chain from a real blob: a real one is a random 8-byte
1238
- * suffix plus ChaCha20/Poly1305 output, both uniform without the key. This is
1239
- * the ONLY path for ⊥ — never zeros (distinguishable), never omission (a length
1240
- * or SCALE-tag difference is a permanent, retroactive privacy label).
1241
- */
1242
- declare function randomOutgoingBlob(): Uint8Array;
1243
-
1244
1279
  /** The fields a payment slip carries — all public on-chain. */
1245
1280
  interface PaymentSlipFields {
1246
1281
  /** 0x-prefixed 32-byte LE commitment hex of the recipient output. */
@@ -1292,7 +1327,7 @@ declare function decodePaymentSlip(text: string): Uint8Array | null;
1292
1327
  * Derive the deterministic 32-byte ephemeral secret for self-note `index`.
1293
1328
  * Feed it to EncryptedMemo.encrypt / NoteBuilder.build as `ephSkOverride`.
1294
1329
  */
1295
- declare function deriveSelfEphSk(spendingKey: bigint, index: number): Uint8Array;
1330
+ declare function deriveSelfEphSk(viewingSecretKey: Uint8Array, index: number): Uint8Array;
1296
1331
  /** One precomputed self-note window entry. */
1297
1332
  interface SelfEphWindowEntry {
1298
1333
  index: number;
@@ -1307,11 +1342,11 @@ interface SelfEphWindowEntry {
1307
1342
  * needed to decrypt the memo. One EC pass up front; scanning then matches
1308
1343
  * hints by ephPk hex equality with no per-hint EC work.
1309
1344
  *
1310
- * @param spendingKey Wallet spending key (the seed of the derivation).
1345
+ * @param viewingSecretKey Wallet incoming viewing key (the seed of the derivation).
1311
1346
  * @param ivkPacked The wallet's OWN 32-byte LE packed viewing public key —
1312
1347
  * self memos are encrypted to it.
1313
1348
  */
1314
- declare function selfEphWindow(spendingKey: bigint, ivkPacked: Uint8Array, from: number, count: number): SelfEphWindowEntry[];
1349
+ declare function selfEphWindow(viewingSecretKey: Uint8Array, ivkPacked: Uint8Array, from: number, count: number): SelfEphWindowEntry[];
1315
1350
 
1316
1351
  /**
1317
1352
  * The secret shared by a sender/receiver pair: ECDH between one side's viewing
@@ -1353,6 +1388,83 @@ interface PairwiseEphWindowEntry {
1353
1388
  */
1354
1389
  declare function pairwiseEphWindow(pairSecret: Uint8Array, receiverIvkPacked: Uint8Array, from: number, count: number): PairwiseEphWindowEntry[];
1355
1390
 
1391
+ /**
1392
+ * The ephemeral secret for the `index`-th note this wallet sends. Feed it to
1393
+ * `NoteBuilder.build` as `ephSkOverride`.
1394
+ */
1395
+ declare function deriveOutgoingEphSk(outgoingViewingKey: Uint8Array, index: number): Uint8Array;
1396
+ /** The ephPk this wallet publishes at `index` — the memo's last 32 bytes. */
1397
+ declare function deriveOutgoingEphPk(outgoingViewingKey: Uint8Array, index: number): string;
1398
+ /** One precomputed outgoing window entry. */
1399
+ interface OutgoingEphWindowEntry {
1400
+ index: number;
1401
+ /** 0x-prefixed LE-packed ephPk — byte-identical to the memo's last 32 bytes. */
1402
+ ephPkHex: string;
1403
+ }
1404
+ /**
1405
+ * Precompute [from, from+count) of the outgoing sequence.
1406
+ *
1407
+ * Only the ephPk, never a shared secret: unlike the self and pairwise windows,
1408
+ * the secret that opens an outgoing memo depends on the RECIPIENT's viewing
1409
+ * key, which is not known until the note is matched. Recovery derives it then,
1410
+ * per candidate, with `deriveOutgoingSharedSecret`.
1411
+ */
1412
+ declare function outgoingEphWindow(outgoingViewingKey: Uint8Array, from: number, count: number): OutgoingEphWindowEntry[];
1413
+ /**
1414
+ * The secret that opens the memo of the payment this wallet sent at `index`.
1415
+ *
1416
+ * The same value the recipient computes from their viewing secret key and the
1417
+ * published ephPk — reached from the other side, which is what lets the sender
1418
+ * read back a memo sealed toward someone else.
1419
+ *
1420
+ * Throws on a viewing key that is not a curve point; callers sweeping
1421
+ * candidates should treat that as "not this one".
1422
+ */
1423
+ declare function deriveOutgoingSharedSecret(outgoingViewingKey: Uint8Array, index: number, recipientIvkPacked: Uint8Array): Uint8Array;
1424
+ /**
1425
+ * The next unused outgoing index, read off the chain.
1426
+ *
1427
+ * The sender can predict every ephPk they would publish, so the highest one
1428
+ * that actually appears on chain names the last index used. This is what the
1429
+ * pairwise counter cannot do — its ephemerals depend on a counterparty the
1430
+ * restored wallet no longer knows — and it is why an outgoing sequence may
1431
+ * start at 0 instead of degrading to a random ephemeral.
1432
+ *
1433
+ * ## This result must never be trusted on its own
1434
+ *
1435
+ * Omission is a WRITE attack: a feed that hides the highest index makes this
1436
+ * return a value already published, and using it republishes that ephPk. Two
1437
+ * rules follow, both enforced by the caller in `reserveOutgoingIndex`:
1438
+ *
1439
+ * - never move a counter BACKWARDS — a local counter always wins;
1440
+ * - after a restore, where there is no local counter to compare against,
1441
+ * leave a gap so a feed would have to hide a whole run of consecutive
1442
+ * top indexes rather than a single one.
1443
+ *
1444
+ * @param window How many indexes to consider — must cover the highest ever
1445
+ * used, or a high index stays invisible and gets handed out
1446
+ * again. Grow it with the counter, as `selfEphGap` does.
1447
+ */
1448
+ declare function reconstructOutgoingIndex(outgoingViewingKey: Uint8Array, publishedEphPks: Iterable<string>, window: number): number;
1449
+
1450
+ /**
1451
+ * Building a note: its commitment, its nullifier, and the memo that recovers it.
1452
+ *
1453
+ * Everything is local — no chain access — and every note leaves here in one of
1454
+ * two shapes:
1455
+ *
1456
+ * - STEALTH, when the caller supplies both halves of a privacy address. The
1457
+ * commitment covers a one-time owner key, so two payments to one address
1458
+ * cannot be linked, and the memo carries what the recipient needs to
1459
+ * rederive the matching spending key.
1460
+ * - PLAIN, for a wallet's own notes (a shield, a change note), where the
1461
+ * commitment covers the wallet's global owner key because it has to find
1462
+ * the note again with its own keys.
1463
+ *
1464
+ * The blinding is the note's only real secret once an amount is guessed, so an
1465
+ * absent one is drawn from a CSPRNG and never from anything predictable.
1466
+ */
1467
+
1356
1468
  /**
1357
1469
  * Builds ZK notes (commitment + nullifier) and encrypted memos locally.
1358
1470
  *
@@ -1380,7 +1492,7 @@ declare class NoteBuilder {
1380
1492
  * @param input.value Amount in planck (required).
1381
1493
  * @param input.assetId Asset ID — default 0n (native ORB-Privacy).
1382
1494
  * @param input.ownerPk Sender's or recipient's global BabyJubJub Ax — default 0n.
1383
- * @param input.blinding Random scalar — defaults to BigInt(Date.now()).
1495
+ * @param input.blinding Random scalar — defaults to a CSPRNG draw.
1384
1496
  * @param input.spendingKey Secret key for nullifier — default 0n.
1385
1497
  * @param input.viewingPublicKey Recipient's 32-byte LE packed BJJ ivk. Triggers memo encryption.
1386
1498
  * @param input.recipientOwnerPk Recipient's global ownerPk. Required with viewingPublicKey
@@ -1397,7 +1509,7 @@ declare class NoteBuilder {
1397
1509
  * @param note The ZkNote whose fields populate the plaintext.
1398
1510
  * @param recipientIvkPacked 32-byte LE packed BJJ viewing public key of the recipient.
1399
1511
  * Pass `new Uint8Array(32)` (default) for a public/dummy memo.
1400
- * @param sourcePk 32-byte counterparty BabyJubJub Ax.
1512
+ * @param sourcePk 32-byte counterparty BabyJubJub Ax.
1401
1513
  * Pass `new Uint8Array(32)` (default) for no counterparty.
1402
1514
  */
1403
1515
  static buildMemo(note: ZkNote, recipientIvkPacked?: Uint8Array, sourcePk?: Uint8Array): Uint8Array;
@@ -1453,8 +1565,9 @@ interface TryDecryptOptions {
1453
1565
  * 1-byte tag in memo nonce[0], and skip the AEAD decrypt on mismatch
1454
1566
  * (255/256 of foreign notes; `reason: 'view_tag_mismatch'`).
1455
1567
  *
1456
- * Only enable for commitments at/after the wallet's tagActivationLeaf
1457
- * legacy memos carry a random byte there and would be silently dropped.
1568
+ * Enable only for commitments at or after the leaf where tags were switched
1569
+ * on the scanner tracks it as `ScanKeys.viewTagActivationLeaf`. Older
1570
+ * memos carry a random byte there and would be dropped in silence.
1458
1571
  */
1459
1572
  viewTag?: boolean;
1460
1573
  /**
@@ -1489,41 +1602,6 @@ declare function tryDecryptNoteVerbose(commitment: ScanCommitment, viewingSecret
1489
1602
  note: ZkNote | null;
1490
1603
  reason?: string;
1491
1604
  };
1492
- /** A scan hint plus the per-transaction OVK blob the indexer serves alongside it. */
1493
- type OutgoingHint = ScanCommitment & {
1494
- ovkBlob?: string | null;
1495
- };
1496
- /**
1497
- * Recover a note the caller SENT, using their outgoing viewing key (ovk).
1498
- *
1499
- * The sender's memo was encrypted toward the RECIPIENT, so they cannot reopen it
1500
- * directly. Instead the ovk blob wraps the memo's shared secret; unwrapping it
1501
- * gives the same secret the recipient gets via ECDH, and feeding it to the shared
1502
- * decrypt-and-verify step rebuilds the sent note's public facts.
1503
- *
1504
- * Returns an OutgoingNoteRecord (value, recipient stealth pk, counterparty,
1505
- * circuit version) — NEVER a spendable note: no spendingKey, no nullifier. The
1506
- * sender does not own this note. Never throws (runs in recovery loops).
1507
- *
1508
- * Security (OVK plan §3.4), in three non-algebraic but sound layers:
1509
- * 1. The blob MAC (openOutgoingBlob) proves whoever wrote it knew the ovk, and
1510
- * the ock binds it to THIS (commitment, ephPk) — blobs are not transplantable.
1511
- * 2. The memo MAC (inside decryptAndVerifyPlaintext) proves this shared secret
1512
- * is the one that encrypted this memo — the same secret the recipient derives.
1513
- * 3. The commitment check ties the recovered fields to the note in the tree.
1514
- * Not verifiable by the sender: that `sharedSecret` corresponds to `ephPk` — that
1515
- * needs the recipient's ivsk. Residual: a leaked ovk lets an attacker fabricate a
1516
- * self-consistent (commitment, memo, blob) and plant false outgoing history. It
1517
- * moves no funds; Zcash accepts the same residual. Mitigated in the app by only
1518
- * running this over commitments from extrinsics that spent our own nullifiers.
1519
- *
1520
- * @param hint scan hint with commitmentHex, encryptedMemo, and ovkBlob.
1521
- * @param ovk the sender's 32-byte outgoing viewing key.
1522
- * @param opts viewTagActivationLeaf gates the view-tag check for legacy memos.
1523
- */
1524
- declare function tryRecoverOutgoing(hint: OutgoingHint, ovk: Uint8Array, opts?: {
1525
- viewTagActivationLeaf?: number;
1526
- }): OutgoingNoteRecord | null;
1527
1605
  /**
1528
1606
  * Collect what a sender can still say about a note they sent.
1529
1607
  *
@@ -1543,6 +1621,43 @@ declare function tryRecoverOutgoing(hint: OutgoingHint, ovk: Uint8Array, opts?:
1543
1621
  */
1544
1622
  declare function collectOutgoingFacts(hint: ScanCommitment): NoteFacts | null;
1545
1623
 
1624
+ /**
1625
+ * Seal (or open — XOR is its own inverse) a recipient's packed viewing key for
1626
+ * carriage in a change note's `sourcePk`.
1627
+ *
1628
+ * Returns the input unchanged when it is not 32 bytes: a caller with a
1629
+ * malformed key must not silently ship a truncated one, and every consumer
1630
+ * here already treats an unusable book entry as "no recipient known".
1631
+ *
1632
+ * @param ivkPacked 32-byte packed viewing key, sealed or plain
1633
+ * @param outgoingViewingKey this wallet's OUTGOING viewing key (ovk) — the
1634
+ * capability "see what I sent", delegable on its own
1635
+ * @param paymentCommitmentHex the PAYMENT's commitment — NOT the change note's.
1636
+ * It is the one value both sides hold: the sender
1637
+ * has it while building, and a restored wallet has
1638
+ * it the moment the outgoing window matches that
1639
+ * payment, which is the only time the entry is
1640
+ * needed.
1641
+ */
1642
+ declare function sealRecipientBookEntry(ivkPacked: Uint8Array, outgoingViewingKey: Uint8Array, paymentCommitmentHex: string): Uint8Array;
1643
+ /**
1644
+ * Recover a recipient's packed viewing key from a change note's `sourcePk`.
1645
+ *
1646
+ * The value is transported as a raw 32-byte little-endian integer with no field
1647
+ * reduction, so a ciphertext above the BN254 modulus survives intact — which
1648
+ * matters, because most of them are.
1649
+ *
1650
+ * The result is unverified by construction: whether it is really a viewing key
1651
+ * is settled by trying to open the payment memo with it.
1652
+ *
1653
+ * Returns 32 zero bytes for anything that cannot be a `sourcePk` — negative, or
1654
+ * wider than the 32 bytes the field is carved from. A memo can never yield one
1655
+ * (the slice is 32 bytes), but this is public API taking a bigint, and every
1656
+ * neighbour here answers an impossible input with a value no memo will open
1657
+ * rather than an exception a caller has to catch mid-scan.
1658
+ */
1659
+ declare function openRecipientBookEntry(sealedSourcePk: bigint, outgoingViewingKey: Uint8Array, paymentCommitmentHex: string): Uint8Array;
1660
+
1546
1661
  /**
1547
1662
  * Proving what ONE note holds, without granting any power to spend it.
1548
1663
  *
@@ -1631,12 +1746,24 @@ interface ProofOptions {
1631
1746
  * `Data cannot be cloned, out of memory`.
1632
1747
  *
1633
1748
  * Pass it only when the host knows better than the heuristic — a desktop
1634
- * app certain of its environment, or a benchmark pinning one mode. The
1635
- * proof is byte-identical either way; only the wall-clock changes.
1749
+ * app certain of its environment, or a benchmark pinning one mode. Either
1750
+ * mode yields a proof the chain verifies; only the wall-clock changes.
1636
1751
  */
1637
1752
  singleThread?: boolean;
1638
1753
  }
1639
1754
 
1755
+ /**
1756
+ * Witness assembly for the 2-in/2-out private-transfer circuit.
1757
+ *
1758
+ * Pure marshalling: bigints and hex in, decimal strings the prover accepts out.
1759
+ * Nothing here VALIDATES the witness — the circuit is what enforces every
1760
+ * constraint, and a caller that assembles an inconsistent one gets a proof
1761
+ * failure rather than a wrong proof.
1762
+ *
1763
+ * Always two inputs and two outputs. A single-note spend pads slot B with a
1764
+ * dummy (see `buildDummyTransferInput`), because the circuit's shape is fixed.
1765
+ */
1766
+
1640
1767
  /** A single input note for a private transfer. */
1641
1768
  interface TransferInputNote {
1642
1769
  nullifier: bigint;
@@ -1668,20 +1795,49 @@ interface PrivateTransferProofInputs {
1668
1795
  merkleRoot: string;
1669
1796
  inputs: [TransferInputNote, TransferInputNote];
1670
1797
  outputs: [TransferOutputNote, TransferOutputNote];
1671
- /** Gasless fee in planck (default 0n). Must satisfy: input_sum == output_sum + fee */
1798
+ /**
1799
+ * Gasless fee in planck (default 0n).
1800
+ *
1801
+ * The circuit constrains `input_sum == output_sum + fee`. Nothing here
1802
+ * checks it — the caller balances the amounts (`transferNotes` does, before
1803
+ * proving) and an unbalanced witness fails at proof time.
1804
+ */
1672
1805
  fee?: bigint;
1673
1806
  }
1674
1807
  /**
1675
1808
  * Generate a Groth16 proof for a PrivateTransfer operation.
1809
+ *
1810
+ * The merkle root arrives LITTLE-ENDIAN hex and is converted here; passing it
1811
+ * big-endian yields a root the circuit cannot match against the paths.
1676
1812
  */
1677
1813
  declare function generateTransferProof(params: PrivateTransferProofInputs, options?: ProofOptions): Promise<ProofResult>;
1678
1814
 
1679
1815
  /**
1680
- * Leaves one forest tree holds. Pinned to the runtime's `MaxLeavesPerTree`
1681
- * (2^20), guarded on-chain by the pallet's `integrity_test` — it can never
1682
- * change on a live chain, so deriving tree membership locally is safe.
1816
+ * Which notes pay an amount, and what the forest allows.
1817
+ *
1818
+ * The circuit is 2-in/2-out, so a spend draws on at most TWO notes and the
1819
+ * selection is a search over pairs rather than a greedy sum. A pair must agree
1820
+ * on two things beyond covering the amount: the same CIRCUIT VERSION, since one
1821
+ * proof is verified against one VK, and the same FOREST TREE, since both paths
1822
+ * resolve under one root and a cross-tree pair can never converge.
1823
+ *
1824
+ * Pure arithmetic over notes: nothing here reserves anything or touches the
1825
+ * chain, and every guard it applies is one the circuit would apply anyway,
1826
+ * seconds later and without naming the note.
1827
+ */
1828
+
1829
+ /**
1830
+ * Leaves one forest tree holds — the value this SDK assumes for the chain's
1831
+ * `MaxLeavesPerTree`.
1832
+ *
1833
+ * The pallet's `integrity_test` requires that constant to be a power of two no
1834
+ * greater than `2^MAX_TREE_DEPTH`, and forbids changing it on a live chain,
1835
+ * precisely because clients derive `tree_id` from a global leaf index with it.
1836
+ * It does NOT pin it to 2^20: a chain configured lower still passes, and this
1837
+ * constant would then place notes in the wrong tree.
1683
1838
  *
1684
- * Exported so a host can reason about tree boundaries.
1839
+ * Exported so a host can reason about tree boundaries — and so a deployment on
1840
+ * a differently-configured chain has one place to look.
1685
1841
  */
1686
1842
  declare const LEAVES_PER_TREE: number;
1687
1843
  /**
@@ -1922,6 +2078,18 @@ declare function deriveSpendingKeyMessageV2(chainId: number, address: string): s
1922
2078
  */
1923
2079
  declare function canonicalAccountId(address: string): string;
1924
2080
 
2081
+ /**
2082
+ * The derivation scheme an identity was built under.
2083
+ *
2084
+ * Only `v3` exists. It lives here, beside the derivations themselves, rather
2085
+ * than in the wallet: the vault name and the address scheme both hang off it,
2086
+ * and having `protocol` reach up into `wallet` for the type would invert the
2087
+ * layering for a single string union.
2088
+ *
2089
+ * Kept as a named union rather than dropped: the next scheme gets added here,
2090
+ * and a bare string would let a typo pick a vault nobody ever wrote to.
2091
+ */
2092
+ type IdentityVersion = 'v3';
1925
2093
  /**
1926
2094
  * Reduces master key bytes to the circuit's spending-key scalar.
1927
2095
  *
@@ -1934,17 +2102,46 @@ declare function canonicalAccountId(address: string): string;
1934
2102
  */
1935
2103
  declare function deriveSpendingKeyFromMaster(masterBytes: Uint8Array): bigint;
1936
2104
  /**
1937
- * Derive a 32-byte viewing secret key (ivsk) from the spending key.
2105
+ * The v2 viewing secret key, chained off the SPENDING key.
1938
2106
  * ivsk = HKDF-SHA256(ikm=bigintTo32Le(spendingKey), info="orbinum-ivk-v1")
1939
2107
  *
1940
- * The ivsk is intentionally derived from the already-reduced spending key scalar
1941
- * (not from masterBytes) so that it stays bound to the specific key identity
1942
- * loaded in this session. The spendingKey must already be in [1, BABYJUB_SUBORDER).
2108
+ * NOT what a v3 wallet uses. `deriveViewingSecretKeyV3(masterBytes)` is the
2109
+ * live branch, and the two produce DIFFERENT keys from one signature: deriving
2110
+ * this one beside a v3 identity yields a second wallet whose notes the first
2111
+ * cannot see. `PrivacyKeyManager` only ever calls the v3 branch.
2112
+ *
2113
+ * Chaining off the spending key is exactly what v3 removed — it made a viewing
2114
+ * key impossible to delegate, since anything that revealed it revealed the key
2115
+ * that spends. Kept for reading material derived under the older scheme.
1943
2116
  *
1944
- * SECURITY: This is a symmetric secret — never embed it in a shareable address.
1945
- * Use deriveViewingPublicKey() to obtain the public component for sharing.
2117
+ * SECURITY: a symmetric secret — never embed it in a shareable address. Use
2118
+ * `deriveViewingPublicKey()` for the public half.
1946
2119
  */
1947
2120
  declare function deriveViewingSecretKey(spendingKey: bigint): Uint8Array;
2121
+ /**
2122
+ * The v3 spending scalar — the authority to move funds, and nothing else.
2123
+ *
2124
+ * Reduced into the curve's scalar field like its v2 counterpart, so the circuit
2125
+ * accepts it unchanged. Unlike v2 it is a LEAF: no other key hangs off it.
2126
+ */
2127
+ declare function deriveSpendingKeyV3(rootSecret: Uint8Array): bigint;
2128
+ /**
2129
+ * The v3 incoming viewing key — read what arrives, spend nothing.
2130
+ *
2131
+ * This is the branch that makes a watch-only wallet possible: it is a sibling of
2132
+ * the spending key, so holding it proves nothing about the spending key and
2133
+ * gives no path to it.
2134
+ */
2135
+ declare function deriveViewingSecretKeyV3(rootSecret: Uint8Array): Uint8Array;
2136
+ /**
2137
+ * The v3 outgoing viewing key — read what this wallet SENT.
2138
+ *
2139
+ * Seeds the outgoing ephemerals and the recipient book, so a holder can
2140
+ * enumerate every payment the wallet made and name who received it. That is the
2141
+ * capability, not a leak — but it makes this strictly more sensitive than the
2142
+ * incoming viewing key, which only reveals what arrived.
2143
+ */
2144
+ declare function deriveOutgoingViewingKeyV3(rootSecret: Uint8Array): Uint8Array;
1948
2145
  /**
1949
2146
  * Derive the packed BabyJubJub viewing public key (ivk) from ivsk bytes.
1950
2147
  *
@@ -1955,7 +2152,7 @@ declare function deriveViewingSecretKey(spendingKey: bigint): Uint8Array;
1955
2152
  * The packed bigint is stored in little-endian so it is consistent with the
1956
2153
  * rest of the SDK's 32-byte scalar encoding (bigintTo32Le / bytesToBigintLE).
1957
2154
  *
1958
- * @param ivsk 32-byte HKDF output from deriveViewingSecretKey().
2155
+ * @param ivsk 32-byte viewing secret key — `deriveViewingSecretKeyV3` on v3.
1959
2156
  * @returns 32-byte LE-encoded packed BJJ point (goes in the privacy address).
1960
2157
  */
1961
2158
  declare function deriveViewingPublicKey(ivsk: Uint8Array): Uint8Array;
@@ -1966,20 +2163,6 @@ declare function deriveViewingPublicKey(ivsk: Uint8Array): Uint8Array;
1966
2163
  * Returns 0n if BabyJubJub computation fails (e.g. invalid scalar).
1967
2164
  */
1968
2165
  declare function deriveOwnerPk(spendingKey: bigint): bigint;
1969
- /**
1970
- * Derive the 32-byte outgoing viewing key (ovk) from master bytes.
1971
- * ovk = HKDF-SHA256(ikm=masterBytes, info="orbinum-ovk-v1")
1972
- *
1973
- * Mirror of the vault-key derivation: rooted at masterBytes, not the spendingKey
1974
- * scalar (see the derivation chain above for why). The ovk lets the SENDER of a
1975
- * private transfer recover what they sent — it wraps the memo's shared secret so
1976
- * a cold restore rebuilds the outgoing history. Sibling of the ivsk, delegable
1977
- * independently.
1978
- *
1979
- * SECRET. Never embed it in a shareable address — it stays out of
1980
- * encodePrivacyAddress by construction (there is no public component to derive).
1981
- */
1982
- declare function deriveOutgoingViewingKey(masterBytes: Uint8Array): Uint8Array;
1983
2166
 
1984
2167
  /**
1985
2168
  * PrivacyKeyManager
@@ -1994,17 +2177,31 @@ declare function deriveOutgoingViewingKey(masterBytes: Uint8Array): Uint8Array;
1994
2177
  * The caller (application layer) is responsible for key persistence and session
1995
2178
  * caching. Each instance holds independent state — safe for multi-wallet use.
1996
2179
  *
1997
- * Derivation scheme (from wallet signature):
2180
+ * Derivation scheme (from wallet signature) — every branch hangs off the root:
1998
2181
  * sig → HKDF → masterBytes (32 bytes, stable root for all derived keys)
1999
- * ├── spendingKey = BigInt(masterBytes) % BABYJUB_SUBORDER (circuit scalar)
2000
- * ├── viewingSecretKey = HKDF(bigintTo32Le(spendingKey), info="orbinum-ivk-v1") ← NEVER shared
2001
- * ├── viewingPublicKey = packPoint(BJJ_mul(Base8, ivsk_scalar))embedded in privacy address
2182
+ * ├── spendingKey = HKDF(masterBytes, info="orbinum-spend-v3") (circuit scalar)
2183
+ * ├── viewingSecretKey = HKDF(masterBytes, info="orbinum-ivk-v3") ← delegable
2184
+ * ├── outgoingViewingKey= HKDF(masterBytes, info="orbinum-ovk-v3") ← own-payment recovery
2185
+ * ├── viewingPublicKey = packPoint(BJJ_mul(Base8, ivsk_scalar)) ← in the privacy address
2002
2186
  * ├── ownerPk = BabyJubJub Ax from (spendingKey × Base8)
2003
2187
  * └── vaultKey = HKDF(masterBytes, info="orbinum-vault-key-v1") ← stable
2004
2188
  *
2189
+ * The branches are DISJOINT: knowing one reveals nothing about the others, so a
2190
+ * viewing key can be handed out without handing out the ability to spend.
2191
+ *
2005
2192
  * Cache format: "mk:0x{masterBytes_hex}" — storing masterBytes (not the sk scalar)
2006
2193
  * ensures the vault key remains stable if the circuit modulus ever changes.
2007
2194
  */
2195
+
2196
+ /**
2197
+ * Every address scheme this decoder still READS.
2198
+ *
2199
+ * Reading an old address is not the same as supporting the old identity: an
2200
+ * address is just an owner key plus a viewing key, and paying one works no
2201
+ * matter which scheme derived it. Only `orbpriv3` is ever EMITTED — see
2202
+ * `encodePrivacyAddress`.
2203
+ */
2204
+ type PrivacyAddressScheme = 'orbpriv1' | 'orbpriv2' | 'orbpriv3';
2008
2205
  declare class PrivacyKeyManager {
2009
2206
  private _state;
2010
2207
  /**
@@ -2012,11 +2209,12 @@ declare class PrivacyKeyManager {
2012
2209
  * Derives viewingSecretKey, viewingPublicKeyPacked, and ownerPk immediately.
2013
2210
  * Replaces any previously loaded key.
2014
2211
  *
2015
- * @param spendingKey Circuit scalar: BigInt(masterBytes) % BABYJUB_SUBORDER, clamped to [1, ∞).
2016
- * @param masterBytes Raw 32-byte HKDF output before modular reduction. Used to derive
2017
- * the stable vault key (HKDF(masterBytes, info="orbinum-vault-key-v1")).
2212
+ * @param _legacySpendingKey IGNORED. Kept so v2-era callers still compile;
2213
+ * the scalar is re-derived from `masterBytes`.
2214
+ * @param masterBytes Raw 32-byte HKDF output. The root every v3 branch and
2215
+ * the stable vault key hang off.
2018
2216
  */
2019
- load(spendingKey: bigint, masterBytes: Uint8Array): Promise<void>;
2217
+ load(_legacySpendingKey: bigint, masterBytes: Uint8Array): Promise<void>;
2020
2218
  /** Clear all key material from memory. Call on vault lock / sign-out. */
2021
2219
  clear(): void;
2022
2220
  /** Returns true if a spending key has been loaded. */
@@ -2037,13 +2235,24 @@ declare class PrivacyKeyManager {
2037
2235
  * Throws if not loaded.
2038
2236
  */
2039
2237
  getViewingPublicKeyPacked(): Uint8Array;
2040
- /** Returns the BabyJubJub owner public key (x-coordinate). Throws if not loaded. */
2041
- getOwnerPk(): bigint;
2042
2238
  /**
2043
- * Returns the 32-byte outgoing viewing key (ovk). Throws if not loaded.
2044
- * Used to seal/open the outgoing blob that lets the sender recover a transfer.
2239
+ * The OUTGOING viewing key (ovk) reads what this wallet SENT.
2240
+ *
2241
+ * Seeds the outgoing ephemeral sequence and opens the recipient book, so a
2242
+ * holder can enumerate every payment the wallet made and name who received
2243
+ * it. That makes it strictly more sensitive than the incoming viewing key,
2244
+ * which yields amounts and no counterparties: hand it out only where the
2245
+ * payment graph is meant to travel with it.
2246
+ *
2247
+ * A scan without it still finds every note the wallet OWNS and simply
2248
+ * reconstructs no sent history — the watch-only case.
2249
+ *
2250
+ * SECURITY: never expose this in an address or a network request.
2251
+ * Throws if not loaded.
2045
2252
  */
2046
2253
  getOutgoingViewingKey(): Uint8Array;
2254
+ /** Returns the BabyJubJub owner public key (x-coordinate). Throws if not loaded. */
2255
+ getOwnerPk(): bigint;
2047
2256
  /** Returns the spending key as a 32-byte little-endian Uint8Array. Throws if not loaded. */
2048
2257
  getSpendingKeyBytes(): Uint8Array;
2049
2258
  /**
@@ -2062,7 +2271,7 @@ declare class PrivacyKeyManager {
2062
2271
  * Exports a shareable privacy address encoding the owner public key and
2063
2272
  * viewing PUBLIC key of the currently loaded identity.
2064
2273
  *
2065
- * Format: `orbpriv2:{ownerPk_hex}:{viewingPublicKey_hex}:{checksum}`
2274
+ * Format: `orbpriv3:{ownerPk_hex}:{viewingPublicKey_hex}:{checksum}`
2066
2275
  *
2067
2276
  * The recipient uses this address so the sender can:
2068
2277
  * 1. Embed `ownerPk` in the note commitment (Poseidon4 input).
@@ -2077,24 +2286,35 @@ declare class PrivacyKeyManager {
2077
2286
  *
2078
2287
  * Throws if no key is loaded.
2079
2288
  */
2080
- encodePrivacyAddress(): string;
2289
+ encodePrivacyAddress(version?: IdentityVersion): string;
2081
2290
  /**
2082
2291
  * Decode a privacy address into `{ ownerPkHex, viewingPublicKeyHex }`, or
2083
2292
  * `null` if it does not parse.
2084
2293
  *
2085
- * Accepts both:
2086
- * - `orbpriv2:{ownerPk}:{ivk}:{checksum}` — checksum verified; a mismatch
2087
- * (corrupted paste) returns null.
2088
- * - `orbpriv1:{ownerPk}:{ivk}` — legacy, no checksum. Still read so addresses
2089
- * shared before v2 keep resolving; only v2 is emitted.
2294
+ * Reads every scheme ever emitted; only `orbpriv3` is written.
2295
+ *
2296
+ * - `orbpriv3` / `orbpriv2` — checksum verified; a mismatch (corrupted
2297
+ * paste, or a relabelled prefix) returns null.
2298
+ * - `orbpriv1` legacy, predates the checksum entirely. The FIELDS are
2299
+ * validated exactly as above, since this branch has no integrity check
2300
+ * of its own.
2301
+ *
2302
+ * Reading an old address is not the same as supporting an old identity. An
2303
+ * address is an owner key and a viewing key; paying one works whatever
2304
+ * scheme derived it, and whoever shares it may not have upgraded yet.
2305
+ * Refusing them here would break incoming payments for no gain.
2090
2306
  */
2091
2307
  static decodePrivacyAddress(address: string): {
2092
2308
  ownerPkHex: string;
2093
2309
  viewingPublicKeyHex: string;
2310
+ scheme: PrivacyAddressScheme;
2094
2311
  } | null;
2095
2312
  /**
2096
2313
  * Load keys from a cached "mk:0x{masterBytes_hex}" string produced by exportHex().
2097
- * Throws if the format is invalid or masterBytes length is not 32 bytes.
2314
+ *
2315
+ * Throws on a bad prefix, on non-hex content, and on a master that is not
2316
+ * 32 usable bytes — an all-zero one included, since every wallet would
2317
+ * derive the same identity from it.
2098
2318
  */
2099
2319
  importFromHex(hex: string): Promise<void>;
2100
2320
  }
@@ -2110,14 +2330,19 @@ declare const MIN_SIGNATURE_BYTES = 32;
2110
2330
  *
2111
2331
  * masterBytes = HKDF-SHA256(ikm=sigBytes, salt=empty, info="orbinum-sk-v2:{chainId}:{address}")
2112
2332
  *
2113
- * These bytes are the stable root for ALL derived keys:
2114
- * - spendingKey (circuit scalar) = BigInt(masterBytes) % BABYJUB_SUBORDER
2115
- * - viewingSecretKey = HKDF(bigintTo32Le(spendingKey), info="orbinum-ivk-v1")
2333
+ * These bytes are the stable root for ALL derived keys, each its own HKDF
2334
+ * branch of the SAME root none derived from another:
2335
+ * - spendingKey (circuit scalar) = HKDF(masterBytes, info="orbinum-spend-v3")
2336
+ * - viewingSecretKey = HKDF(masterBytes, info="orbinum-ivk-v3")
2337
+ * - outgoingViewingKey = HKDF(masterBytes, info="orbinum-ovk-v3")
2116
2338
  * - vaultKey = HKDF(masterBytes, info="orbinum-vault-key-v1")
2117
2339
  *
2118
- * Separating masterBytes from the circuit scalar means the viewingSecretKey and
2119
- * vault key are STABLE across any future change to the modulus — they never
2120
- * depend on which prime field the circuit uses.
2340
+ * Disjoint branches are what make a viewing key delegable: handing one out
2341
+ * grants sight and never the ability to spend.
2342
+ *
2343
+ * Separating masterBytes from the circuit scalar also keeps the viewing and
2344
+ * vault keys STABLE across any future change to the modulus — they never depend
2345
+ * on which prime field the circuit uses.
2121
2346
  *
2122
2347
  * @throws If the signature is not valid hex, or carries less entropy than the
2123
2348
  * shortest real signing scheme (see {@link MIN_SIGNATURE_BYTES}).
@@ -2126,12 +2351,13 @@ declare function deriveMasterKeyBytes(signatureHex: string, chainId: number, add
2126
2351
  /**
2127
2352
  * Derives an Orbinum spending key from a wallet signature.
2128
2353
  *
2129
- * Uses HKDF-SHA256(ikm=sigBytes, salt=empty, info="orbinum-sk-v2:{chainId}:{address}")
2130
- * and reduces the resulting 32-byte value modulo BABYJUB_SUBORDER.
2354
+ * Reduces the master bytes modulo BABYJUB_SUBORDER directly. NOT the v3
2355
+ * branch a v3 wallet derives its spending key with
2356
+ * `deriveSpendingKeyV3(masterBytes)`, through HKDF under `orbinum-spend-v3`,
2357
+ * and the two produce different scalars from the same signature.
2131
2358
  *
2132
- * IMPORTANT: viewingSecretKey and vaultKey must be derived from masterBytes (via
2133
- * deriveMasterKeyBytes), NOT from this spending key scalar. This ensures those
2134
- * keys remain stable if the circuit's modulus ever changes again.
2359
+ * Kept for reading material derived under the older scheme. Every other key
2360
+ * comes from `masterBytes` and never from this scalar, so it is not a root.
2135
2361
  *
2136
2362
  * @param signatureHex 0x-prefixed or bare hex of the wallet signature.
2137
2363
  * @param chainId Chain ID used when building the signing message.
@@ -2142,6 +2368,21 @@ declare function deriveMasterKeyBytes(signatureHex: string, chainId: number, add
2142
2368
  */
2143
2369
  declare function deriveSpendingKeyFromSignature(signatureHex: string, chainId: number, address: string): Promise<bigint>;
2144
2370
 
2371
+ /**
2372
+ * Witness assembly for the unshield circuit — one note in, a public payout and
2373
+ * an optional change note out.
2374
+ *
2375
+ * The only entry point here that BUILDS anything rather than marshalling it:
2376
+ * a partial unshield needs a change note, and its blinding and commitment are
2377
+ * computed here when the caller does not supply them. The blinding comes from
2378
+ * a CSPRNG — it is the only unknown in `Poseidon4(value, assetId, ownerPk,
2379
+ * blinding)` once an observer guesses the amount, so a predictable one makes
2380
+ * the commitment brute-forceable.
2381
+ *
2382
+ * A total unshield has no change, and the circuit constrains its commitment to
2383
+ * be exactly zero — not a commitment to a zero-value note.
2384
+ */
2385
+
2145
2386
  /**
2146
2387
  * Inputs required to generate an Unshield proof.
2147
2388
  *
@@ -2698,7 +2939,10 @@ declare class EvmClient {
2698
2939
  request<T>(method: string, params?: unknown[]): Promise<T>;
2699
2940
  /**
2700
2941
  * Performs multiple JSON-RPC calls in a single HTTP request (batch).
2701
- * Results are returned in the same order as `calls`, as a typed tuple.
2942
+ *
2943
+ * Results come back in the order of `calls`, as a typed tuple. A call the
2944
+ * server answered with an error, or did not answer at all, lands as `null`
2945
+ * in its own slot — never shifting the ones after it.
2702
2946
  */
2703
2947
  batchRequest<T extends unknown[]>(calls: Array<{
2704
2948
  method: string;
@@ -2726,7 +2970,10 @@ declare class EvmClient {
2726
2970
  sendRawTransaction(signedHex: string): Promise<string>;
2727
2971
  /** Executes a read-only call without creating a transaction. Returns the raw ABI-encoded response. */
2728
2972
  call(to: string, data: string, from?: string): Promise<string>;
2729
- /** Estimates the gas required for a transaction. Returns the estimate in wei as a `bigint`. */
2973
+ /**
2974
+ * Estimates a transaction's gas, in GAS UNITS — not wei. Multiply by
2975
+ * `getGasPrice()` for a cost.
2976
+ */
2730
2977
  estimateGas(params: {
2731
2978
  from?: string;
2732
2979
  to: string;
@@ -2787,7 +3034,11 @@ interface EvmTransaction {
2787
3034
  nonce: number;
2788
3035
  /** ABI-encoded call data (0x-prefixed hex). `'0x'` for plain transfers. */
2789
3036
  input: string;
2790
- /** `1` for success, `0` for revert. */
3037
+ /**
3038
+ * `1` for success, `0` for revert — and `0` ALSO when no receipt could be
3039
+ * read, which is what a pending transaction or a failed receipt call looks
3040
+ * like. Not proof of a revert on its own.
3041
+ */
2791
3042
  status: number;
2792
3043
  /** Deployed contract address for contract-creation transactions; `null` otherwise. */
2793
3044
  contractAddress: string | null;
@@ -2823,7 +3074,12 @@ interface EvmAddressInfo {
2823
3074
  codeSize: number;
2824
3075
  /** Deployed bytecode (truncated to 100 bytes + ellipsis for display). `'0x'` for EOAs. */
2825
3076
  code: string;
2826
- /** Up to 50 most recent logs emitted by or received by this address. */
3077
+ /**
3078
+ * Up to 50 recent logs EMITTED BY this address — `eth_getLogs` filters on
3079
+ * the emitting contract, so nothing where the address merely appears as an
3080
+ * indexed topic is here. A plain EOA emits nothing and gets an empty list;
3081
+ * `getTokenTransfers` is the call that finds received activity.
3082
+ */
2827
3083
  recentLogs: EvmLog[];
2828
3084
  }
2829
3085
  /** EVM block header with transaction hashes (not full tx objects). */
@@ -2865,7 +3121,12 @@ interface EvmTxSummary {
2865
3121
  gasUsed: number;
2866
3122
  /** Gas price in wei, as a 0x-prefixed hex string. */
2867
3123
  gasPrice: string;
2868
- /** `true` when the transaction succeeded (receipt status `0x1`). */
3124
+ /**
3125
+ * `true` on receipt status `0x1` — and ALSO when no receipt could be read,
3126
+ * since a transaction still in the pool has not failed. Optimistic by
3127
+ * design, and the opposite default from `EvmTransaction.status`: read it as
3128
+ * "not known to have failed", never as confirmed success.
3129
+ */
2869
3130
  status: boolean;
2870
3131
  /** `true` when the transaction created a new contract (`to` is `null`). */
2871
3132
  isContractCreation: boolean;
@@ -2926,7 +3187,7 @@ declare class EvmExplorer {
2926
3187
  getTransactionsByAddress(address: string, maxBlocks?: number): Promise<EvmTxSummary[]>;
2927
3188
  /**
2928
3189
  * Returns aggregated on-chain data for an EVM address: balance, nonce,
2929
- * bytecode (truncated), and up to 50 recent logs from the last 1 000 blocks.
3190
+ * bytecode (truncated), and up to 50 logs it EMITTED in the last 1 001 blocks.
2930
3191
  */
2931
3192
  getAddressInfo(address: string): Promise<EvmAddressInfo>;
2932
3193
  /** Returns the native token balance of `address`, formatted as a decimal string (no symbol). */
@@ -2941,7 +3202,7 @@ declare class EvmExplorer {
2941
3202
  */
2942
3203
  getTokenInfo(address: string): Promise<TokenInfo | null>;
2943
3204
  /**
2944
- * Returns ERC-20 `Transfer` events for `address` from the last 1 000 blocks.
3205
+ * Returns ERC-20 `Transfer` events for `address` from the last 1 001 blocks.
2945
3206
  * When `holderAddress` is provided, restricts results to transfers sent or received by that address.
2946
3207
  */
2947
3208
  getTokenTransfers(address: string, holderAddress?: string): Promise<TokenTransfer[]>;
@@ -3316,8 +3577,14 @@ declare class PrivacyModule {
3316
3577
  constructor(substrate: SubstrateClient);
3317
3578
  /** Returns the current Merkle tree root. */
3318
3579
  getMerkleRoot(): Promise<string>;
3319
- /** Returns the Merkle proof for the given leaf index or commitment hex. */
3320
- getMerkleProof(leafIndex: number | string): Promise<RpcV2MerkleProof>;
3580
+ /**
3581
+ * Returns the Merkle proof for a LEAF INDEX.
3582
+ *
3583
+ * The node declares this parameter `u32`, so a commitment hex is rejected
3584
+ * by deserialisation before any lookup runs — `getMerkleProofByCommitment`
3585
+ * is the one that takes a string.
3586
+ */
3587
+ getMerkleProof(leafIndex: number): Promise<RpcV2MerkleProof>;
3321
3588
  /**
3322
3589
  * Returns the Merkle inclusion proof for a given commitment hex,
3323
3590
  * bundled with the Merkle root.
@@ -3572,9 +3839,17 @@ declare class CryptoPrecompiles {
3572
3839
  * const stats = await client.privacy.getPoolStats();
3573
3840
  * console.log('root:', stats.merkleRoot, 'leaves:', stats.commitmentCount);
3574
3841
  *
3575
- * // Shield tokens (with a SubstrateSigner)
3842
+ * // Shield tokens (with a SubstrateSigner). `NoteBuilder.build` produces the
3843
+ * // commitment and the 180-byte memo together — both are required, and a note
3844
+ * // shielded without a valid memo is unrecoverable.
3845
+ * const note = await NoteBuilder.build({ value: 1000n, assetId: 1n, ownerPk });
3576
3846
  * const result = await client.shieldedPool.shield(
3577
- * { assetId: 1, amount: 1000n, commitment: '0xabc...' },
3847
+ * {
3848
+ * assetId: 1,
3849
+ * amount: note.value,
3850
+ * commitment: note.commitmentHex,
3851
+ * encryptedMemo: Uint8Array.from(note.memo),
3852
+ * },
3578
3853
  * signer,
3579
3854
  * );
3580
3855
  * console.log('tx ok:', result.ok, 'block:', result.blockHash);
@@ -3797,9 +4072,13 @@ declare class OrbinumClientProvider {
3797
4072
  private stopStableTimer;
3798
4073
  /**
3799
4074
  * Schedules the next connection attempt using exponential backoff
3800
- * (capped at `reconnectMaxMs`) with full jitter, then transitions to
3801
- * `'reconnecting'`. Jitter (a random fraction of the delay) spreads out
3802
- * reconnects so many clients don't retry in lockstep after a shared outage.
4075
+ * (capped at `reconnectMaxMs`) with EQUAL jitter, then transitions to
4076
+ * `'reconnecting'`.
4077
+ *
4078
+ * The delay lands in `[capped/2, capped]` — half fixed, half random. That
4079
+ * spreads a shared outage's reconnects over a `capped/2` window instead of
4080
+ * having every client retry in lockstep, while keeping a floor so the first
4081
+ * attempts do not all pile up near zero the way full jitter allows.
3803
4082
  */
3804
4083
  private scheduleReconnect;
3805
4084
  private cancelReconnect;
@@ -3832,11 +4111,19 @@ declare class OrbinumClientProvider {
3832
4111
  }
3833
4112
 
3834
4113
  /**
3835
- * Utilities for decoding on-chain extrinsic arguments and event data.
4114
+ * Naming the positional fields of a decoded extrinsic or event.
4115
+ *
4116
+ * A Substrate/PAPI node returns `arg0`, `arg1`, … when metadata-based decoding
4117
+ * is unavailable. These two tables map those positions onto the field names the
4118
+ * pallets declare, so a caller reads `nullifier` instead of `arg2`.
4119
+ *
4120
+ * BEST EFFORT, BY METHOD NAME ONLY. Neither helper is given the pallet section,
4121
+ * so two pallets that declare the same method name are indistinguishable here —
4122
+ * see the `Executed` note below. A consumer that needs certainty decodes
4123
+ * against metadata instead.
3836
4124
  *
3837
- * Substrate/PAPI nodes return positional arg keys (`arg0`, `arg1`, …) when
3838
- * metadata-based decoding is unavailable. These helpers map those positions
3839
- * to human-readable semantic names for all Orbinum pallets.
4125
+ * `events.ts` is the canonical model of the shielded-pool events; this file is
4126
+ * a display convenience and does not replace it.
3840
4127
  */
3841
4128
  /**
3842
4129
  * Maps raw extrinsic args (which may use positional keys like `arg0`, `arg1`)
@@ -3872,16 +4159,14 @@ declare class RpcV2Module {
3872
4159
  /**
3873
4160
  * Classification of shielded-pool failures out of raw chain errors.
3874
4161
  *
3875
- * These are pallet vocabulary, not app policy: the strings come from the
3876
- * runtime's error variants, and what each one MEANS for a wallet — resync the
3877
- * vault, purge a note is protocol knowledge. A consumer without these treats
3878
- * a ghost note as a generic failure and leaves the unspendable note in place.
4162
+ * Pallet vocabulary, not app policy. The strings are the runtime's error
4163
+ * variants, and what each MEANS for a wallet — resync, purge a note, retry — is
4164
+ * protocol knowledge: without it a ghost note reads as a generic failure and
4165
+ * the unspendable note stays in the vault.
3879
4166
  *
3880
- * The split with a host is deliberate. This module answers "what kind of
3881
- * failure is this, and what should a wallet DO about it"; the words shown to a
3882
- * person, in whatever language, belong to the host. Two wallets should agree
3883
- * that `UnknownMerkleRoot` means "rescan and retry" while phrasing it however
3884
- * they like.
4167
+ * This module answers "what kind of failure, and what should a wallet DO"; the
4168
+ * words a person reads belong to the host. Two wallets should agree that
4169
+ * `UnknownMerkleRoot` means "rescan and retry" while phrasing it differently.
3885
4170
  */
3886
4171
  /**
3887
4172
  * The pallet error name inside a raw chain error, or null.
@@ -4022,7 +4307,7 @@ type ShieldedEvent = {
4022
4307
  amount: bigint;
4023
4308
  /** 0x-prefixed 32-byte Poseidon commitment (LE). */
4024
4309
  commitment: string;
4025
- /** 0x-prefixed encrypted memo bytes (104 bytes). */
4310
+ /** 0x-prefixed encrypted memo (180 bytes see `ENCRYPTED_MEMO_SIZE`). */
4026
4311
  encryptedMemo: string;
4027
4312
  /** Leaf index assigned in the Merkle tree. */
4028
4313
  leafIndex: number;
@@ -4051,7 +4336,11 @@ type CommitmentsInsertedEvent = {
4051
4336
  };
4052
4337
  /**
4053
4338
  * Emitted by `unshield()` when a note is withdrawn to the public chain.
4054
- * Rust variant: `Unshielded { nullifier, amount, recipient, change_commitment }`
4339
+ *
4340
+ * Rust variant: `Unshielded { nullifier, amount, recipient, change_commitment,
4341
+ * change_encrypted_memo, change_leaf_index }`. The last two are NOT modelled
4342
+ * here — a consumer that needs the change note's memo or its leaf position
4343
+ * reads the raw event, or finds the note by scanning.
4055
4344
  */
4056
4345
  type UnshieldedEvent = {
4057
4346
  /** 0x-prefixed 32-byte Poseidon nullifier (LE). */
@@ -4074,7 +4363,11 @@ type MerkleRootUpdatedEvent = {
4074
4363
  oldRoot: string;
4075
4364
  /** 0x-prefixed new Merkle root (32 bytes, LE). */
4076
4365
  newRoot: string;
4077
- /** Total number of leaves after the update. */
4366
+ /**
4367
+ * Total leaves ever inserted, GLOBAL across the whole forest — never the
4368
+ * current tree's size. Dense and monotonic, which is what indexer chunking
4369
+ * and scan cursors rely on; per-tree size is `treeSize % LEAVES_PER_TREE`.
4370
+ */
4078
4371
  treeSize: number;
4079
4372
  };
4080
4373
  /**
@@ -4098,7 +4391,13 @@ type AssetVerifiedEvent = {
4098
4391
  type AssetUnverifiedEvent = {
4099
4392
  assetId: number;
4100
4393
  };
4101
- /** All events emitted by pallet-shielded-pool as a discriminated union. */
4394
+ /**
4395
+ * The pallet events this SDK models, as a discriminated union.
4396
+ *
4397
+ * NOT every variant: `TreeSealed` (forest rotation) and `ValidatorFeesClaimed`
4398
+ * are emitted by the pallet and absent here. A consumer that acts on a tree
4399
+ * rotation has to read the raw event.
4400
+ */
4102
4401
  type ShieldedPoolEvent = {
4103
4402
  type: 'Shielded';
4104
4403
  data: ShieldedEvent;
@@ -4557,6 +4856,20 @@ interface TransferFactsSource {
4557
4856
  byCommitments(commitments: string[]): Promise<TransferFactsRow[]>;
4558
4857
  }
4559
4858
 
4859
+ /**
4860
+ * What a scan reports: progress while it runs, and its outcome when it ends.
4861
+ *
4862
+ * A host renders both, so these are the scanner's public vocabulary. The
4863
+ * counters are DIAGNOSTICS, not results — the notes themselves land in the
4864
+ * vault, and every field here exists so a host can tell a scan that found
4865
+ * nothing from a scan that failed to look.
4866
+ *
4867
+ * That distinction is the reason the discovery counters are surfaced at all:
4868
+ * the fast paths fail silently by design (a window that never matches costs
4869
+ * only speed), so without them "the fast path is off" and "the fast path ran
4870
+ * and matched nothing" are indistinguishable.
4871
+ */
4872
+
4560
4873
  /** Live progress, reported after each scanned page or chunk. */
4561
4874
  interface ScanProgress {
4562
4875
  scanned: number;
@@ -4573,12 +4886,48 @@ interface ScanResult {
4573
4886
  purged: number;
4574
4887
  /** Whether this was a full scan (from leafIndex 0) or an incremental one. */
4575
4888
  incremental: boolean;
4889
+ /**
4890
+ * Notes this wallet SENT, recovered during the same sweep.
4891
+ *
4892
+ * Free of extra requests — the outgoing ephPk window recognises our own
4893
+ * payments, and the change notes in the same feed carry the recipient keys
4894
+ * that open them. Feed them to `reconstructOutgoingTxRecords` to turn a
4895
+ * derived history row into the exact amount, plus a re-issued payment slip.
4896
+ */
4897
+ sentNotes: SentNoteMatch[];
4898
+ /**
4899
+ * How the scan found what it found — diagnostics, not results.
4900
+ *
4901
+ * The two fast routes recognise a note by hash lookup against a precomputed
4902
+ * window instead of a trial ECDH per hint, and they are the difference
4903
+ * between a scan that takes seconds and one that takes minutes. Both fail
4904
+ * SILENTLY: a window that never matches costs only speed, so the notes
4905
+ * quietly take the expensive path and nothing reports it.
4906
+ *
4907
+ * Surfaced here so a host can tell "the fast path is off" from "the fast
4908
+ * path ran". The counters existed before this and were accumulated where
4909
+ * nothing could read them.
4910
+ */
4911
+ discovery: {
4912
+ /** Own notes matched via the self-eph window (shields, change). */
4913
+ self: number;
4914
+ /** Notes from registered counterparties matched via their pairwise window. */
4915
+ pairwise: number;
4916
+ /** Hints rejected by the 1-byte view-tag filter, before any AEAD work. */
4917
+ tagFiltered: number;
4918
+ };
4576
4919
  }
4577
4920
 
4578
4921
  interface WalletScanKeys {
4579
4922
  viewingKey: Uint8Array;
4580
4923
  spendingKey: bigint;
4581
4924
  ownerPk: bigint;
4925
+ /**
4926
+ * Outgoing viewing key (ovk). Omit to scan WITHOUT payment history: every
4927
+ * note the wallet owns is still found, and the sender-side record of what
4928
+ * it paid simply is not reconstructed. That is the watch-only case.
4929
+ */
4930
+ outgoingViewingKey?: Uint8Array;
4582
4931
  }
4583
4932
  interface RunScanParams {
4584
4933
  vault: VaultStore;
@@ -4620,7 +4969,6 @@ declare function gapMargin(windowSize: number): number;
4620
4969
  declare function resolveSelfEphCeiling(params: {
4621
4970
  /** The wallet's notes AFTER persist — self notes carry their ephPk in the memo. */
4622
4971
  notes: ZkNote[];
4623
- spendingKey: bigint;
4624
4972
  viewingKey: Uint8Array;
4625
4973
  /** Highest window-matched index reported by the scan. */
4626
4974
  scanMaxIndex: number | null;
@@ -4632,10 +4980,11 @@ declare function resolveSelfEphCeiling(params: {
4632
4980
  * the gap margin) falls inside the fast path, and never past `MAX_EPH_WINDOW`.
4633
4981
  *
4634
4982
  * The counter is read from a config that survives restores and hand-editing, so
4635
- * a non-finite value is treated as "no history" rather than propagated: `NaN`
4636
- * would make the builder's `i < from + count` false immediately and return an
4637
- * EMPTY window, silently disabling the fast path, while `Infinity` would make
4638
- * that same loop never terminate.
4983
+ * a non-finite value is treated as "no history" rather than propagated. The
4984
+ * window builders reject a non-integer `count` outright, so passing one through
4985
+ * would abort the scan with a throw turning a corrupt stored counter into a
4986
+ * wallet that cannot scan at all, where falling back to the default size costs
4987
+ * only the fast path for indexes past it.
4639
4988
  */
4640
4989
  declare function windowSizeForCounter(counter: number): number;
4641
4990
 
@@ -4719,8 +5068,41 @@ interface ScanOutcome {
4719
5068
  * path fired rather than the notes quietly taking the expensive route.
4720
5069
  */
4721
5070
  pairwiseDiscovered: number;
5071
+ /**
5072
+ * Notes this wallet SENT, recovered in the same sweep.
5073
+ *
5074
+ * Free of extra requests: the outgoing ephPk window recognises our own
5075
+ * payments, and the change notes in the same feed carry the recipient keys
5076
+ * that open them. Kept out of `scanEntries` — the sender does not own them.
5077
+ */
5078
+ sentNotes: SentNoteMatch[];
5079
+ /**
5080
+ * Recipient viewing keys learned while opening payments, as lowercase hex.
5081
+ *
5082
+ * Accumulated across the whole scan and fed forward, so a second payment to
5083
+ * the same person opens from the first one's key.
5084
+ */
5085
+ learnedRecipients: Set<string>;
5086
+ /**
5087
+ * Payments recognised as ours that no batch managed to open.
5088
+ *
5089
+ * Their book entry sits in a change note that landed in a different page.
5090
+ * Kept so the caller can retry them once the whole feed has been read —
5091
+ * dropping one costs a history row and a re-issuable slip for good.
5092
+ */
5093
+ unmatchedSent: UnmatchedSentHint[];
5094
+ /**
5095
+ * Every sealed book entry the scan saw, as decimal `sourcePk` strings.
5096
+ *
5097
+ * A payment and its change can land in different pages, and then neither
5098
+ * half is usable alone. Accumulating both across the whole scan is what lets
5099
+ * the final retry pair them up.
5100
+ */
5101
+ sealedBookEntries: Set<string>;
4722
5102
  /** Highest self-eph index seen — the caller bumps the vault counter past it. */
4723
5103
  maxSelfEphIndex: number | null;
5104
+ /** Highest outgoing index seen — same repair, for the payment sequence. */
5105
+ maxOutgoingEphIndex: number | null;
4724
5106
  }
4725
5107
  interface CollectScanEntriesParams {
4726
5108
  source: ScanHintSource;
@@ -4760,7 +5142,7 @@ interface CollectScanEntriesParams {
4760
5142
  * discoveries into the set; harmless, since the ghost purge filters them.
4761
5143
  */
4762
5144
  vaultHexes?: Set<string> | undefined;
4763
- /** Sink for the two non-fatal warnings this phase can emit. */
5145
+ /** Sink for the one non-fatal warning this phase emits: the chunk fallback. */
4764
5146
  onWarning?: ((message: string, cause?: unknown) => void) | undefined;
4765
5147
  }
4766
5148
  /**
@@ -4804,31 +5186,21 @@ declare function resolveSpentStatus(params: {
4804
5186
  }): Promise<Map<string, SpendDetails>>;
4805
5187
 
4806
5188
  /**
4807
- * Spent-status resolution — decides which of the wallet's own nullifiers are
4808
- * spent WITHOUT ever asking the server about a specific nullifier.
5189
+ * Spent-status resolution — which of the wallet's own nullifiers are spent,
5190
+ * WITHOUT ever asking the server about a specific one.
4809
5191
  *
4810
- * This is the most important privacy property in the scanner. The obvious
4811
- * implementation ask the indexer "is nullifier X spent?" tells the server
4812
- * exactly which notes the caller owns, and it is what a re-implementation
4813
- * reaches for by default. Instead the whole set is downloaded and intersected
4814
- * locally, so every request the server sees is identical for every caller.
5192
+ * The most important privacy property in the scanner, and the one a
5193
+ * re-implementation gets wrong by default: asking "is nullifier X spent?" tells
5194
+ * the server exactly which notes the caller owns. Instead the whole set is
5195
+ * downloaded and intersected locally, so every request looks identical for
5196
+ * every caller.
4815
5197
  *
4816
- * Sealed chunks make that affordable: the manifest is universal, only missing
4817
- * chunks are fetched, and the set persists in the injected cache. Per scan this
4818
- * transfers roughly one chunk plus the tail rather than the entire set.
5198
+ * Sealed chunks make that affordable universal manifest, only missing chunks
5199
+ * fetched, the set persisted in the injected cache so a scan transfers about
5200
+ * one chunk plus the tail rather than the entire set.
4819
5201
  *
4820
5202
  * On any hard failure the caller degrades: notes stay unspent and are
4821
- * re-verified on the next pass.
4822
- *
4823
- * ## Hex casing
4824
- *
4825
- * The intersection is an exact string match, so both sides must agree on
4826
- * casing. The wallet lowercases its own nullifiers before asking; the feed's
4827
- * are lowercased HERE, on the way in. Skipping that makes an uppercase-serving
4828
- * indexer report every spent note as unspent — the wallet then offers notes it
4829
- * cannot spend, and each attempt dies on a duplicate nullifier. Normalising at
4830
- * ingestion means the cache only ever holds one form, so a feed that changes
4831
- * casing between runs cannot split one nullifier into two entries.
5203
+ * re-verified next pass.
4832
5204
  */
4833
5205
 
4834
5206
  interface OpenSpentSetParams {
@@ -4881,26 +5253,19 @@ interface PersistParams {
4881
5253
  }
4882
5254
  /**
4883
5255
  * The notes to purge as ghosts: unspent, absent from the on-chain set, and
4884
- * already in the vault when the scan began.
5256
+ * already in the vault when the scan BEGAN.
4885
5257
  *
4886
- * That last condition is what protects a note the user just created.
4887
- * `onChainHexes` is a snapshot frozen as the scan paged the feed, so a
4888
- * shield/transfer that lands mid-scan is legitimately missing from it — the feed
4889
- * had not served it yet, or its leaf sat behind the paging cursor. Purging on
4890
- * absence alone deleted that note from memory AND storage seconds after the user
4891
- * saw it, and only a later rescan brought it back.
5258
+ * That last condition protects a note the user just created. `onChainHexes` is
5259
+ * frozen as the scan pages the feed, so a shield or transfer landing mid-scan is
5260
+ * legitimately missing from it — purging on absence alone deleted such a note
5261
+ * from memory and storage seconds after the user saw it.
4892
5262
  *
4893
- * A note born mid-scan is simply not this scan's business; the next pass, run
4894
- * against a feed that has caught up, reconciles it.
5263
+ * A note born mid-scan is not this scan's business; the next pass reconciles it.
4895
5264
  */
4896
5265
  declare function selectGhosts(notes: ZkNote[], onChainHexes: Set<string>, preScanHexes?: Set<string>): string[];
4897
5266
  /**
4898
- * Phase 3: builds the batched save-entry list, writes it in one transaction,
4899
- * then purges ghost notes (full scan only). Returns the number of ghosts purged.
4900
- *
4901
- * Both the scanned notes and the memo-failed reconciliation feed a SINGLE
4902
- * saveMany → one storage transaction and one cache write, so subscribers
4903
- * re-render once per call instead of once per note.
5267
+ * Phase 3: build the batched save-entry list, write it in one transaction, then
5268
+ * purge ghost notes (full scan only). Returns how many ghosts were purged.
4904
5269
  */
4905
5270
  declare function persistScanResults(params: PersistParams): Promise<number>;
4906
5271
  /**
@@ -4928,6 +5293,12 @@ interface ReconstructedTxRecord {
4928
5293
  /** Set when the fee was unreadable — the amount overstates by exactly it. */
4929
5294
  amountApproximate?: true;
4930
5295
  timestampMs: number;
5296
+ /**
5297
+ * A freshly sealed `orbslip1:` slip, when this row was recovered from the
5298
+ * memo. RE-ISSUED, not remembered: a slip is sealed toward the recipient,
5299
+ * so the sender never held a readable copy. It carries only public facts.
5300
+ */
5301
+ paymentSlip?: string;
4931
5302
  }
4932
5303
  interface ReconstructDeps {
4933
5304
  vault: Pick<VaultStore, 'getAll' | 'getTxRecords' | 'saveTxRecord'>;
@@ -4935,6 +5306,13 @@ interface ReconstructDeps {
4935
5306
  txFacts: TxFactsSource;
4936
5307
  /** Injectable clock — reconstruction stamps rows whose block time is unknown. */
4937
5308
  now?: () => number;
5309
+ /**
5310
+ * Notes this wallet sent, as the scan recovered them.
5311
+ *
5312
+ * Comes from `ScanOutcome.sentNotes` — the sweep already recognises them by
5313
+ * their outgoing ephPk. Omit to keep the arithmetic-only behaviour.
5314
+ */
5315
+ sentNotes?: SentNoteMatch[];
4938
5316
  }
4939
5317
  declare function reconstructOutgoingTxRecords(deps: ReconstructDeps): Promise<void>;
4940
5318
 
@@ -4981,6 +5359,12 @@ interface NoteBuildKeys {
4981
5359
  viewingPublicKey: Uint8Array;
4982
5360
  /** Needed only for pairwise derivation; omit to disable that path. */
4983
5361
  viewingSecretKey?: Uint8Array | undefined;
5362
+ /**
5363
+ * Outgoing viewing key (ovk). Omit and a payment falls back to a random
5364
+ * ephemeral: still spendable by the recipient, but the sender loses the
5365
+ * ability to read it back later.
5366
+ */
5367
+ outgoingViewingKey?: Uint8Array | undefined;
4984
5368
  }
4985
5369
  interface BuildNoteParams {
4986
5370
  /** Note value in the asset's indivisible units. Must be greater than zero. */
@@ -4997,6 +5381,11 @@ interface BuildNoteParams {
4997
5381
  sourcePk?: bigint | undefined;
4998
5382
  /** Recipient's global ownerPk. With `viewingPublicKey`, enables stealth. */
4999
5383
  recipientOwnerPk?: bigint | undefined;
5384
+ /**
5385
+ * Next outgoing index as reconstructed from chain data, for the first
5386
+ * payment after a restore. Taken as a floor only — see `reserveOutgoingIndex`.
5387
+ */
5388
+ outgoingIndexFromChain?: number | undefined;
5000
5389
  /**
5001
5390
  * Circuit version to stamp. A fresh note carries no proof, so this only
5002
5391
  * matters when it is later spent — pass the version the chain reports as
@@ -5012,6 +5401,21 @@ interface BuildNoteDeps {
5012
5401
  */
5013
5402
  storage?: NoteStorage | undefined;
5014
5403
  }
5404
+ /** A built note, plus the outgoing index it published when it is a payment. */
5405
+ interface BuiltNote {
5406
+ note: ZkNote;
5407
+ /**
5408
+ * Which index of the outgoing sequence this note used, when it is a payment
5409
+ * to a privacy address. Absent for self notes and for a payment that fell
5410
+ * back to a random ephemeral.
5411
+ *
5412
+ * The transfer reads it as a flag: an index present means this payment can
5413
+ * be recovered later, so its change note is worth giving a recipient book
5414
+ * entry. The entry is keyed on the payment's commitment, not on this index.
5415
+ */
5416
+ outgoingIndex?: number;
5417
+ }
5418
+ declare function buildZkNoteWithIndex(params: BuildNoteParams, deps: BuildNoteDeps): Promise<BuiltNote>;
5015
5419
  declare function buildZkNote(params: BuildNoteParams, deps: BuildNoteDeps): Promise<ZkNote>;
5016
5420
 
5017
5421
  interface SelfStealthKeys {
@@ -5032,104 +5436,29 @@ interface SelfStealthKeys {
5032
5436
  */
5033
5437
  declare function recoverSelfStealthNote(note: ZkNote, keys: SelfStealthKeys): ZkNote | null;
5034
5438
 
5035
- /** URI namespace. The version is in the path, so a v2 scanner can refuse a v1. */
5036
- declare const NOTE_TRANSFER_URI_SCHEME = "orbinum://notes/v1/";
5037
- /**
5038
- * Max characters per page. Chosen for QR error-correction level M, which stays
5039
- * scannable from a phone at arm's length.
5040
- */
5041
- declare const QR_PAGE_MAX_CHARS = 1800;
5042
- /**
5043
- * One note on the wire. Field names are abbreviated because every byte costs
5044
- * QR density — and they are FROZEN: renaming one breaks every deployed scanner.
5045
- */
5046
- interface NoteTransferEntry {
5047
- /** commitmentHex */
5048
- c: string;
5049
- /** nullifierHex */
5050
- n: string;
5051
- /** value, decimal string */
5052
- val: string;
5053
- /** assetId, decimal string */
5054
- aid: string;
5055
- /** ownerPk, 0x-prefixed 64-char hex */
5056
- opk: string;
5057
- /** blinding, 0x-prefixed 64-char hex */
5058
- bld: string;
5059
- /** spendingKey, 0x-prefixed 64-char hex */
5060
- sk: string;
5061
- }
5062
- interface NoteTransferPayload {
5063
- v: 1;
5064
- /** Encode time (ms). Informational — a scanner may warn on a stale batch. */
5065
- ts: number;
5066
- /** Page index, 0-based. */
5067
- p: number;
5068
- /** Total pages in this batch. */
5069
- pt: number;
5070
- notes: NoteTransferEntry[];
5071
- }
5072
- declare function noteToTransferEntry(note: ZkNote): NoteTransferEntry;
5073
5439
  /**
5074
- * Encodes notes into scannable page URIs.
5440
+ * CLOSED JSON backup: notes moved between a user's devices as a plain file,
5441
+ * WITHOUT exposing any spending key.
5075
5442
  *
5076
- * `now` is injectable so a caller can produce a deterministic batch (tests, or
5077
- * a reproducible export).
5078
- */
5079
- declare function encodeNoteTransferPages(notes: ZkNote[], options?: {
5080
- now?: () => number;
5081
- maxChars?: number;
5082
- }): string[];
5083
- /**
5084
- * Decodes one scanned page, or throws with a reason a scanner can show.
5085
- *
5086
- * Strict on purpose: a partially-understood payload would import notes with
5087
- * missing fields, and an unspendable note in a vault is worse than a failed
5088
- * scan the user can retry.
5089
- */
5090
- declare function decodeNoteTransferPage(uri: string): NoteTransferPayload;
5091
- /**
5092
- * Reassembles a scanned batch into notes.
5093
- *
5094
- * Rejects an incomplete or mixed batch rather than importing what it has: a
5095
- * partial import looks like a successful one, and the user would never learn
5096
- * which notes never made it.
5097
- */
5098
- declare function assembleNoteTransfer(pages: NoteTransferPayload[]): NoteTransferEntry[];
5099
-
5100
- /**
5101
- * CLOSED JSON backup of notes — the same notes moved between a user's devices as
5102
- * a plain file, WITHOUT exposing any spending key.
5103
- *
5104
- * ## The idea
5105
- *
5106
- * A backup entry carries only what is already PUBLIC on chain: the commitment and
5107
- * the encrypted memo. It contains no spending key, so the file is safe to hold
5108
- * and even share — it grants nothing on its own.
5443
+ * An entry carries only what is already PUBLIC on chain the commitment and
5444
+ * the encrypted memo — so the file grants nothing on its own.
5109
5445
  *
5110
- * On import, the recipient DECRYPTS each memo with their own viewing key
5111
- * (`importNotesFromBackup`). Decryption is the proof of ownership:
5112
- * - the memo decrypts the note is theirs a full spendable `ZkNote` is
5113
- * reconstructed, spending key and stealth key derived from their identity;
5114
- * - the memo does not decrypt → the note is someone else's → it is dropped.
5446
+ * DECRYPTION IS THE PROOF OF OWNERSHIP. On import each memo is opened with the
5447
+ * importer's viewing key: one that opens rebuilds a full spendable `ZkNote`
5448
+ * (spending and stealth keys derived from their identity); one that does not is
5449
+ * someone else's note and is dropped.
5115
5450
  *
5116
- * This is NOT a chain scan: only the N memos in the backup are tried, not the
5117
- * whole pool. It moves notes between devices without re-scanning, and a note that
5118
- * is not yours simply fails to import.
5451
+ * Not a chain scan only the N memos in the file are tried, which is what lets
5452
+ * a device restore without re-scanning the pool.
5119
5453
  *
5120
- * ## What travels vs what does not
5121
- *
5122
- * Travels: `commitmentHex`, `encryptedMemo`, and `leafIndex` (informational).
5123
- * Does NOT travel: value, ownerPk, blinding, spendingKey — all recovered by
5124
- * decrypting the memo. The Merkle proof is re-fetched at spend time.
5454
+ * Travels: `commitmentHex`, `encryptedMemo`, `leafIndex` (informational).
5455
+ * Recovered from the memo: value, ownerPk, blinding, spendingKey. The Merkle
5456
+ * proof is re-fetched at spend time.
5125
5457
  */
5126
5458
 
5127
5459
  /** Current backup format version. Bumped only if the entry shape changes. */
5128
5460
  declare const NOTE_BACKUP_VERSION: 1;
5129
- /**
5130
- * One note in a closed backup — public data only. Field names are short but not
5131
- * cryptic; readability matters more than density in a file (unlike the QR path).
5132
- */
5461
+ /** One note in a closed backup — public data only. */
5133
5462
  interface NoteBackupEntry {
5134
5463
  /** 0x-prefixed 32-byte LE commitment hex. */
5135
5464
  commitmentHex: string;
@@ -5189,15 +5518,12 @@ declare function decodeNoteBackup(json: string | object): NoteBackupEntry[];
5189
5518
  declare function importNotesFromBackup(entries: NoteBackupEntry[], keys: BackupImportKeys): ZkNote[];
5190
5519
 
5191
5520
  /**
5192
- * Reconstruct a note from a payment slip.
5521
+ * Reconstruct a note from a payment slip, without scanning the pool.
5193
5522
  *
5194
- * The recipient of a private transfer receives an `orbslip1:` string (or the raw
5195
- * envelope) that the sender produced. Opening it yields the note's public
5196
- * locators commitment, encrypted memo, leaf index which are fed to the SAME
5197
- * decryption path a scan uses (`tryDecryptNote`): it decrypts the memo with the
5198
- * recipient's viewing key, derives the stealth spending key, and verifies the
5199
- * commitment. The result is a fully spendable `ZkNote`, obtained without scanning
5200
- * the pool.
5523
+ * An `orbslip1:` string carries the note's public locators — commitment,
5524
+ * encrypted memo, leaf index. Those feed the SAME path a scan uses
5525
+ * (`tryDecryptNote`), which opens the memo, derives the stealth spending key
5526
+ * and verifies the commitment. Out comes a fully spendable `ZkNote`.
5201
5527
  */
5202
5528
 
5203
5529
  /** Keys the recipient needs to open a slip and reconstruct the note. */
@@ -5306,16 +5632,28 @@ interface TransferSubmitRequest {
5306
5632
  interface TransferDeps {
5307
5633
  privacy: SpendPrivacyReads;
5308
5634
  resolver: Pick<CircuitVersionResolver, 'resolve'>;
5309
- /** Builds the two output notes. Owns keys and the ephemeral reservations. */
5635
+ /**
5636
+ * Builds the two output notes. Owns keys and the ephemeral reservations.
5637
+ *
5638
+ * Returns the outgoing index alongside the note, used only as a signal that
5639
+ * this payment published a DERIVED ephemeral and is therefore recoverable —
5640
+ * which is what decides whether the change note carries a book entry at all.
5641
+ * The entry itself is keyed on the payment's commitment, never on this
5642
+ * index; see `recipientBook`.
5643
+ */
5310
5644
  buildNote: (params: {
5311
5645
  value: bigint;
5312
5646
  assetId: bigint;
5313
- ownerPk: bigint;
5647
+ /** Omit for a note owned by the wallet itself — the builder fills its own. */
5648
+ ownerPk?: bigint;
5314
5649
  spendingKey?: bigint;
5315
5650
  sourcePk: bigint;
5316
5651
  viewingPublicKey?: Uint8Array;
5317
5652
  recipientOwnerPk?: bigint;
5318
- }) => Promise<ZkNote>;
5653
+ }) => Promise<{
5654
+ note: ZkNote;
5655
+ outgoingIndex?: number;
5656
+ }>;
5319
5657
  vault: SpendVault;
5320
5658
  /** Spendable form of a self-addressed stealth note, or null (see selfStealthNote). */
5321
5659
  recoverStealth: (note: ZkNote) => ZkNote | null;
@@ -5327,6 +5665,14 @@ interface TransferDeps {
5327
5665
  * immediately instead of waiting for a rescan.
5328
5666
  */
5329
5667
  selfOwnerPk: bigint | null;
5668
+ /**
5669
+ * The wallet's outgoing viewing key (ovk), when it has one.
5670
+ *
5671
+ * Seals the recipient book into the change note. Absent — a watch-only or
5672
+ * legacy identity — the change keeps the older meaning and the transfer is
5673
+ * simply not recoverable from the seed later.
5674
+ */
5675
+ outgoingViewingKey?: Uint8Array;
5330
5676
  /** Route stamped on persisted notes (explorer link kind). */
5331
5677
  txKind?: TxKind;
5332
5678
  }
@@ -5336,6 +5682,19 @@ interface TransferParams {
5336
5682
  recipientPk: bigint;
5337
5683
  /** Packed viewing key from the recipient's privacy address. Omitted → dummy memo, they must scan. */
5338
5684
  recipientViewingPublicKey?: Uint8Array | undefined;
5685
+ /**
5686
+ * What the recipient sees as `sourcePk` — who paid them.
5687
+ *
5688
+ * PASS IT EXPLICITLY. The default is the spent note's owner, so what gets
5689
+ * disclosed depends on coin selection: spending a received note discloses a
5690
+ * one-time key that names nobody, spending a shield or change note
5691
+ * discloses the wallet's GLOBAL identity, linkable across every payment
5692
+ * made from one. Naming the counterparty is the field's purpose, so neither
5693
+ * is a leak — but the choice should not be made by the coin selector.
5694
+ *
5695
+ * A stable pseudonym for a merchant tracking repeat payments, a
5696
+ * per-recipient value to stay unlinkable, or `0n` to say nothing.
5697
+ */
5339
5698
  senderPk?: bigint | undefined;
5340
5699
  fee?: bigint | undefined;
5341
5700
  }
@@ -5367,7 +5726,18 @@ interface UnshieldSubmitRequest {
5367
5726
  interface UnshieldDeps {
5368
5727
  privacy: SpendPrivacyReads;
5369
5728
  resolver: Pick<CircuitVersionResolver, 'resolve'>;
5370
- /** Builds the stealth change note. Owns keys and the ephemeral reservation. */
5729
+ /**
5730
+ * Builds the stealth change note. Owns the keys it is built under.
5731
+ *
5732
+ * No ephemeral is reserved: passing `recipientOwnerPk` without a
5733
+ * `viewingPublicKey` takes neither the self nor the outgoing branch in
5734
+ * `buildZkNoteWithIndex`, so the note publishes a RANDOM ephemeral and a rescan
5735
+ * finds it by trial decryption rather than by index lookup.
5736
+ *
5737
+ * Shares its shape with the transfer's builder so one bound function serves
5738
+ * both; the outgoing index it may return is unused here, since an unshield
5739
+ * has no recipient to record.
5740
+ */
5371
5741
  buildNote: (params: {
5372
5742
  value: bigint;
5373
5743
  assetId: bigint;
@@ -5375,7 +5745,9 @@ interface UnshieldDeps {
5375
5745
  blinding: bigint;
5376
5746
  spendingKey: bigint;
5377
5747
  recipientOwnerPk: bigint;
5378
- }) => Promise<ZkNote>;
5748
+ }) => Promise<{
5749
+ note: ZkNote;
5750
+ }>;
5379
5751
  vault: SpendVault;
5380
5752
  /**
5381
5753
  * The spendable form of a stealth note this wallet authored, or null. Null
@@ -5441,12 +5813,12 @@ declare function claimFees(deps: FeeClaimDeps, { assetId, amount, signer }: FeeC
5441
5813
  /**
5442
5814
  * Whether a note still matches the commitment stored on chain.
5443
5815
  *
5444
- * The circuit ignores the ownerPk it is passed and rebuilds each input
5445
- * commitment from BabyPbk(spending_key).Ax. If a note's stored spendingKey,
5446
- * value, assetId or blinding drift from what was committed, the recomputed root
5447
- * differs and witness generation dies on the Merkle constraint. Checking here
5448
- * costs one EC multiplication plus a Poseidon hash — around 0.13 ms — against
5449
- * seconds of proving that would fail anyway, and it names the real problem.
5816
+ * The circuit ignores the `ownerPk` it is handed and rebuilds each input
5817
+ * commitment from `BabyPbk(spending_key).Ax`, so a drifted spendingKey, value,
5818
+ * assetId or blinding kills witness generation on the Merkle constraint.
5819
+ *
5820
+ * ~0.13 ms here — one EC mul plus a Poseidon hash — against seconds of proving
5821
+ * that fails anyway, and this one names the note.
5450
5822
  */
5451
5823
  declare function noteMatchesCommitment(note: ZkNote): boolean;
5452
5824
  /**
@@ -5547,12 +5919,11 @@ declare function chainActiveCircuitVersion(zkVerifier: Pick<ZkVerifierModule, 'g
5547
5919
  * A WebSocket drop between submit and finalization surfaces as an error for a
5548
5920
  * tx that may have landed anyway. Reporting that as "failed" is the dangerous
5549
5921
  * outcome: the user retries, and the retry double-spends (shield) or dies on a
5550
- * duplicate nullifier (transfer/unshield). Every wallet has this bug until it
5551
- * explicitly does not.
5922
+ * duplicate nullifier (transfer/unshield).
5552
5923
  *
5553
- * The fix is to distinguish "the chain rejected it" from "we lost the line":
5554
- * only the second is worth polling, and the poll asks an on-chain predicate the
5555
- * caller supplies — a nullifier turned spent, a commitment now in the tree.
5924
+ * So "the chain rejected it" and "we lost the line" are told apart, and only
5925
+ * the second is polled against an on-chain predicate the caller supplies: a
5926
+ * nullifier turned spent, a commitment now in the tree.
5556
5927
  */
5557
5928
 
5558
5929
  /** Whether an error leaves the submit outcome unknown rather than rejected. */
@@ -5582,20 +5953,26 @@ declare function txLandedAfterError(err: unknown, landed: () => Promise<boolean>
5582
5953
  declare const RECOVERED_TX_RESULT: TxResult;
5583
5954
 
5584
5955
  /**
5585
- * Storage key for one cached identity.
5956
+ * Storage name for one account's vault on one chain.
5586
5957
  *
5587
- * Scoped by chainId because the chain is PART of the identity: the spending key
5588
- * is derived with `info = "orbinum-sk-v2:{chainId}:{account}"`, so one wallet
5589
- * yields a different key per network. A key shared across networks would restore
5590
- * one network's identity into another and show an empty vault with nothing to
5591
- * explain it.
5958
+ * `chainFingerprint` is the genesis hash, optional because a host may not know
5959
+ * it during connect: the name then scopes by account alone and is promoted once
5960
+ * it arrives. Opening under the short name and later the long one is safe —
5961
+ * `unlock` detects the change and resets rather than mixing two chains' notes.
5962
+ */
5963
+ declare function vaultStorageName(address: string, chainFingerprint?: string, version?: IdentityVersion): string;
5964
+
5965
+ /**
5966
+ * Storage key for one cached identity — same three parts as the vault name, for
5967
+ * the same reason (see `vaultName`).
5592
5968
  *
5593
- * The account half goes through `canonicalAccountId` for the same reason: it has
5594
- * to match what the derivation keys by, or one identity ends up filed under two
5595
- * names. An SS58 address re-listed under another network prefix is the common
5596
- * way that happens same key, different vault, orphaned notes.
5969
+ * The chain is PART of the identity: the derivation's `info` carries the chain
5970
+ * id, so one wallet yields a different key per network. The account goes through
5971
+ * `canonicalAccountId` so this matches what the derivation keys by an address
5972
+ * re-listed under another SS58 prefix would otherwise file one identity under
5973
+ * two names.
5597
5974
  */
5598
- declare function sessionCacheKey(address: string, chainId: number): string;
5975
+ declare function sessionCacheKey(address: string, chainId: number, version?: IdentityVersion): string;
5599
5976
  interface SessionCacheDeps {
5600
5977
  store: SecretStore;
5601
5978
  /**
@@ -5635,61 +6012,87 @@ declare function restoreSession(deps: SessionCacheDeps, address: string, chainId
5635
6012
  */
5636
6013
  declare function clearSession(store: SecretStore, address: string, chainId?: number): Promise<void>;
5637
6014
 
6015
+ interface WalletIdentity {
6016
+ version: IdentityVersion;
6017
+ spendingKey: bigint;
6018
+ ownerPk: bigint;
6019
+ viewingSecretKey: Uint8Array;
6020
+ viewingPublicKey: Uint8Array;
6021
+ /** Absent on a watch-only identity, which is handed everything except this. */
6022
+ outgoingViewingKey?: Uint8Array;
6023
+ }
5638
6024
  /**
5639
- * Storage name for one account's vault on one chain.
6025
+ * Assemble the identity. v3 is the only scheme.
6026
+ *
6027
+ * Throws on a root that is not 32 bytes or is all zeros. Neither is an error the
6028
+ * derivation itself would raise: the branches are pure HKDF, so they answer for
6029
+ * ANY input — an empty one included — with a complete, deterministic and
6030
+ * publicly derivable identity. This is the only place that catches a caller who
6031
+ * wired the root from something that was not there.
5640
6032
  *
5641
- * `chainFingerprint` is the chain's genesis hash. It is optional because a host
5642
- * may not know it yet during connect; the name then scopes by account alone and
5643
- * is promoted once the fingerprint arrives. Opening a vault under the short name
5644
- * and later under the long one is safe — `VaultStore.unlock` detects the
5645
- * fingerprint change and resets rather than mixing two chains' notes.
6033
+ * Nothing migrates from an older scheme: a note is committed on chain under the
6034
+ * `ownerPk` inside the Merkle tree, so re-deriving an identity does not move it,
6035
+ * it orphans it. Holders unshield first.
6036
+ *
6037
+ * @param rootSecret 32 bytes the master key material. Not retained.
5646
6038
  */
5647
- declare function vaultStorageName(address: string, chainFingerprint?: string): string;
5648
-
6039
+ declare function deriveIdentity(rootSecret: Uint8Array, version: IdentityVersion): WalletIdentity;
5649
6040
  /**
5650
- * Whether browser wallet extensions can be discovered at all.
6041
+ * What a watch-only holder is given: read access, no spend authority.
5651
6042
  *
5652
- * False in React Native, Node, a Cloudflare Worker and an extension service
5653
- * worker. A host that gets false must sign some other way — there is no
5654
- * fallback to arrange here, only a fact to report.
6043
+ * The spending key is absent by TYPE, not by a runtime check a credential
6044
+ * cannot be passed where a spend needs one.
6045
+ *
6046
+ * `includeOutgoing` is opt-in and stays that way: the incoming key reveals what
6047
+ * arrived, the outgoing one reveals who was PAID. Bundling them by default would
6048
+ * hand the payment graph to anyone granted a look at the balance.
6049
+ */
6050
+ interface ViewingCredential {
6051
+ version: IdentityVersion;
6052
+ ownerPk: bigint;
6053
+ viewingSecretKey: Uint8Array;
6054
+ viewingPublicKey: Uint8Array;
6055
+ outgoingViewingKey?: Uint8Array;
6056
+ }
6057
+ declare function exportViewingCredential(identity: WalletIdentity, options?: {
6058
+ includeOutgoing?: boolean;
6059
+ }): ViewingCredential;
6060
+
6061
+ /**
6062
+ * Whether browser wallet extensions can be discovered at all. A host that gets
6063
+ * false must sign some other way — there is no fallback to arrange here.
5655
6064
  */
5656
6065
  declare function hasInjectedExtensions(): boolean;
5657
6066
  /**
5658
6067
  * Installed extension ids, or an empty list where none can exist.
5659
6068
  *
5660
- * Returns empty rather than throwing: "which extensions are present" has an
5661
- * honest answer off-page, and it is "none". A caller enumerating wallets to
5662
- * render a picker wants a list, not an exception.
6069
+ * Empty rather than a throw: off-page, "which extensions are present" has an
6070
+ * honest answer, and it is "none". A caller rendering a picker wants a list.
5663
6071
  */
5664
6072
  declare function getInjectedExtensions(): string[];
5665
6073
  /**
5666
- * Connects to one extension by id.
5667
- *
5668
- * Throws off-page, unlike the enumerator above: the caller asked for a specific
5669
- * extension, so an empty result would misreport "not installed" for what is
5670
- * really "this host cannot reach extensions at all".
6074
+ * Connects to one extension by id. Throws off-page, unlike the enumerator: the
6075
+ * caller named an extension, so silence would misreport "not installed" for
6076
+ * "this host cannot reach extensions at all".
5671
6077
  */
5672
6078
  declare function connectInjectedExtension(name: string, origin?: string): Promise<InjectedExtension>;
5673
6079
 
5674
6080
  /**
5675
- * NoteProvenance — the single vocabulary for "where did this note come from,
5676
- * and where did it go".
6081
+ * NoteProvenance — one vocabulary for "where did this note come from, and where
6082
+ * did it go".
5677
6083
  *
5678
- * Several mechanisms answer that question, and before this module they had
5679
- * separate vocabularies and no contract between them:
6084
+ * Two mechanisms answer that, and they are PROVIDERS of the same fact rather
6085
+ * than competing designs; `ProvenanceSource` records which one spoke:
5680
6086
  *
5681
- * - the memo's `sourcePk` field, readable by whoever can decrypt the note
5682
- * (the recipient always; the sender only for the change note they kept);
5683
- * - a lookup by commitment, which returns only what is already public no
5684
- * amount, no recipient, but enough to re-issue a payment slip.
6087
+ * - the memo's `sourcePk`, readable by whoever can decrypt the note — the
6088
+ * recipient always, the sender only for the change note they kept;
6089
+ * - a lookup by commitment, returning only what is already public: no amount,
6090
+ * no recipient, but enough to re-issue a payment slip.
5685
6091
  *
5686
- * They are not competing designs. They are providers of the same fact, and
5687
- * `ProvenanceSource` records which one spoke. Nothing here derives a key: this
5688
- * layer only holds data already recovered.
5689
- *
5690
- * A sender cannot reopen a memo sealed toward someone else, so the amount and
5691
- * recipient of an outgoing transfer are NOT recoverable from a seed alone. What
5692
- * survives is the ability to hand the recipient a working slip again.
6092
+ * Nothing here derives a key this layer only holds what was already
6093
+ * recovered. And a sender cannot reopen a memo sealed toward someone else, so
6094
+ * the amount and recipient of an outgoing transfer are NOT recoverable from a
6095
+ * seed. What survives is the ability to hand over a working slip again.
5693
6096
  */
5694
6097
 
5695
6098
  /**
@@ -5819,16 +6222,12 @@ declare function selectDescribingNoteByCommitment<T extends Pick<ZkNote, 'source
5819
6222
  /**
5820
6223
  * Merging what a rescan learned into what the wallet already knew.
5821
6224
  *
5822
- * The hazard this exists to remove: reconstruction runs after every scan, over
5823
- * records the wallet may have written itself at submit time. Those local
5824
- * records hold what a recovery path cannot: the amount and the recipient, which
5825
- * live inside a memo sealed toward someone else. Overwriting one loses them.
5826
- *
5827
- * The payment slip itself is not at risk — it can be re-issued from public
5828
- * fields — but a thin `chain` record must never replace a rich local one.
6225
+ * Reconstruction runs after every scan, over records the wallet may have
6226
+ * written ITSELF at submit time and those hold what no recovery path can
6227
+ * reach: the amount and the recipient, which live inside a memo sealed toward
6228
+ * someone else. A thin `chain` record must never replace a rich local one.
5829
6229
  *
5830
- * Before this, the protection was a single spread expression in the
5831
- * reconstruction loop. The rule is now explicit and testable on its own.
6230
+ * The slip is the exception: it can always be re-issued from public fields.
5832
6231
  */
5833
6232
 
5834
6233
  declare function outranks(a: ProvenanceSource, b: ProvenanceSource): boolean;
@@ -5861,25 +6260,20 @@ declare function mergeProvenance(existing: NoteProvenanceRecord, incoming: NoteP
5861
6260
  /**
5862
6261
  * Seal a fresh `orbslip1:` slip for a transfer recovered from history.
5863
6262
  *
5864
- * ## Why the facts are checked here
5865
- *
5866
- * They were looked up by commitment, so they crossed a trust boundary, and
5867
- * sealing them produces an AUTHENTICATED envelope. A valid MAC proves the
5868
- * sender knew the recipient's viewing key not that they are honest, and not
5869
- * that whatever server answered the lookup was. The recipient's wallet then
5870
- * renders those fields with the authority of a decrypted slip, on a device
5871
- * where nothing explains which server supplied them.
5872
- *
5873
- * So the check belongs on THIS side of the wire: a value that slipped through
5874
- * would fail on the recipient's device, or worse, not fail at all.
6263
+ * VALIDATES ON THIS SIDE OF THE WIRE. The facts crossed a trust boundary (they
6264
+ * were looked up by commitment) and sealing them produces an AUTHENTICATED
6265
+ * envelope: a valid MAC proves the sender knew the recipient's viewing key, not
6266
+ * that the server which answered was honest. The recipient's wallet then renders
6267
+ * those fields with the authority of a decrypted slip, on a device that cannot
6268
+ * tell where they came from.
5875
6269
  *
5876
- * Throws on a malformed commitment, memo or leaf index — those are the note's
5877
- * identity, and a slip carrying a wrong one is not a degraded slip but a broken
5878
- * one. `txHash` is informational, so it is DROPPED rather than fatal.
6270
+ * Throws on a malformed commitment, memo or leaf index — they are the note's
6271
+ * identity, and a slip carrying a wrong one is broken, not degraded. `txHash` is
6272
+ * informational, so it is DROPPED instead.
5879
6273
  *
5880
6274
  * @param facts public facts of the sent note, looked up by commitment
5881
6275
  * @param recipientIvkPacked 32-byte packed viewing key from the recipient's privacy address
5882
- * @param txHash the transfer's hash, when known — informational, shown as proof of payment
6276
+ * @param txHash the transfer's hash, when known — informational
5883
6277
  */
5884
6278
  declare function regeneratePaymentSlip(facts: NoteFacts, recipientIvkPacked: Uint8Array, txHash?: string): string;
5885
6279
 
@@ -5931,6 +6325,16 @@ interface OrbinumWalletConfig {
5931
6325
  * that already resolved it — a wrong value yields unspendable notes.
5932
6326
  */
5933
6327
  circuitVersion?: number | undefined;
6328
+ /**
6329
+ * Which derivation scheme this wallet's keys come from.
6330
+ *
6331
+ * Only `v3` exists, so this is here for the NEXT scheme rather than for a
6332
+ * choice available today — passing it changes nothing. v2 was removed, not
6333
+ * deprecated: it chained the viewing key off the spending key, which made a
6334
+ * watch-only wallet impossible and left no branch to hang the outgoing
6335
+ * viewing key on.
6336
+ */
6337
+ identityVersion?: IdentityVersion | undefined;
5934
6338
  }
5935
6339
  interface ScanOptions {
5936
6340
  /**
@@ -5947,8 +6351,16 @@ declare class OrbinumWallet {
5947
6351
  private readonly session;
5948
6352
  private readonly notes;
5949
6353
  private readonly vaultStore;
5950
- /** Set at unlock; every key below is derived from it. */
5951
- private spendingKey;
6354
+ /**
6355
+ * The identity this wallet is unlocked as, or null.
6356
+ *
6357
+ * Held as a bundle rather than a single scalar because v3 keys are SIBLINGS:
6358
+ * the spending key no longer produces the viewing key, so there is nothing
6359
+ * left to derive the others from once unlock returns. Keeping the root out
6360
+ * of this object is deliberate — the branches are what the wallet needs, and
6361
+ * the root is the thing worth not retaining.
6362
+ */
6363
+ private identity;
5952
6364
  constructor(config: OrbinumWalletConfig);
5953
6365
  /** The underlying store, for operations the facade does not cover. */
5954
6366
  get vault(): VaultStore;
@@ -5958,11 +6370,14 @@ declare class OrbinumWallet {
5958
6370
  * signature produces — the same value `PrivacyKeyManager.getMasterBytes()`
5959
6371
  * returns.
5960
6372
  *
5961
- * Master bytes rather than the spending-key scalar, deliberately: the scalar
5962
- * is the master reduced modulo the curve order, so deriving the vault key
5963
- * from it would tie the encrypted vault to the CURRENT modulus. The master
5964
- * is pre-reduction, which keeps a stored vault readable across a modulus
5965
- * change. Everything else viewing keys, ownerPk — comes from the scalar.
6373
+ * Master bytes rather than the spending-key scalar, deliberately: the master
6374
+ * is the root EVERY branch hangs off spending key, viewing key, outgoing
6375
+ * viewing key and vault key are each their own HKDF of it, none derived
6376
+ * from another. Only `ownerPk` comes from the spending scalar, because the
6377
+ * circuit defines it that way.
6378
+ *
6379
+ * It is also what keeps a stored vault readable across a modulus change:
6380
+ * the vault key is derived pre-reduction.
5966
6381
  *
5967
6382
  * Reports whether the stored vault was RESET: a different chain, a different
5968
6383
  * key, or notes the chain no longer recognises. When it was, the wallet
@@ -6027,6 +6442,7 @@ declare class OrbinumWallet {
6027
6442
  spendingKey: bigint;
6028
6443
  viewingSecretKey: Uint8Array;
6029
6444
  ownerPk: bigint;
6445
+ outgoingViewingKey?: Uint8Array;
6030
6446
  };
6031
6447
  /**
6032
6448
  * `recoverStealth` for the spend ops, bound to this wallet's keys.
@@ -6043,10 +6459,13 @@ declare class OrbinumWallet {
6043
6459
  * guessed version produces a note the chain refuses to spend after a
6044
6460
  * verifying-key rotation, and the failure surfaces much later.
6045
6461
  */
6046
- buildOutputNote: (params: Omit<BuildNoteParams, "circuitVersion">) => Promise<ZkNote>;
6462
+ buildOutputNote: (params: Omit<BuildNoteParams, "circuitVersion">) => Promise<{
6463
+ note: ZkNote;
6464
+ outgoingIndex?: number;
6465
+ }>;
6047
6466
  /** The circuit version a new note should carry. */
6048
6467
  circuitVersion(): Promise<number>;
6049
- private requireKey;
6468
+ private requireIdentity;
6050
6469
  }
6051
6470
 
6052
- export { type AssetRegisteredEvent, type AssetUnverifiedEvent, type AssetVerifiedEvent, BABYJUB_SUBORDER, BN254_R, type BackupImportKeys, type BlockInfo, type BuildNoteDeps, type BuildNoteParams, type Bytes32, CachedNullifier, type ChainInfo, ChainModule, type ChunkInfo, CircuitId, CircuitVersionResolver, type ClaimShieldedFeesParams, type ClientProviderConfig, type CoinSelection, type CollectScanEntriesParams, type CommitmentsInsertedEvent, type ConnectionStatus, type CryptoKey$1 as CryptoKey, CryptoPrecompiles, type DecodedPrecompile, DecryptPool, DecryptedMemo, type DynamicBuilder, ENCRYPTED_MEMO_SIZE, EncryptedMemo, EncryptedNoteRecord, EncryptedTxRecord, type EventData, type EventPhase, type EventRecord, type EvmAddressInfo, type EvmBlock, EvmClient, EvmExplorer, type EvmLog, type EvmSigner, type EvmTransaction, type EvmTxRequest, type EvmTxSummary, type ExtrinsicDecoder, type ExtrinsicFacts, type ExtrinsicRecord, type FeeClaimDeps, type FeeClaimParams, type FeeClaimProofInputs, type FeeClaimProofOutput, type FeeClaimStep, type FormatOptions, KNOWN_PALLET_ERRORS, KNOWN_PRECOMPILES, type KnownPrecompileInfo, LEAVES_PER_TREE, MIN_GASLESS_FEE, MIN_SIGNATURE_BYTES, MemoryVaultStorage, type MerkleRootUpdatedEvent, type MutableWalletSession, NATIVE_ASSET_ID, NOTE_BACKUP_VERSION, NOTE_BIGINT_FIELDS, NOTE_TRANSFER_URI_SCHEME, type NoteBackup, type NoteBackupEntry, type NoteBuildKeys, NoteBuilder, type NoteDisclosure, NoteFacts, NoteInput, type NoteOrigin, type NoteProvenanceRecord, NoteStatusUpdate, NoteStorage, type NoteTransferEntry, type NoteTransferPayload, type NoteWithMeta, type NotesCache, NullifierCache, type NullifierChunkBody, type NullifierManifest, type NullifierSource, NullifierSyncMeta, type NullifierTail, type NullifiersSpentEvent, OVK_BLOB_SIZE, type ObservableNotesCache, OrbinumClient, type OrbinumClientConfig, OrbinumClientProvider, OrbinumWallet, type OrbinumWalletConfig, type OutgoingHint, OutgoingNoteRecord, PAGE_SIZE, PAYMENT_SLIP_SCHEME, PRECOMPILE_ADDR, type PairwiseEphWindowEntry, type PalletErrorKind, type PaymentSlipFields, type PersistParams, type PkScope, type PrecompileMethod, PrivacyKeyManager, type PrivacyMerkleProof, PrivacyModule, type PrivateTransferArgs, type PrivateTransferInput, type PrivateTransferOutput, type PrivateTransferParams, type PrivateTransferProofInputs, type ProofOptions, type ProvenanceAmount, type ProvenancePeer, type ProvenanceSource, type ProviderFactory, QR_PAGE_MAX_CHARS, RECOVERED_TX_RESULT, type RawBlock, type RawBlockHeader, type RawTransferInput, type RawTransferOutput, type ReconstructDeps, type ReconstructedTxRecord, type RegisterAssetArgs, type RelayerInfo, RelayerStatusModule, type ResolveSpentSetParams, type ResolvedProverVersion, type ResolvedSpendVersion, type RpcV2MerkleProof, RpcV2Module, type RpcV2NullifierStatus, type RpcV2PoolAssetBalance, type RpcV2PoolStats, type RunScanParams, SPENDING_KEY_CANONICAL_ORIGIN, SPENDING_KEY_VERIFYING_CONTRACT, SPENDING_KEY_WARNING, type ScanChunkManifest, ScanCommitment, type ScanHint, type ScanHintPage, type ScanHintSource, ScanKeys, type ScanOptions, type ScanOutcome, type ScanProgress, type ScanResult, SecretStore, type SelfEphWindowEntry, type SelfStealthKeys, type SessionCacheDeps, type ShieldArgs, type ShieldBatchArgs, type ShieldBatchItem, type ShieldBatchParams, type ShieldOperation, type ShieldParams, type ShieldedEvent, type ShieldedPoolCall, type ShieldedPoolEvent, ShieldedPoolModule, ShieldedPoolPrecompile, type SlipImportKeys, SpendDetails, type SpendPlanProblem, type SpendPrivacyReads, type SpendVault, type SpendableInputsCheck, type SpendingKeyTypedData, type StatusChangeEvent, type StatusListener, SubstrateClient, type SystemHealth, TRANSFER_INPUTS, TRANSFER_OUTPUTS, type TokenInfo, type TokenTransfer, type TransferDeps, type TransferFactsRow, type TransferFactsSource, type TransferInputNote, type TransferOutputNote, type TransferParams, type TransferPlan, type TransferResult, type TransferStep, type TransferSubmitRequest, type TxFactsSource, TxHistoryStore, type TxKind, type TxLandingPollOptions, type TxResult, type UnsafeTxOptions, type UnshieldArgs, type UnshieldDeps, type UnshieldNoteParams, type UnshieldParams, type UnshieldPlan, type UnshieldProofInputs, type UnshieldProofResult, type UnshieldStep, type UnshieldSubmitRequest, type UnshieldedEvent, type UnverifyAssetArgs, VAULT_SCHEMA_VERSION, VaultConfigRecord, VaultLockedError, VaultStorage, VaultStore, type VaultStoreDeps, type VaultUnlockOptions, type VerifyAssetArgs, type VersionedArtifactProvider, type WalletScanKeys, type WalletSession, ZkNote, type ZkVerifierCircuitVersionInfo, type ZkVerifierHistoricalVersion, ZkVerifierModule, type ZkVerifierVersionStats, type ZkVerifierVkHash, accountIdHexToSs58, addressToAccountIdHex, addressToFieldElement, applyBatch, applyNoteStatus, assembleNoteTransfer, base64UrlDecode, base64UrlEncode, bigintTo32Be, bigintTo32Le, bigintTo32LeArr, blindTag, buildConfig, buildDummyTransferInput, buildShieldBatchOperations, buildShieldParams, buildZkNote, bytesToBigintLE, bytesToBjjScalar, cacheSession, canPairWith, canonicalAccountId, chainActiveCircuitVersion, checkSpendableInputs, claimFees, classifyChainError, clearSession, collectNullifiersToQuery, collectOutgoingFacts, collectScanEntries, commitmentHexOf, computeNoteCommitment, computeNullifier, computePathIndices, connectInjectedExtension, createNoteDisclosureKey, createNotesCache, createWalletSession, decodeNoteBackup, decodeNoteDisclosureKey, decodeNoteTransferPage, decodePaymentSlip, decodePrecompileCalldata, decryptJson, decryptNoteRecord, deriveMasterKeyBytes, deriveOutgoingCipherKey, deriveOutgoingViewingKey, deriveOwnerPk, derivePairwiseEphSk, derivePairwiseSharedSecret, deriveSelfEphSk, deriveSpendingKeyFromMaster, deriveSpendingKeyFromSignature, deriveSpendingKeyMessageV2, deriveSpendingKeyTypedData, deriveStealthOwnerPk, deriveStealthSk, deriveVaultBlindKey, deriveVaultKey, deriveViewTag, deriveViewingPublicKey, deriveViewingSecretKey, detectCommitmentMismatch, encodeNoteBackup, encodeNoteTransferPages, encodePaymentSlip, encryptJson, encryptNote, ensureCreatedAt, ensureHexPrefix, evmAddressToAccountId, evmToImplicitSubstrate, evmToMappedAccountHex, evmToSubstrate, extractPalletError, failed, fastMulBase, fastMulPoint, fetchExtrinsicFacts, formatAmountPlain, formatBalance, formatORB, fromBase64, fromHex, gapMargin, generateFeeClaimProof, generateTransferProof, generateUnshieldProof, getInjectedExtensions, getPrecompileLabel, hasCachedSession, hasInjectedExtensions, hasSourcePk, hexToBigint, hexToNumber, implicitSubstrateToEvm, importNotesFromBackup, importPaymentSlip, isAbortError, isAlreadySpentError, isConnectionLossError, isEvmAddress, isGhostNoteError, isHexOfLength, isImplicitEvmAccount, isNativeAsset, isNoteSelfConsistent, isSpendable, isSs58, isSubstrateAddress, isUnifiedAddress, isValidLeafIndex, leHexToBigint, mapExtrinsicArgs, mapZkEventData, markInputsSpent, mergeProvenance, normalizeChainFingerprint, normalizeEvmAddress, normalizeNote, normalizeNotes, noteBlindTag, noteCreatedAt, noteCreatedTxHash, noteMatchesCommitment, noteOrigin, noteSpentTxHash, noteToTransferEntry, noteTxKind, openOutgoingBlob, openPaymentSlip, outranks, pairwiseEphWindow, palletErrorKind, parseAmount, parseEvmAddress, persistCursor, persistScanResults, planTransfer, planUnshield, randomBlinding, randomOutgoingBlob, reconstructOutgoingTxRecords, recoverOwnerPkPoint, recoverSelfStealthNote, refuseIfAlreadySpent, regeneratePaymentSlip, removeByCommitment, requireSessionKeys, reservePairwiseIndex, reserveSelfEphIndex, resolveSelfEphCeiling, resolveSpentSet, resolveSpentStatus, restoreSession, runScan, scalarToHex, scanAbortError, sealOutgoingBlob, sealPaymentSlip, selectDescribingNote, selectDescribingNoteByCommitment, selectGhosts, selectNotes, selfEphWindow, serializeMemo, sessionCacheKey, shortHash, signAndSubmitTx, spendableBalance, stampCreatedAt, stampCreatedTxHash, stampSpentTxHash, substrateSs58ToAccountIdHex, substrateToEvm, toBase64, toHex, toTxResult, transferNotes, treeIdOf, treeOf, truncateMiddle, tryDecryptNote, tryDecryptNoteVerbose, tryRecoverOutgoing, txLandedAfterError, unshieldNote, upsertNote, vaultReplacer, vaultReviver, vaultStorageName, windowSizeForCounter };
6471
+ export { type AssetRegisteredEvent, type AssetUnverifiedEvent, type AssetVerifiedEvent, BABYJUB_SUBORDER, BN254_R, type BackupImportKeys, type BlockInfo, type BuildNoteDeps, type BuildNoteParams, type Bytes32, CachedNullifier, type ChainInfo, ChainModule, type ChunkInfo, CircuitId, CircuitVersionResolver, type ClaimShieldedFeesParams, type ClientProviderConfig, type CoinSelection, type CollectScanEntriesParams, type CommitmentsInsertedEvent, type ConnectionStatus, type CryptoKey$1 as CryptoKey, CryptoPrecompiles, type DecodedPrecompile, DecryptPool, DecryptedMemo, type DynamicBuilder, ENCRYPTED_MEMO_SIZE, EncryptedMemo, EncryptedNoteRecord, EncryptedTxRecord, type EventData, type EventPhase, type EventRecord, type EvmAddressInfo, type EvmBlock, EvmClient, EvmExplorer, type EvmLog, type EvmSigner, type EvmTransaction, type EvmTxRequest, type EvmTxSummary, type ExtrinsicDecoder, type ExtrinsicFacts, type ExtrinsicRecord, type FeeClaimDeps, type FeeClaimParams, type FeeClaimProofInputs, type FeeClaimProofOutput, type FeeClaimStep, type FormatOptions, type IdentityVersion, KNOWN_PALLET_ERRORS, KNOWN_PRECOMPILES, type KnownPrecompileInfo, LEAVES_PER_TREE, MIN_GASLESS_FEE, MIN_SIGNATURE_BYTES, MemoryVaultStorage, type MerkleRootUpdatedEvent, type MutableWalletSession, NATIVE_ASSET_ID, NOTE_BACKUP_VERSION, NOTE_BIGINT_FIELDS, type NoteBackup, type NoteBackupEntry, type NoteBuildKeys, NoteBuilder, type NoteDisclosure, NoteFacts, NoteInput, type NoteOrigin, type NoteProvenanceRecord, NoteStatusUpdate, NoteStorage, type NoteWithMeta, type NotesCache, NullifierCache, type NullifierChunkBody, type NullifierManifest, type NullifierSource, NullifierSyncMeta, type NullifierTail, type NullifiersSpentEvent, type ObservableNotesCache, OrbinumClient, type OrbinumClientConfig, OrbinumClientProvider, OrbinumWallet, type OrbinumWalletConfig, type OutgoingEphWindowEntry, PAGE_SIZE, PAYMENT_SLIP_SCHEME, PRECOMPILE_ADDR, type PairwiseEphWindowEntry, type PalletErrorKind, type PaymentSlipFields, type PersistParams, type PkScope, type PrecompileMethod, PrivacyKeyManager, type PrivacyMerkleProof, PrivacyModule, type PrivateTransferArgs, type PrivateTransferInput, type PrivateTransferOutput, type PrivateTransferParams, type PrivateTransferProofInputs, type ProofOptions, type ProvenanceAmount, type ProvenancePeer, type ProvenanceSource, type ProviderFactory, RECOVERED_TX_RESULT, type RawBlock, type RawBlockHeader, type RawTransferInput, type RawTransferOutput, type ReconstructDeps, type ReconstructedTxRecord, type RegisterAssetArgs, type RelayerInfo, RelayerStatusModule, type ResolveSpentSetParams, type ResolvedProverVersion, type ResolvedSpendVersion, type RpcV2MerkleProof, RpcV2Module, type RpcV2NullifierStatus, type RpcV2PoolAssetBalance, type RpcV2PoolStats, type RunScanParams, SPENDING_KEY_CANONICAL_ORIGIN, SPENDING_KEY_VERIFYING_CONTRACT, SPENDING_KEY_WARNING, type ScanChunkManifest, ScanCommitment, type ScanHint, type ScanHintPage, type ScanHintSource, ScanKeys, type ScanOptions, type ScanOutcome, type ScanProgress, type ScanResult, SecretStore, type SelfEphWindowEntry, type SelfStealthKeys, SentNoteMatch, type SessionCacheDeps, type ShieldArgs, type ShieldBatchArgs, type ShieldBatchItem, type ShieldBatchParams, type ShieldOperation, type ShieldParams, type ShieldedEvent, type ShieldedPoolCall, type ShieldedPoolEvent, ShieldedPoolModule, ShieldedPoolPrecompile, type SlipImportKeys, SpendDetails, type SpendPlanProblem, type SpendPrivacyReads, type SpendVault, type SpendableInputsCheck, type SpendingKeyTypedData, type StatusChangeEvent, type StatusListener, SubstrateClient, type SystemHealth, TRANSFER_INPUTS, TRANSFER_OUTPUTS, type TokenInfo, type TokenTransfer, type TransferDeps, type TransferFactsRow, type TransferFactsSource, type TransferInputNote, type TransferOutputNote, type TransferParams, type TransferPlan, type TransferResult, type TransferStep, type TransferSubmitRequest, type TxFactsSource, TxHistoryStore, type TxKind, type TxLandingPollOptions, type TxResult, UnmatchedSentHint, type UnsafeTxOptions, type UnshieldArgs, type UnshieldDeps, type UnshieldNoteParams, type UnshieldParams, type UnshieldPlan, type UnshieldProofInputs, type UnshieldProofResult, type UnshieldStep, type UnshieldSubmitRequest, type UnshieldedEvent, type UnverifyAssetArgs, VAULT_SCHEMA_VERSION, VaultConfigRecord, VaultLockedError, VaultStorage, VaultStore, type VaultStoreDeps, type VaultUnlockOptions, type VerifyAssetArgs, type VersionedArtifactProvider, type ViewingCredential, type WalletIdentity, type WalletScanKeys, type WalletSession, ZkNote, type ZkVerifierCircuitVersionInfo, type ZkVerifierHistoricalVersion, ZkVerifierModule, type ZkVerifierVersionStats, type ZkVerifierVkHash, accountIdHexToSs58, addressToAccountIdHex, addressToFieldElement, applyBatch, applyNoteStatus, assertSecretKeyBytes, base64UrlDecode, base64UrlEncode, bigintTo32Be, bigintTo32Le, bigintTo32LeArr, blindTag, buildConfig, buildDummyTransferInput, buildShieldBatchOperations, buildShieldParams, buildZkNote, buildZkNoteWithIndex, bytesToBigintLE, bytesToBjjScalar, cacheSession, canPairWith, canonicalAccountId, chainActiveCircuitVersion, checkSpendableInputs, claimFees, classifyChainError, clearSession, collectNullifiersToQuery, collectOutgoingFacts, collectScanEntries, commitmentHexOf, computeNoteCommitment, computeNullifier, computePathIndices, connectInjectedExtension, createNoteDisclosureKey, createNotesCache, createWalletSession, decodeNoteBackup, decodeNoteDisclosureKey, decodePaymentSlip, decodePrecompileCalldata, decryptJson, decryptNoteRecord, deriveIdentity, deriveMasterKeyBytes, deriveOutgoingEphPk, deriveOutgoingEphSk, deriveOutgoingSharedSecret, deriveOutgoingViewingKeyV3, deriveOwnerPk, derivePairwiseEphSk, derivePairwiseSharedSecret, deriveSelfEphSk, deriveSpendingKeyFromMaster, deriveSpendingKeyFromSignature, deriveSpendingKeyMessageV2, deriveSpendingKeyTypedData, deriveSpendingKeyV3, deriveStealthOwnerPk, deriveStealthSk, deriveVaultBlindKey, deriveVaultKey, deriveViewTag, deriveViewingPublicKey, deriveViewingSecretKey, deriveViewingSecretKeyV3, detectCommitmentMismatch, encodeNoteBackup, encodePaymentSlip, encryptJson, encryptNote, ensureCreatedAt, ensureHexPrefix, evmAddressToAccountId, evmToImplicitSubstrate, evmToMappedAccountHex, evmToSubstrate, exportViewingCredential, extractPalletError, failed, fastMulBase, fastMulPoint, fetchExtrinsicFacts, formatAmountPlain, formatBalance, formatORB, fromBase64, fromHex, gapMargin, generateFeeClaimProof, generateTransferProof, generateUnshieldProof, getInjectedExtensions, getPrecompileLabel, hasCachedSession, hasInjectedExtensions, hasSourcePk, hexToBigint, hexToNumber, implicitSubstrateToEvm, importNotesFromBackup, importPaymentSlip, isAbortError, isAlreadySpentError, isConnectionLossError, isEvmAddress, isGhostNoteError, isHexOfLength, isImplicitEvmAccount, isNativeAsset, isNoteSelfConsistent, isSpendable, isSubstrateAddress, isUnifiedAddress, isUsableSecretKey, isValidLeafIndex, leHexToBigint, mapExtrinsicArgs, mapZkEventData, markInputsSpent, mergeProvenance, normalizeChainFingerprint, normalizeEvmAddress, normalizeNote, normalizeNotes, noteBlindTag, noteCreatedAt, noteCreatedTxHash, noteMatchesCommitment, noteOrigin, noteSpentTxHash, noteTxKind, openPaymentSlip, openRecipientBookEntry, outgoingEphWindow, outranks, pairwiseEphWindow, palletErrorKind, parseAmount, parseEvmAddress, persistCursor, persistScanResults, planTransfer, planUnshield, randomBlinding, reconstructOutgoingIndex, reconstructOutgoingTxRecords, recoverOwnerPkPoint, recoverSelfStealthNote, refuseIfAlreadySpent, regeneratePaymentSlip, registerPairwiseCounterparty, removeByCommitment, requireSessionKeys, reserveOutgoingIndex, reserveSelfEphIndex, resolveSelfEphCeiling, resolveSpentSet, resolveSpentStatus, restoreSession, runScan, scalarToHex, scanAbortError, sealPaymentSlip, sealRecipientBookEntry, selectDescribingNote, selectDescribingNoteByCommitment, selectGhosts, selectNotes, selfEphWindow, serializeMemo, sessionCacheKey, shortHash, signAndSubmitTx, spendableBalance, stampCreatedAt, stampCreatedTxHash, stampSpentTxHash, substrateSs58ToAccountIdHex, substrateToEvm, toBase64, toHex, toTxResult, transferNotes, treeIdOf, treeOf, truncateMiddle, tryDecryptNote, tryDecryptNoteVerbose, txLandedAfterError, unpackUsableViewingKey, unshieldNote, upsertNote, vaultReplacer, vaultReviver, vaultStorageName, windowSizeForCounter };