Skip to content

Commit

Permalink
w3sper: Add stake and unstake methods
Browse files Browse the repository at this point in the history
- Add `StakeTransfer` class in `transaction` module
- Add `UnstakeTransfer` class in `transaction` module
- Remove `BookEntry#balance()` method
- Add `BookEntry#info` getter that returns both `balance` and `stake` info
- Add `BookEntry#stake` method
- Add `BookEntry#unstake` method
- Add `bookkeeper#minimumStake` getter
- Add `getMinimumStake()` function in `protocol-driver` module
- Add `stake()` function in `protocol-driver` module
- Add `unstake()` function in `protocol-driver` module
- Change `assets/genesis.toml` to have enough balance for staking
- Add stake transactions tests
- Rename `stake_test.js` into `stake_info_test.js`

Resolves #2949, #2950
See also #2955
  • Loading branch information
ZER0 committed Nov 12, 2024
1 parent 6201808 commit 7c01de0
Show file tree
Hide file tree
Showing 7 changed files with 441 additions and 32 deletions.
30 changes: 24 additions & 6 deletions w3sper.js/src/bookkeeper.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
Transfer,
UnshieldTransfer,
ShieldTransfer,
StakeTransfer,
UnstakeTransfer,
} from "../src/transaction.js";

class BookEntry {
Expand All @@ -21,12 +23,16 @@ class BookEntry {
Object.freeze(this);
}

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

stakeInfo() {
return this.bookkeeper.stakeInfo(this.profile.account);
get info() {
const entry = this;
return {
balance(type) {
return entry.bookkeeper.balance(entry.profile[type]);
},
stake() {
return entry.bookkeeper.stakeInfo(entry.profile.account);
},
};
}

transfer(amount) {
Expand All @@ -40,6 +46,14 @@ class BookEntry {
shield(amount) {
return new ShieldTransfer(this).amount(amount);
}

stake(amount) {
return new StakeTransfer(this).amount(amount);
}

unstake() {
return new UnstakeTransfer(this);
}
}

export class Bookkeeper {
Expand All @@ -63,6 +77,10 @@ export class Bookkeeper {
}
}

get minimumStake() {
return ProtocolDriver.getMinimumStake();
}

stakeInfo(identifier) {
const type = ProfileGenerator.typeOf(String(identifier));
if (type !== "account") {
Expand Down
170 changes: 160 additions & 10 deletions w3sper.js/src/protocol-driver/mod.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ const rng = () => new Uint8Array(32); //crypto.getRandomValues(new Uint8Array(32

const uninit = Object.freeze([
none`No Protocol Driver loaded yet. Call "load" first.`,
none`No size set yet. Load the Protocol Driver first.`,
none`No globals set yet. Load the Protocol Driver first.`,
]);

let [protocolDriverModule, driverEntrySize] = uninit;
let [protocolDriverModule, driverGlobals] = uninit;

export const getEntrySize = () => driverEntrySize;
export const getEntrySize = () => driverGlobals.then(([size]) => size);
export const getMinimumStake = () => driverGlobals.then(([, min]) => min);

export function load(source, importsURL) {
// If the module is already loaded, no need to load it again.
Expand All @@ -36,25 +37,26 @@ export function load(source, importsURL) {

// Parse known globals once.

driverEntrySize = protocolDriverModule.task(
driverGlobals = protocolDriverModule.task(
withAllocator(async function (_exports, allocator) {
const { ptr, u32 } = allocator.types;
const { ptr, u32, u64 } = allocator.types;
const { globals } = allocator;

const key = await u32(ptr(globals.KEY_SIZE));
const item = await u32(ptr(globals.ITEM_SIZE));
const minimumStake = await u64(ptr(globals.MINIMUM_STAKE));

return { key, item };
return [{ key, item }, minimumStake];
}),
)();
}

export function unload() {
if (protocolDriverModule instanceof none || driverEntrySize instanceof none) {
if (protocolDriverModule instanceof none || driverGlobals instanceof none) {
return Promise.resolve();
} else {
return Promise.all([protocolDriverModule, driverEntrySize]).then(() => {
[protocolDriverModule, driverEntrySize] = uninit;
return Promise.all([protocolDriverModule, driverGlobals]).then(() => {
[protocolDriverModule, driverGlobals] = uninit;
});
}
}
Expand Down Expand Up @@ -235,7 +237,7 @@ export const mapOwned = (owners, notes) =>
throw new Error("All owners must be generated from the same source");
}

let { key: keySize, item: itemSize } = await driverEntrySize;
let { key: keySize, item: itemSize } = await getEntrySize();
let entrySize = keySize + itemSize;

let notesBuffer = new Uint8Array(
Expand Down Expand Up @@ -792,6 +794,154 @@ export const shield = async (info) =>
return [tx_buffer, hash];
})();

export const stake = async (info) =>
protocolDriverModule.task(async function (
{ malloc, moonlight_stake },
{ memcpy },
) {
const ptr = Object.create(null);

const seed = new Uint8Array(await info.profile.seed);

ptr.seed = await malloc(64);
await memcpy(ptr.seed, seed, 64);

const profile_index = +info.profile;

const stake_value = new Uint8Array(8);
new DataView(stake_value.buffer).setBigUint64(0, info.stake_value, true);
ptr.stake_value = await malloc(8);
await memcpy(ptr.stake_value, stake_value);

const gas_limit = new Uint8Array(8);
new DataView(gas_limit.buffer).setBigUint64(0, info.gas_limit, true);
ptr.gas_limit = await malloc(8);
await memcpy(ptr.gas_limit, gas_limit);

const gas_price = new Uint8Array(8);
new DataView(gas_price.buffer).setBigUint64(0, info.gas_price, true);
ptr.gas_price = await malloc(8);
await memcpy(ptr.gas_price, gas_price);

const nonce = new Uint8Array(8);
new DataView(nonce.buffer).setBigUint64(0, info.nonce, true);
ptr.nonce = await malloc(8);
await memcpy(ptr.nonce, nonce);

const stake_nonce = new Uint8Array(8);
new DataView(stake_nonce.buffer).setBigUint64(0, info.stake_nonce, true);
ptr.stake_nonce = await malloc(8);
await memcpy(ptr.stake_nonce, stake_nonce);

let tx = await malloc(4);
let hash = await malloc(64);

const code = await moonlight_stake(
ptr.seed,
profile_index,
ptr.stake_value,
ptr.gas_limit,
ptr.gas_price,
ptr.nonce,
info.chainId,
ptr.stake_nonce,
tx,
hash,
);

if (code > 0) throw DriverError.from(code);

let tx_ptr = new DataView((await memcpy(null, tx, 4)).buffer).getUint32(
0,
true,
);

let tx_len = new DataView((await memcpy(null, tx_ptr, 4)).buffer).getUint32(
0,
true,
);

const tx_buffer = await memcpy(null, tx_ptr + 4, tx_len);

hash = new TextDecoder().decode(await memcpy(null, hash, 64));
return [tx_buffer, hash];
})();

export const unstake = async (info) =>
protocolDriverModule.task(async function (
{ malloc, moonlight_unstake },
{ memcpy },
) {
const ptr = Object.create(null);

ptr.rng = await malloc(32);
await memcpy(ptr.rng, new Uint8Array(rng()));

const seed = new Uint8Array(await info.profile.seed);

ptr.seed = await malloc(64);
await memcpy(ptr.seed, seed, 64);

const profile_index = +info.profile;

const unstake_value = new Uint8Array(8);
new DataView(unstake_value.buffer).setBigUint64(
0,
info.unstake_value,
true,
);
ptr.unstake_value = await malloc(8);
await memcpy(ptr.unstake_value, unstake_value);

const gas_limit = new Uint8Array(8);
new DataView(gas_limit.buffer).setBigUint64(0, info.gas_limit, true);
ptr.gas_limit = await malloc(8);
await memcpy(ptr.gas_limit, gas_limit);

const gas_price = new Uint8Array(8);
new DataView(gas_price.buffer).setBigUint64(0, info.gas_price, true);
ptr.gas_price = await malloc(8);
await memcpy(ptr.gas_price, gas_price);

const nonce = new Uint8Array(8);
new DataView(nonce.buffer).setBigUint64(0, info.nonce, true);
ptr.nonce = await malloc(8);
await memcpy(ptr.nonce, nonce);

let tx = await malloc(4);
let hash = await malloc(64);

const code = await moonlight_unstake(
ptr.rng,
ptr.seed,
profile_index,
ptr.unstake_value,
ptr.gas_limit,
ptr.gas_price,
ptr.nonce,
info.chainId,
tx,
hash,
);

if (code > 0) throw DriverError.from(code);

let tx_ptr = new DataView((await memcpy(null, tx, 4)).buffer).getUint32(
0,
true,
);

let tx_len = new DataView((await memcpy(null, tx_ptr, 4)).buffer).getUint32(
0,
true,
);

const tx_buffer = await memcpy(null, tx_ptr + 4, tx_len);

hash = new TextDecoder().decode(await memcpy(null, hash, 64));
return [tx_buffer, hash];
})();

function serializeMemo(memo) {
if (!memo) {
return null;
Expand Down
88 changes: 85 additions & 3 deletions w3sper.js/src/transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@ class AccountTransfer extends Transfer {
let nonce;
if ("nonce" in attributes) {
({ nonce } = attributes);
} else if (typeof this.bookentry?.balance === "function") {
({ nonce } = await this.bookentry.balance("account"));
} else if (typeof this.bookentry?.info.balance === "function") {
({ nonce } = await this.bookentry.info.balance("account"));
}

nonce += 1n;
Expand Down Expand Up @@ -285,7 +285,7 @@ export class ShieldTransfer extends BasicTransfer {
const { chainId } = await network.node.info;

// Obtain the nonce
let { nonce } = await this.bookentry.balance("account");
let { nonce } = await this.bookentry.info.balance("account");

nonce += 1n;

Expand All @@ -305,3 +305,85 @@ export class ShieldTransfer extends BasicTransfer {
});
}
}

export class StakeTransfer extends BasicTransfer {
constructor(from) {
super(from);
}

async build(network) {
const { attributes } = this;
const { amount: stake_value, gas } = attributes;
const { profile, bookkeeper } = this.bookentry;

const minimumStake = await bookkeeper.minimumStake;

if (stake_value < minimumStake) {
throw new Error(`Stake value must be greater than ${minimumStake}`);
}

// Get the chain id from the network
const { chainId } = await network.node.info;

// Obtain the nonces
let { nonce } = await this.bookentry.info.balance("account");
let { nonce: stake_nonce } = await this.bookentry.info.stake();

nonce += 1n;
stake_nonce += 1n;

let [buffer, hash] = await ProtocolDriver.stake({
profile,
stake_value,
stake_nonce,
gas_limit: gas.limit,
gas_price: gas.price,
nonce,
chainId,
});

return Object.freeze({
buffer,
hash,
nonce,
});
}
}

export class UnstakeTransfer extends BasicTransfer {
constructor(from) {
super(from);
}

async build(network) {
const { attributes } = this;
const { gas } = attributes;
const { profile } = this.bookentry;

// Get the chain id from the network
const { chainId } = await network.node.info;

// Obtain the nonces
let { nonce } = await this.bookentry.info.balance("account");

// Obtain the staked amount
let { amount } = await this.bookentry.info.stake();

nonce += 1n;

let [buffer, hash] = await ProtocolDriver.unstake({
profile,
unstake_value: amount.total,
gas_limit: gas.limit,
gas_price: gas.price,
nonce,
chainId,
});

return Object.freeze({
buffer,
hash,
nonce,
});
}
}
Loading

0 comments on commit 7c01de0

Please sign in to comment.