diff --git a/Cargo.lock b/Cargo.lock index 15ff03b..737290a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2138,21 +2138,6 @@ dependencies = [ "serde", ] -[[package]] -name = "ic-ledger-types" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a144b5b4c9e4b164b338440cfbdcf9f30ff45e3d48a3be62d2424996dc93d6f" -dependencies = [ - "candid", - "crc32fast", - "hex", - "ic-cdk 0.16.0", - "serde", - "serde_bytes", - "sha2 0.10.8", -] - [[package]] name = "ic-management-canister-types" version = "0.9.0" @@ -2450,7 +2435,6 @@ dependencies = [ "hex", "ic-cdk 0.16.0", "ic-cdk-timers", - "ic-ledger-types", "ic-stable-structures", "icrc-ledger-types 0.1.6", "lib_panda", @@ -3182,7 +3166,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" dependencies = [ "anyhow", - "itertools 0.11.0", + "itertools 0.12.1", "proc-macro2", "quote", "syn 2.0.82", diff --git a/src/cli_airdrop/src/block.rs b/src/cli_airdrop/src/block.rs index e3b3efb..723db5c 100644 --- a/src/cli_airdrop/src/block.rs +++ b/src/cli_airdrop/src/block.rs @@ -232,11 +232,7 @@ impl<'a> Iterator for BlocksIter<'a> { } } - if let Some(block) = self.blocks.remove(&index) { - Some((index, block)) - } else { - None - } + self.blocks.remove(&index).map(|block| (index, block)) } } diff --git a/src/cli_airdrop/src/main.rs b/src/cli_airdrop/src/main.rs index 5b03bb6..f255145 100644 --- a/src/cli_airdrop/src/main.rs +++ b/src/cli_airdrop/src/main.rs @@ -4,10 +4,11 @@ use clap::{Parser, Subcommand}; use ic_agent::identity::AnonymousIdentity; use ic_icrc1::Operation; use icrc_ledger_types::icrc1::account::Account; -use num_traits::{cast::ToPrimitive, Saturating}; +use num_traits::cast::ToPrimitive; use serde::{Deserialize, Serialize}; use serde_bytes::{ByteArray, ByteBuf}; use sha2::Digest; +use std::fmt::Write; use std::{ collections::BTreeMap, time::{SystemTime, UNIX_EPOCH}, @@ -47,6 +48,15 @@ pub struct Cli { #[derive(Subcommand)] pub enum Commands { + Blob { + /// file path to read + #[arg(long)] + path: String, + + /// file path to write + #[arg(long)] + output: Option, + }, Neurons {}, Ledger { /// blocks store directory @@ -60,6 +70,7 @@ pub enum Commands { }, } +// cargo run -p cli_airdrop -- blob --path ./debug/ledger_airdrops_1730272519.cbor.7c054a384a1db11259b9451b172b8c8eedc01144e448d802c686f9bbd1f47381 // cargo run -p cli_airdrop -- neurons // cargo run -p cli_airdrop -- sync --store ./debug/panda_blocks // cargo run -p cli_airdrop -- ledger --store ./debug/panda_blocks @@ -73,6 +84,27 @@ async fn main() -> Result<(), String> { let snapshot = SNAPSHOT_TIME.min(now); match &cli.command { + Some(Commands::Blob { path, output }) => match std::fs::read(path) { + Ok(data) => { + let s = data.iter().fold(String::new(), |mut output, b| { + let _ = write!(output, "\\{b:02x}"); + output + }); + // candid blob: + let s = format!("blob \"{}\"", s); + match output { + Some(output) => { + std::fs::write(output, s.as_bytes()).map_err(format_error)?; + } + None => { + println!("{}", s); + } + } + } + Err(err) => { + return Err(format!("{:?}", err)); + } + }, Some(Commands::Neurons {}) => { let cli = NeuronAgent { agent, @@ -112,7 +144,7 @@ async fn main() -> Result<(), String> { total_e8s += amount; airdrops .entry(principal) - .or_insert_with(|| vec![]) + .or_default() .push(Airdrop(amount, None, Some(neuron_id))); } } @@ -223,8 +255,8 @@ async fn main() -> Result<(), String> { )); total_e8s += *amount; airdrops - .entry(account.owner.clone()) - .or_insert_with(|| vec![]) + .entry(account.owner) + .or_default() .push(Airdrop( *amount, account.subaccount.map(ByteArray::from), diff --git a/src/ic_message_profile/src/store.rs b/src/ic_message_profile/src/store.rs index dd5159b..8628622 100644 --- a/src/ic_message_profile/src/store.rs +++ b/src/ic_message_profile/src/store.rs @@ -218,7 +218,7 @@ pub mod profile { let mut m = r.borrow_mut(); match m.get(&user) { None => Err("profile not found".to_string()), - Some(mut p) => f(&mut p).inspect(|r| { + Some(mut p) => f(&mut p).inspect(|_r| { m.insert(user, p); }), } diff --git a/src/ic_panda_luckypool/Cargo.toml b/src/ic_panda_luckypool/Cargo.toml index 9a3adbb..736e9fe 100644 --- a/src/ic_panda_luckypool/Cargo.toml +++ b/src/ic_panda_luckypool/Cargo.toml @@ -26,7 +26,6 @@ ic-cdk = { workspace = true } ic-cdk-timers = { workspace = true } ic-stable-structures = { workspace = true } icrc-ledger-types = { workspace = true } -ic-ledger-types = "0.13" once_cell = "1.19" scopeguard = "1.2" finl_unicode = "1.2" diff --git a/src/ic_panda_luckypool/ic_panda_luckypool.did b/src/ic_panda_luckypool/ic_panda_luckypool.did index ca68f42..066b9bd 100644 --- a/src/ic_panda_luckypool/ic_panda_luckypool.did +++ b/src/ic_panda_luckypool/ic_panda_luckypool.did @@ -11,6 +11,11 @@ type AddPrizeInputV2 = record { quantity : nat16; expire : nat16; }; +type Airdrop = record { + weight : nat64; + subaccount : opt text; + neuron_id : opt text; +}; type AirdropClaimInput = record { recaptcha : opt text; challenge : text; @@ -40,6 +45,21 @@ type AirdropStateOutput = record { claimed : nat; claimable : nat; }; +type Airdrops108Output = record { + status : int8; + ledger_updated_at : nat64; + airdrops : vec Airdrop; + ledger_weight_total : nat64; + tokens_per_weight : float64; + error : opt text; + neurons_hash : text; + neurons_airdropped : bool; + ledger_hash : text; + tokens_distributed : nat64; + neurons_weight_total : nat64; + neurons_updated_at : nat64; + ledger_airdropped : bool; +}; type CaptchaOutput = record { challenge : text; img_base64 : text }; type ClaimPrizeInput = record { challenge : blob; code : text }; type ClaimPrizeOutput = record { @@ -102,18 +122,21 @@ type PrizeOutput = record { }; type Result = variant { Ok : PrizeOutput; Err : text }; type Result_1 = variant { Ok; Err : text }; -type Result_10 = variant { Ok : NameOutput; Err : text }; -type Result_11 = variant { Ok : State; Err }; -type Result_12 = variant { Ok : nat; Err : text }; -type Result_13 = variant { Ok : principal; Err }; +type Result_10 = variant { Ok : nat64; Err : text }; +type Result_11 = variant { Ok : opt NameOutput; Err }; +type Result_12 = variant { Ok : principal; Err : text }; +type Result_13 = variant { Ok : NameOutput; Err : text }; +type Result_14 = variant { Ok : State; Err }; +type Result_15 = variant { Ok : nat; Err : text }; +type Result_16 = variant { Ok : principal; Err }; type Result_2 = variant { Ok : AirdropStateOutput; Err : text }; type Result_3 = variant { Ok : AirdropStateOutput; Err }; -type Result_4 = variant { Ok : CaptchaOutput; Err : text }; -type Result_5 = variant { Ok : ClaimPrizeOutput; Err : text }; -type Result_6 = variant { Ok : LuckyDrawOutput; Err : text }; -type Result_7 = variant { Ok : text; Err : text }; -type Result_8 = variant { Ok : opt NameOutput; Err }; -type Result_9 = variant { Ok : principal; Err : text }; +type Result_4 = variant { Ok : Airdrops108Output; Err }; +type Result_5 = variant { Ok : CaptchaOutput; Err : text }; +type Result_6 = variant { Ok : ClaimPrizeOutput; Err : text }; +type Result_7 = variant { Ok : LuckyDrawOutput; Err : text }; +type Result_8 = variant { Ok : text; Err : text }; +type Result_9 = variant { Ok : bool; Err : text }; type State = record { latest_luckydraw_logs : vec LuckyDrawLog; total_luckydraw : nat64; @@ -135,34 +158,39 @@ type State = record { service : () -> { add_prize : (AddPrizeInputV2) -> (Result); admin_collect_icp : (nat) -> (Result_1); + admin_collect_tokens : (nat) -> (Result_1); admin_set_managers : (vec principal) -> (Result_1); airdrop : (AirdropClaimInput) -> (Result_2); airdrop_codes_of : (principal) -> (vec AirdropCodeOutput) query; airdrop_logs : (opt nat, opt nat) -> (vec AirdropLog) query; airdrop_state_of : (opt principal) -> (Result_3) query; + airdrops108_of : (opt principal) -> (Result_4) query; api_version : () -> (nat16) query; - captcha : () -> (Result_4); - claim_prize : (ClaimPrizeInput) -> (Result_5); + captcha : () -> (Result_5); + claim_prize : (ClaimPrizeInput) -> (Result_6); harvest : (AirdropHarvestInput) -> (Result_2); - luckydraw : (LuckyDrawInput) -> (Result_6); + luckydraw : (LuckyDrawInput) -> (Result_7); luckydraw_logs : (opt nat, opt nat) -> (vec LuckyDrawLog) query; manager_add_notification : (Notification) -> (Result_1); - manager_add_prize : (AddPrizeInput) -> (Result_7); - manager_add_prize_v2 : (AddPrizeInputV2) -> (Result_7); + manager_add_prize : (AddPrizeInput) -> (Result_8); + manager_add_prize_v2 : (AddPrizeInputV2) -> (Result_8); manager_ban_users : (vec principal) -> (Result_1); - manager_get_airdrop_key : () -> (Result_7) query; + manager_get_airdrop_key : () -> (Result_8) query; manager_remove_notifications : (blob) -> (Result_1); manager_set_challenge_pub_key : (text) -> (Result_1); + manager_start_airdrops108 : () -> (Result_9); manager_update_airdrop_amount : (nat64) -> (Result_1); manager_update_airdrop_balance : (nat64) -> (Result_1); + manager_update_airdrops108_ledger_list : (blob) -> (Result_10); + manager_update_airdrops108_neurons_list : (blob) -> (Result_10); manager_update_prize_subsidy : ( opt record { nat64; nat16; nat32; nat8; nat32; nat16 }, ) -> (Result_1); my_luckydraw_logs : (opt nat, opt nat) -> (vec LuckyDrawLog) query; - name_lookup : (text) -> (Result_8) query; - name_of : (opt principal) -> (Result_8) query; + name_lookup : (text) -> (Result_11) query; + name_of : (opt principal) -> (Result_11) query; notifications : () -> (vec Notification) query; - principal_by_luckycode : (text) -> (Result_9) query; + principal_by_luckycode : (text) -> (Result_12) query; prize : (text) -> (Result_2); prize_claim_logs : (principal, opt nat, opt nat) -> (vec PrizeClaimLog) query; prize_info : (text, opt principal) -> (Result) query; @@ -171,13 +199,14 @@ service : () -> { prizes_of : (opt principal) -> ( vec record { nat32; nat32; nat16; nat32; nat16; nat16 }, ) query; - register_name : (NameInput) -> (Result_10); - state : () -> (Result_11) query; - unregister_name : (NameInput) -> (Result_12); - update_name : (NameInput) -> (Result_10); - validate2_admin_collect_icp : (nat) -> (Result_7); - validate2_admin_set_managers : (vec principal) -> (Result_7); + register_name : (NameInput) -> (Result_13); + state : () -> (Result_14) query; + unregister_name : (NameInput) -> (Result_15); + update_name : (NameInput) -> (Result_13); + validate2_admin_collect_icp : (nat) -> (Result_8); + validate2_admin_set_managers : (vec principal) -> (Result_8); validate_admin_collect_icp : (nat) -> (Result_1); + validate_admin_collect_tokens : (nat) -> (Result_8); validate_admin_set_managers : (vec principal) -> (Result_1); - whoami : () -> (Result_13) query; + whoami : () -> (Result_16) query; } diff --git a/src/ic_panda_luckypool/src/api_admin.rs b/src/ic_panda_luckypool/src/api_admin.rs index 09c72ac..66c6a9b 100644 --- a/src/ic_panda_luckypool/src/api_admin.rs +++ b/src/ic_panda_luckypool/src/api_admin.rs @@ -1,11 +1,15 @@ use base64::{engine::general_purpose, Engine}; use candid::{Nat, Principal}; -use lib_panda::{bytes32_from_base64, Cryptogram}; -use std::collections::BTreeSet; +use ciborium::from_reader; +use icrc_ledger_types::icrc1::account::Account; +use lib_panda::{bytes32_from_base64, sha256, Cryptogram}; +use serde_bytes::ByteBuf; +use std::collections::{BTreeMap, BTreeSet}; +use std::time::Duration; use crate::{ - icp_transfer_to, is_authenticated, is_controller, store, token_balance_of, types, ANONYMOUS, - DAO_CANISTER, ICP_1, ICP_CANISTER, SECOND, TOKEN_1, TRANS_FEE, + icp_transfer_to, is_authenticated, is_controller, store, token_transfer_to, types, + AIRDROP108_TIME_NS, AIRDROP108_TOKENS, ANONYMOUS, DAO_CANISTER, ICP_1, SECOND, TOKEN_1, }; #[ic_cdk::update(guard = "is_controller")] @@ -17,25 +21,46 @@ async fn admin_collect_icp(amount: Nat) -> Result<(), String> { } #[ic_cdk::update] -async fn validate_admin_collect_icp(amount: Nat) -> Result<(), String> { +fn validate_admin_collect_icp(amount: Nat) -> Result<(), String> { if amount < ICP_1 { return Err("amount must be at least 1 ICP".to_string()); } - let balance = token_balance_of(ICP_CANISTER, ic_cdk::id()) - .await - .unwrap_or(Nat::from(0u64)); + Ok(()) +} - if amount + TRANS_FEE > balance { - return Err(format!("insufficient ICP balance: {}", balance)); - } +#[ic_cdk::update] +fn validate2_admin_collect_icp(amount: Nat) -> Result { + validate_admin_collect_icp(amount)?; + Ok("ok".to_string()) +} +#[ic_cdk::update(guard = "is_controller")] +async fn admin_collect_tokens(amount: Nat) -> Result<(), String> { + // https://dashboard.internetcomputer.org/sns/d7wvo-iiaaa-aaaaq-aacsq-cai/account/dwv6s-6aaaa-aaaaq-aacta-cai-3ajyuja.f6cc24dd368235dbdf2b3c792e399ac10f00a0003373de6d0960ae55ca873ebb + token_transfer_to( + Account { + owner: DAO_CANISTER, + subaccount: Some( + hex::decode("f6cc24dd368235dbdf2b3c792e399ac10f00a0003373de6d0960ae55ca873ebb") + .unwrap() + .try_into() + .unwrap(), + ), + }, + amount, + "COLLECT".to_string(), + ) + .await + .map_err(|err| format!("failed to collect PANDA, {}", err))?; Ok(()) } #[ic_cdk::update] -async fn validate2_admin_collect_icp(amount: Nat) -> Result { - validate_admin_collect_icp(amount).await?; +async fn validate_admin_collect_tokens(amount: Nat) -> Result { + if amount < TOKEN_1 { + return Err("amount must be at least 1 PANDA".to_string()); + } Ok("ok".to_string()) } @@ -208,3 +233,93 @@ fn manager_set_challenge_pub_key(key: String) -> Result<(), String> { store::keys::set_challenge_pub_key(key); Ok(()) } + +#[ic_cdk::update(guard = "is_authenticated")] +fn manager_update_airdrops108_ledger_list(data: ByteBuf) -> Result { + if !store::state::is_manager(&ic_cdk::caller()) { + return Err("user is not a manager".to_string()); + } + let now = ic_cdk::api::time(); + if now + 3600 * SECOND > AIRDROP108_TIME_NS { + return Err("can not update airdrop list".to_string()); + } + + let airdrops: BTreeMap> = + from_reader(&data[..]).map_err(|err| format!("failed to decode airdrops: {:?}", err))?; + let hash = sha256(&data); + let principals = airdrops.keys().cloned().collect::>(); + let weight_total = airdrops.values().flatten().map(|a| a.0).sum::(); + let count = principals.len() as u64; + store::state::with_mut(|r| { + let airdrops108 = r.airdrops108.get_or_insert(Default::default()); + if airdrops108.status != 0 { + return Err("can not update airdrop list".to_string()); + } + airdrops108.ledger = airdrops; + airdrops108.ledger_todo_list = principals; + airdrops108.ledger_hash = hash.into(); + airdrops108.ledger_updated_at = now / 1_000_000; + airdrops108.ledger_weight_total = weight_total; + airdrops108.tokens_per_weight = + AIRDROP108_TOKENS as f64 / (weight_total + airdrops108.neurons_weight_total) as f64; + Ok(()) + })?; + + Ok(count) +} + +#[ic_cdk::update(guard = "is_authenticated")] +fn manager_update_airdrops108_neurons_list(data: ByteBuf) -> Result { + if !store::state::is_manager(&ic_cdk::caller()) { + return Err("user is not a manager".to_string()); + } + let now = ic_cdk::api::time(); + if now + 3600 * SECOND > AIRDROP108_TIME_NS { + return Err("can not update airdrop list".to_string()); + } + + let airdrops: BTreeMap> = + from_reader(&data[..]).map_err(|err| format!("failed to decode airdrops: {:?}", err))?; + let hash = sha256(&data); + let principals = airdrops.keys().cloned().collect::>(); + let weight_total = airdrops.values().flatten().map(|a| a.0).sum::(); + let count = principals.len() as u64; + store::state::with_mut(|r| { + let airdrops108 = r.airdrops108.get_or_insert(Default::default()); + if airdrops108.status != 0 { + return Err("can not update airdrop list".to_string()); + } + airdrops108.neurons = airdrops; + airdrops108.neurons_todo_list = principals; + airdrops108.neurons_hash = hash.into(); + airdrops108.neurons_updated_at = now / 1_000_000; + airdrops108.neurons_weight_total = weight_total; + airdrops108.tokens_per_weight = + AIRDROP108_TOKENS as f64 / (weight_total + airdrops108.ledger_weight_total) as f64; + Ok(()) + })?; + + Ok(count) +} + +#[ic_cdk::update(guard = "is_authenticated")] +fn manager_start_airdrops108() -> Result { + if !store::state::is_manager(&ic_cdk::caller()) { + return Err("user is not a manager".to_string()); + } + + let res = store::state::with_mut(|r| { + if let Some(ref mut airdrops108) = r.airdrops108 { + if !airdrops108.status < 1 { + airdrops108.status = 1; + let delay = AIRDROP108_TIME_NS.saturating_sub(ic_cdk::api::time()); + ic_cdk_timers::set_timer(Duration::from_nanos(delay), || { + ic_cdk::spawn(store::state::start_airdrops108()) + }); + return true; + } + } + false + }); + Ok(res) +} diff --git a/src/ic_panda_luckypool/src/api_query.rs b/src/ic_panda_luckypool/src/api_query.rs index 7e8674a..ff186fe 100644 --- a/src/ic_panda_luckypool/src/api_query.rs +++ b/src/ic_panda_luckypool/src/api_query.rs @@ -14,8 +14,8 @@ fn whoami() -> Result { } #[ic_cdk::query] -fn state() -> Result { - Ok(store::state::with(|r| r.clone())) +fn state() -> Result { + Ok(store::state::with(|r| r.to_info())) } #[ic_cdk::query] @@ -56,6 +56,12 @@ async fn name_lookup(name: String) -> Result, ()> { ) } +#[ic_cdk::query] +async fn airdrops108_of(owner: Option) -> Result { + let owner = owner.unwrap_or(ic_cdk::caller()); + Ok(store::state::with(|r| r.airdrops(owner))) +} + #[ic_cdk::query] async fn airdrop_state_of(owner: Option) -> Result { let owner = owner.unwrap_or(ic_cdk::caller()); diff --git a/src/ic_panda_luckypool/src/api_update.rs b/src/ic_panda_luckypool/src/api_update.rs index 3bde990..3a80756 100644 --- a/src/ic_panda_luckypool/src/api_update.rs +++ b/src/ic_panda_luckypool/src/api_update.rs @@ -1,4 +1,5 @@ use candid::Nat; +use icrc_ledger_types::icrc1::account::Account; use lib_panda::{mac_256, ChallengeState, Cryptogram, Ed25519Message, VerifyingKey}; use serde_bytes::ByteBuf; @@ -220,7 +221,16 @@ async fn harvest(args: types::AirdropHarvestInput) -> Result Result Result { + Err("The lucky draw has been suspend. See: https://dashboard.internetcomputer.org/sns/d7wvo-iiaaa-aaaaq-aacsq-cai/proposal/108".to_string())?; + let icp01 = if args.icp == 0 { args.amount.map_or(0, |v| nat_to_u64(&v) * 10 / TOKEN_1) } else { @@ -519,9 +531,16 @@ async fn unregister_name(args: types::NameInput) -> Result { let n = (du / y + if r > 3600 * 24 * 7 { 1 } else { 0 }) as u32; let refund = name_state.2.saturating_sub(n * name_state.3) as u64 * TOKEN_1; if refund > 0 { - let _ = token_transfer_to(caller, Nat::from(refund), "NAME:UNREG".to_string()) - .await - .map_err(|err| format!("failed to refund, {}", err))?; + let _ = token_transfer_to( + Account { + owner: caller, + subaccount: None, + }, + Nat::from(refund), + "NAME:UNREG".to_string(), + ) + .await + .map_err(|err| format!("failed to refund, {}", err))?; } Ok(Nat::from(refund)) diff --git a/src/ic_panda_luckypool/src/lib.rs b/src/ic_panda_luckypool/src/lib.rs index d10e597..89ed5ed 100644 --- a/src/ic_panda_luckypool/src/lib.rs +++ b/src/ic_panda_luckypool/src/lib.rs @@ -25,11 +25,15 @@ const TRANS_FEE: u64 = 10_000; // 0.0001 ICP const TOKEN_1: u64 = 100_000_000; const TOKEN_SMALL_UNIT: u64 = 10_000; // 0.0001 token const MAX_PRIZE_CLAIMABLE: u64 = 420_000; // 420_000 tokens * TOKEN_SMALL_UNIT < u32::MAX -const ICP_1: u64 = ic_ledger_types::Tokens::SUBDIVIDABLE_BY; +const ICP_1: u64 = 100_000_000; -static ANONYMOUS: Principal = Principal::anonymous(); -static ICP_CANISTER: Principal = ic_ledger_types::MAINNET_LEDGER_CANISTER_ID; +// https://dashboard.internetcomputer.org/sns/d7wvo-iiaaa-aaaaq-aacsq-cai/proposal/108 +const AIRDROP108_TIME_NS: u64 = 1731283200 * SECOND; // '2024-11-11T00:00:00.000Z' +const AIRDROP108_TOKENS: u64 = 320_000_000 * TOKEN_1; +static ANONYMOUS: Principal = Principal::anonymous(); +// MAINNET_LEDGER_CANISTER_ID +static ICP_CANISTER: Principal = Principal::from_slice(&[0, 0, 0, 0, 0, 0, 0, 2, 1, 1]); // "druyg-tyaaa-aaaaq-aactq-cai" PANDA token canister id static TOKEN_CANISTER: Principal = Principal::from_slice(&[0, 0, 0, 0, 2, 0, 0, 167, 1, 1]); // "dwv6s-6aaaa-aaaaq-aacta-cai" ICPanda DAO canister id @@ -95,16 +99,13 @@ async fn icp_transfer_from(user: Principal, amount: Nat, memo: String) -> Result res.map_err(|err| format!("failed to transfer ICP from user, error: {:?}", err)) } -async fn token_transfer_to(user: Principal, amount: Nat, memo: String) -> Result { +async fn token_transfer_to(account: Account, amount: Nat, memo: String) -> Result { let (res,): (Result,) = ic_cdk::call( TOKEN_CANISTER, "icrc1_transfer", (TransferArg { from_subaccount: None, - to: Account { - owner: user, - subaccount: None, - }, + to: account, fee: None, created_at_time: None, memo: Some(Memo(ByteBuf::from(memo.to_bytes()))), diff --git a/src/ic_panda_luckypool/src/store.rs b/src/ic_panda_luckypool/src/store.rs index 6be3674..0acb890 100644 --- a/src/ic_panda_luckypool/src/store.rs +++ b/src/ic_panda_luckypool/src/store.rs @@ -5,10 +5,11 @@ use ic_stable_structures::{ storable::Bound, DefaultMemoryImpl, StableBTreeMap, StableCell, StableLog, StableMinHeap, Storable, }; +use icrc_ledger_types::icrc1::account::Account; use lib_panda::{mac_256, Cryptogram}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; -use serde_bytes::ByteBuf; +use serde_bytes::{ByteArray, ByteBuf}; use std::{ borrow::Cow, cell::RefCell, @@ -21,7 +22,7 @@ use crate::{types, SECOND, TOKEN_1, TOKEN_SMALL_UNIT}; type Memory = VirtualMemory; -#[derive(CandidType, Clone, Default, Deserialize, Serialize)] +#[derive(Clone, Default, Deserialize, Serialize)] pub struct State { pub airdrop_balance: u64, pub total_airdrop: u64, @@ -39,8 +40,103 @@ pub struct State { pub airdrop_amount: Option, pub prize_subsidy: Option, pub lucky_code: Option, + pub airdrops108: Option, +} + +impl State { + pub fn to_info(&self) -> types::State { + types::State { + airdrop_balance: self.airdrop_balance, + total_airdrop: self.total_airdrop, + total_airdrop_count: self.total_airdrop_count, + total_luckydraw: self.total_luckydraw, + total_luckydraw_icp: self.total_luckydraw_icp, + total_luckydraw_count: self.total_luckydraw_count, + total_prize: self.total_prize, + total_prize_count: self.total_prize_count, + total_prizes_count: self.total_prizes_count, + latest_airdrop_logs: self.latest_airdrop_logs.clone(), + luckiest_luckydraw_logs: self.luckiest_luckydraw_logs.clone(), + latest_luckydraw_logs: self.latest_luckydraw_logs.clone(), + managers: self.managers.clone(), + airdrop_amount: self.airdrop_amount, + prize_subsidy: self + .prize_subsidy + .as_ref() + .map(|x| types::SysPrizeSubsidy(x.0, x.1, x.2, x.3, x.4, x.5)), + lucky_code: self.lucky_code, + } + } + + pub fn airdrops(&self, user: Principal) -> types::Airdrops108Output { + self.airdrops108 + .as_ref() + .map(|x| { + let mut airdrops: Vec = Vec::new(); + let mut ledger_airdropped = false; + let mut neurons_airdropped = false; + if let Some(list) = x.neurons.get(&user) { + ledger_airdropped = !list.is_empty() && !x.ledger_todo_list.contains(&user); + list.iter() + .map(|a| types::Airdrop { + weight: a.0, + subaccount: a.1.as_ref().map(|x| hex::encode(x.as_slice())), + neuron_id: a.2.as_ref().map(|x| hex::encode(x.as_slice())), + }) + .for_each(|x| airdrops.push(x)) + }; + if let Some(list) = x.ledger.get(&user) { + neurons_airdropped = !list.is_empty() && !x.neurons_todo_list.contains(&user); + list.iter() + .map(|a| types::Airdrop { + weight: a.0, + subaccount: a.1.as_ref().map(|x| hex::encode(x.as_slice())), + neuron_id: a.2.as_ref().map(|x| hex::encode(x.as_slice())), + }) + .for_each(|x| airdrops.push(x)) + }; + types::Airdrops108Output { + airdrops, + ledger_airdropped, + neurons_airdropped, + ledger_hash: hex::encode(x.ledger_hash.as_slice()), + ledger_updated_at: x.ledger_updated_at, + ledger_weight_total: x.ledger_weight_total, + neurons_hash: hex::encode(x.neurons_hash.as_slice()), + neurons_updated_at: x.neurons_updated_at, + neurons_weight_total: x.neurons_weight_total, + tokens_distributed: x.tokens_distributed, + tokens_per_weight: x.tokens_per_weight, + status: x.status, + error: x.error.clone(), + } + }) + .unwrap_or_default() + } } +#[derive(Clone, Default, Deserialize, Serialize)] +pub struct Airdrops108 { + pub ledger: BTreeMap>, + pub ledger_todo_list: BTreeSet, + pub ledger_hash: ByteArray<32>, + pub ledger_updated_at: u64, + pub ledger_weight_total: u64, + pub neurons: BTreeMap>, + pub neurons_todo_list: BTreeSet, + pub neurons_hash: ByteArray<32>, + pub neurons_updated_at: u64, + pub neurons_weight_total: u64, + pub tokens_distributed: u64, + pub tokens_per_weight: f64, + pub status: i8, // -1: error, 0: wait, 1: started, 2: finished + pub error: Option, +} + +// owner -> (amount_e8s, opt subaccount, opt neuronid) +#[derive(Clone, Deserialize, Serialize)] +pub struct Airdrop(pub u64, pub Option>, pub Option); + impl Storable for State { const BOUND: Bound = Bound::Unbounded; @@ -1233,6 +1329,8 @@ pub mod naming { pub mod state { use super::*; + use crate::{token_balance_of, token_transfer_to, TOKEN_CANISTER}; + use std::time::Duration; pub fn is_manager(caller: &Principal) -> bool { STATE_HEAP.with(|r| { @@ -1291,6 +1389,109 @@ pub mod state { }); }); } + + pub async fn start_airdrops108() { + match process_airdrops108().await { + Ok(true) => { + ic_cdk_timers::set_timer(Duration::from_nanos(0), || { + ic_cdk::spawn(start_airdrops108()) + }); + } + Ok(false) => { + with_mut(|s| { + if let Some(a) = s.airdrops108.as_mut() { + a.status = 2; + } + }); + } + Err(err) => { + with_mut(|s| { + if let Some(a) = s.airdrops108.as_mut() { + a.error = Some(err); + a.status = -1; + }; + }); + } + }; + } + + pub async fn process_airdrops108() -> Result { + let mut is_ledger = false; + let res = with(|s| { + s.airdrops108.as_ref().map(|a| { + if let Some(user) = a.ledger_todo_list.first() { + is_ledger = true; + let weight_total = a + .ledger + .get(user) + .unwrap_or(&vec![]) + .iter() + .map(|v| v.0) + .sum::(); + return if weight_total > 0 { + Some((*user, a.tokens_per_weight, weight_total)) + } else { + None + }; + } + if let Some(user) = a.neurons_todo_list.first() { + let weight_total = a + .neurons + .get(user) + .unwrap_or(&vec![]) + .iter() + .map(|v| v.0) + .sum::(); + return if weight_total > 0 { + Some((*user, a.tokens_per_weight, weight_total)) + } else { + None + }; + } + None::<(Principal, f64, u64)> + }) + }) + .flatten(); + + if let Some((user, tokens_per_weight, weight_total)) = res { + let check_balance = if is_ledger { + token_balance_of(TOKEN_CANISTER, user).await? + } else { + Nat::from(weight_total) + }; + // https://dashboard.internetcomputer.org/sns/d7wvo-iiaaa-aaaaq-aacsq-cai/proposal/184 + // if the user don't have enough balance, we will skip the airdrop. + let airdropped = if check_balance >= weight_total { + let amount = (weight_total as f64 * tokens_per_weight).round() as u64; + token_transfer_to( + Account { + owner: user, + subaccount: None, + }, + amount.into(), + "AIRDROP108".to_string(), + ) + .await?; + amount + } else { + 0 + }; + + with_mut(|s| { + if let Some(a) = s.airdrops108.as_mut() { + if is_ledger { + a.ledger_todo_list.remove(&user); + } else { + a.neurons_todo_list.remove(&user); + } + a.tokens_distributed += airdropped; + }; + }); + return Ok(true); + } + + Ok(false) + } } pub mod notification { diff --git a/src/ic_panda_luckypool/src/types.rs b/src/ic_panda_luckypool/src/types.rs index 1696388..27a1ac6 100644 --- a/src/ic_panda_luckypool/src/types.rs +++ b/src/ic_panda_luckypool/src/types.rs @@ -3,10 +3,65 @@ use ciborium::from_reader; use ic_stable_structures::Storable; use serde::{Deserialize, Serialize}; use serde_bytes::ByteBuf; +use std::collections::BTreeSet; use url::Url; use crate::{store, utils, MAX_PRIZE_CLAIMABLE, TOKEN_1}; +#[derive(CandidType, Clone, Serialize)] +pub struct State { + pub airdrop_balance: u64, + pub total_airdrop: u64, + pub total_airdrop_count: u64, + pub total_luckydraw: u64, + pub total_luckydraw_icp: u64, + pub total_luckydraw_count: u64, + pub total_prize: Option, + pub total_prize_count: Option, // prize claiming count + pub total_prizes_count: Option, // total prizes count + pub latest_airdrop_logs: Vec, // latest 10 airdrop logs + pub luckiest_luckydraw_logs: Vec, // latest 10 luckiest luckydraw logs + pub latest_luckydraw_logs: Vec, // latest 10 luckydraw logs + pub managers: Option>, + pub airdrop_amount: Option, + pub prize_subsidy: Option, + pub lucky_code: Option, +} + +#[derive(CandidType, Default, Clone, Serialize)] +pub struct Airdrops108Output { + pub airdrops: Vec, + pub ledger_airdropped: bool, + pub ledger_hash: String, + pub ledger_updated_at: u64, // ms + pub ledger_weight_total: u64, + pub neurons_airdropped: bool, + pub neurons_hash: String, + pub neurons_updated_at: u64, + pub neurons_weight_total: u64, + pub tokens_per_weight: f64, + pub tokens_distributed: u64, + pub status: i8, + pub error: Option, +} + +#[derive(CandidType, Clone, Serialize)] +pub struct Airdrop { + pub weight: u64, + pub subaccount: Option, + pub neuron_id: Option, +} + +#[derive(CandidType, Clone, Serialize)] +pub struct SysPrizeSubsidy( + pub u64, // Prize fee in PANDA * TOKEN_1 + pub u16, // Min quantity requirement for subsidy + pub u32, // Min total amount tokens requirement for subsidy + pub u8, // Subsidy ratio, [0, 50] + pub u32, // Max subsidy tokens per prize + pub u16, // Subsidy count limit +); + #[derive(CandidType, Clone, Serialize)] pub struct CaptchaOutput { pub img_base64: String,