Skip to content

Commit 8faa87e

Browse files
authored
chore: migrate motoko/hello_cycles to icp-cli (#1381)
1 parent f51b370 commit 8faa87e

10 files changed

Lines changed: 142 additions & 158 deletions

File tree

.github/workflows/hello_cycles.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: hello_cycles
2+
3+
on:
4+
push:
5+
branches: [master]
6+
pull_request:
7+
paths:
8+
- motoko/hello_cycles/**
9+
- .github/workflows/hello_cycles.yml
10+
11+
concurrency:
12+
group: ${{ github.workflow }}-${{ github.ref }}
13+
cancel-in-progress: true
14+
15+
jobs:
16+
motoko-hello_cycles:
17+
runs-on: ubuntu-24.04
18+
container: ghcr.io/dfinity/icp-dev-env-motoko:0.3.2
19+
env:
20+
ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
21+
steps:
22+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
23+
- name: Deploy and test
24+
working-directory: motoko/hello_cycles
25+
run: |
26+
icp network start -d
27+
icp deploy
28+
make test

.github/workflows/motoko-hello_cycles-example.yaml

Lines changed: 0 additions & 40 deletions
This file was deleted.

motoko/hello_cycles/Makefile

Lines changed: 28 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,32 @@
1-
.PHONY: all
2-
all: build
3-
4-
.PHONY: build
5-
.SILENT: build
6-
build:
7-
dfx canister create hello_cycles
8-
dfx build
1+
.PHONY: test
92

10-
.PHONY: install
11-
.SILENT: install
12-
install: build
13-
dfx canister install hello_cycles
3+
test:
4+
@echo "=== Test 1: getBalance returns the canister's cycle balance ==="
5+
@result=$$(icp canister call --query backend getBalance '()') && \
6+
echo "$$result" && \
7+
echo "$$result" | grep -qE '^\([0-9][0-9_]* : nat\)$$' && \
8+
echo "PASS" || (echo "FAIL" && exit 1)
149

15-
.PHONY: upgrade
16-
.SILENT: upgrade
17-
upgrade: build
18-
dfx canister install hello_cycles --mode=upgrade
10+
@echo "=== Test 2: acceptCycles accepts 1M cycles sent via the proxy canister ==="
11+
@PROXY=$$(icp network status --json | jq -r .proxy_canister_principal) && \
12+
result=$$(icp canister call backend acceptCycles '()' --proxy "$$PROXY" --cycles 1_000_000) && \
13+
echo "$$result" && \
14+
echo "$$result" | grep -qE 'accepted = [1-9]' && \
15+
echo "PASS" || (echo "FAIL" && exit 1)
1916

20-
.PHONY: test
21-
.SILENT: test
22-
test: install
23-
$(eval WALLET := $(shell dfx identity get-wallet))
24-
$(eval HELLO_CYCLES := $(shell dfx canister id hello_cycles))
25-
echo "dfx version: $(shell dfx --version)"
26-
echo "wallet: $(WALLET)"
27-
echo "hello_cycles: $(HELLO_CYCLES)"
28-
echo "$(dfx canister call hello_cycles wallet_balance)"
29-
# canister has just above 2.9T cycles, so output start with '(2_9'
30-
dfx canister call hello_cycles wallet_balance \
31-
| grep '(2_9' && echo 'PASS'
32-
dfx canister status hello_cycles
33-
dfx canister call $(WALLET) wallet_send '(record { canister = principal "$(HELLO_CYCLES)"; amount = (2000000000000:nat64); } )'
34-
# 2T cycles added, now contains just above 4.9T
35-
dfx canister call hello_cycles wallet_balance \
36-
| grep '(4_9' && echo 'PASS'
37-
echo '(func "$(WALLET)"."wallet_receive", 5000000)'
38-
dfx canister call hello_cycles transfer '(func "$(WALLET)"."wallet_receive", 5000000)' \
39-
| grep '0' && echo 'PASS'
40-
dfx canister call hello_cycles wallet_balance \
41-
| grep '(4_9' && echo 'PASS'
17+
@echo "=== Test 3: sendCycles forwards 5M to itself — within limit, refunded = 0 ==="
18+
@# The canister calls its own acceptCycles as receiver (inter-canister self-call).
19+
@# 5M offered < 10M limit, so all cycles are accepted and none are refunded.
20+
@BACKEND_ID=$$(icp canister status backend -i) && \
21+
result=$$(icp canister call backend sendCycles "(func \"$$BACKEND_ID\".\"acceptCycles\", 5_000_000)") && \
22+
echo "$$result" && \
23+
echo "$$result" | grep -q 'refunded = 0' && \
24+
echo "PASS" || (echo "FAIL" && exit 1)
4225

43-
.PHONY: clean
44-
.SILENT: clean
45-
clean:
46-
rm -fr .dfx
26+
@echo "=== Test 4: sendCycles forwards 15M to itself — over limit, refunded = 5_000_000 ==="
27+
@# 15M offered > 10M limit, so 10M are accepted and exactly 5M are refunded.
28+
@BACKEND_ID=$$(icp canister status backend -i) && \
29+
result=$$(icp canister call backend sendCycles "(func \"$$BACKEND_ID\".\"acceptCycles\", 15_000_000)") && \
30+
echo "$$result" && \
31+
echo "$$result" | grep -q 'refunded = 5_000_000' && \
32+
echo "PASS" || (echo "FAIL" && exit 1)

motoko/hello_cycles/README.md

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,51 @@
11
# Hello, cycles!
22

3-
The `hello_cycles` sample project provides a simple example to illustrate how you might add functions to receive cycles, transfer cycles, and check your cycle balance with a simple Motoko actor (canister).
3+
On the Internet Computer, canisters pay for computation and storage with cycles. This example demonstrates the three fundamental cycle management operations in Motoko:
44

5-
This sample project assumes that you are using the default cycles wallet canister that is created for you.
5+
1. **Inspect the balance** — read how many cycles the canister currently holds.
6+
2. **Accept incoming cycles** — when a caller attaches cycles to a call, the canister must explicitly claim them. Unclaimed cycles are automatically refunded to the caller.
7+
3. **Send cycles to another canister** — attach cycles to an outgoing inter-canister call and learn how many were refunded (not accepted by the receiver).
68

7-
This example consists of the following functions (see `src/hello_cycles/main.mo`):
9+
Operations 2 and 3 are two perspectives on the same transaction:
10+
- The **receiver** calls `Cycles.accept()` and returns how many it took (`accepted`).
11+
- The **sender** reads `Cycles.refunded()` after the call returns to learn how many cycles came back.
812

9-
- The `wallet_balance : () -> async Nat`: enables you to check the current cycle balance for the canister.
13+
## Functions
1014

11-
- The `wallet_receive : () -> { amount : Nat64 }`: enables the program to accept cycles that are sent to the canister from a wallet. Both the name and type of this function are dictated by the wallet's implementation (so don't mess with them).
15+
- `getBalance()` — returns the canister's current cycle balance as `Nat`.
16+
- `acceptCycles()` — accepts up to 10 million cycles from the caller; returns `{ accepted : Nat64 }`. Any excess is refunded automatically.
17+
- `sendCycles(receiver, amount)` — forwards `amount` cycles from this canister's balance to `receiver`; returns `{ refunded : Nat }`. A non-zero `refunded` means the receiver did not accept all of the offered cycles.
1218

13-
- The `transfer : (shared () -> (), Nat) -> async { refunded : Nat }`: enables the program to transfer cycles to any shared function with candid signature `"() -> ()"` (assuming it accepts cycles). One example is the wallet's own `wallet_receive : () -> ()` function.
19+
## Build and deploy from the command line
1420

15-
:::caution
16-
The wallet's `wallet_receive` return type differs from hello_cycle's `wallet_receive`.
17-
:::
21+
### Prerequisites
1822

19-
## Deploying from ICP Ninja
23+
- Node.js
24+
- icp-cli: `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm`
25+
- ic-mops: `npm install -g ic-mops`
26+
- jq (used in `make test` to read the proxy canister principal)
2027

21-
[![](https://icp.ninja/assets/open.svg)](https://icp.ninja/editor?g=https://github.com/dfinity/examples/tree/master/motoko/hello_cycles)
28+
### Install
2229

23-
## Build and deploy from the command-line
24-
25-
### 1. [Download and install the IC SDK.](https://internetcomputer.org/docs/building-apps/getting-started/install)
30+
```bash
31+
git clone https://github.com/dfinity/examples
32+
cd examples/motoko/hello_cycles
33+
```
2634

27-
### 2. Download your project from ICP Ninja using the 'Download files' button on the upper left corner, or [clone the GitHub examples repository.](https://github.com/dfinity/examples/)
35+
### Deploy and test
2836

29-
### 3. Navigate into the project's directory.
37+
```bash
38+
icp network start -d
39+
icp deploy
40+
make test
41+
icp network stop
42+
```
3043

31-
### 4. Deploy the project to your local environment:
44+
The tests cover both perspectives:
3245

33-
```
34-
dfx start --background --clean && dfx deploy
35-
```
46+
- **Test 2 (receiver side)**: calls `acceptCycles` through the local [proxy canister](https://cli.internetcomputer.org/0.3/guides/proxy-canister/) with 1M attached cycles — verifies `accepted > 0`. External callers cannot attach cycles directly; icp-cli routes the cycles via the proxy, which is automatically deployed on the local network.
47+
- **Tests 3 & 4 (sender side)**: calls `sendCycles` with the canister's **own** `acceptCycles` as the receiver (an inter-canister self-call — no second canister needed). The 5M/15M cycles leave the canister via `sendCycles` and arrive at `acceptCycles`, which accepts up to the 10M limit. Test 3 sends 5M → `refunded = 0` (within limit). Test 4 sends 15M → `refunded = 5_000_000` (5M over the limit). In practice, `sendCycles` would target a different canister.
3648

3749
## Security considerations and best practices
3850

39-
If you base your application on this example, it is recommended that you familiarize yourself with and adhere to the [security best practices](https://internetcomputer.org/docs/building-apps/security/overview) for developing on ICP. This example may not implement all the best practices.
40-
51+
Refer to the [security best practices](https://docs.internetcomputer.org/guides/security/overview) for information on security and best practices for your ICP app.

motoko/hello_cycles/backend/app.mo

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import Nat "mo:core/Nat";
2+
import Nat64 "mo:core/Nat64";
3+
import Cycles "mo:core/Cycles";
4+
5+
actor HelloCycles {
6+
7+
/// Maximum cycles this canister accepts per call.
8+
let limit = 10_000_000;
9+
10+
/// Returns the canister's current cycle balance.
11+
public query func getBalance() : async Nat {
12+
Cycles.balance();
13+
};
14+
15+
/// Accepts cycles that the caller attached to this call, up to `limit`.
16+
/// The remainder is automatically refunded to the caller.
17+
/// Returns how many cycles were actually accepted.
18+
///
19+
/// This is the RECEIVER perspective: the canister decides how much to keep.
20+
public func acceptCycles() : async { accepted : Nat64 } {
21+
let available = Cycles.available(); // total cycles the caller attached
22+
let accepted = Cycles.accept<system>(Nat.min(available, limit)); // claim up to limit
23+
{ accepted = Nat64.fromNat(accepted) };
24+
};
25+
26+
/// Sends `amount` cycles from this canister's balance to `receiver`.
27+
/// Returns how many cycles were refunded (not accepted by the receiver).
28+
///
29+
/// `receiver` is typed as `shared () -> async {}` (returning the empty record).
30+
/// Because `{ accepted : Nat64 } <: {}` in Motoko's structural record typing,
31+
/// `acceptCycles` is a valid argument even though it returns a non-empty record.
32+
///
33+
/// This is the SENDER perspective: the canister spends from its own
34+
/// balance and learns how many cycles came back unused.
35+
public func sendCycles(receiver : shared () -> async {}, amount : Nat) : async { refunded : Nat } {
36+
ignore await (with cycles = amount) receiver(); // attach `amount`; ignore return value
37+
{ refunded = Cycles.refunded() }; // how many the receiver did not accept
38+
};
39+
40+
};

motoko/hello_cycles/dfx.json

Lines changed: 0 additions & 14 deletions
This file was deleted.

motoko/hello_cycles/icp.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
canisters:
2+
- name: backend
3+
recipe:
4+
type: "@dfinity/motoko@v5.0.0"

motoko/hello_cycles/mops.toml

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
# Motoko dependencies (https://mops.one/)
2-
31
[toolchain]
4-
moc = "1.5.1"
2+
moc = "1.9.0"
53

64
[dependencies]
7-
core = "2.4.0"
5+
core = "2.5.0"
86

97
[moc]
10-
# M0236: use context dot notation (e.g. map.get(k) instead of Map.get(map, compare, k))
11-
# M0237: redundant explicit implicit arguments (e.g. Nat.compare is inferred automatically)
12-
# M0223: redundant type instantiation (e.g. Array.tabulate instead of Array.tabulate<T>)
13-
args = ["-W=M0236,M0237,M0223"]
8+
# M0236: use context dot notation
9+
# M0237: redundant explicit implicit arguments
10+
# M0223: redundant type instantiation
11+
args = ["--default-persistent-actors", "-W=M0236,M0237,M0223"]
12+
13+
[canisters.backend]
14+
main = "backend/app.mo"

motoko/hello_cycles/package-lock.json

Lines changed: 0 additions & 6 deletions
This file was deleted.

motoko/hello_cycles/src/hello_cycles/main.mo

Lines changed: 0 additions & 26 deletions
This file was deleted.

0 commit comments

Comments
 (0)