@dusk-network/app-core 0.3.2 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/README.md +15 -11
- package/package.json +1 -1
- package/src/activity.d.ts +5 -1
- package/src/activity.js +7 -1
- package/src/bridge.d.ts +26 -10
- package/src/bridge.js +184 -101
- package/src/duskevm-withdrawals.d.ts +31 -6
- package/src/duskevm-withdrawals.js +234 -18
- package/src/history.d.ts +34 -10
- package/src/history.js +80 -18
- package/src/shielded.d.ts +21 -6
- package/src/shielded.js +155 -42
package/src/shielded.js
CHANGED
|
@@ -38,6 +38,17 @@ export const HEIGHTS_PER_REQUEST = 15;
|
|
|
38
38
|
// (`convert`, `mint`, `withdraw`), where a `phoenix` event lists whole notes.
|
|
39
39
|
const RECEIVER_START = 328;
|
|
40
40
|
const RECEIVER_END = 392;
|
|
41
|
+
// Such an event makes one transparent note, and carries the note's whole
|
|
42
|
+
// shielded address with its value right after it; the note holds that
|
|
43
|
+
// value in the 8 bytes after its position.
|
|
44
|
+
const ADDRESS_END = 648;
|
|
45
|
+
const VALUE_START = 656;
|
|
46
|
+
const VALUE_END = 664;
|
|
47
|
+
const MAKES_NOTE = new Set(["convert", "mint", "withdraw"]);
|
|
48
|
+
// A `convert` event: its sender, then where its value went, a shielded
|
|
49
|
+
// address (0) or a public account (1).
|
|
50
|
+
const CONVERT_BYTES = 544;
|
|
51
|
+
const CONVERT_RECEIVER = 208;
|
|
41
52
|
// A request for 15 blocks and their events may take a while.
|
|
42
53
|
const BLOCKS_TIMEOUT_MS = 30_000;
|
|
43
54
|
const MAX_U64 = (1n << 64n) - 1n;
|
|
@@ -328,6 +339,24 @@ function randomIndex(n, random) {
|
|
|
328
339
|
|
|
329
340
|
const cryptoRandom = () => crypto.getRandomValues(new Uint32Array(1))[0];
|
|
330
341
|
|
|
342
|
+
/**
|
|
343
|
+
* The heights in a random order.
|
|
344
|
+
*
|
|
345
|
+
* @param {ReadonlyArray<number>} list
|
|
346
|
+
* @param {() => number} random
|
|
347
|
+
*/
|
|
348
|
+
function shuffled(list, random) {
|
|
349
|
+
const heights = [...list];
|
|
350
|
+
|
|
351
|
+
for (let i = heights.length - 1; i > 0; i -= 1) {
|
|
352
|
+
const j = randomIndex(i + 1, random);
|
|
353
|
+
|
|
354
|
+
[heights[i], heights[j]] = [heights[j], heights[i]];
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return heights;
|
|
358
|
+
}
|
|
359
|
+
|
|
331
360
|
/**
|
|
332
361
|
* @param {ReadonlyArray<number>} pool
|
|
333
362
|
* @param {ReadonlyArray<number>} real
|
|
@@ -348,15 +377,7 @@ export function pickHeights(pool, real, { keep = [], perHeight = DECOYS_PER_HEIG
|
|
|
348
377
|
free.pop();
|
|
349
378
|
}
|
|
350
379
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
for (let i = heights.length - 1; i > 0; i -= 1) {
|
|
354
|
-
const j = randomIndex(i + 1, random);
|
|
355
|
-
|
|
356
|
-
[heights[i], heights[j]] = [heights[j], heights[i]];
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
return { decoys, heights };
|
|
380
|
+
return { decoys, heights: shuffled([...own, ...decoys], random) };
|
|
360
381
|
}
|
|
361
382
|
|
|
362
383
|
// ---------------------------------------------------------------------------
|
|
@@ -430,11 +451,12 @@ export async function fetchShieldedBlocks(archive, heights, { isCurrent = always
|
|
|
430
451
|
// ---------------------------------------------------------------------------
|
|
431
452
|
|
|
432
453
|
/**
|
|
433
|
-
* What one transaction did to the profile's notes
|
|
454
|
+
* What one transaction did to the profile's notes, and what its `convert`
|
|
455
|
+
* event says it was.
|
|
434
456
|
*
|
|
435
|
-
* @typedef {{ hash: string, height: number, made: bigint, spent: bigint, topics: Set<string
|
|
457
|
+
* @typedef {{ hash: string, height: number, made: bigint, spent: bigint, topics: Set<string>, conversion: EntryKind | null }} Tally
|
|
436
458
|
*
|
|
437
|
-
* @typedef {{ matched: boolean, part: string, value: bigint, whole: string }} MadeNote
|
|
459
|
+
* @typedef {{ matched: boolean, part: string, tail: string, value: bigint, whole: string }} MadeNote
|
|
438
460
|
*/
|
|
439
461
|
|
|
440
462
|
/**
|
|
@@ -451,35 +473,78 @@ function hexHas(hay, needle) {
|
|
|
451
473
|
return false;
|
|
452
474
|
}
|
|
453
475
|
|
|
476
|
+
/**
|
|
477
|
+
* What a `convert` event did: a shield, into a note, or an unshield, to a
|
|
478
|
+
* public account; null for an event of another shape.
|
|
479
|
+
*
|
|
480
|
+
* @param {string} data - its data, as lower-case hex
|
|
481
|
+
* @returns {EntryKind | null}
|
|
482
|
+
*/
|
|
483
|
+
function conversionOf(data) {
|
|
484
|
+
const tag = data.length === 2 * CONVERT_BYTES ? data.slice(2 * CONVERT_RECEIVER, 2 * CONVERT_RECEIVER + 2) : "";
|
|
485
|
+
|
|
486
|
+
return tag === "00" ? ENTRY_KIND.SHIELD : tag === "01" ? ENTRY_KIND.UNSHIELD : null;
|
|
487
|
+
}
|
|
488
|
+
|
|
454
489
|
/**
|
|
455
490
|
* Adds one transfer contract event to its transaction's tally. A `phoenix`
|
|
456
491
|
* event names the nullifiers its transaction spent and the notes it made
|
|
457
|
-
* (change and the fee's refund among them); an event paying a
|
|
458
|
-
* receiver from a public account or a contract
|
|
492
|
+
* (change and the fee's refund among them), whole; an event paying a
|
|
493
|
+
* shielded receiver from a public account or a contract made one note, and
|
|
494
|
+
* is kept in `makers` until every whole note of the block is matched.
|
|
459
495
|
*
|
|
460
496
|
* @param {Tally} tally
|
|
461
497
|
* @param {any} event
|
|
462
498
|
* @param {MadeNote[]} made - the profile's notes at this height
|
|
463
499
|
* @param {{ nullifier: string, value: bigint }[]} spent - the profile's spent notes
|
|
500
|
+
* @param {{ tally: Tally, data: string }[]} makers
|
|
464
501
|
*/
|
|
465
|
-
function addTransferEvent(tally, event, made, spent) {
|
|
502
|
+
function addTransferEvent(tally, event, made, spent, makers) {
|
|
466
503
|
const data = String(event.data).toLowerCase();
|
|
467
|
-
const
|
|
504
|
+
const topic = String(event.topic);
|
|
468
505
|
|
|
469
|
-
tally.topics.add(
|
|
506
|
+
tally.topics.add(topic);
|
|
470
507
|
|
|
471
|
-
if (
|
|
472
|
-
|
|
473
|
-
|
|
508
|
+
if (topic === "convert") tally.conversion ??= conversionOf(data);
|
|
509
|
+
if (MAKES_NOTE.has(topic)) makers.push({ data, tally });
|
|
510
|
+
if (topic !== "phoenix") return;
|
|
511
|
+
|
|
512
|
+
for (const n of spent) if (hexHas(data, n.nullifier)) tally.spent += n.value;
|
|
474
513
|
|
|
475
514
|
for (const n of made) {
|
|
476
|
-
if (!n.matched && hexHas(data,
|
|
515
|
+
if (!n.matched && hexHas(data, n.whole)) {
|
|
477
516
|
tally.made += n.value;
|
|
478
517
|
n.matched = true;
|
|
479
518
|
}
|
|
480
519
|
}
|
|
481
520
|
}
|
|
482
521
|
|
|
522
|
+
/**
|
|
523
|
+
* Gives each event that made one note the note it made: first the one
|
|
524
|
+
* whose shielded address and value it carries, then one whose address it
|
|
525
|
+
* names, so two notes to the same address in one block go to an event
|
|
526
|
+
* each.
|
|
527
|
+
*
|
|
528
|
+
* @param {{ tally: Tally, data: string }[]} makers
|
|
529
|
+
* @param {MadeNote[]} made
|
|
530
|
+
*/
|
|
531
|
+
function matchMakers(makers, made) {
|
|
532
|
+
/** @type {Set<{ tally: Tally, data: string }>} */
|
|
533
|
+
const done = new Set();
|
|
534
|
+
|
|
535
|
+
for (const key of /** @type {const} */ (["tail", "part"])) {
|
|
536
|
+
for (const maker of makers) {
|
|
537
|
+
const note = done.has(maker) ? undefined : made.find((n) => !n.matched && hexHas(maker.data, n[key]));
|
|
538
|
+
|
|
539
|
+
if (!note) continue;
|
|
540
|
+
|
|
541
|
+
maker.tally.made += note.value;
|
|
542
|
+
note.matched = true;
|
|
543
|
+
done.add(maker);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
483
548
|
/**
|
|
484
549
|
* @param {Tally} t
|
|
485
550
|
* @returns {EntryKind}
|
|
@@ -488,7 +553,7 @@ function kindOf(t) {
|
|
|
488
553
|
if (t.topics.has("stake:stake")) return ENTRY_KIND.STAKE;
|
|
489
554
|
if (t.topics.has("stake:unstake")) return ENTRY_KIND.UNSTAKE;
|
|
490
555
|
if (t.topics.has("stake:withdraw")) return ENTRY_KIND.WITHDRAW_REWARD;
|
|
491
|
-
if (t.topics.has("convert")) return t.spent > 0n ? ENTRY_KIND.UNSHIELD : ENTRY_KIND.SHIELD;
|
|
556
|
+
if (t.topics.has("convert")) return t.conversion ?? (t.spent > 0n ? ENTRY_KIND.UNSHIELD : ENTRY_KIND.SHIELD);
|
|
492
557
|
|
|
493
558
|
return t.topics.has("deposit") ? ENTRY_KIND.CONTRACT_CALL : ENTRY_KIND.TRANSFER;
|
|
494
559
|
}
|
|
@@ -559,7 +624,7 @@ function tallyNotes(notes, blocks) {
|
|
|
559
624
|
if (known) return known;
|
|
560
625
|
|
|
561
626
|
/** @type {Tally} */
|
|
562
|
-
const tally = { hash, height, made: 0n, spent: 0n, topics: new Set() };
|
|
627
|
+
const tally = { conversion: null, hash, height, made: 0n, spent: 0n, topics: new Set() };
|
|
563
628
|
|
|
564
629
|
txs.set(hash, tally);
|
|
565
630
|
|
|
@@ -575,18 +640,23 @@ function tallyNotes(notes, blocks) {
|
|
|
575
640
|
const made = own.map((n) => ({
|
|
576
641
|
matched: false,
|
|
577
642
|
part: hex(n.leaf.subarray(RECEIVER_START, RECEIVER_END)),
|
|
643
|
+
tail: hex(n.leaf.subarray(RECEIVER_START, ADDRESS_END)) + hex(n.leaf.subarray(VALUE_START, VALUE_END)),
|
|
578
644
|
value: BigInt(n.value),
|
|
579
645
|
whole: hex(n.leaf.subarray(0, NOTE_BYTES)),
|
|
580
646
|
}));
|
|
647
|
+
/** @type {{ tally: Tally, data: string }[]} */
|
|
648
|
+
const makers = [];
|
|
581
649
|
|
|
582
650
|
for (const event of /** @type {any[]} */ (events)) {
|
|
583
651
|
const source = String(event.source ?? event.target ?? "").toLowerCase();
|
|
584
652
|
const hash = String(event.origin).toLowerCase();
|
|
585
653
|
|
|
586
654
|
if (source === STAKE_CONTRACT) tallyOf(hash, height).topics.add(`stake:${event.topic}`);
|
|
587
|
-
else if (source === TRANSFER_CONTRACT) addTransferEvent(tallyOf(hash, height), event, made, spent);
|
|
655
|
+
else if (source === TRANSFER_CONTRACT) addTransferEvent(tallyOf(hash, height), event, made, spent, makers);
|
|
588
656
|
}
|
|
589
657
|
|
|
658
|
+
matchMakers(makers, made);
|
|
659
|
+
|
|
590
660
|
if (height > 0) unnamed += made.filter((n) => !n.matched).length;
|
|
591
661
|
}
|
|
592
662
|
|
|
@@ -626,26 +696,41 @@ const fingerprintOf = (notes) =>
|
|
|
626
696
|
.join(",");
|
|
627
697
|
|
|
628
698
|
/**
|
|
629
|
-
*
|
|
630
|
-
* can't be read rejects: taking it for none would draw new decoys for the
|
|
631
|
-
* same notes, and a node could compare the two sets.
|
|
699
|
+
* A kept history, as far as it reads as one; null for none.
|
|
632
700
|
*
|
|
633
|
-
* @param {
|
|
634
|
-
* @returns {
|
|
701
|
+
* @param {unknown} kept
|
|
702
|
+
* @returns {ShieldedRecord | null}
|
|
635
703
|
*/
|
|
636
|
-
|
|
637
|
-
/** @type {any} */
|
|
638
|
-
const kept = await store.get();
|
|
639
|
-
|
|
704
|
+
function recordOf(kept) {
|
|
640
705
|
if (!kept || typeof kept !== "object") return null;
|
|
641
706
|
|
|
707
|
+
const { decoys, entries, fingerprint } = /** @type {any} */ (kept);
|
|
708
|
+
|
|
642
709
|
return {
|
|
643
|
-
decoys: Array.isArray(
|
|
644
|
-
entries: Array.isArray(
|
|
645
|
-
fingerprint: typeof
|
|
710
|
+
decoys: Array.isArray(decoys) ? decoys.filter(isHeight) : [],
|
|
711
|
+
entries: Array.isArray(entries) ? entries : [],
|
|
712
|
+
fingerprint: typeof fingerprint === "string" ? fingerprint : "",
|
|
646
713
|
};
|
|
647
714
|
}
|
|
648
715
|
|
|
716
|
+
/**
|
|
717
|
+
* The kept history; null for none. A store that can't be read rejects:
|
|
718
|
+
* taking it for none would draw new decoys for the same notes, and a node
|
|
719
|
+
* could compare the two sets.
|
|
720
|
+
*
|
|
721
|
+
* @param {ShieldedStore} store
|
|
722
|
+
* @returns {Promise<ShieldedRecord | null>}
|
|
723
|
+
*/
|
|
724
|
+
const keptRecord = async (store) => recordOf(await store.get());
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* `decoys`, with those of `more` it lacks after them: kept decoys only grow.
|
|
728
|
+
*
|
|
729
|
+
* @param {ReadonlyArray<number>} decoys
|
|
730
|
+
* @param {ReadonlyArray<number>} more
|
|
731
|
+
*/
|
|
732
|
+
const grown = (decoys, more) => [...decoys, ...more.filter((h) => !decoys.includes(h))];
|
|
733
|
+
|
|
649
734
|
/**
|
|
650
735
|
* @param {ShieldedHistoryOptions} options
|
|
651
736
|
* @returns {Promise<ShieldedHistory>}
|
|
@@ -704,7 +789,7 @@ export async function shieldedHistory({ archive, notes, valueOf, tree, store, tr
|
|
|
704
789
|
// Not for a height the tree as read doesn't have: no decoy could be one.
|
|
705
790
|
if (!pool || real.some((h) => !inTree.has(h))) return unfinished;
|
|
706
791
|
|
|
707
|
-
|
|
792
|
+
let { decoys, heights } = pickHeights(pool, real, { keep: kept?.decoys ?? [], random });
|
|
708
793
|
|
|
709
794
|
// Without another height to ask among, asking would name the profile's own.
|
|
710
795
|
if (!decoys.length) return unfinished;
|
|
@@ -712,13 +797,36 @@ export async function shieldedHistory({ archive, notes, valueOf, tree, store, tr
|
|
|
712
797
|
// The decoys are kept before any block is asked for, beside what was kept
|
|
713
798
|
// before: a read that fails partway asks among the same ones next time,
|
|
714
799
|
// so no two requests differ in their decoys alone. They are read back:
|
|
715
|
-
// nothing is asked unless they are kept.
|
|
800
|
+
// nothing is asked unless they are kept. A store with `update` draws and
|
|
801
|
+
// keeps them in one transaction, from the decoys kept then: two reads at
|
|
802
|
+
// once ask among the same ones, the first kept.
|
|
716
803
|
if (!sameHeights(decoys, kept?.decoys ?? [])) {
|
|
717
|
-
|
|
804
|
+
/** @type {ShieldedRecord | null} */
|
|
805
|
+
let drawn = null;
|
|
718
806
|
|
|
719
|
-
|
|
807
|
+
if (typeof store.update === "function") {
|
|
808
|
+
const back = recordOf(
|
|
809
|
+
await store.update((now) => {
|
|
810
|
+
const record = recordOf(now);
|
|
811
|
+
const more = pickHeights(pool, real, { keep: record?.decoys ?? [], random }).decoys;
|
|
720
812
|
|
|
721
|
-
|
|
813
|
+
return (drawn = { decoys: more, entries: record?.entries ?? [], fingerprint: record?.fingerprint ?? "" });
|
|
814
|
+
})
|
|
815
|
+
) ?? (await keptRecord(store));
|
|
816
|
+
|
|
817
|
+
if (!back || !drawn || !sameHeights(back.decoys, /** @type {ShieldedRecord} */ (drawn).decoys)) {
|
|
818
|
+
throw new Error("The decoys weren't kept, so no block was asked for");
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
decoys = back.decoys;
|
|
822
|
+
heights = shuffled([...real, ...decoys], random ?? cryptoRandom);
|
|
823
|
+
} else {
|
|
824
|
+
await store.put({ decoys, entries: kept?.entries ?? [], fingerprint: kept?.fingerprint ?? "" });
|
|
825
|
+
|
|
826
|
+
const back = await keptRecord(store);
|
|
827
|
+
|
|
828
|
+
if (!back || !sameHeights(back.decoys, decoys)) throw new Error("The decoys weren't kept, so no block was asked for");
|
|
829
|
+
}
|
|
722
830
|
|
|
723
831
|
current();
|
|
724
832
|
}
|
|
@@ -737,7 +845,12 @@ export async function shieldedHistory({ archive, notes, valueOf, tree, store, tr
|
|
|
737
845
|
|
|
738
846
|
const entries = entriesOf(tallies, blocks);
|
|
739
847
|
|
|
740
|
-
|
|
848
|
+
// Kept over the decoys kept now, which only grow.
|
|
849
|
+
if (typeof store.update === "function") {
|
|
850
|
+
await store.update((now) => ({ decoys: grown(recordOf(now)?.decoys ?? [], decoys), entries, fingerprint }));
|
|
851
|
+
} else {
|
|
852
|
+
await store.put({ decoys, entries, fingerprint });
|
|
853
|
+
}
|
|
741
854
|
|
|
742
855
|
// A wallet that changed while its history was kept gets none of it.
|
|
743
856
|
current();
|