Making Canister Calls in Rust Serverless Functions
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.
When a document is added to the request
collection, a serverless function is triggered to:
- Check if the user has enough ICP in their wallet
- Transfer ICP from the user's wallet to the Satellite using the ICRC ledger's
transfer_from
method - Mark the request as
processed
if the transfer succeeds
This pattern is useful for building workflows that require on-chain asset transfers or other canister calls in response to user actions.
You can browse the source code here: github.com/junobuild/examples/tree/main/functions/rust/calls
Folder Structure
rust/calls/
├── src/
│ ├── satellite/ # Rust Satellite serverless function
│ │ ├── src/
│ │ │ ├── lib.rs # Main Rust logic for Satellite
│ │ │ ├── services.rs # Helper logic for balance, transfer, status
│ │ │ ├── types.rs # Data model for requests
│ │ │ ├── ledger_icrc.rs # Ledger helper functions
│ │ │ └── ...
│ │ ├── satellite.did # Candid interface definition
│ │ └── Cargo.toml # Rust package config
│ ├── declarations/ # TypeScript declarations for Satellite
│ ├── components/ # React frontend components
│ ├── services/ # Frontend service logic
│ ├── types/ # Frontend type definitions
│ ├── main.tsx # Frontend entry
│ └── ...
├── juno.config.ts # Juno Satellite configuration
├── package.json # Frontend dependencies
└──
Key Features
- Serverless Canister Calls: Demonstrates how to perform ICRC ledger calls (e.g.,
transfer_from
) from Rust serverless functions. - Atomic Request Processing: Ensures that request status is only updated if the transfer succeeds.
- Wallet Balance Checks: Fails early if the user does not have enough ICP.
- Minimal React UI: A simple React frontend is included to test and demonstrate the logic.
Main Backend Components
- src/satellite/src/lib.rs: The entry point for the Satellite serverless function. Triggers the canister call and updates request status on document set.
- src/satellite/src/services.rs: Helper logic for checking wallet balance, performing the transfer, and updating request status.
- src/satellite/src/types.rs: Data model for requests and status.
- src/satellite/Cargo.toml: Rust package configuration for the Satellite function.
Example: Canister Call on Document Set
Here’s the actual Rust logic from lib.rs
and services.rs
:
// src/satellite/src/lib.rs
mod env;
mod ledger_icrc;
mod services;
mod types;
mod utils;
use crate::services::{assert_wallet_balance, set_request_processed, transfer_icp_from_wallet};
use crate::types::RequestData;
use crate::utils::icp_ledger_id;
use ic_cdk::id;
use icrc_ledger_types::icrc1::account::Account;
use junobuild_macros::on_set_doc;
use junobuild_satellite::{include_satellite, OnSetDocContext};
use junobuild_utils::decode_doc_data;
// Triggered when a new document is set in the "request" collection
#[on_set_doc(collections = ["request"])]
async fn on_set_doc(context: OnSetDocContext) -> Result<(), String> {
// Init data
let data: RequestData = decode_doc_data(&context.data.data.after.data)?;
let request_amount = data.amount.value;
let fee = data.fee.as_ref().map(|fee| fee.value);
let ledger_id = icp_ledger_id()?;
let from_account: Account = Account::from(context.caller);
// Check current account balance
assert_wallet_balance(&ledger_id, &from_account, &request_amount, &fee).await?;
// Update request status to processed (atomic with transfer)
set_request_processed(context.data.key, &data, &context.data.data.after.version)?;
// Transfer from wallet to satellite
let to_account: Account = Account::from(id());
transfer_icp_from_wallet(
&ledger_id,
&from_account,
&to_account,
&request_amount,
&fee,
)
.await?;
Ok(())
}
include_satellite!();
// src/satellite/src/services.rs
/// Asserts that the given account has enough balance to cover the amount and fee.
pub async fn assert_wallet_balance(
ledger_id: &Principal,
from_account: &Account,
amount: &u64,
fee: &Option<u64>,
) -> Result<(), String> {
let balance = icrc_balance_of(&ledger_id, &from_account).await?;
let total = amount.saturating_add(fee.unwrap_or(10_000u64));
if balance < total {
return Err(format!("Balance {} is smaller than {}", balance, total));
}
Ok(())
}
/// Transfers ICP from one account to another using `icrc2_transfer_from`.
pub async fn transfer_icp_from_wallet(
ledger_id: &Principal,
from_account: &Account,
to_account: &Account,
amount: &u64,
fee: &Option<u64>,
) -> Result<(), String> {
let result = icrc_transfer_from(
&ledger_id,
&from_account,
&to_account,
&Nat::from(amount.clone()),
&fee.map(|fee| Nat::from(fee)),
)
.await
.map_err(|e| format!("Failed to call ICRC ledger icrc_transfer_from: {:?}", e))
.and_then(|result| {
result.map_err(|e| format!("Failed to execute the transfer from: {:?}", e))
})?;
print(format!("Result of the transfer from is {:?}", result));
Ok(())
}
/// Updates the request document status to `Processed`.
pub fn set_request_processed(
key: String,
original_data: &RequestData,
original_version: &Option<u64>,
) -> Result<(), String> {
let update_data: RequestData = RequestData {
status: RequestStatus::Processed,
..original_data.clone()
};
let data = encode_doc_data(&update_data)?;
let doc: SetDoc = SetDoc {
data,
description: None,
version: original_version.clone(),
};
let _ = set_doc_store(id(), "request".to_string(), key, doc)?;
Ok(())
}
Explanation:
- When a request is submitted, the
on_set_doc
hook is triggered for therequest
collection. - The function checks the user's wallet balance, updates the request status, and performs the ICP transfer atomically.
- If any step fails, the entire operation is reverted.
- The frontend can monitor request status and balances via the exposed APIs.
How to Run
- Clone the repo:
git clone https://github.com/junobuild/examples
cd rust/calls
2. Install dependencies:
npm install
3. Start Juno local emulator:
Requires the Juno CLI to be available npm i -g @junobuild/cli
juno dev start
4. Create a Satellite for local dev:
- Visit http://localhost:5866 and follow the instructions.
- Update
juno.config.ts
with your Satellite ID.
- Create required collections:
request
in Datastore: http://localhost:5866/datastore
- Start the frontend dev server (in a separate terminal):
npm run dev
- Build the serverless functions (in a separate terminal):
juno functions build
The emulator will automatically upgrade your Satellite and live reload the changes.
Juno-Specific Configuration
- juno.config.ts: Defines Satellite IDs for development/production, build source, and predeploy steps. See the Configuration reference for details.
- vite.config.ts: Registers the
juno
plugin to inject environment variables automatically. See the Vite Plugin reference for more information.
Production Deployment
- Create a Satellite on the Juno Console for mainnet.
- Update
juno.config.ts
with the production Satellite ID. - Build and deploy the frontend:
npm run build
juno deploy
- Build and upgrade the serverless functions:
juno functions build
juno functions upgrade
Notes
- This example focuses on the Rust serverless function and canister call integration. The frontend is intentionally minimal and included only for demonstration.
- Use this project as a starting point for workflows that require on-chain asset transfers or canister calls in response to user actions.
Real-World Example
Want to see how assertions and serverless logic are used in a live project?
Check out proposals.network, an open-source app built with Juno:
- GitHub: github.com/peterpeterparker/proposals.network
- Example logic: src/satellite/src/lib.rs
This app uses:
#[on_delete_doc]
and#[assert_delete_doc]
to validate and clean up related documents and assets- Shared helper modules like
assert
,delete
, andtypes
to keep logic organized - A real-world pattern of chaining asset/document deletions with assertions
It’s a great reference for more advanced setups and multi-collection coordination.
References
- Serverless Functions Guide
- Functions Development
- Rust SDK Reference
- Rust Utils Reference
- Run Local Development
- CLI Reference
- Configuration Reference
- Datastore Collections
Crate Docs
These crates are used to build and extend serverless functions in Rust with Juno:
- junobuild-satellite: Core features and runtime for building a Satellite in Rust, including hooks, assertions, and datastore integration.
- junobuild-macros: Procedural macros for declaratively attaching hooks and assertions.
- junobuild-utils: Utility helpers for working with documents, including data encoding, decoding, and assertion context handling.
- junobuild-shared: Shared types and helpers for Juno projects. Used by all containers including the Console.
- junobuild-storage: Storage helpers for working with assets and HTTP headers in Juno.
-
icrc-ledger-types: Types for interacting with the ICRC ledger standard.
-
ic-cdk: Internet Computer canister development kit for Rust.