use crate::{
states::{
Competition, LeaderEntry, Participant, CALLER_PROGRAM_ID, MAX_LEADERBOARD_LEN,
PARTICIPANT_SEED,
},
CompetitionError,
};
use anchor_lang::prelude::*;
use gmsol_callback::{interface::ActionKind, CALLBACK_AUTHORITY_SEED};
use gmsol_programs::gmsol_store::accounts::TradeData;
#[derive(Accounts)]
#[instruction(authority_bump: u8)]
pub struct OnCreated<'info> {
#[account(
seeds = [CALLBACK_AUTHORITY_SEED],
bump = authority_bump,
seeds::program = CALLER_PROGRAM_ID,
)]
pub authority: Signer<'info>,
pub competition: Account<'info, Competition>,
#[account(
seeds = [
PARTICIPANT_SEED,
competition.key().as_ref(),
trader.key().as_ref(),
],
bump = participant.bump,
has_one = competition,
has_one = trader,
)]
pub participant: Account<'info, Participant>,
pub trader: UncheckedAccount<'info>,
pub action: UncheckedAccount<'info>,
}
impl OnCreated<'_> {
pub(crate) fn invoke(
ctx: Context<Self>,
_authority_bump: u8,
action_kind: u8,
callback_version: u8,
_extra_account_count: u8,
) -> Result<()> {
require_eq!(callback_version, 0);
require_eq!(
action_kind,
ActionKind::Order as u8,
CompetitionError::InvalidActionKind
);
ctx.accounts.validate_competition()?;
Ok(())
}
fn validate_competition(&self) -> Result<()> {
let now = Clock::get()?.unix_timestamp;
let comp = &self.competition;
require!(
comp.is_ongoing(now),
CompetitionError::OutsideCompetitionTime
);
Ok(())
}
}
#[derive(Accounts)]
#[instruction(authority_bump: u8)]
pub struct OnExecuted<'info> {
#[account(
seeds = [CALLBACK_AUTHORITY_SEED],
bump = authority_bump,
seeds::program = CALLER_PROGRAM_ID,
)]
pub authority: Signer<'info>,
#[account(mut)]
pub competition: Account<'info, Competition>,
#[account(
mut,
seeds = [
PARTICIPANT_SEED,
competition.key().as_ref(),
trader.key().as_ref(),
],
bump
)]
pub participant: UncheckedAccount<'info>,
pub trader: UncheckedAccount<'info>,
pub action: UncheckedAccount<'info>,
pub position: UncheckedAccount<'info>,
pub trade_event: Option<AccountLoader<'info, TradeData>>,
}
impl OnExecuted<'_> {
pub(crate) fn invoke(
ctx: Context<Self>,
_authority_bump: u8,
action_kind: u8,
callback_version: u8,
success: bool,
extra_account_count: u8,
) -> Result<()> {
require_eq!(callback_version, 0);
require_eq!(
action_kind,
ActionKind::Order as u8,
CompetitionError::InvalidActionKind
);
require_gte!(extra_account_count, 2);
if !success {
msg!("competition: ignore failed order");
return Ok(());
}
let clock = Clock::get()?;
let now = clock.unix_timestamp;
let comp = &mut ctx.accounts.competition;
if !comp.is_ongoing(now) {
msg!("competition: outside of the competition time");
return Ok(());
}
let volume = {
let Some(trade_event) = &ctx.accounts.trade_event else {
msg!("competition: no trade event");
return Ok(());
};
let trade_event = trade_event
.load()
.map_err(|_| CompetitionError::InvalidTradeEvent)?;
require_keys_eq!(
trade_event.user,
ctx.accounts.trader.key(),
CompetitionError::InvalidTradeEvent
);
let volume = if comp.only_count_increase {
trade_event
.after
.size_in_usd
.saturating_sub(trade_event.before.size_in_usd)
} else {
trade_event
.after
.size_in_usd
.abs_diff(trade_event.before.size_in_usd)
};
if volume == 0 {
msg!("competition: skipped trade with zero volume");
return Ok(());
}
volume
};
ctx.accounts.with_participant(|comp, part| {
part.volume = part.volume.saturating_add(volume);
let time_diff = now.saturating_sub(part.last_updated_at);
part.last_updated_at = now;
if time_diff <= comp.volume_merge_window {
part.merged_volume = part.merged_volume.saturating_add(volume);
if part.merged_volume >= comp.volume_threshold {
Self::extend_competition_time(comp, part, part.merged_volume)?;
part.merged_volume = 0;
}
} else {
if volume >= comp.volume_threshold {
Self::extend_competition_time(comp, part, volume)?;
part.merged_volume = 0;
} else {
part.merged_volume = volume;
}
}
Self::update_leaderboard(comp, part);
msg!(
"competition: trader={} new_volume={} volume_delta={} merged_volume={}",
part.trader,
part.volume,
volume,
part.merged_volume
);
Ok(())
})?;
Ok(())
}
fn with_participant(
&mut self,
f: impl FnOnce(&mut Competition, &mut Participant) -> Result<()>,
) -> Result<()> {
let AccountInfo {
key,
lamports,
data,
owner,
rent_epoch,
is_signer,
is_writable,
executable,
} = self.participant.as_ref();
let mut lamports = lamports.borrow_mut();
let mut data = data.borrow_mut();
let info = AccountInfo::new(
key,
*is_signer,
*is_writable,
*lamports,
*data,
owner,
*executable,
*rent_epoch,
);
let mut participant = Account::<Participant>::try_from(&info)?;
require_keys_eq!(participant.trader, self.trader.key());
require_keys_eq!(participant.competition, self.competition.key());
(f)(&mut self.competition, &mut participant)?;
participant.exit(&crate::ID)?;
Ok(())
}
fn extend_competition_time(
comp: &mut Competition,
part: &Participant,
volume: u128,
) -> Result<()> {
let now = Clock::get()?.unix_timestamp;
let old_end_time = comp.end_time;
let proposed_end_time = old_end_time.saturating_add(comp.extension_duration);
let max_end_time = now.saturating_add(comp.extension_cap);
comp.end_time = proposed_end_time.min(max_end_time).max(old_end_time);
comp.extension_triggerer = Some(part.trader);
debug_assert!(comp.end_time >= old_end_time);
let actual_extension = comp.end_time - old_end_time;
msg!(
"competition: extended time by {} seconds due to volume={} from trader={}",
actual_extension,
volume,
part.trader
);
Ok(())
}
fn update_leaderboard(comp: &mut Competition, part: &Participant) {
let entry = if let Some(pos) = comp
.leaderboard
.iter()
.position(|e| e.address == part.trader)
{
let mut old = comp.leaderboard.remove(pos);
old.volume = part.volume;
old
} else {
LeaderEntry {
address: part.trader,
volume: part.volume,
}
};
let insert_pos = comp
.leaderboard
.iter()
.rposition(|e| e.volume >= entry.volume)
.map(|pos| pos + 1)
.unwrap_or(0);
if insert_pos < MAX_LEADERBOARD_LEN as usize {
comp.leaderboard.insert(insert_pos, entry);
if comp.leaderboard.len() > MAX_LEADERBOARD_LEN as usize {
comp.leaderboard.truncate(MAX_LEADERBOARD_LEN as usize);
}
}
}
}
#[derive(Accounts)]
#[instruction(authority_bump: u8)]
pub struct OnCallback<'info> {
#[account(
seeds = [CALLBACK_AUTHORITY_SEED],
bump = authority_bump,
seeds::program = CALLER_PROGRAM_ID,
)]
pub authority: Signer<'info>,
pub competition: UncheckedAccount<'info>,
pub participant: UncheckedAccount<'info>,
pub trader: UncheckedAccount<'info>,
pub action: UncheckedAccount<'info>,
}