use std::fmt::{Display, Formatter};
use cosmwasm_schema::{cw_serde, QueryResponses};
use cosmwasm_std::{ensure, Coin, Decimal, Timestamp, Uint128};
use cw_ownable::{cw_ownable_execute, cw_ownable_query};
use crate::error::ContractError;
const MAX_NAME_LENGTH: usize = 200;
const MAX_DESCRIPTION_LENGTH: usize = 2000;
const MAX_TYPE_LENGTH: usize = 200;
#[cw_serde]
pub struct InstantiateMsg {
pub owner: Option<String>,
pub action: Option<CampaignAction>,
}
#[cw_ownable_execute]
#[cw_serde]
pub enum ExecuteMsg {
ManageCampaign { action: CampaignAction },
Claim {
receiver: Option<String>,
amount: Option<Uint128>,
},
AddAllocations {
allocations: Vec<(String, Uint128)>,
},
ReplaceAddress {
old_address: String,
new_address: String,
},
RemoveAddress {
address: String,
},
BlacklistAddress {
address: String,
blacklist: bool,
},
ManageAuthorizedWallets {
addresses: Vec<String>,
authorized: bool,
},
Sweep {
denom: String,
amount: Option<Uint128>,
},
}
#[cw_ownable_query]
#[cw_serde]
#[derive(QueryResponses)]
pub enum QueryMsg {
#[returns(CampaignResponse)]
Campaign {},
#[returns(RewardsResponse)]
Rewards {
receiver: String,
},
#[returns(ClaimedResponse)]
Claimed {
address: Option<String>,
start_from: Option<String>,
limit: Option<u16>,
},
#[returns(AllocationsResponse)]
Allocations {
address: Option<String>,
start_after: Option<String>,
limit: Option<u16>,
},
#[returns(BlacklistResponse)]
IsBlacklisted {
address: String,
},
#[returns(AuthorizedResponse)]
IsAuthorized {
address: String,
},
#[returns(AuthorizedWalletsResponse)]
AuthorizedWallets {
start_after: Option<String>,
limit: Option<u32>,
},
}
#[cw_serde]
pub struct MigrateMsg {}
pub type CampaignResponse = Campaign;
#[cw_serde]
pub struct RewardsResponse {
pub claimed: Vec<Coin>,
pub pending: Vec<Coin>,
pub available_to_claim: Vec<Coin>,
}
#[cw_serde]
pub struct ClaimedResponse {
pub claimed: Vec<(String, Coin)>,
}
#[cw_serde]
pub struct AllocationsResponse {
pub allocations: Vec<(String, Coin)>,
}
#[cw_serde]
pub struct BlacklistResponse {
pub is_blacklisted: bool,
}
#[cw_serde]
pub struct AuthorizedResponse {
pub is_authorized: bool,
}
#[cw_serde]
pub struct AuthorizedWalletsResponse {
pub wallets: Vec<String>,
}
#[cw_serde]
pub enum CampaignAction {
CreateCampaign {
params: Box<CampaignParams>,
},
CloseCampaign {},
}
#[cw_serde]
pub struct Campaign {
pub name: String,
pub description: String,
#[serde(rename = "type")]
pub ty: String,
pub total_reward: Coin,
pub claimed: Coin,
pub distribution_type: Vec<DistributionType>,
pub start_time: u64,
pub end_time: u64,
pub closed: Option<u64>,
}
impl Display for Campaign {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Campaign {{ name: {}, description: {}, type: {}, total_reward: {}, claimed: {}, distribution_type: {:?}, start_time: {}, end_time: {}, closed: {:?} }}",
self.name,
self.description,
self.ty,
self.total_reward,
self.claimed,
self.distribution_type,
self.start_time,
self.end_time,
self.closed
)
}
}
impl Campaign {
pub fn from_params(params: CampaignParams) -> Self {
let reward_denom = params.total_reward.denom.clone();
Campaign {
name: params.name,
description: params.description,
ty: params.ty,
total_reward: params.total_reward,
claimed: Coin {
denom: reward_denom,
amount: Uint128::zero(),
},
distribution_type: params.distribution_type,
start_time: params.start_time,
end_time: params.end_time,
closed: None,
}
}
pub fn has_started(&self, current_time: &Timestamp) -> bool {
current_time.seconds() >= self.start_time
}
pub fn has_ended(&self, current_time: &Timestamp) -> bool {
current_time.seconds() >= self.end_time
}
}
#[cw_serde]
pub struct CampaignParams {
pub name: String,
pub description: String,
#[serde(rename = "type")]
pub ty: String,
pub total_reward: Coin,
pub distribution_type: Vec<DistributionType>,
pub start_time: u64,
pub end_time: u64,
pub contract_label: String,
}
impl CampaignParams {
pub fn validate_campaign_name_description(&self) -> Result<(), ContractError> {
ensure!(
!self.name.is_empty(),
ContractError::InvalidCampaignParam {
param: "name".to_string(),
reason: "cannot be empty".to_string(),
}
);
ensure!(
self.name.len() <= MAX_NAME_LENGTH,
ContractError::InvalidCampaignParam {
param: "name".to_string(),
reason: format!("cannot be longer than {} characters", MAX_NAME_LENGTH),
}
);
ensure!(
!self.description.is_empty(),
ContractError::InvalidCampaignParam {
param: "description".to_string(),
reason: "cannot be empty".to_string(),
}
);
ensure!(
self.description.len() <= MAX_DESCRIPTION_LENGTH,
ContractError::InvalidCampaignParam {
param: "description".to_string(),
reason: format!(
"cannot be longer than {} characters",
MAX_DESCRIPTION_LENGTH
),
}
);
Ok(())
}
pub fn validate_campaign_type(&self) -> Result<(), ContractError> {
ensure!(
!self.ty.is_empty(),
ContractError::InvalidCampaignParam {
param: "type".to_string(),
reason: "cannot be empty".to_string(),
}
);
ensure!(
self.ty.len() <= MAX_TYPE_LENGTH,
ContractError::InvalidCampaignParam {
param: "type".to_string(),
reason: format!("cannot be longer than {} characters", MAX_TYPE_LENGTH),
}
);
Ok(())
}
pub fn validate_campaign_times(&self, current_time: Timestamp) -> Result<(), ContractError> {
ensure!(
self.start_time < self.end_time,
ContractError::InvalidCampaignParam {
param: "start_time".to_string(),
reason: "cannot be greater or equal than end_time".to_string(),
}
);
ensure!(
self.start_time >= current_time.seconds(),
ContractError::InvalidCampaignParam {
param: "start_time".to_string(),
reason: "cannot be less than the current time".to_string(),
}
);
Ok(())
}
pub fn validate_campaign_distribution(&self) -> Result<(), ContractError> {
let mut total_percentage = Decimal::zero();
ensure!(
!self.distribution_type.is_empty() && self.distribution_type.len() <= 2,
ContractError::InvalidCampaignParam {
param: "distribution_type".to_string(),
reason: "invalid number of distribution types, should be at least 1, maximum 2"
.to_string(),
}
);
for dist in self.distribution_type.iter() {
let (percentage, start_time, end_time, cliff_duration) = match dist {
DistributionType::LinearVesting {
percentage,
start_time,
end_time,
cliff_duration,
} => (percentage, start_time, Some(end_time), cliff_duration),
DistributionType::LumpSum {
percentage,
start_time,
} => (percentage, start_time, None, &None),
};
ensure!(
percentage != Decimal::zero(),
ContractError::ZeroDistributionPercentage
);
total_percentage = total_percentage.checked_add(*percentage)?;
ensure!(
*start_time >= self.start_time,
ContractError::InvalidStartDistributionTime {
start_time: *start_time,
campaign_start_time: self.start_time,
}
);
if let Some(end_time) = end_time {
ensure!(
end_time > start_time,
ContractError::InvalidDistributionTimes {
start_time: *start_time,
end_time: *end_time,
}
);
ensure!(
*end_time <= self.end_time,
ContractError::InvalidEndDistributionTime {
end_time: *end_time,
campaign_end_time: self.end_time,
}
);
}
if let Some(cliff_duration) = cliff_duration {
ensure!(
*cliff_duration > 0u64,
ContractError::InvalidCampaignParam {
param: "cliff_duration".to_string(),
reason: "cannot be zero".to_string(),
}
);
ensure!(
*cliff_duration < end_time.unwrap() - start_time,
ContractError::InvalidCampaignParam {
param: "cliff_duration".to_string(),
reason: "cannot be greater or equal than the distribution duration"
.to_string(),
}
);
}
}
ensure!(
total_percentage == Decimal::percent(100),
ContractError::InvalidDistributionPercentage {
expected: Decimal::percent(100),
actual: total_percentage,
}
);
Ok(())
}
pub fn validate_rewards(&self) -> Result<(), ContractError> {
ensure!(
self.total_reward.amount > Uint128::zero(),
ContractError::InvalidCampaignParam {
param: "total_reward".to_string(),
reason: "cannot be zero".to_string()
}
);
Ok(())
}
}
#[cw_serde]
pub enum DistributionType {
LinearVesting {
percentage: Decimal,
start_time: u64,
end_time: u64,
cliff_duration: Option<u64>,
},
LumpSum {
percentage: Decimal,
start_time: u64,
},
}
impl DistributionType {
pub fn has_started(&self, current_time: &Timestamp) -> bool {
let start_time = match self {
DistributionType::LinearVesting { start_time, .. } => start_time,
DistributionType::LumpSum { start_time, .. } => start_time,
};
current_time.seconds() >= *start_time
}
}