Skip to content

Commit 512e793

Browse files
authored
chore: migrate motoko/ic-pos to icp-cli (#1442)
1 parent 45e6c56 commit 512e793

120 files changed

Lines changed: 1480 additions & 9956 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ic_pos.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: ic_pos
2+
3+
on:
4+
push:
5+
branches: [master]
6+
pull_request:
7+
paths:
8+
- motoko/ic-pos/**
9+
- .github/workflows/ic_pos.yml
10+
11+
concurrency:
12+
group: ${{ github.workflow }}-${{ github.ref }}
13+
cancel-in-progress: true
14+
15+
jobs:
16+
motoko-ic_pos:
17+
runs-on: ubuntu-24.04
18+
container: ghcr.io/dfinity/icp-dev-env-motoko:1.0.1
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/ic-pos
25+
run: |
26+
icp network start -d
27+
bash deploy.sh
28+
bash test.sh

motoko/ic-pos/.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ target/
1717
node_modules/
1818
dist/
1919

20+
# generated canister bindings (icp-bindgen)
21+
frontend/src/bindings/
22+
23+
# TypeScript incremental build info
24+
*.tsbuildinfo
25+
2026
# environment variables
2127
.env
2228

motoko/ic-pos/CHANGELOG.md

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

motoko/ic-pos/README.md

Lines changed: 57 additions & 168 deletions
Large diffs are not rendered by default.

motoko/ic-pos/backend/app.mo

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import Debug "mo:core/Debug";
2+
import Map "mo:core/Map";
3+
import Nat "mo:core/Nat";
4+
import Nat8 "mo:core/Nat8";
5+
import Nat64 "mo:core/Nat64";
6+
import Principal "mo:core/Principal";
7+
import Runtime "mo:core/Runtime";
8+
import Text "mo:core/Text";
9+
import Time "mo:core/Time";
10+
11+
import Types "app.types";
12+
13+
// This actor:
14+
// - stores merchant information,
15+
// - monitors the ICRC-1 ledger for incoming transfers, and
16+
// - logs where a merchant notification would be sent.
17+
//
18+
// The ledger canister is resolved at runtime from the injected
19+
// `PUBLIC_CANISTER_ID:icrc1_ledger` environment variable:
20+
// local: the pre-built ICRC-1 ledger deployed by deploy.sh
21+
// ic: the TICRC1 test ledger (see icp.yaml)
22+
//
23+
// `_startBlock` is the ledger block index to start monitoring from.
24+
//
25+
// NOTE: notifications are illustrative. The original example sent email/SMS via
26+
// an HTTPS outcall to a third party; this version only logs that a notification
27+
// could be sent. To implement real notifications, use HTTPS outcalls — see
28+
// https://docs.internetcomputer.org/guides/backends/https-outcalls
29+
//
30+
// NOTE: scanning the ledger's global transaction log sequentially does not scale
31+
// to a busy shared ledger (such as TICRC1); it is illustrative only. A
32+
// production app would query the index canister per merchant account instead
33+
// (as this example's frontend already does).
34+
actor class Main(_startBlock : Nat) {
35+
36+
// Minimal subset of the ICRC-1 ledger interface this actor uses. Candid
37+
// ignores wire fields not declared here, so only the read fields are listed.
38+
type Account = { owner : Principal };
39+
type Transfer = { to : Account; from : Account; amount : Nat };
40+
type Transaction = { kind : Text; transfer : ?Transfer };
41+
type GetTransactionsRequest = { start : Nat; length : Nat };
42+
type GetTransactionsResponse = { transactions : [Transaction] };
43+
type Ledger = actor {
44+
get_transactions : GetTransactionsRequest -> async GetTransactionsResponse;
45+
icrc1_symbol : () -> async Text;
46+
icrc1_decimals : () -> async Nat8;
47+
};
48+
49+
let merchantStore = Map.empty<Text, Types.Merchant>();
50+
// Next ledger block index to scan for incoming transfers.
51+
var nextBlock : Nat = _startBlock;
52+
// Token metadata, cached lazily from the ledger to format logged amounts.
53+
// Transient: re-fetched on demand after an upgrade.
54+
transient var tokenSymbol : Text = "tokens";
55+
transient var tokenDecimals : Nat = 8;
56+
transient var metadataLoaded : Bool = false;
57+
58+
// How many ledger blocks to scan per timer tick. Draining a batch (rather than
59+
// one block per tick) means a payment is logged within a single tick even when
60+
// the monitor is catching up on a backlog (e.g. the initial mint).
61+
let scanBatchSize : Nat = 100;
62+
63+
// Get the caller's merchant information.
64+
public query (context) func getMerchant() : async Types.Response<Types.Merchant> {
65+
switch (merchantStore.get(context.caller.toText())) {
66+
case (?merchant) {
67+
{ status = 200; status_text = "OK"; data = ?merchant; error_text = null };
68+
};
69+
case null {
70+
{
71+
status = 404;
72+
status_text = "Not Found";
73+
data = null;
74+
error_text = ?("Merchant with principal ID: " # context.caller.toText() # " not found.");
75+
};
76+
};
77+
};
78+
};
79+
80+
// Create or update the caller's merchant information.
81+
public shared (context) func updateMerchant(merchant : Types.Merchant) : async Types.Response<Types.Merchant> {
82+
merchantStore.add(context.caller.toText(), merchant);
83+
{ status = 200; status_text = "OK"; data = ?merchant; error_text = null };
84+
};
85+
86+
// Resolve the ledger canister, injected as PUBLIC_CANISTER_ID:icrc1_ledger.
87+
func ledger<system>() : Ledger {
88+
switch (Runtime.envVar<system>("PUBLIC_CANISTER_ID:icrc1_ledger")) {
89+
case (?id) actor (id) : Ledger;
90+
case null Runtime.trap("PUBLIC_CANISTER_ID:icrc1_ledger not set — run icp deploy");
91+
};
92+
};
93+
94+
// Fetch the token's symbol and decimals from the ledger once, then cache them.
95+
func loadMetadataOnce<system>() : async () {
96+
if (metadataLoaded) return;
97+
let l = ledger<system>();
98+
tokenSymbol := await l.icrc1_symbol();
99+
tokenDecimals := Nat8.toNat(await l.icrc1_decimals());
100+
metadataLoaded := true;
101+
};
102+
103+
// Render a base-unit amount using the cached decimals and symbol, e.g. with
104+
// 8 decimals: 100_000_000 -> "1 LICRC1", 50_000 -> "0.0005 LICRC1".
105+
func formatAmount(amount : Nat) : Text {
106+
let base = 10 ** tokenDecimals;
107+
let whole = (amount / base).toText();
108+
let frac = amount % base;
109+
if (frac == 0) return whole # " " # tokenSymbol;
110+
// Left-pad the fractional digits to `tokenDecimals`, then drop trailing zeros.
111+
var fracText = frac.toText();
112+
while (fracText.size() < tokenDecimals) { fracText := "0" # fracText };
113+
whole # "." # Text.trimEnd(fracText, #char '0') # " " # tokenSymbol;
114+
};
115+
116+
// Scan for new transactions and log a would-be notification for each payment
117+
// to a merchant that has notifications enabled. Called by the global timer.
118+
system func timer(setGlobalTimer : Nat64 -> ()) : async () {
119+
let next = Nat64.fromIntWrap(Time.now()) + 20_000_000_000; // 20 seconds
120+
setGlobalTimer(next);
121+
await notify<system>();
122+
};
123+
124+
func notify<system>() : async () {
125+
let response = await ledger<system>().get_transactions({ start = nextBlock; length = scanBatchSize });
126+
let txs = response.transactions;
127+
if (txs.size() == 0) return; // caught up; check again next tick
128+
nextBlock += txs.size();
129+
130+
var i = 0;
131+
label scan while (i < txs.size()) {
132+
let t = txs[i];
133+
i += 1;
134+
if (t.kind != "transfer") continue scan;
135+
switch (t.transfer) {
136+
case (?transfer) {
137+
switch (merchantStore.get(transfer.to.owner.toText())) {
138+
case (?merchant) {
139+
// A payment for a merchant with notifications enabled: emit a
140+
// canister log (observable via `icp canister logs backend`) marking
141+
// where a real notification would be sent. To actually send one,
142+
// use HTTPS outcalls —
143+
// https://docs.internetcomputer.org/guides/backends/https-outcalls
144+
if (merchant.email_notifications or merchant.phone_notifications) {
145+
if (not metadataLoaded) await loadMetadataOnce<system>();
146+
Debug.print(
147+
"Payment of " # formatAmount(transfer.amount) # " received by merchant '" # merchant.name #
148+
"' from " # transfer.from.owner.toText() #
149+
". A notification could be sent here via HTTPS outcalls."
150+
);
151+
};
152+
};
153+
case null {};
154+
};
155+
};
156+
case null {};
157+
};
158+
};
159+
};
160+
};

motoko/ic-pos/backend/backend.did

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
type Response =
2+
record {
3+
data: opt Merchant;
4+
error_text: opt text;
5+
status: nat16;
6+
status_text: text;
7+
};
8+
type Merchant =
9+
record {
10+
email_address: text;
11+
email_notifications: bool;
12+
name: text;
13+
phone_notifications: bool;
14+
phone_number: text;
15+
};
16+
type Main =
17+
service {
18+
getMerchant: () -> (Response) query;
19+
updateMerchant: (merchant: Merchant) -> (Response);
20+
};
21+
service : (_startBlock: nat) -> Main

motoko/ic-pos/canister_ids.json

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

motoko/ic-pos/components.json

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

motoko/ic-pos/deploy.sh

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
#!/usr/bin/env bash
2+
set -e
3+
4+
# Local deploy for ic-pos. Use `bash deploy.sh` (not `icp deploy`): the ICRC-1
5+
# ledger and index require init args (minting account, initial balances, ledger
6+
# id) that are only known after identities exist.
7+
#
8+
# For mainnet, deploy with `icp deploy -e ic` instead — the ledger, index, and
9+
# Internet Identity are not deployed there (the app uses the shared TICRC1 test
10+
# token and the production II; see icp.yaml and the README).
11+
#
12+
# Prerequisite: a running local network (`icp network start -d`).
13+
14+
# Two dedicated identities:
15+
# - ic-pos-minter: the ledger's minting account (mints the initial supply).
16+
# - ic-pos-dev: a normal, pre-funded holder you pay merchants from.
17+
# These must be DIFFERENT identities — and independent of your current default —
18+
# so that transfers from ic-pos-dev are real "transfer"s (debited, with a fee)
19+
# that the backend monitor picks up. If the minter and the holder were the same
20+
# identity, its "transfers" would be mints: no debit, no fee, and the monitor
21+
# (which only reacts to transfers) would ignore them.
22+
icp identity new ic-pos-minter --storage plaintext 2>/dev/null || true
23+
icp identity new ic-pos-dev --storage plaintext 2>/dev/null || true
24+
MINTER=$(icp identity principal --identity ic-pos-minter)
25+
DEV=$(icp identity principal --identity ic-pos-dev)
26+
27+
# 1. ICRC-1 ledger — minting account = ic-pos-minter; ic-pos-dev pre-funded.
28+
# Named distinctly from the shared mainnet TICRC1 token to make clear this is a
29+
# throwaway local ledger, not the real thing.
30+
icp deploy icrc1_ledger --mode reinstall -y --args "(variant { Init = record { \
31+
token_name = \"Local ICRC-1\"; \
32+
token_symbol = \"LICRC1\"; \
33+
minting_account = record { owner = principal \"$MINTER\" }; \
34+
initial_balances = vec { record { record { owner = principal \"$DEV\" }; 1_000_000_000_000 : nat } }; \
35+
metadata = vec {}; \
36+
transfer_fee = 10_000 : nat; \
37+
archive_options = record { \
38+
trigger_threshold = 2000 : nat64; \
39+
num_blocks_to_archive = 1000 : nat64; \
40+
controller_id = principal \"$MINTER\" }; \
41+
feature_flags = opt record { icrc2 = true } } })"
42+
43+
# 2. ICRC-1 index — points at the ledger we just deployed.
44+
LEDGER_ID=$(icp canister status icrc1_ledger -i)
45+
icp deploy icrc1_index --mode reinstall -y --args "(opt variant { Init = record { \
46+
ledger_id = principal \"$LEDGER_ID\"; \
47+
retrieve_blocks_from_ledger_interval_seconds = opt (1 : nat64) } })"
48+
49+
# 3. Backend and frontend (init args come from icp.yaml). Internet Identity is
50+
# provided by the local network (ii: true in icp.yaml), not deployed here.
51+
icp deploy backend
52+
icp deploy frontend
53+
54+
echo
55+
echo "Deployed. The test tokens are held by the 'ic-pos-dev' identity"
56+
echo "(your default identity has none). Pass --identity ic-pos-dev to spend"
57+
echo "them — no need to change your selected identity:"
58+
echo
59+
echo " # check the balance"
60+
echo " icp token \$(icp canister status icrc1_ledger -i) balance --identity ic-pos-dev"
61+
echo
62+
echo " # pay a merchant 1 LICRC1 (amounts are in base units; 8 decimals),"
63+
echo " # a real transfer the backend monitor picks up"
64+
echo " icp canister call icrc1_ledger icrc1_transfer \\"
65+
echo " '(record { to = record { owner = principal \"<MERCHANT_PRINCIPAL>\" }; amount = 100_000_000 : nat })' \\"
66+
echo " --identity ic-pos-dev"

0 commit comments

Comments
 (0)