Skip to content

Commit

Permalink
Merge pull request #2355 from get10101/feat/referrals
Browse files Browse the repository at this point in the history
feat: Add referral feature
  • Loading branch information
bonomat authored Apr 9, 2024
2 parents 15c6a81 + 8c5727d commit 41afeb3
Show file tree
Hide file tree
Showing 62 changed files with 1,770 additions and 333 deletions.
1 change: 1 addition & 0 deletions coordinator/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ payout_curve = { path = "../crates/payout_curve" }
prometheus = "0.13.3"
rand = "0.8.5"
rust_decimal = { version = "1", features = ["serde-with-float"] }
rust_decimal_macros = "1"
semver = "1.0"
serde = "1.0.147"
serde_json = "1"
Expand Down
2 changes: 2 additions & 0 deletions coordinator/example-settings/prod-coordinator-settings.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ rollover_window_open_scheduler = "0 5 15 * * 5,6"
rollover_window_close_scheduler = "0 5 13 * * 5,6"
close_expired_position_scheduler = "0 0 12 * * *"
close_liquidated_position_scheduler = "0 0 12 * * *"
update_user_bonus_status_scheduler = "0 0 0 * * *"
whitelist_enabled = false
whitelisted_makers = []
min_quantity = 1
margin_call_percentage = 0.1
order_matching_fee_rate = 0.003

[ln_dlc]
off_chain_sync_interval = 5
Expand Down
2 changes: 2 additions & 0 deletions coordinator/example-settings/test-coordinator-settings.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ rollover_window_open_scheduler = "0 5 16 * * *"
rollover_window_close_scheduler = "0 5 22 * * *"
close_expired_position_scheduler = "0 0 12 * * *"
close_liquidated_position_scheduler = "0 0 12 * * *"
update_user_bonus_status_scheduler = "0 0 0 * * *"
whitelist_enabled = false
# Default testnet maker
whitelisted_makers = ["035eccdd1f05c65b433cf38e3b2597e33715e0392cb14d183e812f1319eb7b6794"]
min_quantity = 1
maintenance_margin_rate = 0.1
order_matching_fee_rate = 0.003

[ln_dlc]
off_chain_sync_interval = 5
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- This file should undo anything in `up.sql`
ALTER TABLE trade_params
DROP COLUMN matching_fee;

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- Your SQL goes here
ALTER TABLE trade_params
ADD COLUMN matching_fee BIGINT NOT NULL DEFAULT 0;
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
DROP VIEW IF EXISTS user_referral_summary_view;

ALTER TABLE users
DROP COLUMN referral_code;
ALTER TABLE users
DROP COLUMN used_referral_code;

DROP TABLE IF EXISTS bonus_tiers;
DROP TYPE IF EXISTS "BonusStatus_Type";

Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
ALTER TABLE users
ADD COLUMN referral_code TEXT NOT NULL GENERATED ALWAYS AS (
UPPER(RIGHT(pubkey, 6))
) STORED UNIQUE;
ALTER TABLE users
ADD COLUMN used_referral_code TEXT;

CREATE TYPE "BonusStatus_Type" AS ENUM ('Referral', 'Referent');

CREATE TABLE bonus_tiers (
id SERIAL PRIMARY KEY,
tier_level INTEGER NOT NULL,
min_users_to_refer INTEGER NOT NULL,
fee_rebate REAL NOT NULL,
bonus_tier_type "BonusStatus_Type" NOT NULL,
active BOOLEAN NOT NULL
);

INSERT INTO bonus_tiers (tier_level, min_users_to_refer, fee_rebate, bonus_tier_type, active)
VALUES (0, 0, 0.0, 'Referral', true),
(1, 3, 0.20, 'Referral', true),
(2, 5, 0.35, 'Referral', true),
(3, 10, 0.50, 'Referral', true),
(4, 0, 0.1, 'Referent', true);

CREATE VIEW user_referral_summary_view AS
SELECT u2.pubkey AS referring_user,
u1.used_referral_code as referring_user_referral_code,
u1.pubkey AS referred_user,
u1.referral_code AS referred_user_referral_code,
u1.contact,
u1.nickname,
u1.timestamp,
COALESCE(SUM(t.quantity), 0) AS referred_user_total_quantity
FROM users u1
JOIN users u2 ON u1.used_referral_code = u2.referral_code
LEFT JOIN trades t ON t.trader_pubkey = u1.pubkey
GROUP BY u1.pubkey, u1.referral_code, u2.pubkey, u1.used_referral_code, u1.contact, u1.nickname, u1.timestamp;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
alter table matches drop column matching_fee_sats;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- Your SQL goes here
alter table matches add column matching_fee_sats BIGINT NOT NULL DEFAULT 0;

update matches set matching_fee_sats=((quantity/execution_price) * 100000000)*0.003;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
drop table if exists bonus_status;
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- Your SQL goes here

CREATE TABLE IF NOT EXISTS bonus_status (
id SERIAL PRIMARY KEY,
trader_pubkey TEXT NOT NULL,
tier_level INTEGER NOT NULL,
fee_rebate REAL NOT NULL DEFAULT 0.0,
bonus_type "BonusStatus_Type" NOT NULL,
activation_timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
deactivation_timestamp TIMESTAMP WITH TIME ZONE NOT NULL
);
110 changes: 110 additions & 0 deletions coordinator/src/db/bonus_status.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
use crate::db::bonus_tiers;
use crate::schema::bonus_status;
use crate::schema::sql_types::BonusStatusType;
use bitcoin::secp256k1::PublicKey;
use diesel::AsExpression;
use diesel::ExpressionMethods;
use diesel::FromSqlRow;
use diesel::Insertable;
use diesel::PgConnection;
use diesel::QueryDsl;
use diesel::QueryResult;
use diesel::Queryable;
use diesel::RunQueryDsl;
use time::OffsetDateTime;

/// A user's referral bonus status may be active for this much days at max
const MAX_DAYS_FOR_ACTIVE_REFERRAL_STATUS: i64 = 30;

#[derive(Debug, Clone, Copy, PartialEq, FromSqlRow, AsExpression)]
#[diesel(sql_type = BonusStatusType)]
pub enum BonusType {
/// The bonus is because he referred so many users
Referral,
/// The user has been referred and gets a bonus
Referent,
}

#[allow(dead_code)]
// this is needed because the fields needs to be here to satisfy diesel
#[derive(Queryable, Debug, Clone)]
#[diesel(table_name = bonus_status)]
pub(crate) struct BonusStatus {
pub(crate) id: i32,
pub(crate) trader_pubkey: String,
pub(crate) tier_level: i32,
pub(crate) fee_rebate: f32,
pub(crate) bonus_type: BonusType,
pub(crate) activation_timestamp: OffsetDateTime,
pub(crate) deactivation_timestamp: OffsetDateTime,
}

impl From<BonusType> for commons::BonusStatusType {
fn from(value: BonusType) -> Self {
match value {
BonusType::Referral => commons::BonusStatusType::Referral,
BonusType::Referent => commons::BonusStatusType::Referent,
}
}
}

#[derive(Insertable, Debug, Clone)]
#[diesel(table_name = bonus_status)]
pub(crate) struct NewBonusStatus {
pub(crate) trader_pubkey: String,
pub(crate) tier_level: i32,
pub(crate) fee_rebate: f32,
pub(crate) bonus_type: BonusType,
pub(crate) activation_timestamp: OffsetDateTime,
pub(crate) deactivation_timestamp: OffsetDateTime,
}

/// This function might return multiple status for a single user
///
/// Because he might have moved up into the next level without the old level being expired. The
/// caller is responsible in picking the most suitable status
pub(crate) fn active_status_for_user(
conn: &mut PgConnection,
trader_pubkey: &PublicKey,
) -> QueryResult<Vec<BonusStatus>> {
bonus_status::table
.filter(bonus_status::trader_pubkey.eq(trader_pubkey.to_string()))
.filter(bonus_status::deactivation_timestamp.gt(OffsetDateTime::now_utc()))
.load(conn)
}

pub(crate) fn insert(
conn: &mut PgConnection,
trader_pk: &PublicKey,
tier_level: i32,
bonus_type: BonusType,
) -> QueryResult<BonusStatus> {
let existing_status_for_user = active_status_for_user(conn, trader_pk)?;
let bonus_tier = bonus_tiers::tier_by_tier_level(conn, tier_level)?;

if let Some(status) = existing_status_for_user
.into_iter()
.find(|status| status.tier_level == tier_level)
{
tracing::debug!(
trader_pubkey = trader_pk.to_string(),
tier_level,
"User has already gained bonus status"
);
return Ok(status);
}

let bonus_status = diesel::insert_into(bonus_status::table)
.values(NewBonusStatus {
trader_pubkey: trader_pk.to_string(),
tier_level,
fee_rebate: bonus_tier.fee_rebate,
bonus_type,
activation_timestamp: OffsetDateTime::now_utc(),
deactivation_timestamp: OffsetDateTime::now_utc()
+ time::Duration::days(MAX_DAYS_FOR_ACTIVE_REFERRAL_STATUS),
})
.get_result(conn)?;

Ok(bonus_status)
}
87 changes: 87 additions & 0 deletions coordinator/src/db/bonus_tiers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use crate::db::bonus_status::BonusType;
use crate::schema::bonus_tiers;
use bitcoin::secp256k1::PublicKey;
use diesel::pg::sql_types::Timestamptz;
use diesel::prelude::*;
use diesel::sql_query;
use diesel::sql_types::Float;
use diesel::sql_types::Text;
use diesel::PgConnection;
use diesel::QueryResult;
use diesel::Queryable;
use rust_decimal::Decimal;
use time::OffsetDateTime;

pub struct Referral {
pub volume: Decimal,
}

#[derive(Queryable, Debug, Clone)]
#[diesel(table_name = bonus_tiers)]
// this is needed because some fields are unused but need to be here for diesel
#[allow(dead_code)]
pub(crate) struct BonusTier {
pub(crate) id: i32,
pub(crate) tier_level: i32,
pub(crate) min_users_to_refer: i32,
pub(crate) fee_rebate: f32,
pub(crate) bonus_tier_type: BonusType,
pub(crate) active: bool,
}

/// Returns all active bonus tiers for given types
pub(crate) fn all_active_by_type(
conn: &mut PgConnection,
types: Vec<BonusType>,
) -> QueryResult<Vec<BonusTier>> {
bonus_tiers::table
.filter(bonus_tiers::active.eq(true))
.filter(bonus_tiers::bonus_tier_type.eq_any(types))
.load::<BonusTier>(conn)
}

pub(crate) fn tier_by_tier_level(
conn: &mut PgConnection,
tier_level: i32,
) -> QueryResult<BonusTier> {
bonus_tiers::table
.filter(bonus_tiers::tier_level.eq(tier_level))
.first(conn)
}

#[derive(Debug, QueryableByName, Clone)]
pub struct UserReferralSummaryView {
#[diesel(sql_type = Text)]
pub referring_user: String,
#[diesel(sql_type = Text)]
pub referring_user_referral_code: String,
#[diesel(sql_type = Text)]
pub referred_user: String,
#[diesel(sql_type = Text)]
pub referred_user_referral_code: String,
#[diesel(sql_type = Timestamptz)]
pub timestamp: OffsetDateTime,
#[diesel(sql_type = Float)]
pub referred_user_total_quantity: f32,
}

/// Returns all referred users for by referrer with trading volume > 0
pub(crate) fn all_referrals_by_referring_user(
conn: &mut PgConnection,
trader_pubkey: &PublicKey,
) -> QueryResult<Vec<UserReferralSummaryView>> {
// we have to do this manually because diesel does not support views. If you make a change to
// below, make sure to test this against a life db as errors will only be thrown at runtime
let query = "SELECT referring_user, referring_user_referral_code, \
referred_user, \
referred_user_referral_code, \
timestamp, \
referred_user_total_quantity \
FROM user_referral_summary_view where referring_user = $1 \
and referred_user_total_quantity > 0";
let summaries: Vec<UserReferralSummaryView> = sql_query(query)
.bind::<Text, _>(trader_pubkey.to_string())
.load(conn)?;

Ok(summaries)
}
22 changes: 22 additions & 0 deletions coordinator/src/db/custom_types.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use crate::db::bonus_status::BonusType;
use crate::db::dlc_channels::DlcChannelState;
use crate::db::dlc_messages::MessageType;
use crate::db::dlc_protocols::DlcProtocolState;
use crate::db::dlc_protocols::DlcProtocolType;
use crate::db::polls::PollType;
use crate::db::positions::ContractSymbol;
use crate::db::positions::PositionState;
use crate::schema::sql_types::BonusStatusType;
use crate::schema::sql_types::ContractSymbolType;
use crate::schema::sql_types::DirectionType;
use crate::schema::sql_types::DlcChannelStateType;
Expand Down Expand Up @@ -233,3 +235,23 @@ impl FromSql<DlcChannelStateType, Pg> for DlcChannelState {
}
}
}

impl ToSql<BonusStatusType, Pg> for BonusType {
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> serialize::Result {
match *self {
BonusType::Referral => out.write_all(b"Referral")?,
BonusType::Referent => out.write_all(b"Referent")?,
}
Ok(IsNull::No)
}
}

impl FromSql<BonusStatusType, Pg> for BonusType {
fn from_sql(bytes: PgValue<'_>) -> deserialize::Result<Self> {
match bytes.as_bytes() {
b"Referral" => Ok(BonusType::Referral),
b"Referent" => Ok(BonusType::Referent),
_ => Err("Unrecognized enum variant".into()),
}
}
}
2 changes: 2 additions & 0 deletions coordinator/src/db/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pub mod bonus_status;
pub mod bonus_tiers;
pub mod channel_opening_params;
pub mod collaborative_reverts;
pub mod custom_types;
Expand Down
3 changes: 3 additions & 0 deletions coordinator/src/db/trade_params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::dlc_protocol::ProtocolId;
use crate::orderbook::db::custom_types::Direction;
use crate::schema::trade_params;
use bitcoin::secp256k1::PublicKey;
use bitcoin::Amount;
use diesel::ExpressionMethods;
use diesel::PgConnection;
use diesel::QueryDsl;
Expand All @@ -23,6 +24,7 @@ pub(crate) struct TradeParams {
pub leverage: f32,
pub average_price: f32,
pub direction: Direction,
pub matching_fee: i64,
}

pub(crate) fn insert(
Expand Down Expand Up @@ -68,6 +70,7 @@ impl From<TradeParams> for dlc_protocol::TradeParams {
leverage: value.leverage,
average_price: value.average_price,
direction: trade::Direction::from(value.direction),
matching_fee: Amount::from_sat(value.matching_fee as u64),
}
}
}
Loading

0 comments on commit 41afeb3

Please sign in to comment.