From 118c9a59fcdd4a5273fadfba5f2429e19a5d173c Mon Sep 17 00:00:00 2001 From: Franco Testagrossa Date: Thu, 2 May 2024 09:19:32 +0200 Subject: [PATCH] Refactor bounty service --- Cargo.lock | 18 +++++++ bounty/Cargo.toml | 5 +- bounty/bounty.did | 11 ++++ bounty/src/api/accept.rs | 48 ++++++++++++++++++ bounty/src/api/deposit.rs | 45 +++++++++++++++++ bounty/src/api/icrc1.rs | 74 +++++++++++++++++++++++++++ bounty/src/api/init.rs | 36 +++++++++++++ bounty/src/api/state.rs | 22 ++++++++ bounty/src/lib.rs | 104 ++++++-------------------------------- 9 files changed, 274 insertions(+), 89 deletions(-) create mode 100644 bounty/src/api/accept.rs create mode 100644 bounty/src/api/deposit.rs create mode 100644 bounty/src/api/icrc1.rs create mode 100644 bounty/src/api/init.rs create mode 100644 bounty/src/api/state.rs diff --git a/Cargo.lock b/Cargo.lock index ac5442c..5e37c33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,6 +68,9 @@ dependencies = [ "candid", "ic-cdk", "ic-cdk-macros", + "ic-ledger-types", + "num-bigint", + "num-traits", "serde", "serde_bytes", "serde_derive", @@ -234,6 +237,21 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ic-ledger-types" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d162fc508161221cc57bec8c0c4e029ae7c9eb92db461237499bb38c811ed4e" +dependencies = [ + "candid", + "crc32fast", + "hex", + "ic-cdk", + "serde", + "serde_bytes", + "sha2", +] + [[package]] name = "ic0" version = "0.21.1" diff --git a/bounty/Cargo.toml b/bounty/Cargo.toml index eba4de0..a3f3b16 100644 --- a/bounty/Cargo.toml +++ b/bounty/Cargo.toml @@ -12,7 +12,10 @@ crate-type = ["cdylib"] candid = "0.10.4" ic-cdk = "0.13.2" ic-cdk-macros = "0.13.2" +ic-ledger-types="0.10.0" serde = "1.0.199" serde_derive = "1.0.199" serde_json = "1.0.116" -serde_bytes = "0.11.14" \ No newline at end of file +serde_bytes = "0.11.14" +num-bigint = "0.4.4" +num-traits = "0.2.18" \ No newline at end of file diff --git a/bounty/bounty.did b/bounty/bounty.did index dbcdcf5..4f125ab 100644 --- a/bounty/bounty.did +++ b/bounty/bounty.did @@ -3,7 +3,18 @@ type Contributor = record { crypto_address: text; }; +type DepositReceipt = + variant { + Err: DepositErr; + Ok: nat; + }; +type DepositErr = + variant { + TransferFailure; + }; + service : (principal, int32) -> { "healthcheck": () -> (text); "accept": (Contributor) -> (); + "deposit": () -> (DepositReceipt); } diff --git a/bounty/src/api/accept.rs b/bounty/src/api/accept.rs new file mode 100644 index 0000000..beaad81 --- /dev/null +++ b/bounty/src/api/accept.rs @@ -0,0 +1,48 @@ +use super::state::{Contributor, BOUNTY_STATE}; + +pub fn accept_impl(contributor: Contributor) -> () { + BOUNTY_STATE.with(|state| { + if let Some(ref mut bounty_canister) = *state.borrow_mut() { + // Add the contributor to the interested contributors list + bounty_canister.interested_contributors.push(contributor); + } + }); +} + +#[cfg(test)] +mod test_accept { + use super::*; + use candid::Principal; + use crate::api::init::init_impl; + + #[test] + fn test_accept() { + let authority = + Principal::from_text("t2y5w-qp34w-qixaj-s67wp-syrei-5yqse-xbed6-z5nsd-fszmf-izgt2-lqe") + .unwrap(); + init_impl(authority, 123); + BOUNTY_STATE.with(|state| { + let bounty_canister = state.borrow(); + if let Some(ref bounty_canister) = *bounty_canister { + assert_eq!(bounty_canister.interested_contributors.len(), 0); + } else { + panic!("Bounty canister state not initialized"); + } + }); + let contributor = + Principal::from_text("t2y5w-qp34w-qixaj-s67wp-syrei-5yqse-xbed6-z5nsd-fszmf-izgt2-lqe") + .unwrap(); + accept_impl(Contributor { + address: contributor, + crypto_address: "contributor_address".to_string(), + }); + BOUNTY_STATE.with(|state| { + let bounty_canister = state.borrow(); + if let Some(ref bounty_canister) = *bounty_canister { + assert_eq!(bounty_canister.interested_contributors.len(), 1); + } else { + panic!("Bounty canister state not initialized"); + } + }); + } +} diff --git a/bounty/src/api/deposit.rs b/bounty/src/api/deposit.rs new file mode 100644 index 0000000..6039ce3 --- /dev/null +++ b/bounty/src/api/deposit.rs @@ -0,0 +1,45 @@ +use ic_cdk::api::{caller, id}; + +use super::*; + +use icrc1::{ICRC1, MAINNET_ICRC1_LEDGER_CANISTER_ID}; + +use candid::{CandidType, Nat, Principal}; + +#[derive(CandidType)] +pub enum DepositErr { + TransferFailure, +} + +pub type DepositReceipt = Result; + +pub async fn deposit_impl() -> DepositReceipt { + // FIXME check caller equals the owner who initialized the bounty. + let caller = caller(); + let icrc1_ledger_canister_id = + Principal::from_text(MAINNET_ICRC1_LEDGER_CANISTER_ID).unwrap(); + + let amount = deposit_icrc1(caller, icrc1_ledger_canister_id).await?; + return DepositReceipt::Ok(amount); +} + +async fn deposit_icrc1( + caller: Principal, + icrc1_token_canister_id: Principal +) -> Result { + let icrc1_token = ICRC1::new(icrc1_token_canister_id); + let icrc1_token_fee = icrc1_token.get_metadata().await.fee; + + // depends on: + // dfx canister call icrc1_ledger_canister icrc2_approve "(record { amount = 100_000; spender = record{owner = principal \"SPENDER_PRINCIPAL\";} })" + let allowance = icrc1_token.allowance(caller, id()).await; + + let available = allowance - icrc1_token_fee; + + icrc1_token + .transfer_from(caller, id(), available.to_owned()) + .await + .map_err(|_| DepositErr::TransferFailure)?; + + Ok(available) +} diff --git a/bounty/src/api/icrc1.rs b/bounty/src/api/icrc1.rs new file mode 100644 index 0000000..54084a2 --- /dev/null +++ b/bounty/src/api/icrc1.rs @@ -0,0 +1,74 @@ +use candid::{CandidType, Deserialize, Nat, Principal}; +use ic_cdk::api::call::call; + +pub struct ICRC1 { + principal: Principal, +} + +#[derive(CandidType, Debug, PartialEq, Deserialize)] +pub enum TxError { + InsufficientBalance, + InsufficientAllowance, + Unauthorized, + LedgerTrap, + AmountTooSmall, + BlockUsed, + ErrorOperationStyle, + ErrorTo, + Other, +} +pub type TxReceipt = Result; + +#[allow(non_snake_case)] +#[derive(CandidType, Clone, Debug, Deserialize)] +pub struct Metadata { + pub logo: String, + pub name: String, + pub symbol: String, + pub decimals: u8, + pub totalSupply: Nat, + pub owner: Principal, + pub fee: Nat, +} + +// pub const ICRC1_FEE: u64 = 10_000; +pub const MAINNET_ICRC1_LEDGER_CANISTER_ID: &str = "mxzaz-hqaaa-aaaar-qaada-cai"; + +impl ICRC1 { + pub fn new(principal: Principal) -> Self { + ICRC1 { principal } + } + + // pub async fn transfer(&self, target: Principal, amount: Nat) -> TxReceipt { + // let call_result: Result<(TxReceipt,), _> = + // call(self.principal, "icrc1_transfer", (target, amount)).await; + + // call_result.unwrap().0 + // } + + pub async fn transfer_from( + &self, + source: Principal, + target: Principal, + amount: Nat, + ) -> TxReceipt { + let call_result: Result<(TxReceipt,), _> = + call(self.principal, "icrc2_transfer_from", (source, target, amount)).await; + + call_result.unwrap().0 + } + + pub async fn allowance(&self, owner: Principal, spender: Principal) -> Nat { + let call_result: Result<(Nat,), _> = + call(self.principal, "icrc2_allowance", (owner, spender)).await; + + call_result.unwrap().0 + } + + pub async fn get_metadata(&self) -> Metadata { + let call_result: Result<(Metadata,), _> = + call(self.principal, "icrc1_metadata", ()).await; + + call_result.unwrap().0 + } +} \ No newline at end of file diff --git a/bounty/src/api/init.rs b/bounty/src/api/init.rs new file mode 100644 index 0000000..301df18 --- /dev/null +++ b/bounty/src/api/init.rs @@ -0,0 +1,36 @@ +use candid::Principal; +use super::state::{BountyState, BOUNTY_STATE}; + +pub fn init_impl(authority: Principal, github_issue_id: i32) -> () { + BOUNTY_STATE.with(|state| { + *state.borrow_mut() = Some(BountyState { + authority, + github_issue_id, + interested_contributors: Vec::new(), + claimed: false + }); + }); +} + +#[cfg(test)] +mod test_init { + use super::*; + + #[test] + fn test_init() { + BOUNTY_STATE.with(|state| { + let bounty_canister = state.borrow(); + assert!(bounty_canister.is_none()); + }); + + let authority = + Principal::from_text("t2y5w-qp34w-qixaj-s67wp-syrei-5yqse-xbed6-z5nsd-fszmf-izgt2-lqe") + .unwrap(); + + init_impl(authority, 123); + BOUNTY_STATE.with(|state| { + let bounty_canister = state.borrow(); + assert!(bounty_canister.is_some()); + }); + } +} diff --git a/bounty/src/api/state.rs b/bounty/src/api/state.rs new file mode 100644 index 0000000..8a91c01 --- /dev/null +++ b/bounty/src/api/state.rs @@ -0,0 +1,22 @@ + +use candid::{CandidType, Principal}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, CandidType)] +pub struct BountyState { + pub authority: Principal, + pub github_issue_id: i32, + pub interested_contributors: Vec, + pub claimed: bool +} + +#[derive(Debug, Serialize, Deserialize, CandidType)] +pub struct Contributor { + pub address: Principal, + pub crypto_address: String, +} + +// Define thread-local storage for the bounty canister state +thread_local! { + pub static BOUNTY_STATE: std::cell::RefCell> = std::cell::RefCell::new(None); +} diff --git a/bounty/src/lib.rs b/bounty/src/lib.rs index 40eb3da..8586a6f 100644 --- a/bounty/src/lib.rs +++ b/bounty/src/lib.rs @@ -1,103 +1,31 @@ -use candid::{CandidType, Principal}; -use serde::{Deserialize, Serialize}; +use candid::Principal; -#[derive(Debug, Serialize, Deserialize, CandidType)] -pub struct Contributor { - pub address: Principal, - pub crypto_address: String, +mod api { + pub mod state; + pub mod init; + pub mod accept; + pub mod deposit; + pub mod icrc1; } -#[derive(Debug, Serialize, Deserialize, CandidType)] -struct BountyState { - authority: Principal, - github_issue_id: i32, - interested_contributors: Vec, - claimed: bool, -} - -// Define thread-local storage for the bounty canister state -thread_local! { - static BOUNTY_STATE: std::cell::RefCell> = std::cell::RefCell::new(None); -} +use api::state::Contributor; +use api::init::init_impl; +use api::accept::accept_impl; +use api::deposit::{deposit_impl, DepositReceipt}; #[ic_cdk::init] fn init(authority: Principal, github_issue_id: i32) -> () { - BOUNTY_STATE.with(|state| { - *state.borrow_mut() = Some(BountyState { - authority, - github_issue_id, - interested_contributors: Vec::new(), - claimed: false, - }); - }); -} - -#[cfg(test)] -mod test_init { - use super::*; - #[test] - - fn test_init() { - BOUNTY_STATE.with(|state| { - let bounty_canister = state.borrow(); - assert!(bounty_canister.is_none()); - }); - - let authority = - Principal::from_text("t2y5w-qp34w-qixaj-s67wp-syrei-5yqse-xbed6-z5nsd-fszmf-izgt2-lqe") - .unwrap(); - - init(authority, 123); - BOUNTY_STATE.with(|state| { - let bounty_canister = state.borrow(); - assert!(bounty_canister.is_some()); - }); - } + init_impl(authority, github_issue_id); } #[ic_cdk::update] fn accept(contributor: Contributor) -> () { - BOUNTY_STATE.with(|state| { - if let Some(ref mut bounty_canister) = *state.borrow_mut() { - // Add the contributor to the interested contributors list - bounty_canister.interested_contributors.push(contributor); - } - }); + accept_impl(contributor); } -#[cfg(test)] -mod test_accept { - use super::*; - #[test] - fn test_accept() { - let authority = - Principal::from_text("t2y5w-qp34w-qixaj-s67wp-syrei-5yqse-xbed6-z5nsd-fszmf-izgt2-lqe") - .unwrap(); - init(authority, 123); - BOUNTY_STATE.with(|state| { - let bounty_canister = state.borrow(); - if let Some(ref bounty_canister) = *bounty_canister { - assert_eq!(bounty_canister.interested_contributors.len(), 0); - } else { - panic!("Bounty canister state not initialized"); - } - }); - let contributor = - Principal::from_text("t2y5w-qp34w-qixaj-s67wp-syrei-5yqse-xbed6-z5nsd-fszmf-izgt2-lqe") - .unwrap(); - accept(Contributor { - address: contributor, - crypto_address: "contributor_address".to_string(), - }); - BOUNTY_STATE.with(|state| { - let bounty_canister = state.borrow(); - if let Some(ref bounty_canister) = *bounty_canister { - assert_eq!(bounty_canister.interested_contributors.len(), 1); - } else { - panic!("Bounty canister state not initialized"); - } - }); - } +#[ic_cdk::update] +async fn deposit() -> DepositReceipt { + return deposit_impl().await; } #[ic_cdk::update]