Skip to content

Commit

Permalink
add furnace backend
Browse files Browse the repository at this point in the history
  • Loading branch information
seniorjoinu committed Oct 4, 2024
1 parent e9c55a9 commit dc1e13e
Show file tree
Hide file tree
Showing 9 changed files with 772 additions and 1 deletion.
26 changes: 26 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,7 @@ num-bigint = "0.4"
chrono = { version = "0.4", default-features = false }
futures = { version = "0.3", default-features = false }
lazy_static = "1.4"
garde = { version = "0.18", features = ["derive"] }
sha2 = "0.10"
garde = { version = "0.18", features = ["derive", "url", "unicode"] }
html-escape = "0.2"
url = "2.5"
4 changes: 4 additions & 0 deletions backend/src/shared/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,9 @@ ic-ledger-types = { workspace = true }
ic-canister-sig-creation = { workspace = true }
ic-verifiable-credentials = { workspace = true }

garde = { workspace = true }
html-escape = { workspace = true }
url = { workspace = true }

[build-dependencies]
dotenv = "0.15"
114 changes: 114 additions & 0 deletions backend/src/shared/src/furnace/api.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
use candid::{CandidType, Nat, Principal};
use garde::Validate;
use ic_e8s::c::E8s;
use serde::Deserialize;

use crate::{burner::types::TimestampNs, Guard};

use super::{
state::FurnaceState,
types::{PositionId, MIN_ALLOWED_USD_BURN_QTY_E8S},
};

#[derive(CandidType, Deserialize, Validate)]
pub struct CreatePositionRequest {
#[garde(skip)]
pub token_can_id: Principal,
#[garde(skip)]
pub qty: Nat,
#[garde(skip)]
pub pid: Principal,
#[garde(length(graphemes, min = 5, max = 128))]
pub title: Option<String>,
#[garde(url)]
pub link: Option<String>,
}

impl Guard<FurnaceState> for CreatePositionRequest {
fn validate_and_escape(
&mut self,
state: &FurnaceState,
_caller: Principal,
_now: TimestampNs,
) -> Result<(), String> {
self.validate(&()).map_err(|e| e.to_string())?;

if let Some(title) = &self.title {
let trimmed_title = title.trim().to_string();

if trimmed_title.len() < 5 {
return Err(String::from("Title too short"));
}

self.title = Some(trimmed_title);
}

let info = state.get_info_ref();
let usd_value = info
.get_whitelisted_token_usd_value(&self.token_can_id, self.qty.clone())
.ok_or(String::from("The token is not whitelisted"))?;

let min_usd_value = E8s::from(MIN_ALLOWED_USD_BURN_QTY_E8S);
if usd_value < min_usd_value {
return Err(String::from("Too few burned tokens"));
}

Ok(())
}
}

#[derive(CandidType, Deserialize)]
pub struct CreatePositionResponse {
pub position_id: PositionId,
}

#[derive(CandidType, Deserialize, Validate)]
pub struct AffectPositionRequest {
#[garde(skip)]
pub position_id: PositionId,
#[garde(skip)]
pub token_can_id: Principal,
#[garde(skip)]
pub qty: Nat,
#[garde(skip)]
pub downvote: bool,
}

impl Guard<FurnaceState> for AffectPositionRequest {
fn validate_and_escape(
&mut self,
state: &FurnaceState,
_caller: Principal,
_now: TimestampNs,
) -> Result<(), String> {
self.validate(&()).map_err(|e| e.to_string())?;

let info = state.get_info_ref();
let usd_value = info
.get_whitelisted_token_usd_value(&self.token_can_id, self.qty.clone())
.ok_or(String::from("The token is not whitelisted"))?;

let min_usd_value = E8s::from(MIN_ALLOWED_USD_BURN_QTY_E8S);
if usd_value < min_usd_value {
return Err(String::from("Too few burned tokens"));
}

let position = state
.positions
.get(&self.position_id)
.ok_or(String::from("Position not found"))?;

if position.title.is_none() && position.link.is_none() && self.downvote {
return Err(String::from(
"Unable to downvote a position without attached data",
));
}

Ok(())
}
}

#[derive(CandidType, Deserialize)]
pub struct AffectPositionResponse {
pub new_position_value_usd: E8s,
}
3 changes: 3 additions & 0 deletions backend/src/shared/src/furnace/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod api;
pub mod state;
pub mod types;
193 changes: 193 additions & 0 deletions backend/src/shared/src/furnace/state.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
use std::collections::BTreeMap;

use candid::Principal;
use ic_e8s::c::E8s;
use ic_stable_structures::{Cell, StableBTreeMap};

use crate::burner::types::{Memory, TimestampNs};

use super::{
api::{
AffectPositionRequest, AffectPositionResponse, CreatePositionRequest,
CreatePositionResponse,
},
types::{FurnaceInfo, FurnacePosition, FurnaceWinner, FurnaceWinnerHistoryEntry, PositionId},
};

pub struct FurnaceState {
pub cur_round_entries: StableBTreeMap<PositionId, E8s, Memory>,
pub winners: StableBTreeMap<TimestampNs, FurnaceWinnerHistoryEntry, Memory>,
pub positions: StableBTreeMap<PositionId, FurnacePosition, Memory>,
pub info: Cell<FurnaceInfo, Memory>,
}

impl FurnaceState {
/// Precondition: tokens already burned, request validated
pub fn create_position(
&mut self,
req: CreatePositionRequest,
caller: Principal,
) -> CreatePositionResponse {
let mut info = self.get_info();
let id = info.generate_position_id();

let position = FurnacePosition {
id,
owner_pid: caller,
participant_pid: req.pid,
title: req.title,
link: req.link,
};

self.positions.insert(id, position);

let usd_value = info.note_burned_tokens(&req.token_can_id, req.qty);

self.cur_round_entries.insert(id, usd_value);

self.set_info(info);

CreatePositionResponse { position_id: id }
}

// TODO: update position

/// Precondition: tokens already burned, request validated
pub fn affect_position(&mut self, req: AffectPositionRequest) -> AffectPositionResponse {
let mut info = self.get_info();

let usd_value = info.note_burned_tokens(&req.token_can_id, req.qty);

let prev_usd_value = self
.cur_round_entries
.get(&req.position_id)
.unwrap_or_default();

let new_usd_value = if req.downvote {
if prev_usd_value < usd_value {
E8s::zero()
} else {
prev_usd_value - usd_value
}
} else {
prev_usd_value + usd_value
};

self.cur_round_entries
.insert(req.position_id, new_usd_value.clone());

self.set_info(info);

AffectPositionResponse {
new_position_value_usd: new_usd_value,
}
}

// TODO: don't forget to stop/restart
pub fn raffle_round(
&mut self,
cur_prize_fund_icp: E8s,
now: TimestampNs,
) -> Option<FurnaceWinnerHistoryEntry> {
let mut info = self.get_info();

let prize_distribution = info.calculate_prize_distribution(cur_prize_fund_icp.clone());
let random_numbers = info.generate_random_numbers(prize_distribution.len());

let winners_opt = self.find_winners(info.usd_burnt_cur_round.clone(), random_numbers);
if winners_opt.is_none() {
return None;
}

let mut winners = winners_opt.unwrap();
winners.sort_by(|(_, votes_a), (_, votes_b)| votes_a.cmp(votes_b));

let mut result = Vec::new();
for (position_id, prize_icp) in winners {
let position = self
.positions
.get(&position_id)
.expect("Position not found");

let entry = FurnaceWinner {
prize_icp,
position,
};

result.push(entry);
}

let winner_history_entry = FurnaceWinnerHistoryEntry {
timestamp: now,
round: info.current_round,
jackpot: cur_prize_fund_icp,
winners: result,
};

self.winners.insert(now, winner_history_entry.clone());
self.cur_round_entries.clear_new();
self.positions.clear_new();

Some(winner_history_entry)
}

fn find_winners(
&mut self,
total_burned_usd: E8s,
mut random_numbers: Vec<E8s>,
) -> Option<Vec<(PositionId, E8s)>> {
if self.cur_round_entries.is_empty() {
return None;
}

let mut iter = self.cur_round_entries.iter();
let mut from = E8s::zero();
let mut to = E8s::zero();
let mut result = Vec::new();

loop {
let entry_opt = iter.next();
if entry_opt.is_none() {
break;
}

let (position_id, votes) = entry_opt.unwrap();
to += &votes / &total_burned_usd;

let mut found = false;
for i in 0..random_numbers.len() {
{
let rng = random_numbers.get(i).unwrap();

if rng >= &from && rng <= &to {
result.push((position_id, votes.clone()));
found = true;
}
}

if found {
random_numbers.remove(i);
break;
}
}

from = to.clone();
}

debug_assert!(random_numbers.is_empty());

Some(result)
}

pub fn get_info(&self) -> FurnaceInfo {
self.info.get().clone()
}

pub fn get_info_ref(&self) -> &FurnaceInfo {
self.info.get()
}

fn set_info(&mut self, info: FurnaceInfo) {
self.info.set(info).expect("Unable to store info");
}
}
Loading

0 comments on commit dc1e13e

Please sign in to comment.