Skip to content
Draft
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
make test

rust-daily_planner:
runs-on: ubuntu-24.04
container: ghcr.io/dfinity/icp-dev-env-rust:1.0.0
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
make test
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: 39 additions & 0 deletions rust/daily_planner/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
.PHONY: test

test:
@echo "=== Test 1/6: get_day_data returns null for a date with no notes ==="
@result=$$(icp canister call --query backend get_day_data '("2000-01-15")') && \
echo "$$result" && \
echo "$$result" | grep -q 'null' && \
echo "PASS" || (echo "FAIL" && exit 1)

@echo "=== Test 2/6: add_note returns ok result ==="
@result=$$(icp canister call backend add_note '("2000-01-15", "Buy groceries")') && \
echo "$$result" && \
echo "$$result" | grep -q 'ok' && \
echo "PASS" || (echo "FAIL" && exit 1)

@echo "=== Test 3/6: get_day_data returns stored note ==="
@result=$$(icp canister call --query backend get_day_data '("2000-01-15")') && \
echo "$$result" && \
echo "$$result" | grep -q 'Buy groceries' && \
echo "PASS" || (echo "FAIL" && exit 1)

@echo "=== Test 4/6: get_month_data returns entry for the stored month ==="
@result=$$(icp canister call --query backend get_month_data '(2000 : nat, 1 : nat)') && \
echo "$$result" && \
echo "$$result" | grep -q 'Buy groceries' && \
echo "PASS" || (echo "FAIL" && exit 1)

@echo "=== Test 5/6: complete_note marks note as completed ==="
@icp canister call backend complete_note '("2000-01-15", 0 : nat)' && \
result=$$(icp canister call --query backend get_day_data '("2000-01-15")') && \
echo "$$result" && \
echo "$$result" | grep -q 'is_completed = true' && \
echo "PASS" || (echo "FAIL" && exit 1)

@echo "=== Test 6/6: get_month_data returns empty list for a different month ==="
@result=$$(icp canister call --query backend get_month_data '(1999 : nat, 12 : nat)') && \
echo "$$result" && \
echo "$$result" | grep -q 'vec {}' && \
echo "PASS" || (echo "FAIL" && exit 1)
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
make test
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.
10 changes: 4 additions & 6 deletions rust/daily_planner/backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@ 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-stable-structures = "0.6.5"
serde = "1.0.210"
serde_json = "1.0.138"
serde = "1.0"
serde_json = "1.0"
24 changes: 24 additions & 0 deletions rust/daily_planner/backend/backend.did
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
type DayDataEntry = record {
notes : vec Note;
on_this_day : opt OnThisDay;
};

type Note = record {
id : nat;
content : text;
is_completed : bool;
};

type OnThisDay = record {
title : text;
year : text;
wiki_link : text;
};

service : {
get_day_data : (text) -> (opt DayDataEntry) query;
get_month_data : (nat, nat) -> (vec record { text; DayDataEntry }) query;
add_note : (text, text) -> (variant { ok : text; err : text });
complete_note : (text, nat) -> ();
fetch_and_store_on_this_day : (text) -> (variant { ok : text; err : text });
};
43 changes: 25 additions & 18 deletions rust/daily_planner/backend/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
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,
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 @@ -99,7 +102,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 @@ -154,21 +157,26 @@ async fn fetch_and_store_on_this_day(date: String) -> Result<String, String> {
// 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 {
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.icp0.io.
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 +202,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
Loading
Loading