Juno is in **maintenance** and not actively developed anymore.

# 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 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│   │   └── 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/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 { Principal } from "@icp-sdk/core/principal";import {  type AssertSetDoc,  defineAssert,  defineHook,  type OnSetDoc} from "@junobuild/functions";import {  type Account,  encodeIcrcAccount} from "@junobuild/functions/canisters/ledger/icrc";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<AssertSetDoc>({  collections: [COLLECTION_REQUEST],  assert: (context) => {    // We validate that the data submitted for create or update matches the expected schema.    const person = decodeDocData<RequestData>(context.data.data.proposed.data);    RequestDataSchema.parse(person);  }});export const onSetDoc = defineHook<OnSetDoc>({  collections: [COLLECTION_REQUEST],  run: async (context) => {    // ###############    // Init data    // ###############    const {      data: {        key,        data: {          after: { version }        }      }    } = context;    const data = decodeDocData<RequestData>(context.data.data.after.data);    const { amount: requestAmount, fee } = data;    const ledgerId = ICP_LEDGER_ID;    const fromAccount: Account = {      owner: Principal.fromUint8Array(context.caller)    };    // ###############    // Check current account balance. This way the process can stop early on    // ###############    await assertWalletBalance({      ledgerId,      fromAccount,      amount: requestAmount,      fee    });    // ###############    // The request is about to be processed by transferring the amount via the ICP ledger.    // We update the status beforehand. Since the function is atomic, a failed transfer reverts everything.    // This avoids a case where the transfer succeeds but the status isn't updated — even if unlikely.    // This is for demo only. In production, proper error handling and bookkeeping would be required.    // ###############    setRequestProcessed({      key,      version,      data    });    // ###############    // Transfer from wallet to satellite.    // ###############    const toAccount: Account = {      owner: id()    };    await transferIcpFromWallet({      ledgerId,      fromAccount,      toAccount,      amount: requestAmount,      fee    });    console.log(`${requestAmount} ICP transferred to Satellite 🥳`);  }});
```

```
// src/satellite/services.tsimport { Principal } from "@icp-sdk/core/principal";import {  IcrcLedgerCanister,  type Account,  type TransferFromArgs,  type Tokens} from "@junobuild/functions/canisters/ledger/icrc";export const assertWalletBalance = async ({  ledgerId,  fromAccount,  amount,  fee}: {  ledgerId: Principal;  fromAccount: Account;  amount: bigint;  fee: bigint | undefined;}) => {  const { icrc1BalanceOf } = new IcrcLedgerCanister({ canisterId: ledgerId });  const balance = await icrc1BalanceOf({ account: fromAccount });  const total = amount + (fee ?? 10_000n);  if (balance < total) {    const encodedAccountText = encodeIcrcAccount(fromAccount);    throw new Error(      `Balance ${balance} is smaller than ${total} for account ${encodedAccountText}.`    );  }};export const transferIcpFromWallet = async ({  ledgerId,  fromAccount,  toAccount,  amount,  fee}: {  ledgerId: Principal;  fromAccount: Account;  toAccount: Account;  amount: bigint;  fee: bigint | undefined;}): Promise<Tokens> => {  const args: TransferFromArgs = {    amount,    from: fromAccount,    to: toAccount,    fee  };  const { icrc2TransferFrom } = new IcrcLedgerCanister({    canisterId: ledgerId  });  const result = await icrc2TransferFrom({ args });  if ("Err" in result) {    throw new Error(      `Failed to transfer ICP from wallet: ${JSON.stringify(result, jsonReplacer)}`    );  }  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 emulator 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 hosting 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)