Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/daily_planner.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
pull_request:
paths:
- motoko/daily_planner/**
- rust/daily_planner/**
- .github/workflows/daily_planner.yml

concurrency:
Expand All @@ -27,3 +28,17 @@ jobs:
icp network start -d
icp deploy
bash test.sh

rust-daily_planner:
runs-on: ubuntu-24.04
container: ghcr.io/dfinity/icp-dev-env-rust:1.0.1
env:
ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Deploy and test
working-directory: rust/daily_planner
run: |
icp network start -d
icp deploy
bash test.sh
20 changes: 0 additions & 20 deletions rust/daily_planner/.devcontainer/devcontainer.json

This file was deleted.

113 changes: 0 additions & 113 deletions rust/daily_planner/BUILD.md

This file was deleted.

39 changes: 27 additions & 12 deletions rust/daily_planner/README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,42 @@
# Daily planner
# Daily Planner

Daily planner features a monthly calender that can be used to track daily activities, appointments, or tasks. Data for each task is stored onchain. For each day, a historic fact can be queried using HTTPS outcalls, which is a feature that allows ICP canisters to obtain data from external sources.
Daily Planner is a full-stack ICP example featuring a monthly calendar that tracks daily notes and tasks stored on the network. For each day, a historic fact can be fetched from an external API using HTTPS outcalls, demonstrating how ICP canisters can access data from external services.

## Deploying from ICP Ninja
## Build and deploy from the command line

When viewing this project in ICP Ninja, you can deploy it directly to the mainnet for free by clicking "Run" in the upper right corner. Open this project in ICP Ninja:
### Prerequisites

[![](https://icp.ninja/assets/open.svg)](https://icp.ninja/i?g=https://github.com/dfinity/examples/rust/daily_planner)
- Node.js
- icp-cli: `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm`

## Build and deploy from the command-line
### Install

### 1. [Download and install the IC SDK.](https://internetcomputer.org/docs/building-apps/getting-started/install)
```bash
git clone https://github.com/dfinity/examples
cd examples/rust/daily_planner
```

### 2. Download your project from ICP Ninja using the 'Download files' button on the upper left corner, or [clone the GitHub examples repository.](https://github.com/dfinity/examples/)
### Deploy and test

### 3. Navigate into the project's directory.
```bash
icp network start -d
icp deploy
bash test.sh
icp network stop
```

### 4. Deploy the project to your local environment:
To run the frontend in development mode with hot reload:

```bash
npm run dev
```
dfx start --background --clean && dfx deploy

## Updating the Candid interface

```bash
icp build backend && candid-extractor target/wasm32-unknown-unknown/release/backend.wasm > backend/backend.did
```

## Security considerations and best practices

If you base your application on this example, it is recommended that you familiarize yourself with and adhere to the [security best practices](https://internetcomputer.org/docs/building-apps/security/overview) for developing on ICP. This example may not implement all the best practices.
Refer to the [security best practices](https://docs.internetcomputer.org/guides/security/overview) for information on security and best practices for your ICP app.
11 changes: 5 additions & 6 deletions rust/daily_planner/backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@ name = "backend"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
crate-type = ["cdylib"]
path = "lib.rs"

[dependencies]
candid = "0.10.10"
ic-cdk = "0.16.0"
candid = "0.10"
ic-cdk = "0.20"
ic-cdk-management-canister = "0.1.1"
ic-stable-structures = "0.6.5"
serde = "1.0.210"
serde_json = "1.0.138"
serde = "1.0"
serde_json = "1.0"
11 changes: 11 additions & 0 deletions rust/daily_planner/backend/backend.did
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
type DayDataEntry = record { on_this_day : opt OnThisDay; notes : vec Note };
type Note = record { id : nat; content : text; is_completed : bool };
type OnThisDay = record { title : text; year : text; wiki_link : text };
type Result = variant { Ok : text; Err : text };
service : {
add_note : (text, text) -> (Result);
complete_note : (text, nat) -> ();
fetch_and_store_on_this_day : (text) -> (Result);
get_day_data : (text) -> (opt DayDataEntry) query;
get_month_data : (nat, nat) -> (vec record { text; DayDataEntry }) query;
}
51 changes: 30 additions & 21 deletions rust/daily_planner/backend/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use candid::{CandidType, Decode, Deserialize, Encode, Nat};
use ic_cdk::api::management_canister::http_request::{
http_request, CanisterHttpRequestArgument, HttpMethod, HttpResponse, TransformArgs,
TransformContext,
use ic_cdk::api::canister_self;
use ic_cdk_management_canister::{
http_request, HttpMethod, HttpRequestArgs, HttpRequestResult, TransformArgs, TransformContext,
TransformFunc,
};
use ic_stable_structures::memory_manager::{MemoryId, MemoryManager, VirtualMemory};
use ic_stable_structures::storable::{Bound, Storable};
Expand Down Expand Up @@ -75,8 +76,12 @@ fn get_day_data(date: String) -> Option<DayDataEntry> {
// Query function to get data for a full month.
#[ic_cdk::query]
fn get_month_data(year: Nat, month: Nat) -> Vec<(String, DayDataEntry)> {
// `Nat`s display with '_' as thousand separators.
let month_prefix = format!("{}-{}-", year, month).replace('_', "");
// Strip Nat thousand-separator underscores and zero-pad month to 2 digits
// to match the ISO date format used as keys (e.g. "2024-01-15").
let year_str = format!("{}", year).replace('_', "");
let month_raw = format!("{}", month).replace('_', "");
let month_str = format!("{:0>2}", month_raw);
let month_prefix = format!("{}-{}-", year_str, month_str);
STATE.with(|s| {
s.borrow()
.day_data_entries
Expand All @@ -99,7 +104,7 @@ fn add_note(date: String, content: String) -> Result<String, String> {
};
day_data.notes.push(new_note);
state.day_data_entries.insert(date.clone(), day_data);
Ok(format!("Added not for date: {date}"))
Ok(format!("Added note for date: {date}"))
// Currently returns no errors, but could be extended to e.g. reject creation of notes in the past.
})
}
Expand Down Expand Up @@ -153,22 +158,27 @@ async fn fetch_and_store_on_this_day(date: String) -> Result<String, String> {
// TransformContext is used to specify how the HTTP response is processed before consensus tries to agree on a response.
// This is useful to e.g. filter out timestamps/sessionIDs out of headers that will be different across the responses the different replicas receive.
// If the data (including status, headers and body) they receive does not match across the nodes, the canister will reject the response!
// You can read more about it here: https://internetcomputer.org/docs/current/developer-docs/smart-contracts/advanced-features/https-outcalls/https-outcalls-how-to-use.
let transform_context = TransformContext::from_name("transform".to_string(), vec![]);
let request = CanisterHttpRequestArgument {
// You can read more about it here: https://docs.internetcomputer.org/guides/backends/https-outcalls.
let request = HttpRequestArgs {
url,
method: HttpMethod::GET,
body: None,
max_response_bytes: None, // Can be set to limit cost. Our response has no predictable size, so we set no limit.
headers: vec![],
transform: Some(transform_context),
transform: Some(TransformContext {
function: TransformFunc::new(canister_self(), "transform".to_string()),
context: vec![],
}),
// Replicated mode: all subnet nodes make the request independently,
// providing strong integrity guarantees via consensus.
is_replicated: Some(true),
};

// Perform HTTPS outcall using roughly 100B cycles.
// See https outcall cost calculator: https://7joko-hiaaa-aaaal-ajz7a-cai.icp0.io.
// Perform HTTPS outcall. Cycles are automatically calculated and attached.
// Unused cycles are returned.
let quote = match http_request(request, 100_000_000_000).await {
Ok((response,)) => {
// See https outcall cost calculator: https://7joko-hiaaa-aaaal-ajz7a-cai.icp.net.
let quote = match http_request(&request).await {
Ok(response) => {
let body_string =
String::from_utf8(response.body).expect("Response is not UTF-8 encoded.");
let Some(otd) = http_response_to_on_this_day(&body_string) else {
Expand All @@ -194,17 +204,16 @@ async fn fetch_and_store_on_this_day(date: String) -> Result<String, String> {
}

// Query function to turn the raw HTTP responses into responses that nodes can run consensus on.
#[ic_cdk::query]
fn transform(raw: TransformArgs) -> HttpResponse {
HttpResponse {
status: raw.response.status,
body: raw.response.body,
headers: vec![], // We filter out the headers, as they don't match accross nodes.
#[ic_cdk::query(hidden = true)]
fn transform(raw: TransformArgs) -> HttpRequestResult {
HttpRequestResult {
headers: vec![], // We filter out the headers, as they don't match across nodes.
..raw.response
}
}

fn http_response_to_on_this_day(http: &str) -> Option<OnThisDay> {
let json: serde_json::Value = serde_json::from_str(&http).ok()?;
let json: serde_json::Value = serde_json::from_str(http).ok()?;
let title = json["events"][0]["description"].as_str()?;
let year = json["events"][0]["year"].as_str()?;
let wiki_link = json["events"][0]["wikipedia"][0]["wikipedia"].as_str()?;
Expand Down
29 changes: 0 additions & 29 deletions rust/daily_planner/dfx.json

This file was deleted.

6 changes: 3 additions & 3 deletions rust/daily_planner/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"private": true,
"type": "module",
"scripts": {
"prebuild": "npm i --include=dev && dfx generate backend",
"prebuild": "npm i --include=dev",
"build": "vite build",
"dev": "vite"
},
Expand All @@ -13,10 +13,10 @@
"react-dom": "18.3.1"
},
"devDependencies": {
"@icp-sdk/bindgen": "~0.2.2",
"@types/react": "18.3.12",
"@types/react-dom": "18.3.1",
"@vitejs/plugin-react": "4.3.3",
"vite": "5.4.11",
"vite-plugin-environment": "1.1.3"
"vite": "5.4.11"
}
}
Loading
Loading