@orbinum/sdk 2.1.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 +904 -485
- package/dist/index.d.ts +904 -485
- package/dist/index.js +958 -501
- package/dist/index.mjs +540 -302
- 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
|
-
|
|
2700
|
+
if (b.length < 5 || b[2] !== TIMESTAMP_PALLET || b[3] !== 0) return null;
|
|
2701
|
+
return decodeCompact(b, 4);
|
|
2559
2702
|
} catch {
|
|
2560
|
-
return
|
|
2703
|
+
return null;
|
|
2561
2704
|
}
|
|
2562
|
-
});
|
|
2563
|
-
if (
|
|
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;
|
|
2571
|
-
} catch {
|
|
2572
|
-
}
|
|
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) {
|
|
@@ -3606,7 +3745,13 @@ var PrivacyModule = class {
|
|
|
3606
3745
|
async getMerkleRoot() {
|
|
3607
3746
|
return this.substrate.request("privacy_getMerkleRoot", []);
|
|
3608
3747
|
}
|
|
3609
|
-
/**
|
|
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
|
+
*/
|
|
3610
3755
|
async getMerkleProof(leafIndex) {
|
|
3611
3756
|
const raw = await this.substrate.request("privacy_getMerkleProof", [
|
|
3612
3757
|
leafIndex
|
|
@@ -4556,9 +4701,13 @@ var OrbinumClientProvider = class _OrbinumClientProvider {
|
|
|
4556
4701
|
// ─── Reconnection ───────────────────────────────────────────────────────
|
|
4557
4702
|
/**
|
|
4558
4703
|
* Schedules the next connection attempt using exponential backoff
|
|
4559
|
-
* (capped at `reconnectMaxMs`) with
|
|
4560
|
-
* `'reconnecting'`.
|
|
4561
|
-
*
|
|
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.
|
|
4562
4711
|
*/
|
|
4563
4712
|
scheduleReconnect() {
|
|
4564
4713
|
if (this._reconnectTimer) clearTimeout(this._reconnectTimer);
|
|
@@ -4950,6 +5099,16 @@ function mapZkEventData(method, data) {
|
|
|
4950
5099
|
index: get(4, "leaf_index") || get(4, "index")
|
|
4951
5100
|
};
|
|
4952
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
|
+
}
|
|
4953
5112
|
if (m === "privatetransfer") {
|
|
4954
5113
|
return {
|
|
4955
5114
|
nullifiers: get(0, "nullifiers"),
|
|
@@ -4991,12 +5150,12 @@ function mapZkEventData(method, data) {
|
|
|
4991
5150
|
if (m_norm === "log") {
|
|
4992
5151
|
return { log: get(0, "log") };
|
|
4993
5152
|
}
|
|
4994
|
-
if (m_norm === "created" || m_norm === "createdfailed" || m_norm === "
|
|
5153
|
+
if (m_norm === "created" || m_norm === "createdfailed" || m_norm === "executedfailed") {
|
|
4995
5154
|
return { address: get(0, "address") };
|
|
4996
5155
|
}
|
|
4997
5156
|
if (m_norm === "executed") {
|
|
4998
5157
|
return {
|
|
4999
|
-
from: get(0, "from"),
|
|
5158
|
+
from: get(0, "from") ?? get(0, "address"),
|
|
5000
5159
|
to: get(1, "to"),
|
|
5001
5160
|
transaction_hash: get(2, "transaction_hash"),
|
|
5002
5161
|
exit_reason: get(3, "exit_reason")
|
|
@@ -5136,10 +5295,10 @@ var ERROR_KINDS = {
|
|
|
5136
5295
|
CommitmentAlreadyExists: "shape",
|
|
5137
5296
|
UnknownMerkleRoot: "stale-proof",
|
|
5138
5297
|
// Amounts and fees
|
|
5139
|
-
AmountTooSmall: "amount",
|
|
5140
5298
|
InvalidAmount: "amount",
|
|
5141
5299
|
InsufficientPoolBalance: "amount",
|
|
5142
5300
|
FeeTooLow: "amount",
|
|
5301
|
+
InsufficientPendingFees: "amount",
|
|
5143
5302
|
// Proofs
|
|
5144
5303
|
InvalidProof: "proof",
|
|
5145
5304
|
ProofVerificationFailed: "proof",
|
|
@@ -5148,12 +5307,14 @@ var ERROR_KINDS = {
|
|
|
5148
5307
|
InvalidAssetId: "asset",
|
|
5149
5308
|
AssetNotVerified: "asset",
|
|
5150
5309
|
AssetIdMismatch: "asset",
|
|
5310
|
+
AssetIdAlreadyExists: "asset",
|
|
5151
5311
|
// Request shape
|
|
5152
5312
|
InvalidMemoSize: "shape",
|
|
5153
5313
|
MemoCommitmentMismatch: "shape",
|
|
5154
5314
|
TooManyInputsOrOutputs: "shape",
|
|
5155
5315
|
InvalidRecipient: "shape",
|
|
5156
5316
|
EmptyBatch: "shape",
|
|
5317
|
+
FeeRecipientUnavailable: "shape",
|
|
5157
5318
|
// Capacity
|
|
5158
5319
|
MerkleTreeFull: "capacity",
|
|
5159
5320
|
// Balances pallet, reached through the same path
|
|
@@ -5512,6 +5673,7 @@ function buildConfig(existing, chainFingerprint) {
|
|
|
5512
5673
|
updatedAt: now,
|
|
5513
5674
|
...chainFingerprint ? { chainFingerprint } : {},
|
|
5514
5675
|
...existing?.selfEphCounter !== void 0 ? { selfEphCounter: existing.selfEphCounter } : {},
|
|
5676
|
+
...existing?.outgoingEphCounter !== void 0 ? { outgoingEphCounter: existing.outgoingEphCounter } : {},
|
|
5515
5677
|
...existing?.pairwiseCounterparties !== void 0 ? { pairwiseCounterparties: existing.pairwiseCounterparties } : {}
|
|
5516
5678
|
};
|
|
5517
5679
|
}
|
|
@@ -5523,6 +5685,7 @@ async function writeConfig(storage, fallback, chainFingerprint) {
|
|
|
5523
5685
|
}
|
|
5524
5686
|
function mergeCounters(current, fallback) {
|
|
5525
5687
|
const selfEphCounter = higher(current.selfEphCounter, fallback?.selfEphCounter);
|
|
5688
|
+
const outgoingEphCounter = higher(current.outgoingEphCounter, fallback?.outgoingEphCounter);
|
|
5526
5689
|
const parties = mergePairwise(current.pairwiseCounterparties, fallback?.pairwiseCounterparties);
|
|
5527
5690
|
return {
|
|
5528
5691
|
createdAt: current.createdAt ?? fallback?.createdAt,
|
|
@@ -5530,6 +5693,7 @@ function mergeCounters(current, fallback) {
|
|
|
5530
5693
|
// is exactOptionalPropertyTypes, so an explicit undefined is a type
|
|
5531
5694
|
// error AND would round-trip through structured clone as a present key.
|
|
5532
5695
|
...selfEphCounter !== void 0 ? { selfEphCounter } : {},
|
|
5696
|
+
...outgoingEphCounter !== void 0 ? { outgoingEphCounter } : {},
|
|
5533
5697
|
...parties !== void 0 ? { pairwiseCounterparties: parties } : {}
|
|
5534
5698
|
};
|
|
5535
5699
|
}
|
|
@@ -5567,6 +5731,7 @@ function mergePairwise(current, fallback) {
|
|
|
5567
5731
|
|
|
5568
5732
|
// src/wallet/vault/storage/ephemeralIndex.ts
|
|
5569
5733
|
var MAX_EPH_INDEX = 4294967294;
|
|
5734
|
+
var RESTORE_SAFETY_GAP = 8;
|
|
5570
5735
|
function usableCounter(value) {
|
|
5571
5736
|
if (typeof value !== "number" || !Number.isInteger(value)) return null;
|
|
5572
5737
|
if (value < 0 || value > MAX_EPH_INDEX) return null;
|
|
@@ -5581,27 +5746,51 @@ async function reserveSelfEphIndex(storage) {
|
|
|
5581
5746
|
if (!updated) throw new Error("vault not initialized");
|
|
5582
5747
|
return (usableCounter(updated.selfEphCounter) ?? 1) - 1;
|
|
5583
5748
|
}
|
|
5584
|
-
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) {
|
|
5585
5778
|
const key = ivkHex.toLowerCase();
|
|
5586
5779
|
const updated = await storage.updateConfig((config) => {
|
|
5587
|
-
const
|
|
5588
|
-
const addedAt = config.pairwiseCounterparties?.[key]?.addedAt;
|
|
5780
|
+
const existing = config.pairwiseCounterparties?.[key];
|
|
5589
5781
|
return {
|
|
5590
5782
|
...config,
|
|
5591
5783
|
pairwiseCounterparties: {
|
|
5592
5784
|
...config.pairwiseCounterparties,
|
|
5593
5785
|
[key]: {
|
|
5594
|
-
nextIndex: (
|
|
5595
|
-
addedAt: typeof addedAt === "number" ? addedAt : Date.now()
|
|
5786
|
+
nextIndex: usableCounter(existing?.nextIndex) ?? 0,
|
|
5787
|
+
addedAt: typeof existing?.addedAt === "number" ? existing.addedAt : Date.now()
|
|
5596
5788
|
}
|
|
5597
5789
|
},
|
|
5598
5790
|
updatedAt: Date.now()
|
|
5599
5791
|
};
|
|
5600
5792
|
});
|
|
5601
5793
|
if (!updated) throw new Error("vault not initialized");
|
|
5602
|
-
const nextIndex = usableCounter(updated.pairwiseCounterparties?.[key]?.nextIndex);
|
|
5603
|
-
if (nextIndex === null || nextIndex <= 1) return null;
|
|
5604
|
-
return nextIndex - 1;
|
|
5605
5794
|
}
|
|
5606
5795
|
|
|
5607
5796
|
// src/wallet/vault/notes/record.ts
|
|
@@ -6036,19 +6225,44 @@ async function processHints(ctx, hints, total) {
|
|
|
6036
6225
|
}
|
|
6037
6226
|
}
|
|
6038
6227
|
outcome.scanned++;
|
|
6039
|
-
if (!hint.encryptedMemo
|
|
6228
|
+
if (!hint.encryptedMemo) {
|
|
6040
6229
|
outcome.noMemo++;
|
|
6041
6230
|
continue;
|
|
6042
6231
|
}
|
|
6043
6232
|
toDecrypt.push(hint);
|
|
6044
6233
|
}
|
|
6045
|
-
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
|
+
);
|
|
6046
6250
|
outcome.tagFiltered += tagFiltered;
|
|
6047
6251
|
outcome.selfDiscovered += selfMatched;
|
|
6048
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);
|
|
6049
6257
|
if (maxSelfEphIndex !== null) {
|
|
6050
6258
|
outcome.maxSelfEphIndex = Math.max(outcome.maxSelfEphIndex ?? -1, maxSelfEphIndex);
|
|
6051
6259
|
}
|
|
6260
|
+
if (maxOutgoingEphIndex != null) {
|
|
6261
|
+
outcome.maxOutgoingEphIndex = Math.max(
|
|
6262
|
+
outcome.maxOutgoingEphIndex ?? -1,
|
|
6263
|
+
maxOutgoingEphIndex
|
|
6264
|
+
);
|
|
6265
|
+
}
|
|
6052
6266
|
const pageEntries = [];
|
|
6053
6267
|
for (const [i, decrypted] of notes.entries()) {
|
|
6054
6268
|
const hint = toDecrypt[i];
|
|
@@ -6145,7 +6359,12 @@ async function collectScanEntries(params) {
|
|
|
6145
6359
|
tagFiltered: 0,
|
|
6146
6360
|
selfDiscovered: 0,
|
|
6147
6361
|
pairwiseDiscovered: 0,
|
|
6148
|
-
|
|
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
|
|
6149
6368
|
};
|
|
6150
6369
|
const ctx = {
|
|
6151
6370
|
outcome,
|
|
@@ -6361,12 +6580,18 @@ function createScanCheckpoint(params) {
|
|
|
6361
6580
|
// src/wallet/worker/kernel/types.ts
|
|
6362
6581
|
var SELF_EPH_WINDOW = 1024;
|
|
6363
6582
|
var PAIRWISE_EPH_WINDOW = 64;
|
|
6583
|
+
var OUTGOING_EPH_WINDOW = 64;
|
|
6364
6584
|
var EMPTY_BATCH_RESULT = {
|
|
6365
6585
|
notes: [],
|
|
6366
6586
|
tagFiltered: 0,
|
|
6367
6587
|
selfMatched: 0,
|
|
6368
6588
|
pairwiseMatched: 0,
|
|
6369
|
-
maxSelfEphIndex: null
|
|
6589
|
+
maxSelfEphIndex: null,
|
|
6590
|
+
maxOutgoingEphIndex: null,
|
|
6591
|
+
sentNotes: [],
|
|
6592
|
+
learnedRecipients: [],
|
|
6593
|
+
unmatchedSent: [],
|
|
6594
|
+
sealedBookEntries: []
|
|
6370
6595
|
};
|
|
6371
6596
|
|
|
6372
6597
|
// src/wallet/scanner/selfEphGap.ts
|
|
@@ -6378,7 +6603,7 @@ function gapMargin(windowSize) {
|
|
|
6378
6603
|
return Math.max(1, Math.floor(windowSize / 16));
|
|
6379
6604
|
}
|
|
6380
6605
|
function resolveSelfEphCeiling(params) {
|
|
6381
|
-
const { notes,
|
|
6606
|
+
const { notes, viewingKey, scanMaxIndex } = params;
|
|
6382
6607
|
const windowSize = params.windowSize !== void 0 && params.windowSize > 0 ? params.windowSize : SELF_EPH_WINDOW;
|
|
6383
6608
|
if (scanMaxIndex === null) return null;
|
|
6384
6609
|
if (scanMaxIndex < windowSize - gapMargin(windowSize)) return scanMaxIndex;
|
|
@@ -6391,7 +6616,7 @@ function resolveSelfEphCeiling(params) {
|
|
|
6391
6616
|
let max = scanMaxIndex;
|
|
6392
6617
|
let from = windowSize;
|
|
6393
6618
|
for (; ; ) {
|
|
6394
|
-
const slice = selfEphWindow(
|
|
6619
|
+
const slice = selfEphWindow(viewingKey, ivkPacked, from, windowSize);
|
|
6395
6620
|
let matched = false;
|
|
6396
6621
|
for (const entry of slice) {
|
|
6397
6622
|
if (ownEphPks.has(entry.ephPkHex.toLowerCase())) {
|
|
@@ -6404,12 +6629,12 @@ function resolveSelfEphCeiling(params) {
|
|
|
6404
6629
|
}
|
|
6405
6630
|
return max;
|
|
6406
6631
|
}
|
|
6407
|
-
var
|
|
6632
|
+
var MAX_EPH_WINDOW2 = SELF_EPH_WINDOW * 64;
|
|
6408
6633
|
function windowSizeForCounter(counter) {
|
|
6409
6634
|
const used = Number.isFinite(counter) ? counter : 0;
|
|
6410
6635
|
const needed = used + gapMargin(SELF_EPH_WINDOW);
|
|
6411
6636
|
const rounded = Math.ceil(needed / SELF_EPH_WINDOW) * SELF_EPH_WINDOW;
|
|
6412
|
-
return Math.min(
|
|
6637
|
+
return Math.min(MAX_EPH_WINDOW2, Math.max(SELF_EPH_WINDOW, rounded));
|
|
6413
6638
|
}
|
|
6414
6639
|
|
|
6415
6640
|
// src/wallet/scanner/phases/spentStatus.ts
|
|
@@ -6447,6 +6672,8 @@ async function resolveSpentStatus(params) {
|
|
|
6447
6672
|
}
|
|
6448
6673
|
|
|
6449
6674
|
// src/wallet/scanner/pipeline.ts
|
|
6675
|
+
var OUTGOING_RESCAN_GAP = 8;
|
|
6676
|
+
var RETRY_BOOK_LIMIT = 64;
|
|
6450
6677
|
async function runScan(params) {
|
|
6451
6678
|
const { vault, storage, hints, nullifiers, pool, keys, sinceLeafIndex, signal, onWarning } = params;
|
|
6452
6679
|
const isIncremental = sinceLeafIndex !== void 0;
|
|
@@ -6454,6 +6681,7 @@ async function runScan(params) {
|
|
|
6454
6681
|
const preScanHexes = new Set(existingHexes);
|
|
6455
6682
|
const config = await storage.getConfig().catch(() => null);
|
|
6456
6683
|
const selfEphWindowSize = windowSizeForCounter(config?.selfEphCounter ?? 0);
|
|
6684
|
+
const outgoingEphWindowSize = windowSizeForCounter(config?.outgoingEphCounter ?? 0);
|
|
6457
6685
|
const pairwiseCounterparties = Object.keys(config?.pairwiseCounterparties ?? {}).flatMap(
|
|
6458
6686
|
(hex) => {
|
|
6459
6687
|
try {
|
|
@@ -6482,7 +6710,12 @@ async function runScan(params) {
|
|
|
6482
6710
|
// worth it on full scans and restores, waste on small incremental ticks.
|
|
6483
6711
|
selfEph: !isIncremental,
|
|
6484
6712
|
selfEphWindowSize,
|
|
6485
|
-
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
|
|
6486
6719
|
},
|
|
6487
6720
|
existingHexes,
|
|
6488
6721
|
sinceLeafIndex,
|
|
@@ -6496,6 +6729,49 @@ async function runScan(params) {
|
|
|
6496
6729
|
onPage: (entries) => checkpoint.savePageNotes(entries),
|
|
6497
6730
|
onBatchDone: (maxLeafIndex) => checkpoint.advanceCursor(maxLeafIndex)
|
|
6498
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
|
+
}
|
|
6499
6775
|
const nullifiersToQuery = collectNullifiersToQuery(
|
|
6500
6776
|
scan.scanEntries,
|
|
6501
6777
|
scan.onChainHexes,
|
|
@@ -6525,7 +6801,6 @@ async function runScan(params) {
|
|
|
6525
6801
|
try {
|
|
6526
6802
|
ceiling = resolveSelfEphCeiling({
|
|
6527
6803
|
notes: vault.getAll(),
|
|
6528
|
-
spendingKey: keys.spendingKey,
|
|
6529
6804
|
viewingKey: keys.viewingKey,
|
|
6530
6805
|
scanMaxIndex: scan.maxSelfEphIndex,
|
|
6531
6806
|
windowSize: selfEphWindowSize
|
|
@@ -6535,6 +6810,13 @@ async function runScan(params) {
|
|
|
6535
6810
|
await bumpSelfEphCounterTo(storage, ceiling + 1).catch(() => {
|
|
6536
6811
|
});
|
|
6537
6812
|
}
|
|
6813
|
+
if (scan.maxOutgoingEphIndex !== null) {
|
|
6814
|
+
await bumpOutgoingCounterTo(
|
|
6815
|
+
storage,
|
|
6816
|
+
scan.maxOutgoingEphIndex + 1 + OUTGOING_RESCAN_GAP
|
|
6817
|
+
).catch(() => {
|
|
6818
|
+
});
|
|
6819
|
+
}
|
|
6538
6820
|
if (scan.decryptFailed > 0 && scan.found === 0 && scan.alreadyPresent === 0) {
|
|
6539
6821
|
onWarning?.(
|
|
6540
6822
|
`no notes recovered from ${scan.decryptFailed} hint(s) with memos; if this wallet should hold notes, check the keys it was unlocked with`
|
|
@@ -6546,7 +6828,13 @@ async function runScan(params) {
|
|
|
6546
6828
|
decryptFailed: scan.decryptFailed,
|
|
6547
6829
|
alreadyPresent: scan.alreadyPresent,
|
|
6548
6830
|
purged,
|
|
6549
|
-
incremental: isIncremental
|
|
6831
|
+
incremental: isIncremental,
|
|
6832
|
+
sentNotes: scan.sentNotes,
|
|
6833
|
+
discovery: {
|
|
6834
|
+
self: scan.selfDiscovered,
|
|
6835
|
+
pairwise: scan.pairwiseDiscovered,
|
|
6836
|
+
tagFiltered: scan.tagFiltered
|
|
6837
|
+
}
|
|
6550
6838
|
};
|
|
6551
6839
|
}
|
|
6552
6840
|
async function bumpSelfEphCounterTo(storage, next) {
|
|
@@ -6554,6 +6842,11 @@ async function bumpSelfEphCounterTo(storage, next) {
|
|
|
6554
6842
|
(config) => (config.selfEphCounter ?? 0) >= next ? config : { ...config, selfEphCounter: next, updatedAt: Date.now() }
|
|
6555
6843
|
);
|
|
6556
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
|
+
}
|
|
6557
6850
|
|
|
6558
6851
|
// src/wallet/scanner/history/extrinsicFacts.ts
|
|
6559
6852
|
var UNKNOWN = { fee: null, success: true };
|
|
@@ -6692,10 +6985,8 @@ function regeneratePaymentSlip(facts, recipientIvkPacked, txHash) {
|
|
|
6692
6985
|
commitmentHex: facts.commitmentHex,
|
|
6693
6986
|
encryptedMemo: facts.encryptedMemo,
|
|
6694
6987
|
...facts.leafIndex !== void 0 ? { leafIndex: facts.leafIndex } : {},
|
|
6695
|
-
//
|
|
6988
|
+
// The recipient renders this as an explorer link, so an unconstrained
|
|
6696
6989
|
// string is a URL injection wearing the authority of a decrypted slip.
|
|
6697
|
-
// Dropped rather than fatal: the slip still rebuilds the note, which is
|
|
6698
|
-
// the part that matters.
|
|
6699
6990
|
...isHexOfLength(txHash, 32) ? { txHash } : {}
|
|
6700
6991
|
});
|
|
6701
6992
|
return encodePaymentSlip(envelope);
|
|
@@ -6720,15 +7011,37 @@ async function loadExistingRecords(vault) {
|
|
|
6720
7011
|
function recordKey(transfer) {
|
|
6721
7012
|
return transfer.hash ?? `${transfer.blockNumber}-${transfer.extrinsicIndex ?? 0}`;
|
|
6722
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
|
+
}
|
|
6723
7035
|
async function reconstructOutgoingTxRecords(deps) {
|
|
6724
7036
|
const { vault, transfers } = deps;
|
|
6725
7037
|
const now = deps.now ?? Date.now;
|
|
6726
7038
|
const allNotes = vault.getAll();
|
|
6727
7039
|
const spentNotes = allNotes.filter((n) => n.spent);
|
|
6728
7040
|
if (spentNotes.length === 0) return;
|
|
7041
|
+
const sentCommitments = (deps.sentNotes ?? []).map((s) => s.commitmentHex);
|
|
6729
7042
|
const [outgoingTransfers, commitmentTransfers] = await Promise.all([
|
|
6730
7043
|
transfers.byNullifiers(spentNotes.map((n) => n.nullifierHex)),
|
|
6731
|
-
transfers.byCommitments(allNotes.map((n) => n.commitmentHex))
|
|
7044
|
+
transfers.byCommitments([...allNotes.map((n) => n.commitmentHex), ...sentCommitments])
|
|
6732
7045
|
]);
|
|
6733
7046
|
if (outgoingTransfers.length === 0) return;
|
|
6734
7047
|
const noteByNullifier = new Map(spentNotes.map((n) => [n.nullifierHex, n]));
|
|
@@ -6737,64 +7050,85 @@ async function reconstructOutgoingTxRecords(deps) {
|
|
|
6737
7050
|
commitmentTransfers.map((ct) => [extrinsicKey(ct), ct.matchedCommitments ?? []])
|
|
6738
7051
|
);
|
|
6739
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);
|
|
6740
7056
|
for (const transfer of outgoingTransfers) {
|
|
6741
7057
|
const existing = existingByKey.get(recordKey(transfer));
|
|
6742
|
-
|
|
7058
|
+
const known = existing?.recipientPkHex && existing.recipientPkHex !== ZERO_PK;
|
|
7059
|
+
if (known && existing?.paymentSlip) continue;
|
|
6743
7060
|
const inputNotes = (transfer.matchedNullifiers ?? []).map((h) => noteByNullifier.get(h)).filter((n) => n !== void 0);
|
|
6744
7061
|
if (inputNotes.length === 0) continue;
|
|
6745
7062
|
const changeNote = selectDescribingNoteByCommitment(
|
|
6746
7063
|
commitmentsByExtrinsic.get(extrinsicKey(transfer)) ?? [],
|
|
6747
7064
|
noteByCommitment
|
|
6748
7065
|
);
|
|
6749
|
-
const
|
|
7066
|
+
const sentForRow = sentFor(transfer);
|
|
7067
|
+
const recipientPkHex = sentForRow ? toPkHex(sentForRow.recipientStealthPk) : recoveryEnabled ? ZERO_PK : toPkHex(changeNote?.sourcePk);
|
|
6750
7068
|
if (existing) {
|
|
6751
|
-
|
|
6752
|
-
|
|
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
|
+
}
|
|
6753
7079
|
continue;
|
|
6754
7080
|
}
|
|
6755
7081
|
const { fee, success } = await fetchExtrinsicFacts(deps.txFacts, transfer.hash);
|
|
6756
7082
|
const totalInputValue = inputNotes.reduce((sum, n) => sum + n.value, 0n);
|
|
6757
|
-
const
|
|
7083
|
+
const derivedAmount = totalInputValue - (changeNote?.value ?? 0n) - (fee ?? 0n);
|
|
7084
|
+
const transferAmount = sentForRow?.value ?? derivedAmount;
|
|
6758
7085
|
if (transferAmount <= 0n) continue;
|
|
6759
7086
|
const record = {
|
|
6760
7087
|
id: recordKey(transfer),
|
|
6761
7088
|
type: "private_transfer",
|
|
6762
7089
|
blockNumber: transfer.blockNumber,
|
|
6763
7090
|
hash: transfer.hash ?? "",
|
|
6764
|
-
assetId: inputNotes[0].assetId.toString(),
|
|
7091
|
+
assetId: (sentForRow?.assetId ?? inputNotes[0].assetId).toString(),
|
|
6765
7092
|
amount: transferAmount.toString(),
|
|
6766
7093
|
recipientPkHex,
|
|
6767
7094
|
status: success ? "success" : "failed",
|
|
6768
|
-
...fee !== null ? { feePlanck: fee.toString() } : {
|
|
6769
|
-
|
|
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) : {}
|
|
6770
7101
|
};
|
|
6771
7102
|
await vault.saveTxRecord(record);
|
|
6772
7103
|
}
|
|
6773
7104
|
}
|
|
6774
7105
|
|
|
6775
7106
|
// src/wallet/ops/notes/buildNote.ts
|
|
6776
|
-
async function
|
|
7107
|
+
async function buildZkNoteWithIndex(params, deps) {
|
|
6777
7108
|
const { keys, storage } = deps;
|
|
6778
7109
|
const ownerPk = params.ownerPk ?? keys.ownerPk;
|
|
6779
7110
|
const spendingKey = params.spendingKey ?? keys.spendingKey;
|
|
6780
7111
|
const viewingPublicKey = params.viewingPublicKey ?? keys.viewingPublicKey;
|
|
6781
7112
|
const isSelf = params.viewingPublicKey === void 0 && params.recipientOwnerPk === void 0;
|
|
6782
7113
|
let ephSkOverride;
|
|
6783
|
-
|
|
7114
|
+
let outgoingIndex;
|
|
7115
|
+
if (isSelf && storage && keys.viewingSecretKey) {
|
|
6784
7116
|
try {
|
|
6785
|
-
ephSkOverride = deriveSelfEphSk(
|
|
7117
|
+
ephSkOverride = deriveSelfEphSk(
|
|
7118
|
+
keys.viewingSecretKey,
|
|
7119
|
+
await reserveSelfEphIndex(storage)
|
|
7120
|
+
);
|
|
6786
7121
|
} catch {
|
|
6787
7122
|
}
|
|
6788
|
-
} else if (params.viewingPublicKey && storage && keys.
|
|
7123
|
+
} else if (params.viewingPublicKey && storage && keys.outgoingViewingKey) {
|
|
6789
7124
|
try {
|
|
6790
|
-
const index = await
|
|
6791
|
-
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
}
|
|
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));
|
|
6798
7132
|
} catch {
|
|
6799
7133
|
}
|
|
6800
7134
|
}
|
|
@@ -6814,7 +7148,13 @@ async function buildZkNote(params, deps) {
|
|
|
6814
7148
|
},
|
|
6815
7149
|
...ephSkOverride !== void 0 && { ephSkOverride }
|
|
6816
7150
|
});
|
|
6817
|
-
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;
|
|
6818
7158
|
}
|
|
6819
7159
|
|
|
6820
7160
|
// src/wallet/ops/notes/selfStealthNote.ts
|
|
@@ -6833,89 +7173,6 @@ function recoverSelfStealthNote(note, keys) {
|
|
|
6833
7173
|
return recovered;
|
|
6834
7174
|
}
|
|
6835
7175
|
|
|
6836
|
-
// src/wallet/ops/notes/noteTransfer.ts
|
|
6837
|
-
var NOTE_TRANSFER_URI_SCHEME = "orbinum://notes/v1/";
|
|
6838
|
-
var QR_PAGE_MAX_CHARS = 1800;
|
|
6839
|
-
function noteToTransferEntry(note) {
|
|
6840
|
-
return {
|
|
6841
|
-
c: note.commitmentHex,
|
|
6842
|
-
n: note.nullifierHex,
|
|
6843
|
-
val: note.value.toString(),
|
|
6844
|
-
aid: note.assetId.toString(),
|
|
6845
|
-
opk: scalarToHex(note.ownerPk),
|
|
6846
|
-
bld: scalarToHex(note.blinding),
|
|
6847
|
-
sk: scalarToHex(note.spendingKey)
|
|
6848
|
-
};
|
|
6849
|
-
}
|
|
6850
|
-
function encodeNoteTransferPages(notes, options = {}) {
|
|
6851
|
-
const now = options.now ?? Date.now;
|
|
6852
|
-
const maxChars = options.maxChars ?? QR_PAGE_MAX_CHARS;
|
|
6853
|
-
const entries = notes.map(noteToTransferEntry);
|
|
6854
|
-
const batches = [];
|
|
6855
|
-
let batch = [];
|
|
6856
|
-
for (const entry of entries) {
|
|
6857
|
-
batch.push(entry);
|
|
6858
|
-
const probe = { v: 1, ts: 0, p: 0, pt: 1, notes: batch };
|
|
6859
|
-
const size = NOTE_TRANSFER_URI_SCHEME.length + base64UrlEncode(JSON.stringify(probe)).length;
|
|
6860
|
-
if (size > maxChars && batch.length > 1) {
|
|
6861
|
-
batch.pop();
|
|
6862
|
-
batches.push(batch);
|
|
6863
|
-
batch = [entry];
|
|
6864
|
-
}
|
|
6865
|
-
}
|
|
6866
|
-
if (batch.length > 0) batches.push(batch);
|
|
6867
|
-
const ts = now();
|
|
6868
|
-
return batches.map((notesInPage, i) => {
|
|
6869
|
-
const payload = {
|
|
6870
|
-
v: 1,
|
|
6871
|
-
ts,
|
|
6872
|
-
p: i,
|
|
6873
|
-
pt: batches.length,
|
|
6874
|
-
notes: notesInPage
|
|
6875
|
-
};
|
|
6876
|
-
return NOTE_TRANSFER_URI_SCHEME + base64UrlEncode(JSON.stringify(payload));
|
|
6877
|
-
});
|
|
6878
|
-
}
|
|
6879
|
-
function decodeNoteTransferPage(uri) {
|
|
6880
|
-
if (!uri.startsWith(NOTE_TRANSFER_URI_SCHEME)) {
|
|
6881
|
-
throw new Error("Not an Orbinum note-transfer code.");
|
|
6882
|
-
}
|
|
6883
|
-
let payload;
|
|
6884
|
-
try {
|
|
6885
|
-
payload = JSON.parse(base64UrlDecode(uri.slice(NOTE_TRANSFER_URI_SCHEME.length)));
|
|
6886
|
-
} catch {
|
|
6887
|
-
throw new Error("Note-transfer code is corrupt.");
|
|
6888
|
-
}
|
|
6889
|
-
const p = payload;
|
|
6890
|
-
if (p.v !== 1) {
|
|
6891
|
-
throw new Error(`Unsupported note-transfer version: ${String(p.v)}.`);
|
|
6892
|
-
}
|
|
6893
|
-
if (typeof p.p !== "number" || typeof p.pt !== "number" || !Array.isArray(p.notes) || p.notes.some((n) => !isEntry(n))) {
|
|
6894
|
-
throw new Error("Note-transfer code is missing required fields.");
|
|
6895
|
-
}
|
|
6896
|
-
return p;
|
|
6897
|
-
}
|
|
6898
|
-
function isEntry(value) {
|
|
6899
|
-
if (typeof value !== "object" || value === null) return false;
|
|
6900
|
-
const e = value;
|
|
6901
|
-
return ["c", "n", "val", "aid", "opk", "bld", "sk"].every(
|
|
6902
|
-
(k) => typeof e[k] === "string" && e[k].length > 0
|
|
6903
|
-
);
|
|
6904
|
-
}
|
|
6905
|
-
function assembleNoteTransfer(pages) {
|
|
6906
|
-
if (pages.length === 0) throw new Error("No pages scanned.");
|
|
6907
|
-
const total = pages[0].pt;
|
|
6908
|
-
if (pages.some((p) => p.pt !== total)) {
|
|
6909
|
-
throw new Error("Pages come from different batches.");
|
|
6910
|
-
}
|
|
6911
|
-
const seen = new Set(pages.map((p) => p.p));
|
|
6912
|
-
if (seen.size !== total) {
|
|
6913
|
-
const missing = [...Array(total).keys()].filter((i) => !seen.has(i));
|
|
6914
|
-
throw new Error(`Missing page(s): ${missing.map((i) => i + 1).join(", ")} of ${total}.`);
|
|
6915
|
-
}
|
|
6916
|
-
return [...pages].sort((a, b) => a.p - b.p).flatMap((p) => p.notes);
|
|
6917
|
-
}
|
|
6918
|
-
|
|
6919
7176
|
// src/wallet/ops/notes/noteBackup.ts
|
|
6920
7177
|
var NOTE_BACKUP_VERSION = 1;
|
|
6921
7178
|
function noteToBackupEntry(note) {
|
|
@@ -6950,12 +7207,13 @@ function decodeNoteBackup(json) {
|
|
|
6950
7207
|
if (p.v !== NOTE_BACKUP_VERSION) {
|
|
6951
7208
|
throw new Error(`Unsupported note-backup version: ${String(p.v)}.`);
|
|
6952
7209
|
}
|
|
6953
|
-
if (!Array.isArray(p.notes) || p.notes.some((n) => !
|
|
7210
|
+
if (!Array.isArray(p.notes) || p.notes.some((n) => !isEntry(n))) {
|
|
6954
7211
|
throw new Error("Note backup is missing required fields.");
|
|
6955
7212
|
}
|
|
6956
7213
|
return p.notes;
|
|
6957
7214
|
}
|
|
6958
7215
|
function importNotesFromBackup(entries, keys) {
|
|
7216
|
+
assertSecretKeyBytes(keys.viewingSecretKey, "importNotesFromBackup: viewingSecretKey");
|
|
6959
7217
|
const out = [];
|
|
6960
7218
|
for (const entry of entries) {
|
|
6961
7219
|
const commitment = {
|
|
@@ -6979,12 +7237,14 @@ function importNotesFromBackup(entries, keys) {
|
|
|
6979
7237
|
}
|
|
6980
7238
|
return out;
|
|
6981
7239
|
}
|
|
6982
|
-
function
|
|
7240
|
+
function isEntry(value) {
|
|
6983
7241
|
if (typeof value !== "object" || value === null) return false;
|
|
6984
7242
|
const e = value;
|
|
6985
|
-
if (
|
|
6986
|
-
if (
|
|
6987
|
-
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") {
|
|
6988
7248
|
return false;
|
|
6989
7249
|
}
|
|
6990
7250
|
return true;
|
|
@@ -6992,6 +7252,7 @@ function isEntry2(value) {
|
|
|
6992
7252
|
|
|
6993
7253
|
// src/wallet/ops/notes/paymentSlipImport.ts
|
|
6994
7254
|
function importPaymentSlip(slip, keys) {
|
|
7255
|
+
assertSecretKeyBytes(keys.viewingSecretKey, "importPaymentSlip: viewingSecretKey");
|
|
6995
7256
|
const envelope = typeof slip === "string" ? decodePaymentSlip(slip) : slip;
|
|
6996
7257
|
if (!envelope) return null;
|
|
6997
7258
|
const fields = openPaymentSlip(keys.viewingSecretKey, envelope);
|
|
@@ -7120,10 +7381,7 @@ async function transferNotes(deps, params, onProgress) {
|
|
|
7120
7381
|
const merkleRoot = merkleA.root;
|
|
7121
7382
|
onProgress?.("building-output-notes");
|
|
7122
7383
|
const effectiveSenderPk = senderPk ?? noteA.ownerPk;
|
|
7123
|
-
const
|
|
7124
|
-
deriveViewingSecretKey(noteA.spendingKey)
|
|
7125
|
-
);
|
|
7126
|
-
const recipientNote = await deps.buildNote({
|
|
7384
|
+
const { note: recipientNote, outgoingIndex } = await deps.buildNote({
|
|
7127
7385
|
value: transferAmount,
|
|
7128
7386
|
assetId: noteA.assetId,
|
|
7129
7387
|
ownerPk: recipientPk,
|
|
@@ -7133,13 +7391,17 @@ async function transferNotes(deps, params, onProgress) {
|
|
|
7133
7391
|
// With a viewing key present this activates stealth derivation.
|
|
7134
7392
|
recipientOwnerPk: recipientPk
|
|
7135
7393
|
});
|
|
7136
|
-
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({
|
|
7137
7402
|
value: changeValue,
|
|
7138
7403
|
assetId: noteA.assetId,
|
|
7139
|
-
|
|
7140
|
-
spendingKey: noteA.spendingKey,
|
|
7141
|
-
sourcePk: recipientNote.ownerPk,
|
|
7142
|
-
viewingPublicKey: senderViewingPublicKey
|
|
7404
|
+
sourcePk: changeSourcePk
|
|
7143
7405
|
});
|
|
7144
7406
|
onProgress?.("generating-zk");
|
|
7145
7407
|
const noteToInput = (note, proof) => ({
|
|
@@ -7256,7 +7518,7 @@ async function unshieldNote(deps, params, onProgress) {
|
|
|
7256
7518
|
if (changeValue > 0n) {
|
|
7257
7519
|
const { ownerPk, spendingKey } = deps.stealthKeys;
|
|
7258
7520
|
changeBlinding = randomBlinding();
|
|
7259
|
-
changeNote = await deps.buildNote({
|
|
7521
|
+
changeNote = (await deps.buildNote({
|
|
7260
7522
|
value: changeValue,
|
|
7261
7523
|
assetId: note.assetId,
|
|
7262
7524
|
ownerPk,
|
|
@@ -7264,7 +7526,7 @@ async function unshieldNote(deps, params, onProgress) {
|
|
|
7264
7526
|
spendingKey,
|
|
7265
7527
|
// Activates stealth: the commitment covers a one-time ownerPk.
|
|
7266
7528
|
recipientOwnerPk: ownerPk
|
|
7267
|
-
});
|
|
7529
|
+
})).note;
|
|
7268
7530
|
}
|
|
7269
7531
|
const { provider, version: circuitVersion } = await deps.resolver.resolve(
|
|
7270
7532
|
import_proof_generator7.CircuitType.Unshield,
|
|
@@ -7550,8 +7812,9 @@ var KEY_PREFIX = "orbinum_sk_";
|
|
|
7550
7812
|
function isEnvelope(value) {
|
|
7551
7813
|
return typeof value === "object" && value !== null && value.v === 1 && typeof value.iv === "string" && typeof value.ct === "string";
|
|
7552
7814
|
}
|
|
7553
|
-
function sessionCacheKey(address, chainId) {
|
|
7554
|
-
|
|
7815
|
+
function sessionCacheKey(address, chainId, version = "v3") {
|
|
7816
|
+
const marker = `${version}_`;
|
|
7817
|
+
return `${KEY_PREFIX}${marker}${chainId}_${canonicalAccountId(address)}`;
|
|
7555
7818
|
}
|
|
7556
7819
|
async function hasCachedSession(store, address, chainId) {
|
|
7557
7820
|
return await store.get(sessionCacheKey(address, chainId)) !== null;
|
|
@@ -7622,9 +7885,55 @@ function createDeviceKeyProvider(store) {
|
|
|
7622
7885
|
|
|
7623
7886
|
// src/wallet/identity/vaultName.ts
|
|
7624
7887
|
var PREFIX2 = "orbinum-vault";
|
|
7625
|
-
|
|
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") {
|
|
7626
7900
|
const account = canonicalAccountId(address);
|
|
7627
|
-
|
|
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
|
+
};
|
|
7628
7937
|
}
|
|
7629
7938
|
|
|
7630
7939
|
// src/wallet/identity/injectedExtensions.ts
|
|
@@ -7648,23 +7957,21 @@ var cachedWindow = null;
|
|
|
7648
7957
|
function clearKnownEphWindow() {
|
|
7649
7958
|
cachedWindow = null;
|
|
7650
7959
|
}
|
|
7651
|
-
function bytesKey(bytes) {
|
|
7652
|
-
let out = "";
|
|
7653
|
-
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
7654
|
-
return out;
|
|
7655
|
-
}
|
|
7656
7960
|
function getKnownEphWindow(keys) {
|
|
7657
7961
|
const selfSize = keys.selfEphWindowSize ?? SELF_EPH_WINDOW;
|
|
7658
7962
|
const pairSize = keys.pairwiseWindowSize ?? PAIRWISE_EPH_WINDOW;
|
|
7963
|
+
const outSize = keys.outgoingEphWindowSize ?? OUTGOING_EPH_WINDOW;
|
|
7659
7964
|
const counterparties = keys.pairwiseCounterparties ?? [];
|
|
7660
|
-
|
|
7661
|
-
|
|
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(",");
|
|
7662
7968
|
if (cachedWindow?.cacheKey === cacheKey) return cachedWindow.window;
|
|
7663
7969
|
try {
|
|
7664
7970
|
const ivkPacked = deriveViewingPublicKey(keys.viewingKey);
|
|
7665
7971
|
const byEphPk = /* @__PURE__ */ new Map();
|
|
7972
|
+
const outgoingByEphPk = /* @__PURE__ */ new Map();
|
|
7666
7973
|
if (keys.selfEph) {
|
|
7667
|
-
for (const e of selfEphWindow(keys.
|
|
7974
|
+
for (const e of selfEphWindow(keys.viewingKey, ivkPacked, 0, selfSize)) {
|
|
7668
7975
|
byEphPk.set(e.ephPkHex.toLowerCase(), {
|
|
7669
7976
|
sharedSecret: e.sharedSecret,
|
|
7670
7977
|
index: e.index,
|
|
@@ -7685,7 +7992,12 @@ function getKnownEphWindow(keys) {
|
|
|
7685
7992
|
}
|
|
7686
7993
|
}
|
|
7687
7994
|
}
|
|
7688
|
-
|
|
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 } };
|
|
7689
8001
|
return cachedWindow.window;
|
|
7690
8002
|
} catch {
|
|
7691
8003
|
return null;
|
|
@@ -7694,21 +8006,32 @@ function getKnownEphWindow(keys) {
|
|
|
7694
8006
|
|
|
7695
8007
|
// src/wallet/worker/kernel/decryptBatch.ts
|
|
7696
8008
|
function hintEphPkHex(hint) {
|
|
7697
|
-
if (hint.ephPkHex) return hint.ephPkHex.toLowerCase();
|
|
7698
8009
|
const memo = hint.encryptedMemo;
|
|
7699
8010
|
if (!memo || memo.length < 66) return null;
|
|
7700
8011
|
return ("0x" + memo.slice(-64)).toLowerCase();
|
|
7701
8012
|
}
|
|
8013
|
+
function sealedBookEntry(note) {
|
|
8014
|
+
if (note.sourcePk === void 0 || note.sourcePk === 0n) return null;
|
|
8015
|
+
return note.sourcePk;
|
|
8016
|
+
}
|
|
7702
8017
|
function decryptHintBatch(hints, keys) {
|
|
7703
8018
|
const activation = keys.viewTagActivationLeaf ?? null;
|
|
7704
8019
|
const knownWindow = getKnownEphWindow(keys);
|
|
8020
|
+
const sentNotes = [];
|
|
8021
|
+
const learnedRecipients = /* @__PURE__ */ new Set();
|
|
7705
8022
|
let tagFiltered = 0;
|
|
7706
8023
|
let selfMatched = 0;
|
|
7707
8024
|
let pairwiseMatched = 0;
|
|
7708
8025
|
let maxSelfEphIndex = null;
|
|
8026
|
+
let maxOutgoingEphIndex = null;
|
|
8027
|
+
const sealedEntries = [];
|
|
8028
|
+
const pendingSent = [];
|
|
8029
|
+
const seenSent = /* @__PURE__ */ new Set();
|
|
8030
|
+
const unmatchedSent = [];
|
|
7709
8031
|
const notes = hints.map((hint) => {
|
|
7710
8032
|
try {
|
|
7711
|
-
const
|
|
8033
|
+
const ephPkHex = hintEphPkHex(hint) ?? "";
|
|
8034
|
+
const known = knownWindow?.byEphPk.get(ephPkHex);
|
|
7712
8035
|
if (known) {
|
|
7713
8036
|
const result2 = tryDecryptNoteVerbose(
|
|
7714
8037
|
hint,
|
|
@@ -7723,12 +8046,26 @@ function decryptHintBatch(hints, keys) {
|
|
|
7723
8046
|
if (maxSelfEphIndex === null || known.index > maxSelfEphIndex) {
|
|
7724
8047
|
maxSelfEphIndex = known.index;
|
|
7725
8048
|
}
|
|
8049
|
+
const sealed = sealedBookEntry(result2.note);
|
|
8050
|
+
if (sealed !== null) sealedEntries.push(sealed);
|
|
7726
8051
|
} else {
|
|
7727
8052
|
pairwiseMatched++;
|
|
7728
8053
|
}
|
|
7729
8054
|
return result2.note;
|
|
7730
8055
|
}
|
|
7731
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
|
+
}
|
|
7732
8069
|
const viewTag = activation !== null && hint.leafIndex >= activation;
|
|
7733
8070
|
const result = tryDecryptNoteVerbose(
|
|
7734
8071
|
hint,
|
|
@@ -7743,7 +8080,44 @@ function decryptHintBatch(hints, keys) {
|
|
|
7743
8080
|
return null;
|
|
7744
8081
|
}
|
|
7745
8082
|
});
|
|
7746
|
-
|
|
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
|
+
};
|
|
7747
8121
|
}
|
|
7748
8122
|
|
|
7749
8123
|
// src/wallet/worker/pool/types.ts
|
|
@@ -7765,21 +8139,52 @@ function createMainThreadPool() {
|
|
|
7765
8139
|
let selfMatched = 0;
|
|
7766
8140
|
let pairwiseMatched = 0;
|
|
7767
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
|
+
);
|
|
7768
8149
|
for (let i = 0; i < hints.length; i += DECRYPT_YIELD_EVERY) {
|
|
7769
8150
|
if (i > 0) {
|
|
7770
8151
|
await yieldToBrowser();
|
|
7771
8152
|
if (signal?.aborted) throw scanAbortError();
|
|
7772
8153
|
}
|
|
7773
|
-
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
|
+
});
|
|
7774
8158
|
notes.push(...burst.notes);
|
|
7775
8159
|
tagFiltered += burst.tagFiltered;
|
|
7776
8160
|
selfMatched += burst.selfMatched;
|
|
7777
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);
|
|
7778
8166
|
if (burst.maxSelfEphIndex !== null) {
|
|
7779
8167
|
maxSelfEphIndex = Math.max(maxSelfEphIndex ?? -1, burst.maxSelfEphIndex);
|
|
7780
8168
|
}
|
|
8169
|
+
if (burst.maxOutgoingEphIndex != null) {
|
|
8170
|
+
maxOutgoingEphIndex = Math.max(
|
|
8171
|
+
maxOutgoingEphIndex ?? -1,
|
|
8172
|
+
burst.maxOutgoingEphIndex
|
|
8173
|
+
);
|
|
8174
|
+
}
|
|
7781
8175
|
}
|
|
7782
|
-
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
|
+
};
|
|
7783
8188
|
},
|
|
7784
8189
|
terminate() {
|
|
7785
8190
|
clearKnownEphWindow();
|
|
@@ -7811,7 +8216,12 @@ function runOnWorker(worker, payload, signal) {
|
|
|
7811
8216
|
tagFiltered: data.tagFiltered ?? 0,
|
|
7812
8217
|
selfMatched: data.selfMatched ?? 0,
|
|
7813
8218
|
pairwiseMatched: data.pairwiseMatched ?? 0,
|
|
7814
|
-
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 ?? []
|
|
7815
8225
|
});
|
|
7816
8226
|
};
|
|
7817
8227
|
worker.onerror = () => {
|
|
@@ -7838,9 +8248,19 @@ function mergeResults(results) {
|
|
|
7838
8248
|
tagFiltered: results.reduce((sum, r) => sum + r.tagFiltered, 0),
|
|
7839
8249
|
selfMatched: results.reduce((sum, r) => sum + r.selfMatched, 0),
|
|
7840
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 ?? []))],
|
|
7841
8257
|
maxSelfEphIndex: results.reduce(
|
|
7842
8258
|
(max, r) => r.maxSelfEphIndex === null ? max : Math.max(max ?? -1, r.maxSelfEphIndex),
|
|
7843
8259
|
null
|
|
8260
|
+
),
|
|
8261
|
+
maxOutgoingEphIndex: results.reduce(
|
|
8262
|
+
(max, r) => r.maxOutgoingEphIndex == null ? max : Math.max(max ?? -1, r.maxOutgoingEphIndex),
|
|
8263
|
+
null
|
|
7844
8264
|
)
|
|
7845
8265
|
};
|
|
7846
8266
|
}
|
|
@@ -7904,25 +8324,36 @@ var OrbinumWallet = class {
|
|
|
7904
8324
|
session;
|
|
7905
8325
|
notes;
|
|
7906
8326
|
vaultStore;
|
|
7907
|
-
/**
|
|
7908
|
-
|
|
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;
|
|
7909
8337
|
/** The underlying store, for operations the facade does not cover. */
|
|
7910
8338
|
get vault() {
|
|
7911
8339
|
return this.vaultStore;
|
|
7912
8340
|
}
|
|
7913
8341
|
get unlocked() {
|
|
7914
|
-
return this.session.unlocked && this.
|
|
8342
|
+
return this.session.unlocked && this.identity !== null;
|
|
7915
8343
|
}
|
|
7916
8344
|
/**
|
|
7917
8345
|
* Opens the wallet from the master key bytes a `SpendingKeyRequest`
|
|
7918
8346
|
* signature produces — the same value `PrivacyKeyManager.getMasterBytes()`
|
|
7919
8347
|
* returns.
|
|
7920
8348
|
*
|
|
7921
|
-
* Master bytes rather than the spending-key scalar, deliberately: the
|
|
7922
|
-
* is the
|
|
7923
|
-
*
|
|
7924
|
-
*
|
|
7925
|
-
*
|
|
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.
|
|
7926
8357
|
*
|
|
7927
8358
|
* Reports whether the stored vault was RESET: a different chain, a different
|
|
7928
8359
|
* key, or notes the chain no longer recognises. When it was, the wallet
|
|
@@ -7936,14 +8367,12 @@ var OrbinumWallet = class {
|
|
|
7936
8367
|
* against a vault that was never read.
|
|
7937
8368
|
*/
|
|
7938
8369
|
async unlock(masterBytes, options = {}) {
|
|
7939
|
-
|
|
7940
|
-
throw new Error(`Master key must be 32 bytes, got ${masterBytes.length}.`);
|
|
7941
|
-
}
|
|
8370
|
+
assertSecretKeyBytes(masterBytes, "Master key");
|
|
7942
8371
|
this.session.open(
|
|
7943
8372
|
await deriveVaultKey(masterBytes),
|
|
7944
8373
|
await deriveVaultBlindKey(masterBytes)
|
|
7945
8374
|
);
|
|
7946
|
-
this.
|
|
8375
|
+
this.identity = deriveIdentity(masterBytes, this.config.identityVersion ?? "v3");
|
|
7947
8376
|
try {
|
|
7948
8377
|
return await this.vaultStore.unlock(this.session.cryptoKey, options);
|
|
7949
8378
|
} catch (err) {
|
|
@@ -7954,8 +8383,12 @@ var OrbinumWallet = class {
|
|
|
7954
8383
|
/** Drops the in-memory keys. The stored vault is untouched. */
|
|
7955
8384
|
lock() {
|
|
7956
8385
|
this.session.lock();
|
|
7957
|
-
this.
|
|
8386
|
+
this.identity = null;
|
|
7958
8387
|
this.notes.set([]);
|
|
8388
|
+
try {
|
|
8389
|
+
this.config.pool.terminate();
|
|
8390
|
+
} catch {
|
|
8391
|
+
}
|
|
7959
8392
|
}
|
|
7960
8393
|
/** The notes currently held. Empty while locked. */
|
|
7961
8394
|
getNotes() {
|
|
@@ -7967,11 +8400,8 @@ var OrbinumWallet = class {
|
|
|
7967
8400
|
}
|
|
7968
8401
|
/** This wallet's privacy identity: the keys a payer needs to address it. */
|
|
7969
8402
|
privacyKeys() {
|
|
7970
|
-
const
|
|
7971
|
-
return {
|
|
7972
|
-
ownerPk: deriveOwnerPk(spendingKey),
|
|
7973
|
-
viewingPublicKey: deriveViewingPublicKey(deriveViewingSecretKey(spendingKey))
|
|
7974
|
-
};
|
|
8403
|
+
const { ownerPk, viewingPublicKey } = this.requireIdentity();
|
|
8404
|
+
return { ownerPk, viewingPublicKey };
|
|
7975
8405
|
}
|
|
7976
8406
|
/**
|
|
7977
8407
|
* Walks the feed, recovers this wallet's notes, and persists them with their
|
|
@@ -7981,15 +8411,21 @@ var OrbinumWallet = class {
|
|
|
7981
8411
|
* `sinceLeafIndex` for a full scan, which is what a restore needs.
|
|
7982
8412
|
*/
|
|
7983
8413
|
async scan(options = {}) {
|
|
7984
|
-
const
|
|
7985
|
-
const viewingKey = deriveViewingSecretKey(spendingKey);
|
|
8414
|
+
const identity = this.requireIdentity();
|
|
7986
8415
|
return runScan({
|
|
7987
8416
|
vault: this.vaultStore,
|
|
7988
8417
|
storage: this.config.storage,
|
|
7989
8418
|
hints: this.config.hints,
|
|
7990
8419
|
nullifiers: this.config.nullifiers,
|
|
7991
8420
|
pool: this.config.pool,
|
|
7992
|
-
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
|
+
},
|
|
7993
8429
|
sinceLeafIndex: options.sinceLeafIndex,
|
|
7994
8430
|
viewTagActivationLeaf: this.config.viewTagActivationLeaf,
|
|
7995
8431
|
onProgress: options.onProgress,
|
|
@@ -8011,13 +8447,14 @@ var OrbinumWallet = class {
|
|
|
8011
8447
|
* reusing an index would republish an ephPk and link the two notes.
|
|
8012
8448
|
*/
|
|
8013
8449
|
async buildNote(params) {
|
|
8014
|
-
const
|
|
8450
|
+
const identity = this.requireIdentity();
|
|
8015
8451
|
return buildZkNote(params, {
|
|
8016
8452
|
keys: {
|
|
8017
|
-
ownerPk:
|
|
8018
|
-
spendingKey,
|
|
8019
|
-
viewingPublicKey:
|
|
8020
|
-
viewingSecretKey:
|
|
8453
|
+
ownerPk: identity.ownerPk,
|
|
8454
|
+
spendingKey: identity.spendingKey,
|
|
8455
|
+
viewingPublicKey: identity.viewingPublicKey,
|
|
8456
|
+
viewingSecretKey: identity.viewingSecretKey,
|
|
8457
|
+
...identity.outgoingViewingKey ? { outgoingViewingKey: identity.outgoingViewingKey } : {}
|
|
8021
8458
|
},
|
|
8022
8459
|
storage: this.config.storage
|
|
8023
8460
|
});
|
|
@@ -8034,11 +8471,12 @@ var OrbinumWallet = class {
|
|
|
8034
8471
|
* Treat the result as secret: it can spend every note this wallet holds.
|
|
8035
8472
|
*/
|
|
8036
8473
|
spendKeys() {
|
|
8037
|
-
const
|
|
8474
|
+
const identity = this.requireIdentity();
|
|
8038
8475
|
return {
|
|
8039
|
-
spendingKey,
|
|
8040
|
-
viewingSecretKey:
|
|
8041
|
-
ownerPk:
|
|
8476
|
+
spendingKey: identity.spendingKey,
|
|
8477
|
+
viewingSecretKey: identity.viewingSecretKey,
|
|
8478
|
+
ownerPk: identity.ownerPk,
|
|
8479
|
+
...identity.outgoingViewingKey ? { outgoingViewingKey: identity.outgoingViewingKey } : {}
|
|
8042
8480
|
};
|
|
8043
8481
|
}
|
|
8044
8482
|
/**
|
|
@@ -8060,7 +8498,20 @@ var OrbinumWallet = class {
|
|
|
8060
8498
|
* verifying-key rotation, and the failure surfaces much later.
|
|
8061
8499
|
*/
|
|
8062
8500
|
buildOutputNote = async (params) => {
|
|
8063
|
-
|
|
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
|
+
);
|
|
8064
8515
|
};
|
|
8065
8516
|
/** The circuit version a new note should carry. */
|
|
8066
8517
|
async circuitVersion() {
|
|
@@ -8072,11 +8523,11 @@ var OrbinumWallet = class {
|
|
|
8072
8523
|
}
|
|
8073
8524
|
return chainActiveCircuitVersion(this.config.zkVerifier);
|
|
8074
8525
|
}
|
|
8075
|
-
|
|
8076
|
-
if (this.
|
|
8526
|
+
requireIdentity() {
|
|
8527
|
+
if (this.identity === null) {
|
|
8077
8528
|
throw new Error("Wallet is locked. Call unlock() before using it.");
|
|
8078
8529
|
}
|
|
8079
|
-
return this.
|
|
8530
|
+
return this.identity;
|
|
8080
8531
|
}
|
|
8081
8532
|
};
|
|
8082
8533
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -8108,9 +8559,8 @@ var OrbinumWallet = class {
|
|
|
8108
8559
|
NATIVE_ASSET_ID,
|
|
8109
8560
|
NOTE_BACKUP_VERSION,
|
|
8110
8561
|
NOTE_BIGINT_FIELDS,
|
|
8111
|
-
NOTE_TRANSFER_URI_SCHEME,
|
|
8112
8562
|
NoteBuilder,
|
|
8113
|
-
|
|
8563
|
+
OUTGOING_EPH_WINDOW,
|
|
8114
8564
|
OrbinumClient,
|
|
8115
8565
|
OrbinumClientProvider,
|
|
8116
8566
|
OrbinumWallet,
|
|
@@ -8120,7 +8570,6 @@ var OrbinumWallet = class {
|
|
|
8120
8570
|
PRECOMPILE_ADDR,
|
|
8121
8571
|
PrivacyKeyManager,
|
|
8122
8572
|
PrivacyModule,
|
|
8123
|
-
QR_PAGE_MAX_CHARS,
|
|
8124
8573
|
RECOVERED_TX_RESULT,
|
|
8125
8574
|
RelayerStatusModule,
|
|
8126
8575
|
RpcV2Module,
|
|
@@ -8145,7 +8594,7 @@ var OrbinumWallet = class {
|
|
|
8145
8594
|
addressToFieldElement,
|
|
8146
8595
|
applyBatch,
|
|
8147
8596
|
applyNoteStatus,
|
|
8148
|
-
|
|
8597
|
+
assertSecretKeyBytes,
|
|
8149
8598
|
base58,
|
|
8150
8599
|
base64UrlDecode,
|
|
8151
8600
|
base64UrlEncode,
|
|
@@ -8158,6 +8607,7 @@ var OrbinumWallet = class {
|
|
|
8158
8607
|
buildShieldBatchOperations,
|
|
8159
8608
|
buildShieldParams,
|
|
8160
8609
|
buildZkNote,
|
|
8610
|
+
buildZkNoteWithIndex,
|
|
8161
8611
|
bytesToBigintLE,
|
|
8162
8612
|
bytesToBjjScalar,
|
|
8163
8613
|
cacheSession,
|
|
@@ -8187,15 +8637,17 @@ var OrbinumWallet = class {
|
|
|
8187
8637
|
createWorkerPool,
|
|
8188
8638
|
decodeNoteBackup,
|
|
8189
8639
|
decodeNoteDisclosureKey,
|
|
8190
|
-
decodeNoteTransferPage,
|
|
8191
8640
|
decodePaymentSlip,
|
|
8192
8641
|
decodePrecompileCalldata,
|
|
8193
8642
|
decryptHintBatch,
|
|
8194
8643
|
decryptJson,
|
|
8195
8644
|
decryptNoteRecord,
|
|
8645
|
+
deriveIdentity,
|
|
8196
8646
|
deriveMasterKeyBytes,
|
|
8197
|
-
|
|
8198
|
-
|
|
8647
|
+
deriveOutgoingEphPk,
|
|
8648
|
+
deriveOutgoingEphSk,
|
|
8649
|
+
deriveOutgoingSharedSecret,
|
|
8650
|
+
deriveOutgoingViewingKeyV3,
|
|
8199
8651
|
deriveOwnerPk,
|
|
8200
8652
|
derivePairwiseEphSk,
|
|
8201
8653
|
derivePairwiseSharedSecret,
|
|
@@ -8204,6 +8656,7 @@ var OrbinumWallet = class {
|
|
|
8204
8656
|
deriveSpendingKeyFromSignature,
|
|
8205
8657
|
deriveSpendingKeyMessageV2,
|
|
8206
8658
|
deriveSpendingKeyTypedData,
|
|
8659
|
+
deriveSpendingKeyV3,
|
|
8207
8660
|
deriveStealthOwnerPk,
|
|
8208
8661
|
deriveStealthSk,
|
|
8209
8662
|
deriveVaultBlindKey,
|
|
@@ -8211,9 +8664,9 @@ var OrbinumWallet = class {
|
|
|
8211
8664
|
deriveViewTag,
|
|
8212
8665
|
deriveViewingPublicKey,
|
|
8213
8666
|
deriveViewingSecretKey,
|
|
8667
|
+
deriveViewingSecretKeyV3,
|
|
8214
8668
|
detectCommitmentMismatch,
|
|
8215
8669
|
encodeNoteBackup,
|
|
8216
|
-
encodeNoteTransferPages,
|
|
8217
8670
|
encodePaymentSlip,
|
|
8218
8671
|
encryptJson,
|
|
8219
8672
|
encryptNote,
|
|
@@ -8223,6 +8676,7 @@ var OrbinumWallet = class {
|
|
|
8223
8676
|
evmToImplicitSubstrate,
|
|
8224
8677
|
evmToMappedAccountHex,
|
|
8225
8678
|
evmToSubstrate,
|
|
8679
|
+
exportViewingCredential,
|
|
8226
8680
|
extractPalletError,
|
|
8227
8681
|
failed,
|
|
8228
8682
|
fastMulBase,
|
|
@@ -8263,9 +8717,9 @@ var OrbinumWallet = class {
|
|
|
8263
8717
|
isNativeAsset,
|
|
8264
8718
|
isNoteSelfConsistent,
|
|
8265
8719
|
isSpendable,
|
|
8266
|
-
isSs58,
|
|
8267
8720
|
isSubstrateAddress,
|
|
8268
8721
|
isUnifiedAddress,
|
|
8722
|
+
isUsableSecretKey,
|
|
8269
8723
|
isValidLeafIndex,
|
|
8270
8724
|
leHexToBigint,
|
|
8271
8725
|
mapExtrinsicArgs,
|
|
@@ -8282,10 +8736,10 @@ var OrbinumWallet = class {
|
|
|
8282
8736
|
noteMatchesCommitment,
|
|
8283
8737
|
noteOrigin,
|
|
8284
8738
|
noteSpentTxHash,
|
|
8285
|
-
noteToTransferEntry,
|
|
8286
8739
|
noteTxKind,
|
|
8287
|
-
openOutgoingBlob,
|
|
8288
8740
|
openPaymentSlip,
|
|
8741
|
+
openRecipientBookEntry,
|
|
8742
|
+
outgoingEphWindow,
|
|
8289
8743
|
outranks,
|
|
8290
8744
|
pairwiseEphWindow,
|
|
8291
8745
|
palletErrorKind,
|
|
@@ -8296,15 +8750,18 @@ var OrbinumWallet = class {
|
|
|
8296
8750
|
planTransfer,
|
|
8297
8751
|
planUnshield,
|
|
8298
8752
|
randomBlinding,
|
|
8299
|
-
|
|
8753
|
+
reconstructOutgoingIndex,
|
|
8300
8754
|
reconstructOutgoingTxRecords,
|
|
8301
8755
|
recoverOwnerPkPoint,
|
|
8302
8756
|
recoverSelfStealthNote,
|
|
8757
|
+
recoverSentFromSharedSecret,
|
|
8758
|
+
recoverSentNote,
|
|
8303
8759
|
refuseIfAlreadySpent,
|
|
8304
8760
|
regeneratePaymentSlip,
|
|
8761
|
+
registerPairwiseCounterparty,
|
|
8305
8762
|
removeByCommitment,
|
|
8306
8763
|
requireSessionKeys,
|
|
8307
|
-
|
|
8764
|
+
reserveOutgoingIndex,
|
|
8308
8765
|
reserveSelfEphIndex,
|
|
8309
8766
|
resolveSelfEphCeiling,
|
|
8310
8767
|
resolveSpentSet,
|
|
@@ -8313,8 +8770,8 @@ var OrbinumWallet = class {
|
|
|
8313
8770
|
runScan,
|
|
8314
8771
|
scalarToHex,
|
|
8315
8772
|
scanAbortError,
|
|
8316
|
-
sealOutgoingBlob,
|
|
8317
8773
|
sealPaymentSlip,
|
|
8774
|
+
sealRecipientBookEntry,
|
|
8318
8775
|
selectDescribingNote,
|
|
8319
8776
|
selectDescribingNoteByCommitment,
|
|
8320
8777
|
selectGhosts,
|
|
@@ -8340,10 +8797,10 @@ var OrbinumWallet = class {
|
|
|
8340
8797
|
truncateMiddle,
|
|
8341
8798
|
tryDecryptNote,
|
|
8342
8799
|
tryDecryptNoteVerbose,
|
|
8343
|
-
tryRecoverOutgoing,
|
|
8344
8800
|
txLandedAfterError,
|
|
8345
8801
|
u128,
|
|
8346
8802
|
u64,
|
|
8803
|
+
unpackUsableViewingKey,
|
|
8347
8804
|
unshieldNote,
|
|
8348
8805
|
upsertNote,
|
|
8349
8806
|
vaultReplacer,
|