Skip to content

Commit

Permalink
init bpay repo
Browse files Browse the repository at this point in the history
  • Loading branch information
DenisCarriere committed Apr 24, 2024
1 parent b9ac751 commit 308eb7f
Show file tree
Hide file tree
Showing 16 changed files with 1,657 additions and 2 deletions.
19 changes: 19 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Bun Test

on: push

jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install CDT
run: |
curl https://gateway.pinata.cloud/ipfs/QmfFhXmbaZMgwVs51MHcUemG1A2XCNSTByRcAEG7S6iXaD -o cdt_4.0.1-1_amd64.deb
sudo apt install ./cdt_4.0.1-1_amd64.deb
- run: bun install
- run: bun run build
- run: bun run test
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Prerequisites
*.d
node_modules
.DS_Store
*.wasm
*.abi

# Compiled Object files
*.slo
Expand Down
28 changes: 26 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,26 @@
# eosio.bpay
eosio.bpay
# EOS Block Pay (`eosio.bpay`) [![Bun Test](https://github.com/eosnetworkfoundation/eosio.bpay/actions/workflows/test.yml/badge.svg)](https://github.com/eosnetworkfoundation/eosio.bpay/actions/workflows/test.yml)

## Overview

The `eosio.bpay` contract handles system block pay distribution.

## Development and Testing

### Build Instructions

To compile the contract, developers can use the following command:

```sh
$ cdt-cpp eosio.bpay.cpp -I ./include
```

### Testing Framework

The contract includes a comprehensive testing suite designed to validate its functionality. The tests are executed using the following commands:

```sh
$ npm test

> test
> bun test
```
3 changes: 3 additions & 0 deletions eosio.bpay.clauses.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<h1 class="clause">UserAgreement</h1>

The `eosio.bpay` contract handles block pay distribution.
10 changes: 10 additions & 0 deletions eosio.bpay.contracts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<h1 class="contract">claimrewards</h1>

---
spec_version: "0.2.0"
title: claimrewards
summary: 'Claim Rewards'
icon: https://gateway.pinata.cloud/ipfs/QmZ4HSZDuSrZ4BHawtZRhVfwyYJ4DepNJqVDzxY59KveiM#3830f1ce8cb07f7757dbcf383b1ec1b11914ac34a1f9d8b065f07600fa9dac19
---

Claim rewards {{ owner }} has earned as a block producer.
75 changes: 75 additions & 0 deletions eosio.bpay.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#include "eosio.bpay.hpp"

namespace eosio {

[[eosio::action]]
void bpay::claimrewards( const name owner ) {
require_auth( owner );

rewards_table _rewards( get_self(), get_self().value );

const auto& row = _rewards.get( owner.value, "not eligible to claim producer block pay" );
check( row.quantity.amount > 0, "no rewards to claim");

// transfer rewards to owner
eosio::token::transfer_action transfer( "eosio.token"_n, { get_self(), "active"_n });
transfer.send( get_self(), owner, row.quantity, "producer block pay" );

_rewards.modify(row, get_self(), [&](auto& row) {
row.quantity.amount = 0;
});
}

[[eosio::on_notify("*::transfer")]]
void bpay::on_transfer( const name from, const name to, const asset quantity, const string memo )
{
if (from == get_self() || to != get_self()) {
return;
}

check( get_first_receiver() == "eosio.token"_n, "only eosio.token allowed") ;
check( quantity.symbol == eosio::symbol("EOS", 4), "only EOS token allowed" );

rewards_table _rewards( get_self(), get_self().value );
eosiosystem::system_contract::producers_table _producers( "eosio"_n, "eosio"_n.value );

// get voter secondary index
auto idx = _producers.get_index<"prototalvote"_n>();

// get producer with the most votes
// reverse iterator to get the last element
auto prod = idx.rbegin();

// get top 21 producers by vote
std::vector<name> top_producers;
for (int i = 0; i < 21; i++) {
if ( prod->is_active == false ) {
prod++;
i--;
continue;
}
top_producers.push_back(prod->owner);
print(i, " ", prod->owner, " ", prod->total_votes, "\n");
prod++;
}

// calculate rewards equal share for top 21 producers
asset reward = quantity / 21;

// update rewards table
for (auto producer : top_producers) {
auto row = _rewards.find( producer.value );
if (row == _rewards.end()) {
_rewards.emplace( get_self(), [&](auto& row) {
row.owner = producer;
row.quantity = reward;
});
} else {
_rewards.modify(row, get_self(), [&](auto& row) {
row.quantity += reward;
});
}
}
}

} /// namespace eosio
55 changes: 55 additions & 0 deletions eosio.bpay.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#pragma once

#include <eosio/eosio.hpp>
#include <eosio.system/eosio.system.hpp>
#include <eosio.token/eosio.token.hpp>

using namespace std;

namespace eosio {
/**
* The `eosio.bpay` contract handles system bpay distribution.
*/
class [[eosio::contract("eosio.bpay")]] bpay : public contract {
public:
using contract::contract;

/**
* ## TABLE `rewards`
*
* @param owner - block producer owner account
* @param quantity - reward quantity in EOS
*
* ### example
*
* ```json
* [
* {
* "owner": "alice",
* "quantity": "8.800 EOS"
* }
* ]
* ```
*/
struct [[eosio::table("rewards")]] rewards_row {
name owner;
asset quantity;

uint64_t primary_key() const { return owner.value; }
};
typedef eosio::multi_index< "rewards"_n, rewards_row > rewards_table;

/**
* Claim rewards for a block producer.
*
* @param owner - block producer owner account
*/
[[eosio::action]]
void claimrewards( const name owner);

[[eosio::on_notify("*::transfer")]]
void on_transfer( const name from, const name to, const asset quantity, const string memo );

private:
};
} /// namespace eosio
23 changes: 23 additions & 0 deletions eosio.bpay.permission.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
owner:
threshold: 1
accounts:
- permission:
actor: eosio
permission: active
weight: 1

active:
threshold: 2
accounts:
- permission:
actor: enf
permission: active
weight: 1
- permission:
actor: eosio.prods
permission: prod.minor
weight: 1
- permission:
actor: eosio.bpay
permission: eosio.code
weight: 2
140 changes: 140 additions & 0 deletions eosio.bpay.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { Asset, Name } from '@wharfkit/antelope'
import { describe, expect, test } from 'bun:test'
import { Blockchain, expectToThrow } from '@eosnetwork/vert'

// Vert EOS VM
const blockchain = new Blockchain()

// 21 producers
const alice = "bp.alice"
const bob = "bp.bob"
const charly = "bp.charly"
const producers = [
alice,
bob,
charly,
"bp.d",
"bp.e",
"bp.f",
"bp.g",
"bp.h",
"bp.i",
"bp.j",
"bp.k",
"bp.l",
"bp.m",
"bp.n",
"bp.o",
"bp.p",
"bp.q",
"bp.r",
"bp.s",
"bp.t",
"bp.u",
]
const standby = "bp.standby"
const inactive = "bp.inactive"
blockchain.createAccounts(...producers, standby, inactive)

const bpay_contract = 'eosio.bpay'
const contracts = {
bpay: blockchain.createContract(bpay_contract, bpay_contract, true),
token: blockchain.createContract('eosio.token', 'external/eosio.token/eosio.token', true),
system: blockchain.createContract('eosio', 'external/eosio.system/eosio', true),
fake: {
token: blockchain.createContract('fake.token', 'external/eosio.token/eosio.token', true),
},
}

function getProducers(owner: string) {
const scope = Name.from("eosio").value.value
const primary_key = Name.from(owner).value.value
return contracts.system.tables
.producers(scope)
.getTableRow(primary_key)
}

function getRewards(owner: string) {
const scope = Name.from(bpay_contract).value.value
const primary_key = Name.from(owner).value.value
const row = contracts.bpay.tables
.rewards(scope)
.getTableRow(primary_key)
if (!row) return 0;
return Asset.from(row.quantity).units.toNumber()
}

function getTokenBalance(account: string, symcode: string) {
const scope = Name.from(account).value.value
const primary_key = Asset.SymbolCode.from(symcode).value.value
const row = contracts.token.tables
.accounts(scope)
.getTableRow(primary_key)
if (!row) return 0;
return Asset.from(row.balance).units.toNumber()
}

describe(bpay_contract, () => {
test('eosio::setproducers', async () => {
let votes = 100.0;
for ( const producer of producers ) {
await contracts.system.actions.setproducer([producer, votes, true]).send()

Check failure on line 81 in eosio.bpay.spec.ts

View workflow job for this annotation

GitHub Actions / build-and-test

TypeError: contracts.system.actions.setproducer is not a function. (In 'contracts.system.actions.setproducer([producer

at /home/runner/work/eosio.bpay/eosio.bpay/eosio.bpay.spec.ts:81:19 at /home/runner/work/eosio.bpay/eosio.bpay/eosio.bpay.spec.ts:78:33
votes -= 1.0;
}
await contracts.system.actions.setproducer([standby, 1, true]).send() // lowest vote
await contracts.system.actions.setproducer([inactive, 200.0, false]).send() // highest vote, but inactive
})

test('eosio.token::issue::EOS', async () => {
const supply = `1000000000.0000 EOS`
await contracts.token.actions.create(['eosio.token', supply]).send()

Check failure on line 90 in eosio.bpay.spec.ts

View workflow job for this annotation

GitHub Actions / build-and-test

TypeError: contracts.token.actions.create is not a function. (In 'contracts.token.actions.create(["eosio.token"

at /home/runner/work/eosio.bpay/eosio.bpay/eosio.bpay.spec.ts:90:15 at /home/runner/work/eosio.bpay/eosio.bpay/eosio.bpay.spec.ts:88:37
await contracts.token.actions.issue(['eosio.token', supply, '']).send()
})

test("eosio.bpay::transfer", async () => {
await contracts.token.actions.transfer(['eosio.token', bpay_contract, '2100.0000 EOS', '']).send();

Check failure on line 95 in eosio.bpay.spec.ts

View workflow job for this annotation

GitHub Actions / build-and-test

TypeError: contracts.token.actions.transfer is not a function. (In 'contracts.token.actions.transfer(["eosio.token"

at /home/runner/work/eosio.bpay/eosio.bpay/eosio.bpay.spec.ts:95:15 at /home/runner/work/eosio.bpay/eosio.bpay/eosio.bpay.spec.ts:94:34
const before = {
bpay: {
balance: getTokenBalance(bpay_contract, 'EOS'),
},
alice: {
balance: getTokenBalance(alice, 'EOS'),
},
bob: {
balance: getTokenBalance(bob, 'EOS'),
},

}
await contracts.bpay.actions.claimrewards([alice]).send(alice);

const after = {
bpay: {
balance: getTokenBalance(bpay_contract, 'EOS'),
},
alice: {
balance: getTokenBalance(alice, 'EOS'),
},
bob: {
balance: getTokenBalance(bob, 'EOS'),
},
}

// EOS
expect(after.bpay.balance - before.bpay.balance).toBe(-1000000)
expect(after.alice.balance - before.alice.balance).toBe(1000000)
expect(after.bob.balance - before.bob.balance).toBe(0)
});

test('eosio.bpay::claimrewards::error - no rewards to claim', async () => {
const action = contracts.bpay.actions.claimrewards([alice]).send(alice);
await expectToThrow(action, 'eosio_assert: no rewards to claim');
})

test.skip('eosio.bpay::claimrewards::error - not eligible to claim block rewards', async () => {
for ( const owner of [inactive, standby] ) {
const action = contracts.bpay.actions.claimrewards([owner]).send(owner);
await expectToThrow(action, 'eosio_assert: not eligible to claim producer block pay');
}
})

})
Loading

0 comments on commit 308eb7f

Please sign in to comment.