Skip to content

Commit c75a381

Browse files
feat: draft of a reference implementation (#27)
This change introduces a draft of the reference implementation for the standard. The implementation is not well tested yet but appears to work for basic flows.
1 parent dba8bfe commit c75a381

File tree

7 files changed

+377
-1
lines changed

7 files changed

+377
-1
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,4 @@ jobs:
2828
2929
- name: Test
3030
run: |
31-
"${GITHUB_WORKSPACE}/bin/bazel" test //... --test_output=all
31+
"${GITHUB_WORKSPACE}/bin/bazel" test //... --test_output=errors

BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ load("//bazel:didc_test.bzl", "didc_check_test", "didc_subtype_test")
22

33
package(default_visibility = ["//visibility:public"])
44

5+
exports_files(["ICRC-1.did"])
6+
57
genrule(
68
name = "candid",
79
srcs = [":README.md"],

WORKSPACE.bazel

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,27 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
22
load("@bazel_tools//tools/build_defs/repo:git.bzl", "new_git_repository")
33
load("//bazel:didc_repo.bzl", "didc_repository")
44

5+
http_archive(
6+
name = "rules_motoko",
7+
sha256 = "9b677fc5d3b42749d13b7734b3a87d4d40135499a189e843ae3f183965e255b7",
8+
strip_prefix = "rules_motoko-0.1.0",
9+
urls = ["https://github.com/dfinity/rules_motoko/archive/refs/tags/v0.1.0.zip"],
10+
)
11+
12+
http_archive(
13+
name = "motoko_base",
14+
build_file_content = """
15+
filegroup(name = "sources", srcs = glob(["*.mo"]), visibility = ["//visibility:public"])
16+
""",
17+
sha256 = "582d1c90faa65047354ae7530f09160dd7e04882991287ced7ea7a72bd89d06e",
18+
strip_prefix = "motoko-base-moc-0.6.24/src",
19+
urls = ["https://github.com/dfinity/motoko-base/archive/refs/tags/moc-0.6.24.zip"],
20+
)
21+
22+
load("@rules_motoko//motoko:repositories.bzl", "rules_motoko_dependencies")
23+
24+
rules_motoko_dependencies()
25+
526
http_archive(
627
name = "io_bazel_rules_go",
728
sha256 = "16e9fca53ed6bd4ff4ad76facc9b7b651a89db1689a2877d6fd7b82aa824e366",

bazel/didc_test.bzl

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
load("@rules_motoko//motoko:defs.bzl", "MotokoActorInfo")
2+
13
def _didc_check_impl(ctx):
24
didc = ctx.executable._didc
35
script = "\n".join(
@@ -51,3 +53,14 @@ didc_subtype_test = rule(
5153
},
5254
test = True,
5355
)
56+
57+
def _mo_actor_did_impl(ctx):
58+
did_file = ctx.attr.actor[MotokoActorInfo].didl
59+
return [DefaultInfo(files = depset([did_file]))]
60+
61+
motoko_actor_did_file = rule(
62+
implementation = _mo_actor_did_impl,
63+
attrs = {
64+
"actor": attr.label(providers = [MotokoActorInfo]),
65+
},
66+
)

ref/BUILD.bazel

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
load("@rules_motoko//motoko:defs.bzl", "motoko_binary", "motoko_library")
2+
load("//bazel:didc_test.bzl", "didc_subtype_test", "motoko_actor_did_file")
3+
4+
motoko_library(
5+
name = "base",
6+
srcs = ["@motoko_base//:sources"],
7+
)
8+
9+
motoko_binary(
10+
name = "icrc1_ref",
11+
entry = "ICRC1.mo",
12+
deps = [":base"],
13+
)
14+
15+
motoko_actor_did_file(
16+
name = "icrc1_ref_did",
17+
actor = ":icrc1_ref",
18+
)
19+
20+
didc_subtype_test(
21+
name = "ref_candid_check",
22+
did = ":icrc1_ref_did",
23+
previous = "//:ICRC-1.did",
24+
)

ref/ICRC1.mo

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
import Array "mo:base/Array";
2+
import Blob "mo:base/Blob";
3+
import Buffer "mo:base/Buffer";
4+
import Principal "mo:base/Principal";
5+
import Option "mo:base/Option";
6+
import Error "mo:base/Error";
7+
import Time "mo:base/Time";
8+
import Int "mo:base/Int";
9+
import Nat8 "mo:base/Nat8";
10+
import Nat64 "mo:base/Nat64";
11+
12+
13+
actor class Ledger(init : {
14+
initial_mints : [ { account : { principal : Principal; subaccount : ?Blob }; amount : Nat } ];
15+
minting_account : { principal : Principal; subaccount : ?Blob };
16+
token_name : Text;
17+
token_symbol : Text;
18+
decimals : Nat8;
19+
transfer_fee : Nat;
20+
}) = this {
21+
22+
public type Account = { principal : Principal; subaccount : ?Subaccount };
23+
public type Subaccount = Blob;
24+
public type Tokens = Nat;
25+
public type Memo = Nat64;
26+
public type Timestamp = Nat64;
27+
public type Duration = Nat64;
28+
public type TxIndex = Nat;
29+
public type TxLog = Buffer.Buffer<Transaction>;
30+
31+
public type Value = { #Nat : Nat; #Int : Int; #Blob : Blob; #Text : Text; };
32+
33+
let permittedDriftNanos : Duration = 60_000_000_000;
34+
let transactionWindowNanos : Duration = 24 * 60 * 60 * 1_000_000_000;
35+
let defaultSubaccount : Subaccount = Blob.fromArrayMut(Array.init(32, 0 : Nat8));
36+
37+
public type TxKind = { #Burn; #Mint; #Transfer };
38+
39+
public type Transfer = {
40+
to : Account;
41+
from : Account;
42+
memo : ?Memo;
43+
amount : Tokens;
44+
fee : ?Tokens;
45+
created_at_time : ?Timestamp;
46+
};
47+
48+
public type Transaction = {
49+
args : Transfer;
50+
kind : TxKind;
51+
// Effective fee for this transaction.
52+
fee : Tokens;
53+
timestamp : Timestamp;
54+
};
55+
56+
public type TransferError = {
57+
#BadFee : { expected_fee : Tokens };
58+
#BadBurn : { min_burn_amount : Tokens };
59+
#InsufficientFunds : { balance : Tokens };
60+
#TooOld : { allowed_window_nanos : Duration };
61+
#CreatedInFuture;
62+
#Duplicate : { duplicate_of : TxIndex };
63+
#GenericError : { error_code : Nat; message : Text };
64+
};
65+
66+
public type TransferResult = {
67+
#Ok : TxIndex;
68+
#Err : TransferError;
69+
};
70+
71+
// Checks whether two accounts are semantically equal.
72+
func accountsEqual(lhs : Account, rhs : Account) : Bool {
73+
let lhsSubaccount = Option.get(lhs.subaccount, defaultSubaccount);
74+
let rhsSubaccount = Option.get(rhs.subaccount, defaultSubaccount);
75+
76+
Principal.equal(lhs.principal, rhs.principal) and Blob.equal(lhsSubaccount, rhsSubaccount)
77+
};
78+
79+
// Computes the balance of the specified account.
80+
func balance(account : Account, log : TxLog) : Nat {
81+
var sum = 0;
82+
for (tx in log.vals()) {
83+
switch (tx.kind) {
84+
case (#Burn) {
85+
if (accountsEqual(tx.args.from, account)) { sum -= tx.args.amount };
86+
};
87+
case (#Mint) {
88+
if (accountsEqual(tx.args.to, account)) { sum += tx.args.amount };
89+
};
90+
case (#Transfer) {
91+
if (accountsEqual(tx.args.from, account)) { sum -= tx.args.amount + tx.fee };
92+
if (accountsEqual(tx.args.to, account)) { sum += tx.args.amount };
93+
};
94+
}
95+
};
96+
sum
97+
};
98+
99+
// Computes the total token supply.
100+
func totalSupply(log: TxLog) : Tokens {
101+
var total = 0;
102+
for (tx in log.vals()) {
103+
switch (tx.kind) {
104+
case (#Burn) { total -= tx.args.amount };
105+
case (#Mint) { total += tx.args.amount };
106+
case (#Transfer) { total -= tx.fee };
107+
}
108+
};
109+
total
110+
};
111+
112+
// Finds a transaction in the transaction log within the (now - tx_window) time frame.
113+
func findTransfer(transfer : Transfer, log : TxLog, now : Timestamp) : ?TxIndex {
114+
var i = 0;
115+
for (tx in log.vals()) {
116+
if (tx.args == transfer
117+
and (tx.timestamp < now + permittedDriftNanos)
118+
and (now - tx.timestamp) < transactionWindowNanos + permittedDriftNanos) { return ?i; };
119+
i += 1;
120+
};
121+
null
122+
};
123+
124+
// Checks if the principal is anonymous.
125+
func isAnonymous(p : Principal) : Bool {
126+
Blob.equal(Principal.toBlob(p), Blob.fromArray([0x04]))
127+
};
128+
129+
// Constructs the transaction log corresponding to the init argument.
130+
func makeGenesisChain() : TxLog {
131+
validateSubaccount(init.minting_account.subaccount);
132+
133+
let now = Nat64.fromNat(Int.abs(Time.now()));
134+
let log = Buffer.Buffer<Transaction>(100);
135+
for ({account; amount} in Array.vals(init.initial_mints)) {
136+
validateSubaccount(account.subaccount);
137+
let tx : Transaction = {
138+
args = {
139+
from = init.minting_account;
140+
to = account;
141+
amount = amount;
142+
fee = null;
143+
memo = null;
144+
created_at_time = ?now;
145+
};
146+
kind = #Mint;
147+
fee = 0;
148+
timestamp = now;
149+
};
150+
log.add(tx);
151+
};
152+
log
153+
};
154+
155+
// Traps if the specified blob is not a valid subaccount.
156+
func validateSubaccount(s : ?Subaccount) {
157+
let subaccount = Option.get(s, defaultSubaccount);
158+
assert (subaccount.size() == 32);
159+
};
160+
161+
// The list of all transactions.
162+
var log : TxLog = makeGenesisChain();
163+
164+
// The stable representation of the transaction log.
165+
// Used only during upgrades.
166+
stable var persistedLog : [Transaction] = [];
167+
168+
system func preupgrade() {
169+
persistedLog := log.toArray();
170+
};
171+
172+
system func postupgrade() {
173+
log := Buffer.Buffer(persistedLog.size());
174+
for (tx in Array.vals(persistedLog)) {
175+
log.add(tx);
176+
};
177+
};
178+
179+
public shared({ caller }) func icrc1_transfer({
180+
from_subaccount : ?Subaccount;
181+
to : Account;
182+
amount : Tokens;
183+
fee : ?Tokens;
184+
memo : ?Memo;
185+
created_at_time : ?Timestamp;
186+
}) : async TransferResult {
187+
if (isAnonymous(caller)) {
188+
throw Error.reject("anonymous user is not allowed to transfer funds");
189+
};
190+
191+
let now = Nat64.fromNat(Int.abs(Time.now()));
192+
193+
let txTime : Timestamp = Option.get(created_at_time, now);
194+
195+
if ((txTime > now) and (txTime - now > permittedDriftNanos)) {
196+
return #Err(#CreatedInFuture);
197+
};
198+
199+
if ((txTime < now) and (now - txTime > transactionWindowNanos + permittedDriftNanos)) {
200+
return #Err(#TooOld { allowed_window_nanos = transactionWindowNanos });
201+
};
202+
203+
validateSubaccount(from_subaccount);
204+
validateSubaccount(to.subaccount);
205+
206+
let from = { principal = caller; subaccount = from_subaccount };
207+
208+
let args : Transfer = {
209+
from = from;
210+
to = to;
211+
amount = amount;
212+
memo = memo;
213+
fee = fee;
214+
created_at_time = created_at_time;
215+
};
216+
217+
switch (findTransfer(args, log, now)) {
218+
case (?height) { return #Err(#Duplicate { duplicate_of = height }) };
219+
case null { };
220+
};
221+
222+
let minter = init.minting_account;
223+
224+
let (kind, effectiveFee) = if (accountsEqual(from, minter)) {
225+
if (Option.get(fee, 0) != 0) {
226+
return #Err(#BadFee { expected_fee = 0 });
227+
};
228+
(#Mint, 0)
229+
} else if (accountsEqual(to, minter)) {
230+
if (Option.get(fee, 0) != 0) {
231+
return #Err(#BadFee { expected_fee = 0 });
232+
};
233+
234+
if (amount < init.transfer_fee) {
235+
return #Err(#BadBurn { min_burn_amount = init.transfer_fee });
236+
};
237+
238+
let debitBalance = balance(from, log);
239+
if (debitBalance < amount) {
240+
return #Err(#InsufficientFunds { balance = debitBalance });
241+
};
242+
243+
(#Burn, 0)
244+
} else {
245+
let effectiveFee = init.transfer_fee;
246+
if (Option.get(fee, effectiveFee) != effectiveFee) {
247+
return #Err(#BadFee { expected_fee = init.transfer_fee });
248+
};
249+
250+
let debitBalance = balance(from, log);
251+
if (debitBalance < amount + effectiveFee) {
252+
return #Err(#InsufficientFunds { balance = debitBalance });
253+
};
254+
255+
(#Transfer, effectiveFee)
256+
};
257+
258+
let tx : Transaction = {
259+
args = args;
260+
kind = kind;
261+
fee = effectiveFee;
262+
timestamp = now;
263+
};
264+
265+
let txIndex = log.size();
266+
log.add(tx);
267+
#Ok(txIndex)
268+
};
269+
270+
public query func icrc1_balance_of(account : Account) : async Tokens {
271+
balance(account, log)
272+
};
273+
274+
public query func icrc1_total_supply() : async Tokens {
275+
totalSupply(log)
276+
};
277+
278+
public query func icrc1_name() : async Text {
279+
init.token_name
280+
};
281+
282+
public query func icrc1_symbol() : async Text {
283+
init.token_symbol
284+
};
285+
286+
public query func icrc1_decimals() : async Nat8 {
287+
init.decimals
288+
};
289+
290+
public query func icrc1_metadata() : async [(Text, Value)] {
291+
[
292+
("icrc1:name", #Text(init.token_name)),
293+
("icrc1:symbol", #Text(init.token_symbol)),
294+
("icrc1:decimals", #Nat(Nat8.toNat(init.decimals))),
295+
]
296+
};
297+
298+
public query func icrc1_supported_standards() : async [{ name : Text; url : Text }] {
299+
[
300+
{ name = "ICRC-1"; url = "https://github.com/dfinity/ICRC-1" }
301+
]
302+
};
303+
}

0 commit comments

Comments
 (0)