Skip to content

Commit 1c9f5d6

Browse files
docs: use functions canisters lib in calls example (#591)
* docs: use functions canisters lib in calls example Signed-off-by: David Dal Busco <david.dalbusco@outlook.com> * 📄 Update LLMs.txt snapshot for PR review --------- Signed-off-by: David Dal Busco <david.dalbusco@outlook.com> Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 568cf4e commit 1c9f5d6

2 files changed

Lines changed: 79 additions & 23 deletions

File tree

.llms-snapshots/llms-full.txt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5162,7 +5162,7 @@ You can browse the source code here: [github.com/junobuild/examples/tree/main/fu
51625162
## Folder Structure
51635163

51645164
```
5165-
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└── ...
5165+
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└── ...
51665166
```
51675167

51685168
---
@@ -5180,7 +5180,6 @@ typescript/calls/├── src/│ ├── satellite/ # TypeScript
51805180

51815181
* **src/satellite/index.ts**: The entry point for the Satellite serverless function. Triggers the canister call and updates request status on document set.
51825182
* **src/satellite/services.ts**: Helper logic for checking wallet balance, performing the transfer, and updating request status.
5183-
* **src/satellite/ledger-icrc.ts**: Helper functions for interacting with the ICRC ledger.
51845183
* **src/types/request.ts**: Data model for requests and status.
51855184

51865185
---
@@ -5190,11 +5189,11 @@ typescript/calls/├── src/│ ├── satellite/ # TypeScript
51905189
Here’s the actual TypeScript logic from `index.ts` and `services.ts`:
51915190

51925191
```
5193-
// 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<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), 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 }); }});
5192+
// src/satellite/index.tsimport { Principal } from "@icp-sdk/core/principal";import { type AssertSetDoc, defineAssert, defineHook, type OnSetDoc} from "@junobuild/functions";import { IcrcLedgerDid } 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: IcrcLedgerDid.Account = { owner: Principal.fromUint8Array(context.caller), subaccount: [] }; // ############### // 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: IcrcLedgerDid.Account = { owner: id(), subaccount: [] }; await transferIcpFromWallet({ ledgerId, fromAccount, toAccount, amount: requestAmount, fee }); console.log(`${requestAmount} ICP transferred to Satellite 🥳`); }});
51945193
```
51955194

51965195
```
5197-
// 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<bigint> => { 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 });};
5196+
// src/satellite/services.tsexport const assertWalletBalance = async ({ ledgerId, fromAccount, amount, fee}: { ledgerId: Principal; fromAccount: IcrcLedgerDid.Account; amount: bigint; fee: bigint | undefined;}) => { const { icrc1BalanceOf } = new IcrcLedgerCanister({ canisterId: ledgerId }); const balance = await icrc1BalanceOf({ account: fromAccount }); const total = amount + (fee ?? IC_TRANSACTION_FEE_ICP); if (balance < total) { const encodedAccountText = encodeIcrcAccount({ owner: fromAccount.owner, subaccount: fromNullable(fromAccount.subaccount) }); throw new Error( `Balance ${balance} is smaller than ${total} for account ${encodedAccountText}.` ); }};export const transferIcpFromWallet = async ({ ledgerId, fromAccount, amount, fee, toAccount}: { ledgerId: Principal; fromAccount: IcrcLedgerDid.Account; toAccount: IcrcLedgerDid.Account; amount: bigint; fee: bigint | undefined;}): Promise<IcrcLedgerDid.Tokens> => { const args: IcrcLedgerDid.TransferFromArgs = { amount, from: fromAccount, to: toAccount, created_at_time: toNullable(), fee: toNullable(fee), memo: toNullable(), spender_subaccount: toNullable() }; 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 });};
51985197
```
51995198

52005199
**Explanation:**

docs/examples/functions/typescript/canister-calls.mdx

Lines changed: 76 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ keywords:
55
[
66
typescript,
77
canister call,
8+
ledger,
89
transfer_from,
910
icrc,
1011
icp,
@@ -40,7 +41,6 @@ typescript/calls/
4041
│ ├── satellite/ # TypeScript Satellite serverless function
4142
│ │ ├── index.ts # Main TypeScript logic for Satellite
4243
│ │ ├── services.ts # Helper logic for balance, transfer, status
43-
│ │ ├── ledger-icrc.ts # Ledger helper functions
4444
│ │ └── tsconfig.json # TypeScript config for Satellite
4545
│ ├── declarations/
4646
│ │ └── satellite/ # TypeScript declarations for Satellite
@@ -69,7 +69,6 @@ typescript/calls/
6969

7070
- **src/satellite/index.ts**: The entry point for the Satellite serverless function. Triggers the canister call and updates request status on document set.
7171
- **src/satellite/services.ts**: Helper logic for checking wallet balance, performing the transfer, and updating request status.
72-
- **src/satellite/ledger-icrc.ts**: Helper functions for interacting with the ICRC ledger.
7372
- **src/types/request.ts**: Data model for requests and status.
7473

7574
---
@@ -80,14 +79,14 @@ Here’s the actual TypeScript logic from `index.ts` and `services.ts`:
8079

8180
```ts
8281
// src/satellite/index.ts
83-
import { Account } from "@dfinity/ledger-icrc/dist/candid/icrc_ledger";
84-
import { Principal } from "@dfinity/principal";
82+
import { Principal } from "@icp-sdk/core/principal";
8583
import {
8684
type AssertSetDoc,
8785
defineAssert,
8886
defineHook,
8987
type OnSetDoc
9088
} from "@junobuild/functions";
89+
import { IcrcLedgerDid } from "@junobuild/functions/canisters/ledger/icrc";
9190
import { id } from "@junobuild/functions/ic-cdk";
9291
import { decodeDocData } from "@junobuild/functions/sdk";
9392
import { COLLECTION_REQUEST, ICP_LEDGER_ID } from "../constants/app.constants";
@@ -103,14 +102,18 @@ export const assertSetDoc = defineAssert<AssertSetDoc>({
103102
assert: (context) => {
104103
// We validate that the data submitted for create or update matches the expected schema.
105104
const person = decodeDocData<RequestData>(context.data.data.proposed.data);
105+
106106
RequestDataSchema.parse(person);
107107
}
108108
});
109109

110110
export const onSetDoc = defineHook<OnSetDoc>({
111111
collections: [COLLECTION_REQUEST],
112112
run: async (context) => {
113+
// ###############
113114
// Init data
115+
// ###############
116+
114117
const {
115118
data: {
116119
key,
@@ -119,38 +122,59 @@ export const onSetDoc = defineHook<OnSetDoc>({
119122
}
120123
}
121124
} = context;
125+
122126
const data = decodeDocData<RequestData>(context.data.data.after.data);
127+
123128
const { amount: requestAmount, fee } = data;
129+
124130
const ledgerId = ICP_LEDGER_ID;
125-
const fromAccount: Account = {
131+
132+
const fromAccount: IcrcLedgerDid.Account = {
126133
owner: Principal.fromUint8Array(context.caller),
127134
subaccount: []
128135
};
129-
// Check current account balance
136+
137+
// ###############
138+
// Check current account balance. This way the process can stop early on
139+
// ###############
130140
await assertWalletBalance({
131141
ledgerId,
132142
fromAccount,
133143
amount: requestAmount,
134144
fee
135145
});
136-
// Update request status to processed (atomic with transfer)
146+
147+
// ###############
148+
// The request is about to be processed by transferring the amount via the ICP ledger.
149+
// We update the status beforehand. Since the function is atomic, a failed transfer reverts everything.
150+
// This avoids a case where the transfer succeeds but the status isn't updated — even if unlikely.
151+
// This is for demo only. In production, proper error handling and bookkeeping would be required.
152+
// ###############
153+
137154
setRequestProcessed({
138155
key,
139156
version,
140157
data
141158
});
142-
// Transfer from wallet to satellite
143-
const toAccount: Account = {
159+
160+
// ###############
161+
// Transfer from wallet to satellite.
162+
// ###############
163+
164+
const toAccount: IcrcLedgerDid.Account = {
144165
owner: id(),
145166
subaccount: []
146167
};
168+
147169
await transferIcpFromWallet({
148170
ledgerId,
149171
fromAccount,
150172
toAccount,
151173
amount: requestAmount,
152174
fee
153175
});
176+
177+
console.log(`${requestAmount} ICP transferred to Satellite 🥳`);
154178
}
155179
});
156180
```
@@ -164,35 +188,65 @@ export const assertWalletBalance = async ({
164188
fee
165189
}: {
166190
ledgerId: Principal;
167-
fromAccount: Account;
191+
fromAccount: IcrcLedgerDid.Account;
168192
amount: bigint;
169193
fee: bigint | undefined;
170194
}) => {
171-
const balance = await icrcBalanceOf({
172-
ledgerId,
195+
const { icrc1BalanceOf } = new IcrcLedgerCanister({ canisterId: ledgerId });
196+
197+
const balance = await icrc1BalanceOf({
173198
account: fromAccount
174199
});
200+
175201
const total = amount + (fee ?? IC_TRANSACTION_FEE_ICP);
202+
176203
if (balance < total) {
204+
const encodedAccountText = encodeIcrcAccount({
205+
owner: fromAccount.owner,
206+
subaccount: fromNullable(fromAccount.subaccount)
207+
});
208+
177209
throw new Error(
178-
`Balance ${balance} is smaller than ${total} for account ${fromAccount.owner.toText()}.`
210+
`Balance ${balance} is smaller than ${total} for account ${encodedAccountText}.`
179211
);
180212
}
181213
};
182214

183-
export const transferIcpFromWallet = async (params: {
215+
export const transferIcpFromWallet = async ({
216+
ledgerId,
217+
fromAccount,
218+
amount,
219+
fee,
220+
toAccount
221+
}: {
184222
ledgerId: Principal;
185-
fromAccount: Account;
186-
toAccount: Account;
223+
fromAccount: IcrcLedgerDid.Account;
224+
toAccount: IcrcLedgerDid.Account;
187225
amount: bigint;
188226
fee: bigint | undefined;
189-
}): Promise<bigint> => {
190-
const result = await icrcTransferFrom(params);
227+
}): Promise<IcrcLedgerDid.Tokens> => {
228+
const args: IcrcLedgerDid.TransferFromArgs = {
229+
amount,
230+
from: fromAccount,
231+
to: toAccount,
232+
created_at_time: toNullable(),
233+
fee: toNullable(fee),
234+
memo: toNullable(),
235+
spender_subaccount: toNullable()
236+
};
237+
238+
const { icrc2TransferFrom } = new IcrcLedgerCanister({
239+
canisterId: ledgerId
240+
});
241+
242+
const result = await icrc2TransferFrom({ args });
243+
191244
if ("Err" in result) {
192245
throw new Error(
193-
`Failed to transfer ICP from wallet: ${JSON.stringify(result)}`
246+
`Failed to transfer ICP from wallet: ${JSON.stringify(result, jsonReplacer)}`
194247
);
195248
}
249+
196250
return result.Ok;
197251
};
198252

@@ -209,11 +263,14 @@ export const setRequestProcessed = ({
209263
...currentData,
210264
status: "processed"
211265
};
266+
212267
const data = encodeDocData(updateData);
268+
213269
const doc: SetDoc = {
214270
data,
215271
version: originalVersion
216272
};
273+
217274
setDocStore({
218275
caller: id(),
219276
collection: COLLECTION_REQUEST,

0 commit comments

Comments
 (0)