Skip to content

Commit c00d48f

Browse files
docs: canister calls example
Signed-off-by: David Dal Busco <david.dalbusco@outlook.com>
1 parent 0617ffd commit c00d48f

2 files changed

Lines changed: 262 additions & 1 deletion

File tree

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
---
2+
title: Making Canister Calls in Rust Serverless Functions
3+
description: An example showing how to call external canisters (e.g., ICRC ledger) from a serverless function written in Rust using Juno Satellites.
4+
keywords:
5+
[
6+
rust,
7+
canister call,
8+
transfer_from,
9+
icrc,
10+
icp,
11+
serverless,
12+
juno,
13+
satellite,
14+
example
15+
]
16+
sidebar_label: Canister Calls
17+
---
18+
19+
# Making Canister Calls in Rust Serverless Functions
20+
21+
This example demonstrates how to use **Rust serverless functions** to perform canister calls (such as `transfer_from` on the ICP ledger) in response to Datastore events in your Juno **Satellite**.
22+
23+
When a document is added to the `request` collection, a serverless function is triggered to:
24+
25+
- Check if the user has enough ICP in their wallet
26+
- Transfer ICP from the user's wallet to the Satellite using the ICRC ledger's `transfer_from` method
27+
- Mark the request as `processed` if the transfer succeeds
28+
29+
This pattern is useful for building workflows that require on-chain asset transfers or other canister calls in response to user actions.
30+
31+
You can browse the source code here: [github.com/junobuild/examples/tree/main/rust/calls](https://github.com/junobuild/examples/tree/main/rust/calls)
32+
33+
---
34+
35+
## Folder Structure
36+
37+
```
38+
rust/calls/
39+
├── src/
40+
│ ├── satellite/ # Rust Satellite serverless function
41+
│ │ ├── src/
42+
│ │ │ ├── lib.rs # Main Rust logic for Satellite
43+
│ │ │ ├── services.rs # Helper logic for balance, transfer, status
44+
│ │ │ ├── types.rs # Data model for requests
45+
│ │ │ ├── ledger_icrc.rs # Ledger helper functions
46+
│ │ │ └── ...
47+
│ │ ├── satellite.did # Candid interface definition
48+
│ │ └── Cargo.toml # Rust package config
49+
│ ├── declarations/ # TypeScript declarations for Satellite
50+
│ ├── components/ # React frontend components
51+
│ ├── services/ # Frontend service logic
52+
│ ├── types/ # Frontend type definitions
53+
│ ├── main.tsx # Frontend entry
54+
│ └── ...
55+
├── juno.config.ts # Juno Satellite configuration
56+
├── package.json # Frontend dependencies
57+
└──
58+
```
59+
60+
---
61+
62+
## Key Features
63+
64+
- **Serverless Canister Calls**: Demonstrates how to perform ICRC ledger calls (e.g., `transfer_from`) from Rust serverless functions.
65+
- **Atomic Request Processing**: Ensures that request status is only updated if the transfer succeeds.
66+
- **Wallet Balance Checks**: Fails early if the user does not have enough ICP.
67+
- **Minimal React UI**: A simple React frontend is included to test and demonstrate the logic.
68+
69+
---
70+
71+
## Main Backend Components
72+
73+
- **src/satellite/src/lib.rs**: The entry point for the Satellite serverless function. Triggers the canister call and updates request status on document set.
74+
- **src/satellite/src/services.rs**: Helper logic for checking wallet balance, performing the transfer, and updating request status.
75+
- **src/satellite/src/types.rs**: Data model for requests and status.
76+
- **src/satellite/Cargo.toml**: Rust package configuration for the Satellite function.
77+
78+
---
79+
80+
## Example: Canister Call on Document Set
81+
82+
Here’s the actual Rust logic from `lib.rs` and `services.rs`:
83+
84+
```rust
85+
// src/satellite/src/lib.rs
86+
mod env;
87+
mod ledger_icrc;
88+
mod services;
89+
mod types;
90+
mod utils;
91+
92+
use crate::services::{assert_wallet_balance, set_request_processed, transfer_icp_from_wallet};
93+
use crate::types::RequestData;
94+
use crate::utils::icp_ledger_id;
95+
use ic_cdk::id;
96+
use icrc_ledger_types::icrc1::account::Account;
97+
use junobuild_macros::on_set_doc;
98+
use junobuild_satellite::{include_satellite, OnSetDocContext};
99+
use junobuild_utils::decode_doc_data;
100+
101+
// Triggered when a new document is set in the "request" collection
102+
#[on_set_doc(collections = ["request"])]
103+
async fn on_set_doc(context: OnSetDocContext) -> Result<(), String> {
104+
// Init data
105+
let data: RequestData = decode_doc_data(&context.data.data.after.data)?;
106+
let request_amount = data.amount.value;
107+
let fee = data.fee.as_ref().map(|fee| fee.value);
108+
let ledger_id = icp_ledger_id()?;
109+
let from_account: Account = Account::from(context.caller);
110+
111+
// Check current account balance
112+
assert_wallet_balance(&ledger_id, &from_account, &request_amount, &fee).await?;
113+
114+
// Update request status to processed (atomic with transfer)
115+
set_request_processed(context.data.key, &data, &context.data.data.after.version)?;
116+
117+
// Transfer from wallet to satellite
118+
let to_account: Account = Account::from(id());
119+
transfer_icp_from_wallet(
120+
&ledger_id,
121+
&from_account,
122+
&to_account,
123+
&request_amount,
124+
&fee,
125+
)
126+
.await?;
127+
128+
Ok(())
129+
}
130+
131+
include_satellite!();
132+
```
133+
134+
```rust
135+
// src/satellite/src/services.rs
136+
/// Asserts that the given account has enough balance to cover the amount and fee.
137+
pub async fn assert_wallet_balance(
138+
ledger_id: &Principal,
139+
from_account: &Account,
140+
amount: &u64,
141+
fee: &Option<u64>,
142+
) -> Result<(), String> {
143+
let balance = icrc_balance_of(&ledger_id, &from_account).await?;
144+
let total = amount.saturating_add(fee.unwrap_or(10_000u64));
145+
if balance < total {
146+
return Err(format!("Balance {} is smaller than {}", balance, total));
147+
}
148+
Ok(())
149+
}
150+
151+
/// Transfers ICP from one account to another using `icrc2_transfer_from`.
152+
pub async fn transfer_icp_from_wallet(
153+
ledger_id: &Principal,
154+
from_account: &Account,
155+
to_account: &Account,
156+
amount: &u64,
157+
fee: &Option<u64>,
158+
) -> Result<(), String> {
159+
let result = icrc_transfer_from(
160+
&ledger_id,
161+
&from_account,
162+
&to_account,
163+
&Nat::from(amount.clone()),
164+
&fee.map(|fee| Nat::from(fee)),
165+
)
166+
.await
167+
.map_err(|e| format!("Failed to call ICRC ledger icrc_transfer_from: {:?}", e))
168+
.and_then(|result| {
169+
result.map_err(|e| format!("Failed to execute the transfer from: {:?}", e))
170+
})?;
171+
print(format!("Result of the transfer from is {:?}", result));
172+
Ok(())
173+
}
174+
175+
/// Updates the request document status to `Processed`.
176+
pub fn set_request_processed(
177+
key: String,
178+
original_data: &RequestData,
179+
original_version: &Option<u64>,
180+
) -> Result<(), String> {
181+
let update_data: RequestData = RequestData {
182+
status: RequestStatus::Processed,
183+
..original_data.clone()
184+
};
185+
let data = encode_doc_data(&update_data)?;
186+
let doc: SetDoc = SetDoc {
187+
data,
188+
description: None,
189+
version: original_version.clone(),
190+
};
191+
let _ = set_doc_store(id(), "request".to_string(), key, doc)?;
192+
Ok(())
193+
}
194+
```
195+
196+
**Explanation:**
197+
198+
- When a request is submitted, the `on_set_doc` hook is triggered for the `request` collection.
199+
- The function checks the user's wallet balance, updates the request status, and performs the ICP transfer atomically.
200+
- If any step fails, the entire operation is reverted.
201+
- The frontend can monitor request status and balances via the exposed APIs.
202+
203+
---
204+
205+
## How to Run
206+
207+
1. **Clone the repo**:
208+
209+
```bash
210+
git clone https://github.com/junobuild/examples
211+
cd rust/calls
212+
```
213+
214+
import HowToStart from "../../components/how-to-start.mdx";
215+
216+
<HowToStart index={2} />
217+
218+
import CreateSatellite from "../../components/create-a-satellite.mdx";
219+
220+
<CreateSatellite index={4} />
221+
222+
5. **Create required collections**:
223+
224+
- `request` in Datastore: [http://localhost:5866/datastore](http://localhost:5866/datastore)
225+
226+
import HowToRun from "../components/how-to-run.md";
227+
228+
<HowToRun />
229+
230+
---
231+
232+
import Config from "../components/config.md";
233+
234+
<Config />
235+
236+
---
237+
238+
import ProdDeploy from "../components/prod-deploy.md";
239+
240+
<ProdDeploy />
241+
242+
---
243+
244+
## Notes
245+
246+
- This example focuses on the Rust serverless function and canister call integration. The frontend is intentionally minimal and included only for demonstration.
247+
- Use this project as a starting point for workflows that require on-chain asset transfers or canister calls in response to user actions.
248+
249+
import ProposalsNetwork from "./components/proposals-network.md";
250+
251+
<ProposalsNetwork />
252+
253+
---
254+
255+
import References from "./components/references.md";
256+
257+
<References />* [icrc-ledger-types](https://docs.rs/icrc-ledger-types): Types
258+
for interacting with the ICRC ledger standard. *
259+
[ic-cdk](https://docs.rs/ic-cdk): Internet Computer canister development kit for
260+
Rust.

sidebars.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,8 @@ const sidebars: SidebarsConfig = {
122122
items: [
123123
"examples/functions/rust/assertion",
124124
"examples/functions/rust/mutating-docs-with-hooks",
125-
"examples/functions/rust/generating-assets-with-hooks"
125+
"examples/functions/rust/generating-assets-with-hooks",
126+
"examples/functions/rust/canister-calls"
126127
]
127128
}
128129
]

0 commit comments

Comments
 (0)