From 986e7fb44f4d7a530bf68ebc88aa883e5c819a73 Mon Sep 17 00:00:00 2001 From: Andrea Scartabelli Date: Mon, 2 Dec 2024 10:59:19 +0100 Subject: [PATCH] wip --- w3sper.js/src/network/syncer/account.js | 88 +++++++++++++- w3sper.js/tests/history_test.js | 152 ++++++++++++++++++++++++ 2 files changed, 236 insertions(+), 4 deletions(-) create mode 100644 w3sper.js/tests/history_test.js diff --git a/w3sper.js/src/network/syncer/account.js b/w3sper.js/src/network/syncer/account.js index 17e5ab9e46..b0b6f0ca4b 100644 --- a/w3sper.js/src/network/syncer/account.js +++ b/w3sper.js/src/network/syncer/account.js @@ -155,8 +155,8 @@ export class AccountSyncer extends EventTarget { async balances(profiles) { const balances = await accountsIntoRaw(profiles).then((rawUsers) => rawUsers.map((user) => - this.#network.contracts.transferContract.call.account(user), - ), + this.#network.contracts.transferContract.call.account(user) + ) ); return Promise.all(balances) @@ -165,6 +165,86 @@ export class AccountSyncer extends EventTarget { .then((buffers) => buffers.map(parseBalance)); } + /** + * Fetches the moonlight transactions history for + * the given profiles. + * + * @param {Array} profiles + * @param {Object} [options={}] + * @param {bigint} [options.from] + * @param {AbortSignal} [options.signal] + * @returns {ReadableStream} + */ + history(profiles, options = {}) { + const max = (a, b) => (a > b ? a : b); + const HISTORY_CHUNK_SIZE = 100n; + const { signal } = options; + + let data = []; + let idx = 0; + + return new ReadableStream({ + cancel(reason) { + console.log("Stream canceled:", reason); + }, + + async pull(controller) { + if (signal?.aborted) { + this.cancel(signal.reason ?? "Abort signal received"); + controller.close(); + return; + } + + if (idx < data.length) { + controller.enqueue(data[idx++]); + } else { + controller.close(); + } + }, + + start: async (controller) => { + const to = options.from ?? (await this.#network.blockHeight); + const from = max(0n, to - HISTORY_CHUNK_SIZE); + + data = await Promise.all( + profiles.map((profile) => { + const key = profile.account.toString(); + + return Promise.all( + ["sender", "receiver"].map((type) => + this.#network + .query( + `moonlightHistory( + ${type}: "${key}", + fromBlock: ${from}, + toBlock: ${to} + ) { json }`, + { signal } + ) + .then((result) => result.moonlightHistory?.json ?? []) + ) + ); + }) + ) + .then((results) => + results + .flat(2) + .map((data) => { + return { + blockHeight: BigInt(data.block_height), + id: data.origin, + }; + }) + .sort((a, b) => Number(b.blockHeight - a.blockHeight)) + ) + .catch((error) => { + console.error("Error fetching data", error); + controller.error(error); + }); + }, + }); + } + /** * Fetches the stakes for the given profiles. * @@ -174,8 +254,8 @@ export class AccountSyncer extends EventTarget { async stakes(profiles) { const stakes = await accountsIntoRaw(profiles).then((rawUsers) => rawUsers.map((user) => - this.#network.contracts.stakeContract.call.get_stake(user), - ), + this.#network.contracts.stakeContract.call.get_stake(user) + ) ); return Promise.all(stakes) diff --git a/w3sper.js/tests/history_test.js b/w3sper.js/tests/history_test.js new file mode 100644 index 0000000000..913517510e --- /dev/null +++ b/w3sper.js/tests/history_test.js @@ -0,0 +1,152 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +import { + AccountSyncer, + Bookkeeper, + Network, + ProfileGenerator, + Transfer, + useAsProtocolDriver, +} from "@dusk/w3sper"; + +import { + assert, + getLocalWasmBuffer, + seeder, + test, + Treasury, +} from "./harness.js"; + +const resolveAfter = (delay, value) => + new Promise((resolve) => { + setTimeout(() => resolve(value), delay); + }); + +const skip = () => {}; + +const getSortedHashes = (txs) => + txs + .toSorted((a, b) => Number(b.blockHeight - a.blockHeight)) + .map((tx) => tx.id); + +async function getTxsFromStream(txsStream) { + const result = []; + + for await (const tx of txsStream) { + result.push(tx); + } + + return result; +} + +const profileIndexer = (rawTransfers) => (profileIdx) => + rawTransfers.reduce((result, current, idx) => { + if (current.from === profileIdx || current.to === profileIdx) { + result.push(idx); + } + + return result; + }, []); + +skip("debug", async () => { + const network = await Network.connect("http://localhost:8080/"); + + const profileGenerator = new ProfileGenerator(seeder); + const profiles = [ + await profileGenerator.default, + await profileGenerator.next(), + await profileGenerator.next(), + ]; + const syncer = new AccountSyncer(network); + + const txs = await getTxsFromStream(await syncer.history(profiles)); + + console.log(txs); + + await network.disconnect(); +}); + +test("accounts history", async () => { + const rawTransfers = [ + { amount: 10n, from: 0, to: 1 }, + { amount: 30n, from: 0, to: 1 }, + { amount: 20n, from: 1, to: 0 }, + { amount: 12n, from: 2, to: 1 }, + ]; + const getIndexesForProfile = profileIndexer(rawTransfers); + const { profiles, transfers } = await useAsProtocolDriver( + await getLocalWasmBuffer() + ).then(async () => { + const profileGenerator = new ProfileGenerator(seeder); + const profiles = await Promise.all([ + profileGenerator.default, + profileGenerator.next(), + profileGenerator.next(), + ]); + const nonces = Array(profiles.length).fill(0n); + const transfers = await Promise.all( + rawTransfers.map((raw) => + new Transfer(profiles[raw.from]) + .amount(raw.amount) + .to(profiles[raw.to].account) + .nonce(nonces[raw.from]++) + .chain(Network.LOCALNET) + .gas({ limit: 500_000_000n }) + .build() + ) + ); + + return { profiles, transfers }; + }); + + const network = await Network.connect("http://localhost:8080/"); + + for (const transfer of transfers) { + await network.execute(transfer); + await network.transactions.withId(transfer.hash).once.executed(); + } + + console.log("start waiting", new Date()); + + await resolveAfter(15000); + + console.log("end waiting", new Date()); + + const syncer = new AccountSyncer(network); + + let txs; + + // All transactions + const allHashes = transfers + .flatMap((transfer) => [transfer.hash, transfer.hash]) + .toReversed(); + + txs = await getTxsFromStream(syncer.history(profiles)); + + assert.equal( + getSortedHashes(txs).join(","), + allHashes.join(","), + "All hashes" + ); + + // Per profile transactions + for (let i = 0; i < profiles.length; i++) { + const profileHashes = getIndexesForProfile(i) + .map((idx) => transfers[idx].hash) + .toReversed(); + + txs = await getTxsFromStream(syncer.history([profiles[i]])); + + assert.equal( + getSortedHashes(txs).join(","), + profileHashes.join(","), + `hashes for profile ${i}` + ); + } + + await network.disconnect(); +});