Skip to content

Commit

Permalink
web-wallet: Retrieve and cache stake info using w3sper
Browse files Browse the repository at this point in the history
Resolves @2946
  • Loading branch information
ascartabelli committed Nov 13, 2024
1 parent 11a572d commit 326aee8
Showing 15 changed files with 334 additions and 45 deletions.
15 changes: 14 additions & 1 deletion web-wallet/src/lib/vendor/w3sper.js/src/bookkeeper.js
Original file line number Diff line number Diff line change
@@ -22,10 +22,14 @@ class BookEntry {
Object.freeze(this);
}

async balance(type) {
balance(type) {
return this.bookkeeper.balance(this.profile[type]);
}

stakeInfo() {
return this.bookkeeper.stakeInfo(this.profile.account);
}

transfer(amount) {
return new Transfer(this).amount(amount);
}
@@ -60,6 +64,15 @@ export class Bookkeeper {
}
}

stakeInfo(identifier) {
const type = ProfileGenerator.typeOf(String(identifier));
if (type !== "account") {
throw new TypeError("Only accounts can stake");
}

return this.#treasury.stakeInfo(identifier);
}

async pick(identifier, amount) {
const notes = await this.#treasury.address(identifier);
const seed = await ProfileGenerator.seedFrom(identifier);
26 changes: 26 additions & 0 deletions web-wallet/src/lib/vendor/w3sper.js/src/encoders/b16.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// @ts-nocheck
// 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.

export const encode = (buffer) =>
Array.from(buffer)
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("");

export function decode(string) {
// Check if the string has an even length and contains only valid hex characters
if (string.length % 2 !== 0 || !/^[\da-fA-F]+$/.test(string)) {
return null;
}

const buffer = new Uint8Array(string.length / 2);

for (let i = 0; i < string.length; i += 2) {
buffer[i / 2] = parseInt(string.slice(i, i + 2), 16);
}

return buffer;
}
Original file line number Diff line number Diff line change
@@ -35,4 +35,10 @@ export class Contracts {
"0100000000000000000000000000000000000000000000000000000000000000"
);
}

get stakeContract() {
return this.withId(
"0200000000000000000000000000000000000000000000000000000000000000"
);
}
}
Original file line number Diff line number Diff line change
@@ -5,11 +5,40 @@
//
// Copyright (c) DUSK NETWORK. All rights reserved.

export class Transactions {
import { Gas } from "../../gas.js";
import { RuesScope } from "../../rues/scope.js";
import { RuesEvent } from "../../rues/event.js";
import * as base16 from "../../encoders/b16.js";

class TransactionExecutedEvent extends RuesEvent {
constructor(type) {
super(type);
}

get gasPaid() {
return new Gas({
limit: this.payload["gas_spent"],
price: this.payload.inner.fee["gas_price"],
}).total;
}

memo(options = {}) {
const buffer = base16.decode(this.payload.inner.memo);

if (options.as === "string") {
return new TextDecoder().decode(buffer);
}

return buffer;
}
}

export class Transactions extends RuesScope {
#scope = null;

constructor(rues) {
this.#scope = rues.scope("transactions");
super("transactions");
this.#scope = rues.scope(this);
}

preverify(tx) {
@@ -45,4 +74,13 @@ export class Transactions {
withId(id) {
return this.#scope.withId(id);
}

eventFrom(ruesEvent) {
switch (ruesEvent.origin.topic) {
case "executed":
return TransactionExecutedEvent.from(ruesEvent);
}

return ruesEvent;
}
}
2 changes: 1 addition & 1 deletion web-wallet/src/lib/vendor/w3sper.js/src/network/mod.js
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ import { Node } from "./components/node.js";
import { Blocks } from "./components/blocks.js";
import { Transactions } from "./components/transactions.js";
import { Contracts } from "./components/contracts.js";
import { Gas } from "./gas.js";
import { Gas } from "../gas.js";

export { Gas };
export { AddressSyncer } from "./syncer/address.js";
160 changes: 148 additions & 12 deletions web-wallet/src/lib/vendor/w3sper.js/src/network/syncer/account.js
Original file line number Diff line number Diff line change
@@ -6,8 +6,93 @@
// Copyright (c) DUSK NETWORK. All rights reserved.

import * as ProtocolDriver from "../../protocol-driver/mod.js";
import * as base58 from "../../b58.js";
import * as base58 from "../../encoders/b58.js";

/**
* Represents the value staked, locked, and eligibility of a stake.
*/
class StakeAmount {
/** @type {bigint} */
value = 0n;
/** @type {bigint} */
locked = 0n;
/** @type {bigint} */
eligibility = 0n;

/**
* Returns the total amount of staked value, including locked value.
*
* @returns {bigint} Total staked amount.
*/
get total() {
return this.value + this.locked;
}
}

/**
* Holds information about a user's stake, including amount, reward,
* and a nonce to prevent repeat attacks. Also tracks faults.
*/
class StakeInfo {
/** @type {StakeAmount|null} */
amount;
/** @type {bigint} */
reward;
/** @type {bigint} */
nonce;
/** @type {number} */
faults;
/** @type {number} */
hardFaults;

constructor() {
this.amount = null;
this.reward = 0n;
this.nonce = 0n;
this.faults = 0;
this.hardFaults = 0;
}

/**
* Parses a buffer into a {StakeInfo} instance.
*
* @param {ArrayBuffer} buffer - The buffer containing stake data.
* @returns {StakeInfo} The parsed {StakeInfo} instance.
*/
static parse(buffer) {
const view = new DataView(buffer);
const stakeInfo = new StakeInfo();
const hasStake = view.getUint8(0) === 1;

if (!hasStake) {
return Object.freeze(stakeInfo);
}

const hasStakeAmount = view.getUint8(8) === 1;

if (hasStakeAmount) {
stakeInfo.amount = new StakeAmount();
stakeInfo.amount.value = view.getBigUint64(16, true);
stakeInfo.amount.locked = view.getBigUint64(24, true);
stakeInfo.amount.eligibility = view.getBigUint64(32, true);
}

stakeInfo.reward = view.getBigUint64(40, true);
stakeInfo.nonce = view.getBigUint64(48, true);
stakeInfo.faults = view.getUint8(56);
stakeInfo.hardFaults = view.getUint8(57);

return Object.freeze(stakeInfo);
}
}

/**
* Converts a resource, either a string or an object with an account,
* into an account buffer if it has a byteLength of 96.
*
* @param {Object|string} resource - The resource to convert.
* @returns {ArrayBuffer|Object|string} The account buffer or the resource.
*/
function intoAccount(resource) {
if (resource?.account?.valueOf()?.byteLength === 96) {
return resource.account;
@@ -21,31 +106,82 @@ function intoAccount(resource) {
return resource;
}

/**
* Converts account profiles into raw representations.
*
* @param {Array<Object>} profiles - Array of profile objects.
* @returns {Promise<Array<Uint8Array>>} The raw account buffers.
*/
const accountsIntoRaw = (profiles) =>
ProtocolDriver.accountsIntoRaw(profiles.map(intoAccount));

/**
* Parses a buffer to extract account balance information.
*
* @param {ArrayBuffer} buffer - The buffer containing balance data.
* @returns {{ nonce: bigint, value: bigint }} The parsed balance data.
*/
const parseBalance = (buffer) => {
const view = new DataView(buffer);
const nonce = view.getBigUint64(0, true);
const value = view.getBigUint64(8, true);

return { nonce, value };
};

/**
* Syncs account data by querying the network for balance and stake information.
*
* @extends EventTarget
*/
export class AccountSyncer extends EventTarget {
/** @type {Object} */
#network;

/**
* Creates an AccountSyncer instance.
* @param {Object} network - The network interface for accessing accounts.
*/
constructor(network) {
super();
this.#network = network;
}

/**
* Fetches the balances for the given profiles.
*
* @param {Array<Object>} profiles - Array of profile objects.
* @returns {Promise<Array<{ nonce: bigint, value: bigint }>>} Array of balances.
*/
async balances(profiles) {
const rawUsers = await ProtocolDriver.accountsIntoRaw(
profiles.map(intoAccount)
const balances = await accountsIntoRaw(profiles).then((rawUsers) =>
rawUsers.map((user) =>
this.#network.contracts.transferContract.call.account(user)
)
);

let balances = rawUsers.map((user) =>
this.#network.contracts.transferContract.call.account(user)
return Promise.all(balances)
.then((responses) => responses.map((resp) => resp.arrayBuffer()))
.then((buffers) => Promise.all(buffers))
.then((buffers) => buffers.map(parseBalance));
}

/**
* Fetches the stakes for the given profiles.
*
* @param {Array<Object>} profiles - Array of profile objects.
* @returns {Promise<Array<StakeInfo>>} Array of parsed stake information.
*/
async stakes(profiles) {
const stakes = await accountsIntoRaw(profiles).then((rawUsers) =>
rawUsers.map((user) =>
this.#network.contracts.stakeContract.call.get_stake(user)
)
);

return await Promise.all(balances)
return Promise.all(stakes)
.then((responses) => responses.map((resp) => resp.arrayBuffer()))
.then((buffers) => Promise.all(buffers))
.then((buffers) =>
buffers.map((buffer) => ({
nonce: new DataView(buffer).getBigUint64(0, true),
value: new DataView(buffer).getBigUint64(8, true),
}))
);
.then((buffers) => buffers.map(StakeInfo.parse));
}
}
2 changes: 1 addition & 1 deletion web-wallet/src/lib/vendor/w3sper.js/src/profile.js
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@
// Copyright (c) DUSK NETWORK. All rights reserved.

import * as ProtocolDriver from "../src/protocol-driver/mod.js";
import * as base58 from "./b58.js";
import * as base58 from "./encoders/b58.js";

const _index = Symbol("profile::index");
const _seeder = Symbol("profile::seeder");
36 changes: 35 additions & 1 deletion web-wallet/src/lib/vendor/w3sper.js/src/protocol-driver/mod.js
Original file line number Diff line number Diff line change
@@ -574,6 +574,15 @@ export const moonlight = async (info) =>
let tx = await malloc(4);
let hash = await malloc(64);

const data = serializeMemo(info.data);

if (data) {
ptr.data = await malloc(data.byteLength);
await memcpy(ptr.data, data);
} else {
ptr.data = null;
}

// Copy the value to the WASM memory
const code = await moonlight(
ptr.seed,
@@ -585,7 +594,7 @@ export const moonlight = async (info) =>
ptr.gas_price,
ptr.nonce,
info.chainId,
info.data,
ptr.data,
tx,
hash
);
@@ -783,3 +792,28 @@ export const shield = async (info) =>
hash = new TextDecoder().decode(await memcpy(null, hash, 64));
return [tx_buffer, hash];
})();

function serializeMemo(memo) {
if (!memo) {
return null;
}

let buffer = null;
if (typeof memo === "string") {
buffer = new TextEncoder().encode(memo);
} else if (memo instanceof ArrayBuffer) {
buffer = new Uint8Array(memo);
} else if (memo instanceof Uint8Array) {
buffer = memo;
}

if (!buffer) {
return null;
}

const memoBuffer = new Uint8Array(1 + buffer.byteLength);
memoBuffer[0] = 3; // Memo type
memoBuffer.set(buffer, 1);

return new Uint8Array(DataBuffer.from(memoBuffer));
}
Loading

0 comments on commit 326aee8

Please sign in to comment.