diff --git a/.github/workflows/ci-backend.yml b/.github/workflows/ci-backend.yml index 511a967..390e64a 100644 --- a/.github/workflows/ci-backend.yml +++ b/.github/workflows/ci-backend.yml @@ -10,6 +10,7 @@ on: - .github/workflows/provision-darwin.sh - .github/workflows/provision-linux.sh - .github/workflows/backend.yaml + - .ic-commit concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/provision-darwin.sh b/.github/workflows/provision-darwin.sh index a29b42b..220d769 100755 --- a/.github/workflows/provision-darwin.sh +++ b/.github/workflows/provision-darwin.sh @@ -11,21 +11,34 @@ bash install-brew.sh rm install-brew.sh # Install Node. -version=${NODE_VERSION:=14.15.4} +version=${NODE_VERSION:=20.12.2} curl --location --output node.pkg "https://nodejs.org/dist/v$version/node-v$version.pkg" sudo installer -pkg node.pkg -store -target / rm node.pkg # Install DFINITY SDK. -curl --location --output install-dfx.sh "https://raw.githubusercontent.com/dfinity/sdk/dfxvm-install-script/install.sh" -DFX_VERSION=${DFX_VERSION:=0.19.0} DFXVM_INIT_YES=true bash install-dfx.sh +curl --location --output install-dfx.sh "https://raw.githubusercontent.com/dfinity/sdk/master/public/install-dfxvm.sh" +DFX_VERSION=${DFX_VERSION:=0.20.0} DFXVM_INIT_YES=true bash install-dfx.sh rm install-dfx.sh echo "$HOME/Library/Application Support/org.dfinity.dfx/bin" >> $GITHUB_PATH source "$HOME/Library/Application Support/org.dfinity.dfx/env" dfx cache install +# check the current ic-commit found in the main branch, check if it differs from the one in this PR branch +# if so, update the dfx cache with the latest ic artifacts +if [ -f "${GITHUB_WORKSPACE}/.ic-commit" ]; then + stable_sha=$(curl https://raw.githubusercontent.com/dfinity/examples/master/.ic-commit) + current_sha=$(sed <"$GITHUB_WORKSPACE/.ic-commit" 's/#.*$//' | sed '/^$/d') + arch="x86_64-darwin" + if [ "$current_sha" != "$stable_sha" ]; then + export current_sha + export arch + sh "$GITHUB_WORKSPACE/.github/workflows/update-dfx-cache.sh" + fi +fi + # Install ic-repl -version=0.1.2 +version=0.7.0 curl --location --output ic-repl "https://github.com/chenyan2002/ic-repl/releases/download/$version/ic-repl-macos" mv ./ic-repl /usr/local/bin/ic-repl chmod a+x /usr/local/bin/ic-repl @@ -54,4 +67,4 @@ mv "${HOME}/bin/wasmtime-v${wasmtime_version}-x86_64-macos/wasmtime" "${HOME}/bi rm "wasmtime-v${wasmtime_version}-x86_64-macos.tar.xz" # Exit temporary directory. -popd +popd \ No newline at end of file diff --git a/.github/workflows/provision-linux.sh b/.github/workflows/provision-linux.sh index 507527f..f4eadac 100755 --- a/.github/workflows/provision-linux.sh +++ b/.github/workflows/provision-linux.sh @@ -6,21 +6,33 @@ set -ex pushd /tmp # Install Node. -wget --output-document install-node.sh "https://deb.nodesource.com/setup_14.x" +wget --output-document install-node.sh "https://deb.nodesource.com/setup_20.x" sudo bash install-node.sh sudo apt-get install --yes nodejs rm install-node.sh # Install DFINITY SDK. -wget --output-document install-dfx.sh "https://raw.githubusercontent.com/dfinity/sdk/dfxvm-install-script/install.sh" -DFX_VERSION=${DFX_VERSION:=0.19.0} DFXVM_INIT_YES=true bash install-dfx.sh +wget --output-document install-dfx.sh "https://raw.githubusercontent.com/dfinity/sdk/master/public/install-dfxvm.sh" +DFX_VERSION=${DFX_VERSION:=0.20.0} DFXVM_INIT_YES=true bash install-dfx.sh rm install-dfx.sh echo "$HOME/.local/share/dfx/bin" >> $GITHUB_PATH source "$HOME/.local/share/dfx/env" dfx cache install +# check the current ic-commit found in the main branch, check if it differs from the one in this PR branch +# if so, update the dfx cache with the latest ic artifacts +if [ -f "${GITHUB_WORKSPACE}/.ic-commit" ]; then + stable_sha=$(curl https://raw.githubusercontent.com/dfinity/examples/master/.ic-commit) + current_sha=$(sed <"$GITHUB_WORKSPACE/.ic-commit" 's/#.*$//' | sed '/^$/d') + arch="x86_64-linux" + if [ "$current_sha" != "$stable_sha" ]; then + export current_sha + export arch + sh "$GITHUB_WORKSPACE/.github/workflows/update-dfx-cache.sh" + fi +fi # Install ic-repl -version=0.1.2 +version=0.7.0 curl --location --output ic-repl "https://github.com/chenyan2002/ic-repl/releases/download/$version/ic-repl-linux64" mv ./ic-repl /usr/local/bin/ic-repl chmod a+x /usr/local/bin/ic-repl @@ -53,4 +65,4 @@ echo "$HOME/bin" >> $GITHUB_PATH echo "$HOME/.cargo/bin" >> $GITHUB_PATH # Exit temporary directory. -popd +popd \ No newline at end of file diff --git a/.ic_commit b/.ic_commit new file mode 100644 index 0000000..8b38a12 --- /dev/null +++ b/.ic_commit @@ -0,0 +1 @@ +63acf4f88b20ec0c6384f4e18f0f6f69fc5d9b9f \ No newline at end of file diff --git a/Makefile b/Makefile index e2684d7..fdf0e81 100644 --- a/Makefile +++ b/Makefile @@ -97,9 +97,9 @@ test-4: # install cat $$TMP_FILE; \ rm -f $$TMP_FILE -.PHONY: test-a -.SILENT: test-a -test-a: # install +.PHONY: test +.SILENT: test +test: install # Call the backend canister for healthcheck and capture the output @echo "Calling healthcheck on backend canister..." @TMP_FILE=$$(mktemp); \ diff --git a/README.md b/README.md index 2ea881d..058a349 100644 --- a/README.md +++ b/README.md @@ -26,4 +26,4 @@ dfx start --background # Deploys your canisters to the replica and generates your candid interface dfx deploy -``` +``` \ No newline at end of file diff --git a/backend/backend.did b/backend/backend.did index 27ac4a8..d11b7fe 100644 --- a/backend/backend.did +++ b/backend/backend.did @@ -71,6 +71,12 @@ type User = record { type GithubToken = text; +type GithubIssueId = text; + +type GithubPullRequestId = text; + +type BountyAmount = nat32; + // Bounty Service type Contributor = record { @@ -88,22 +94,31 @@ type DepositReceipt = variant { }; type AcceptErr = variant { - IssueNotFound : record { github_issue_id : text }; - CantAcceptedTwice; + IssueNotFound : record { GithubIssueId }; }; type AcceptReceipt = opt AcceptErr; +type RegisterIssueErr = null; + +type RegisterIssueReceipt = opt RegisterIssueErr; + +type UnRegisterIssueErr = null; + +type UnRegisterIssueReceipt = opt UnRegisterIssueErr; + service : (authority: principal) -> { + // GitHub Service "get_issue": (GithubToken) -> (IssueReceipt); "get_fixed_by": (GithubToken) -> (FixedByReceipt); "get_is_merged": (GithubToken) -> (IsMergedReceipt); "get_merged_details": (GithubToken) -> (MergeDetailsReceipt); + // Bounty Service "healthcheck": () -> (text); - "accept": (Contributor, github_issue_id: text, github_pr_id: text) -> (AcceptReceipt); + "accept": (Contributor, GithubIssueId, GithubPullRequestId) -> (AcceptReceipt); "deposit": () -> (DepositReceipt); -} - - + "register_issue": (Contributor, GithubIssueId, BountyAmount) -> (RegisterIssueReceipt); + "unregister_issue": (GithubIssueId) -> (UnRegisterIssueReceipt); +} \ No newline at end of file diff --git a/backend/src/bounty/api/claim.rs b/backend/src/bounty/api/claim.rs index c644053..bd22682 100644 --- a/backend/src/bounty/api/claim.rs +++ b/backend/src/bounty/api/claim.rs @@ -184,7 +184,7 @@ fn extract_regex(regex: &str, str: &str) -> Option { let error_message = format!("Error (regex): {}", err); print!("{}", error_message); None - }, + } Ok(re) => { if let Some(captures) = re.captures(str) { if let Some(number) = captures.get(1) { diff --git a/backend/src/bounty/api/register_issue.rs b/backend/src/bounty/api/register_issue.rs new file mode 100644 index 0000000..1f03f91 --- /dev/null +++ b/backend/src/bounty/api/register_issue.rs @@ -0,0 +1,112 @@ +use std::collections::HashMap; + +use super::state::IssueId; +use super::state::{Bounty, Contributor, Issue, BOUNTY_STATE}; + +use candid::Nat; + +pub type RegisterIssueError = (); + +pub type RegisterIssueReceipt = Option; + +pub fn register_issue_impl( + contributor: Contributor, + github_issue_id: IssueId, + amount: Nat, +) -> RegisterIssueReceipt { + return BOUNTY_STATE.with(|state| { + if let Some(ref mut bounty_canister) = *state.borrow_mut() { + let issue_exists = bounty_canister.github_issues.contains_key(&github_issue_id); + if !issue_exists { + let github_issue = Issue { + id: github_issue_id.clone(), + maintainer: contributor, + bounty: Bounty { + amount: amount, + winner: None, + accepted_prs: HashMap::new(), + }, + }; + // TODO: Check contributor it's registered and github_issue_id exists on github + bounty_canister + .github_issues + .insert(github_issue_id.clone(), github_issue); + } + None + } else { + panic!("Bounty canister state not initialized") + } + }); +} + +#[cfg(test)] +mod test_register_issue { + use super::*; + use crate::bounty::api::init::init_impl; + use candid::{Nat, Principal}; + use num_bigint::BigUint; + + #[test] + fn test_register_issue() { + let authority = Principal::anonymous(); + + init_impl(authority); + + let github_issue_id = "input-output-hk/hydra/issues/1370".to_string(); + + let contributor = Contributor { + address: Principal::anonymous(), + crypto_address: "0x1234".to_string(), + }; + + let bounty_amount: Nat = Nat(BigUint::from(100u32)); + + let r: Option = + register_issue_impl(contributor, github_issue_id.clone(), bounty_amount); + + assert!(r.is_none()); + + BOUNTY_STATE.with(|state| { + let bounty_canister = state.borrow(); + if let Some(ref bounty_canister) = *bounty_canister { + assert!(bounty_canister + .github_issues + .get(&github_issue_id) + .is_some()); + } else { + panic!("Bounty canister state not initialized"); + } + }); + } + #[test] + fn test_cant_register_issue_twice() { + let authority = Principal::anonymous(); + + init_impl(authority); + + let github_issue_id = "input-output-hk/hydra/issues/1370".to_string(); + + let contributor = Contributor { + address: Principal::anonymous(), + crypto_address: "0x1234".to_string(), + }; + + let bounty_amount: Nat = Nat(BigUint::from(100u32)); + + let r: Option = register_issue_impl( + contributor.clone(), + github_issue_id.clone(), + bounty_amount.clone(), + ); + + assert!(r.is_none()); + + let r2: Option = register_issue_impl( + contributor.clone(), + github_issue_id.clone(), + bounty_amount.clone(), + ); + + assert!(r2.is_none()); + } +} diff --git a/backend/src/bounty/api/state.rs b/backend/src/bounty/api/state.rs index 64126be..38c4ecb 100644 --- a/backend/src/bounty/api/state.rs +++ b/backend/src/bounty/api/state.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use candid::{CandidType, Principal}; +use candid::{CandidType, Nat, Principal}; use serde::{Deserialize, Serialize}; pub type IssueId = String; @@ -20,7 +20,7 @@ pub struct PullRequest { #[derive(Debug, Serialize, Deserialize, CandidType, Clone, Builder)] pub struct Bounty { - pub amount: i32, + pub amount: Nat, pub winner: Option, pub accepted_prs: HashMap, } @@ -35,7 +35,7 @@ pub struct Issue { #[derive(Debug, Serialize, Deserialize, CandidType)] pub struct BountyState { pub authority: Principal, - pub github_issues: HashMap, + pub github_issues: HashMap, } // Define thread-local storage for the bounty canister state // WASM is single-threaded by nature. [RefCell] and [thread_local!] are used despite being not totally safe primitives. @@ -44,7 +44,7 @@ pub struct BountyState { // Here we use [thread_local!] because it is simpler. thread_local! { // Currently, a single canister smart contract is limited to 4 GB of storage due to WebAssembly limitations. - // To ensure that our canister does not exceed this limit, we restrict memory usage to at most 2 GB because + // To ensure that our canister does not exceed this limit, we restrict memory usage to at most 2 GB because // up to 2x memory may be needed for data serialization during canister upgrades. pub static BOUNTY_STATE: std::cell::RefCell> = std::cell::RefCell::new(None); } diff --git a/backend/src/bounty/api/unregister_issue.rs b/backend/src/bounty/api/unregister_issue.rs new file mode 100644 index 0000000..0bd621c --- /dev/null +++ b/backend/src/bounty/api/unregister_issue.rs @@ -0,0 +1,99 @@ +use super::state::BOUNTY_STATE; +use crate::IssueId; +pub type UnRegisterIssueError = (); + +pub type UnRegisterIssueReceipt = Option; + +pub fn unregister_issue_impl(github_issue_id: IssueId) -> UnRegisterIssueReceipt { + return BOUNTY_STATE.with(|state| { + if let Some(ref mut bounty_canister) = *state.borrow_mut() { + let issue_exists = bounty_canister.github_issues.contains_key(&github_issue_id); + + if issue_exists { + // TODO: Check contributor it's registered and github_issue_id exists on github + bounty_canister.github_issues.remove(&github_issue_id); + } + None + } else { + panic!("Bounty canister state not initialized") + } + }); +} + +#[cfg(test)] +mod test_unregister_issue { + use super::*; + use crate::bounty::api::init::init_impl; + use crate::bounty::api::register_issue::RegisterIssueError; + use crate::bounty::api::state::{Contributor, BOUNTY_STATE}; + use crate::register_issue_impl; + use candid::{Nat, Principal}; + use num_bigint::BigUint; + + #[test] + fn test_unregister_issue() { + let authority = Principal::anonymous(); + + init_impl(authority); + + let github_issue_id = "input-output-hk/hydra/issues/1370".to_string(); + + let contributor = Contributor { + address: Principal::anonymous(), + crypto_address: "0x1234".to_string(), + }; + + let bounty_amount: Nat = Nat(BigUint::from(100u32)); + + let r: Option = register_issue_impl( + contributor.clone(), + github_issue_id.clone(), + bounty_amount.clone(), + ); + + assert!(r.is_none()); + let r2: Option = unregister_issue_impl(github_issue_id.clone()); + assert!(r2.is_none()); + + BOUNTY_STATE.with(|state| { + let bounty_canister = state.borrow(); + if let Some(ref bounty_canister) = *bounty_canister { + assert!(bounty_canister + .github_issues + .get(&github_issue_id) + .is_none()); + } else { + panic!("Bounty canister state not initialized"); + } + }); + } + + #[test] + fn test_unregister_issue_twice() { + let authority = Principal::anonymous(); + + init_impl(authority); + + let github_issue_id = "input-output-hk/hydra/issues/1370".to_string(); + + let contributor = Contributor { + address: Principal::anonymous(), + crypto_address: "0x1234".to_string(), + }; + + let bounty_amount: Nat = Nat(BigUint::from(100u32)); + + let r: Option = register_issue_impl( + contributor.clone(), + github_issue_id.clone(), + bounty_amount.clone(), + ); + + assert!(r.is_none()); + let r2: Option = unregister_issue_impl(github_issue_id.clone()); + assert!(r2.is_none()); + + let r3: Option = unregister_issue_impl(github_issue_id.clone()); + assert!(r3.is_none()); + } +} diff --git a/backend/src/lib.rs b/backend/src/lib.rs index 6b2a569..1108586 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -1,7 +1,7 @@ #[macro_use] extern crate derive_builder; -use candid::Principal; +use candid::{Nat, Principal}; pub mod provider { pub mod github { @@ -28,14 +28,18 @@ pub mod bounty { pub mod deposit; pub mod icrc1; pub mod init; + pub mod register_issue; pub mod state; + pub mod unregister_issue; } } use bounty::api::accept::{accept_impl, AcceptReceipt}; -use bounty::api::init::init_impl; use bounty::api::deposit::{deposit_impl, DepositReceipt}; -use bounty::api::state::Contributor; +use bounty::api::init::init_impl; +use bounty::api::register_issue::{register_issue_impl, RegisterIssueReceipt}; +use bounty::api::state::{Contributor, IssueId, PullRequestId}; +use bounty::api::unregister_issue::{unregister_issue_impl, UnRegisterIssueReceipt}; // GITHUB SERVICE #[ic_cdk::update] @@ -97,7 +101,11 @@ fn init(authority: Principal) -> () { } #[ic_cdk::update] -fn accept(contributor: Contributor, github_issue_id: String, github_pr_id: String) -> AcceptReceipt { +fn accept( + contributor: Contributor, + github_issue_id: IssueId, + github_pr_id: PullRequestId, +) -> AcceptReceipt { return accept_impl(contributor, github_issue_id, github_pr_id); } @@ -110,3 +118,17 @@ async fn deposit() -> DepositReceipt { async fn healthcheck() -> String { return "OK".to_string(); } + +#[ic_cdk::update] +fn register_issue( + contributor: Contributor, + github_issue_id: IssueId, + amount: Nat, +) -> RegisterIssueReceipt { + return register_issue_impl(contributor, github_issue_id, amount); +} + +#[ic_cdk::update] +fn unregister_issue(github_issue_id: IssueId) -> UnRegisterIssueReceipt { + return unregister_issue_impl(github_issue_id); +} diff --git a/package.json b/package.json index de8412f..5a37812 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "webpack-dev-server": "^4.8.1" }, "engines": { - "node": "^12 || ^14 || ^16 || ^18" + "node": "16 || ^18 || ^20" }, "browserslist": [ "last 2 chrome version",