Skip to content

Commit

Permalink
w3sper: Add withdraw stake's reward method
Browse files Browse the repository at this point in the history
- Add `WithdrawStakeRewardTransfer` class in `transaction`
- Add `BookEntry#withdraw` method
- Add `withdraw` function in `protocol-driver`
- Add withdraws tests

Resolves #2952
  • Loading branch information
ZER0 committed Nov 13, 2024
1 parent 7c01de0 commit 641ab0c
Show file tree
Hide file tree
Showing 5 changed files with 292 additions and 1 deletion.
5 changes: 5 additions & 0 deletions w3sper.js/src/bookkeeper.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
ShieldTransfer,
StakeTransfer,
UnstakeTransfer,
WithdrawStakeRewardTransfer,
} from "../src/transaction.js";

class BookEntry {
Expand Down Expand Up @@ -54,6 +55,10 @@ class BookEntry {
unstake() {
return new UnstakeTransfer(this);
}

withdraw(amount) {
return new WithdrawStakeRewardTransfer(this).amount(amount);
}
}

export class Bookkeeper {
Expand Down
75 changes: 75 additions & 0 deletions w3sper.js/src/protocol-driver/mod.js
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,81 @@ export const unstake = async (info) =>
return [tx_buffer, hash];
})();

export const withdraw = async (info) =>
protocolDriverModule.task(async function (
{ malloc, moonlight_stake_reward },
{ 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 reward_amount = new Uint8Array(8);
new DataView(reward_amount.buffer).setBigUint64(
0,
info.reward_amount,
true,
);
ptr.reward_amount = await malloc(8);
await memcpy(ptr.reward_amount, reward_amount);

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_stake_reward(
ptr.rng,
ptr.seed,
profile_index,
ptr.reward_amount,
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
50 changes: 50 additions & 0 deletions w3sper.js/src/transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -387,3 +387,53 @@ export class UnstakeTransfer extends BasicTransfer {
});
}
}

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

async build(network) {
const { attributes } = this;
const { amount: reward_amount, 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 { reward } = await this.bookentry.info.stake();

if (!reward) {
throw new Error(`No stake available to withdraw the reward from`);
} else if (reward_amount > reward) {
throw new Error(
`The withdrawed reward amount must be less or equal to ${reward}`,
);
} else if (!reward_amount) {
throw new Error(
`Can't wiotdraw an empty reward amount. I mean, you could, but it would be pointless.`,
);
}

nonce += 1n;

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

return Object.freeze({
buffer,
hash,
nonce,
});
}
}
1 change: 0 additions & 1 deletion w3sper.js/tests/assets/genesis.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
[[stake]]
address = 'oCqYsUMRqpRn2kSabH52Gt6FQCwH5JXj5MtRdYVtjMSJ73AFvdbPf98p3gz98fQwNy9ZBiDem6m9BivzURKFSKLYWP3N9JahSPZs9PnZ996P18rTGAjQTNFsxtbrKx79yWu'
amount = 1_000_000_000_000
reward = 1_000_000_000_000

[[moonlight_account]]
address = 'oCqYsUMRqpRn2kSabH52Gt6FQCwH5JXj5MtRdYVtjMSJ73AFvdbPf98p3gz98fQwNy9ZBiDem6m9BivzURKFSKLYWP3N9JahSPZs9PnZ996P18rTGAjQTNFsxtbrKx79yWu'
Expand Down
162 changes: 162 additions & 0 deletions w3sper.js/tests/transfer_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -436,3 +436,165 @@ test("unstake", async () => {

await network.disconnect();
});

test("withdraw stake reward with no stake", async () => {
const network = await Network.connect("http://localhost:8080/");
const profiles = new ProfileGenerator(seeder);

const users = [
await profiles.default,
await profiles.next(),
await profiles.next(),
];

const accounts = new AccountSyncer(network);

const treasury = new Treasury(users);
const bookkeeper = new Bookkeeper(treasury);
const bookentry = bookkeeper.as(users[2]);

await treasury.update({ accounts });

let stakeInfo = await bookentry.info.stake();

assert.equal(stakeInfo.amount, null);

let transfer = bookentry.withdraw(1000n).gas({ limit: 500_000_000n });

assert.reject(async () => await network.execute(transfer));

await network.disconnect();
});

test("withdraw stake reward greater than available", async () => {
const network = await Network.connect("http://localhost:8080/");
const profiles = new ProfileGenerator(seeder);

const users = [
await profiles.default,
await profiles.next(),
await profiles.next(),
];

const accounts = new AccountSyncer(network);

const treasury = new Treasury(users);
const bookkeeper = new Bookkeeper(treasury);
const bookentry = bookkeeper.as(users[0]);

await treasury.update({ accounts });

const stakeInfo = await bookentry.info.stake();

const transfer = bookentry
.withdraw(stakeInfo.reward + 1n)
.gas({ limit: 500_000_000n });

assert.reject(async () => await network.execute(transfer));

await network.disconnect();
});

test("withdraw partial stake reward", async () => {
const network = await Network.connect("http://localhost:8080/");
const profiles = new ProfileGenerator(seeder);

const users = [await profiles.default, await profiles.next()];

const accounts = new AccountSyncer(network);

const treasury = new Treasury(users);
const bookkeeper = new Bookkeeper(treasury);
const bookentry = bookkeeper.as(users[0]);

await treasury.update({ accounts });

let stakeInfo = await bookentry.info.stake();
const accountBalance = await bookentry.info.balance("account");

const claimAmount = stakeInfo.reward / 2n;

const transfer = bookentry.withdraw(claimAmount).gas({ limit: 500_000_000n });

const { hash } = await network.execute(transfer);

const evt = await network.transactions.withId(hash).once.executed();
const { gasPaid } = evt;

await treasury.update({ accounts });

stakeInfo = await bookentry.info.stake();
const newAccountBalance = await bookentry.info.balance("account");

assert.equal(
newAccountBalance.value,
accountBalance.value + claimAmount - gasPaid,
);

await network.disconnect();
});

test("withdraw full stake reward", async () => {
const network = await Network.connect("http://localhost:8080/");
const profiles = new ProfileGenerator(seeder);

const users = [await profiles.default, await profiles.next()];

const accounts = new AccountSyncer(network);

const treasury = new Treasury(users);
const bookkeeper = new Bookkeeper(treasury);
const bookentry = bookkeeper.as(users[0]);

await treasury.update({ accounts });

let stakeInfo = await bookentry.info.stake();
const accountBalance = await bookentry.info.balance("account");

const claimAmount = stakeInfo.reward;

const transfer = bookentry.withdraw(claimAmount).gas({ limit: 500_000_000n });

const { hash } = await network.execute(transfer);

const evt = await network.transactions.withId(hash).once.executed();
const { gasPaid } = evt;

await treasury.update({ accounts });

stakeInfo = await bookentry.info.stake();
const newAccountBalance = await bookentry.info.balance("account");

assert.equal(
newAccountBalance.value,
accountBalance.value + claimAmount - gasPaid,
);

await network.disconnect();
});

test("withdraw 0 as stake reward", async () => {
const network = await Network.connect("http://localhost:8080/");
const profiles = new ProfileGenerator(seeder);

const users = [await profiles.default, await profiles.next()];

const accounts = new AccountSyncer(network);

const treasury = new Treasury(users);
const bookkeeper = new Bookkeeper(treasury);
const bookentry = bookkeeper.as(users[0]);

await treasury.update({ accounts });

let stakeInfo = await bookentry.info.stake();
const accountBalance = await bookentry.info.balance("account");

const claimAmount = 0;

const transfer = bookentry.withdraw(claimAmount).gas({ limit: 500_000_000n });

assert.reject(async () => await network.execute(transfer));

await network.disconnect();
});

0 comments on commit 641ab0c

Please sign in to comment.