diff --git a/.llms-snapshots/llms-full.txt b/.llms-snapshots/llms-full.txt index bcf0f465..ad31868c 100644 --- a/.llms-snapshots/llms-full.txt +++ b/.llms-snapshots/llms-full.txt @@ -2233,7 +2233,7 @@ Write serverless backend logic for your app using TypeScript or Rust. These exam [## 🗃️ TypeScript -2 items](/docs/examples/functions/typescript.md) +3 items](/docs/examples/functions/typescript.md) # Angular Example @@ -3189,6 +3189,10 @@ An example demonstrating how to write custom assertions in TypeScript for Juno s An example demonstrating how to modify and re-save documents in Juno Satellites using TypeScript hooks.](/docs/examples/functions/typescript/mutating-docs.md) +[## 📄️ Canister Calls + +An example showing how to call external canisters (e.g., ICRC ledger) from a serverless function written in TypeScript using Juno Satellites.](/docs/examples/functions/typescript/canister-calls.md) + # Rust Assertion Example This example demonstrates how to write a **custom assertion** in **Rust** for a Juno **serverless function**. It shows how to intercept and validate data operations—such as rejecting specific content—before it's written to the datastore. @@ -3549,7 +3553,9 @@ These crates are used to build and extend serverless functions in Rust with Juno * [junobuild-shared](https://docs.rs/junobuild-shared): Shared types and helpers for Juno projects. Used by all containers including the Console. * [junobuild-storage](https://docs.rs/junobuild-storage): Storage helpers for working with assets and HTTP headers in Juno. -\* [icrc-ledger-types](https://docs.rs/icrc-ledger-types): Types for interacting with the ICRC ledger standard. \* [ic-cdk](https://docs.rs/ic-cdk): Internet Computer canister development kit for Rust. +* [icrc-ledger-types](https://docs.rs/icrc-ledger-types): Types for interacting with the ICRC ledger standard. + +* [ic-cdk](https://docs.rs/ic-cdk): Internet Computer canister development kit for Rust. # Generating Assets with Rust Serverless Functions @@ -4051,6 +4057,182 @@ It’s a great reference for more advanced setups and orchestration. * [Configuration Reference](/docs/reference/configuration.md) * [Datastore Collections](/docs/build/datastore/collections.md) +# Making Canister Calls in TypeScript Serverless Functions + +This example demonstrates how to use **TypeScript 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/typescript/calls](https://github.com/junobuild/examples/tree/main/functions/typescript/calls) + +--- + +## Folder Structure + +``` +typescript/calls/├── src/│ ├── satellite/ # TypeScript Satellite serverless function│ │ ├── index.ts # Main TypeScript logic for Satellite│ │ ├── services.ts # Helper logic for balance, transfer, status│ │ ├── ledger-icrc.ts # Ledger helper functions│ │ └── tsconfig.json # TypeScript config for Satellite│ ├── declarations/│ │ └── satellite/ # 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 TypeScript 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/index.ts**: The entry point for the Satellite serverless function. Triggers the canister call and updates request status on document set. +* **src/satellite/services.ts**: Helper logic for checking wallet balance, performing the transfer, and updating request status. +* **src/satellite/ledger-icrc.ts**: Helper functions for interacting with the ICRC ledger. +* **src/types/request.ts**: Data model for requests and status. + +--- + +## Example: Canister Call on Document Set + +Here’s the actual TypeScript logic from `index.ts` and `services.ts`: + +``` +// src/satellite/index.tsimport { Account } from "@dfinity/ledger-icrc/dist/candid/icrc_ledger";import { Principal } from "@dfinity/principal";import { type AssertSetDoc, defineAssert, defineHook, type OnSetDoc} from "@junobuild/functions";import { id } from "@junobuild/functions/ic-cdk";import { decodeDocData } from "@junobuild/functions/sdk";import { COLLECTION_REQUEST, ICP_LEDGER_ID } from "../constants/app.constants";import { RequestData, RequestDataSchema } from "../types/request";import { assertWalletBalance, setRequestProcessed, transferIcpFromWallet} from "./services";export const assertSetDoc = defineAssert({ collections: [COLLECTION_REQUEST], assert: (context) => { // We validate that the data submitted for create or update matches the expected schema. const person = decodeDocData(context.data.data.proposed.data); RequestDataSchema.parse(person); }});export const onSetDoc = defineHook({ collections: [COLLECTION_REQUEST], run: async (context) => { // Init data const { data: { key, data: { after: { version } } } } = context; const data = decodeDocData(context.data.data.after.data); const { amount: requestAmount, fee } = data; const ledgerId = ICP_LEDGER_ID; const fromAccount: Account = { owner: Principal.fromUint8Array(context.caller), subaccount: [] }; // Check current account balance await assertWalletBalance({ ledgerId, fromAccount, amount: requestAmount, fee }); // Update request status to processed (atomic with transfer) setRequestProcessed({ key, version, data }); // Transfer from wallet to satellite const toAccount: Account = { owner: id(), subaccount: [] }; await transferIcpFromWallet({ ledgerId, fromAccount, toAccount, amount: requestAmount, fee }); }}); +``` + +``` +// src/satellite/services.tsexport const assertWalletBalance = async ({ ledgerId, fromAccount, amount, fee}: { ledgerId: Principal; fromAccount: Account; amount: bigint; fee: bigint | undefined;}) => { const balance = await icrcBalanceOf({ ledgerId, account: fromAccount }); const total = amount + (fee ?? IC_TRANSACTION_FEE_ICP); if (balance < total) { throw new Error( `Balance ${balance} is smaller than ${total} for account ${fromAccount.owner.toText()}.` ); }};export const transferIcpFromWallet = async (params: { ledgerId: Principal; fromAccount: Account; toAccount: Account; amount: bigint; fee: bigint | undefined;}): Promise => { const result = await icrcTransferFrom(params); if ("Err" in result) { throw new Error( `Failed to transfer ICP from wallet: ${JSON.stringify(result)}` ); } return result.Ok;};export const setRequestProcessed = ({ key, data: currentData, version: originalVersion}: { key: string; data: RequestData; version: bigint | undefined;}) => { const updateData: RequestData = { ...currentData, status: "processed" }; const data = encodeDocData(updateData); const doc: SetDoc = { data, version: originalVersion }; setDocStore({ caller: id(), collection: COLLECTION_REQUEST, doc, key });}; +``` + +**Explanation:** + +* When a request is submitted, the `onSetDoc` hook is triggered for the `request` 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 + +1. **Clone the repo**: + +``` +git clone https://github.com/junobuild/examplescd typescript/calls +``` + +2. **Install dependencies**: + +``` +npm install +``` + +3. **Start Juno local emulator**: + +**Important:** + +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](http://localhost:5866) and follow the instructions. +* Update `juno.config.ts` with your Satellite ID. + +5. **Create required collections**: + +* `request` in Datastore: [http://localhost:5866/datastore](http://localhost:5866/datastore) + +6. **Start the frontend dev server** (in a separate terminal): + +``` +npm run dev +``` + +7. **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](/docs/reference/configuration.md) for details. +* **vite.config.ts**: Registers the `juno` plugin to inject environment variables automatically. See the [Vite Plugin reference](/docs/reference/plugins.md#vite-plugin) for more information. + +--- + +## Production Deployment + +* Create a Satellite on the [Juno Console](https://console.juno.build) for mainnet. +* Update `juno.config.ts` with the production Satellite ID. +* Build and deploy the frontend: + +``` +npm run buildjuno deploy +``` + +* Build and upgrade the serverless functions: + +``` +juno functions buildjuno functions upgrade +``` + +--- + +## Notes + +* This example focuses on the TypeScript serverless function. The frontend is intentionally minimal and included only for demonstration. +* Use this project as a starting point for writing custom backend logic in TypeScript using Juno serverless functions and canister calls. + +--- + +## Real-World Example + +Want to see how assertions and serverless logic are used in a live project? + +Check out [cycles.watch](https://cycles.watch), an open-source app built with Juno: + +* GitHub: [github.com/peterpeterparker/cycles.watch](https://github.com/peterpeterparker/cycles.watch) +* Example logic: [src/satellite/index.ts](https://github.com/peterpeterparker/cycles.watch/blob/main/src/satellite/index.ts) + +This app uses: + +* `assertSetDoc` to validate requests +* `onSetDoc` to implement a swap-like feature that performs various canister calls +* Service modules to keep logic organized +* A real-world pattern for chaining calls and document insertions with assertions + +It’s a great reference for more advanced setups and orchestration. + +--- + +## References + +* [Serverless Functions Guide](/docs/guides/typescript.md) +* [Functions Development](/docs/build/functions.md) +* [TypeScript SDK Reference](/docs/reference/functions/typescript/sdk.md) +* [TypeScript ic-cdk Reference](/docs/reference/functions/typescript/ic-cdk.md) +* [TypeScript Utils Reference](/docs/reference/functions/typescript/utils.md) +* [Run Local Development](/docs/guides/local-development.md) +* [CLI Reference](/docs/reference/cli.md) +* [Configuration Reference](/docs/reference/configuration.md) +* [Datastore Collections](/docs/build/datastore/collections.md) + # Mutating Documents with TypeScript Hooks This example demonstrates how to use **hooks in TypeScript** to modify documents automatically when they're created or updated in your Juno **Satellite**. diff --git a/.llms-snapshots/llms.txt b/.llms-snapshots/llms.txt index 2ea82eff..b93aacca 100644 --- a/.llms-snapshots/llms.txt +++ b/.llms-snapshots/llms.txt @@ -76,6 +76,7 @@ Juno is your self-contained serverless platform for building full-stack web apps ## Examples - Functions - Typescript - [TypeScript Assertions Example](https://juno.build/docs/examples/functions/typescript/assertion.md): An example demonstrating how to write custom assertions in TypeScript for Juno serverless functions. +- [Making Canister Calls in TypeScript Serverless Functions](https://juno.build/docs/examples/functions/typescript/canister-calls.md): An example showing how to call external canisters (e.g., ICRC ledger) from a serverless function written in TypeScript using Juno Satellites. - [Mutating Documents with TypeScript Hooks](https://juno.build/docs/examples/functions/typescript/mutating-docs.md): An example demonstrating how to modify and re-save documents in Juno Satellites using TypeScript hooks. ## Guides diff --git a/docs/examples/functions/typescript/canister-calls.mdx b/docs/examples/functions/typescript/canister-calls.mdx new file mode 100644 index 00000000..f24aa294 --- /dev/null +++ b/docs/examples/functions/typescript/canister-calls.mdx @@ -0,0 +1,289 @@ +--- +title: Making Canister Calls in TypeScript Serverless Functions +description: An example showing how to call external canisters (e.g., ICRC ledger) from a serverless function written in TypeScript using Juno Satellites. +keywords: + [ + typescript, + canister call, + transfer_from, + icrc, + icp, + serverless, + juno, + satellite, + example + ] +sidebar_label: Canister Calls +--- + +# Making Canister Calls in TypeScript Serverless Functions + +This example demonstrates how to use **TypeScript 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/typescript/calls](https://github.com/junobuild/examples/tree/main/functions/typescript/calls) + +--- + +## Folder Structure + +``` +typescript/calls/ +├── src/ +│ ├── satellite/ # TypeScript Satellite serverless function +│ │ ├── index.ts # Main TypeScript logic for Satellite +│ │ ├── services.ts # Helper logic for balance, transfer, status +│ │ ├── ledger-icrc.ts # Ledger helper functions +│ │ └── tsconfig.json # TypeScript config for Satellite +│ ├── declarations/ +│ │ └── satellite/ # 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 TypeScript 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/index.ts**: The entry point for the Satellite serverless function. Triggers the canister call and updates request status on document set. +- **src/satellite/services.ts**: Helper logic for checking wallet balance, performing the transfer, and updating request status. +- **src/satellite/ledger-icrc.ts**: Helper functions for interacting with the ICRC ledger. +- **src/types/request.ts**: Data model for requests and status. + +--- + +## Example: Canister Call on Document Set + +Here’s the actual TypeScript logic from `index.ts` and `services.ts`: + +```ts +// src/satellite/index.ts +import { Account } from "@dfinity/ledger-icrc/dist/candid/icrc_ledger"; +import { Principal } from "@dfinity/principal"; +import { + type AssertSetDoc, + defineAssert, + defineHook, + type OnSetDoc +} from "@junobuild/functions"; +import { id } from "@junobuild/functions/ic-cdk"; +import { decodeDocData } from "@junobuild/functions/sdk"; +import { COLLECTION_REQUEST, ICP_LEDGER_ID } from "../constants/app.constants"; +import { RequestData, RequestDataSchema } from "../types/request"; +import { + assertWalletBalance, + setRequestProcessed, + transferIcpFromWallet +} from "./services"; + +export const assertSetDoc = defineAssert({ + collections: [COLLECTION_REQUEST], + assert: (context) => { + // We validate that the data submitted for create or update matches the expected schema. + const person = decodeDocData(context.data.data.proposed.data); + RequestDataSchema.parse(person); + } +}); + +export const onSetDoc = defineHook({ + collections: [COLLECTION_REQUEST], + run: async (context) => { + // Init data + const { + data: { + key, + data: { + after: { version } + } + } + } = context; + const data = decodeDocData(context.data.data.after.data); + const { amount: requestAmount, fee } = data; + const ledgerId = ICP_LEDGER_ID; + const fromAccount: Account = { + owner: Principal.fromUint8Array(context.caller), + subaccount: [] + }; + // Check current account balance + await assertWalletBalance({ + ledgerId, + fromAccount, + amount: requestAmount, + fee + }); + // Update request status to processed (atomic with transfer) + setRequestProcessed({ + key, + version, + data + }); + // Transfer from wallet to satellite + const toAccount: Account = { + owner: id(), + subaccount: [] + }; + await transferIcpFromWallet({ + ledgerId, + fromAccount, + toAccount, + amount: requestAmount, + fee + }); + } +}); +``` + +```ts +// src/satellite/services.ts +export const assertWalletBalance = async ({ + ledgerId, + fromAccount, + amount, + fee +}: { + ledgerId: Principal; + fromAccount: Account; + amount: bigint; + fee: bigint | undefined; +}) => { + const balance = await icrcBalanceOf({ + ledgerId, + account: fromAccount + }); + const total = amount + (fee ?? IC_TRANSACTION_FEE_ICP); + if (balance < total) { + throw new Error( + `Balance ${balance} is smaller than ${total} for account ${fromAccount.owner.toText()}.` + ); + } +}; + +export const transferIcpFromWallet = async (params: { + ledgerId: Principal; + fromAccount: Account; + toAccount: Account; + amount: bigint; + fee: bigint | undefined; +}): Promise => { + const result = await icrcTransferFrom(params); + if ("Err" in result) { + throw new Error( + `Failed to transfer ICP from wallet: ${JSON.stringify(result)}` + ); + } + return result.Ok; +}; + +export const setRequestProcessed = ({ + key, + data: currentData, + version: originalVersion +}: { + key: string; + data: RequestData; + version: bigint | undefined; +}) => { + const updateData: RequestData = { + ...currentData, + status: "processed" + }; + const data = encodeDocData(updateData); + const doc: SetDoc = { + data, + version: originalVersion + }; + setDocStore({ + caller: id(), + collection: COLLECTION_REQUEST, + doc, + key + }); +}; +``` + +**Explanation:** + +- When a request is submitted, the `onSetDoc` hook is triggered for the `request` 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 + +1. **Clone the repo**: + +```bash +git clone https://github.com/junobuild/examples +cd typescript/calls +``` + +import HowToStart from "../../components/how-to-start.mdx"; + + + +import CreateSatellite from "../../components/create-a-satellite.mdx"; + + + +5. **Create required collections**: + +- `request` in Datastore: [http://localhost:5866/datastore](http://localhost:5866/datastore) + +import HowToRun from "../components/how-to-run.md"; + + + +--- + +import Config from "../components/config.md"; + + + +--- + +import ProdDeploy from "../components/prod-deploy.md"; + + + +--- + +## Notes + +- This example focuses on the TypeScript serverless function. The frontend is intentionally minimal and included only for demonstration. +- Use this project as a starting point for writing custom backend logic in TypeScript using Juno serverless functions and canister calls. + +--- + +import CyclesWatch from "./components/cycles-watch.md"; + + + +--- + +import References from "./components/references.md"; + + diff --git a/sidebars.ts b/sidebars.ts index 973181ff..4aecc477 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -140,7 +140,8 @@ const sidebars: SidebarsConfig = { }, items: [ "examples/functions/typescript/assertion", - "examples/functions/typescript/mutating-docs" + "examples/functions/typescript/mutating-docs", + "examples/functions/typescript/canister-calls" ] } ]