@orbinum/sdk 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -18
- package/dist/adapters/indexeddb/index.d.mts +10 -10
- package/dist/adapters/indexeddb/index.d.ts +10 -10
- package/dist/{chunk-VYKKBXOE.mjs → chunk-54HFH63J.mjs} +493 -256
- package/dist/{index-CLpM1984.d.mts → index-B-dbSIPN.d.mts} +209 -52
- package/dist/{index-CLpM1984.d.ts → index-B-dbSIPN.d.ts} +209 -52
- package/dist/index.d.mts +977 -498
- package/dist/index.d.ts +977 -498
- package/dist/index.js +990 -512
- package/dist/index.mjs +572 -313
- package/dist/{secretStore-CF6Nse__.d.ts → secretStore-CCYo4hna.d.mts} +15 -11
- package/dist/{secretStore-CF6Nse__.d.mts → secretStore-CCYo4hna.d.ts} +15 -11
- package/dist/wallet/worker/index.d.mts +1 -1
- package/dist/wallet/worker/index.d.ts +1 -1
- package/dist/wallet/worker/index.js +428 -111
- package/dist/wallet/worker/index.mjs +3 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -47,9 +47,8 @@ __export(index_exports, {
|
|
|
47
47
|
NATIVE_ASSET_ID: () => NATIVE_ASSET_ID,
|
|
48
48
|
NOTE_BACKUP_VERSION: () => NOTE_BACKUP_VERSION,
|
|
49
49
|
NOTE_BIGINT_FIELDS: () => NOTE_BIGINT_FIELDS,
|
|
50
|
-
NOTE_TRANSFER_URI_SCHEME: () => NOTE_TRANSFER_URI_SCHEME,
|
|
51
50
|
NoteBuilder: () => NoteBuilder,
|
|
52
|
-
|
|
51
|
+
OUTGOING_EPH_WINDOW: () => OUTGOING_EPH_WINDOW,
|
|
53
52
|
OrbinumClient: () => OrbinumClient,
|
|
54
53
|
OrbinumClientProvider: () => OrbinumClientProvider,
|
|
55
54
|
OrbinumWallet: () => OrbinumWallet,
|
|
@@ -59,7 +58,6 @@ __export(index_exports, {
|
|
|
59
58
|
PRECOMPILE_ADDR: () => PRECOMPILE_ADDR,
|
|
60
59
|
PrivacyKeyManager: () => PrivacyKeyManager,
|
|
61
60
|
PrivacyModule: () => PrivacyModule,
|
|
62
|
-
QR_PAGE_MAX_CHARS: () => QR_PAGE_MAX_CHARS,
|
|
63
61
|
RECOVERED_TX_RESULT: () => RECOVERED_TX_RESULT,
|
|
64
62
|
RelayerStatusModule: () => RelayerStatusModule,
|
|
65
63
|
RpcV2Module: () => RpcV2Module,
|
|
@@ -84,7 +82,7 @@ __export(index_exports, {
|
|
|
84
82
|
addressToFieldElement: () => addressToFieldElement,
|
|
85
83
|
applyBatch: () => applyBatch,
|
|
86
84
|
applyNoteStatus: () => applyNoteStatus,
|
|
87
|
-
|
|
85
|
+
assertSecretKeyBytes: () => assertSecretKeyBytes,
|
|
88
86
|
base58: () => import_base.base58,
|
|
89
87
|
base64UrlDecode: () => base64UrlDecode,
|
|
90
88
|
base64UrlEncode: () => base64UrlEncode,
|
|
@@ -97,6 +95,7 @@ __export(index_exports, {
|
|
|
97
95
|
buildShieldBatchOperations: () => buildShieldBatchOperations,
|
|
98
96
|
buildShieldParams: () => buildShieldParams,
|
|
99
97
|
buildZkNote: () => buildZkNote,
|
|
98
|
+
buildZkNoteWithIndex: () => buildZkNoteWithIndex,
|
|
100
99
|
bytesToBigintLE: () => bytesToBigintLE,
|
|
101
100
|
bytesToBjjScalar: () => bytesToBjjScalar,
|
|
102
101
|
cacheSession: () => cacheSession,
|
|
@@ -126,15 +125,17 @@ __export(index_exports, {
|
|
|
126
125
|
createWorkerPool: () => createWorkerPool,
|
|
127
126
|
decodeNoteBackup: () => decodeNoteBackup,
|
|
128
127
|
decodeNoteDisclosureKey: () => decodeNoteDisclosureKey,
|
|
129
|
-
decodeNoteTransferPage: () => decodeNoteTransferPage,
|
|
130
128
|
decodePaymentSlip: () => decodePaymentSlip,
|
|
131
129
|
decodePrecompileCalldata: () => decodePrecompileCalldata,
|
|
132
130
|
decryptHintBatch: () => decryptHintBatch,
|
|
133
131
|
decryptJson: () => decryptJson,
|
|
134
132
|
decryptNoteRecord: () => decryptNoteRecord,
|
|
133
|
+
deriveIdentity: () => deriveIdentity,
|
|
135
134
|
deriveMasterKeyBytes: () => deriveMasterKeyBytes,
|
|
136
|
-
|
|
137
|
-
|
|
135
|
+
deriveOutgoingEphPk: () => deriveOutgoingEphPk,
|
|
136
|
+
deriveOutgoingEphSk: () => deriveOutgoingEphSk,
|
|
137
|
+
deriveOutgoingSharedSecret: () => deriveOutgoingSharedSecret,
|
|
138
|
+
deriveOutgoingViewingKeyV3: () => deriveOutgoingViewingKeyV3,
|
|
138
139
|
deriveOwnerPk: () => deriveOwnerPk,
|
|
139
140
|
derivePairwiseEphSk: () => derivePairwiseEphSk,
|
|
140
141
|
derivePairwiseSharedSecret: () => derivePairwiseSharedSecret,
|
|
@@ -143,6 +144,7 @@ __export(index_exports, {
|
|
|
143
144
|
deriveSpendingKeyFromSignature: () => deriveSpendingKeyFromSignature,
|
|
144
145
|
deriveSpendingKeyMessageV2: () => deriveSpendingKeyMessageV2,
|
|
145
146
|
deriveSpendingKeyTypedData: () => deriveSpendingKeyTypedData,
|
|
147
|
+
deriveSpendingKeyV3: () => deriveSpendingKeyV3,
|
|
146
148
|
deriveStealthOwnerPk: () => deriveStealthOwnerPk,
|
|
147
149
|
deriveStealthSk: () => deriveStealthSk,
|
|
148
150
|
deriveVaultBlindKey: () => deriveVaultBlindKey,
|
|
@@ -150,9 +152,9 @@ __export(index_exports, {
|
|
|
150
152
|
deriveViewTag: () => deriveViewTag,
|
|
151
153
|
deriveViewingPublicKey: () => deriveViewingPublicKey,
|
|
152
154
|
deriveViewingSecretKey: () => deriveViewingSecretKey,
|
|
155
|
+
deriveViewingSecretKeyV3: () => deriveViewingSecretKeyV3,
|
|
153
156
|
detectCommitmentMismatch: () => detectCommitmentMismatch,
|
|
154
157
|
encodeNoteBackup: () => encodeNoteBackup,
|
|
155
|
-
encodeNoteTransferPages: () => encodeNoteTransferPages,
|
|
156
158
|
encodePaymentSlip: () => encodePaymentSlip,
|
|
157
159
|
encryptJson: () => encryptJson,
|
|
158
160
|
encryptNote: () => encryptNote,
|
|
@@ -162,6 +164,7 @@ __export(index_exports, {
|
|
|
162
164
|
evmToImplicitSubstrate: () => evmToImplicitSubstrate,
|
|
163
165
|
evmToMappedAccountHex: () => evmToMappedAccountHex,
|
|
164
166
|
evmToSubstrate: () => evmToSubstrate,
|
|
167
|
+
exportViewingCredential: () => exportViewingCredential,
|
|
165
168
|
extractPalletError: () => extractPalletError,
|
|
166
169
|
failed: () => failed,
|
|
167
170
|
fastMulBase: () => fastMulBase,
|
|
@@ -202,9 +205,9 @@ __export(index_exports, {
|
|
|
202
205
|
isNativeAsset: () => isNativeAsset,
|
|
203
206
|
isNoteSelfConsistent: () => isNoteSelfConsistent,
|
|
204
207
|
isSpendable: () => isSpendable,
|
|
205
|
-
isSs58: () => isSs58,
|
|
206
208
|
isSubstrateAddress: () => isSubstrateAddress,
|
|
207
209
|
isUnifiedAddress: () => isUnifiedAddress,
|
|
210
|
+
isUsableSecretKey: () => isUsableSecretKey,
|
|
208
211
|
isValidLeafIndex: () => isValidLeafIndex,
|
|
209
212
|
leHexToBigint: () => leHexToBigint,
|
|
210
213
|
mapExtrinsicArgs: () => mapExtrinsicArgs,
|
|
@@ -221,10 +224,10 @@ __export(index_exports, {
|
|
|
221
224
|
noteMatchesCommitment: () => noteMatchesCommitment,
|
|
222
225
|
noteOrigin: () => noteOrigin,
|
|
223
226
|
noteSpentTxHash: () => noteSpentTxHash,
|
|
224
|
-
noteToTransferEntry: () => noteToTransferEntry,
|
|
225
227
|
noteTxKind: () => noteTxKind,
|
|
226
|
-
openOutgoingBlob: () => openOutgoingBlob,
|
|
227
228
|
openPaymentSlip: () => openPaymentSlip,
|
|
229
|
+
openRecipientBookEntry: () => openRecipientBookEntry,
|
|
230
|
+
outgoingEphWindow: () => outgoingEphWindow,
|
|
228
231
|
outranks: () => outranks,
|
|
229
232
|
pairwiseEphWindow: () => pairwiseEphWindow,
|
|
230
233
|
palletErrorKind: () => palletErrorKind,
|
|
@@ -235,15 +238,18 @@ __export(index_exports, {
|
|
|
235
238
|
planTransfer: () => planTransfer,
|
|
236
239
|
planUnshield: () => planUnshield,
|
|
237
240
|
randomBlinding: () => randomBlinding,
|
|
238
|
-
|
|
241
|
+
reconstructOutgoingIndex: () => reconstructOutgoingIndex,
|
|
239
242
|
reconstructOutgoingTxRecords: () => reconstructOutgoingTxRecords,
|
|
240
243
|
recoverOwnerPkPoint: () => recoverOwnerPkPoint,
|
|
241
244
|
recoverSelfStealthNote: () => recoverSelfStealthNote,
|
|
245
|
+
recoverSentFromSharedSecret: () => recoverSentFromSharedSecret,
|
|
246
|
+
recoverSentNote: () => recoverSentNote,
|
|
242
247
|
refuseIfAlreadySpent: () => refuseIfAlreadySpent,
|
|
243
248
|
regeneratePaymentSlip: () => regeneratePaymentSlip,
|
|
249
|
+
registerPairwiseCounterparty: () => registerPairwiseCounterparty,
|
|
244
250
|
removeByCommitment: () => removeByCommitment,
|
|
245
251
|
requireSessionKeys: () => requireSessionKeys,
|
|
246
|
-
|
|
252
|
+
reserveOutgoingIndex: () => reserveOutgoingIndex,
|
|
247
253
|
reserveSelfEphIndex: () => reserveSelfEphIndex,
|
|
248
254
|
resolveSelfEphCeiling: () => resolveSelfEphCeiling,
|
|
249
255
|
resolveSpentSet: () => resolveSpentSet,
|
|
@@ -252,8 +258,8 @@ __export(index_exports, {
|
|
|
252
258
|
runScan: () => runScan,
|
|
253
259
|
scalarToHex: () => scalarToHex,
|
|
254
260
|
scanAbortError: () => scanAbortError,
|
|
255
|
-
sealOutgoingBlob: () => sealOutgoingBlob,
|
|
256
261
|
sealPaymentSlip: () => sealPaymentSlip,
|
|
262
|
+
sealRecipientBookEntry: () => sealRecipientBookEntry,
|
|
257
263
|
selectDescribingNote: () => selectDescribingNote,
|
|
258
264
|
selectDescribingNoteByCommitment: () => selectDescribingNoteByCommitment,
|
|
259
265
|
selectGhosts: () => selectGhosts,
|
|
@@ -279,10 +285,10 @@ __export(index_exports, {
|
|
|
279
285
|
truncateMiddle: () => truncateMiddle,
|
|
280
286
|
tryDecryptNote: () => tryDecryptNote,
|
|
281
287
|
tryDecryptNoteVerbose: () => tryDecryptNoteVerbose,
|
|
282
|
-
tryRecoverOutgoing: () => tryRecoverOutgoing,
|
|
283
288
|
txLandedAfterError: () => txLandedAfterError,
|
|
284
289
|
u128: () => import_substrate_bindings3.u128,
|
|
285
290
|
u64: () => import_substrate_bindings3.u64,
|
|
291
|
+
unpackUsableViewingKey: () => unpackUsableViewingKey,
|
|
286
292
|
unshieldNote: () => unshieldNote,
|
|
287
293
|
upsertNote: () => upsertNote,
|
|
288
294
|
vaultReplacer: () => vaultReplacer,
|
|
@@ -317,12 +323,23 @@ function ensureHexPrefix(hex) {
|
|
|
317
323
|
return hex.startsWith("0x") ? hex : `0x${hex}`;
|
|
318
324
|
}
|
|
319
325
|
function hexToNumber(hex) {
|
|
320
|
-
|
|
326
|
+
const clean = /^0x/i.test(hex) ? hex.slice(2) : hex;
|
|
327
|
+
if (!/^[0-9a-fA-F]+$/.test(clean)) {
|
|
328
|
+
throw new Error(`Invalid hex quantity: "${hex}"`);
|
|
329
|
+
}
|
|
330
|
+
const value = Number(BigInt("0x" + clean));
|
|
331
|
+
if (!Number.isSafeInteger(value)) {
|
|
332
|
+
throw new Error(`Hex quantity exceeds safe integer range: "${hex}"`);
|
|
333
|
+
}
|
|
334
|
+
return value;
|
|
321
335
|
}
|
|
322
336
|
function hexToBigint(hex) {
|
|
323
337
|
return BigInt(hex);
|
|
324
338
|
}
|
|
325
339
|
function scalarToHex(value) {
|
|
340
|
+
if (value < 0n || value >> 256n !== 0n) {
|
|
341
|
+
throw new Error(`scalarToHex: value must fit in 32 unsigned bytes, got ${value}`);
|
|
342
|
+
}
|
|
326
343
|
return "0x" + value.toString(16).padStart(64, "0");
|
|
327
344
|
}
|
|
328
345
|
|
|
@@ -464,6 +481,7 @@ var BABYJUB_SUBORDER = 273603035897990940278080071815715938607681397215856725920
|
|
|
464
481
|
var import_baby_jubjub = require("@zk-kit/baby-jubjub");
|
|
465
482
|
var BJJ_A = 168700n;
|
|
466
483
|
var BJJ_D = 168696n;
|
|
484
|
+
var BJJ_COFACTOR = 8n;
|
|
467
485
|
function _modpow(base, exp, mod) {
|
|
468
486
|
let result = 1n;
|
|
469
487
|
base = base % mod;
|
|
@@ -522,6 +540,28 @@ function recoverOwnerPkPoint(ax) {
|
|
|
522
540
|
return [ax, yAlt];
|
|
523
541
|
}
|
|
524
542
|
}
|
|
543
|
+
function unpackUsableViewingKey(packed) {
|
|
544
|
+
const point = (0, import_baby_jubjub.unpackPoint)(packed);
|
|
545
|
+
if (!point) return null;
|
|
546
|
+
if (point[0] === 0n && point[1] === 1n) return null;
|
|
547
|
+
const cleared = (0, import_baby_jubjub.mulPointEscalar)(point, BJJ_COFACTOR);
|
|
548
|
+
if (cleared[0] === 0n && cleared[1] === 1n) return null;
|
|
549
|
+
return point;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// src/foundation/crypto/keyGuards.ts
|
|
553
|
+
var SECRET_KEY_SIZE = 32;
|
|
554
|
+
function isUsableSecretKey(key) {
|
|
555
|
+
return key !== void 0 && key !== null && key.length === SECRET_KEY_SIZE && !key.every((b) => b === 0);
|
|
556
|
+
}
|
|
557
|
+
function assertSecretKeyBytes(key, label) {
|
|
558
|
+
if (key.length !== SECRET_KEY_SIZE) {
|
|
559
|
+
throw new Error(`${label} must be ${SECRET_KEY_SIZE} bytes, got ${key.length}`);
|
|
560
|
+
}
|
|
561
|
+
if (key.every((b) => b === 0)) {
|
|
562
|
+
throw new Error(`${label} is all zeros \u2014 that is not key material`);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
525
565
|
|
|
526
566
|
// src/foundation/crypto/bjj-fast.ts
|
|
527
567
|
var import_edwards = require("@noble/curves/abstract/edwards.js");
|
|
@@ -697,9 +737,6 @@ function parseEvmAddress(addr) {
|
|
|
697
737
|
const withPrefix = addr.startsWith("0x") ? addr : `0x${addr}`;
|
|
698
738
|
return isEvmAddress(withPrefix) ? withPrefix.toLowerCase() : null;
|
|
699
739
|
}
|
|
700
|
-
function isSs58(addr) {
|
|
701
|
-
return !addr.startsWith("0x") && addr.length >= 46 && addr.length <= 50;
|
|
702
|
-
}
|
|
703
740
|
function isEvmAddress(addr) {
|
|
704
741
|
return /^0x[0-9a-fA-F]{40}$/.test(addr);
|
|
705
742
|
}
|
|
@@ -829,11 +866,10 @@ function addressToAccountIdHex(addr) {
|
|
|
829
866
|
function addressToFieldElement(address) {
|
|
830
867
|
const accountIdHex = addressToAccountIdHex(address);
|
|
831
868
|
if (!accountIdHex) throw new Error(`Cannot resolve address to AccountId32: ${address}`);
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
for (let i = 0; i < 32; i++) {
|
|
835
|
-
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
869
|
+
if (!isHexOfLength(accountIdHex, 32)) {
|
|
870
|
+
throw new Error(`AccountId32 must be 32 bytes of hex: ${accountIdHex}`);
|
|
836
871
|
}
|
|
872
|
+
const bytes = fromHex(accountIdHex);
|
|
837
873
|
return bytesToBigintLE(bytes) % BN254_R;
|
|
838
874
|
}
|
|
839
875
|
|
|
@@ -848,6 +884,15 @@ var KEY_DOMAIN = new TextEncoder().encode("orbinum-note-encryption-v1");
|
|
|
848
884
|
var VIEW_TAG_DOMAIN = new TextEncoder().encode("orbinum-view-tag-v1");
|
|
849
885
|
var MEMO_PLAINTEXT_SIZE = 120;
|
|
850
886
|
function serializeMemo(value, ownerPk, blinding, assetId, sourcePk, circuitVersion) {
|
|
887
|
+
if (value < 0n || value >> 128n !== 0n) {
|
|
888
|
+
throw new Error(`serializeMemo: value must fit in 128 unsigned bits, got ${value}`);
|
|
889
|
+
}
|
|
890
|
+
if (!Number.isInteger(assetId) || assetId < 0 || assetId > 4294967295) {
|
|
891
|
+
throw new Error(`serializeMemo: assetId must be a u32, got ${assetId}`);
|
|
892
|
+
}
|
|
893
|
+
if (!Number.isInteger(circuitVersion) || circuitVersion < 0 || circuitVersion > 4294967295) {
|
|
894
|
+
throw new Error(`serializeMemo: circuitVersion must be a u32, got ${circuitVersion}`);
|
|
895
|
+
}
|
|
851
896
|
const buf = new Uint8Array(MEMO_PLAINTEXT_SIZE);
|
|
852
897
|
const view = new DataView(buf.buffer);
|
|
853
898
|
view.setBigUint64(0, value & 0xffffffffffffffffn, true);
|
|
@@ -879,6 +924,9 @@ var CIPHERTEXT_SIZE = 136;
|
|
|
879
924
|
var EPH_PK_SIZE = 32;
|
|
880
925
|
var ENCRYPTED_MEMO_SIZE = NONCE_SIZE + CIPHERTEXT_SIZE + EPH_PK_SIZE;
|
|
881
926
|
function bytesToBjjScalar(bytes) {
|
|
927
|
+
if (bytes.length !== 32) {
|
|
928
|
+
throw new Error(`bytesToBjjScalar: expected 32 bytes, got ${bytes.length}`);
|
|
929
|
+
}
|
|
882
930
|
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
883
931
|
return BigInt("0x" + hex) % BABYJUB_SUBORDER || 1n;
|
|
884
932
|
}
|
|
@@ -902,7 +950,7 @@ function parsePlaintext(nonce, ciphertextWithMac, encKey) {
|
|
|
902
950
|
}
|
|
903
951
|
var EncryptedMemo = {
|
|
904
952
|
/**
|
|
905
|
-
* Build and encrypt a memo for a note using ECDH (
|
|
953
|
+
* Build and encrypt a memo for a note using ECDH (180 bytes).
|
|
906
954
|
*
|
|
907
955
|
* @param value Note value in planck.
|
|
908
956
|
* @param ownerPk 32-byte owner public key (LE).
|
|
@@ -942,7 +990,7 @@ var EncryptedMemo = {
|
|
|
942
990
|
const ephPkPoint = fastMulBase(ephSkScalar);
|
|
943
991
|
ephPkPackedBytes = bigintTo32Le((0, import_baby_jubjub3.packPoint)(ephPkPoint));
|
|
944
992
|
const ivkPackedBigint = bytesToBigintLE(recipientIvkPacked);
|
|
945
|
-
const ivkPoint = (
|
|
993
|
+
const ivkPoint = unpackUsableViewingKey(ivkPackedBigint);
|
|
946
994
|
if (!ivkPoint)
|
|
947
995
|
throw new Error("EncryptedMemo.encrypt: invalid recipient viewing public key");
|
|
948
996
|
const sharedPoint = fastMulPoint(ivkPoint, ephSkScalar);
|
|
@@ -1032,7 +1080,7 @@ var EncryptedMemo = {
|
|
|
1032
1080
|
if (ephPkPackedBigint === 0n) {
|
|
1033
1081
|
return new Uint8Array(32);
|
|
1034
1082
|
}
|
|
1035
|
-
const ephPkPoint = (
|
|
1083
|
+
const ephPkPoint = unpackUsableViewingKey(ephPkPackedBigint);
|
|
1036
1084
|
if (!ephPkPoint) return null;
|
|
1037
1085
|
const ivskScalar = bytesToBjjScalar(viewingSecretKey);
|
|
1038
1086
|
const sharedPoint = fastMulPoint(ephPkPoint, ivskScalar);
|
|
@@ -1042,9 +1090,9 @@ var EncryptedMemo = {
|
|
|
1042
1090
|
* Cheap view-tag check: does memo nonce[0] match the tag derived from
|
|
1043
1091
|
* `sharedSecret`? One SHA256 + one byte compare — no AEAD work.
|
|
1044
1092
|
*
|
|
1045
|
-
* Only meaningful for memos
|
|
1046
|
-
*
|
|
1047
|
-
*
|
|
1093
|
+
* Only meaningful for memos that carry a tag — at or after
|
|
1094
|
+
* `ScanKeys.viewTagActivationLeaf`. An older memo has a random byte there
|
|
1095
|
+
* and would false-negative 255/256 of the time.
|
|
1048
1096
|
*/
|
|
1049
1097
|
checkViewTag(memoBytes, sharedSecret) {
|
|
1050
1098
|
if (memoBytes.length !== ENCRYPTED_MEMO_SIZE) return false;
|
|
@@ -1070,72 +1118,11 @@ var EncryptedMemo = {
|
|
|
1070
1118
|
}
|
|
1071
1119
|
};
|
|
1072
1120
|
|
|
1073
|
-
// src/protocol/memo/
|
|
1121
|
+
// src/protocol/memo/PaymentSlip.ts
|
|
1074
1122
|
var import_chacha2 = require("@noble/ciphers/chacha.js");
|
|
1075
1123
|
var import_utils2 = require("@noble/ciphers/utils.js");
|
|
1076
1124
|
var import_hkdf2 = require("@noble/hashes/hkdf.js");
|
|
1077
1125
|
var import_sha23 = require("@noble/hashes/sha2.js");
|
|
1078
|
-
var OCK_DOMAIN = new TextEncoder().encode("orbinum-outgoing-cipher-v1");
|
|
1079
|
-
var NONCE_PREFIX = new TextEncoder().encode("OVK1");
|
|
1080
|
-
var NONCE_SUFFIX_SIZE = 8;
|
|
1081
|
-
var SHARED_SECRET_SIZE = 32;
|
|
1082
|
-
var MAC_SIZE = 16;
|
|
1083
|
-
var OVK_BLOB_SIZE = NONCE_SUFFIX_SIZE + SHARED_SECRET_SIZE + MAC_SIZE;
|
|
1084
|
-
function deriveOutgoingCipherKey(ovk, commitmentBytes, ephPkBytes) {
|
|
1085
|
-
if (commitmentBytes.length !== 32) {
|
|
1086
|
-
throw new Error(
|
|
1087
|
-
`OutgoingBlob: commitmentBytes must be 32 bytes, got ${commitmentBytes.length}`
|
|
1088
|
-
);
|
|
1089
|
-
}
|
|
1090
|
-
if (ephPkBytes.length !== 32) {
|
|
1091
|
-
throw new Error(`OutgoingBlob: ephPkBytes must be 32 bytes, got ${ephPkBytes.length}`);
|
|
1092
|
-
}
|
|
1093
|
-
const salt = new Uint8Array(64);
|
|
1094
|
-
salt.set(commitmentBytes, 0);
|
|
1095
|
-
salt.set(ephPkBytes, 32);
|
|
1096
|
-
return (0, import_hkdf2.hkdf)(import_sha23.sha256, ovk, salt, OCK_DOMAIN, 32);
|
|
1097
|
-
}
|
|
1098
|
-
function buildNonce(suffix) {
|
|
1099
|
-
const nonce = new Uint8Array(12);
|
|
1100
|
-
nonce.set(NONCE_PREFIX, 0);
|
|
1101
|
-
nonce.set(suffix, 4);
|
|
1102
|
-
return nonce;
|
|
1103
|
-
}
|
|
1104
|
-
function sealOutgoingBlob(ovk, sharedSecret, commitmentBytes, ephPkBytes) {
|
|
1105
|
-
if (sharedSecret.length !== SHARED_SECRET_SIZE) {
|
|
1106
|
-
throw new Error(`OutgoingBlob: sharedSecret must be 32 bytes, got ${sharedSecret.length}`);
|
|
1107
|
-
}
|
|
1108
|
-
const ock = deriveOutgoingCipherKey(ovk, commitmentBytes, ephPkBytes);
|
|
1109
|
-
const suffix = (0, import_utils2.randomBytes)(NONCE_SUFFIX_SIZE);
|
|
1110
|
-
const cipher = (0, import_chacha2.chacha20poly1305)(ock, buildNonce(suffix));
|
|
1111
|
-
const sealed = cipher.encrypt(sharedSecret);
|
|
1112
|
-
const blob = new Uint8Array(OVK_BLOB_SIZE);
|
|
1113
|
-
blob.set(suffix, 0);
|
|
1114
|
-
blob.set(sealed, NONCE_SUFFIX_SIZE);
|
|
1115
|
-
return blob;
|
|
1116
|
-
}
|
|
1117
|
-
function openOutgoingBlob(ovk, blob, commitmentBytes, ephPkBytes) {
|
|
1118
|
-
if (blob.length !== OVK_BLOB_SIZE) return null;
|
|
1119
|
-
if (commitmentBytes.length !== 32 || ephPkBytes.length !== 32) return null;
|
|
1120
|
-
try {
|
|
1121
|
-
const ock = deriveOutgoingCipherKey(ovk, commitmentBytes, ephPkBytes);
|
|
1122
|
-
const suffix = blob.subarray(0, NONCE_SUFFIX_SIZE);
|
|
1123
|
-
const sealed = blob.subarray(NONCE_SUFFIX_SIZE);
|
|
1124
|
-
const cipher = (0, import_chacha2.chacha20poly1305)(ock, buildNonce(suffix));
|
|
1125
|
-
return cipher.decrypt(sealed);
|
|
1126
|
-
} catch {
|
|
1127
|
-
return null;
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
function randomOutgoingBlob() {
|
|
1131
|
-
return (0, import_utils2.randomBytes)(OVK_BLOB_SIZE);
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
// src/protocol/memo/PaymentSlip.ts
|
|
1135
|
-
var import_chacha3 = require("@noble/ciphers/chacha.js");
|
|
1136
|
-
var import_utils3 = require("@noble/ciphers/utils.js");
|
|
1137
|
-
var import_hkdf3 = require("@noble/hashes/hkdf.js");
|
|
1138
|
-
var import_sha24 = require("@noble/hashes/sha2.js");
|
|
1139
1126
|
var import_baby_jubjub4 = require("@zk-kit/baby-jubjub");
|
|
1140
1127
|
|
|
1141
1128
|
// src/protocol/spend/coinSelection.ts
|
|
@@ -1199,16 +1186,16 @@ function buildDummyTransferInput(assetId) {
|
|
|
1199
1186
|
|
|
1200
1187
|
// src/protocol/memo/PaymentSlip.ts
|
|
1201
1188
|
var SLIP_DOMAIN = new TextEncoder().encode("orbinum-payment-slip-v1");
|
|
1202
|
-
var
|
|
1189
|
+
var NONCE_PREFIX = new TextEncoder().encode("SLP1");
|
|
1203
1190
|
var EPH_PK_SIZE2 = 32;
|
|
1204
|
-
var
|
|
1191
|
+
var NONCE_SUFFIX_SIZE = 8;
|
|
1205
1192
|
var MAX_SLIP_ENVELOPE_SIZE = 4096;
|
|
1206
1193
|
function deriveSlipKey(sharedSecret) {
|
|
1207
|
-
return (0,
|
|
1194
|
+
return (0, import_hkdf2.hkdf)(import_sha23.sha256, sharedSecret, void 0, SLIP_DOMAIN, 32);
|
|
1208
1195
|
}
|
|
1209
|
-
function
|
|
1196
|
+
function buildNonce(suffix) {
|
|
1210
1197
|
const nonce = new Uint8Array(12);
|
|
1211
|
-
nonce.set(
|
|
1198
|
+
nonce.set(NONCE_PREFIX, 0);
|
|
1212
1199
|
nonce.set(suffix, 4);
|
|
1213
1200
|
return nonce;
|
|
1214
1201
|
}
|
|
@@ -1218,36 +1205,36 @@ function sealPaymentSlip(recipientIvkPacked, fields) {
|
|
|
1218
1205
|
`PaymentSlip: recipientIvk must be 32 bytes, got ${recipientIvkPacked.length}`
|
|
1219
1206
|
);
|
|
1220
1207
|
}
|
|
1221
|
-
const ivkPoint = (
|
|
1208
|
+
const ivkPoint = unpackUsableViewingKey(bytesToBigintLE(recipientIvkPacked));
|
|
1222
1209
|
if (!ivkPoint) throw new Error("PaymentSlip: invalid recipient viewing public key");
|
|
1223
|
-
const ephSkScalar = bytesToBjjScalar((0,
|
|
1210
|
+
const ephSkScalar = bytesToBjjScalar((0, import_utils2.randomBytes)(32));
|
|
1224
1211
|
const ephPkPacked = bigintTo32Le((0, import_baby_jubjub4.packPoint)(fastMulBase(ephSkScalar)));
|
|
1225
1212
|
const sharedPoint = fastMulPoint(ivkPoint, ephSkScalar);
|
|
1226
1213
|
const sharedSecret = bigintTo32Le(sharedPoint[0]);
|
|
1227
1214
|
const slipKey = deriveSlipKey(sharedSecret);
|
|
1228
|
-
const suffix = (0,
|
|
1229
|
-
const cipher = (0,
|
|
1215
|
+
const suffix = (0, import_utils2.randomBytes)(NONCE_SUFFIX_SIZE);
|
|
1216
|
+
const cipher = (0, import_chacha2.chacha20poly1305)(slipKey, buildNonce(suffix));
|
|
1230
1217
|
const plaintext = new TextEncoder().encode(JSON.stringify(fields));
|
|
1231
1218
|
const sealed = cipher.encrypt(plaintext);
|
|
1232
|
-
const envelope = new Uint8Array(EPH_PK_SIZE2 +
|
|
1219
|
+
const envelope = new Uint8Array(EPH_PK_SIZE2 + NONCE_SUFFIX_SIZE + sealed.length);
|
|
1233
1220
|
envelope.set(ephPkPacked, 0);
|
|
1234
1221
|
envelope.set(suffix, EPH_PK_SIZE2);
|
|
1235
|
-
envelope.set(sealed, EPH_PK_SIZE2 +
|
|
1222
|
+
envelope.set(sealed, EPH_PK_SIZE2 + NONCE_SUFFIX_SIZE);
|
|
1236
1223
|
return envelope;
|
|
1237
1224
|
}
|
|
1238
1225
|
function openPaymentSlip(recipientIvsk, envelope) {
|
|
1239
|
-
if (envelope.length < EPH_PK_SIZE2 +
|
|
1226
|
+
if (envelope.length < EPH_PK_SIZE2 + NONCE_SUFFIX_SIZE + 16) return null;
|
|
1240
1227
|
if (envelope.length > MAX_SLIP_ENVELOPE_SIZE) return null;
|
|
1241
1228
|
try {
|
|
1242
1229
|
const ephPkPacked = envelope.subarray(0, EPH_PK_SIZE2);
|
|
1243
|
-
const ephPkPoint = (
|
|
1230
|
+
const ephPkPoint = unpackUsableViewingKey(bytesToBigintLE(ephPkPacked));
|
|
1244
1231
|
if (!ephPkPoint) return null;
|
|
1245
1232
|
const ivskScalar = bytesToBjjScalar(recipientIvsk);
|
|
1246
1233
|
const sharedSecret = bigintTo32Le(fastMulPoint(ephPkPoint, ivskScalar)[0]);
|
|
1247
1234
|
const slipKey = deriveSlipKey(sharedSecret);
|
|
1248
|
-
const suffix = envelope.subarray(EPH_PK_SIZE2, EPH_PK_SIZE2 +
|
|
1249
|
-
const sealed = envelope.subarray(EPH_PK_SIZE2 +
|
|
1250
|
-
const cipher = (0,
|
|
1235
|
+
const suffix = envelope.subarray(EPH_PK_SIZE2, EPH_PK_SIZE2 + NONCE_SUFFIX_SIZE);
|
|
1236
|
+
const sealed = envelope.subarray(EPH_PK_SIZE2 + NONCE_SUFFIX_SIZE);
|
|
1237
|
+
const cipher = (0, import_chacha2.chacha20poly1305)(slipKey, buildNonce(suffix));
|
|
1251
1238
|
const plaintext = cipher.decrypt(sealed);
|
|
1252
1239
|
const parsed = JSON.parse(new TextDecoder().decode(plaintext));
|
|
1253
1240
|
if (!isHexOfLength(parsed["commitmentHex"], 32)) return null;
|
|
@@ -1268,7 +1255,7 @@ function openPaymentSlip(recipientIvsk, envelope) {
|
|
|
1268
1255
|
}
|
|
1269
1256
|
var PAYMENT_SLIP_SCHEME = "orbslip1:";
|
|
1270
1257
|
function slipChecksum(payload) {
|
|
1271
|
-
const digest = (0,
|
|
1258
|
+
const digest = (0, import_sha23.sha256)(new TextEncoder().encode(PAYMENT_SLIP_SCHEME + payload));
|
|
1272
1259
|
return toHex(digest.slice(0, 4)).slice(2);
|
|
1273
1260
|
}
|
|
1274
1261
|
function encodePaymentSlip(envelope) {
|
|
@@ -1284,38 +1271,48 @@ function decodePaymentSlip(text) {
|
|
|
1284
1271
|
const checksum = rest.slice(sep + 1);
|
|
1285
1272
|
if (slipChecksum(payload) !== checksum) return null;
|
|
1286
1273
|
try {
|
|
1287
|
-
|
|
1288
|
-
const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
1289
|
-
if (clean.length % 2 !== 0) return null;
|
|
1290
|
-
const bytes = new Uint8Array(clean.length / 2);
|
|
1291
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
1292
|
-
bytes[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
|
|
1293
|
-
}
|
|
1294
|
-
return bytes;
|
|
1274
|
+
return fromHex(base64UrlDecode(payload));
|
|
1295
1275
|
} catch {
|
|
1296
1276
|
return null;
|
|
1297
1277
|
}
|
|
1298
1278
|
}
|
|
1299
1279
|
|
|
1300
1280
|
// src/protocol/eph/selfEph.ts
|
|
1301
|
-
var
|
|
1281
|
+
var import_sha24 = require("@noble/hashes/sha2.js");
|
|
1282
|
+
|
|
1283
|
+
// src/protocol/eph/windowBounds.ts
|
|
1284
|
+
var MAX_EPH_WINDOW = 1 << 20;
|
|
1285
|
+
|
|
1286
|
+
// src/protocol/eph/selfEph.ts
|
|
1302
1287
|
var import_baby_jubjub5 = require("@zk-kit/baby-jubjub");
|
|
1303
|
-
var SELF_EPH_DOMAIN = new TextEncoder().encode("orbinum-self-eph-
|
|
1304
|
-
function deriveSelfEphSk(
|
|
1305
|
-
|
|
1288
|
+
var SELF_EPH_DOMAIN = new TextEncoder().encode("orbinum-self-eph-v3");
|
|
1289
|
+
function deriveSelfEphSk(viewingSecretKey, index) {
|
|
1290
|
+
if (!Number.isInteger(index) || index < 0 || index > 4294967295) {
|
|
1291
|
+
throw new Error(`deriveSelfEphSk: index must be a u32, got ${index}`);
|
|
1292
|
+
}
|
|
1293
|
+
assertSecretKeyBytes(viewingSecretKey, "deriveSelfEphSk: viewingSecretKey");
|
|
1294
|
+
const h = import_sha24.sha256.create();
|
|
1306
1295
|
h.update(SELF_EPH_DOMAIN);
|
|
1307
|
-
h.update(
|
|
1296
|
+
h.update(viewingSecretKey);
|
|
1308
1297
|
const idx = new Uint8Array(4);
|
|
1309
1298
|
new DataView(idx.buffer).setUint32(0, index >>> 0, true);
|
|
1310
1299
|
h.update(idx);
|
|
1311
1300
|
return h.digest();
|
|
1312
1301
|
}
|
|
1313
|
-
function selfEphWindow(
|
|
1302
|
+
function selfEphWindow(viewingSecretKey, ivkPacked, from, count) {
|
|
1314
1303
|
const ivkPoint = (0, import_baby_jubjub5.unpackPoint)(bytesToBigintLE(ivkPacked));
|
|
1315
1304
|
if (!ivkPoint) throw new Error("selfEphWindow: invalid viewing public key");
|
|
1316
1305
|
const entries = [];
|
|
1306
|
+
if (!Number.isInteger(from) || from < 0) {
|
|
1307
|
+
throw new Error(`selfEphWindow: from must be a non-negative integer, got ${from}`);
|
|
1308
|
+
}
|
|
1309
|
+
if (!Number.isInteger(count) || count > MAX_EPH_WINDOW) {
|
|
1310
|
+
throw new Error(
|
|
1311
|
+
`selfEphWindow: count must be an integer <= ${MAX_EPH_WINDOW}, got ${count}`
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1317
1314
|
for (let i = from; i < from + count; i++) {
|
|
1318
|
-
const scalar = bytesToBjjScalar(deriveSelfEphSk(
|
|
1315
|
+
const scalar = bytesToBjjScalar(deriveSelfEphSk(viewingSecretKey, i));
|
|
1319
1316
|
const ephPk = fastMulBase(scalar);
|
|
1320
1317
|
const sharedPoint = fastMulPoint(ivkPoint, scalar);
|
|
1321
1318
|
entries.push({
|
|
@@ -1328,17 +1325,22 @@ function selfEphWindow(spendingKey, ivkPacked, from, count) {
|
|
|
1328
1325
|
}
|
|
1329
1326
|
|
|
1330
1327
|
// src/protocol/eph/pairwiseEph.ts
|
|
1331
|
-
var
|
|
1328
|
+
var import_sha25 = require("@noble/hashes/sha2.js");
|
|
1332
1329
|
var import_baby_jubjub6 = require("@zk-kit/baby-jubjub");
|
|
1333
1330
|
var PAIRWISE_EPH_DOMAIN = new TextEncoder().encode("orbinum-pairwise-eph-v1");
|
|
1334
1331
|
function derivePairwiseSharedSecret(myViewingSk, theirIvkPacked) {
|
|
1335
|
-
const theirPoint = (
|
|
1332
|
+
const theirPoint = unpackUsableViewingKey(bytesToBigintLE(theirIvkPacked));
|
|
1336
1333
|
if (!theirPoint) throw new Error("derivePairwiseSharedSecret: invalid viewing public key");
|
|
1334
|
+
assertSecretKeyBytes(myViewingSk, "derivePairwiseSharedSecret: myViewingSk");
|
|
1337
1335
|
const shared = fastMulPoint(theirPoint, bytesToBjjScalar(myViewingSk));
|
|
1338
1336
|
return bigintTo32Le(shared[0]);
|
|
1339
1337
|
}
|
|
1340
1338
|
function derivePairwiseEphSk(pairSecret, index) {
|
|
1341
|
-
|
|
1339
|
+
if (!Number.isInteger(index) || index < 0 || index > 4294967295) {
|
|
1340
|
+
throw new Error(`derivePairwiseEphSk: index must be a u32, got ${index}`);
|
|
1341
|
+
}
|
|
1342
|
+
assertSecretKeyBytes(pairSecret, "derivePairwiseEphSk: pairSecret");
|
|
1343
|
+
const h = import_sha25.sha256.create();
|
|
1342
1344
|
h.update(PAIRWISE_EPH_DOMAIN);
|
|
1343
1345
|
h.update(pairSecret);
|
|
1344
1346
|
const idx = new Uint8Array(4);
|
|
@@ -1350,6 +1352,14 @@ function pairwiseEphWindow(pairSecret, receiverIvkPacked, from, count) {
|
|
|
1350
1352
|
const ivkPoint = (0, import_baby_jubjub6.unpackPoint)(bytesToBigintLE(receiverIvkPacked));
|
|
1351
1353
|
if (!ivkPoint) throw new Error("pairwiseEphWindow: invalid viewing public key");
|
|
1352
1354
|
const entries = [];
|
|
1355
|
+
if (!Number.isInteger(from) || from < 0) {
|
|
1356
|
+
throw new Error(`pairwiseEphWindow: from must be a non-negative integer, got ${from}`);
|
|
1357
|
+
}
|
|
1358
|
+
if (!Number.isInteger(count) || count > MAX_EPH_WINDOW) {
|
|
1359
|
+
throw new Error(
|
|
1360
|
+
`pairwiseEphWindow: count must be an integer <= ${MAX_EPH_WINDOW}, got ${count}`
|
|
1361
|
+
);
|
|
1362
|
+
}
|
|
1353
1363
|
for (let i = from; i < from + count; i++) {
|
|
1354
1364
|
const scalar = bytesToBjjScalar(derivePairwiseEphSk(pairSecret, i));
|
|
1355
1365
|
const ephPk = fastMulBase(scalar);
|
|
@@ -1363,12 +1373,64 @@ function pairwiseEphWindow(pairSecret, receiverIvkPacked, from, count) {
|
|
|
1363
1373
|
return entries;
|
|
1364
1374
|
}
|
|
1365
1375
|
|
|
1376
|
+
// src/protocol/eph/outgoingEph.ts
|
|
1377
|
+
var import_sha26 = require("@noble/hashes/sha2.js");
|
|
1378
|
+
var import_baby_jubjub7 = require("@zk-kit/baby-jubjub");
|
|
1379
|
+
var OUTGOING_EPH_DOMAIN = new TextEncoder().encode("orbinum-outgoing-eph-v3");
|
|
1380
|
+
function deriveOutgoingEphSk(outgoingViewingKey, index) {
|
|
1381
|
+
if (!Number.isInteger(index) || index < 0 || index > 4294967295) {
|
|
1382
|
+
throw new Error(`deriveOutgoingEphSk: index must be a u32, got ${index}`);
|
|
1383
|
+
}
|
|
1384
|
+
assertSecretKeyBytes(outgoingViewingKey, "deriveOutgoingEphSk: outgoingViewingKey");
|
|
1385
|
+
const h = import_sha26.sha256.create();
|
|
1386
|
+
h.update(OUTGOING_EPH_DOMAIN);
|
|
1387
|
+
h.update(outgoingViewingKey);
|
|
1388
|
+
const idx = new Uint8Array(4);
|
|
1389
|
+
new DataView(idx.buffer).setUint32(0, index >>> 0, true);
|
|
1390
|
+
h.update(idx);
|
|
1391
|
+
return h.digest();
|
|
1392
|
+
}
|
|
1393
|
+
function deriveOutgoingEphPk(outgoingViewingKey, index) {
|
|
1394
|
+
const scalar = bytesToBjjScalar(deriveOutgoingEphSk(outgoingViewingKey, index));
|
|
1395
|
+
return toHex(bigintTo32Le((0, import_baby_jubjub7.packPoint)(fastMulBase(scalar)))).toLowerCase();
|
|
1396
|
+
}
|
|
1397
|
+
function outgoingEphWindow(outgoingViewingKey, from, count) {
|
|
1398
|
+
const entries = [];
|
|
1399
|
+
if (!Number.isInteger(from) || from < 0) {
|
|
1400
|
+
throw new Error(`outgoingEphWindow: from must be a non-negative integer, got ${from}`);
|
|
1401
|
+
}
|
|
1402
|
+
if (!Number.isInteger(count) || count > MAX_EPH_WINDOW) {
|
|
1403
|
+
throw new Error(
|
|
1404
|
+
`outgoingEphWindow: count must be an integer <= ${MAX_EPH_WINDOW}, got ${count}`
|
|
1405
|
+
);
|
|
1406
|
+
}
|
|
1407
|
+
for (let i = from; i < from + count; i++) {
|
|
1408
|
+
entries.push({ index: i, ephPkHex: deriveOutgoingEphPk(outgoingViewingKey, i) });
|
|
1409
|
+
}
|
|
1410
|
+
return entries;
|
|
1411
|
+
}
|
|
1412
|
+
function deriveOutgoingSharedSecret(outgoingViewingKey, index, recipientIvkPacked) {
|
|
1413
|
+
const ivkPoint = (0, import_baby_jubjub7.unpackPoint)(bytesToBigintLE(recipientIvkPacked));
|
|
1414
|
+
if (!ivkPoint) throw new Error("deriveOutgoingSharedSecret: invalid viewing public key");
|
|
1415
|
+
const scalar = bytesToBjjScalar(deriveOutgoingEphSk(outgoingViewingKey, index));
|
|
1416
|
+
return bigintTo32Le(fastMulPoint(ivkPoint, scalar)[0]);
|
|
1417
|
+
}
|
|
1418
|
+
function reconstructOutgoingIndex(outgoingViewingKey, publishedEphPks, window) {
|
|
1419
|
+
const published = /* @__PURE__ */ new Set();
|
|
1420
|
+
for (const hex of publishedEphPks) published.add(hex.toLowerCase());
|
|
1421
|
+
let highest = -1;
|
|
1422
|
+
for (let i = 0; i < window; i++) {
|
|
1423
|
+
if (published.has(deriveOutgoingEphPk(outgoingViewingKey, i))) highest = i;
|
|
1424
|
+
}
|
|
1425
|
+
return highest + 1;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1366
1428
|
// src/protocol/types.ts
|
|
1367
1429
|
var CURRENT_CIRCUIT_VERSION = 1;
|
|
1368
1430
|
|
|
1369
1431
|
// src/protocol/note/NoteBuilder.ts
|
|
1370
|
-
var
|
|
1371
|
-
var
|
|
1432
|
+
var import_baby_jubjub8 = require("@zk-kit/baby-jubjub");
|
|
1433
|
+
var import_utils3 = require("@noble/ciphers/utils.js");
|
|
1372
1434
|
var import_poseidon_lite = require("poseidon-lite");
|
|
1373
1435
|
var NoteBuilder = class {
|
|
1374
1436
|
/**
|
|
@@ -1377,7 +1439,7 @@ var NoteBuilder = class {
|
|
|
1377
1439
|
* @param input.value Amount in planck (required).
|
|
1378
1440
|
* @param input.assetId Asset ID — default 0n (native ORB-Privacy).
|
|
1379
1441
|
* @param input.ownerPk Sender's or recipient's global BabyJubJub Ax — default 0n.
|
|
1380
|
-
* @param input.blinding Random scalar — defaults to
|
|
1442
|
+
* @param input.blinding Random scalar — defaults to a CSPRNG draw.
|
|
1381
1443
|
* @param input.spendingKey Secret key for nullifier — default 0n.
|
|
1382
1444
|
* @param input.viewingPublicKey Recipient's 32-byte LE packed BJJ ivk. Triggers memo encryption.
|
|
1383
1445
|
* @param input.recipientOwnerPk Recipient's global ownerPk. Required with viewingPublicKey
|
|
@@ -1388,7 +1450,7 @@ var NoteBuilder = class {
|
|
|
1388
1450
|
const value = input.value;
|
|
1389
1451
|
const assetId = input.assetId ?? 0n;
|
|
1390
1452
|
const ownerPk = input.ownerPk ?? 0n;
|
|
1391
|
-
const blinding = input.blinding ??
|
|
1453
|
+
const blinding = input.blinding ?? randomBlinding();
|
|
1392
1454
|
const spendingKey = input.spendingKey ?? 0n;
|
|
1393
1455
|
const sourcePk = input.sourcePk ?? 0n;
|
|
1394
1456
|
const circuitVersion = input.circuitVersion ?? CURRENT_CIRCUIT_VERSION;
|
|
@@ -1397,13 +1459,13 @@ var NoteBuilder = class {
|
|
|
1397
1459
|
if (useStealth) {
|
|
1398
1460
|
const recipientOwnerPk = input.recipientOwnerPk;
|
|
1399
1461
|
const recipientIvkPacked = input.viewingPublicKey;
|
|
1400
|
-
const ephSk = input.ephSkOverride ?? (0,
|
|
1462
|
+
const ephSk = input.ephSkOverride ?? (0, import_utils3.randomBytes)(32);
|
|
1401
1463
|
const ivkPackedBigint = bytesToBigintLE(recipientIvkPacked);
|
|
1402
|
-
const ivkPoint = (
|
|
1464
|
+
const ivkPoint = unpackUsableViewingKey(ivkPackedBigint);
|
|
1403
1465
|
if (!ivkPoint)
|
|
1404
1466
|
throw new Error("NoteBuilder.build: invalid recipient viewing public key");
|
|
1405
1467
|
const ephSkScalar = BigInt(toHex(ephSk)) % BABYJUB_SUBORDER || 1n;
|
|
1406
|
-
const sharedPoint = (0,
|
|
1468
|
+
const sharedPoint = (0, import_baby_jubjub8.mulPointEscalar)(ivkPoint, ephSkScalar);
|
|
1407
1469
|
const sharedSecret = bigintTo32Le(sharedPoint[0]);
|
|
1408
1470
|
const recipientPkPoint = recoverOwnerPkPoint(recipientOwnerPk);
|
|
1409
1471
|
if (!recipientPkPoint)
|
|
@@ -1438,21 +1500,6 @@ var NoteBuilder = class {
|
|
|
1438
1500
|
throw new Error(
|
|
1439
1501
|
`NoteBuilder.build: invariant violated \u2014 memo must be ${ENCRYPTED_MEMO_SIZE} bytes, got ${memo.length}`
|
|
1440
1502
|
);
|
|
1441
|
-
let ovkBlob;
|
|
1442
|
-
if (input.outgoingViewingKey !== void 0) {
|
|
1443
|
-
const ephPkBytes = Uint8Array.from(memo.slice(148, 180));
|
|
1444
|
-
const sealed = sealOutgoingBlob(
|
|
1445
|
-
input.outgoingViewingKey,
|
|
1446
|
-
sharedSecret,
|
|
1447
|
-
stealthCommitmentBytes,
|
|
1448
|
-
ephPkBytes
|
|
1449
|
-
);
|
|
1450
|
-
if (sealed.length !== OVK_BLOB_SIZE)
|
|
1451
|
-
throw new Error(
|
|
1452
|
-
`NoteBuilder.build: invariant violated \u2014 ovkBlob must be ${OVK_BLOB_SIZE} bytes, got ${sealed.length}`
|
|
1453
|
-
);
|
|
1454
|
-
ovkBlob = Array.from(sealed);
|
|
1455
|
-
}
|
|
1456
1503
|
return {
|
|
1457
1504
|
value,
|
|
1458
1505
|
assetId,
|
|
@@ -1467,8 +1514,7 @@ var NoteBuilder = class {
|
|
|
1467
1514
|
commitmentHex: toHex(commitmentBytes2),
|
|
1468
1515
|
nullifierHex: toHex(nullifierBytes2),
|
|
1469
1516
|
memo,
|
|
1470
|
-
sourcePk
|
|
1471
|
-
...ovkBlob ? { ovkBlob } : {}
|
|
1517
|
+
sourcePk
|
|
1472
1518
|
};
|
|
1473
1519
|
}
|
|
1474
1520
|
const commitment = (0, import_poseidon_lite.poseidon4)([value, assetId, ownerPk, blinding]);
|
|
@@ -1518,7 +1564,7 @@ var NoteBuilder = class {
|
|
|
1518
1564
|
* @param note The ZkNote whose fields populate the plaintext.
|
|
1519
1565
|
* @param recipientIvkPacked 32-byte LE packed BJJ viewing public key of the recipient.
|
|
1520
1566
|
* Pass `new Uint8Array(32)` (default) for a public/dummy memo.
|
|
1521
|
-
* @param sourcePk
|
|
1567
|
+
* @param sourcePk 32-byte counterparty BabyJubJub Ax.
|
|
1522
1568
|
* Pass `new Uint8Array(32)` (default) for no counterparty.
|
|
1523
1569
|
*/
|
|
1524
1570
|
static buildMemo(note, recipientIvkPacked, sourcePk) {
|
|
@@ -1537,18 +1583,6 @@ var NoteBuilder = class {
|
|
|
1537
1583
|
|
|
1538
1584
|
// src/protocol/note/NoteDecryptor.ts
|
|
1539
1585
|
var import_poseidon_lite2 = require("poseidon-lite");
|
|
1540
|
-
function decryptAndVerifyPlaintext(memoBytes, commitmentBytes, sharedSecret, effectiveOwnerPk) {
|
|
1541
|
-
const plaintext = EncryptedMemo.decryptWithSharedSecret(
|
|
1542
|
-
memoBytes,
|
|
1543
|
-
commitmentBytes,
|
|
1544
|
-
sharedSecret
|
|
1545
|
-
);
|
|
1546
|
-
if (!plaintext) return null;
|
|
1547
|
-
const ownerPk = effectiveOwnerPk ?? plaintext.ownerPk;
|
|
1548
|
-
const recomputed = (0, import_poseidon_lite2.poseidon4)([plaintext.value, plaintext.assetId, ownerPk, plaintext.blinding]);
|
|
1549
|
-
if (recomputed !== bytesToBigintLE(commitmentBytes)) return null;
|
|
1550
|
-
return plaintext;
|
|
1551
|
-
}
|
|
1552
1586
|
function computeNullifier(commitment, spendingKey) {
|
|
1553
1587
|
return (0, import_poseidon_lite2.poseidon2)([commitment, spendingKey]);
|
|
1554
1588
|
}
|
|
@@ -1631,40 +1665,6 @@ function tryDecryptNoteVerbose(commitment, viewingSecretKey, spendingKey, ownOwn
|
|
|
1631
1665
|
}
|
|
1632
1666
|
};
|
|
1633
1667
|
}
|
|
1634
|
-
function tryRecoverOutgoing(hint, ovk, opts) {
|
|
1635
|
-
if (!hint.ovkBlob || !hint.encryptedMemo) return null;
|
|
1636
|
-
let commitmentBytes;
|
|
1637
|
-
let memoBytes;
|
|
1638
|
-
let blobBytes;
|
|
1639
|
-
try {
|
|
1640
|
-
commitmentBytes = fromHex(hint.commitmentHex);
|
|
1641
|
-
memoBytes = fromHex(hint.encryptedMemo);
|
|
1642
|
-
blobBytes = fromHex(hint.ovkBlob);
|
|
1643
|
-
} catch {
|
|
1644
|
-
return null;
|
|
1645
|
-
}
|
|
1646
|
-
if (memoBytes.length !== ENCRYPTED_MEMO_SIZE) return null;
|
|
1647
|
-
if (blobBytes.length !== OVK_BLOB_SIZE) return null;
|
|
1648
|
-
const ephPkBytes = memoBytes.subarray(148, 180);
|
|
1649
|
-
const sharedSecret = openOutgoingBlob(ovk, blobBytes, commitmentBytes, ephPkBytes);
|
|
1650
|
-
if (!sharedSecret) return null;
|
|
1651
|
-
const activation = opts?.viewTagActivationLeaf;
|
|
1652
|
-
if (activation !== void 0 && isValidLeafIndex(hint.leafIndex) && hint.leafIndex >= activation && !EncryptedMemo.checkViewTag(memoBytes, sharedSecret)) {
|
|
1653
|
-
return null;
|
|
1654
|
-
}
|
|
1655
|
-
const plaintext = decryptAndVerifyPlaintext(memoBytes, commitmentBytes, sharedSecret, null);
|
|
1656
|
-
if (!plaintext) return null;
|
|
1657
|
-
return {
|
|
1658
|
-
commitmentHex: hint.commitmentHex,
|
|
1659
|
-
...isValidLeafIndex(hint.leafIndex) ? { leafIndex: hint.leafIndex } : {},
|
|
1660
|
-
value: plaintext.value,
|
|
1661
|
-
assetId: plaintext.assetId,
|
|
1662
|
-
recipientStealthPk: plaintext.ownerPk,
|
|
1663
|
-
blinding: plaintext.blinding,
|
|
1664
|
-
sourcePk: plaintext.sourcePk,
|
|
1665
|
-
circuitVersion: plaintext.circuitVersion
|
|
1666
|
-
};
|
|
1667
|
-
}
|
|
1668
1668
|
function collectOutgoingFacts(hint) {
|
|
1669
1669
|
if (!isHexOfLength(hint.commitmentHex, 32)) return null;
|
|
1670
1670
|
if (!isHexOfLength(hint.encryptedMemo, ENCRYPTED_MEMO_SIZE)) return null;
|
|
@@ -1675,6 +1675,89 @@ function collectOutgoingFacts(hint) {
|
|
|
1675
1675
|
};
|
|
1676
1676
|
}
|
|
1677
1677
|
|
|
1678
|
+
// src/protocol/note/recoverSent.ts
|
|
1679
|
+
function recoverSentFromSharedSecret(hint, outgoingSharedSecret, ephIndex) {
|
|
1680
|
+
if (!isHexOfLength(hint.commitmentHex, 32)) return null;
|
|
1681
|
+
if (!isHexOfLength(hint.encryptedMemo, ENCRYPTED_MEMO_SIZE)) return null;
|
|
1682
|
+
try {
|
|
1683
|
+
const plaintext = EncryptedMemo.decryptWithSharedSecret(
|
|
1684
|
+
fromHex(hint.encryptedMemo),
|
|
1685
|
+
fromHex(hint.commitmentHex),
|
|
1686
|
+
outgoingSharedSecret
|
|
1687
|
+
);
|
|
1688
|
+
if (!plaintext) return null;
|
|
1689
|
+
return {
|
|
1690
|
+
commitmentHex: hint.commitmentHex,
|
|
1691
|
+
...isValidLeafIndex(hint.leafIndex) ? { leafIndex: hint.leafIndex } : {},
|
|
1692
|
+
value: plaintext.value,
|
|
1693
|
+
assetId: plaintext.assetId,
|
|
1694
|
+
// On an outgoing note the memo's ownerPk IS the recipient's stealth
|
|
1695
|
+
// owner — the builder wrote it that way.
|
|
1696
|
+
recipientStealthPk: plaintext.ownerPk,
|
|
1697
|
+
blinding: plaintext.blinding,
|
|
1698
|
+
sourcePk: plaintext.sourcePk,
|
|
1699
|
+
circuitVersion: plaintext.circuitVersion,
|
|
1700
|
+
ephIndex
|
|
1701
|
+
};
|
|
1702
|
+
} catch {
|
|
1703
|
+
return null;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
function recoverSentNote(params) {
|
|
1707
|
+
const { hint, outgoingViewingKey, ephIndex, recipientCandidates } = params;
|
|
1708
|
+
if (!isHexOfLength(hint.commitmentHex, 32)) return null;
|
|
1709
|
+
if (!isHexOfLength(hint.encryptedMemo, ENCRYPTED_MEMO_SIZE)) return null;
|
|
1710
|
+
for (const candidate of recipientCandidates) {
|
|
1711
|
+
let sharedSecret;
|
|
1712
|
+
try {
|
|
1713
|
+
sharedSecret = deriveOutgoingSharedSecret(outgoingViewingKey, ephIndex, candidate);
|
|
1714
|
+
} catch {
|
|
1715
|
+
continue;
|
|
1716
|
+
}
|
|
1717
|
+
const facts = recoverSentFromSharedSecret(hint, sharedSecret, ephIndex);
|
|
1718
|
+
if (facts) return { ...facts, recipientIvk: candidate };
|
|
1719
|
+
}
|
|
1720
|
+
return null;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
// src/protocol/note/recipientBook.ts
|
|
1724
|
+
var import_sha27 = require("@noble/hashes/sha2.js");
|
|
1725
|
+
var RECIPIENT_BOOK_DOMAIN = new TextEncoder().encode("orbinum-recipient-book-v3");
|
|
1726
|
+
var IVK_SIZE = 32;
|
|
1727
|
+
function bookKeystream(outgoingViewingKey, paymentCommitmentHex) {
|
|
1728
|
+
if (outgoingViewingKey.length !== 32) {
|
|
1729
|
+
throw new Error(
|
|
1730
|
+
`recipientBook: outgoingViewingKey must be 32 bytes, got ${outgoingViewingKey.length}`
|
|
1731
|
+
);
|
|
1732
|
+
}
|
|
1733
|
+
if (outgoingViewingKey.every((b) => b === 0)) {
|
|
1734
|
+
throw new Error(
|
|
1735
|
+
"recipientBook: outgoingViewingKey is all zeros \u2014 the book would be public"
|
|
1736
|
+
);
|
|
1737
|
+
}
|
|
1738
|
+
const normalised = paymentCommitmentHex.toLowerCase().replace(/^0x/, "");
|
|
1739
|
+
const h = import_sha27.sha256.create();
|
|
1740
|
+
h.update(RECIPIENT_BOOK_DOMAIN);
|
|
1741
|
+
h.update(outgoingViewingKey);
|
|
1742
|
+
h.update(new TextEncoder().encode(normalised));
|
|
1743
|
+
return h.digest();
|
|
1744
|
+
}
|
|
1745
|
+
function sealRecipientBookEntry(ivkPacked, outgoingViewingKey, paymentCommitmentHex) {
|
|
1746
|
+
if (ivkPacked.length !== IVK_SIZE) return ivkPacked;
|
|
1747
|
+
const keystream = bookKeystream(outgoingViewingKey, paymentCommitmentHex);
|
|
1748
|
+
const out = new Uint8Array(IVK_SIZE);
|
|
1749
|
+
for (let i = 0; i < IVK_SIZE; i++) out[i] = ivkPacked[i] ^ keystream[i];
|
|
1750
|
+
return out;
|
|
1751
|
+
}
|
|
1752
|
+
function openRecipientBookEntry(sealedSourcePk, outgoingViewingKey, paymentCommitmentHex) {
|
|
1753
|
+
if (sealedSourcePk < 0n || sealedSourcePk >> 256n !== 0n) return new Uint8Array(IVK_SIZE);
|
|
1754
|
+
return sealRecipientBookEntry(
|
|
1755
|
+
bigintTo32Le(sealedSourcePk),
|
|
1756
|
+
outgoingViewingKey,
|
|
1757
|
+
paymentCommitmentHex
|
|
1758
|
+
);
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1678
1761
|
// src/protocol/note/NoteDisclosure.ts
|
|
1679
1762
|
var import_poseidon_lite3 = require("poseidon-lite");
|
|
1680
1763
|
var PREFIX = "orbdisc:";
|
|
@@ -1819,11 +1902,10 @@ ${canonicalAccountId(address)}`;
|
|
|
1819
1902
|
}
|
|
1820
1903
|
|
|
1821
1904
|
// src/protocol/keys/PrivacyKeys.ts
|
|
1822
|
-
var
|
|
1823
|
-
var
|
|
1824
|
-
var
|
|
1905
|
+
var import_hkdf3 = require("@noble/hashes/hkdf.js");
|
|
1906
|
+
var import_sha28 = require("@noble/hashes/sha2.js");
|
|
1907
|
+
var import_baby_jubjub9 = require("@zk-kit/baby-jubjub");
|
|
1825
1908
|
var IVK_DOMAIN = new TextEncoder().encode("orbinum-ivk-v1");
|
|
1826
|
-
var OVK_DOMAIN = new TextEncoder().encode("orbinum-ovk-v1");
|
|
1827
1909
|
var KEY_VERSION = "v2";
|
|
1828
1910
|
function deriveSpendingKeyFromMaster(masterBytes) {
|
|
1829
1911
|
const skBigint = BigInt(toHex(masterBytes)) % BABYJUB_SUBORDER;
|
|
@@ -1831,12 +1913,26 @@ function deriveSpendingKeyFromMaster(masterBytes) {
|
|
|
1831
1913
|
}
|
|
1832
1914
|
function deriveViewingSecretKey(spendingKey) {
|
|
1833
1915
|
const ikm = bigintTo32Le(spendingKey);
|
|
1834
|
-
return (0,
|
|
1916
|
+
return (0, import_hkdf3.hkdf)(import_sha28.sha256, ikm, void 0, IVK_DOMAIN, 32);
|
|
1917
|
+
}
|
|
1918
|
+
var SPEND_V3_DOMAIN = new TextEncoder().encode("orbinum-spend-v3");
|
|
1919
|
+
var IVK_V3_DOMAIN = new TextEncoder().encode("orbinum-ivk-v3");
|
|
1920
|
+
var OVK_V3_DOMAIN = new TextEncoder().encode("orbinum-ovk-v3");
|
|
1921
|
+
var deriveBranch = (rootSecret, domain) => (0, import_hkdf3.hkdf)(import_sha28.sha256, rootSecret, void 0, domain, 32);
|
|
1922
|
+
function deriveSpendingKeyV3(rootSecret) {
|
|
1923
|
+
const scalar = BigInt(toHex(deriveBranch(rootSecret, SPEND_V3_DOMAIN))) % BABYJUB_SUBORDER;
|
|
1924
|
+
return scalar === 0n ? 1n : scalar;
|
|
1925
|
+
}
|
|
1926
|
+
function deriveViewingSecretKeyV3(rootSecret) {
|
|
1927
|
+
return deriveBranch(rootSecret, IVK_V3_DOMAIN);
|
|
1928
|
+
}
|
|
1929
|
+
function deriveOutgoingViewingKeyV3(rootSecret) {
|
|
1930
|
+
return deriveBranch(rootSecret, OVK_V3_DOMAIN);
|
|
1835
1931
|
}
|
|
1836
1932
|
function deriveViewingPublicKey(ivsk) {
|
|
1837
1933
|
const ivskScalar = BigInt(toHex(ivsk)) % BABYJUB_SUBORDER || 1n;
|
|
1838
1934
|
const ivkPoint = fastMulBase(ivskScalar);
|
|
1839
|
-
const packed = (0,
|
|
1935
|
+
const packed = (0, import_baby_jubjub9.packPoint)(ivkPoint);
|
|
1840
1936
|
return bigintTo32Le(packed);
|
|
1841
1937
|
}
|
|
1842
1938
|
function deriveOwnerPk(spendingKey) {
|
|
@@ -1847,15 +1943,21 @@ function deriveOwnerPk(spendingKey) {
|
|
|
1847
1943
|
return 0n;
|
|
1848
1944
|
}
|
|
1849
1945
|
}
|
|
1850
|
-
function deriveOutgoingViewingKey(masterBytes) {
|
|
1851
|
-
return (0, import_hkdf4.hkdf)(import_sha27.sha256, masterBytes, void 0, OVK_DOMAIN, 32);
|
|
1852
|
-
}
|
|
1853
1946
|
|
|
1854
1947
|
// src/protocol/keys/PrivacyKeyManager.ts
|
|
1855
|
-
var
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
const
|
|
1948
|
+
var import_sha29 = require("@noble/hashes/sha2.js");
|
|
1949
|
+
var SCHEME_BY_VERSION = { v2: "orbpriv2", v3: "orbpriv3" };
|
|
1950
|
+
function usableAddressFields(ownerPkHex, ivkHex) {
|
|
1951
|
+
const ownerPk = ownerPkHex.toLowerCase();
|
|
1952
|
+
const ivk = ivkHex.toLowerCase();
|
|
1953
|
+
if (!isHexOfLength(ownerPk, 32) || !isHexOfLength(ivk, 32)) return null;
|
|
1954
|
+
if (/^0x0+$/.test(ownerPk) || /^0x0+$/.test(ivk)) return null;
|
|
1955
|
+
if (!unpackUsableViewingKey(bytesToBigintLE(fromHex(ivk)))) return null;
|
|
1956
|
+
return { ownerPkHex: ownerPk, viewingPublicKeyHex: ivk };
|
|
1957
|
+
}
|
|
1958
|
+
function privacyAddressChecksum(scheme, ownerPkHex, ivkHex) {
|
|
1959
|
+
const body = `${scheme}:${ownerPkHex}:${ivkHex}`;
|
|
1960
|
+
const digest = (0, import_sha29.sha256)(new TextEncoder().encode(body));
|
|
1859
1961
|
return toHex(digest.slice(0, 4)).slice(2);
|
|
1860
1962
|
}
|
|
1861
1963
|
var PrivacyKeyManager = class {
|
|
@@ -1864,30 +1966,29 @@ var PrivacyKeyManager = class {
|
|
|
1864
1966
|
masterBytes: null,
|
|
1865
1967
|
viewingSecretKey: null,
|
|
1866
1968
|
viewingPublicKeyPacked: null,
|
|
1867
|
-
ownerPk: null
|
|
1868
|
-
outgoingViewingKey: null
|
|
1969
|
+
ownerPk: null
|
|
1869
1970
|
};
|
|
1870
1971
|
/**
|
|
1871
1972
|
* Load a spending key and its corresponding master bytes into the in-memory session.
|
|
1872
1973
|
* Derives viewingSecretKey, viewingPublicKeyPacked, and ownerPk immediately.
|
|
1873
1974
|
* Replaces any previously loaded key.
|
|
1874
1975
|
*
|
|
1875
|
-
* @param
|
|
1876
|
-
*
|
|
1877
|
-
*
|
|
1976
|
+
* @param _legacySpendingKey IGNORED. Kept so v2-era callers still compile;
|
|
1977
|
+
* the scalar is re-derived from `masterBytes`.
|
|
1978
|
+
* @param masterBytes Raw 32-byte HKDF output. The root every v3 branch and
|
|
1979
|
+
* the stable vault key hang off.
|
|
1878
1980
|
*/
|
|
1879
|
-
async load(
|
|
1880
|
-
const
|
|
1981
|
+
async load(_legacySpendingKey, masterBytes) {
|
|
1982
|
+
const spendingKey = deriveSpendingKeyV3(masterBytes);
|
|
1983
|
+
const viewingSecretKey = deriveViewingSecretKeyV3(masterBytes);
|
|
1881
1984
|
const viewingPublicKeyPacked = deriveViewingPublicKey(viewingSecretKey);
|
|
1882
1985
|
const ownerPk = deriveOwnerPk(spendingKey);
|
|
1883
|
-
const outgoingViewingKey = deriveOutgoingViewingKey(masterBytes);
|
|
1884
1986
|
this._state = {
|
|
1885
1987
|
spendingKey,
|
|
1886
1988
|
masterBytes,
|
|
1887
1989
|
viewingSecretKey,
|
|
1888
1990
|
viewingPublicKeyPacked,
|
|
1889
|
-
ownerPk
|
|
1890
|
-
outgoingViewingKey
|
|
1991
|
+
ownerPk
|
|
1891
1992
|
};
|
|
1892
1993
|
}
|
|
1893
1994
|
/** Clear all key material from memory. Call on vault lock / sign-out. */
|
|
@@ -1897,8 +1998,7 @@ var PrivacyKeyManager = class {
|
|
|
1897
1998
|
masterBytes: null,
|
|
1898
1999
|
viewingSecretKey: null,
|
|
1899
2000
|
viewingPublicKeyPacked: null,
|
|
1900
|
-
ownerPk: null
|
|
1901
|
-
outgoingViewingKey: null
|
|
2001
|
+
ownerPk: null
|
|
1902
2002
|
};
|
|
1903
2003
|
}
|
|
1904
2004
|
/** Returns true if a spending key has been loaded. */
|
|
@@ -1936,6 +2036,24 @@ var PrivacyKeyManager = class {
|
|
|
1936
2036
|
}
|
|
1937
2037
|
return this._state.viewingPublicKeyPacked;
|
|
1938
2038
|
}
|
|
2039
|
+
/**
|
|
2040
|
+
* The OUTGOING viewing key (ovk) — reads what this wallet SENT.
|
|
2041
|
+
*
|
|
2042
|
+
* Seeds the outgoing ephemeral sequence and opens the recipient book, so a
|
|
2043
|
+
* holder can enumerate every payment the wallet made and name who received
|
|
2044
|
+
* it. That makes it strictly more sensitive than the incoming viewing key,
|
|
2045
|
+
* which yields amounts and no counterparties: hand it out only where the
|
|
2046
|
+
* payment graph is meant to travel with it.
|
|
2047
|
+
*
|
|
2048
|
+
* A scan without it still finds every note the wallet OWNS and simply
|
|
2049
|
+
* reconstructs no sent history — the watch-only case.
|
|
2050
|
+
*
|
|
2051
|
+
* SECURITY: never expose this in an address or a network request.
|
|
2052
|
+
* Throws if not loaded.
|
|
2053
|
+
*/
|
|
2054
|
+
getOutgoingViewingKey() {
|
|
2055
|
+
return deriveOutgoingViewingKeyV3(this.getMasterBytes());
|
|
2056
|
+
}
|
|
1939
2057
|
/** Returns the BabyJubJub owner public key (x-coordinate). Throws if not loaded. */
|
|
1940
2058
|
getOwnerPk() {
|
|
1941
2059
|
if (this._state.ownerPk === null) {
|
|
@@ -1943,16 +2061,6 @@ var PrivacyKeyManager = class {
|
|
|
1943
2061
|
}
|
|
1944
2062
|
return this._state.ownerPk;
|
|
1945
2063
|
}
|
|
1946
|
-
/**
|
|
1947
|
-
* Returns the 32-byte outgoing viewing key (ovk). Throws if not loaded.
|
|
1948
|
-
* Used to seal/open the outgoing blob that lets the sender recover a transfer.
|
|
1949
|
-
*/
|
|
1950
|
-
getOutgoingViewingKey() {
|
|
1951
|
-
if (this._state.outgoingViewingKey === null) {
|
|
1952
|
-
throw new Error("PrivacyKeyManager: no key loaded. Call load() first.");
|
|
1953
|
-
}
|
|
1954
|
-
return this._state.outgoingViewingKey;
|
|
1955
|
-
}
|
|
1956
2064
|
/** Returns the spending key as a 32-byte little-endian Uint8Array. Throws if not loaded. */
|
|
1957
2065
|
getSpendingKeyBytes() {
|
|
1958
2066
|
return bigintTo32Le(this.getSpendingKey());
|
|
@@ -1981,7 +2089,7 @@ var PrivacyKeyManager = class {
|
|
|
1981
2089
|
* Exports a shareable privacy address encoding the owner public key and
|
|
1982
2090
|
* viewing PUBLIC key of the currently loaded identity.
|
|
1983
2091
|
*
|
|
1984
|
-
* Format: `
|
|
2092
|
+
* Format: `orbpriv3:{ownerPk_hex}:{viewingPublicKey_hex}:{checksum}`
|
|
1985
2093
|
*
|
|
1986
2094
|
* The recipient uses this address so the sender can:
|
|
1987
2095
|
* 1. Embed `ownerPk` in the note commitment (Poseidon4 input).
|
|
@@ -1996,44 +2104,64 @@ var PrivacyKeyManager = class {
|
|
|
1996
2104
|
*
|
|
1997
2105
|
* Throws if no key is loaded.
|
|
1998
2106
|
*/
|
|
1999
|
-
encodePrivacyAddress() {
|
|
2107
|
+
encodePrivacyAddress(version = "v3") {
|
|
2000
2108
|
const ownerPk = this.getOwnerPk();
|
|
2001
2109
|
const ivkPacked = this.getViewingPublicKeyPacked();
|
|
2002
2110
|
const ownerPkHex = scalarToHex(ownerPk);
|
|
2003
2111
|
const ivkHex = toHex(ivkPacked);
|
|
2004
|
-
const
|
|
2005
|
-
|
|
2112
|
+
const scheme = SCHEME_BY_VERSION[version];
|
|
2113
|
+
const checksum = privacyAddressChecksum(scheme, ownerPkHex, ivkHex);
|
|
2114
|
+
return `${scheme}:${ownerPkHex}:${ivkHex}:${checksum}`;
|
|
2006
2115
|
}
|
|
2007
2116
|
/**
|
|
2008
2117
|
* Decode a privacy address into `{ ownerPkHex, viewingPublicKeyHex }`, or
|
|
2009
2118
|
* `null` if it does not parse.
|
|
2010
2119
|
*
|
|
2011
|
-
*
|
|
2012
|
-
*
|
|
2013
|
-
*
|
|
2014
|
-
*
|
|
2015
|
-
*
|
|
2120
|
+
* Reads every scheme ever emitted; only `orbpriv3` is written.
|
|
2121
|
+
*
|
|
2122
|
+
* - `orbpriv3` / `orbpriv2` — checksum verified; a mismatch (corrupted
|
|
2123
|
+
* paste, or a relabelled prefix) returns null.
|
|
2124
|
+
* - `orbpriv1` — legacy, predates the checksum entirely. The FIELDS are
|
|
2125
|
+
* validated exactly as above, since this branch has no integrity check
|
|
2126
|
+
* of its own.
|
|
2127
|
+
*
|
|
2128
|
+
* Reading an old address is not the same as supporting an old identity. An
|
|
2129
|
+
* address is an owner key and a viewing key; paying one works whatever
|
|
2130
|
+
* scheme derived it, and whoever shares it may not have upgraded yet.
|
|
2131
|
+
* Refusing them here would break incoming payments for no gain.
|
|
2016
2132
|
*/
|
|
2017
2133
|
static decodePrivacyAddress(address) {
|
|
2018
2134
|
const parts = address.split(":");
|
|
2019
|
-
|
|
2135
|
+
for (const version of ["v2", "v3"]) {
|
|
2136
|
+
if (parts[0] !== SCHEME_BY_VERSION[version]) continue;
|
|
2020
2137
|
if (parts.length !== 4) return null;
|
|
2021
2138
|
const [, ownerPkHex, viewingPublicKeyHex, checksum] = parts;
|
|
2022
2139
|
if (!ownerPkHex || !viewingPublicKeyHex || !checksum) return null;
|
|
2023
|
-
if (privacyAddressChecksum(
|
|
2024
|
-
|
|
2140
|
+
if (privacyAddressChecksum(
|
|
2141
|
+
SCHEME_BY_VERSION[version],
|
|
2142
|
+
ownerPkHex,
|
|
2143
|
+
viewingPublicKeyHex
|
|
2144
|
+
) !== checksum) {
|
|
2145
|
+
return null;
|
|
2146
|
+
}
|
|
2147
|
+
const fields = usableAddressFields(ownerPkHex, viewingPublicKeyHex);
|
|
2148
|
+
return fields ? { ...fields, scheme: SCHEME_BY_VERSION[version] } : null;
|
|
2025
2149
|
}
|
|
2026
2150
|
if (parts[0] === "orbpriv1") {
|
|
2027
2151
|
if (parts.length !== 3) return null;
|
|
2028
2152
|
const [, ownerPkHex, viewingPublicKeyHex] = parts;
|
|
2029
2153
|
if (!ownerPkHex || !viewingPublicKeyHex) return null;
|
|
2030
|
-
|
|
2154
|
+
const fields = usableAddressFields(ownerPkHex, viewingPublicKeyHex);
|
|
2155
|
+
return fields ? { ...fields, scheme: "orbpriv1" } : null;
|
|
2031
2156
|
}
|
|
2032
2157
|
return null;
|
|
2033
2158
|
}
|
|
2034
2159
|
/**
|
|
2035
2160
|
* Load keys from a cached "mk:0x{masterBytes_hex}" string produced by exportHex().
|
|
2036
|
-
*
|
|
2161
|
+
*
|
|
2162
|
+
* Throws on a bad prefix, on non-hex content, and on a master that is not
|
|
2163
|
+
* 32 usable bytes — an all-zero one included, since every wallet would
|
|
2164
|
+
* derive the same identity from it.
|
|
2037
2165
|
*/
|
|
2038
2166
|
async importFromHex(hex) {
|
|
2039
2167
|
if (!hex.startsWith("mk:")) {
|
|
@@ -2041,21 +2169,15 @@ var PrivacyKeyManager = class {
|
|
|
2041
2169
|
'PrivacyKeyManager: invalid cache format. Expected "mk:0x{masterBytes_hex}".'
|
|
2042
2170
|
);
|
|
2043
2171
|
}
|
|
2044
|
-
const
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
if (masterBytes.length !== 32) {
|
|
2048
|
-
throw new Error("PrivacyKeyManager: invalid master bytes \u2014 expected 32 bytes.");
|
|
2049
|
-
}
|
|
2050
|
-
const masterBigint = BigInt("0x" + h);
|
|
2051
|
-
const sk = masterBigint % BABYJUB_SUBORDER || 1n;
|
|
2052
|
-
await this.load(sk, masterBytes);
|
|
2172
|
+
const masterBytes = fromHex(hex.slice(3));
|
|
2173
|
+
assertSecretKeyBytes(masterBytes, "PrivacyKeyManager: master");
|
|
2174
|
+
await this.load(0n, masterBytes);
|
|
2053
2175
|
}
|
|
2054
2176
|
};
|
|
2055
2177
|
|
|
2056
2178
|
// src/protocol/keys/spendingKeyDerivation.ts
|
|
2057
|
-
var
|
|
2058
|
-
var
|
|
2179
|
+
var import_hkdf4 = require("@noble/hashes/hkdf.js");
|
|
2180
|
+
var import_sha210 = require("@noble/hashes/sha2.js");
|
|
2059
2181
|
var MIN_SIGNATURE_BYTES = 32;
|
|
2060
2182
|
var MIN_DISTINCT_BYTES = 8;
|
|
2061
2183
|
function assertUsableSignature(sigBytes) {
|
|
@@ -2077,7 +2199,7 @@ async function deriveMasterKeyBytes(signatureHex, chainId, address) {
|
|
|
2077
2199
|
const info = new TextEncoder().encode(
|
|
2078
2200
|
`orbinum-sk-${KEY_VERSION}:${chainId}:${canonicalAccountId(address)}`
|
|
2079
2201
|
);
|
|
2080
|
-
return (0,
|
|
2202
|
+
return (0, import_hkdf4.hkdf)(import_sha210.sha256, sigBytes, new Uint8Array(0), info, 32);
|
|
2081
2203
|
}
|
|
2082
2204
|
async function deriveSpendingKeyFromSignature(signatureHex, chainId, address) {
|
|
2083
2205
|
return deriveSpendingKeyFromMaster(await deriveMasterKeyBytes(signatureHex, chainId, address));
|
|
@@ -2085,8 +2207,8 @@ async function deriveSpendingKeyFromSignature(signatureHex, chainId, address) {
|
|
|
2085
2207
|
|
|
2086
2208
|
// src/protocol/proving/unshield.ts
|
|
2087
2209
|
var import_proof_generator = require("@orbinum/proof-generator");
|
|
2088
|
-
var
|
|
2089
|
-
var
|
|
2210
|
+
var import_utils4 = require("@noble/ciphers/utils.js");
|
|
2211
|
+
var import_baby_jubjub10 = require("@zk-kit/baby-jubjub");
|
|
2090
2212
|
var import_poseidon_lite4 = require("poseidon-lite");
|
|
2091
2213
|
|
|
2092
2214
|
// src/protocol/proving/merkle.ts
|
|
@@ -2118,8 +2240,8 @@ async function generateUnshieldProof(inputs, options = {}) {
|
|
|
2118
2240
|
if (changeValue < 0n) {
|
|
2119
2241
|
throw new Error("changeValue must be >= 0.");
|
|
2120
2242
|
}
|
|
2121
|
-
const changeOwnerPubkey = inputs.changeOwnerPubkey ?? (0,
|
|
2122
|
-
const changeBlinding = inputs.changeBlinding ?? (changeValue > 0n ? bytesToBigintLE((0,
|
|
2243
|
+
const changeOwnerPubkey = inputs.changeOwnerPubkey ?? (0, import_baby_jubjub10.mulPointEscalar)(import_baby_jubjub10.Base8, inputs.spendingKey)[0];
|
|
2244
|
+
const changeBlinding = inputs.changeBlinding ?? (changeValue > 0n ? bytesToBigintLE((0, import_utils4.randomBytes)(32)) : 0n);
|
|
2123
2245
|
const changeCommitment = changeValue > 0n ? (0, import_poseidon_lite4.poseidon4)([changeValue, inputs.assetId, changeOwnerPubkey, changeBlinding]) : 0n;
|
|
2124
2246
|
const circuitInputs = {
|
|
2125
2247
|
merkle_root: leHexToBigint(inputs.merkleRoot).toString(),
|
|
@@ -2355,6 +2477,25 @@ function wsUrlToHttp(wsUrl) {
|
|
|
2355
2477
|
}
|
|
2356
2478
|
|
|
2357
2479
|
// src/chain/substrate/SubstrateClient.ts
|
|
2480
|
+
var TIMESTAMP_PALLET = 1;
|
|
2481
|
+
function decodeCompact(bytes, offset) {
|
|
2482
|
+
const first = bytes[offset];
|
|
2483
|
+
if (first === void 0) return null;
|
|
2484
|
+
const mode = first & 3;
|
|
2485
|
+
if (mode === 0) return first >>> 2;
|
|
2486
|
+
if (mode === 1) {
|
|
2487
|
+
const b1 = bytes[offset + 1];
|
|
2488
|
+
return b1 === void 0 ? null : (first >>> 2 | b1 << 6) >>> 0;
|
|
2489
|
+
}
|
|
2490
|
+
const width = mode === 2 ? 4 : (first >>> 2) + 5;
|
|
2491
|
+
if (offset + width > bytes.length) return null;
|
|
2492
|
+
let value = 0n;
|
|
2493
|
+
const start = mode === 2 ? offset : offset + 1;
|
|
2494
|
+
const end = mode === 2 ? offset + 4 : offset + width;
|
|
2495
|
+
for (let i = end - 1; i >= start; i--) value = value << 8n | BigInt(bytes[i]);
|
|
2496
|
+
if (mode === 2) value >>= 2n;
|
|
2497
|
+
return value > BigInt(Number.MAX_SAFE_INTEGER) ? null : Number(value);
|
|
2498
|
+
}
|
|
2358
2499
|
var SubstrateClient = class _SubstrateClient {
|
|
2359
2500
|
constructor(_papi, _httpUrl, _owned) {
|
|
2360
2501
|
this._papi = _papi;
|
|
@@ -2552,25 +2693,17 @@ var SubstrateClient = class _SubstrateClient {
|
|
|
2552
2693
|
}
|
|
2553
2694
|
}
|
|
2554
2695
|
if (!timestampMs) {
|
|
2555
|
-
const
|
|
2696
|
+
const ts = extrinsics.reduce((found, hex) => {
|
|
2697
|
+
if (found !== null) return found;
|
|
2556
2698
|
try {
|
|
2557
2699
|
const b = fromHex(hex);
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
return false;
|
|
2561
|
-
}
|
|
2562
|
-
});
|
|
2563
|
-
if (tsHex) {
|
|
2564
|
-
try {
|
|
2565
|
-
const b = fromHex(tsHex);
|
|
2566
|
-
const view = new DataView(b.buffer, b.byteOffset + 6, 8);
|
|
2567
|
-
const lo = view.getUint32(0, true);
|
|
2568
|
-
const hi = view.getUint32(4, true);
|
|
2569
|
-
const ms = lo + hi * 4294967296;
|
|
2570
|
-
if (ms > 0) timestampMs = ms;
|
|
2700
|
+
if (b.length < 5 || b[2] !== TIMESTAMP_PALLET || b[3] !== 0) return null;
|
|
2701
|
+
return decodeCompact(b, 4);
|
|
2571
2702
|
} catch {
|
|
2703
|
+
return null;
|
|
2572
2704
|
}
|
|
2573
|
-
}
|
|
2705
|
+
}, null);
|
|
2706
|
+
if (ts !== null && ts > 0) timestampMs = ts;
|
|
2574
2707
|
}
|
|
2575
2708
|
const author = _SubstrateClient.extractAuthorFromLogs(header.digest.logs, ss58Prefix);
|
|
2576
2709
|
return { header, extrinsics, timestampMs, author };
|
|
@@ -2840,7 +2973,10 @@ var EvmClient = class {
|
|
|
2840
2973
|
}
|
|
2841
2974
|
/**
|
|
2842
2975
|
* Performs multiple JSON-RPC calls in a single HTTP request (batch).
|
|
2843
|
-
*
|
|
2976
|
+
*
|
|
2977
|
+
* Results come back in the order of `calls`, as a typed tuple. A call the
|
|
2978
|
+
* server answered with an error, or did not answer at all, lands as `null`
|
|
2979
|
+
* in its own slot — never shifting the ones after it.
|
|
2844
2980
|
*/
|
|
2845
2981
|
async batchRequest(calls) {
|
|
2846
2982
|
const body = calls.map((c, i) => ({
|
|
@@ -2852,8 +2988,8 @@ var EvmClient = class {
|
|
|
2852
2988
|
const res = await postJsonWithRetry(this.rpcUrl, JSON.stringify(body));
|
|
2853
2989
|
if (!res.ok) throw new Error(`EVM HTTP ${res.status}: ${res.statusText}`);
|
|
2854
2990
|
const arr = await res.json();
|
|
2855
|
-
arr.
|
|
2856
|
-
return
|
|
2991
|
+
const byId = new Map(arr.map((r) => [r.id, r]));
|
|
2992
|
+
return calls.map((_, i) => byId.get(i + 1)?.result ?? null);
|
|
2857
2993
|
}
|
|
2858
2994
|
// ─── Convenience wrappers ─────────────────────────────────────────────────
|
|
2859
2995
|
/** Returns the native token balance (in wei) for an EVM address. */
|
|
@@ -2899,7 +3035,10 @@ var EvmClient = class {
|
|
|
2899
3035
|
if (from) txObj["from"] = from;
|
|
2900
3036
|
return this.request("eth_call", [txObj, "latest"]);
|
|
2901
3037
|
}
|
|
2902
|
-
/**
|
|
3038
|
+
/**
|
|
3039
|
+
* Estimates a transaction's gas, in GAS UNITS — not wei. Multiply by
|
|
3040
|
+
* `getGasPrice()` for a cost.
|
|
3041
|
+
*/
|
|
2903
3042
|
async estimateGas(params) {
|
|
2904
3043
|
const hex = await this.request("eth_estimateGas", [params]);
|
|
2905
3044
|
return hexToBigint(hex);
|
|
@@ -3150,7 +3289,7 @@ var EvmExplorer = class _EvmExplorer {
|
|
|
3150
3289
|
// --- Address ---
|
|
3151
3290
|
/**
|
|
3152
3291
|
* Returns aggregated on-chain data for an EVM address: balance, nonce,
|
|
3153
|
-
* bytecode (truncated), and up to 50
|
|
3292
|
+
* bytecode (truncated), and up to 50 logs it EMITTED in the last 1 001 blocks.
|
|
3154
3293
|
*/
|
|
3155
3294
|
async getAddressInfo(address) {
|
|
3156
3295
|
const latest = await this.evm.getBlockNumber().catch(() => 0);
|
|
@@ -3233,7 +3372,7 @@ var EvmExplorer = class _EvmExplorer {
|
|
|
3233
3372
|
};
|
|
3234
3373
|
}
|
|
3235
3374
|
/**
|
|
3236
|
-
* Returns ERC-20 `Transfer` events for `address` from the last 1
|
|
3375
|
+
* Returns ERC-20 `Transfer` events for `address` from the last 1 001 blocks.
|
|
3237
3376
|
* When `holderAddress` is provided, restricts results to transfers sent or received by that address.
|
|
3238
3377
|
*/
|
|
3239
3378
|
async getTokenTransfers(address, holderAddress) {
|
|
@@ -3460,7 +3599,11 @@ var ShieldedPoolModule = class {
|
|
|
3460
3599
|
* Withdraws tokens from the shielded pool to a public address.
|
|
3461
3600
|
* Submits as an UNSIGNED (gasless) transaction — fee is embedded in the ZK proof.
|
|
3462
3601
|
* Pass a `signer` to fall back to signed submission (e.g. for testing).
|
|
3463
|
-
* Extrinsic: shieldedPool.unshield(proof, merkleRoot, nullifier, assetId, amount, recipient, fee, changeCommitment, changeEncryptedMemo,
|
|
3602
|
+
* Extrinsic: shieldedPool.unshield(proof, merkleRoot, nullifier, assetId, amount, recipient, fee, changeCommitment, changeEncryptedMemo, circuitVersion)
|
|
3603
|
+
*
|
|
3604
|
+
* The relay fee recipient is NOT a parameter: the chain takes it from the
|
|
3605
|
+
* dispatch origin. Submitting unsigned credits the block author; submitting
|
|
3606
|
+
* through the EVM precompile credits whoever signed that transaction.
|
|
3464
3607
|
*/
|
|
3465
3608
|
async unshield(params, signer, options) {
|
|
3466
3609
|
const entry = resolveTx(this.substrate.unsafe, "ShieldedPool", "unshield");
|
|
@@ -3484,8 +3627,6 @@ var ShieldedPoolModule = class {
|
|
|
3484
3627
|
fee: params.fee ?? 0n,
|
|
3485
3628
|
change_commitment: changeCommitment,
|
|
3486
3629
|
change_encrypted_memo: changeEncryptedMemo,
|
|
3487
|
-
relayer: void 0,
|
|
3488
|
-
// Option<H160> — None for direct Substrate submissions
|
|
3489
3630
|
circuit_version: params.circuitVersion
|
|
3490
3631
|
});
|
|
3491
3632
|
if (signer) {
|
|
@@ -3497,7 +3638,11 @@ var ShieldedPoolModule = class {
|
|
|
3497
3638
|
* Performs a private (shielded) transfer between two notes.
|
|
3498
3639
|
* Submits as an UNSIGNED (gasless) transaction — fee is embedded in the ZK proof.
|
|
3499
3640
|
* Pass a `signer` to fall back to signed submission (e.g. for testing).
|
|
3500
|
-
* Extrinsic: shieldedPool.privateTransfer(proof, merkleRoot, nullifiers, commitments, memos, assetId, fee,
|
|
3641
|
+
* Extrinsic: shieldedPool.privateTransfer(proof, merkleRoot, nullifiers, commitments, memos, assetId, fee, circuitVersion)
|
|
3642
|
+
*
|
|
3643
|
+
* The relay fee recipient is NOT a parameter: the chain takes it from the
|
|
3644
|
+
* dispatch origin. Submitting unsigned credits the block author; submitting
|
|
3645
|
+
* through the EVM precompile credits whoever signed that transaction.
|
|
3501
3646
|
*/
|
|
3502
3647
|
async privateTransfer(params, signer, options) {
|
|
3503
3648
|
const nullifiers = params.inputs.map((inp) => inp.nullifier);
|
|
@@ -3518,8 +3663,6 @@ var ShieldedPoolModule = class {
|
|
|
3518
3663
|
encrypted_memos: memos,
|
|
3519
3664
|
asset_id: params.assetId,
|
|
3520
3665
|
fee: params.fee ?? 0n,
|
|
3521
|
-
relayer: void 0,
|
|
3522
|
-
// Option<H160> — None for direct Substrate submissions
|
|
3523
3666
|
circuit_version: params.circuitVersion
|
|
3524
3667
|
});
|
|
3525
3668
|
if (signer) {
|
|
@@ -3602,7 +3745,13 @@ var PrivacyModule = class {
|
|
|
3602
3745
|
async getMerkleRoot() {
|
|
3603
3746
|
return this.substrate.request("privacy_getMerkleRoot", []);
|
|
3604
3747
|
}
|
|
3605
|
-
/**
|
|
3748
|
+
/**
|
|
3749
|
+
* Returns the Merkle proof for a LEAF INDEX.
|
|
3750
|
+
*
|
|
3751
|
+
* The node declares this parameter `u32`, so a commitment hex is rejected
|
|
3752
|
+
* by deserialisation before any lookup runs — `getMerkleProofByCommitment`
|
|
3753
|
+
* is the one that takes a string.
|
|
3754
|
+
*/
|
|
3606
3755
|
async getMerkleProof(leafIndex) {
|
|
3607
3756
|
const raw = await this.substrate.request("privacy_getMerkleProof", [
|
|
3608
3757
|
leafIndex
|
|
@@ -4036,7 +4185,9 @@ var ShieldedPoolPrecompile = class {
|
|
|
4036
4185
|
* hidden by design, so any address (a relayer included) can submit a valid
|
|
4037
4186
|
* proof.
|
|
4038
4187
|
*
|
|
4039
|
-
* Extrinsic: `shieldedPool.privateTransfer(proof, merkleRoot, nullifiers,
|
|
4188
|
+
* Extrinsic: `shieldedPool.privateTransfer(proof, merkleRoot, nullifiers,
|
|
4189
|
+
* commitments, memos, assetId, fee, circuitVersion)` — eight arguments; see
|
|
4190
|
+
* `buildPrivateTransferCalldata` for the encoding order.
|
|
4040
4191
|
*/
|
|
4041
4192
|
async privateTransfer(params, signer) {
|
|
4042
4193
|
return signer({ to: this.addr, data: buildPrivateTransferCalldata(params) });
|
|
@@ -4048,7 +4199,14 @@ var ShieldedPoolPrecompile = class {
|
|
|
4048
4199
|
* `params.recipientAddress` must be a 0x-prefixed AccountId32. To send to an
|
|
4049
4200
|
* EVM address, derive it first with `evmToImplicitSubstrate(evmAddr)`.
|
|
4050
4201
|
*
|
|
4051
|
-
* Extrinsic: `shieldedPool.unshield(proof, merkleRoot, nullifier, assetId,
|
|
4202
|
+
* Extrinsic: `shieldedPool.unshield(proof, merkleRoot, nullifier, assetId,
|
|
4203
|
+
* amount, recipient, fee, changeCommitment, changeEncryptedMemo,
|
|
4204
|
+
* circuitVersion)` — ten arguments; see `buildUnshieldCalldata`.
|
|
4205
|
+
*
|
|
4206
|
+
* **The relay fee goes to whoever `signer` is.** The chain takes the recipient
|
|
4207
|
+
* from `msg.sender`, not from calldata, so the account behind this signer is
|
|
4208
|
+
* the one credited — and it is also the one paying gas. Relaying on someone
|
|
4209
|
+
* else's behalf and being paid for it is the same act here.
|
|
4052
4210
|
*/
|
|
4053
4211
|
async unshield(params, signer) {
|
|
4054
4212
|
return signer({ to: this.addr, data: buildUnshieldCalldata(params) });
|
|
@@ -4067,7 +4225,9 @@ var ShieldedPoolPrecompile = class {
|
|
|
4067
4225
|
* The `msg.sender` address is the validator identity, and must match the
|
|
4068
4226
|
* one with pending fees.
|
|
4069
4227
|
*
|
|
4070
|
-
* Extrinsic: `shieldedPool.claim_shielded_fees(commitment, amount, assetId,
|
|
4228
|
+
* Extrinsic: `shieldedPool.claim_shielded_fees(commitment, amount, assetId,
|
|
4229
|
+
* memo, proof, publicSignals, circuitVersion)` — seven arguments; see
|
|
4230
|
+
* `buildClaimShieldedFeesCalldata`.
|
|
4071
4231
|
*/
|
|
4072
4232
|
async claimShieldedFees(params, signer) {
|
|
4073
4233
|
return signer({
|
|
@@ -4081,7 +4241,12 @@ var ShieldedPoolPrecompile = class {
|
|
|
4081
4241
|
// AccountId32, so estimating from a different address measures a different
|
|
4082
4242
|
// call.
|
|
4083
4243
|
async estimateShieldGas(params, from) {
|
|
4084
|
-
return this.evm.estimateGas({
|
|
4244
|
+
return this.evm.estimateGas({
|
|
4245
|
+
from,
|
|
4246
|
+
to: this.addr,
|
|
4247
|
+
data: buildShieldCalldata(params),
|
|
4248
|
+
value: `0x${params.amount.toString(16)}`
|
|
4249
|
+
});
|
|
4085
4250
|
}
|
|
4086
4251
|
async estimatePrivateTransferGas(params, from) {
|
|
4087
4252
|
return this.evm.estimateGas({
|
|
@@ -4536,9 +4701,13 @@ var OrbinumClientProvider = class _OrbinumClientProvider {
|
|
|
4536
4701
|
// ─── Reconnection ───────────────────────────────────────────────────────
|
|
4537
4702
|
/**
|
|
4538
4703
|
* Schedules the next connection attempt using exponential backoff
|
|
4539
|
-
* (capped at `reconnectMaxMs`) with
|
|
4540
|
-
* `'reconnecting'`.
|
|
4541
|
-
*
|
|
4704
|
+
* (capped at `reconnectMaxMs`) with EQUAL jitter, then transitions to
|
|
4705
|
+
* `'reconnecting'`.
|
|
4706
|
+
*
|
|
4707
|
+
* The delay lands in `[capped/2, capped]` — half fixed, half random. That
|
|
4708
|
+
* spreads a shared outage's reconnects over a `capped/2` window instead of
|
|
4709
|
+
* having every client retry in lockstep, while keeping a floor so the first
|
|
4710
|
+
* attempts do not all pile up near zero the way full jitter allows.
|
|
4542
4711
|
*/
|
|
4543
4712
|
scheduleReconnect() {
|
|
4544
4713
|
if (this._reconnectTimer) clearTimeout(this._reconnectTimer);
|
|
@@ -4930,6 +5099,16 @@ function mapZkEventData(method, data) {
|
|
|
4930
5099
|
index: get(4, "leaf_index") || get(4, "index")
|
|
4931
5100
|
};
|
|
4932
5101
|
}
|
|
5102
|
+
if (m === "nullifiersspent") {
|
|
5103
|
+
return { nullifiers: get(0, "nullifiers") };
|
|
5104
|
+
}
|
|
5105
|
+
if (m === "commitmentsinserted") {
|
|
5106
|
+
return {
|
|
5107
|
+
commitments: get(0, "commitments"),
|
|
5108
|
+
memos: get(1, "encrypted_memos") || get(1, "memos"),
|
|
5109
|
+
indices: get(2, "leaf_indices") || get(2, "indices")
|
|
5110
|
+
};
|
|
5111
|
+
}
|
|
4933
5112
|
if (m === "privatetransfer") {
|
|
4934
5113
|
return {
|
|
4935
5114
|
nullifiers: get(0, "nullifiers"),
|
|
@@ -4971,12 +5150,12 @@ function mapZkEventData(method, data) {
|
|
|
4971
5150
|
if (m_norm === "log") {
|
|
4972
5151
|
return { log: get(0, "log") };
|
|
4973
5152
|
}
|
|
4974
|
-
if (m_norm === "created" || m_norm === "createdfailed" || m_norm === "
|
|
5153
|
+
if (m_norm === "created" || m_norm === "createdfailed" || m_norm === "executedfailed") {
|
|
4975
5154
|
return { address: get(0, "address") };
|
|
4976
5155
|
}
|
|
4977
5156
|
if (m_norm === "executed") {
|
|
4978
5157
|
return {
|
|
4979
|
-
from: get(0, "from"),
|
|
5158
|
+
from: get(0, "from") ?? get(0, "address"),
|
|
4980
5159
|
to: get(1, "to"),
|
|
4981
5160
|
transaction_hash: get(2, "transaction_hash"),
|
|
4982
5161
|
exit_reason: get(3, "exit_reason")
|
|
@@ -5116,10 +5295,10 @@ var ERROR_KINDS = {
|
|
|
5116
5295
|
CommitmentAlreadyExists: "shape",
|
|
5117
5296
|
UnknownMerkleRoot: "stale-proof",
|
|
5118
5297
|
// Amounts and fees
|
|
5119
|
-
AmountTooSmall: "amount",
|
|
5120
5298
|
InvalidAmount: "amount",
|
|
5121
5299
|
InsufficientPoolBalance: "amount",
|
|
5122
5300
|
FeeTooLow: "amount",
|
|
5301
|
+
InsufficientPendingFees: "amount",
|
|
5123
5302
|
// Proofs
|
|
5124
5303
|
InvalidProof: "proof",
|
|
5125
5304
|
ProofVerificationFailed: "proof",
|
|
@@ -5128,12 +5307,14 @@ var ERROR_KINDS = {
|
|
|
5128
5307
|
InvalidAssetId: "asset",
|
|
5129
5308
|
AssetNotVerified: "asset",
|
|
5130
5309
|
AssetIdMismatch: "asset",
|
|
5310
|
+
AssetIdAlreadyExists: "asset",
|
|
5131
5311
|
// Request shape
|
|
5132
5312
|
InvalidMemoSize: "shape",
|
|
5133
5313
|
MemoCommitmentMismatch: "shape",
|
|
5134
5314
|
TooManyInputsOrOutputs: "shape",
|
|
5135
5315
|
InvalidRecipient: "shape",
|
|
5136
5316
|
EmptyBatch: "shape",
|
|
5317
|
+
FeeRecipientUnavailable: "shape",
|
|
5137
5318
|
// Capacity
|
|
5138
5319
|
MerkleTreeFull: "capacity",
|
|
5139
5320
|
// Balances pallet, reached through the same path
|
|
@@ -5188,6 +5369,7 @@ function methodOf(fnSig) {
|
|
|
5188
5369
|
if (fnSig.startsWith("unshield(")) return "unshield";
|
|
5189
5370
|
if (fnSig.startsWith("privateTransfer(")) return "privateTransfer";
|
|
5190
5371
|
if (fnSig.startsWith("shieldBatch(")) return "shieldBatch";
|
|
5372
|
+
if (fnSig.startsWith("claimShieldedFees(")) return "claimShieldedFees";
|
|
5191
5373
|
if (fnSig.startsWith("shield(")) return "shield";
|
|
5192
5374
|
return null;
|
|
5193
5375
|
}
|
|
@@ -5491,6 +5673,7 @@ function buildConfig(existing, chainFingerprint) {
|
|
|
5491
5673
|
updatedAt: now,
|
|
5492
5674
|
...chainFingerprint ? { chainFingerprint } : {},
|
|
5493
5675
|
...existing?.selfEphCounter !== void 0 ? { selfEphCounter: existing.selfEphCounter } : {},
|
|
5676
|
+
...existing?.outgoingEphCounter !== void 0 ? { outgoingEphCounter: existing.outgoingEphCounter } : {},
|
|
5494
5677
|
...existing?.pairwiseCounterparties !== void 0 ? { pairwiseCounterparties: existing.pairwiseCounterparties } : {}
|
|
5495
5678
|
};
|
|
5496
5679
|
}
|
|
@@ -5502,6 +5685,7 @@ async function writeConfig(storage, fallback, chainFingerprint) {
|
|
|
5502
5685
|
}
|
|
5503
5686
|
function mergeCounters(current, fallback) {
|
|
5504
5687
|
const selfEphCounter = higher(current.selfEphCounter, fallback?.selfEphCounter);
|
|
5688
|
+
const outgoingEphCounter = higher(current.outgoingEphCounter, fallback?.outgoingEphCounter);
|
|
5505
5689
|
const parties = mergePairwise(current.pairwiseCounterparties, fallback?.pairwiseCounterparties);
|
|
5506
5690
|
return {
|
|
5507
5691
|
createdAt: current.createdAt ?? fallback?.createdAt,
|
|
@@ -5509,6 +5693,7 @@ function mergeCounters(current, fallback) {
|
|
|
5509
5693
|
// is exactOptionalPropertyTypes, so an explicit undefined is a type
|
|
5510
5694
|
// error AND would round-trip through structured clone as a present key.
|
|
5511
5695
|
...selfEphCounter !== void 0 ? { selfEphCounter } : {},
|
|
5696
|
+
...outgoingEphCounter !== void 0 ? { outgoingEphCounter } : {},
|
|
5512
5697
|
...parties !== void 0 ? { pairwiseCounterparties: parties } : {}
|
|
5513
5698
|
};
|
|
5514
5699
|
}
|
|
@@ -5546,6 +5731,7 @@ function mergePairwise(current, fallback) {
|
|
|
5546
5731
|
|
|
5547
5732
|
// src/wallet/vault/storage/ephemeralIndex.ts
|
|
5548
5733
|
var MAX_EPH_INDEX = 4294967294;
|
|
5734
|
+
var RESTORE_SAFETY_GAP = 8;
|
|
5549
5735
|
function usableCounter(value) {
|
|
5550
5736
|
if (typeof value !== "number" || !Number.isInteger(value)) return null;
|
|
5551
5737
|
if (value < 0 || value > MAX_EPH_INDEX) return null;
|
|
@@ -5560,27 +5746,51 @@ async function reserveSelfEphIndex(storage) {
|
|
|
5560
5746
|
if (!updated) throw new Error("vault not initialized");
|
|
5561
5747
|
return (usableCounter(updated.selfEphCounter) ?? 1) - 1;
|
|
5562
5748
|
}
|
|
5563
|
-
async function
|
|
5749
|
+
async function reserveOutgoingIndex(storage, fromChain) {
|
|
5750
|
+
const recovered = usableCounter(fromChain);
|
|
5751
|
+
let exhausted = false;
|
|
5752
|
+
const updated = await storage.updateConfig((config) => {
|
|
5753
|
+
const raw = config.outgoingEphCounter;
|
|
5754
|
+
if (typeof raw === "number" && Number.isSafeInteger(raw) && raw > MAX_EPH_INDEX) {
|
|
5755
|
+
exhausted = true;
|
|
5756
|
+
return config;
|
|
5757
|
+
}
|
|
5758
|
+
exhausted = false;
|
|
5759
|
+
const stored = usableCounter(raw) ?? 0;
|
|
5760
|
+
const floor = recovered !== null && recovered > stored ? Math.min(recovered + RESTORE_SAFETY_GAP, MAX_EPH_INDEX) : stored;
|
|
5761
|
+
return {
|
|
5762
|
+
...config,
|
|
5763
|
+
outgoingEphCounter: floor + 1,
|
|
5764
|
+
updatedAt: Date.now()
|
|
5765
|
+
};
|
|
5766
|
+
});
|
|
5767
|
+
if (!updated) throw new Error("vault not initialized");
|
|
5768
|
+
if (exhausted || typeof updated.outgoingEphCounter === "number" && Number.isSafeInteger(updated.outgoingEphCounter) && updated.outgoingEphCounter > MAX_EPH_INDEX) {
|
|
5769
|
+
throw new Error("outgoing ephemeral sequence exhausted");
|
|
5770
|
+
}
|
|
5771
|
+
const committed = usableCounter(updated.outgoingEphCounter);
|
|
5772
|
+
if (committed === null || committed - 1 >= MAX_EPH_INDEX) {
|
|
5773
|
+
throw new Error("outgoing ephemeral sequence exhausted");
|
|
5774
|
+
}
|
|
5775
|
+
return committed - 1;
|
|
5776
|
+
}
|
|
5777
|
+
async function registerPairwiseCounterparty(storage, ivkHex) {
|
|
5564
5778
|
const key = ivkHex.toLowerCase();
|
|
5565
5779
|
const updated = await storage.updateConfig((config) => {
|
|
5566
|
-
const
|
|
5567
|
-
const addedAt = config.pairwiseCounterparties?.[key]?.addedAt;
|
|
5780
|
+
const existing = config.pairwiseCounterparties?.[key];
|
|
5568
5781
|
return {
|
|
5569
5782
|
...config,
|
|
5570
5783
|
pairwiseCounterparties: {
|
|
5571
5784
|
...config.pairwiseCounterparties,
|
|
5572
5785
|
[key]: {
|
|
5573
|
-
nextIndex: (
|
|
5574
|
-
addedAt: typeof addedAt === "number" ? addedAt : Date.now()
|
|
5786
|
+
nextIndex: usableCounter(existing?.nextIndex) ?? 0,
|
|
5787
|
+
addedAt: typeof existing?.addedAt === "number" ? existing.addedAt : Date.now()
|
|
5575
5788
|
}
|
|
5576
5789
|
},
|
|
5577
5790
|
updatedAt: Date.now()
|
|
5578
5791
|
};
|
|
5579
5792
|
});
|
|
5580
5793
|
if (!updated) throw new Error("vault not initialized");
|
|
5581
|
-
const nextIndex = usableCounter(updated.pairwiseCounterparties?.[key]?.nextIndex);
|
|
5582
|
-
if (nextIndex === null || nextIndex <= 1) return null;
|
|
5583
|
-
return nextIndex - 1;
|
|
5584
5794
|
}
|
|
5585
5795
|
|
|
5586
5796
|
// src/wallet/vault/notes/record.ts
|
|
@@ -6015,19 +6225,44 @@ async function processHints(ctx, hints, total) {
|
|
|
6015
6225
|
}
|
|
6016
6226
|
}
|
|
6017
6227
|
outcome.scanned++;
|
|
6018
|
-
if (!hint.encryptedMemo
|
|
6228
|
+
if (!hint.encryptedMemo) {
|
|
6019
6229
|
outcome.noMemo++;
|
|
6020
6230
|
continue;
|
|
6021
6231
|
}
|
|
6022
6232
|
toDecrypt.push(hint);
|
|
6023
6233
|
}
|
|
6024
|
-
const {
|
|
6234
|
+
const {
|
|
6235
|
+
notes,
|
|
6236
|
+
tagFiltered,
|
|
6237
|
+
selfMatched,
|
|
6238
|
+
pairwiseMatched,
|
|
6239
|
+
maxSelfEphIndex,
|
|
6240
|
+
maxOutgoingEphIndex,
|
|
6241
|
+
sentNotes,
|
|
6242
|
+
learnedRecipients,
|
|
6243
|
+
unmatchedSent,
|
|
6244
|
+
sealedBookEntries
|
|
6245
|
+
} = await ctx.pool.decryptBatch(
|
|
6246
|
+
toDecrypt,
|
|
6247
|
+
{ ...ctx.keys, recipientCandidates: [...outcome.learnedRecipients].map(fromHex) },
|
|
6248
|
+
ctx.signal
|
|
6249
|
+
);
|
|
6025
6250
|
outcome.tagFiltered += tagFiltered;
|
|
6026
6251
|
outcome.selfDiscovered += selfMatched;
|
|
6027
6252
|
outcome.pairwiseDiscovered += pairwiseMatched;
|
|
6253
|
+
outcome.sentNotes.push(...sentNotes ?? []);
|
|
6254
|
+
outcome.unmatchedSent.push(...unmatchedSent ?? []);
|
|
6255
|
+
for (const e of sealedBookEntries ?? []) outcome.sealedBookEntries.add(e);
|
|
6256
|
+
for (const r of learnedRecipients ?? []) outcome.learnedRecipients.add(r);
|
|
6028
6257
|
if (maxSelfEphIndex !== null) {
|
|
6029
6258
|
outcome.maxSelfEphIndex = Math.max(outcome.maxSelfEphIndex ?? -1, maxSelfEphIndex);
|
|
6030
6259
|
}
|
|
6260
|
+
if (maxOutgoingEphIndex != null) {
|
|
6261
|
+
outcome.maxOutgoingEphIndex = Math.max(
|
|
6262
|
+
outcome.maxOutgoingEphIndex ?? -1,
|
|
6263
|
+
maxOutgoingEphIndex
|
|
6264
|
+
);
|
|
6265
|
+
}
|
|
6031
6266
|
const pageEntries = [];
|
|
6032
6267
|
for (const [i, decrypted] of notes.entries()) {
|
|
6033
6268
|
const hint = toDecrypt[i];
|
|
@@ -6124,7 +6359,12 @@ async function collectScanEntries(params) {
|
|
|
6124
6359
|
tagFiltered: 0,
|
|
6125
6360
|
selfDiscovered: 0,
|
|
6126
6361
|
pairwiseDiscovered: 0,
|
|
6127
|
-
|
|
6362
|
+
sentNotes: [],
|
|
6363
|
+
unmatchedSent: [],
|
|
6364
|
+
sealedBookEntries: /* @__PURE__ */ new Set(),
|
|
6365
|
+
learnedRecipients: new Set((params.keys.recipientCandidates ?? []).map((c) => toHex(c))),
|
|
6366
|
+
maxSelfEphIndex: null,
|
|
6367
|
+
maxOutgoingEphIndex: null
|
|
6128
6368
|
};
|
|
6129
6369
|
const ctx = {
|
|
6130
6370
|
outcome,
|
|
@@ -6340,12 +6580,18 @@ function createScanCheckpoint(params) {
|
|
|
6340
6580
|
// src/wallet/worker/kernel/types.ts
|
|
6341
6581
|
var SELF_EPH_WINDOW = 1024;
|
|
6342
6582
|
var PAIRWISE_EPH_WINDOW = 64;
|
|
6583
|
+
var OUTGOING_EPH_WINDOW = 64;
|
|
6343
6584
|
var EMPTY_BATCH_RESULT = {
|
|
6344
6585
|
notes: [],
|
|
6345
6586
|
tagFiltered: 0,
|
|
6346
6587
|
selfMatched: 0,
|
|
6347
6588
|
pairwiseMatched: 0,
|
|
6348
|
-
maxSelfEphIndex: null
|
|
6589
|
+
maxSelfEphIndex: null,
|
|
6590
|
+
maxOutgoingEphIndex: null,
|
|
6591
|
+
sentNotes: [],
|
|
6592
|
+
learnedRecipients: [],
|
|
6593
|
+
unmatchedSent: [],
|
|
6594
|
+
sealedBookEntries: []
|
|
6349
6595
|
};
|
|
6350
6596
|
|
|
6351
6597
|
// src/wallet/scanner/selfEphGap.ts
|
|
@@ -6357,7 +6603,7 @@ function gapMargin(windowSize) {
|
|
|
6357
6603
|
return Math.max(1, Math.floor(windowSize / 16));
|
|
6358
6604
|
}
|
|
6359
6605
|
function resolveSelfEphCeiling(params) {
|
|
6360
|
-
const { notes,
|
|
6606
|
+
const { notes, viewingKey, scanMaxIndex } = params;
|
|
6361
6607
|
const windowSize = params.windowSize !== void 0 && params.windowSize > 0 ? params.windowSize : SELF_EPH_WINDOW;
|
|
6362
6608
|
if (scanMaxIndex === null) return null;
|
|
6363
6609
|
if (scanMaxIndex < windowSize - gapMargin(windowSize)) return scanMaxIndex;
|
|
@@ -6370,7 +6616,7 @@ function resolveSelfEphCeiling(params) {
|
|
|
6370
6616
|
let max = scanMaxIndex;
|
|
6371
6617
|
let from = windowSize;
|
|
6372
6618
|
for (; ; ) {
|
|
6373
|
-
const slice = selfEphWindow(
|
|
6619
|
+
const slice = selfEphWindow(viewingKey, ivkPacked, from, windowSize);
|
|
6374
6620
|
let matched = false;
|
|
6375
6621
|
for (const entry of slice) {
|
|
6376
6622
|
if (ownEphPks.has(entry.ephPkHex.toLowerCase())) {
|
|
@@ -6383,12 +6629,12 @@ function resolveSelfEphCeiling(params) {
|
|
|
6383
6629
|
}
|
|
6384
6630
|
return max;
|
|
6385
6631
|
}
|
|
6386
|
-
var
|
|
6632
|
+
var MAX_EPH_WINDOW2 = SELF_EPH_WINDOW * 64;
|
|
6387
6633
|
function windowSizeForCounter(counter) {
|
|
6388
6634
|
const used = Number.isFinite(counter) ? counter : 0;
|
|
6389
6635
|
const needed = used + gapMargin(SELF_EPH_WINDOW);
|
|
6390
6636
|
const rounded = Math.ceil(needed / SELF_EPH_WINDOW) * SELF_EPH_WINDOW;
|
|
6391
|
-
return Math.min(
|
|
6637
|
+
return Math.min(MAX_EPH_WINDOW2, Math.max(SELF_EPH_WINDOW, rounded));
|
|
6392
6638
|
}
|
|
6393
6639
|
|
|
6394
6640
|
// src/wallet/scanner/phases/spentStatus.ts
|
|
@@ -6426,6 +6672,8 @@ async function resolveSpentStatus(params) {
|
|
|
6426
6672
|
}
|
|
6427
6673
|
|
|
6428
6674
|
// src/wallet/scanner/pipeline.ts
|
|
6675
|
+
var OUTGOING_RESCAN_GAP = 8;
|
|
6676
|
+
var RETRY_BOOK_LIMIT = 64;
|
|
6429
6677
|
async function runScan(params) {
|
|
6430
6678
|
const { vault, storage, hints, nullifiers, pool, keys, sinceLeafIndex, signal, onWarning } = params;
|
|
6431
6679
|
const isIncremental = sinceLeafIndex !== void 0;
|
|
@@ -6433,6 +6681,7 @@ async function runScan(params) {
|
|
|
6433
6681
|
const preScanHexes = new Set(existingHexes);
|
|
6434
6682
|
const config = await storage.getConfig().catch(() => null);
|
|
6435
6683
|
const selfEphWindowSize = windowSizeForCounter(config?.selfEphCounter ?? 0);
|
|
6684
|
+
const outgoingEphWindowSize = windowSizeForCounter(config?.outgoingEphCounter ?? 0);
|
|
6436
6685
|
const pairwiseCounterparties = Object.keys(config?.pairwiseCounterparties ?? {}).flatMap(
|
|
6437
6686
|
(hex) => {
|
|
6438
6687
|
try {
|
|
@@ -6461,7 +6710,12 @@ async function runScan(params) {
|
|
|
6461
6710
|
// worth it on full scans and restores, waste on small incremental ticks.
|
|
6462
6711
|
selfEph: !isIncremental,
|
|
6463
6712
|
selfEphWindowSize,
|
|
6464
|
-
pairwiseCounterparties
|
|
6713
|
+
pairwiseCounterparties,
|
|
6714
|
+
// Sent-note discovery rides the same full-scan/restore budget as
|
|
6715
|
+
// self discovery: on an incremental tick the history is already
|
|
6716
|
+
// there, and the window would be paid for nothing.
|
|
6717
|
+
outgoingEph: !isIncremental,
|
|
6718
|
+
outgoingEphWindowSize
|
|
6465
6719
|
},
|
|
6466
6720
|
existingHexes,
|
|
6467
6721
|
sinceLeafIndex,
|
|
@@ -6475,6 +6729,49 @@ async function runScan(params) {
|
|
|
6475
6729
|
onPage: (entries) => checkpoint.savePageNotes(entries),
|
|
6476
6730
|
onBatchDone: (maxLeafIndex) => checkpoint.advanceCursor(maxLeafIndex)
|
|
6477
6731
|
});
|
|
6732
|
+
const ovk = keys.outgoingViewingKey;
|
|
6733
|
+
if (scan.unmatchedSent.length > 0 && ovk) {
|
|
6734
|
+
const proven = [...scan.learnedRecipients].map(fromHex);
|
|
6735
|
+
const keptEntries = [...scan.sealedBookEntries].slice(-RETRY_BOOK_LIMIT);
|
|
6736
|
+
const sealed = [];
|
|
6737
|
+
for (const entry of keptEntries) {
|
|
6738
|
+
try {
|
|
6739
|
+
sealed.push(BigInt(entry));
|
|
6740
|
+
} catch {
|
|
6741
|
+
}
|
|
6742
|
+
}
|
|
6743
|
+
const retries = scan.unmatchedSent.slice(-RETRY_BOOK_LIMIT);
|
|
6744
|
+
if (retries.length < scan.unmatchedSent.length) {
|
|
6745
|
+
onWarning?.(
|
|
6746
|
+
`sent-note retry capped at ${retries.length} of ${scan.unmatchedSent.length}; the rest resolve on the next full scan`
|
|
6747
|
+
);
|
|
6748
|
+
}
|
|
6749
|
+
if (keptEntries.length < scan.sealedBookEntries.size) {
|
|
6750
|
+
onWarning?.(
|
|
6751
|
+
`recipient book capped at ${keptEntries.length} of ${scan.sealedBookEntries.size} entries; older payments resolve on the next full scan`
|
|
6752
|
+
);
|
|
6753
|
+
}
|
|
6754
|
+
for (const { hint, ephIndex } of retries) {
|
|
6755
|
+
if (signal?.aborted) throw scanAbortError();
|
|
6756
|
+
const candidates = [
|
|
6757
|
+
...sealed.map((e) => openRecipientBookEntry(e, ovk, hint.commitmentHex)),
|
|
6758
|
+
...proven
|
|
6759
|
+
];
|
|
6760
|
+
const sent = recoverSentNote({
|
|
6761
|
+
hint,
|
|
6762
|
+
outgoingViewingKey: ovk,
|
|
6763
|
+
ephIndex,
|
|
6764
|
+
recipientCandidates: candidates
|
|
6765
|
+
});
|
|
6766
|
+
if (!sent) continue;
|
|
6767
|
+
const { recipientIvk, ...facts } = sent;
|
|
6768
|
+
scan.sentNotes.push({
|
|
6769
|
+
...facts,
|
|
6770
|
+
counterpartyIvkHex: toHex(recipientIvk),
|
|
6771
|
+
encryptedMemo: hint.encryptedMemo ?? ""
|
|
6772
|
+
});
|
|
6773
|
+
}
|
|
6774
|
+
}
|
|
6478
6775
|
const nullifiersToQuery = collectNullifiersToQuery(
|
|
6479
6776
|
scan.scanEntries,
|
|
6480
6777
|
scan.onChainHexes,
|
|
@@ -6504,7 +6801,6 @@ async function runScan(params) {
|
|
|
6504
6801
|
try {
|
|
6505
6802
|
ceiling = resolveSelfEphCeiling({
|
|
6506
6803
|
notes: vault.getAll(),
|
|
6507
|
-
spendingKey: keys.spendingKey,
|
|
6508
6804
|
viewingKey: keys.viewingKey,
|
|
6509
6805
|
scanMaxIndex: scan.maxSelfEphIndex,
|
|
6510
6806
|
windowSize: selfEphWindowSize
|
|
@@ -6514,6 +6810,13 @@ async function runScan(params) {
|
|
|
6514
6810
|
await bumpSelfEphCounterTo(storage, ceiling + 1).catch(() => {
|
|
6515
6811
|
});
|
|
6516
6812
|
}
|
|
6813
|
+
if (scan.maxOutgoingEphIndex !== null) {
|
|
6814
|
+
await bumpOutgoingCounterTo(
|
|
6815
|
+
storage,
|
|
6816
|
+
scan.maxOutgoingEphIndex + 1 + OUTGOING_RESCAN_GAP
|
|
6817
|
+
).catch(() => {
|
|
6818
|
+
});
|
|
6819
|
+
}
|
|
6517
6820
|
if (scan.decryptFailed > 0 && scan.found === 0 && scan.alreadyPresent === 0) {
|
|
6518
6821
|
onWarning?.(
|
|
6519
6822
|
`no notes recovered from ${scan.decryptFailed} hint(s) with memos; if this wallet should hold notes, check the keys it was unlocked with`
|
|
@@ -6525,7 +6828,13 @@ async function runScan(params) {
|
|
|
6525
6828
|
decryptFailed: scan.decryptFailed,
|
|
6526
6829
|
alreadyPresent: scan.alreadyPresent,
|
|
6527
6830
|
purged,
|
|
6528
|
-
incremental: isIncremental
|
|
6831
|
+
incremental: isIncremental,
|
|
6832
|
+
sentNotes: scan.sentNotes,
|
|
6833
|
+
discovery: {
|
|
6834
|
+
self: scan.selfDiscovered,
|
|
6835
|
+
pairwise: scan.pairwiseDiscovered,
|
|
6836
|
+
tagFiltered: scan.tagFiltered
|
|
6837
|
+
}
|
|
6529
6838
|
};
|
|
6530
6839
|
}
|
|
6531
6840
|
async function bumpSelfEphCounterTo(storage, next) {
|
|
@@ -6533,6 +6842,11 @@ async function bumpSelfEphCounterTo(storage, next) {
|
|
|
6533
6842
|
(config) => (config.selfEphCounter ?? 0) >= next ? config : { ...config, selfEphCounter: next, updatedAt: Date.now() }
|
|
6534
6843
|
);
|
|
6535
6844
|
}
|
|
6845
|
+
async function bumpOutgoingCounterTo(storage, next) {
|
|
6846
|
+
await storage.updateConfig(
|
|
6847
|
+
(config) => (config.outgoingEphCounter ?? 0) >= next ? config : { ...config, outgoingEphCounter: next, updatedAt: Date.now() }
|
|
6848
|
+
);
|
|
6849
|
+
}
|
|
6536
6850
|
|
|
6537
6851
|
// src/wallet/scanner/history/extrinsicFacts.ts
|
|
6538
6852
|
var UNKNOWN = { fee: null, success: true };
|
|
@@ -6671,10 +6985,8 @@ function regeneratePaymentSlip(facts, recipientIvkPacked, txHash) {
|
|
|
6671
6985
|
commitmentHex: facts.commitmentHex,
|
|
6672
6986
|
encryptedMemo: facts.encryptedMemo,
|
|
6673
6987
|
...facts.leafIndex !== void 0 ? { leafIndex: facts.leafIndex } : {},
|
|
6674
|
-
//
|
|
6988
|
+
// The recipient renders this as an explorer link, so an unconstrained
|
|
6675
6989
|
// string is a URL injection wearing the authority of a decrypted slip.
|
|
6676
|
-
// Dropped rather than fatal: the slip still rebuilds the note, which is
|
|
6677
|
-
// the part that matters.
|
|
6678
6990
|
...isHexOfLength(txHash, 32) ? { txHash } : {}
|
|
6679
6991
|
});
|
|
6680
6992
|
return encodePaymentSlip(envelope);
|
|
@@ -6699,15 +7011,37 @@ async function loadExistingRecords(vault) {
|
|
|
6699
7011
|
function recordKey(transfer) {
|
|
6700
7012
|
return transfer.hash ?? `${transfer.blockNumber}-${transfer.extrinsicIndex ?? 0}`;
|
|
6701
7013
|
}
|
|
7014
|
+
function sentByCommitment(sentNotes) {
|
|
7015
|
+
return new Map(sentNotes.map((s) => [s.commitmentHex.toLowerCase(), s]));
|
|
7016
|
+
}
|
|
7017
|
+
function reissuedSlip(sent, hash) {
|
|
7018
|
+
try {
|
|
7019
|
+
const facts = {
|
|
7020
|
+
commitmentHex: sent.commitmentHex,
|
|
7021
|
+
encryptedMemo: sent.encryptedMemo,
|
|
7022
|
+
...sent.leafIndex !== void 0 ? { leafIndex: sent.leafIndex } : {}
|
|
7023
|
+
};
|
|
7024
|
+
return {
|
|
7025
|
+
paymentSlip: regeneratePaymentSlip(
|
|
7026
|
+
facts,
|
|
7027
|
+
fromHex(sent.counterpartyIvkHex),
|
|
7028
|
+
hash ?? void 0
|
|
7029
|
+
)
|
|
7030
|
+
};
|
|
7031
|
+
} catch {
|
|
7032
|
+
return {};
|
|
7033
|
+
}
|
|
7034
|
+
}
|
|
6702
7035
|
async function reconstructOutgoingTxRecords(deps) {
|
|
6703
7036
|
const { vault, transfers } = deps;
|
|
6704
7037
|
const now = deps.now ?? Date.now;
|
|
6705
7038
|
const allNotes = vault.getAll();
|
|
6706
7039
|
const spentNotes = allNotes.filter((n) => n.spent);
|
|
6707
7040
|
if (spentNotes.length === 0) return;
|
|
7041
|
+
const sentCommitments = (deps.sentNotes ?? []).map((s) => s.commitmentHex);
|
|
6708
7042
|
const [outgoingTransfers, commitmentTransfers] = await Promise.all([
|
|
6709
7043
|
transfers.byNullifiers(spentNotes.map((n) => n.nullifierHex)),
|
|
6710
|
-
transfers.byCommitments(allNotes.map((n) => n.commitmentHex))
|
|
7044
|
+
transfers.byCommitments([...allNotes.map((n) => n.commitmentHex), ...sentCommitments])
|
|
6711
7045
|
]);
|
|
6712
7046
|
if (outgoingTransfers.length === 0) return;
|
|
6713
7047
|
const noteByNullifier = new Map(spentNotes.map((n) => [n.nullifierHex, n]));
|
|
@@ -6716,64 +7050,85 @@ async function reconstructOutgoingTxRecords(deps) {
|
|
|
6716
7050
|
commitmentTransfers.map((ct) => [extrinsicKey(ct), ct.matchedCommitments ?? []])
|
|
6717
7051
|
);
|
|
6718
7052
|
const existingByKey = await loadExistingRecords(vault);
|
|
7053
|
+
const sentIndex = sentByCommitment(deps.sentNotes ?? []);
|
|
7054
|
+
const recoveryEnabled = deps.sentNotes !== void 0;
|
|
7055
|
+
const sentFor = (row) => (commitmentsByExtrinsic.get(extrinsicKey(row)) ?? []).map((hex) => sentIndex.get(hex.toLowerCase())).find((s) => s !== void 0);
|
|
6719
7056
|
for (const transfer of outgoingTransfers) {
|
|
6720
7057
|
const existing = existingByKey.get(recordKey(transfer));
|
|
6721
|
-
|
|
7058
|
+
const known = existing?.recipientPkHex && existing.recipientPkHex !== ZERO_PK;
|
|
7059
|
+
if (known && existing?.paymentSlip) continue;
|
|
6722
7060
|
const inputNotes = (transfer.matchedNullifiers ?? []).map((h) => noteByNullifier.get(h)).filter((n) => n !== void 0);
|
|
6723
7061
|
if (inputNotes.length === 0) continue;
|
|
6724
7062
|
const changeNote = selectDescribingNoteByCommitment(
|
|
6725
7063
|
commitmentsByExtrinsic.get(extrinsicKey(transfer)) ?? [],
|
|
6726
7064
|
noteByCommitment
|
|
6727
7065
|
);
|
|
6728
|
-
const
|
|
7066
|
+
const sentForRow = sentFor(transfer);
|
|
7067
|
+
const recipientPkHex = sentForRow ? toPkHex(sentForRow.recipientStealthPk) : recoveryEnabled ? ZERO_PK : toPkHex(changeNote?.sourcePk);
|
|
6729
7068
|
if (existing) {
|
|
6730
|
-
|
|
6731
|
-
|
|
7069
|
+
const patch = {
|
|
7070
|
+
// Only when it ADDS something: this runs over the whole
|
|
7071
|
+
// outgoing history on every scan, and rewriting what is already
|
|
7072
|
+
// there would make each one a write.
|
|
7073
|
+
...!known && recipientPkHex !== ZERO_PK ? { recipientPkHex } : {},
|
|
7074
|
+
...sentForRow && !existing.paymentSlip ? reissuedSlip(sentForRow, transfer.hash) : {}
|
|
7075
|
+
};
|
|
7076
|
+
if (Object.keys(patch).length > 0) {
|
|
7077
|
+
await vault.saveTxRecord({ ...existing, ...patch });
|
|
7078
|
+
}
|
|
6732
7079
|
continue;
|
|
6733
7080
|
}
|
|
6734
7081
|
const { fee, success } = await fetchExtrinsicFacts(deps.txFacts, transfer.hash);
|
|
6735
7082
|
const totalInputValue = inputNotes.reduce((sum, n) => sum + n.value, 0n);
|
|
6736
|
-
const
|
|
7083
|
+
const derivedAmount = totalInputValue - (changeNote?.value ?? 0n) - (fee ?? 0n);
|
|
7084
|
+
const transferAmount = sentForRow?.value ?? derivedAmount;
|
|
6737
7085
|
if (transferAmount <= 0n) continue;
|
|
6738
7086
|
const record = {
|
|
6739
7087
|
id: recordKey(transfer),
|
|
6740
7088
|
type: "private_transfer",
|
|
6741
7089
|
blockNumber: transfer.blockNumber,
|
|
6742
7090
|
hash: transfer.hash ?? "",
|
|
6743
|
-
assetId: inputNotes[0].assetId.toString(),
|
|
7091
|
+
assetId: (sentForRow?.assetId ?? inputNotes[0].assetId).toString(),
|
|
6744
7092
|
amount: transferAmount.toString(),
|
|
6745
7093
|
recipientPkHex,
|
|
6746
7094
|
status: success ? "success" : "failed",
|
|
6747
|
-
...fee !== null ? { feePlanck: fee.toString() } : {
|
|
6748
|
-
|
|
7095
|
+
...fee !== null ? { feePlanck: fee.toString() } : {},
|
|
7096
|
+
...fee === null && !sentForRow ? { amountApproximate: true } : {},
|
|
7097
|
+
timestampMs: transfer.timestampMs ?? now(),
|
|
7098
|
+
// Best-effort: a slip that cannot be sealed costs the convenience of
|
|
7099
|
+
// handing it over again, never the history row.
|
|
7100
|
+
...sentForRow ? reissuedSlip(sentForRow, transfer.hash) : {}
|
|
6749
7101
|
};
|
|
6750
7102
|
await vault.saveTxRecord(record);
|
|
6751
7103
|
}
|
|
6752
7104
|
}
|
|
6753
7105
|
|
|
6754
7106
|
// src/wallet/ops/notes/buildNote.ts
|
|
6755
|
-
async function
|
|
7107
|
+
async function buildZkNoteWithIndex(params, deps) {
|
|
6756
7108
|
const { keys, storage } = deps;
|
|
6757
7109
|
const ownerPk = params.ownerPk ?? keys.ownerPk;
|
|
6758
7110
|
const spendingKey = params.spendingKey ?? keys.spendingKey;
|
|
6759
7111
|
const viewingPublicKey = params.viewingPublicKey ?? keys.viewingPublicKey;
|
|
6760
7112
|
const isSelf = params.viewingPublicKey === void 0 && params.recipientOwnerPk === void 0;
|
|
6761
7113
|
let ephSkOverride;
|
|
6762
|
-
|
|
7114
|
+
let outgoingIndex;
|
|
7115
|
+
if (isSelf && storage && keys.viewingSecretKey) {
|
|
6763
7116
|
try {
|
|
6764
|
-
ephSkOverride = deriveSelfEphSk(
|
|
7117
|
+
ephSkOverride = deriveSelfEphSk(
|
|
7118
|
+
keys.viewingSecretKey,
|
|
7119
|
+
await reserveSelfEphIndex(storage)
|
|
7120
|
+
);
|
|
6765
7121
|
} catch {
|
|
6766
7122
|
}
|
|
6767
|
-
} else if (params.viewingPublicKey && storage && keys.
|
|
7123
|
+
} else if (params.viewingPublicKey && storage && keys.outgoingViewingKey) {
|
|
6768
7124
|
try {
|
|
6769
|
-
const index = await
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
}
|
|
7125
|
+
const index = await reserveOutgoingIndex(storage, params.outgoingIndexFromChain);
|
|
7126
|
+
ephSkOverride = deriveOutgoingEphSk(keys.outgoingViewingKey, index);
|
|
7127
|
+
outgoingIndex = index;
|
|
7128
|
+
} catch {
|
|
7129
|
+
}
|
|
7130
|
+
try {
|
|
7131
|
+
await registerPairwiseCounterparty(storage, toHex(params.viewingPublicKey));
|
|
6777
7132
|
} catch {
|
|
6778
7133
|
}
|
|
6779
7134
|
}
|
|
@@ -6793,7 +7148,13 @@ async function buildZkNote(params, deps) {
|
|
|
6793
7148
|
},
|
|
6794
7149
|
...ephSkOverride !== void 0 && { ephSkOverride }
|
|
6795
7150
|
});
|
|
6796
|
-
return
|
|
7151
|
+
return {
|
|
7152
|
+
note: stampCreatedAt(note, Date.now()),
|
|
7153
|
+
...outgoingIndex !== void 0 ? { outgoingIndex } : {}
|
|
7154
|
+
};
|
|
7155
|
+
}
|
|
7156
|
+
async function buildZkNote(params, deps) {
|
|
7157
|
+
return (await buildZkNoteWithIndex(params, deps)).note;
|
|
6797
7158
|
}
|
|
6798
7159
|
|
|
6799
7160
|
// src/wallet/ops/notes/selfStealthNote.ts
|
|
@@ -6812,89 +7173,6 @@ function recoverSelfStealthNote(note, keys) {
|
|
|
6812
7173
|
return recovered;
|
|
6813
7174
|
}
|
|
6814
7175
|
|
|
6815
|
-
// src/wallet/ops/notes/noteTransfer.ts
|
|
6816
|
-
var NOTE_TRANSFER_URI_SCHEME = "orbinum://notes/v1/";
|
|
6817
|
-
var QR_PAGE_MAX_CHARS = 1800;
|
|
6818
|
-
function noteToTransferEntry(note) {
|
|
6819
|
-
return {
|
|
6820
|
-
c: note.commitmentHex,
|
|
6821
|
-
n: note.nullifierHex,
|
|
6822
|
-
val: note.value.toString(),
|
|
6823
|
-
aid: note.assetId.toString(),
|
|
6824
|
-
opk: scalarToHex(note.ownerPk),
|
|
6825
|
-
bld: scalarToHex(note.blinding),
|
|
6826
|
-
sk: scalarToHex(note.spendingKey)
|
|
6827
|
-
};
|
|
6828
|
-
}
|
|
6829
|
-
function encodeNoteTransferPages(notes, options = {}) {
|
|
6830
|
-
const now = options.now ?? Date.now;
|
|
6831
|
-
const maxChars = options.maxChars ?? QR_PAGE_MAX_CHARS;
|
|
6832
|
-
const entries = notes.map(noteToTransferEntry);
|
|
6833
|
-
const batches = [];
|
|
6834
|
-
let batch = [];
|
|
6835
|
-
for (const entry of entries) {
|
|
6836
|
-
batch.push(entry);
|
|
6837
|
-
const probe = { v: 1, ts: 0, p: 0, pt: 1, notes: batch };
|
|
6838
|
-
const size = NOTE_TRANSFER_URI_SCHEME.length + base64UrlEncode(JSON.stringify(probe)).length;
|
|
6839
|
-
if (size > maxChars && batch.length > 1) {
|
|
6840
|
-
batch.pop();
|
|
6841
|
-
batches.push(batch);
|
|
6842
|
-
batch = [entry];
|
|
6843
|
-
}
|
|
6844
|
-
}
|
|
6845
|
-
if (batch.length > 0) batches.push(batch);
|
|
6846
|
-
const ts = now();
|
|
6847
|
-
return batches.map((notesInPage, i) => {
|
|
6848
|
-
const payload = {
|
|
6849
|
-
v: 1,
|
|
6850
|
-
ts,
|
|
6851
|
-
p: i,
|
|
6852
|
-
pt: batches.length,
|
|
6853
|
-
notes: notesInPage
|
|
6854
|
-
};
|
|
6855
|
-
return NOTE_TRANSFER_URI_SCHEME + base64UrlEncode(JSON.stringify(payload));
|
|
6856
|
-
});
|
|
6857
|
-
}
|
|
6858
|
-
function decodeNoteTransferPage(uri) {
|
|
6859
|
-
if (!uri.startsWith(NOTE_TRANSFER_URI_SCHEME)) {
|
|
6860
|
-
throw new Error("Not an Orbinum note-transfer code.");
|
|
6861
|
-
}
|
|
6862
|
-
let payload;
|
|
6863
|
-
try {
|
|
6864
|
-
payload = JSON.parse(base64UrlDecode(uri.slice(NOTE_TRANSFER_URI_SCHEME.length)));
|
|
6865
|
-
} catch {
|
|
6866
|
-
throw new Error("Note-transfer code is corrupt.");
|
|
6867
|
-
}
|
|
6868
|
-
const p = payload;
|
|
6869
|
-
if (p.v !== 1) {
|
|
6870
|
-
throw new Error(`Unsupported note-transfer version: ${String(p.v)}.`);
|
|
6871
|
-
}
|
|
6872
|
-
if (typeof p.p !== "number" || typeof p.pt !== "number" || !Array.isArray(p.notes) || p.notes.some((n) => !isEntry(n))) {
|
|
6873
|
-
throw new Error("Note-transfer code is missing required fields.");
|
|
6874
|
-
}
|
|
6875
|
-
return p;
|
|
6876
|
-
}
|
|
6877
|
-
function isEntry(value) {
|
|
6878
|
-
if (typeof value !== "object" || value === null) return false;
|
|
6879
|
-
const e = value;
|
|
6880
|
-
return ["c", "n", "val", "aid", "opk", "bld", "sk"].every(
|
|
6881
|
-
(k) => typeof e[k] === "string" && e[k].length > 0
|
|
6882
|
-
);
|
|
6883
|
-
}
|
|
6884
|
-
function assembleNoteTransfer(pages) {
|
|
6885
|
-
if (pages.length === 0) throw new Error("No pages scanned.");
|
|
6886
|
-
const total = pages[0].pt;
|
|
6887
|
-
if (pages.some((p) => p.pt !== total)) {
|
|
6888
|
-
throw new Error("Pages come from different batches.");
|
|
6889
|
-
}
|
|
6890
|
-
const seen = new Set(pages.map((p) => p.p));
|
|
6891
|
-
if (seen.size !== total) {
|
|
6892
|
-
const missing = [...Array(total).keys()].filter((i) => !seen.has(i));
|
|
6893
|
-
throw new Error(`Missing page(s): ${missing.map((i) => i + 1).join(", ")} of ${total}.`);
|
|
6894
|
-
}
|
|
6895
|
-
return [...pages].sort((a, b) => a.p - b.p).flatMap((p) => p.notes);
|
|
6896
|
-
}
|
|
6897
|
-
|
|
6898
7176
|
// src/wallet/ops/notes/noteBackup.ts
|
|
6899
7177
|
var NOTE_BACKUP_VERSION = 1;
|
|
6900
7178
|
function noteToBackupEntry(note) {
|
|
@@ -6929,12 +7207,13 @@ function decodeNoteBackup(json) {
|
|
|
6929
7207
|
if (p.v !== NOTE_BACKUP_VERSION) {
|
|
6930
7208
|
throw new Error(`Unsupported note-backup version: ${String(p.v)}.`);
|
|
6931
7209
|
}
|
|
6932
|
-
if (!Array.isArray(p.notes) || p.notes.some((n) => !
|
|
7210
|
+
if (!Array.isArray(p.notes) || p.notes.some((n) => !isEntry(n))) {
|
|
6933
7211
|
throw new Error("Note backup is missing required fields.");
|
|
6934
7212
|
}
|
|
6935
7213
|
return p.notes;
|
|
6936
7214
|
}
|
|
6937
7215
|
function importNotesFromBackup(entries, keys) {
|
|
7216
|
+
assertSecretKeyBytes(keys.viewingSecretKey, "importNotesFromBackup: viewingSecretKey");
|
|
6938
7217
|
const out = [];
|
|
6939
7218
|
for (const entry of entries) {
|
|
6940
7219
|
const commitment = {
|
|
@@ -6958,12 +7237,14 @@ function importNotesFromBackup(entries, keys) {
|
|
|
6958
7237
|
}
|
|
6959
7238
|
return out;
|
|
6960
7239
|
}
|
|
6961
|
-
function
|
|
7240
|
+
function isEntry(value) {
|
|
6962
7241
|
if (typeof value !== "object" || value === null) return false;
|
|
6963
7242
|
const e = value;
|
|
6964
|
-
if (
|
|
6965
|
-
if (
|
|
6966
|
-
if (
|
|
7243
|
+
if (!isHexOfLength(e["commitmentHex"], 32)) return false;
|
|
7244
|
+
if (!isHexOfLength(e["encryptedMemo"], ENCRYPTED_MEMO_SIZE)) return false;
|
|
7245
|
+
if (e["leafIndex"] !== void 0 && !isValidLeafIndex(e["leafIndex"])) return false;
|
|
7246
|
+
if (e["spent"] !== void 0 && typeof e["spent"] !== "boolean") return false;
|
|
7247
|
+
if (e["spentAt"] !== void 0 && e["spentAt"] !== null && typeof e["spentAt"] !== "number") {
|
|
6967
7248
|
return false;
|
|
6968
7249
|
}
|
|
6969
7250
|
return true;
|
|
@@ -6971,6 +7252,7 @@ function isEntry2(value) {
|
|
|
6971
7252
|
|
|
6972
7253
|
// src/wallet/ops/notes/paymentSlipImport.ts
|
|
6973
7254
|
function importPaymentSlip(slip, keys) {
|
|
7255
|
+
assertSecretKeyBytes(keys.viewingSecretKey, "importPaymentSlip: viewingSecretKey");
|
|
6974
7256
|
const envelope = typeof slip === "string" ? decodePaymentSlip(slip) : slip;
|
|
6975
7257
|
if (!envelope) return null;
|
|
6976
7258
|
const fields = openPaymentSlip(keys.viewingSecretKey, envelope);
|
|
@@ -7099,10 +7381,7 @@ async function transferNotes(deps, params, onProgress) {
|
|
|
7099
7381
|
const merkleRoot = merkleA.root;
|
|
7100
7382
|
onProgress?.("building-output-notes");
|
|
7101
7383
|
const effectiveSenderPk = senderPk ?? noteA.ownerPk;
|
|
7102
|
-
const
|
|
7103
|
-
deriveViewingSecretKey(noteA.spendingKey)
|
|
7104
|
-
);
|
|
7105
|
-
const recipientNote = await deps.buildNote({
|
|
7384
|
+
const { note: recipientNote, outgoingIndex } = await deps.buildNote({
|
|
7106
7385
|
value: transferAmount,
|
|
7107
7386
|
assetId: noteA.assetId,
|
|
7108
7387
|
ownerPk: recipientPk,
|
|
@@ -7112,13 +7391,17 @@ async function transferNotes(deps, params, onProgress) {
|
|
|
7112
7391
|
// With a viewing key present this activates stealth derivation.
|
|
7113
7392
|
recipientOwnerPk: recipientPk
|
|
7114
7393
|
});
|
|
7115
|
-
const
|
|
7394
|
+
const changeSourcePk = outgoingIndex !== void 0 && recipientViewingPublicKey !== void 0 && deps.outgoingViewingKey !== void 0 ? bytesToBigintLE(
|
|
7395
|
+
sealRecipientBookEntry(
|
|
7396
|
+
recipientViewingPublicKey,
|
|
7397
|
+
deps.outgoingViewingKey,
|
|
7398
|
+
recipientNote.commitmentHex
|
|
7399
|
+
)
|
|
7400
|
+
) : recipientNote.ownerPk;
|
|
7401
|
+
const { note: changeNote } = await deps.buildNote({
|
|
7116
7402
|
value: changeValue,
|
|
7117
7403
|
assetId: noteA.assetId,
|
|
7118
|
-
|
|
7119
|
-
spendingKey: noteA.spendingKey,
|
|
7120
|
-
sourcePk: recipientNote.ownerPk,
|
|
7121
|
-
viewingPublicKey: senderViewingPublicKey
|
|
7404
|
+
sourcePk: changeSourcePk
|
|
7122
7405
|
});
|
|
7123
7406
|
onProgress?.("generating-zk");
|
|
7124
7407
|
const noteToInput = (note, proof) => ({
|
|
@@ -7235,7 +7518,7 @@ async function unshieldNote(deps, params, onProgress) {
|
|
|
7235
7518
|
if (changeValue > 0n) {
|
|
7236
7519
|
const { ownerPk, spendingKey } = deps.stealthKeys;
|
|
7237
7520
|
changeBlinding = randomBlinding();
|
|
7238
|
-
changeNote = await deps.buildNote({
|
|
7521
|
+
changeNote = (await deps.buildNote({
|
|
7239
7522
|
value: changeValue,
|
|
7240
7523
|
assetId: note.assetId,
|
|
7241
7524
|
ownerPk,
|
|
@@ -7243,7 +7526,7 @@ async function unshieldNote(deps, params, onProgress) {
|
|
|
7243
7526
|
spendingKey,
|
|
7244
7527
|
// Activates stealth: the commitment covers a one-time ownerPk.
|
|
7245
7528
|
recipientOwnerPk: ownerPk
|
|
7246
|
-
});
|
|
7529
|
+
})).note;
|
|
7247
7530
|
}
|
|
7248
7531
|
const { provider, version: circuitVersion } = await deps.resolver.resolve(
|
|
7249
7532
|
import_proof_generator7.CircuitType.Unshield,
|
|
@@ -7302,7 +7585,7 @@ async function unshieldNote(deps, params, onProgress) {
|
|
|
7302
7585
|
var import_proof_generator8 = require("@orbinum/proof-generator");
|
|
7303
7586
|
async function claimFees(deps, { assetId, amount, signer }, onStep) {
|
|
7304
7587
|
onStep?.("building-note");
|
|
7305
|
-
const note = await deps.buildNote({ value: amount, assetId: BigInt(assetId) });
|
|
7588
|
+
const { note } = await deps.buildNote({ value: amount, assetId: BigInt(assetId) });
|
|
7306
7589
|
const { provider } = await deps.resolver.resolve(import_proof_generator8.CircuitType.ValueProof, note.circuitVersion);
|
|
7307
7590
|
onStep?.("generating-proof");
|
|
7308
7591
|
const proofOutput = await generateFeeClaimProof(
|
|
@@ -7529,8 +7812,9 @@ var KEY_PREFIX = "orbinum_sk_";
|
|
|
7529
7812
|
function isEnvelope(value) {
|
|
7530
7813
|
return typeof value === "object" && value !== null && value.v === 1 && typeof value.iv === "string" && typeof value.ct === "string";
|
|
7531
7814
|
}
|
|
7532
|
-
function sessionCacheKey(address, chainId) {
|
|
7533
|
-
|
|
7815
|
+
function sessionCacheKey(address, chainId, version = "v3") {
|
|
7816
|
+
const marker = `${version}_`;
|
|
7817
|
+
return `${KEY_PREFIX}${marker}${chainId}_${canonicalAccountId(address)}`;
|
|
7534
7818
|
}
|
|
7535
7819
|
async function hasCachedSession(store, address, chainId) {
|
|
7536
7820
|
return await store.get(sessionCacheKey(address, chainId)) !== null;
|
|
@@ -7601,9 +7885,55 @@ function createDeviceKeyProvider(store) {
|
|
|
7601
7885
|
|
|
7602
7886
|
// src/wallet/identity/vaultName.ts
|
|
7603
7887
|
var PREFIX2 = "orbinum-vault";
|
|
7604
|
-
|
|
7888
|
+
var versionMarker = (version) => `${version}-`;
|
|
7889
|
+
var VERSION_MARKERS = ["v3-"];
|
|
7890
|
+
function assertNoVersionMarker(component, label) {
|
|
7891
|
+
for (const marker of VERSION_MARKERS) {
|
|
7892
|
+
if (component.startsWith(marker)) {
|
|
7893
|
+
throw new Error(
|
|
7894
|
+
`vaultStorageName: ${label} must not start with "${marker}" \u2014 it would collide with another identity version`
|
|
7895
|
+
);
|
|
7896
|
+
}
|
|
7897
|
+
}
|
|
7898
|
+
}
|
|
7899
|
+
function vaultStorageName(address, chainFingerprint, version = "v3") {
|
|
7605
7900
|
const account = canonicalAccountId(address);
|
|
7606
|
-
|
|
7901
|
+
assertNoVersionMarker(account, "account");
|
|
7902
|
+
const marker = versionMarker(version);
|
|
7903
|
+
if (chainFingerprint) assertNoVersionMarker(chainFingerprint.toLowerCase(), "chainFingerprint");
|
|
7904
|
+
return chainFingerprint ? `${PREFIX2}-${marker}${chainFingerprint.toLowerCase()}-${account}` : `${PREFIX2}-${marker}${account}`;
|
|
7905
|
+
}
|
|
7906
|
+
|
|
7907
|
+
// src/wallet/identity/walletIdentity.ts
|
|
7908
|
+
function deriveIdentity(rootSecret, version) {
|
|
7909
|
+
if (rootSecret.length !== 32) {
|
|
7910
|
+
throw new Error(`deriveIdentity: rootSecret must be 32 bytes, got ${rootSecret.length}`);
|
|
7911
|
+
}
|
|
7912
|
+
if (rootSecret.every((b) => b === 0)) {
|
|
7913
|
+
throw new Error("deriveIdentity: rootSecret is all zeros \u2014 every wallet would share it");
|
|
7914
|
+
}
|
|
7915
|
+
if (version !== "v3") {
|
|
7916
|
+
throw new Error(`deriveIdentity: unknown identity version ${String(version)}`);
|
|
7917
|
+
}
|
|
7918
|
+
const spendingKey = deriveSpendingKeyV3(rootSecret);
|
|
7919
|
+
const viewingSecretKey = deriveViewingSecretKeyV3(rootSecret);
|
|
7920
|
+
return {
|
|
7921
|
+
version,
|
|
7922
|
+
spendingKey,
|
|
7923
|
+
ownerPk: deriveOwnerPk(spendingKey),
|
|
7924
|
+
viewingSecretKey,
|
|
7925
|
+
viewingPublicKey: deriveViewingPublicKey(viewingSecretKey),
|
|
7926
|
+
outgoingViewingKey: deriveOutgoingViewingKeyV3(rootSecret)
|
|
7927
|
+
};
|
|
7928
|
+
}
|
|
7929
|
+
function exportViewingCredential(identity, options = {}) {
|
|
7930
|
+
return {
|
|
7931
|
+
version: identity.version,
|
|
7932
|
+
ownerPk: identity.ownerPk,
|
|
7933
|
+
viewingSecretKey: Uint8Array.from(identity.viewingSecretKey),
|
|
7934
|
+
viewingPublicKey: Uint8Array.from(identity.viewingPublicKey),
|
|
7935
|
+
...options.includeOutgoing && identity.outgoingViewingKey ? { outgoingViewingKey: Uint8Array.from(identity.outgoingViewingKey) } : {}
|
|
7936
|
+
};
|
|
7607
7937
|
}
|
|
7608
7938
|
|
|
7609
7939
|
// src/wallet/identity/injectedExtensions.ts
|
|
@@ -7627,23 +7957,21 @@ var cachedWindow = null;
|
|
|
7627
7957
|
function clearKnownEphWindow() {
|
|
7628
7958
|
cachedWindow = null;
|
|
7629
7959
|
}
|
|
7630
|
-
function bytesKey(bytes) {
|
|
7631
|
-
let out = "";
|
|
7632
|
-
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
7633
|
-
return out;
|
|
7634
|
-
}
|
|
7635
7960
|
function getKnownEphWindow(keys) {
|
|
7636
7961
|
const selfSize = keys.selfEphWindowSize ?? SELF_EPH_WINDOW;
|
|
7637
7962
|
const pairSize = keys.pairwiseWindowSize ?? PAIRWISE_EPH_WINDOW;
|
|
7963
|
+
const outSize = keys.outgoingEphWindowSize ?? OUTGOING_EPH_WINDOW;
|
|
7638
7964
|
const counterparties = keys.pairwiseCounterparties ?? [];
|
|
7639
|
-
|
|
7640
|
-
|
|
7965
|
+
const canScanOutgoing = keys.outgoingEph === true && isUsableSecretKey(keys.outgoingViewingKey);
|
|
7966
|
+
if (!keys.selfEph && !canScanOutgoing && counterparties.length === 0) return null;
|
|
7967
|
+
const cacheKey = `${keys.spendingKey.toString(16)}:${keys.selfEph ? selfSize : 0}:${pairSize}:${canScanOutgoing ? outSize : 0}:` + counterparties.map((c) => toHex(c)).join(",");
|
|
7641
7968
|
if (cachedWindow?.cacheKey === cacheKey) return cachedWindow.window;
|
|
7642
7969
|
try {
|
|
7643
7970
|
const ivkPacked = deriveViewingPublicKey(keys.viewingKey);
|
|
7644
7971
|
const byEphPk = /* @__PURE__ */ new Map();
|
|
7972
|
+
const outgoingByEphPk = /* @__PURE__ */ new Map();
|
|
7645
7973
|
if (keys.selfEph) {
|
|
7646
|
-
for (const e of selfEphWindow(keys.
|
|
7974
|
+
for (const e of selfEphWindow(keys.viewingKey, ivkPacked, 0, selfSize)) {
|
|
7647
7975
|
byEphPk.set(e.ephPkHex.toLowerCase(), {
|
|
7648
7976
|
sharedSecret: e.sharedSecret,
|
|
7649
7977
|
index: e.index,
|
|
@@ -7664,7 +7992,12 @@ function getKnownEphWindow(keys) {
|
|
|
7664
7992
|
}
|
|
7665
7993
|
}
|
|
7666
7994
|
}
|
|
7667
|
-
|
|
7995
|
+
if (canScanOutgoing) {
|
|
7996
|
+
for (const e of outgoingEphWindow(keys.outgoingViewingKey, 0, outSize)) {
|
|
7997
|
+
outgoingByEphPk.set(e.ephPkHex.toLowerCase(), e.index);
|
|
7998
|
+
}
|
|
7999
|
+
}
|
|
8000
|
+
cachedWindow = { cacheKey, window: { byEphPk, outgoingByEphPk } };
|
|
7668
8001
|
return cachedWindow.window;
|
|
7669
8002
|
} catch {
|
|
7670
8003
|
return null;
|
|
@@ -7673,21 +8006,32 @@ function getKnownEphWindow(keys) {
|
|
|
7673
8006
|
|
|
7674
8007
|
// src/wallet/worker/kernel/decryptBatch.ts
|
|
7675
8008
|
function hintEphPkHex(hint) {
|
|
7676
|
-
if (hint.ephPkHex) return hint.ephPkHex.toLowerCase();
|
|
7677
8009
|
const memo = hint.encryptedMemo;
|
|
7678
8010
|
if (!memo || memo.length < 66) return null;
|
|
7679
8011
|
return ("0x" + memo.slice(-64)).toLowerCase();
|
|
7680
8012
|
}
|
|
8013
|
+
function sealedBookEntry(note) {
|
|
8014
|
+
if (note.sourcePk === void 0 || note.sourcePk === 0n) return null;
|
|
8015
|
+
return note.sourcePk;
|
|
8016
|
+
}
|
|
7681
8017
|
function decryptHintBatch(hints, keys) {
|
|
7682
8018
|
const activation = keys.viewTagActivationLeaf ?? null;
|
|
7683
8019
|
const knownWindow = getKnownEphWindow(keys);
|
|
8020
|
+
const sentNotes = [];
|
|
8021
|
+
const learnedRecipients = /* @__PURE__ */ new Set();
|
|
7684
8022
|
let tagFiltered = 0;
|
|
7685
8023
|
let selfMatched = 0;
|
|
7686
8024
|
let pairwiseMatched = 0;
|
|
7687
8025
|
let maxSelfEphIndex = null;
|
|
8026
|
+
let maxOutgoingEphIndex = null;
|
|
8027
|
+
const sealedEntries = [];
|
|
8028
|
+
const pendingSent = [];
|
|
8029
|
+
const seenSent = /* @__PURE__ */ new Set();
|
|
8030
|
+
const unmatchedSent = [];
|
|
7688
8031
|
const notes = hints.map((hint) => {
|
|
7689
8032
|
try {
|
|
7690
|
-
const
|
|
8033
|
+
const ephPkHex = hintEphPkHex(hint) ?? "";
|
|
8034
|
+
const known = knownWindow?.byEphPk.get(ephPkHex);
|
|
7691
8035
|
if (known) {
|
|
7692
8036
|
const result2 = tryDecryptNoteVerbose(
|
|
7693
8037
|
hint,
|
|
@@ -7702,12 +8046,26 @@ function decryptHintBatch(hints, keys) {
|
|
|
7702
8046
|
if (maxSelfEphIndex === null || known.index > maxSelfEphIndex) {
|
|
7703
8047
|
maxSelfEphIndex = known.index;
|
|
7704
8048
|
}
|
|
8049
|
+
const sealed = sealedBookEntry(result2.note);
|
|
8050
|
+
if (sealed !== null) sealedEntries.push(sealed);
|
|
7705
8051
|
} else {
|
|
7706
8052
|
pairwiseMatched++;
|
|
7707
8053
|
}
|
|
7708
8054
|
return result2.note;
|
|
7709
8055
|
}
|
|
7710
8056
|
}
|
|
8057
|
+
const outgoingIndex = knownWindow?.outgoingByEphPk.get(ephPkHex);
|
|
8058
|
+
if (outgoingIndex !== void 0) {
|
|
8059
|
+
if (maxOutgoingEphIndex === null || outgoingIndex > maxOutgoingEphIndex) {
|
|
8060
|
+
maxOutgoingEphIndex = outgoingIndex;
|
|
8061
|
+
}
|
|
8062
|
+
const key = hint.commitmentHex.toLowerCase();
|
|
8063
|
+
if (!seenSent.has(key)) {
|
|
8064
|
+
seenSent.add(key);
|
|
8065
|
+
pendingSent.push({ hint, ephIndex: outgoingIndex });
|
|
8066
|
+
}
|
|
8067
|
+
return null;
|
|
8068
|
+
}
|
|
7711
8069
|
const viewTag = activation !== null && hint.leafIndex >= activation;
|
|
7712
8070
|
const result = tryDecryptNoteVerbose(
|
|
7713
8071
|
hint,
|
|
@@ -7722,7 +8080,44 @@ function decryptHintBatch(hints, keys) {
|
|
|
7722
8080
|
return null;
|
|
7723
8081
|
}
|
|
7724
8082
|
});
|
|
7725
|
-
|
|
8083
|
+
const carried = keys.recipientCandidates ?? [];
|
|
8084
|
+
for (const { hint, ephIndex } of pendingSent) {
|
|
8085
|
+
const candidates = [
|
|
8086
|
+
...sealedEntries.map(
|
|
8087
|
+
(e) => openRecipientBookEntry(e, keys.outgoingViewingKey, hint.commitmentHex)
|
|
8088
|
+
),
|
|
8089
|
+
...carried
|
|
8090
|
+
];
|
|
8091
|
+
const sent = recoverSentNote({
|
|
8092
|
+
hint,
|
|
8093
|
+
outgoingViewingKey: keys.outgoingViewingKey,
|
|
8094
|
+
ephIndex,
|
|
8095
|
+
recipientCandidates: candidates
|
|
8096
|
+
});
|
|
8097
|
+
if (!sent) {
|
|
8098
|
+
unmatchedSent.push({ hint, ephIndex });
|
|
8099
|
+
continue;
|
|
8100
|
+
}
|
|
8101
|
+
const { recipientIvk, ...facts } = sent;
|
|
8102
|
+
learnedRecipients.add(toHex(recipientIvk));
|
|
8103
|
+
sentNotes.push({
|
|
8104
|
+
...facts,
|
|
8105
|
+
counterpartyIvkHex: toHex(recipientIvk),
|
|
8106
|
+
encryptedMemo: hint.encryptedMemo ?? ""
|
|
8107
|
+
});
|
|
8108
|
+
}
|
|
8109
|
+
return {
|
|
8110
|
+
notes,
|
|
8111
|
+
tagFiltered,
|
|
8112
|
+
selfMatched,
|
|
8113
|
+
pairwiseMatched,
|
|
8114
|
+
maxSelfEphIndex,
|
|
8115
|
+
maxOutgoingEphIndex,
|
|
8116
|
+
sentNotes,
|
|
8117
|
+
learnedRecipients: [...learnedRecipients],
|
|
8118
|
+
unmatchedSent,
|
|
8119
|
+
sealedBookEntries: sealedEntries.map((e) => e.toString())
|
|
8120
|
+
};
|
|
7726
8121
|
}
|
|
7727
8122
|
|
|
7728
8123
|
// src/wallet/worker/pool/types.ts
|
|
@@ -7744,21 +8139,52 @@ function createMainThreadPool() {
|
|
|
7744
8139
|
let selfMatched = 0;
|
|
7745
8140
|
let pairwiseMatched = 0;
|
|
7746
8141
|
let maxSelfEphIndex = null;
|
|
8142
|
+
let maxOutgoingEphIndex = null;
|
|
8143
|
+
const sentNotes = [];
|
|
8144
|
+
const unmatchedSent = [];
|
|
8145
|
+
const sealedBookEntries = /* @__PURE__ */ new Set();
|
|
8146
|
+
const learnedRecipients = new Set(
|
|
8147
|
+
(keys.recipientCandidates ?? []).map((c) => toHex(c))
|
|
8148
|
+
);
|
|
7747
8149
|
for (let i = 0; i < hints.length; i += DECRYPT_YIELD_EVERY) {
|
|
7748
8150
|
if (i > 0) {
|
|
7749
8151
|
await yieldToBrowser();
|
|
7750
8152
|
if (signal?.aborted) throw scanAbortError();
|
|
7751
8153
|
}
|
|
7752
|
-
const burst = decryptHintBatch(hints.slice(i, i + DECRYPT_YIELD_EVERY),
|
|
8154
|
+
const burst = decryptHintBatch(hints.slice(i, i + DECRYPT_YIELD_EVERY), {
|
|
8155
|
+
...keys,
|
|
8156
|
+
recipientCandidates: [...learnedRecipients].map((h) => fromHex(h))
|
|
8157
|
+
});
|
|
7753
8158
|
notes.push(...burst.notes);
|
|
7754
8159
|
tagFiltered += burst.tagFiltered;
|
|
7755
8160
|
selfMatched += burst.selfMatched;
|
|
7756
8161
|
pairwiseMatched += burst.pairwiseMatched;
|
|
8162
|
+
sentNotes.push(...burst.sentNotes ?? []);
|
|
8163
|
+
unmatchedSent.push(...burst.unmatchedSent ?? []);
|
|
8164
|
+
for (const e of burst.sealedBookEntries ?? []) sealedBookEntries.add(e);
|
|
8165
|
+
for (const r of burst.learnedRecipients ?? []) learnedRecipients.add(r);
|
|
7757
8166
|
if (burst.maxSelfEphIndex !== null) {
|
|
7758
8167
|
maxSelfEphIndex = Math.max(maxSelfEphIndex ?? -1, burst.maxSelfEphIndex);
|
|
7759
8168
|
}
|
|
8169
|
+
if (burst.maxOutgoingEphIndex != null) {
|
|
8170
|
+
maxOutgoingEphIndex = Math.max(
|
|
8171
|
+
maxOutgoingEphIndex ?? -1,
|
|
8172
|
+
burst.maxOutgoingEphIndex
|
|
8173
|
+
);
|
|
8174
|
+
}
|
|
7760
8175
|
}
|
|
7761
|
-
return {
|
|
8176
|
+
return {
|
|
8177
|
+
notes,
|
|
8178
|
+
tagFiltered,
|
|
8179
|
+
selfMatched,
|
|
8180
|
+
pairwiseMatched,
|
|
8181
|
+
maxSelfEphIndex,
|
|
8182
|
+
maxOutgoingEphIndex,
|
|
8183
|
+
sentNotes,
|
|
8184
|
+
learnedRecipients: [...learnedRecipients],
|
|
8185
|
+
unmatchedSent,
|
|
8186
|
+
sealedBookEntries: [...sealedBookEntries]
|
|
8187
|
+
};
|
|
7762
8188
|
},
|
|
7763
8189
|
terminate() {
|
|
7764
8190
|
clearKnownEphWindow();
|
|
@@ -7790,7 +8216,12 @@ function runOnWorker(worker, payload, signal) {
|
|
|
7790
8216
|
tagFiltered: data.tagFiltered ?? 0,
|
|
7791
8217
|
selfMatched: data.selfMatched ?? 0,
|
|
7792
8218
|
pairwiseMatched: data.pairwiseMatched ?? 0,
|
|
7793
|
-
maxSelfEphIndex: data.maxSelfEphIndex ?? null
|
|
8219
|
+
maxSelfEphIndex: data.maxSelfEphIndex ?? null,
|
|
8220
|
+
maxOutgoingEphIndex: data.maxOutgoingEphIndex ?? null,
|
|
8221
|
+
sentNotes: data.sentNotes ?? [],
|
|
8222
|
+
learnedRecipients: data.learnedRecipients ?? [],
|
|
8223
|
+
unmatchedSent: data.unmatchedSent ?? [],
|
|
8224
|
+
sealedBookEntries: data.sealedBookEntries ?? []
|
|
7794
8225
|
});
|
|
7795
8226
|
};
|
|
7796
8227
|
worker.onerror = () => {
|
|
@@ -7817,9 +8248,19 @@ function mergeResults(results) {
|
|
|
7817
8248
|
tagFiltered: results.reduce((sum, r) => sum + r.tagFiltered, 0),
|
|
7818
8249
|
selfMatched: results.reduce((sum, r) => sum + r.selfMatched, 0),
|
|
7819
8250
|
pairwiseMatched: results.reduce((sum, r) => sum + r.pairwiseMatched, 0),
|
|
8251
|
+
sentNotes: results.flatMap((r) => r.sentNotes ?? []),
|
|
8252
|
+
// Deduped: slices run in parallel, so the same change note cannot be
|
|
8253
|
+
// seen twice, but two different ones may name the same recipient.
|
|
8254
|
+
learnedRecipients: [...new Set(results.flatMap((r) => r.learnedRecipients ?? []))],
|
|
8255
|
+
unmatchedSent: results.flatMap((r) => r.unmatchedSent ?? []),
|
|
8256
|
+
sealedBookEntries: [...new Set(results.flatMap((r) => r.sealedBookEntries ?? []))],
|
|
7820
8257
|
maxSelfEphIndex: results.reduce(
|
|
7821
8258
|
(max, r) => r.maxSelfEphIndex === null ? max : Math.max(max ?? -1, r.maxSelfEphIndex),
|
|
7822
8259
|
null
|
|
8260
|
+
),
|
|
8261
|
+
maxOutgoingEphIndex: results.reduce(
|
|
8262
|
+
(max, r) => r.maxOutgoingEphIndex == null ? max : Math.max(max ?? -1, r.maxOutgoingEphIndex),
|
|
8263
|
+
null
|
|
7823
8264
|
)
|
|
7824
8265
|
};
|
|
7825
8266
|
}
|
|
@@ -7883,25 +8324,36 @@ var OrbinumWallet = class {
|
|
|
7883
8324
|
session;
|
|
7884
8325
|
notes;
|
|
7885
8326
|
vaultStore;
|
|
7886
|
-
/**
|
|
7887
|
-
|
|
8327
|
+
/**
|
|
8328
|
+
* The identity this wallet is unlocked as, or null.
|
|
8329
|
+
*
|
|
8330
|
+
* Held as a bundle rather than a single scalar because v3 keys are SIBLINGS:
|
|
8331
|
+
* the spending key no longer produces the viewing key, so there is nothing
|
|
8332
|
+
* left to derive the others from once unlock returns. Keeping the root out
|
|
8333
|
+
* of this object is deliberate — the branches are what the wallet needs, and
|
|
8334
|
+
* the root is the thing worth not retaining.
|
|
8335
|
+
*/
|
|
8336
|
+
identity = null;
|
|
7888
8337
|
/** The underlying store, for operations the facade does not cover. */
|
|
7889
8338
|
get vault() {
|
|
7890
8339
|
return this.vaultStore;
|
|
7891
8340
|
}
|
|
7892
8341
|
get unlocked() {
|
|
7893
|
-
return this.session.unlocked && this.
|
|
8342
|
+
return this.session.unlocked && this.identity !== null;
|
|
7894
8343
|
}
|
|
7895
8344
|
/**
|
|
7896
8345
|
* Opens the wallet from the master key bytes a `SpendingKeyRequest`
|
|
7897
8346
|
* signature produces — the same value `PrivacyKeyManager.getMasterBytes()`
|
|
7898
8347
|
* returns.
|
|
7899
8348
|
*
|
|
7900
|
-
* Master bytes rather than the spending-key scalar, deliberately: the
|
|
7901
|
-
* is the
|
|
7902
|
-
*
|
|
7903
|
-
*
|
|
7904
|
-
*
|
|
8349
|
+
* Master bytes rather than the spending-key scalar, deliberately: the master
|
|
8350
|
+
* is the root EVERY branch hangs off — spending key, viewing key, outgoing
|
|
8351
|
+
* viewing key and vault key are each their own HKDF of it, none derived
|
|
8352
|
+
* from another. Only `ownerPk` comes from the spending scalar, because the
|
|
8353
|
+
* circuit defines it that way.
|
|
8354
|
+
*
|
|
8355
|
+
* It is also what keeps a stored vault readable across a modulus change:
|
|
8356
|
+
* the vault key is derived pre-reduction.
|
|
7905
8357
|
*
|
|
7906
8358
|
* Reports whether the stored vault was RESET: a different chain, a different
|
|
7907
8359
|
* key, or notes the chain no longer recognises. When it was, the wallet
|
|
@@ -7915,14 +8367,12 @@ var OrbinumWallet = class {
|
|
|
7915
8367
|
* against a vault that was never read.
|
|
7916
8368
|
*/
|
|
7917
8369
|
async unlock(masterBytes, options = {}) {
|
|
7918
|
-
|
|
7919
|
-
throw new Error(`Master key must be 32 bytes, got ${masterBytes.length}.`);
|
|
7920
|
-
}
|
|
8370
|
+
assertSecretKeyBytes(masterBytes, "Master key");
|
|
7921
8371
|
this.session.open(
|
|
7922
8372
|
await deriveVaultKey(masterBytes),
|
|
7923
8373
|
await deriveVaultBlindKey(masterBytes)
|
|
7924
8374
|
);
|
|
7925
|
-
this.
|
|
8375
|
+
this.identity = deriveIdentity(masterBytes, this.config.identityVersion ?? "v3");
|
|
7926
8376
|
try {
|
|
7927
8377
|
return await this.vaultStore.unlock(this.session.cryptoKey, options);
|
|
7928
8378
|
} catch (err) {
|
|
@@ -7933,8 +8383,12 @@ var OrbinumWallet = class {
|
|
|
7933
8383
|
/** Drops the in-memory keys. The stored vault is untouched. */
|
|
7934
8384
|
lock() {
|
|
7935
8385
|
this.session.lock();
|
|
7936
|
-
this.
|
|
8386
|
+
this.identity = null;
|
|
7937
8387
|
this.notes.set([]);
|
|
8388
|
+
try {
|
|
8389
|
+
this.config.pool.terminate();
|
|
8390
|
+
} catch {
|
|
8391
|
+
}
|
|
7938
8392
|
}
|
|
7939
8393
|
/** The notes currently held. Empty while locked. */
|
|
7940
8394
|
getNotes() {
|
|
@@ -7946,11 +8400,8 @@ var OrbinumWallet = class {
|
|
|
7946
8400
|
}
|
|
7947
8401
|
/** This wallet's privacy identity: the keys a payer needs to address it. */
|
|
7948
8402
|
privacyKeys() {
|
|
7949
|
-
const
|
|
7950
|
-
return {
|
|
7951
|
-
ownerPk: deriveOwnerPk(spendingKey),
|
|
7952
|
-
viewingPublicKey: deriveViewingPublicKey(deriveViewingSecretKey(spendingKey))
|
|
7953
|
-
};
|
|
8403
|
+
const { ownerPk, viewingPublicKey } = this.requireIdentity();
|
|
8404
|
+
return { ownerPk, viewingPublicKey };
|
|
7954
8405
|
}
|
|
7955
8406
|
/**
|
|
7956
8407
|
* Walks the feed, recovers this wallet's notes, and persists them with their
|
|
@@ -7960,15 +8411,21 @@ var OrbinumWallet = class {
|
|
|
7960
8411
|
* `sinceLeafIndex` for a full scan, which is what a restore needs.
|
|
7961
8412
|
*/
|
|
7962
8413
|
async scan(options = {}) {
|
|
7963
|
-
const
|
|
7964
|
-
const viewingKey = deriveViewingSecretKey(spendingKey);
|
|
8414
|
+
const identity = this.requireIdentity();
|
|
7965
8415
|
return runScan({
|
|
7966
8416
|
vault: this.vaultStore,
|
|
7967
8417
|
storage: this.config.storage,
|
|
7968
8418
|
hints: this.config.hints,
|
|
7969
8419
|
nullifiers: this.config.nullifiers,
|
|
7970
8420
|
pool: this.config.pool,
|
|
7971
|
-
keys: {
|
|
8421
|
+
keys: {
|
|
8422
|
+
viewingKey: identity.viewingSecretKey,
|
|
8423
|
+
spendingKey: identity.spendingKey,
|
|
8424
|
+
ownerPk: identity.ownerPk,
|
|
8425
|
+
// Absent on a watch-only identity: the scan then finds every
|
|
8426
|
+
// note the wallet owns and reconstructs no payment history.
|
|
8427
|
+
...identity.outgoingViewingKey ? { outgoingViewingKey: identity.outgoingViewingKey } : {}
|
|
8428
|
+
},
|
|
7972
8429
|
sinceLeafIndex: options.sinceLeafIndex,
|
|
7973
8430
|
viewTagActivationLeaf: this.config.viewTagActivationLeaf,
|
|
7974
8431
|
onProgress: options.onProgress,
|
|
@@ -7990,13 +8447,14 @@ var OrbinumWallet = class {
|
|
|
7990
8447
|
* reusing an index would republish an ephPk and link the two notes.
|
|
7991
8448
|
*/
|
|
7992
8449
|
async buildNote(params) {
|
|
7993
|
-
const
|
|
8450
|
+
const identity = this.requireIdentity();
|
|
7994
8451
|
return buildZkNote(params, {
|
|
7995
8452
|
keys: {
|
|
7996
|
-
ownerPk:
|
|
7997
|
-
spendingKey,
|
|
7998
|
-
viewingPublicKey:
|
|
7999
|
-
viewingSecretKey:
|
|
8453
|
+
ownerPk: identity.ownerPk,
|
|
8454
|
+
spendingKey: identity.spendingKey,
|
|
8455
|
+
viewingPublicKey: identity.viewingPublicKey,
|
|
8456
|
+
viewingSecretKey: identity.viewingSecretKey,
|
|
8457
|
+
...identity.outgoingViewingKey ? { outgoingViewingKey: identity.outgoingViewingKey } : {}
|
|
8000
8458
|
},
|
|
8001
8459
|
storage: this.config.storage
|
|
8002
8460
|
});
|
|
@@ -8013,11 +8471,12 @@ var OrbinumWallet = class {
|
|
|
8013
8471
|
* Treat the result as secret: it can spend every note this wallet holds.
|
|
8014
8472
|
*/
|
|
8015
8473
|
spendKeys() {
|
|
8016
|
-
const
|
|
8474
|
+
const identity = this.requireIdentity();
|
|
8017
8475
|
return {
|
|
8018
|
-
spendingKey,
|
|
8019
|
-
viewingSecretKey:
|
|
8020
|
-
ownerPk:
|
|
8476
|
+
spendingKey: identity.spendingKey,
|
|
8477
|
+
viewingSecretKey: identity.viewingSecretKey,
|
|
8478
|
+
ownerPk: identity.ownerPk,
|
|
8479
|
+
...identity.outgoingViewingKey ? { outgoingViewingKey: identity.outgoingViewingKey } : {}
|
|
8021
8480
|
};
|
|
8022
8481
|
}
|
|
8023
8482
|
/**
|
|
@@ -8039,7 +8498,20 @@ var OrbinumWallet = class {
|
|
|
8039
8498
|
* verifying-key rotation, and the failure surfaces much later.
|
|
8040
8499
|
*/
|
|
8041
8500
|
buildOutputNote = async (params) => {
|
|
8042
|
-
|
|
8501
|
+
const identity = this.requireIdentity();
|
|
8502
|
+
return buildZkNoteWithIndex(
|
|
8503
|
+
{ ...params, circuitVersion: await this.circuitVersion() },
|
|
8504
|
+
{
|
|
8505
|
+
keys: {
|
|
8506
|
+
ownerPk: identity.ownerPk,
|
|
8507
|
+
spendingKey: identity.spendingKey,
|
|
8508
|
+
viewingPublicKey: identity.viewingPublicKey,
|
|
8509
|
+
viewingSecretKey: identity.viewingSecretKey,
|
|
8510
|
+
...identity.outgoingViewingKey ? { outgoingViewingKey: identity.outgoingViewingKey } : {}
|
|
8511
|
+
},
|
|
8512
|
+
storage: this.config.storage
|
|
8513
|
+
}
|
|
8514
|
+
);
|
|
8043
8515
|
};
|
|
8044
8516
|
/** The circuit version a new note should carry. */
|
|
8045
8517
|
async circuitVersion() {
|
|
@@ -8051,11 +8523,11 @@ var OrbinumWallet = class {
|
|
|
8051
8523
|
}
|
|
8052
8524
|
return chainActiveCircuitVersion(this.config.zkVerifier);
|
|
8053
8525
|
}
|
|
8054
|
-
|
|
8055
|
-
if (this.
|
|
8526
|
+
requireIdentity() {
|
|
8527
|
+
if (this.identity === null) {
|
|
8056
8528
|
throw new Error("Wallet is locked. Call unlock() before using it.");
|
|
8057
8529
|
}
|
|
8058
|
-
return this.
|
|
8530
|
+
return this.identity;
|
|
8059
8531
|
}
|
|
8060
8532
|
};
|
|
8061
8533
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -8087,9 +8559,8 @@ var OrbinumWallet = class {
|
|
|
8087
8559
|
NATIVE_ASSET_ID,
|
|
8088
8560
|
NOTE_BACKUP_VERSION,
|
|
8089
8561
|
NOTE_BIGINT_FIELDS,
|
|
8090
|
-
NOTE_TRANSFER_URI_SCHEME,
|
|
8091
8562
|
NoteBuilder,
|
|
8092
|
-
|
|
8563
|
+
OUTGOING_EPH_WINDOW,
|
|
8093
8564
|
OrbinumClient,
|
|
8094
8565
|
OrbinumClientProvider,
|
|
8095
8566
|
OrbinumWallet,
|
|
@@ -8099,7 +8570,6 @@ var OrbinumWallet = class {
|
|
|
8099
8570
|
PRECOMPILE_ADDR,
|
|
8100
8571
|
PrivacyKeyManager,
|
|
8101
8572
|
PrivacyModule,
|
|
8102
|
-
QR_PAGE_MAX_CHARS,
|
|
8103
8573
|
RECOVERED_TX_RESULT,
|
|
8104
8574
|
RelayerStatusModule,
|
|
8105
8575
|
RpcV2Module,
|
|
@@ -8124,7 +8594,7 @@ var OrbinumWallet = class {
|
|
|
8124
8594
|
addressToFieldElement,
|
|
8125
8595
|
applyBatch,
|
|
8126
8596
|
applyNoteStatus,
|
|
8127
|
-
|
|
8597
|
+
assertSecretKeyBytes,
|
|
8128
8598
|
base58,
|
|
8129
8599
|
base64UrlDecode,
|
|
8130
8600
|
base64UrlEncode,
|
|
@@ -8137,6 +8607,7 @@ var OrbinumWallet = class {
|
|
|
8137
8607
|
buildShieldBatchOperations,
|
|
8138
8608
|
buildShieldParams,
|
|
8139
8609
|
buildZkNote,
|
|
8610
|
+
buildZkNoteWithIndex,
|
|
8140
8611
|
bytesToBigintLE,
|
|
8141
8612
|
bytesToBjjScalar,
|
|
8142
8613
|
cacheSession,
|
|
@@ -8166,15 +8637,17 @@ var OrbinumWallet = class {
|
|
|
8166
8637
|
createWorkerPool,
|
|
8167
8638
|
decodeNoteBackup,
|
|
8168
8639
|
decodeNoteDisclosureKey,
|
|
8169
|
-
decodeNoteTransferPage,
|
|
8170
8640
|
decodePaymentSlip,
|
|
8171
8641
|
decodePrecompileCalldata,
|
|
8172
8642
|
decryptHintBatch,
|
|
8173
8643
|
decryptJson,
|
|
8174
8644
|
decryptNoteRecord,
|
|
8645
|
+
deriveIdentity,
|
|
8175
8646
|
deriveMasterKeyBytes,
|
|
8176
|
-
|
|
8177
|
-
|
|
8647
|
+
deriveOutgoingEphPk,
|
|
8648
|
+
deriveOutgoingEphSk,
|
|
8649
|
+
deriveOutgoingSharedSecret,
|
|
8650
|
+
deriveOutgoingViewingKeyV3,
|
|
8178
8651
|
deriveOwnerPk,
|
|
8179
8652
|
derivePairwiseEphSk,
|
|
8180
8653
|
derivePairwiseSharedSecret,
|
|
@@ -8183,6 +8656,7 @@ var OrbinumWallet = class {
|
|
|
8183
8656
|
deriveSpendingKeyFromSignature,
|
|
8184
8657
|
deriveSpendingKeyMessageV2,
|
|
8185
8658
|
deriveSpendingKeyTypedData,
|
|
8659
|
+
deriveSpendingKeyV3,
|
|
8186
8660
|
deriveStealthOwnerPk,
|
|
8187
8661
|
deriveStealthSk,
|
|
8188
8662
|
deriveVaultBlindKey,
|
|
@@ -8190,9 +8664,9 @@ var OrbinumWallet = class {
|
|
|
8190
8664
|
deriveViewTag,
|
|
8191
8665
|
deriveViewingPublicKey,
|
|
8192
8666
|
deriveViewingSecretKey,
|
|
8667
|
+
deriveViewingSecretKeyV3,
|
|
8193
8668
|
detectCommitmentMismatch,
|
|
8194
8669
|
encodeNoteBackup,
|
|
8195
|
-
encodeNoteTransferPages,
|
|
8196
8670
|
encodePaymentSlip,
|
|
8197
8671
|
encryptJson,
|
|
8198
8672
|
encryptNote,
|
|
@@ -8202,6 +8676,7 @@ var OrbinumWallet = class {
|
|
|
8202
8676
|
evmToImplicitSubstrate,
|
|
8203
8677
|
evmToMappedAccountHex,
|
|
8204
8678
|
evmToSubstrate,
|
|
8679
|
+
exportViewingCredential,
|
|
8205
8680
|
extractPalletError,
|
|
8206
8681
|
failed,
|
|
8207
8682
|
fastMulBase,
|
|
@@ -8242,9 +8717,9 @@ var OrbinumWallet = class {
|
|
|
8242
8717
|
isNativeAsset,
|
|
8243
8718
|
isNoteSelfConsistent,
|
|
8244
8719
|
isSpendable,
|
|
8245
|
-
isSs58,
|
|
8246
8720
|
isSubstrateAddress,
|
|
8247
8721
|
isUnifiedAddress,
|
|
8722
|
+
isUsableSecretKey,
|
|
8248
8723
|
isValidLeafIndex,
|
|
8249
8724
|
leHexToBigint,
|
|
8250
8725
|
mapExtrinsicArgs,
|
|
@@ -8261,10 +8736,10 @@ var OrbinumWallet = class {
|
|
|
8261
8736
|
noteMatchesCommitment,
|
|
8262
8737
|
noteOrigin,
|
|
8263
8738
|
noteSpentTxHash,
|
|
8264
|
-
noteToTransferEntry,
|
|
8265
8739
|
noteTxKind,
|
|
8266
|
-
openOutgoingBlob,
|
|
8267
8740
|
openPaymentSlip,
|
|
8741
|
+
openRecipientBookEntry,
|
|
8742
|
+
outgoingEphWindow,
|
|
8268
8743
|
outranks,
|
|
8269
8744
|
pairwiseEphWindow,
|
|
8270
8745
|
palletErrorKind,
|
|
@@ -8275,15 +8750,18 @@ var OrbinumWallet = class {
|
|
|
8275
8750
|
planTransfer,
|
|
8276
8751
|
planUnshield,
|
|
8277
8752
|
randomBlinding,
|
|
8278
|
-
|
|
8753
|
+
reconstructOutgoingIndex,
|
|
8279
8754
|
reconstructOutgoingTxRecords,
|
|
8280
8755
|
recoverOwnerPkPoint,
|
|
8281
8756
|
recoverSelfStealthNote,
|
|
8757
|
+
recoverSentFromSharedSecret,
|
|
8758
|
+
recoverSentNote,
|
|
8282
8759
|
refuseIfAlreadySpent,
|
|
8283
8760
|
regeneratePaymentSlip,
|
|
8761
|
+
registerPairwiseCounterparty,
|
|
8284
8762
|
removeByCommitment,
|
|
8285
8763
|
requireSessionKeys,
|
|
8286
|
-
|
|
8764
|
+
reserveOutgoingIndex,
|
|
8287
8765
|
reserveSelfEphIndex,
|
|
8288
8766
|
resolveSelfEphCeiling,
|
|
8289
8767
|
resolveSpentSet,
|
|
@@ -8292,8 +8770,8 @@ var OrbinumWallet = class {
|
|
|
8292
8770
|
runScan,
|
|
8293
8771
|
scalarToHex,
|
|
8294
8772
|
scanAbortError,
|
|
8295
|
-
sealOutgoingBlob,
|
|
8296
8773
|
sealPaymentSlip,
|
|
8774
|
+
sealRecipientBookEntry,
|
|
8297
8775
|
selectDescribingNote,
|
|
8298
8776
|
selectDescribingNoteByCommitment,
|
|
8299
8777
|
selectGhosts,
|
|
@@ -8319,10 +8797,10 @@ var OrbinumWallet = class {
|
|
|
8319
8797
|
truncateMiddle,
|
|
8320
8798
|
tryDecryptNote,
|
|
8321
8799
|
tryDecryptNoteVerbose,
|
|
8322
|
-
tryRecoverOutgoing,
|
|
8323
8800
|
txLandedAfterError,
|
|
8324
8801
|
u128,
|
|
8325
8802
|
u64,
|
|
8803
|
+
unpackUsableViewingKey,
|
|
8326
8804
|
unshieldNote,
|
|
8327
8805
|
upsertNote,
|
|
8328
8806
|
vaultReplacer,
|