From 8ba628a90182263bd8794e96ae77fc81a828d70e Mon Sep 17 00:00:00 2001 From: JingJingZhang9 Date: Mon, 19 Jun 2023 23:37:06 +0800 Subject: [PATCH] Add ICRC2 project --- I2-code/LICENSE | 21 + I2-code/dfx.json | 21 + I2-code/docs/ICRC1/Account.html | 8 + I2-code/docs/ICRC1/Account.md | 43 ++ I2-code/docs/ICRC1/ArchiveApi.html | 6 + I2-code/docs/ICRC1/ArchiveApi.md | 23 + I2-code/docs/ICRC1/Canisters/Token.html | 3 + I2-code/docs/ICRC1/Canisters/Token.md | 108 +++ I2-code/docs/ICRC1/Transfer.html | 9 + I2-code/docs/ICRC1/Transfer.md | 45 ++ I2-code/docs/ICRC1/Types.html | 14 + I2-code/docs/ICRC1/Types.md | 271 ++++++++ I2-code/docs/ICRC1/Utils.html | 4 + I2-code/docs/ICRC1/Utils.md | 93 +++ I2-code/docs/ICRC1/lib.html | 26 + I2-code/docs/ICRC1/lib.md | 332 +++++++++ I2-code/docs/index.html | 2 + I2-code/docs/index.md | 7 + I2-code/docs/styles.css | 162 +++++ I2-code/example/.gitignore | 15 + I2-code/example/dfx.json | 23 + I2-code/example/icrc1/main.mo | 97 +++ I2-code/icrc1-default-args.txt | 19 + I2-code/makefile | 28 + I2-code/mops.toml | 13 + I2-code/package-set.dhall | 40 ++ I2-code/readme.md | 94 +++ I2-code/src/ICRC1/Account.mo | 188 +++++ I2-code/src/ICRC1/Canisters/Archive.mo | 238 +++++++ I2-code/src/ICRC1/Canisters/Token.mo | 106 +++ I2-code/src/ICRC1/Transfer.mo | 465 +++++++++++++ I2-code/src/ICRC1/Types.mo | 475 +++++++++++++ I2-code/src/ICRC1/Utils.mo | 500 +++++++++++++ I2-code/src/ICRC1/lib.mo | 562 +++++++++++++++ I2-code/tests/ActorTest.mo | 27 + I2-code/tests/ICRC1/Account.Test.mo | 223 ++++++ I2-code/tests/ICRC1/Archive.ActorTest.mo | 135 ++++ I2-code/tests/ICRC1/ICRC1.ActorTest.mo | 847 +++++++++++++++++++++++ I2-code/tests/test_template.md | 31 + I2-code/tests/utils/ActorSpec.mo | 179 +++++ I2-code/vessel.dhall | 4 + 41 files changed, 5507 insertions(+) create mode 100644 I2-code/LICENSE create mode 100644 I2-code/dfx.json create mode 100644 I2-code/docs/ICRC1/Account.html create mode 100644 I2-code/docs/ICRC1/Account.md create mode 100644 I2-code/docs/ICRC1/ArchiveApi.html create mode 100644 I2-code/docs/ICRC1/ArchiveApi.md create mode 100644 I2-code/docs/ICRC1/Canisters/Token.html create mode 100644 I2-code/docs/ICRC1/Canisters/Token.md create mode 100644 I2-code/docs/ICRC1/Transfer.html create mode 100644 I2-code/docs/ICRC1/Transfer.md create mode 100644 I2-code/docs/ICRC1/Types.html create mode 100644 I2-code/docs/ICRC1/Types.md create mode 100644 I2-code/docs/ICRC1/Utils.html create mode 100644 I2-code/docs/ICRC1/Utils.md create mode 100644 I2-code/docs/ICRC1/lib.html create mode 100644 I2-code/docs/ICRC1/lib.md create mode 100644 I2-code/docs/index.html create mode 100644 I2-code/docs/index.md create mode 100644 I2-code/docs/styles.css create mode 100644 I2-code/example/.gitignore create mode 100644 I2-code/example/dfx.json create mode 100644 I2-code/example/icrc1/main.mo create mode 100644 I2-code/icrc1-default-args.txt create mode 100644 I2-code/makefile create mode 100644 I2-code/mops.toml create mode 100644 I2-code/package-set.dhall create mode 100644 I2-code/readme.md create mode 100644 I2-code/src/ICRC1/Account.mo create mode 100644 I2-code/src/ICRC1/Canisters/Archive.mo create mode 100644 I2-code/src/ICRC1/Canisters/Token.mo create mode 100644 I2-code/src/ICRC1/Transfer.mo create mode 100644 I2-code/src/ICRC1/Types.mo create mode 100644 I2-code/src/ICRC1/Utils.mo create mode 100644 I2-code/src/ICRC1/lib.mo create mode 100644 I2-code/tests/ActorTest.mo create mode 100644 I2-code/tests/ICRC1/Account.Test.mo create mode 100644 I2-code/tests/ICRC1/Archive.ActorTest.mo create mode 100644 I2-code/tests/ICRC1/ICRC1.ActorTest.mo create mode 100644 I2-code/tests/test_template.md create mode 100644 I2-code/tests/utils/ActorSpec.mo create mode 100644 I2-code/vessel.dhall diff --git a/I2-code/LICENSE b/I2-code/LICENSE new file mode 100644 index 00000000..100acba5 --- /dev/null +++ b/I2-code/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Tomi Jaga + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/I2-code/dfx.json b/I2-code/dfx.json new file mode 100644 index 00000000..82ce98fc --- /dev/null +++ b/I2-code/dfx.json @@ -0,0 +1,21 @@ +{ + "version": 1, + "canisters": { + "icrc1": { + "type": "motoko", + "main": "src/ICRC1/Canisters/Token.mo" + }, + "test": { + "type": "motoko", + "main": "tests/ActorTest.mo", + "args": "-v --compacting-gc" + } + }, + "defaults": { + "build": { + "packtool": "mops sources", + "args": "" + } + } + +} diff --git a/I2-code/docs/ICRC1/Account.html b/I2-code/docs/ICRC1/Account.html new file mode 100644 index 00000000..c1860603 --- /dev/null +++ b/I2-code/docs/ICRC1/Account.html @@ -0,0 +1,8 @@ + +

ICRC1/Account

public func validate_subaccount(subaccount : ?T.Subaccount) : Bool

Checks if a subaccount is valid

+

public func validate(account : T.Account) : Bool

Checks if an account is valid

+

public func encode() : T.EncodedAccount

Implementation of ICRC1's Textual representation of accounts Encoding Standard

+

public func decode(encoded : T.EncodedAccount) : ?T.Account

Implementation of ICRC1's Textual representation of accounts Decoding Standard

+

public func fromText(encoded : Text) : ?T.Account

Converts an ICRC-1 Account from its Textual representation to the Account type

+

public func toText(account : T.Account) : Text

Converts an ICRC-1 Account to its Textual representation

+

\ No newline at end of file diff --git a/I2-code/docs/ICRC1/Account.md b/I2-code/docs/ICRC1/Account.md new file mode 100644 index 00000000..9c4c8497 --- /dev/null +++ b/I2-code/docs/ICRC1/Account.md @@ -0,0 +1,43 @@ +# ICRC1/Account + +## Function `validate_subaccount` +``` motoko no-repl +func validate_subaccount(subaccount : ?T.Subaccount) : Bool +``` + +Checks if a subaccount is valid + +## Function `validate` +``` motoko no-repl +func validate(account : T.Account) : Bool +``` + +Checks if an account is valid + +## Function `encode` +``` motoko no-repl +func encode() : T.EncodedAccount +``` + +Implementation of ICRC1's Textual representation of accounts [Encoding Standard](https://github.com/dfinity/ICRC-1/tree/main/standards/ICRC-1#encoding) + +## Function `decode` +``` motoko no-repl +func decode(encoded : T.EncodedAccount) : ?T.Account +``` + +Implementation of ICRC1's Textual representation of accounts [Decoding Standard](https://github.com/dfinity/ICRC-1/tree/main/standards/ICRC-1#decoding) + +## Function `fromText` +``` motoko no-repl +func fromText(encoded : Text) : ?T.Account +``` + +Converts an ICRC-1 Account from its Textual representation to the `Account` type + +## Function `toText` +``` motoko no-repl +func toText(account : T.Account) : Text +``` + +Converts an ICRC-1 `Account` to its Textual representation diff --git a/I2-code/docs/ICRC1/ArchiveApi.html b/I2-code/docs/ICRC1/ArchiveApi.html new file mode 100644 index 00000000..68183432 --- /dev/null +++ b/I2-code/docs/ICRC1/ArchiveApi.html @@ -0,0 +1,6 @@ + +

ICRC1/ArchiveApi

public func create_canister() : async T.ArchiveInterface

creates a new archive canister

+

public func total_txs(archives : T.StableBuffer<T.ArchiveData>) : Nat

Get the total number of archived transactions

+

public func append_transactions(token : T.TokenData) : async ()

Moves the transactions from the ICRC1 canister to the archive canister +and returns a boolean that indicates the success of the data transfer

+

\ No newline at end of file diff --git a/I2-code/docs/ICRC1/ArchiveApi.md b/I2-code/docs/ICRC1/ArchiveApi.md new file mode 100644 index 00000000..c2e7b121 --- /dev/null +++ b/I2-code/docs/ICRC1/ArchiveApi.md @@ -0,0 +1,23 @@ +# ICRC1/ArchiveApi + +## Function `create_canister` +``` motoko no-repl +func create_canister() : async T.ArchiveInterface +``` + +creates a new archive canister + +## Function `total_txs` +``` motoko no-repl +func total_txs(archives : T.StableBuffer) : Nat +``` + +Get the total number of archived transactions + +## Function `append_transactions` +``` motoko no-repl +func append_transactions(token : T.TokenData) : async () +``` + +Moves the transactions from the ICRC1 canister to the archive canister +and returns a boolean that indicates the success of the data transfer diff --git a/I2-code/docs/ICRC1/Canisters/Token.html b/I2-code/docs/ICRC1/Canisters/Token.html new file mode 100644 index 00000000..8d7fc6bb --- /dev/null +++ b/I2-code/docs/ICRC1/Canisters/Token.html @@ -0,0 +1,3 @@ + +

ICRC1/Canisters/Token

type TokenInitArgs = { name : Text; symbol : Text; decimals : Nat8; fee : ICRC1.Balance; max_supply : ICRC1.Balance; minting_account : ?ICRC1.Account; initial_balances : [(ICRC1.Account, ICRC1.Balance)] }

actor class Token(token_args : TokenInitArgs)

public func icrc1_name() : async Text

Functions for the ICRC1 token standard

+

public func icrc1_symbol() : async Text

public func icrc1_decimals() : async Nat8

public func icrc1_fee() : async ICRC1.Balance

public func icrc1_metadata() : async [ICRC1.MetaDatum]

public func icrc1_total_supply() : async ICRC1.Balance

public func icrc1_minting_account() : async ?ICRC1.Account

public func icrc1_balance_of(args : ICRC1.Account) : async ICRC1.Balance

public func icrc1_supported_standards() : async [ICRC1.SupportedStandard]

public func icrc1_transfer(args : ICRC1.TransferArgs) : async Result.Result<ICRC1.Balance, ICRC1.TransferError>

public func mint(args : ICRC1.Mint) : async Result.Result<ICRC1.Balance, ICRC1.TransferError>

public func burn(args : ICRC1.BurnArgs) : async Result.Result<ICRC1.Balance, ICRC1.TransferError>

public func get_transaction(token_id : Nat) : async ?ICRC1.Transaction

public func get_transactions(req : ICRC1.GetTransactionsRequest) : async ()

\ No newline at end of file diff --git a/I2-code/docs/ICRC1/Canisters/Token.md b/I2-code/docs/ICRC1/Canisters/Token.md new file mode 100644 index 00000000..a4bef013 --- /dev/null +++ b/I2-code/docs/ICRC1/Canisters/Token.md @@ -0,0 +1,108 @@ +# ICRC1/Canisters/Token + +## Type `TokenInitArgs` +``` motoko no-repl +type TokenInitArgs = { name : Text; symbol : Text; decimals : Nat8; fee : ICRC1.Balance; max_supply : ICRC1.Balance; minting_account : ?ICRC1.Account; initial_balances : [(ICRC1.Account, ICRC1.Balance)] } +``` + + +## `actor class Token` + + +### Function `icrc1_name` +``` motoko no-repl +func icrc1_name() : async Text +``` + +Functions for the ICRC1 token standard + + +### Function `icrc1_symbol` +``` motoko no-repl +func icrc1_symbol() : async Text +``` + + + +### Function `icrc1_decimals` +``` motoko no-repl +func icrc1_decimals() : async Nat8 +``` + + + +### Function `icrc1_fee` +``` motoko no-repl +func icrc1_fee() : async ICRC1.Balance +``` + + + +### Function `icrc1_metadata` +``` motoko no-repl +func icrc1_metadata() : async [ICRC1.MetaDatum] +``` + + + +### Function `icrc1_total_supply` +``` motoko no-repl +func icrc1_total_supply() : async ICRC1.Balance +``` + + + +### Function `icrc1_minting_account` +``` motoko no-repl +func icrc1_minting_account() : async ?ICRC1.Account +``` + + + +### Function `icrc1_balance_of` +``` motoko no-repl +func icrc1_balance_of(args : ICRC1.Account) : async ICRC1.Balance +``` + + + +### Function `icrc1_supported_standards` +``` motoko no-repl +func icrc1_supported_standards() : async [ICRC1.SupportedStandard] +``` + + + +### Function `icrc1_transfer` +``` motoko no-repl +func icrc1_transfer(args : ICRC1.TransferArgs) : async Result.Result +``` + + + +### Function `mint` +``` motoko no-repl +func mint(args : ICRC1.Mint) : async Result.Result +``` + + + +### Function `burn` +``` motoko no-repl +func burn(args : ICRC1.BurnArgs) : async Result.Result +``` + + + +### Function `get_transaction` +``` motoko no-repl +func get_transaction(token_id : Nat) : async ?ICRC1.Transaction +``` + + + +### Function `get_transactions` +``` motoko no-repl +func get_transactions(req : ICRC1.GetTransactionsRequest) : async () +``` + diff --git a/I2-code/docs/ICRC1/Transfer.html b/I2-code/docs/ICRC1/Transfer.html new file mode 100644 index 00000000..ceccc26b --- /dev/null +++ b/I2-code/docs/ICRC1/Transfer.html @@ -0,0 +1,9 @@ + +

ICRC1/Transfer

public func validate_memo(memo : ?T.Memo) : Bool

Checks if a transaction memo is valid

+

public func is_too_old(token : T.TokenData, created_at_time : Nat64) : Bool

Checks if the created_at_time of a transfer request is before the accepted time range

+

public func is_in_future(token : T.TokenData, created_at_time : Nat64) : Bool

Checks if the created_at_time of a transfer request has not been reached yet relative to the canister's time.

+

public func deduplicate(token : T.TokenData, tx_req : T.TransactionRequest) : Result.Result<(), Nat>

Checks if there is a duplicate transaction that matches the transfer request in the main canister.

+

If a duplicate is found, the function returns an error (#err) with the duplicate transaction's index.

+

public func validate_fee(token : T.TokenData, opt_fee : ?T.Balance) : Bool

Checks if a transfer fee is valid

+

public func validate_request(token : T.TokenData, tx_req : T.TransactionRequest) : Result.Result<(), T.TransferError>

Checks if a transfer request is valid

+

\ No newline at end of file diff --git a/I2-code/docs/ICRC1/Transfer.md b/I2-code/docs/ICRC1/Transfer.md new file mode 100644 index 00000000..4972eb11 --- /dev/null +++ b/I2-code/docs/ICRC1/Transfer.md @@ -0,0 +1,45 @@ +# ICRC1/Transfer + +## Function `validate_memo` +``` motoko no-repl +func validate_memo(memo : ?T.Memo) : Bool +``` + +Checks if a transaction memo is valid + +## Function `is_too_old` +``` motoko no-repl +func is_too_old(token : T.TokenData, created_at_time : Nat64) : Bool +``` + +Checks if the `created_at_time` of a transfer request is before the accepted time range + +## Function `is_in_future` +``` motoko no-repl +func is_in_future(token : T.TokenData, created_at_time : Nat64) : Bool +``` + +Checks if the `created_at_time` of a transfer request has not been reached yet relative to the canister's time. + +## Function `deduplicate` +``` motoko no-repl +func deduplicate(token : T.TokenData, tx_req : T.TransactionRequest) : Result.Result<(), Nat> +``` + +Checks if there is a duplicate transaction that matches the transfer request in the main canister. + +If a duplicate is found, the function returns an error (`#err`) with the duplicate transaction's index. + +## Function `validate_fee` +``` motoko no-repl +func validate_fee(token : T.TokenData, opt_fee : ?T.Balance) : Bool +``` + +Checks if a transfer fee is valid + +## Function `validate_request` +``` motoko no-repl +func validate_request(token : T.TokenData, tx_req : T.TransactionRequest) : Result.Result<(), T.TransferError> +``` + +Checks if a transfer request is valid diff --git a/I2-code/docs/ICRC1/Types.html b/I2-code/docs/ICRC1/Types.html new file mode 100644 index 00000000..0ccc9079 --- /dev/null +++ b/I2-code/docs/ICRC1/Types.html @@ -0,0 +1,14 @@ + +

ICRC1/Types

type Value = {#Nat : Nat; #Int : Int; #Blob : Blob; #Text : Text}

type BlockIndex = Nat

type Subaccount = Blob

type Balance = Nat

type StableBuffer<T> = StableBuffer.StableBuffer<T>

type StableTrieMap<K, V> = STMap.StableTrieMap<K, V>

type Account = { owner : Principal; subaccount : ?Subaccount }

type EncodedAccount = Blob

type SupportedStandard = { name : Text; url : Text }

type Memo = Blob

type Timestamp = Nat64

type Duration = Nat64

type TxIndex = Nat

type MetaDatum = (Text, Value)

type MetaData = [MetaDatum]

type TxKind = {#mint; #burn; #transfer}

type Mint = { to : Account; amount : Balance; memo : ?Blob; created_at_time : ?Nat64 }

type BurnArgs = { from_subaccount : ?Subaccount; amount : Balance; memo : ?Blob; created_at_time : ?Nat64 }

type Burn = { from : Account; amount : Balance; memo : ?Blob; created_at_time : ?Nat64 }

type TransferArgs = { from_subaccount : ?Subaccount; to : Account; amount : Balance; fee : ?Balance; memo : ?Blob; created_at_time : ?Nat64 }

Arguments for a transfer operation

+

type Transfer = { from : Account; to : Account; amount : Balance; fee : ?Balance; memo : ?Blob; created_at_time : ?Nat64 }

type TransactionRequest = { kind : TxKind; from : Account; to : Account; amount : Balance; fee : ?Balance; memo : ?Blob; created_at_time : ?Nat64; encoded : { from : EncodedAccount; to : EncodedAccount } }

Internal representation of a transaction request

+

type Transaction = { kind : Text; mint : ?Mint; burn : ?Burn; transfer : ?Transfer; index : TxIndex; timestamp : Timestamp }

type TimeError = {#TooOld; #CreatedInFuture : { ledger_time : Timestamp }}

type TransferError = TimeError or {#BadFee : { expected_fee : Balance }; #BadBurn : { min_burn_amount : Balance }; #InsufficientFunds : { balance : Balance }; #Duplicate : { duplicate_of : TxIndex }; #TemporarilyUnavailable; #GenericError : { error_code : Nat; message : Text }}

type TransferResult = {#Ok : TxIndex; #Err : TransferError}

type TokenInterface = actor { icrc1_name : shared query () -> async Text; icrc1_symbol : shared query () -> async Text; icrc1_decimals : shared query () -> async Nat8; icrc1_fee : shared query () -> async Balance; icrc1_metadata : shared query () -> async MetaData; icrc1_total_supply : shared query () -> async Balance; icrc1_minting_account : shared query () -> async ?Account; icrc1_balance_of : shared query (Account) -> async Balance; icrc1_transfer : shared (TransferArgs) -> async TransferResult; icrc1_supported_standards : shared query () -> async [SupportedStandard] }

Interface for the ICRC token canister

+

type TxCandidBlob = Blob

type ArchiveInterface = actor { append_transactions : shared ([Transaction]) -> async Result.Result<(), Text>; total_transactions : shared query () -> async Nat; get_transaction : shared query (TxIndex) -> async ?Transaction; get_transactions : shared query (GetTransactionsRequest) -> async TransactionRange; remaining_capacity : shared query () -> async Nat }

The Interface for the Archive canister

+

type InitArgs = { name : Text; symbol : Text; decimals : Nat8; fee : Balance; minting_account : Account; max_supply : Balance; initial_balances : [(Account, Balance)]; min_burn_amount : Balance; advanced_settings : ?AdvancedSettings }

Initial arguments for the setting up the icrc1 token canister

+

type TokenInitArgs = { name : Text; symbol : Text; decimals : Nat8; fee : Balance; max_supply : Balance; initial_balances : [(Account, Balance)]; min_burn_amount : Balance; minting_account : ?Account; advanced_settings : ?AdvancedSettings }

InitArgs with optional fields for initializing a token canister

+

type AdvancedSettings = { burned_tokens : Balance; transaction_window : Timestamp; permitted_drift : Timestamp }

Additional settings for the InitArgs type during initialization of an icrc1 token canister

+

type AccountBalances = StableTrieMap<EncodedAccount, Balance>

type ArchiveData = { var canister : ArchiveInterface; var stored_txs : Nat }

The details of the archive canister

+

type TokenData = { name : Text; symbol : Text; decimals : Nat8; var _fee : Balance; max_supply : Balance; var _minted_tokens : Balance; var _burned_tokens : Balance; minting_account : Account; accounts : AccountBalances; metadata : StableBuffer<MetaDatum>; supported_standards : StableBuffer<SupportedStandard>; transaction_window : Nat; min_burn_amount : Balance; permitted_drift : Nat; transactions : StableBuffer<Transaction>; archive : ArchiveData }

The state of the token canister

+

type GetTransactionsRequest = { start : TxIndex; length : Nat }

The type to request a range of transactions from the ledger canister

+

type TransactionRange = { transactions : [Transaction] }

type QueryArchiveFn = shared query (GetTransactionsRequest) -> async TransactionRange

type ArchivedTransaction = { start : TxIndex; length : Nat; callback : QueryArchiveFn }

type GetTransactionsResponse = { log_length : Nat; first_index : TxIndex; transactions : [Transaction]; archived_transactions : [ArchivedTransaction] }

type RosettaInterface = actor { get_transactions : shared query (GetTransactionsRequest) -> async GetTransactionsResponse }

Functions supported by the rosetta

+

type FullInterface = TokenInterface and RosettaInterface

Interface of the ICRC token and Rosetta canister

+

\ No newline at end of file diff --git a/I2-code/docs/ICRC1/Types.md b/I2-code/docs/ICRC1/Types.md new file mode 100644 index 00000000..eee15ebe --- /dev/null +++ b/I2-code/docs/ICRC1/Types.md @@ -0,0 +1,271 @@ +# ICRC1/Types + +## Type `Value` +``` motoko no-repl +type Value = {#Nat : Nat; #Int : Int; #Blob : Blob; #Text : Text} +``` + + +## Type `BlockIndex` +``` motoko no-repl +type BlockIndex = Nat +``` + + +## Type `Subaccount` +``` motoko no-repl +type Subaccount = Blob +``` + + +## Type `Balance` +``` motoko no-repl +type Balance = Nat +``` + + +## Type `StableBuffer` +``` motoko no-repl +type StableBuffer = StableBuffer.StableBuffer +``` + + +## Type `StableTrieMap` +``` motoko no-repl +type StableTrieMap = STMap.StableTrieMap +``` + + +## Type `Account` +``` motoko no-repl +type Account = { owner : Principal; subaccount : ?Subaccount } +``` + + +## Type `EncodedAccount` +``` motoko no-repl +type EncodedAccount = Blob +``` + + +## Type `SupportedStandard` +``` motoko no-repl +type SupportedStandard = { name : Text; url : Text } +``` + + +## Type `Memo` +``` motoko no-repl +type Memo = Blob +``` + + +## Type `Timestamp` +``` motoko no-repl +type Timestamp = Nat64 +``` + + +## Type `Duration` +``` motoko no-repl +type Duration = Nat64 +``` + + +## Type `TxIndex` +``` motoko no-repl +type TxIndex = Nat +``` + + +## Type `TxLog` +``` motoko no-repl +type TxLog = StableBuffer +``` + + +## Type `MetaDatum` +``` motoko no-repl +type MetaDatum = (Text, Value) +``` + + +## Type `MetaData` +``` motoko no-repl +type MetaData = [MetaDatum] +``` + + +## Type `TxKind` +``` motoko no-repl +type TxKind = {#mint; #burn; #transfer} +``` + + +## Type `Mint` +``` motoko no-repl +type Mint = { to : Account; amount : Balance; memo : ?Blob; created_at_time : ?Nat64 } +``` + + +## Type `BurnArgs` +``` motoko no-repl +type BurnArgs = { from_subaccount : ?Subaccount; amount : Balance; memo : ?Blob; created_at_time : ?Nat64 } +``` + + +## Type `Burn` +``` motoko no-repl +type Burn = { from : Account; amount : Balance; memo : ?Blob; created_at_time : ?Nat64 } +``` + + +## Type `TransferArgs` +``` motoko no-repl +type TransferArgs = { from_subaccount : ?Subaccount; to : Account; amount : Balance; fee : ?Balance; memo : ?Blob; created_at_time : ?Nat64 } +``` + +Arguments for a transfer operation + +## Type `Transfer` +``` motoko no-repl +type Transfer = { from : Account; to : Account; amount : Balance; fee : ?Balance; memo : ?Blob; created_at_time : ?Nat64 } +``` + + +## Type `TransactionRequest` +``` motoko no-repl +type TransactionRequest = { kind : TxKind; from : Account; to : Account; amount : Balance; fee : ?Balance; memo : ?Blob; created_at_time : ?Nat64; encoded : { from : EncodedAccount; to : EncodedAccount } } +``` + +Internal representation of a transaction request + +## Type `Transaction` +``` motoko no-repl +type Transaction = { kind : Text; mint : ?Mint; burn : ?Burn; transfer : ?Transfer; index : TxIndex; timestamp : Timestamp } +``` + + +## Type `TimeError` +``` motoko no-repl +type TimeError = {#TooOld; #CreatedInFuture : { ledger_time : Timestamp }} +``` + + +## Type `TransferError` +``` motoko no-repl +type TransferError = TimeError or {#BadFee : { expected_fee : Balance }; #BadBurn : { min_burn_amount : Balance }; #InsufficientFunds : { balance : Balance }; #Duplicate : { duplicate_of : TxIndex }; #TemporarilyUnavailable; #GenericError : { error_code : Nat; message : Text }} +``` + + +## Type `TransferResult` +``` motoko no-repl +type TransferResult = {#Ok : TxIndex; #Err : TransferError} +``` + + +## Type `TokenInterface` +``` motoko no-repl +type TokenInterface = actor { icrc1_name : shared query () -> async Text; icrc1_symbol : shared query () -> async Text; icrc1_decimals : shared query () -> async Nat8; icrc1_fee : shared query () -> async Balance; icrc1_metadata : shared query () -> async MetaData; icrc1_total_supply : shared query () -> async Balance; icrc1_minting_account : shared query () -> async ?Account; icrc1_balance_of : shared query (Account) -> async Balance; icrc1_transfer : shared (TransferArgs) -> async TransferResult; icrc1_supported_standards : shared query () -> async [SupportedStandard] } +``` + +Interface for the ICRC token canister + +## Type `TxCandidBlob` +``` motoko no-repl +type TxCandidBlob = Blob +``` + + +## Type `ArchiveInterface` +``` motoko no-repl +type ArchiveInterface = actor { append_transactions : shared ([Transaction]) -> async Result.Result<(), Text>; total_transactions : shared query () -> async Nat; get_transaction : shared query (TxIndex) -> async ?Transaction; get_transactions : shared query (GetTransactionsRequest) -> async TransactionRange; remaining_capacity : shared query () -> async Nat } +``` + +The Interface for the Archive canister + +## Type `InitArgs` +``` motoko no-repl +type InitArgs = { name : Text; symbol : Text; decimals : Nat8; fee : Balance; minting_account : Account; max_supply : Balance; initial_balances : [(Account, Balance)]; min_burn_amount : Balance; advanced_settings : ?AdvancedSettings } +``` + +Initial arguments for the setting up the icrc1 token canister + +## Type `TokenInitArgs` +``` motoko no-repl +type TokenInitArgs = { name : Text; symbol : Text; decimals : Nat8; fee : Balance; max_supply : Balance; initial_balances : [(Account, Balance)]; min_burn_amount : Balance; minting_account : ?Account; advanced_settings : ?AdvancedSettings } +``` + +[InitArgs](#type.InitArgs) with optional fields for initializing a token canister + +## Type `AdvancedSettings` +``` motoko no-repl +type AdvancedSettings = { burned_tokens : Balance; transaction_window : Timestamp; permitted_drift : Timestamp } +``` + +Additional settings for the [InitArgs](#type.InitArgs) type during initialization of an icrc1 token canister + +## Type `AccountBalances` +``` motoko no-repl +type AccountBalances = StableTrieMap +``` + + +## Type `ArchiveData` +``` motoko no-repl +type ArchiveData = { var canister : ArchiveInterface; var stored_txs : Nat } +``` + +The details of the archive canister + +## Type `TokenData` +``` motoko no-repl +type TokenData = { name : Text; symbol : Text; decimals : Nat8; var _fee : Balance; max_supply : Balance; var _minted_tokens : Balance; var _burned_tokens : Balance; minting_account : Account; accounts : AccountBalances; metadata : StableBuffer; supported_standards : StableBuffer; transaction_window : Nat; min_burn_amount : Balance; permitted_drift : Nat; transactions : StableBuffer; archive : ArchiveData } +``` + +The state of the token canister + +## Type `GetTransactionsRequest` +``` motoko no-repl +type GetTransactionsRequest = { start : TxIndex; length : Nat } +``` + +The type to request a range of transactions from the ledger canister + +## Type `TransactionRange` +``` motoko no-repl +type TransactionRange = { transactions : [Transaction] } +``` + + +## Type `QueryArchiveFn` +``` motoko no-repl +type QueryArchiveFn = shared query (GetTransactionsRequest) -> async TransactionRange +``` + + +## Type `ArchivedTransaction` +``` motoko no-repl +type ArchivedTransaction = { start : TxIndex; length : Nat; callback : QueryArchiveFn } +``` + + +## Type `GetTransactionsResponse` +``` motoko no-repl +type GetTransactionsResponse = { log_length : Nat; first_index : TxIndex; transactions : [Transaction]; archived_transactions : [ArchivedTransaction] } +``` + + +## Type `RosettaInterface` +``` motoko no-repl +type RosettaInterface = actor { get_transactions : shared query (GetTransactionsRequest) -> async GetTransactionsResponse } +``` + +Functions supported by the rosetta + +## Type `FullInterface` +``` motoko no-repl +type FullInterface = TokenInterface and RosettaInterface +``` + +Interface of the ICRC token and Rosetta canister diff --git a/I2-code/docs/ICRC1/Utils.html b/I2-code/docs/ICRC1/Utils.html new file mode 100644 index 00000000..71780195 --- /dev/null +++ b/I2-code/docs/ICRC1/Utils.html @@ -0,0 +1,4 @@ + +

ICRC1/Utils

public func init_metadata(args : T.InitArgs) : StableBuffer.StableBuffer<T.MetaDatum>

public let default_standard : T.SupportedStandard

public func init_standards() : StableBuffer.StableBuffer<T.SupportedStandard>

public func default_subaccount() : T.Subaccount

public func hash(n : Nat) : Hash.Hash

public func create_transfer_req(
  args : T.TransferArgs,
  owner : Principal,
  tx_kind : T.TxKind
) : T.TransactionRequest

public func kind_to_text(kind : T.TxKind) : Text

public func req_to_tx(tx_req : T.TransactionRequest, index : Nat) : T.Transaction

public func div_ceil(n : Nat, d : Nat) : Nat

public func get_balance(accounts : T.AccountBalances, encoded_account : T.EncodedAccount) : T.Balance

Retrieves the balance of an account

+

public func update_balance(
  accounts : T.AccountBalances,
  encoded_account : T.EncodedAccount,
  update : (T.Balance) -> T.Balance
)

Updates the balance of an account

+

public func transfer_balance(token : T.TokenData, tx_req : T.TransactionRequest)

public func mint_balance(
  token : T.TokenData,
  encoded_account : T.EncodedAccount,
  amount : T.Balance
)

public func burn_balance(
  token : T.TokenData,
  encoded_account : T.EncodedAccount,
  amount : T.Balance
)

public let SB :

\ No newline at end of file diff --git a/I2-code/docs/ICRC1/Utils.md b/I2-code/docs/ICRC1/Utils.md new file mode 100644 index 00000000..3daa6bb7 --- /dev/null +++ b/I2-code/docs/ICRC1/Utils.md @@ -0,0 +1,93 @@ +# ICRC1/Utils + +## Function `init_metadata` +``` motoko no-repl +func init_metadata(args : T.InitArgs) : StableBuffer.StableBuffer +``` + + +## Value `default_standard` +``` motoko no-repl +let default_standard : T.SupportedStandard +``` + + +## Function `init_standards` +``` motoko no-repl +func init_standards() : StableBuffer.StableBuffer +``` + + +## Function `default_subaccount` +``` motoko no-repl +func default_subaccount() : T.Subaccount +``` + + +## Function `hash` +``` motoko no-repl +func hash(n : Nat) : Hash.Hash +``` + + +## Function `create_transfer_req` +``` motoko no-repl +func create_transfer_req(args : T.TransferArgs, owner : Principal, tx_kind : T.TxKind) : T.TransactionRequest +``` + + +## Function `kind_to_text` +``` motoko no-repl +func kind_to_text(kind : T.TxKind) : Text +``` + + +## Function `req_to_tx` +``` motoko no-repl +func req_to_tx(tx_req : T.TransactionRequest, index : Nat) : T.Transaction +``` + + +## Function `div_ceil` +``` motoko no-repl +func div_ceil(n : Nat, d : Nat) : Nat +``` + + +## Function `get_balance` +``` motoko no-repl +func get_balance(accounts : T.AccountBalances, encoded_account : T.EncodedAccount) : T.Balance +``` + +Retrieves the balance of an account + +## Function `update_balance` +``` motoko no-repl +func update_balance(accounts : T.AccountBalances, encoded_account : T.EncodedAccount, update : (T.Balance) -> T.Balance) +``` + +Updates the balance of an account + +## Function `transfer_balance` +``` motoko no-repl +func transfer_balance(token : T.TokenData, tx_req : T.TransactionRequest) +``` + + +## Function `mint_balance` +``` motoko no-repl +func mint_balance(token : T.TokenData, encoded_account : T.EncodedAccount, amount : T.Balance) +``` + + +## Function `burn_balance` +``` motoko no-repl +func burn_balance(token : T.TokenData, encoded_account : T.EncodedAccount, amount : T.Balance) +``` + + +## Value `SB` +``` motoko no-repl +let SB +``` + diff --git a/I2-code/docs/ICRC1/lib.html b/I2-code/docs/ICRC1/lib.html new file mode 100644 index 00000000..6b183bae --- /dev/null +++ b/I2-code/docs/ICRC1/lib.html @@ -0,0 +1,26 @@ + +

ICRC1/lib

type Account = T.Account

type Subaccount = T.Subaccount

type AccountBalances = T.AccountBalances

type Transaction = T.Transaction

type Balance = T.Balance

type TransferArgs = T.TransferArgs

type Mint = T.Mint

type BurnArgs = T.BurnArgs

type TransactionRequest = T.TransactionRequest

type TransferError = T.TransferError

type SupportedStandard = T.SupportedStandard

type InitArgs = T.InitArgs

type TokenInitArgs = T.TokenInitArgs

type TokenData = T.TokenData

type MetaDatum = T.MetaDatum

type TxLog = T.TxLog

type TxIndex = T.TxIndex

type TokenInterface = T.TokenInterface

type RosettaInterface = T.RosettaInterface

type FullInterface = T.FullInterface

type ArchiveInterface = T.ArchiveInterface

type GetTransactionsRequest = T.GetTransactionsRequest

type GetTransactionsResponse = T.GetTransactionsResponse

type QueryArchiveFn = T.QueryArchiveFn

type TransactionRange = T.TransactionRange

type ArchivedTransaction = T.ArchivedTransaction

type TransferResult = T.TransferResult

public let MAX_TRANSACTIONS_IN_LEDGER :

public let MAX_TRANSACTION_BYTES : Nat64

public let MAX_TRANSACTIONS_PER_REQUEST :

public func init(args : T.InitArgs) : T.TokenData

Initialize a new ICRC-1 token

+

public func name(token : T.TokenData) : Text

Retrieve the name of the token

+

public func symbol(token : T.TokenData) : Text

Retrieve the symbol of the token

+

public func decimals() : Nat8

Retrieve the number of decimals specified for the token

+

public func fee(token : T.TokenData) : T.Balance

Retrieve the fee for each transfer

+

public func set_fee(token : T.TokenData, fee : Nat)

Set the fee for each transfer

+

public func metadata(token : T.TokenData) : [T.MetaDatum]

Retrieve all the metadata of the token

+

public func total_supply(token : T.TokenData) : T.Balance

Returns the total supply of circulating tokens

+

public func minted_supply(token : T.TokenData) : T.Balance

Returns the total supply of minted tokens

+

public func burned_supply(token : T.TokenData) : T.Balance

Returns the total supply of burned tokens

+

public func max_supply(token : T.TokenData) : T.Balance

Returns the maximum supply of tokens

+

public func minting_account(token : T.TokenData) : T.Account

Returns the account with the permission to mint tokens

+

Note: The minting account can only participate in minting +and burning transactions, so any tokens sent to it will be +considered burned.

+

public func balance_of(account : T.Account) : T.Balance

Retrieve the balance of a given account

+

public func supported_standards(token : T.TokenData) : [T.SupportedStandard]

Returns an array of standards supported by this token

+

public func balance_from_float(token : T.TokenData, float : Float) : T.Balance

Formats a float to a nat balance and applies the correct number of decimal places

+

public func transfer(
  token : T.TokenData,
  args : T.TransferArgs,
  caller : Principal
) : async T.TransferResult

Transfers tokens from one account to another account (minting and burning included)

+

public func mint(
  token : T.TokenData,
  args : T.Mint,
  caller : Principal
) : async T.TransferResult

Helper function to mint tokens with minimum args

+

public func burn(
  token : T.TokenData,
  args : T.BurnArgs,
  caller : Principal
) : async T.TransferResult

Helper function to burn tokens with minimum args

+

public func total_transactions(token : T.TokenData) : Nat

Returns the total number of transactions that have been processed by the given token.

+

public func get_transaction(token : T.TokenData, tx_index : T.TxIndex) : async ?T.Transaction

Retrieves the transaction specified by the given tx_index

+

public func get_transactions(token : T.TokenData, req : T.GetTransactionsRequest) : T.GetTransactionsResponse

Retrieves the transactions specified by the given range

+

\ No newline at end of file diff --git a/I2-code/docs/ICRC1/lib.md b/I2-code/docs/ICRC1/lib.md new file mode 100644 index 00000000..73836e02 --- /dev/null +++ b/I2-code/docs/ICRC1/lib.md @@ -0,0 +1,332 @@ +# ICRC1/lib + +## Type `Account` +``` motoko no-repl +type Account = T.Account +``` + + +## Type `Subaccount` +``` motoko no-repl +type Subaccount = T.Subaccount +``` + + +## Type `AccountBalances` +``` motoko no-repl +type AccountBalances = T.AccountBalances +``` + + +## Type `Transaction` +``` motoko no-repl +type Transaction = T.Transaction +``` + + +## Type `Balance` +``` motoko no-repl +type Balance = T.Balance +``` + + +## Type `TransferArgs` +``` motoko no-repl +type TransferArgs = T.TransferArgs +``` + + +## Type `Mint` +``` motoko no-repl +type Mint = T.Mint +``` + + +## Type `BurnArgs` +``` motoko no-repl +type BurnArgs = T.BurnArgs +``` + + +## Type `TransactionRequest` +``` motoko no-repl +type TransactionRequest = T.TransactionRequest +``` + + +## Type `TransferError` +``` motoko no-repl +type TransferError = T.TransferError +``` + + +## Type `SupportedStandard` +``` motoko no-repl +type SupportedStandard = T.SupportedStandard +``` + + +## Type `InitArgs` +``` motoko no-repl +type InitArgs = T.InitArgs +``` + + +## Type `TokenInitArgs` +``` motoko no-repl +type TokenInitArgs = T.TokenInitArgs +``` + + +## Type `TokenData` +``` motoko no-repl +type TokenData = T.TokenData +``` + + +## Type `MetaDatum` +``` motoko no-repl +type MetaDatum = T.MetaDatum +``` + + +## Type `TxLog` +``` motoko no-repl +type TxLog = T.TxLog +``` + + +## Type `TxIndex` +``` motoko no-repl +type TxIndex = T.TxIndex +``` + + +## Type `TokenInterface` +``` motoko no-repl +type TokenInterface = T.TokenInterface +``` + + +## Type `RosettaInterface` +``` motoko no-repl +type RosettaInterface = T.RosettaInterface +``` + + +## Type `FullInterface` +``` motoko no-repl +type FullInterface = T.FullInterface +``` + + +## Type `ArchiveInterface` +``` motoko no-repl +type ArchiveInterface = T.ArchiveInterface +``` + + +## Type `GetTransactionsRequest` +``` motoko no-repl +type GetTransactionsRequest = T.GetTransactionsRequest +``` + + +## Type `GetTransactionsResponse` +``` motoko no-repl +type GetTransactionsResponse = T.GetTransactionsResponse +``` + + +## Type `QueryArchiveFn` +``` motoko no-repl +type QueryArchiveFn = T.QueryArchiveFn +``` + + +## Type `TransactionRange` +``` motoko no-repl +type TransactionRange = T.TransactionRange +``` + + +## Type `ArchivedTransaction` +``` motoko no-repl +type ArchivedTransaction = T.ArchivedTransaction +``` + + +## Type `TransferResult` +``` motoko no-repl +type TransferResult = T.TransferResult +``` + + +## Value `MAX_TRANSACTIONS_IN_LEDGER` +``` motoko no-repl +let MAX_TRANSACTIONS_IN_LEDGER +``` + + +## Value `MAX_TRANSACTION_BYTES` +``` motoko no-repl +let MAX_TRANSACTION_BYTES : Nat64 +``` + + +## Value `MAX_TRANSACTIONS_PER_REQUEST` +``` motoko no-repl +let MAX_TRANSACTIONS_PER_REQUEST +``` + + +## Function `init` +``` motoko no-repl +func init(args : T.InitArgs) : T.TokenData +``` + +Initialize a new ICRC-1 token + +## Function `name` +``` motoko no-repl +func name(token : T.TokenData) : Text +``` + +Retrieve the name of the token + +## Function `symbol` +``` motoko no-repl +func symbol(token : T.TokenData) : Text +``` + +Retrieve the symbol of the token + +## Function `decimals` +``` motoko no-repl +func decimals() : Nat8 +``` + +Retrieve the number of decimals specified for the token + +## Function `fee` +``` motoko no-repl +func fee(token : T.TokenData) : T.Balance +``` + +Retrieve the fee for each transfer + +## Function `set_fee` +``` motoko no-repl +func set_fee(token : T.TokenData, fee : Nat) +``` + +Set the fee for each transfer + +## Function `metadata` +``` motoko no-repl +func metadata(token : T.TokenData) : [T.MetaDatum] +``` + +Retrieve all the metadata of the token + +## Function `total_supply` +``` motoko no-repl +func total_supply(token : T.TokenData) : T.Balance +``` + +Returns the total supply of circulating tokens + +## Function `minted_supply` +``` motoko no-repl +func minted_supply(token : T.TokenData) : T.Balance +``` + +Returns the total supply of minted tokens + +## Function `burned_supply` +``` motoko no-repl +func burned_supply(token : T.TokenData) : T.Balance +``` + +Returns the total supply of burned tokens + +## Function `max_supply` +``` motoko no-repl +func max_supply(token : T.TokenData) : T.Balance +``` + +Returns the maximum supply of tokens + +## Function `minting_account` +``` motoko no-repl +func minting_account(token : T.TokenData) : T.Account +``` + +Returns the account with the permission to mint tokens + +Note: **The minting account can only participate in minting +and burning transactions, so any tokens sent to it will be +considered burned.** + +## Function `balance_of` +``` motoko no-repl +func balance_of(account : T.Account) : T.Balance +``` + +Retrieve the balance of a given account + +## Function `supported_standards` +``` motoko no-repl +func supported_standards(token : T.TokenData) : [T.SupportedStandard] +``` + +Returns an array of standards supported by this token + +## Function `balance_from_float` +``` motoko no-repl +func balance_from_float(token : T.TokenData, float : Float) : T.Balance +``` + +Formats a float to a nat balance and applies the correct number of decimal places + +## Function `transfer` +``` motoko no-repl +func transfer(token : T.TokenData, args : T.TransferArgs, caller : Principal) : async T.TransferResult +``` + +Transfers tokens from one account to another account (minting and burning included) + +## Function `mint` +``` motoko no-repl +func mint(token : T.TokenData, args : T.Mint, caller : Principal) : async T.TransferResult +``` + +Helper function to mint tokens with minimum args + +## Function `burn` +``` motoko no-repl +func burn(token : T.TokenData, args : T.BurnArgs, caller : Principal) : async T.TransferResult +``` + +Helper function to burn tokens with minimum args + +## Function `total_transactions` +``` motoko no-repl +func total_transactions(token : T.TokenData) : Nat +``` + +Returns the total number of transactions that have been processed by the given token. + +## Function `get_transaction` +``` motoko no-repl +func get_transaction(token : T.TokenData, tx_index : T.TxIndex) : async ?T.Transaction +``` + +Retrieves the transaction specified by the given `tx_index` + +## Function `get_transactions` +``` motoko no-repl +func get_transactions(token : T.TokenData, req : T.GetTransactionsRequest) : T.GetTransactionsResponse +``` + +Retrieves the transactions specified by the given range diff --git a/I2-code/docs/index.html b/I2-code/docs/index.html new file mode 100644 index 00000000..5f856522 --- /dev/null +++ b/I2-code/docs/index.html @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/I2-code/docs/index.md b/I2-code/docs/index.md new file mode 100644 index 00000000..0381693e --- /dev/null +++ b/I2-code/docs/index.md @@ -0,0 +1,7 @@ +# Index + +* [ICRC1/Account](ICRC1/Account.md) +* [ICRC1/Transfer](ICRC1/Transfer.md) +* [ICRC1/Types](ICRC1/Types.md) +* [ICRC1/Utils](ICRC1/Utils.md) +* [ICRC1/lib](ICRC1/lib.md) diff --git a/I2-code/docs/styles.css b/I2-code/docs/styles.css new file mode 100644 index 00000000..65732bf1 --- /dev/null +++ b/I2-code/docs/styles.css @@ -0,0 +1,162 @@ + +* { + box-sizing: border-box; +} + +body { + background: #fff; + color: #222; + font-family: Circular Std, sans-serif; + line-height: 1.15; + -webkit-font-smoothing: antialiased; + margin: 0; + font-size: 1.0625rem; +} + +.keyword { + color: #264059; +} + +.type { + color: #ad448e; +} + +.parameter { + color: #264059; +} + +.classname { + color: #2c8093; +} + +.fnname { + color: #9a6e31; +} + +.sidebar { + width: 200px; + position: fixed; + left: 0; + top: 0; + bottom: 0; + overflow: auto; + + background-color: #F1F1F1; +} + +.documentation { + margin-left: 230px; + max-width: 960px; +} + +.sidebar > ul { + margin: 0 10px; + padding: 0; + list-style: none; +} + +.sidebar a { + display: block; + text-overflow: ellipsis; + overflow: hidden; + line-height: 15px; + padding: 7px 5px; + font-size: 14px; + font-weight: 400; + transition: border 500ms ease-out; + color: #000; + text-decoration: none; +} + +.sidebar h3 { + border-bottom: 1px #dddddd solid; + font-weight: 500; + margin: 20px 0 15px 0; + padding-bottom: 6px; + text-align: center; +} + +.declaration { + border-bottom: 1px solid #f0f0f0; +} +.declaration:last-of-type { + border-bottom: none; +} + +.declaration :last-child { + border: none; +} + +h1 { + font-weight: 500; + padding-bottom: 6px; + border-bottom: 1px #D5D5D5 dashed; +} + +h4.function-declaration { + font-weight: 600; + margin-top: 16px; +} + +h4.value-declaration { + font-weight: 600; + margin-top: 16px; +} + +h4.type-declaration { + font-weight: 600; + margin-top: 16px; +} + +h4.class-declaration { + font-weight: 600; + margin-top: 16px; +} + +.class-declaration ~ div { + margin-left: 20px; +} + +.index-container { + display: flex; + flex-direction: column; + align-items: center; + font-size: 1.2rem; +} + +.index-header { + font-weight: 400; +} + +.index-listing { + padding: 0; + list-style: none; + max-width: 960px; +} + +.index-item { + margin-bottom: 5px; +} + +.index-item-link { + text-decoration: none; + font-weight: 400; +} + +.index-item-link::after { + content: " \2014\00A0"; +} + +.index-item-comment { + display: inline; +} + +.index-item-comment > * { + display: none; +} + +.index-item-comment > *:first-child { + display: inline; + white-space: nowrap; +} + diff --git a/I2-code/example/.gitignore b/I2-code/example/.gitignore new file mode 100644 index 00000000..a7594306 --- /dev/null +++ b/I2-code/example/.gitignore @@ -0,0 +1,15 @@ +# Various IDEs and Editors +.vscode/ +.idea/ +**/*~ + +# Mac OSX temporary files +.DS_Store +**/.DS_Store + +# dfx temporary files +.dfx/ + +# frontend code +node_modules/ +dist/ diff --git a/I2-code/example/dfx.json b/I2-code/example/dfx.json new file mode 100644 index 00000000..f7671701 --- /dev/null +++ b/I2-code/example/dfx.json @@ -0,0 +1,23 @@ +{ + "version": 1, + "dfx": "0.11.2", + "canisters": { + "icrc1_example": { + "type": "motoko", + "main": "src/icrc1/main.mo" + } + }, + "defaults": { + "build": { + "packtool": "", + "args": "" + } + }, + + "networks": { + "local": { + "bind": "127.0.0.1:8000", + "type": "ephemeral" + } + } +} diff --git a/I2-code/example/icrc1/main.mo b/I2-code/example/icrc1/main.mo new file mode 100644 index 00000000..a8b5ce7b --- /dev/null +++ b/I2-code/example/icrc1/main.mo @@ -0,0 +1,97 @@ +import Iter "mo:base/Iter"; +import Option "mo:base/Option"; +import Time "mo:base/Time"; + +import ExperimentalCycles "mo:base/ExperimentalCycles"; + +import ICRC1 "../../src/ICRC1"; // replace with "mo:icrc1/ICRC1" +import Array "mo:base/Array"; + +shared ({ caller = _owner }) actor class Token( + token_args : ICRC1.TokenInitArgs, +) : async ICRC1.FullInterface { + + stable let token = ICRC1.init({ + token_args with minting_account = Option.get( + token_args.minting_account, + { + owner = _owner; + subaccount = null; + }, + ); + }); + + /// Functions for the ICRC1 token standard + public shared query func icrc1_name() : async Text { + ICRC1.name(token); + }; + + public shared query func icrc1_symbol() : async Text { + ICRC1.symbol(token); + }; + + public shared query func icrc1_decimals() : async Nat8 { + ICRC1.decimals(token); + }; + + public shared query func icrc1_fee() : async ICRC1.Balance { + ICRC1.fee(token); + }; + + public shared query func icrc1_metadata() : async [ICRC1.MetaDatum] { + ICRC1.metadata(token); + }; + + public shared query func icrc1_total_supply() : async ICRC1.Balance { + ICRC1.total_supply(token); + }; + + public shared query func icrc1_minting_account() : async ?ICRC1.Account { + ?ICRC1.minting_account(token); + }; + + public shared query func icrc1_balance_of(args : ICRC1.Account) : async ICRC1.Balance { + ICRC1.balance_of(token, args); + }; + + public shared query func icrc1_supported_standards() : async [ICRC1.SupportedStandard] { + ICRC1.supported_standards(token); + }; + + public shared ({ caller }) func icrc1_transfer(args : ICRC1.TransferArgs) : async ICRC1.TransferResult { + await* ICRC1.transfer(token, args, caller); + }; + + public shared ({ caller }) func mint(args : ICRC1.Mint) : async ICRC1.TransferResult { + await* ICRC1.mint(token, args, caller); + }; + + public shared ({ caller }) func burn(args : ICRC1.BurnArgs) : async ICRC1.TransferResult { + await* ICRC1.burn(token, args, caller); + }; + + public shared ({ caller }) func approve(args : ICRC1.ApproveArgs) : async ICRC1.ApproveResult { + await* ICRC1.approve(token, args, caller); + }; + + public shared ({ caller }) func transfer_from(args : ICRC1.TransferFromArgs) : async ICRC1.TransferFromResult { + await* ICRC1.transfer_from(token, args, caller); + }; + + // Functions from the rosetta icrc1 ledger + public shared query func get_transactions(req : ICRC1.GetTransactionsRequest) : async ICRC1.GetTransactionsResponse { + ICRC1.get_transactions(token, req); + }; + + // Additional functions not included in the ICRC1 standard + public shared func get_transaction(i : ICRC1.TxIndex) : async ?ICRC1.Transaction { + await* ICRC1.get_transaction(token, i); + }; + + // Deposit cycles into this archive canister. + public shared func deposit_cycles() : async () { + let amount = ExperimentalCycles.available(); + let accepted = ExperimentalCycles.accept(amount); + assert (accepted == amount); + }; +}; diff --git a/I2-code/icrc1-default-args.txt b/I2-code/icrc1-default-args.txt new file mode 100644 index 00000000..ad39e593 --- /dev/null +++ b/I2-code/icrc1-default-args.txt @@ -0,0 +1,19 @@ +( record { + name = ""; + symbol = ""; + decimals = 6; + fee = 1_000_000; + max_supply = 1_000_000_000_000; + initial_balances = vec { + record { + record { + owner = principal "r7inp-6aaaa-aaaaa-aaabq-cai"; + subaccount = null; + }; + 100_000_000 + } + }; + min_burn_amount = 10_000; + minting_account = null; + advanced_settings = null; +}) \ No newline at end of file diff --git a/I2-code/makefile b/I2-code/makefile new file mode 100644 index 00000000..6a60474a --- /dev/null +++ b/I2-code/makefile @@ -0,0 +1,28 @@ +.PHONY: test docs actor-test + +dfx-cache-install: + dfx cache install + +test: install-dfx-cache + $(shell dfx cache show)/moc -r $(shell mops sources) -wasi-system-api ./tests/**/**.Test.mo --package base ~/.cache/dfinity/versions/0.13.1/base + +no-warn: dfx-cache-install + find src -type f -name '*.mo' -print0 | xargs -0 $(shell mocv bin current)/moc -r $(shell mops sources) -Werror -wasi-system-api + +docs: + $(shell mocv bin current)/mo-doc + $(shell mocv bin current)/mo-doc --format plain + +actor-test: dfx-cache-install + -dfx start --background + dfx deploy test + dfx ledger fabricate-cycles --canister test + dfx canister call test run_tests + +ref-test: + -dfx start --background --clean + IDENTITY=$$(dfx identity whoami); \ + echo $$IDENTITY; \ + cat icrc1-default-args.txt | xargs -0 dfx deploy icrc1 --identity $$IDENTITY --no-wallet --argument ; \ + CANISTER=$$(dfx canister id icrc1); \ + cd Dfnity-ICRC1-Reference && cargo run --bin runner -- -u http://127.0.0.1:4943 -c $$CANISTER -s ~/.config/dfx/identity/$$IDENTITY/identity.pem \ No newline at end of file diff --git a/I2-code/mops.toml b/I2-code/mops.toml new file mode 100644 index 00000000..cb110ae6 --- /dev/null +++ b/I2-code/mops.toml @@ -0,0 +1,13 @@ +[dependencies] +base = "https://github.com/dfinity/motoko-base#moc-0.7.4" +array = "https://github.com/aviate-labs/array.mo#v0.2.0" +StableTrieMap = "https://github.com/NatLabs/StableTrieMap#main" +StableBuffer = "https://github.com/canscale/StableBuffer#v0.2.0" +itertools = "0.1.1" + +[package] +name = "icrc1" +version = "0.0.1" +description = "A full implementation of the ICRC-1 fungible token standard" +repository = "https://github.com/NatLabs/icrc1" +keywords = [ "icrc1", "fungible", "token", "standard", "dfinity" ] diff --git a/I2-code/package-set.dhall b/I2-code/package-set.dhall new file mode 100644 index 00000000..5149d068 --- /dev/null +++ b/I2-code/package-set.dhall @@ -0,0 +1,40 @@ +let aviate_labs = https://github.com/aviate-labs/package-set/releases/download/v0.1.4/package-set.dhall sha256:30b7e5372284933c7394bad62ad742fec4cb09f605ce3c178d892c25a1a9722e +let vessel_package_set = + https://github.com/dfinity/vessel-package-set/releases/download/mo-0.6.20-20220131/package-set.dhall + +let Package = + { name : Text, version : Text, repo : Text, dependencies : List Text } + +let + -- This is where you can add your own packages to the package-set + additions = + [] : List Package + +let overrides = [ + { + name = "StableTrieMap", + version = "main", + repo = "https://github.com/NatLabs/StableTrieMap", + dependencies = ["base"] : List Text + }, + { + name = "StableBuffer", + version = "v0.2.0", + repo = "https://github.com/canscale/StableBuffer", + dependencies = ["base"] : List Text + }, + { + name = "itertools", + version = "main", + repo = "https://github.com/NatLabs/Itertools.mo", + dependencies = ["base"] : List Text + }, + { + name = "base", + version = "moc-0.7.4", + repo = "https://github.com/dfinity/motoko-base", + dependencies = ["base"] : List Text + }, +] : List Package + +in aviate_labs # vessel_package_set # overrides diff --git a/I2-code/readme.md b/I2-code/readme.md new file mode 100644 index 00000000..7ba48c9d --- /dev/null +++ b/I2-code/readme.md @@ -0,0 +1,94 @@ +# ICRC-2 Implementation +This repo contains the implementation of the +[ICRC-2](https://github.com/dfinity/ICRC-1/blob/main/standards/ICRC-2/README.md). + +## References +- [ICRC-1](https://github.com/NatLabs/icrc1) +- [ICRC1 test](https://github.com/NatLabs/icrc1/blob/main/example/icrc1/main.mo) + + +## Getting Started +- Expose the ICRC-1 from your canister + - Import the `icrc1` lib and expose them in an `actor` class. + + + ```motoko + git clone https://github.com/JingJingZhang9/I3-code.git + dfx start --background --clean + + dfx deploy icrc1 --argument '( record { + name = ""; + symbol = ""; + decimals = 6; + fee = 1_000_000; + max_supply = 1_000_000_000_000; + initial_balances = vec { + record { + record { + owner = principal ""; + subaccount = null; + }; + 100_000_000 + } + }; + min_burn_amount = 10_000; + minting_account = null; + advanced_settings = null; + })' + ``` + +- Create a token dynamically from a canister + ```motoko + import Nat8 "mo:base/Nat8"; + import Token "mo:icrc1/ICRC1/Canisters/Token"; + + actor{ + let decimals = 8; // replace with your chosen number of decimals + + func add_decimals(n: Nat): Nat{ + n * 10 ** decimals + }; + + let pre_mint_account = { + owner = Principal.fromText(""); + subaccount = null; + }; + + let token_canister = Token.Token({ + name = ""; + symbol = ""; + decimals = Nat8.fromNat(decimals); + fee = add_decimals(1); + max_supply = add_decimals(1_000_000); + + // pre-mint 100,000 tokens for the account + initial_balances = [(pre_mint_account, add_decimals(100_000))]; + + min_burn_amount = add_decimals(10); + minting_account = null; // defaults to the canister id of the caller + advanced_settings = null; + }); + } + ``` + +> The fields for the `advanced_settings` record are documented [here](./docs/ICRC1/Types.md#type-advancedsettings) + +## Textual Representation of the ICRC-2 +This library implements the https://github.com/dfinity/ICRC-1/blob/main/standards/ICRC-2/README.md . + +ICRC-2 is an extension of the ICRC-1 standard. ICRC-2 provides a way for account owners to delegate token transfer authorization to a third party, allowing the third party to perform transfers on behalf of the owner: +icrc2_approve: Authorizes the spender to transfer a certain amount of tokens on behalf of the caller from the account { owner = caller; subaccount = from_subaccount }. The number of transfers the spender can initiate from the caller's account is unlimited as long as the total amounts and fees of these transfers do not exceed the allowance. +icrc2_transfer_from: Transfers a certain amount of tokens between two accounts. + + +## Tests +#### Internal Tests +- Download and Install [vessel](https://github.com/dfinity/vessel) +- Run `make test` +- Run `make actor-test` + + + +## Funding + +This library was initially incentivized by [ICDevs](https://icdevs.org/). You can view more about the bounty on the [forum](https://forum.dfinity.org/t/completed-icdevs-org-bounty-26-icrc-1-motoko-up-to-10k/14868/54) or [website](https://icdevs.org/bounties/2022/08/14/ICRC-1-Motoko.html). The bounty was funded by The ICDevs.org community and the DFINITY Foundation and the award was paid to [@NatLabs](https://github.com/NatLabs). If you use this library and gain value from it, please consider a [donation](https://icdevs.org/donations.html) to ICDevs. diff --git a/I2-code/src/ICRC1/Account.mo b/I2-code/src/ICRC1/Account.mo new file mode 100644 index 00000000..8a46ade2 --- /dev/null +++ b/I2-code/src/ICRC1/Account.mo @@ -0,0 +1,188 @@ +import Array "mo:base/Array"; +import Blob "mo:base/Blob"; +import Char "mo:base/Char"; +import Debug "mo:base/Debug"; +import Int "mo:base/Int"; +import Iter "mo:base/Iter"; +import Nat "mo:base/Nat"; +import Nat8 "mo:base/Nat8"; +import Nat32 "mo:base/Nat32"; +import Nat64 "mo:base/Nat64"; +import Option "mo:base/Option"; +import Principal "mo:base/Principal"; +import Result "mo:base/Result"; +import Text "mo:base/Text"; +import Time "mo:base/Time"; + +import ArrayModule "mo:array/Array"; +import Itertools "mo:itertools/Iter"; +import StableBuffer "mo:StableBuffer/StableBuffer"; +import STMap "mo:StableTrieMap"; + +import T "Types"; + +module { + type Iter = Iter.Iter; + + /// Checks if a subaccount is valid + public func validate_subaccount(subaccount : ?T.Subaccount) : Bool { + switch (subaccount) { + case (?bytes) { + bytes.size() == 32; + }; + case (_) true; + }; + }; + + /// Checks if an account is valid + public func validate(account : T.Account) : Bool { + let is_anonymous = Principal.isAnonymous(account.owner); + let invalid_size = Principal.toBlob(account.owner).size() > 29; + + if (is_anonymous or invalid_size) { + false; + } else { + validate_subaccount(account.subaccount); + }; + }; + + func shrink_subaccount(sub : Blob) : (Iter.Iter, Nat8) { + let bytes = Blob.toArray(sub); + var size = Nat8.fromNat(bytes.size()); + + let iter = Itertools.skipWhile( + bytes.vals(), + func(byte : Nat8) : Bool { + if (byte == 0x00) { + size -= 1; + return true; + }; + + false; + }, + ); + + (iter, size); + }; + + func encode_subaccount(sub : Blob) : Iter.Iter { + + let (sub_iter, size) = shrink_subaccount(sub); + if (size == 0) { + return Itertools.empty(); + }; + + let suffix : [Nat8] = [size, 0x7f]; + + Itertools.chain( + sub_iter, + suffix.vals(), + ); + }; + + /// Implementation of ICRC1's Textual representation of accounts [Encoding Standard](https://github.com/dfinity/ICRC-1/tree/main/standards/ICRC-1#encoding) + public func encode({ owner; subaccount } : T.Account) : T.EncodedAccount { + let owner_blob = Principal.toBlob(owner); + + switch (subaccount) { + case (?subaccount) { + Blob.fromArray( + Iter.toArray( + Itertools.chain( + owner_blob.vals(), + encode_subaccount(subaccount), + ), + ), + ); + }; + case (_) { + owner_blob; + }; + }; + }; + + /// Implementation of ICRC1's Textual representation of accounts [Decoding Standard](https://github.com/dfinity/ICRC-1/tree/main/standards/ICRC-1#decoding) + public func decode(encoded : T.EncodedAccount) : ?T.Account { + let bytes = Blob.toArray(encoded); + var size = bytes.size(); + + if (bytes[size - 1] == 0x7f) { + size -= 1; + + let subaccount_size = Nat8.toNat(bytes[size - 1]); + + if (subaccount_size == 0 or subaccount_size > 32) { + return null; + }; + + size -= 1; + let split_index = (size - subaccount_size) : Nat; + + if (bytes[split_index] == 0) { + return null; + }; + + let principal = Principal.fromBlob( + Blob.fromArray( + ArrayModule.slice(bytes, 0, split_index), + ), + ); + + let prefix_zeroes = Itertools.take( + Iter.make(0 : Nat8), + (32 - subaccount_size) : Nat, + ); + + let encoded_subaccount = Itertools.fromArraySlice(bytes, split_index, size); + + let subaccount = Blob.fromArray( + Iter.toArray( + Itertools.chain(prefix_zeroes, encoded_subaccount), + ), + ); + + ?{ owner = principal; subaccount = ?subaccount }; + } else { + ?{ + owner = Principal.fromBlob(encoded); + subaccount = null; + }; + }; + }; + + /// Converts an ICRC-1 Account from its Textual representation to the `Account` type + public func fromText(encoded : Text) : ?T.Account { + let p = Principal.fromText(encoded); + let blob = Principal.toBlob(p); + + decode(blob); + }; + + /// Converts an ICRC-1 `Account` to its Textual representation + public func toText(account : T.Account) : Text { + let blob = encode(account); + let principal = Principal.fromBlob(blob); + Principal.toText(principal); + }; + + func from_hex(char : Char) : Nat8 { + let charCode = Char.toNat32(char); + + if (Char.isDigit(char)) { + let digit = charCode - Char.toNat32('0'); + + return Nat8.fromNat(Nat32.toNat(digit)); + }; + + if (Char.isUppercase(char)) { + let digit = charCode - Char.toNat32('A') + 10; + + return Nat8.fromNat(Nat32.toNat(digit)); + }; + + // lowercase + let digit = charCode - Char.toNat32('a') + 10; + + return Nat8.fromNat(Nat32.toNat(digit)); + }; +}; diff --git a/I2-code/src/ICRC1/Canisters/Archive.mo b/I2-code/src/ICRC1/Canisters/Archive.mo new file mode 100644 index 00000000..74f7022c --- /dev/null +++ b/I2-code/src/ICRC1/Canisters/Archive.mo @@ -0,0 +1,238 @@ +import Prim "mo:prim"; + +import Array "mo:base/Array"; +import Blob "mo:base/Blob"; +import Debug "mo:base/Debug"; +import Iter "mo:base/Iter"; +import Nat "mo:base/Nat"; +import Nat64 "mo:base/Nat64"; +import Hash "mo:base/Hash"; +import Result "mo:base/Result"; + +import ExperimentalCycles "mo:base/ExperimentalCycles"; +import ExperimentalStableMemory "mo:base/ExperimentalStableMemory"; + +import Itertools "mo:itertools/Iter"; +import StableTrieMap "mo:StableTrieMap"; +import U "../Utils"; +import T "../Types"; + +shared ({ caller = ledger_canister_id }) actor class Archive() : async T.ArchiveInterface { + + type Transaction = T.Transaction; + type MemoryBlock = { + offset : Nat64; + size : Nat; + }; + + stable let KiB = 1024; + stable let GiB = KiB ** 3; + stable let MEMORY_PER_PAGE : Nat64 = Nat64.fromNat(64 * KiB); + stable let MIN_PAGES : Nat64 = 32; // 2MiB == 32 * 64KiB + stable var PAGES_TO_GROW : Nat64 = 2048; // 64MiB + stable let MAX_MEMORY = 32 * GiB; + + stable let BUCKET_SIZE = 1000; + stable let MAX_TRANSACTIONS_PER_REQUEST = 5000; + + stable var memory_pages : Nat64 = ExperimentalStableMemory.size(); + stable var total_memory_used : Nat64 = 0; + + stable var filled_buckets = 0; + stable var trailing_txs = 0; + + stable let txStore = StableTrieMap.new(); + + public shared ({ caller }) func append_transactions(txs : [Transaction]) : async Result.Result<(), Text> { + + if (caller != ledger_canister_id) { + return #err("Unauthorized Access: Only the ledger canister can access this archive canister"); + }; + + var txs_iter = txs.vals(); + + if (trailing_txs > 0) { + let last_bucket = StableTrieMap.get( + txStore, + Nat.equal, + U.hash, + filled_buckets, + ); + + switch (last_bucket) { + case (?last_bucket) { + let new_bucket = Iter.toArray( + Itertools.take( + Itertools.chain( + last_bucket.vals(), + Iter.map(txs.vals(), store_tx), + ), + BUCKET_SIZE, + ), + ); + + if (new_bucket.size() == BUCKET_SIZE) { + let offset = (BUCKET_SIZE - last_bucket.size()) : Nat; + + txs_iter := Itertools.fromArraySlice(txs, offset, txs.size()); + } else { + txs_iter := Itertools.empty(); + }; + + store_bucket(new_bucket); + }; + case (_) {}; + }; + }; + + for (chunk in Itertools.chunks(txs_iter, BUCKET_SIZE)) { + store_bucket(Array.map(chunk, store_tx)); + }; + + #ok(); + }; + + func total_txs() : Nat { + (filled_buckets * BUCKET_SIZE) + trailing_txs; + }; + + public shared query func total_transactions() : async Nat { + total_txs(); + }; + + public shared query func get_transaction(tx_index : T.TxIndex) : async ?Transaction { + let bucket_key = tx_index / BUCKET_SIZE; + + let opt_bucket = StableTrieMap.get( + txStore, + Nat.equal, + U.hash, + bucket_key, + ); + + switch (opt_bucket) { + case (?bucket) { + let i = tx_index % BUCKET_SIZE; + if (i < bucket.size()) { + ?get_tx(bucket[tx_index % BUCKET_SIZE]); + } else { + null; + }; + }; + case (_) { + null; + }; + }; + }; + + public shared query func get_transactions(req : T.GetTransactionsRequest) : async T.TransactionRange { + let { start; length } = req; + var iter = Itertools.empty(); + + let end = start + length; + let start_bucket = start / BUCKET_SIZE; + let end_bucket = (Nat.min(end, total_txs()) / BUCKET_SIZE) + 1; + + label _loop for (i in Itertools.range(start_bucket, end_bucket)) { + let opt_bucket = StableTrieMap.get( + txStore, + Nat.equal, + U.hash, + i, + ); + + switch (opt_bucket) { + case (?bucket) { + if (i == start_bucket) { + iter := Itertools.fromArraySlice(bucket, start % BUCKET_SIZE, Nat.min(bucket.size(), end)); + } else if (i + 1 == end_bucket) { + let bucket_iter = Itertools.fromArraySlice(bucket, 0, end % BUCKET_SIZE); + iter := Itertools.chain(iter, bucket_iter); + } else { + iter := Itertools.chain(iter, bucket.vals()); + }; + }; + case (_) { break _loop }; + }; + }; + + let transactions = Iter.toArray( + Iter.map( + Itertools.take(iter, MAX_TRANSACTIONS_PER_REQUEST), + get_tx, + ), + ); + + { transactions }; + }; + + public shared query func remaining_capacity() : async Nat { + MAX_MEMORY - Nat64.toNat(total_memory_used); + }; + + /// Deposit cycles into this archive canister. + public shared func deposit_cycles() : async () { + let amount = ExperimentalCycles.available(); + let accepted = ExperimentalCycles.accept(amount); + assert (accepted == amount); + }; + + func to_blob(tx : Transaction) : Blob { + to_candid (tx); + }; + + func from_blob(tx : Blob) : Transaction { + switch (from_candid (tx) : ?Transaction) { + case (?tx) tx; + case (_) Debug.trap("Could not decode tx blob"); + }; + }; + + func store_tx(tx : Transaction) : MemoryBlock { + let blob = to_blob(tx); + + if ((memory_pages * MEMORY_PER_PAGE) - total_memory_used < (MIN_PAGES * MEMORY_PER_PAGE)) { + ignore ExperimentalStableMemory.grow(PAGES_TO_GROW); + memory_pages += PAGES_TO_GROW; + }; + + let offset = total_memory_used; + + ExperimentalStableMemory.storeBlob( + offset, + blob, + ); + + let mem_block = { + offset; + size = blob.size(); + }; + + total_memory_used += Nat64.fromNat(blob.size()); + mem_block; + }; + + func get_tx({ offset; size } : MemoryBlock) : Transaction { + let blob = ExperimentalStableMemory.loadBlob(offset, size); + + let tx = from_blob(blob); + }; + + func store_bucket(bucket : [MemoryBlock]) { + + StableTrieMap.put( + txStore, + Nat.equal, + U.hash, + filled_buckets, + bucket, + ); + + if (bucket.size() == BUCKET_SIZE) { + filled_buckets += 1; + trailing_txs := 0; + } else { + trailing_txs := bucket.size(); + }; + }; +}; diff --git a/I2-code/src/ICRC1/Canisters/Token.mo b/I2-code/src/ICRC1/Canisters/Token.mo new file mode 100644 index 00000000..a80469a1 --- /dev/null +++ b/I2-code/src/ICRC1/Canisters/Token.mo @@ -0,0 +1,106 @@ +import Array "mo:base/Array"; +import Iter "mo:base/Iter"; +import Option "mo:base/Option"; +import Time "mo:base/Time"; + +import ExperimentalCycles "mo:base/ExperimentalCycles"; + +import SB "mo:StableBuffer/StableBuffer"; + +import ICRC1 ".."; +import Archive "Archive"; + +shared ({ caller = _owner }) actor class Token( + init_args : ICRC1.TokenInitArgs +) : async ICRC1.FullInterface { + + let icrc1_args : ICRC1.InitArgs = { + init_args with minting_account = Option.get( + init_args.minting_account, + { + owner = _owner; + subaccount = null; + }, + ); + }; + + stable let token = ICRC1.init(icrc1_args); + + /// Functions for the ICRC1 token standard + public shared query func icrc1_name() : async Text { + ICRC1.name(token); + }; + + public shared query func icrc1_symbol() : async Text { + ICRC1.symbol(token); + }; + + public shared query func icrc1_decimals() : async Nat8 { + ICRC1.decimals(token); + }; + + public shared query func icrc1_fee() : async ICRC1.Balance { + ICRC1.fee(token); + }; + + public shared query func icrc1_metadata() : async [ICRC1.MetaDatum] { + ICRC1.metadata(token); + }; + + public shared query func icrc1_total_supply() : async ICRC1.Balance { + ICRC1.total_supply(token); + }; + + public shared query func icrc1_minting_account() : async ?ICRC1.Account { + ?ICRC1.minting_account(token); + }; + + public shared query func icrc1_balance_of(args : ICRC1.Account) : async ICRC1.Balance { + ICRC1.balance_of(token, args); + }; + + public shared query func icrc1_supported_standards() : async [ICRC1.SupportedStandard] { + ICRC1.supported_standards(token); + }; + + public shared ({ caller }) func icrc1_transfer(args : ICRC1.TransferArgs) : async ICRC1.TransferResult { + await* ICRC1.transfer(token, args, caller); + }; + + public shared ({ caller }) func mint(args : ICRC1.Mint) : async ICRC1.TransferResult { + await* ICRC1.mint(token, args, caller); + }; + + public shared ({ caller }) func burn(args : ICRC1.BurnArgs) : async ICRC1.TransferResult { + await* ICRC1.burn(token, args, caller); + }; + + public shared ({ caller }) func icrc2_approve(args : ICRC1.ApproveArgs) : async ICRC1.ApproveResult { + await* ICRC1.approve(token, args, caller); + }; + + public shared ({ caller }) func icrc2_transfer_from(args : ICRC1.TransferFromArgs) : async ICRC1.TransferFromResult { + await* ICRC1.transfer_from(token, args, caller); + }; + + public shared query func icrc2_allowance(args : ICRC1.AllowanceArgs) : async ICRC1.Allowance { + ICRC1.get_allowance_of(token, args.account, args.spender); + }; + + // Functions for integration with the rosetta standard + public shared query func get_transactions(req : ICRC1.GetTransactionsRequest) : async ICRC1.GetTransactionsResponse { + ICRC1.get_transactions(token, req); + }; + + // Additional functions not included in the ICRC1 standard + public shared func get_transaction(i : ICRC1.TxIndex) : async ?ICRC1.Transaction { + await* ICRC1.get_transaction(token, i); + }; + + // Deposit cycles into this canister. + public shared func deposit_cycles() : async () { + let amount = ExperimentalCycles.available(); + let accepted = ExperimentalCycles.accept(amount); + assert (accepted == amount); + }; +}; diff --git a/I2-code/src/ICRC1/Transfer.mo b/I2-code/src/ICRC1/Transfer.mo new file mode 100644 index 00000000..529e5a3e --- /dev/null +++ b/I2-code/src/ICRC1/Transfer.mo @@ -0,0 +1,465 @@ +import Array "mo:base/Array"; +import Blob "mo:base/Blob"; +import Debug "mo:base/Debug"; +import Int "mo:base/Int"; +import Iter "mo:base/Iter"; +import Nat "mo:base/Nat"; +import Nat64 "mo:base/Nat64"; +import Nat8 "mo:base/Nat8"; +import Option "mo:base/Option"; +import Principal "mo:base/Principal"; +import Result "mo:base/Result"; +import Time "mo:base/Time"; + +import Itertools "mo:itertools/Iter"; +import StableBuffer "mo:StableBuffer/StableBuffer"; +import STMap "mo:StableTrieMap"; + +import Account "Account"; + +import T "Types"; +import Utils "Utils"; + +module { + let { SB } = Utils; + + /// Checks if a transaction memo is valid + public func validate_memo(memo : ?T.Memo) : Bool { + switch (memo) { + case (?bytes) { + bytes.size() <= 32; + }; + case (_) true; + }; + }; + + /// Checks if the `created_at_time` of a transfer request is before the accepted time range + public func is_too_old(token : T.TokenData, created_at_time : Nat64) : Bool { + let { permitted_drift; transaction_window } = token; + + let lower_bound = Time.now() - transaction_window - permitted_drift; + Nat64.toNat(created_at_time) < lower_bound; + }; + + /// Checks if the `created_at_time` of a transfer request has not been reached yet relative to the canister's time. + public func is_in_future(token : T.TokenData, created_at_time : Nat64) : Bool { + let upper_bound = Time.now() + token.permitted_drift; + Nat64.toNat(created_at_time) > upper_bound; + }; + + /// Checks if there is a duplicate transaction that matches the transfer request in the main canister. + /// + /// If a duplicate is found, the function returns an error (`#err`) with the duplicate transaction's index. + public func deduplicate(token : T.TokenData, tx_req : T.TransactionRequest) : Result.Result<(), Nat> { + // only deduplicates if created_at_time is set + if (tx_req.created_at_time == null) { + return #ok(); + }; + + let { transactions = txs; archive } = token; + + var phantom_txs_size = 0; + let phantom_txs = SB._clearedElemsToIter(txs); + let current_txs = SB.vals(txs); + + let last_2000_txs = if (archive.stored_txs > 0) { + phantom_txs_size := SB.capacity(txs) - SB.size(txs); + Itertools.chain(phantom_txs, current_txs); + } else { + current_txs; + }; + + label for_loop for ((i, tx) in Itertools.enumerate(last_2000_txs)) { + + let is_duplicate = switch (tx_req.kind) { + case (#mint) { + switch (tx.mint) { + case (?mint) { + ignore do ? { + if (is_too_old(token, mint.created_at_time!)) { + break for_loop; + }; + }; + + let mint_req : T.Mint = tx_req; + + mint_req == mint; + }; + case (_) false; + }; + }; + case (#burn) { + switch (tx.burn) { + case (?burn) { + ignore do ? { + if (is_too_old(token, burn.created_at_time!)) { + break for_loop; + }; + }; + let burn_req : T.Burn = tx_req; + + burn_req == burn; + }; + case (_) false; + }; + }; + case (#transfer) { + switch (tx.transfer) { + case (?transfer) { + ignore do ? { + if (is_too_old(token, transfer.created_at_time!)) { + break for_loop; + }; + }; + + let transfer_req : T.Transfer = tx_req; + + transfer_req == transfer; + }; + case (_) false; + }; + }; + }; + + if (is_duplicate) { return #err(tx.index) }; + }; + + #ok(); + }; + + /// Checks if a transfer fee is valid + public func validate_fee( + token : T.TokenData, + opt_fee : ?T.Balance, + ) : Bool { + switch (opt_fee) { + case (?tx_fee) { + if (tx_fee < token._fee) { + return false; + }; + }; + case (null) { + if (token._fee > 0) { + return false; + }; + }; + }; + + true; + }; + + /// Checks if a transfer request is valid + public func validate_request( + token : T.TokenData, + tx_req : T.TransactionRequest, + ) : Result.Result<(), T.TransferError> { + + if (tx_req.from == tx_req.to) { + return #err( + #GenericError({ + error_code = 0; + message = "The sender cannot have the same account as the recipient."; + }) + ); + }; + + if (not Account.validate(tx_req.from)) { + return #err( + #GenericError({ + error_code = 0; + message = "Invalid account entered for sender. " # debug_show (tx_req.from); + }) + ); + }; + + if (not Account.validate(tx_req.to)) { + return #err( + #GenericError({ + error_code = 0; + message = "Invalid account entered for recipient " # debug_show (tx_req.to); + }) + ); + }; + + if (not validate_memo(tx_req.memo)) { + return #err( + #GenericError({ + error_code = 0; + message = "Memo must not be more than 32 bytes"; + }) + ); + }; + + if (tx_req.amount == 0) { + return #err( + #GenericError({ + error_code = 0; + message = "Amount must be greater than 0"; + }) + ); + }; + + switch (tx_req.kind) { + case (#transfer) { + if (not validate_fee(token, tx_req.fee)) { + return #err( + #BadFee { + expected_fee = token._fee; + } + ); + }; + + let balance : T.Balance = Utils.get_balance( + token.accounts, + tx_req.encoded.from, + ); + + if (tx_req.amount + token._fee > balance) { + return #err(#InsufficientFunds { balance }); + }; + }; + + case (#mint) { + if (token.max_supply < token._minted_tokens + tx_req.amount) { + let remaining_tokens = (token.max_supply - token._minted_tokens) : Nat; + + return #err( + #GenericError({ + error_code = 0; + message = "Cannot mint more than " # Nat.toText(remaining_tokens) # " tokens"; + }) + ); + }; + }; + case (#burn) { + if (tx_req.to == token.minting_account and tx_req.amount < token.min_burn_amount) { + return #err( + #BadBurn { min_burn_amount = token.min_burn_amount } + ); + }; + + let balance : T.Balance = Utils.get_balance( + token.accounts, + tx_req.encoded.from, + ); + + if (balance < tx_req.amount) { + return #err(#InsufficientFunds { balance }); + }; + }; + }; + + switch (tx_req.created_at_time) { + case (null) {}; + case (?created_at_time) { + + if (is_too_old(token, created_at_time)) { + return #err(#TooOld); + }; + + if (is_in_future(token, created_at_time)) { + return #err( + #CreatedInFuture { + ledger_time = Nat64.fromNat(Int.abs(Time.now())); + } + ); + }; + + switch (deduplicate(token, tx_req)) { + case (#err(tx_index)) { + return #err( + #Duplicate { + duplicate_of = tx_index; + } + ); + }; + case (_) {}; + }; + }; + }; + + #ok(); + }; + + public func validate_approve_request( + token : T.TokenData, + tx_req : T.ApproveTxRequest, + ) : Result.Result<(), T.ApproveError> { + // TODO: The spender's allowance for the { owner = caller; subaccount = from_subaccount } + // increases by the amount (or decreases if the amount is negative). If the total allowance + // is negative, the ledger MUST reset the allowance to zero. + if (tx_req.from.owner == tx_req.spender.owner) { + return #err( + #GenericError({ + error_code = 0; + message = "The approve Principal cannot be the same Principal as the approver."; + }) + ); + }; + + if (not Account.validate(tx_req.from)) { + return #err( + #GenericError({ + error_code = 0; + message = "Invalid account entered for sender. " # debug_show (tx_req.from); + }) + ); + }; + + if (not Account.validate(tx_req.spender)) { + return #err( + #GenericError({ + error_code = 0; + message = "Invalid account entered for recipient " # debug_show (tx_req.spender); + }) + ); + }; + + if (not validate_memo(tx_req.memo)) { + return #err( + #GenericError({ + error_code = 0; + message = "Memo must not be more than 32 bytes"; + }) + ); + }; + // seems it's not need to let amount < 0, cause type Nat is >= 0 always + if (tx_req.amount < 0) { + return #err( + #GenericError({ + error_code = 0; + message = "Amount must be greater than or euqal 0"; + }) + ); + }; + + switch (tx_req.kind) { + case (#approve) { + if (not validate_fee(token, tx_req.fee)) { + return #err( + #BadFee { + expected_fee = token._fee; + } + ); + }; + + let balance : T.Balance = Utils.get_balance( + token.accounts, + tx_req.encoded.from, + ); + + if (tx_req.amount > balance + token._fee) { + return #err(#InsufficientFunds { balance = balance }); + }; + }; + }; + + // check expected allowance + switch (tx_req.expected_allowance) { + case (null) {}; + case (?expected_allowance) { + let account_pair = Utils.gen_account_from_two_account(tx_req.encoded.from, tx_req.encoded.to); + let saved_allowance = Utils.get_allowance(token.approve_accounts, account_pair); + if (expected_allowance != saved_allowance.allowance) { + return #err( + #AllowanceChanged { + current_allowance = saved_allowance.allowance; + } + ); + }; + }; + }; + + switch (tx_req.created_at_time) { + case (null) {}; + case (?created_at_time) { + + if (is_too_old(token, created_at_time)) { + return #err(#TooOld); + }; + + if (is_in_future(token, created_at_time)) { + return #err( + #CreatedInFuture { + ledger_time = Nat64.fromNat(Int.abs(Time.now())); + } + ); + }; + }; + }; + + #ok(); + }; + + /// Checks if a transfer request is valid + public func validate_transfer_from_request( + token : T.TokenData, + tx_req : T.TransactionFromRequest, + ) : Result.Result<(), T.TransferFromError> { + + let encoded_caller_account = Account.encode({ + owner = tx_req.caller; + subaccount = null; + }); + + let account_pair = Utils.gen_account_from_two_account(tx_req.encoded.from, encoded_caller_account); + // check allowance + let allowance_pair : T.Allowance = Utils.get_allowance( + token.approve_accounts, + account_pair, + ); + + if (tx_req.amount > allowance_pair.allowance + token._fee) { + return #err(#InsufficientAllowance { allowance = allowance_pair.allowance }); + }; + + // check balance + let balance : T.Balance = Utils.get_balance( + token.accounts, + tx_req.encoded.from, + ); + + if (tx_req.amount > balance + token._fee) { + return #err(#InsufficientFunds { balance = balance }); + }; + + // check expire time + // TODO: let expire time be a new type of error + switch (allowance_pair.expires_at) { + case (null) {}; + case (?expires_at_time) { + switch (tx_req.created_at_time) { + case (null) {}; + case (?created_at_time) { + if (created_at_time > expires_at_time) { + return #err(#InsufficientFunds { balance = 0 }); + }; + }; + }; + }; + }; + + switch (tx_req.created_at_time) { + case (null) {}; + case (?created_at_time) { + + if (is_too_old(token, created_at_time)) { + return #err(#TooOld); + }; + + if (is_in_future(token, created_at_time)) { + return #err( + #CreatedInFuture { + ledger_time = Nat64.fromNat(Int.abs(Time.now())); + } + ); + }; + + // check deduplicate is in transfer validate_request + }; + }; + + #ok(); + }; + +}; diff --git a/I2-code/src/ICRC1/Types.mo b/I2-code/src/ICRC1/Types.mo new file mode 100644 index 00000000..368c6f67 --- /dev/null +++ b/I2-code/src/ICRC1/Types.mo @@ -0,0 +1,475 @@ +import Deque "mo:base/Deque"; +import List "mo:base/List"; +import Time "mo:base/Time"; +import Result "mo:base/Result"; + +import STMap "mo:StableTrieMap"; +import StableBuffer "mo:StableBuffer/StableBuffer"; + +module { + + public type Value = { #Nat : Nat; #Int : Int; #Blob : Blob; #Text : Text }; + + public type BlockIndex = Nat; + public type Subaccount = Blob; + public type Balance = Nat; + + public type StableBuffer = StableBuffer.StableBuffer; + public type StableTrieMap = STMap.StableTrieMap; + + public type Account = { + owner : Principal; + subaccount : ?Subaccount; + }; + + public type EncodedAccount = Blob; + + public type SupportedStandard = { + name : Text; + url : Text; + }; + + public type Memo = Blob; + public type Timestamp = Nat64; + public type Duration = Nat64; + public type TxIndex = Nat; + public type TxLog = StableBuffer; + + public type MetaDatum = (Text, Value); + public type MetaData = [MetaDatum]; + + public type ApproveError = { + #BadFee : { expected_fee : Nat }; + // The caller does not have enough funds to pay the approval fee. + #InsufficientFunds : { balance : Nat }; + // The caller specified the [expected_allowance] field, and the current + // allowance did not match the given value. + #AllowanceChanged : {current_allowance : Nat}; + // The approval request expired before the ledger had a chance to apply it. + #Expired : { ledger_time : Nat64 }; + #TooOld; + #CreatedInFuture : { ledger_time : Nat64 }; + #Duplicate : { duplicate_of : Nat }; + #TemporarilyUnavailable; + #GenericError : { error_code : Nat; message : Text }; + }; + + public type ApproveResult = { + #Ok : TxIndex; + #Err : ApproveError; + }; + + public type TxKind = { + #mint; + #burn; + #transfer; + }; + + public type ICRC2TxKind = { + #transfer_from; + }; + + public type OperationKind = { + #approve; + }; + + public type Mint = { + to : Account; + amount : Balance; + memo : ?Blob; + created_at_time : ?Nat64; + }; + + public type BurnArgs = { + from_subaccount : ?Subaccount; + amount : Balance; + memo : ?Blob; + created_at_time : ?Nat64; + }; + + public type Burn = { + from : Account; + amount : Balance; + memo : ?Blob; + created_at_time : ?Nat64; + }; + + /// Arguments for a transfer operation + public type TransferArgs = { + from_subaccount : ?Subaccount; + to : Account; + amount : Balance; + fee : ?Balance; + memo : ?Blob; + + /// The time at which the transaction was created. + /// If this is set, the canister will check for duplicate transactions and reject them. + created_at_time : ?Nat64; + }; + + /// Arguments for a transfer from operation + public type TransferFromArgs = { + from_subaccount : Account; + to : Account; + amount : Balance; + fee : ?Balance; + memo : ?Blob; + + /// The time at which the transaction was created. + /// If this is set, the canister will check for duplicate transactions and reject them. + created_at_time : ?Nat64; + }; + + public type Transfer = { + from : Account; + to : Account; + amount : Balance; + fee : ?Balance; + memo : ?Blob; + created_at_time : ?Nat64; + }; + + public type ApproveArgs = { + from_subaccount : ?Blob; + spender : Principal; + amount : Nat; + expires_at : ?Nat64; + fee : ?Nat; + memo : ?Blob; + created_at_time : ?Nat64; + expected_allowance : ?Nat; + }; + + public type Approve = { + kind : OperationKind; + from : Account; + spender : Account; + amount : Balance; + expires_at : ?Nat64; + fee : ?Balance; + memo : ?Blob; + created_at_time : ?Nat64; + }; + + public type AllowanceArgs = { + account : Account; + spender : Account; + }; + + public type Allowance = { + allowance : Nat; + expires_at : ?Nat64; + }; + + /// Internal representation of a transaction request + public type TransactionRequest = { + kind : TxKind; + from : Account; + to : Account; + amount : Balance; + fee : ?Balance; + memo : ?Blob; + created_at_time : ?Nat64; + encoded : { + from : EncodedAccount; + to : EncodedAccount; + }; + }; + + /// Internal representation of a transaction request + public type TransactionFromRequest = { + kind : ICRC2TxKind; + from : Account; + to : Account; + caller : Principal; + amount : Balance; + fee : ?Balance; + memo : ?Blob; + created_at_time : ?Nat64; + encoded : { + from : EncodedAccount; + to : EncodedAccount; + }; + }; + + public type ApproveTxRequest = { + kind : OperationKind; + from : Account; + spender : Account; + amount : Balance; + expires_at : ?Nat64; + fee : ?Balance; + memo : ?Blob; + created_at_time : ?Nat64; + encoded : { + from : EncodedAccount; + to : EncodedAccount; + }; + expected_allowance : ?Nat; + }; + + public type Transaction = { + kind : Text; + mint : ?Mint; + burn : ?Burn; + transfer : ?Transfer; + index : TxIndex; + timestamp : Timestamp; + }; + + // apart from icrc1 + public type ApproveTransaction = { + approve : Approve; + index : TxIndex; + timestamp : Timestamp; + }; + + public type TimeError = { + #TooOld; + #CreatedInFuture : { ledger_time : Timestamp }; + }; + + public type TransferError = TimeError or { + #BadFee : { expected_fee : Balance }; + #BadBurn : { min_burn_amount : Balance }; + #InsufficientFunds : { balance : Balance }; + #Duplicate : { duplicate_of : TxIndex }; + #TemporarilyUnavailable; + #GenericError : { error_code : Nat; message : Text }; + }; + + public type TransferResult = { + #Ok : TxIndex; + #Err : TransferError; + }; + + public type TransferFromError = { + #BadFee : { expected_fee : Balance }; + #BadBurn : { min_burn_amount : Balance }; + #InsufficientFunds : { balance : Balance }; + #InsufficientAllowance : { allowance : Balance }; + #TooOld; + #CreatedInFuture : { ledger_time : Timestamp }; + #Duplicate : { duplicate_of : TxIndex }; + #TemporarilyUnavailable; + #GenericError : { error_code : Nat; message : Text }; + }; + + public type TransferFromResult = { + #Ok : TxIndex; + #Err : TransferFromError; + }; + + /// Interface for the ICRC token canister + public type TokenInterface = actor { + + /// Returns the name of the token + icrc1_name : shared query () -> async Text; + + /// Returns the symbol of the token + icrc1_symbol : shared query () -> async Text; + + /// Returns the number of decimals the token uses + icrc1_decimals : shared query () -> async Nat8; + + /// Returns the fee charged for each transfer + icrc1_fee : shared query () -> async Balance; + + /// Returns the tokens metadata + icrc1_metadata : shared query () -> async MetaData; + + /// Returns the total supply of the token + icrc1_total_supply : shared query () -> async Balance; + + /// Returns the account that is allowed to mint new tokens + icrc1_minting_account : shared query () -> async ?Account; + + /// Returns the balance of the given account + icrc1_balance_of : shared query (Account) -> async Balance; + + /// Transfers the given amount of tokens from the sender to the recipient + icrc1_transfer : shared (TransferArgs) -> async TransferResult; + + /// Returns the standards supported by this token's implementation + icrc1_supported_standards : shared query () -> async [SupportedStandard]; + + }; + + public type TxCandidBlob = Blob; + + /// The Interface for the Archive canister + public type ArchiveInterface = actor { + /// Appends the given transactions to the archive. + /// > Only the Ledger canister is allowed to call this method + append_transactions : shared ([Transaction]) -> async Result.Result<(), Text>; + + /// Returns the total number of transactions stored in the archive + total_transactions : shared query () -> async Nat; + + /// Returns the transaction at the given index + get_transaction : shared query (TxIndex) -> async ?Transaction; + + /// Returns the transactions in the given range + get_transactions : shared query (GetTransactionsRequest) -> async TransactionRange; + + /// Returns the number of bytes left in the archive before it is full + /// > The capacity of the archive canister is 32GB + remaining_capacity : shared query () -> async Nat; + }; + + /// Initial arguments for the setting up the icrc1 token canister + public type InitArgs = { + name : Text; + symbol : Text; + decimals : Nat8; + fee : Balance; + minting_account : Account; + max_supply : Balance; + initial_balances : [(Account, Balance)]; + min_burn_amount : Balance; + + /// optional settings for the icrc1 canister + advanced_settings : ?AdvancedSettings; + }; + + /// [InitArgs](#type.InitArgs) with optional fields for initializing a token canister + public type TokenInitArgs = { + name : Text; + symbol : Text; + decimals : Nat8; + fee : Balance; + max_supply : Balance; + initial_balances : [(Account, Balance)]; + min_burn_amount : Balance; + + /// optional value that defaults to the caller if not provided + minting_account : ?Account; + + advanced_settings : ?AdvancedSettings; + }; + + /// Additional settings for the [InitArgs](#type.InitArgs) type during initialization of an icrc1 token canister + public type AdvancedSettings = { + /// needed if a token ever needs to be migrated to a new canister + burned_tokens : Balance; + transaction_window : Timestamp; + permitted_drift : Timestamp; + }; + + public type AccountBalances = StableTrieMap; + + public type ApproveBalances = StableTrieMap; + + /// The details of the archive canister + public type ArchiveData = { + /// The reference to the archive canister + var canister : ArchiveInterface; + + /// The number of transactions stored in the archive + var stored_txs : Nat; + }; + + /// The state of the token canister + public type TokenData = { + /// The name of the token + name : Text; + + /// The symbol of the token + symbol : Text; + + /// The number of decimals the token uses + decimals : Nat8; + + /// The fee charged for each transaction + var _fee : Balance; + + /// The maximum supply of the token + max_supply : Balance; + + /// The total amount of minted tokens + var _minted_tokens : Balance; + + /// The total amount of burned tokens + var _burned_tokens : Balance; + + /// The account that is allowed to mint new tokens + /// On initialization, the maximum supply is minted to this account + minting_account : Account; + + /// The balances of all accounts + accounts : AccountBalances; + + /// The balances of all appro + approve_accounts : ApproveBalances; + + /// The metadata for the token + metadata : StableBuffer; + + /// The standards supported by this token's implementation + supported_standards : StableBuffer; + + /// The time window in which duplicate transactions are not allowed + transaction_window : Nat; + + /// The minimum amount of tokens that must be burned in a transaction + min_burn_amount : Balance; + + /// The allowed difference between the ledger time and the time of the device the transaction was created on + permitted_drift : Nat; + + /// The recent transactions that have been processed by the ledger. + /// Only the last 2000 transactions are stored before being archived. + transactions : StableBuffer; + + approve_transactions : StableBuffer; + + /// The record that stores the details to the archive canister and number of transactions stored in it + archive : ArchiveData; + }; + + // Rosetta API + /// The type to request a range of transactions from the ledger canister + public type GetTransactionsRequest = { + start : TxIndex; + length : Nat; + }; + + public type TransactionRange = { + transactions : [Transaction]; + }; + + public type QueryArchiveFn = shared query (GetTransactionsRequest) -> async TransactionRange; + + public type ArchivedTransaction = { + /// The index of the first transaction to be queried in the archive canister + start : TxIndex; + /// The number of transactions to be queried in the archive canister + length : Nat; + + /// The callback function to query the archive canister + callback : QueryArchiveFn; + }; + + public type GetTransactionsResponse = { + /// The number of valid transactions in the ledger and archived canisters that are in the given range + log_length : Nat; + + /// the index of the first tx in the `transactions` field + first_index : TxIndex; + + /// The transactions in the ledger canister that are in the given range + transactions : [Transaction]; + + /// Pagination request for archived transactions in the given range + archived_transactions : [ArchivedTransaction]; + }; + + /// Functions supported by the rosetta + public type RosettaInterface = actor { + get_transactions : shared query (GetTransactionsRequest) -> async GetTransactionsResponse; + }; + + /// Interface of the ICRC token and Rosetta canister + public type FullInterface = TokenInterface and RosettaInterface; + +}; diff --git a/I2-code/src/ICRC1/Utils.mo b/I2-code/src/ICRC1/Utils.mo new file mode 100644 index 00000000..4234bc22 --- /dev/null +++ b/I2-code/src/ICRC1/Utils.mo @@ -0,0 +1,500 @@ +import Array "mo:base/Array"; +import Blob "mo:base/Blob"; +import Debug "mo:base/Debug"; +import Hash "mo:base/Hash"; +import Int "mo:base/Int"; +import Iter "mo:base/Iter"; +import Nat "mo:base/Nat"; +import Nat8 "mo:base/Nat8"; +import Nat32 "mo:base/Nat32"; +import Nat64 "mo:base/Nat64"; +import Option "mo:base/Option"; +import Principal "mo:base/Principal"; +import Result "mo:base/Result"; +import Time "mo:base/Time"; +import Buffer "mo:base/Buffer"; + +import ArrayModule "mo:array/Array"; +import Itertools "mo:itertools/Iter"; +import STMap "mo:StableTrieMap"; +import StableBuffer "mo:StableBuffer/StableBuffer"; + +import Account "Account"; +import T "Types"; + +module { + // Creates a Stable Buffer with the default metadata and returns it. + public func init_metadata(args : T.InitArgs) : StableBuffer.StableBuffer { + let metadata = SB.initPresized(4); + SB.add(metadata, ("icrc1:fee", #Nat(args.fee))); + SB.add(metadata, ("icrc1:name", #Text(args.name))); + SB.add(metadata, ("icrc1:symbol", #Text(args.symbol))); + SB.add(metadata, ("icrc1:decimals", #Nat(Nat8.toNat(args.decimals)))); + + metadata; + }; + + public let default_standard : T.SupportedStandard = { + name = "ICRC-1"; + url = "https://github.com/dfinity/ICRC-1"; + }; + + public let icrc2_standard : T.SupportedStandard = { + name = "ICRC-2"; + url = "https://github.com/dfinity/ICRC-1/tree/main/standards/ICRC-2"; + }; + + // Creates a Stable Buffer with the default supported standards and returns it. + public func init_standards() : StableBuffer.StableBuffer { + let standards = SB.initPresized(4); + SB.add(standards, default_standard); + SB.add(standards, icrc2_standard); + standards; + }; + + // Returns the default subaccount for cases where a user does + // not specify it. + public func default_subaccount() : T.Subaccount { + Blob.fromArray( + Array.tabulate(32, func(_ : Nat) : Nat8 { 0 }) + ); + }; + + // this is a local copy of deprecated Hash.hashNat8 (redefined to suppress the warning) + func hashNat8(key : [Nat32]) : Hash.Hash { + var hash : Nat32 = 0; + for (natOfKey in key.vals()) { + hash := hash +% natOfKey; + hash := hash +% hash << 10; + hash := hash ^ (hash >> 6); + }; + hash := hash +% hash << 3; + hash := hash ^ (hash >> 11); + hash := hash +% hash << 15; + return hash; + }; + + // Computes a hash from the least significant 32-bits of `n`, ignoring other bits. + public func hash(n : Nat) : Hash.Hash { + let j = Nat32.fromNat(n); + hashNat8([ + j & (255 << 0), + j & (255 << 8), + j & (255 << 16), + j & (255 << 24), + ]); + }; + + // Formats the different operation arguements into + // a `TransactionRequest`, an internal type to access fields easier. + public func create_transfer_req( + args : T.TransferArgs, + owner : Principal, + tx_kind : T.TxKind, + ) : T.TransactionRequest { + + let from = { + owner; + subaccount = args.from_subaccount; + }; + + let encoded = { + from = Account.encode(from); + to = Account.encode(args.to); + }; + + switch (tx_kind) { + case (#mint) { + { + args with kind = #mint; + fee = null; + from; + encoded; + }; + }; + case (#burn) { + { + args with kind = #burn; + fee = null; + from; + encoded; + }; + }; + case (#transfer) { + { + args with kind = #transfer; + from; + encoded; + }; + }; + }; + }; + + // Formats the different operation arguements into + // a `TransactionFromRequest`, an new internal type to access fields easier for icrc2. + public func create_transfer_from_req( + args : T.TransferFromArgs, + caller : Principal, + tx_kind : T.ICRC2TxKind, + ) : T.TransactionFromRequest { + + let encoded = { + from = Account.encode(args.from_subaccount); + to = Account.encode(args.to); + }; + + { + args with kind = #transfer_from; + from = args.from_subaccount; + caller; + encoded; + }; + }; + + public func create_approve_req( + args : T.ApproveArgs, + owner : Principal, + tx_kind : T.OperationKind, + ) : T.ApproveTxRequest { + + let from = { + owner; + subaccount = args.from_subaccount; + }; + + let to = { + owner = args.spender; + subaccount = null; + }; + + let encoded = { + from = Account.encode(from); + to = Account.encode(to); + }; + + { + kind = tx_kind; + from = from; + spender = to; + amount = args.amount; + expires_at = args.expires_at; + fee = args.fee; + memo = args.memo; + created_at_time = args.created_at_time; + expected_allowance = args.expected_allowance; + // args with kind = #approve; + encoded; + }; + }; + + // Transforms the transaction kind from `variant` to `Text` + public func kind_to_text(kind : T.TxKind) : Text { + switch (kind) { + case (#mint) "MINT"; + case (#burn) "BURN"; + case (#transfer) "TRANSFER"; + }; + }; + + // Formats the tx request into a finalised transaction + public func req_to_tx(tx_req : T.TransactionRequest, index : Nat) : T.Transaction { + + { + kind = kind_to_text(tx_req.kind); + mint = switch (tx_req.kind) { + case (#mint) { ?tx_req }; + case (_) null; + }; + + burn = switch (tx_req.kind) { + case (#burn) { ?tx_req }; + case (_) null; + }; + + transfer = switch (tx_req.kind) { + case (#transfer) { ?tx_req }; + case (_) null; + }; + + index; + timestamp = Nat64.fromNat(Int.abs(Time.now())); + }; + }; + + public func approve_req_to_tx(tx_req : T.ApproveTxRequest, index : Nat) : T.ApproveTransaction { + + { + kind = "APPROVE"; + approve = tx_req; + index; + timestamp = Nat64.fromNat(Int.abs(Time.now())); + }; + }; + + public func div_ceil(n : Nat, d : Nat) : Nat { + (n + d - 1) / d; + }; + + /// Retrieves the balance of an account + public func get_balance(accounts : T.AccountBalances, encoded_account : T.EncodedAccount) : T.Balance { + let res = STMap.get( + accounts, + Blob.equal, + Blob.hash, + encoded_account, + ); + + switch (res) { + case (?balance) { + balance; + }; + case (_) 0; + }; + }; + + /// Retrieves the balance of an account + public func get_allowance(accounts : T.ApproveBalances, encoded_account : T.EncodedAccount) : T.Allowance { + let res = STMap.get( + accounts, + Blob.equal, + Blob.hash, + encoded_account, + ); + + switch (res) { + case (?balance) { + balance; + }; + case (_) { + { + allowance = 0; + expires_at = null; + }; + }; + }; + }; + + /// Updates the balance of an account + public func update_balance( + accounts : T.AccountBalances, + encoded_account : T.EncodedAccount, + update : (T.Balance) -> T.Balance, + ) { + let prev_balance = get_balance(accounts, encoded_account); + let updated_balance = update(prev_balance); + + if (updated_balance != prev_balance) { + STMap.put( + accounts, + Blob.equal, + Blob.hash, + encoded_account, + updated_balance, + ); + }; + }; + + public func update_approve_balance( + accounts : T.ApproveBalances, + encoded_account : T.EncodedAccount, + update_allowance : (T.Allowance) -> T.Allowance, + change_expires_at : Bool, + ) { + let prev_balance = get_allowance(accounts, encoded_account); + let updated_balance = update_allowance(prev_balance); + + let prev_allowance = prev_balance.allowance; + let prev_expires_at = prev_balance.expires_at; + + let updated_allowance = updated_balance.allowance; + let updated_expires_at = updated_balance.expires_at; + + // update expire time + var expires_at : ?Nat64 = null; + if (change_expires_at) { + expires_at := updated_expires_at; + } else { + expires_at := prev_expires_at; + }; + + let insert_balance = { + allowance = updated_allowance; + expires_at; + }; + + if (updated_balance != prev_balance) { + STMap.put( + accounts, + Blob.equal, + Blob.hash, + encoded_account, + insert_balance, + ); + }; + }; + + // Transfers tokens from the sender to the + // recipient in the tx request + public func transfer_balance( + token : T.TokenData, + tx_req : T.TransactionRequest, + ) { + let { encoded; amount } = tx_req; + + update_balance( + token.accounts, + encoded.from, + func(balance) { + balance - amount; + }, + ); + + update_balance( + token.accounts, + encoded.to, + func(balance) { + balance + amount; + }, + ); + }; + + public func approve( + token : T.TokenData, + tx_req : T.ApproveTxRequest, + ) { + let { encoded; amount; expires_at } = tx_req; + + update_approve_balance( + token.approve_accounts, + gen_account_from_two_account(encoded.from, encoded.to), + func(balance) { + { + allowance = amount; + expires_at = expires_at; + }; + }, + true, + ); + }; + + /// create an account from Approver as `from` account and Spender as `to` account + public func gen_account_from_two_account(from : T.EncodedAccount, to : T.EncodedAccount) : T.EncodedAccount { + let from_buffer : Buffer.Buffer = Buffer.fromArray(Blob.toArray(from)); + let to_buffer : Buffer.Buffer = Buffer.fromArray(Blob.toArray(to)); + from_buffer.append(to_buffer); + let final_array = Buffer.toArray(from_buffer); + Blob.fromArray(final_array); + }; + + public func mint_balance( + token : T.TokenData, + encoded_account : T.EncodedAccount, + amount : T.Balance, + ) { + update_balance( + token.accounts, + encoded_account, + func(balance) { + balance + amount; + }, + ); + + token._minted_tokens += amount; + }; + + public func burn_balance( + token : T.TokenData, + encoded_account : T.EncodedAccount, + amount : T.Balance, + ) { + update_balance( + token.accounts, + encoded_account, + func(balance) { + balance - amount; + }, + ); + + token._burned_tokens += amount; + }; + + public func decrease_allowance( + token : T.TokenData, + encoded_account : T.EncodedAccount, + amount : T.Balance, + ) { + update_approve_balance( + token.approve_accounts, + encoded_account, + func(balance) { + { + allowance = balance.allowance - amount; + expires_at = null; + }; + }, + false, + ); + + }; + + // Stable Buffer Module with some additional functions + public let SB = { + StableBuffer with slice = func(buffer : T.StableBuffer, start : Nat, end : Nat) : [A] { + let size = SB.size(buffer); + if (start >= size) { + return []; + }; + + let slice_len = (Nat.min(end, size) - start) : Nat; + + Array.tabulate( + slice_len, + func(i : Nat) : A { + SB.get(buffer, i + start); + }, + ); + }; + + toIterFromSlice = func(buffer : T.StableBuffer, start : Nat, end : Nat) : Iter.Iter { + if (start >= SB.size(buffer)) { + return Itertools.empty(); + }; + + Iter.map( + Itertools.range(start, Nat.min(SB.size(buffer), end)), + func(i : Nat) : A { + SB.get(buffer, i); + }, + ); + }; + + appendArray = func(buffer : T.StableBuffer, array : [A]) { + for (elem in array.vals()) { + SB.add(buffer, elem); + }; + }; + + getLast = func(buffer : T.StableBuffer) : ?A { + let size = SB.size(buffer); + + if (size > 0) { + SB.getOpt(buffer, (size - 1) : Nat); + } else { + null; + }; + }; + + capacity = func(buffer : T.StableBuffer) : Nat { + buffer.elems.size(); + }; + + _clearedElemsToIter = func(buffer : T.StableBuffer) : Iter.Iter { + Iter.map( + Itertools.range(buffer.count, buffer.elems.size()), + func(i : Nat) : A { + buffer.elems[i]; + }, + ); + }; + }; +}; diff --git a/I2-code/src/ICRC1/lib.mo b/I2-code/src/ICRC1/lib.mo new file mode 100644 index 00000000..89392cf9 --- /dev/null +++ b/I2-code/src/ICRC1/lib.mo @@ -0,0 +1,562 @@ +import Array "mo:base/Array"; +import Blob "mo:base/Blob"; +import Debug "mo:base/Debug"; +import Float "mo:base/Float"; +import Int "mo:base/Int"; +import Iter "mo:base/Iter"; +import Nat "mo:base/Nat"; +import Nat64 "mo:base/Nat64"; +import Nat8 "mo:base/Nat8"; +import Option "mo:base/Option"; +import Principal "mo:base/Principal"; +import EC "mo:base/ExperimentalCycles"; + +import Itertools "mo:itertools/Iter"; +import StableTrieMap "mo:StableTrieMap"; + +import Account "Account"; +import T "Types"; +import Utils "Utils"; +import Transfer "Transfer"; +import Archive "Canisters/Archive"; + +/// The ICRC1 class with all the functions for creating an +/// ICRC1 token on the Internet Computer +module { + let { SB } = Utils; + + public type Account = T.Account; + public type Subaccount = T.Subaccount; + public type AccountBalances = T.AccountBalances; + + public type Transaction = T.Transaction; + public type Balance = T.Balance; + public type TransferArgs = T.TransferArgs; + public type TransferFromArgs = T.TransferFromArgs; + public type Mint = T.Mint; + public type BurnArgs = T.BurnArgs; + public type TransactionRequest = T.TransactionRequest; + public type TransferError = T.TransferError; + + public type SupportedStandard = T.SupportedStandard; + + public type InitArgs = T.InitArgs; + public type TokenInitArgs = T.TokenInitArgs; + public type TokenData = T.TokenData; + public type MetaDatum = T.MetaDatum; + public type TxLog = T.TxLog; + public type TxIndex = T.TxIndex; + + public type TokenInterface = T.TokenInterface; + public type RosettaInterface = T.RosettaInterface; + public type FullInterface = T.FullInterface; + + public type ArchiveInterface = T.ArchiveInterface; + + public type GetTransactionsRequest = T.GetTransactionsRequest; + public type GetTransactionsResponse = T.GetTransactionsResponse; + public type QueryArchiveFn = T.QueryArchiveFn; + public type TransactionRange = T.TransactionRange; + public type ArchivedTransaction = T.ArchivedTransaction; + + public type TransferResult = T.TransferResult; + public type TransferFromResult = T.TransferFromResult; + + public let MAX_TRANSACTIONS_IN_LEDGER = 2000; + public let MAX_TRANSACTION_BYTES : Nat64 = 196; + public let MAX_TRANSACTIONS_PER_REQUEST = 5000; + + public type ApproveArgs = T.ApproveArgs; + + public type AllowanceArgs = T.AllowanceArgs; + + public type Allowance = T.Allowance; + + public type ApproveResult = T.ApproveResult; + + /// Initialize a new ICRC-1 token + public func init(args : T.InitArgs) : T.TokenData { + let { + name; + symbol; + decimals; + fee; + minting_account; + max_supply; + initial_balances; + min_burn_amount; + advanced_settings; + } = args; + + var _burned_tokens = 0; + var permitted_drift = 60_000_000_000; + var transaction_window = 86_400_000_000_000; + + switch (advanced_settings) { + case (?options) { + _burned_tokens := options.burned_tokens; + permitted_drift := Nat64.toNat(options.permitted_drift); + transaction_window := Nat64.toNat(options.transaction_window); + }; + case (null) {}; + }; + + if (not Account.validate(minting_account)) { + Debug.trap("minting_account is invalid"); + }; + + let accounts : T.AccountBalances = StableTrieMap.new(); + + let approve_accounts : T.ApproveBalances = StableTrieMap.new(); + + var _minted_tokens = _burned_tokens; + + for ((i, (account, balance)) in Itertools.enumerate(initial_balances.vals())) { + + if (not Account.validate(account)) { + Debug.trap( + "Invalid Account: Account at index " # debug_show i # " is invalid in 'initial_balances'" + ); + }; + + let encoded_account = Account.encode(account); + + StableTrieMap.put( + accounts, + Blob.equal, + Blob.hash, + encoded_account, + balance, + ); + + _minted_tokens += balance; + }; + + { + name = name; + symbol = symbol; + decimals; + var _fee = fee; + max_supply; + var _minted_tokens = _minted_tokens; + var _burned_tokens = _burned_tokens; + min_burn_amount; + minting_account; + accounts; + approve_accounts; + metadata = Utils.init_metadata(args); + supported_standards = Utils.init_standards(); + transactions = SB.initPresized(MAX_TRANSACTIONS_IN_LEDGER); + approve_transactions = SB.initPresized(MAX_TRANSACTIONS_IN_LEDGER); + // approve_transactions = SB.initPresized(MAX_TRANSACTIONS_IN_LEDGER); + permitted_drift; + transaction_window; + archive = { + var canister = actor ("aaaaa-aa"); + var stored_txs = 0; + }; + }; + }; + + /// Retrieve the name of the token + public func name(token : T.TokenData) : Text { + token.name; + }; + + /// Retrieve the symbol of the token + public func symbol(token : T.TokenData) : Text { + token.symbol; + }; + + /// Retrieve the number of decimals specified for the token + public func decimals({ decimals } : T.TokenData) : Nat8 { + decimals; + }; + + /// Retrieve the fee for each transfer + public func fee(token : T.TokenData) : T.Balance { + token._fee; + }; + + /// Set the fee for each transfer + public func set_fee(token : T.TokenData, fee : Nat) { + token._fee := fee; + }; + + /// Retrieve all the metadata of the token + public func metadata(token : T.TokenData) : [T.MetaDatum] { + SB.toArray(token.metadata); + }; + + /// Returns the total supply of circulating tokens + public func total_supply(token : T.TokenData) : T.Balance { + token._minted_tokens - token._burned_tokens; + }; + + /// Returns the total supply of minted tokens + public func minted_supply(token : T.TokenData) : T.Balance { + token._minted_tokens; + }; + + /// Returns the total supply of burned tokens + public func burned_supply(token : T.TokenData) : T.Balance { + token._burned_tokens; + }; + + /// Returns the maximum supply of tokens + public func max_supply(token : T.TokenData) : T.Balance { + token.max_supply; + }; + + /// Returns the account with the permission to mint tokens + /// + /// Note: **The minting account can only participate in minting + /// and burning transactions, so any tokens sent to it will be + /// considered burned.** + + public func minting_account(token : T.TokenData) : T.Account { + token.minting_account; + }; + + /// Retrieve the balance of a given account + public func balance_of({ accounts } : T.TokenData, account : T.Account) : T.Balance { + let encoded_account = Account.encode(account); + Utils.get_balance(accounts, encoded_account); + }; + + /// Retrieve the balance of a given account and spender + public func get_allowance_of({ approve_accounts } : T.TokenData, account : T.Account, spender : Account) : T.Allowance { + let encoded_account = Account.encode(account); + let encoded_account_spender = Account.encode(spender); + let gen_account = Utils.gen_account_from_two_account(encoded_account, encoded_account_spender); + Utils.get_allowance(approve_accounts, gen_account); + }; + + /// Returns an array of standards supported by this token + public func supported_standards(token : T.TokenData) : [T.SupportedStandard] { + SB.toArray(token.supported_standards); + }; + + /// Formats a float to a nat balance and applies the correct number of decimal places + public func balance_from_float(token : T.TokenData, float : Float) : T.Balance { + if (float <= 0) { + return 0; + }; + + let float_with_decimals = float * (10 ** Float.fromInt(Nat8.toNat(token.decimals))); + + Int.abs(Float.toInt(float_with_decimals)); + }; + + /// Transfers tokens from one account to another account (minting and burning included) + public func transfer( + token : T.TokenData, + args : T.TransferArgs, + caller : Principal, + ) : async* T.TransferResult { + + let from = { + owner = caller; + subaccount = args.from_subaccount; + }; + + let tx_kind = if (from == token.minting_account) { + #mint; + } else if (args.to == token.minting_account) { + #burn; + } else { + #transfer; + }; + + let tx_req = Utils.create_transfer_req(args, caller, tx_kind); + + switch (Transfer.validate_request(token, tx_req)) { + case (#err(errorType)) { + return #Err(errorType); + }; + case (#ok(_)) {}; + }; + + let { encoded; amount } = tx_req; + + // process transaction + switch (tx_req.kind) { + case (#mint) { + Utils.mint_balance(token, encoded.to, amount); + }; + case (#burn) { + Utils.burn_balance(token, encoded.from, amount); + }; + case (#transfer) { + Utils.transfer_balance(token, tx_req); + + // burn fee + Utils.burn_balance(token, encoded.from, token._fee); + }; + }; + + // store transaction + let index = SB.size(token.transactions) + token.archive.stored_txs; + let tx = Utils.req_to_tx(tx_req, index); + SB.add(token.transactions, tx); + + // transfer transaction to archive if necessary + await* update_canister(token); + + #Ok(tx.index); + }; + + /// Transfers tokens from one account to another account (minting and burning included) + public func transfer_from( + token : T.TokenData, + args : T.TransferFromArgs, + caller : Principal, + ) : async* T.TransferFromResult { + + let tx_kind = #transfer_from; + + let tx_transfer_from_req = Utils.create_transfer_from_req(args, caller, tx_kind); + + switch (Transfer.validate_transfer_from_request(token, tx_transfer_from_req)) { + case (#err(errorType)) { + return #Err(errorType); + }; + case (#ok(_)) {}; + }; + + // icrc2 storage is complete, use normal transfer instead + let normal_transfer_args = { + from_subaccount = tx_transfer_from_req.from.subaccount; + to = tx_transfer_from_req.to; + amount = tx_transfer_from_req.amount; + fee = tx_transfer_from_req.fee; + memo = tx_transfer_from_req.memo; + created_at_time = tx_transfer_from_req.created_at_time; + }; + + let normal_tx_kind = #transfer; + let tx_req = Utils.create_transfer_req(normal_transfer_args, + args.from_subaccount.owner, + normal_tx_kind + ); + + switch (Transfer.validate_request(token, tx_req)) { + case (#err(errorType)) { + return #Err(errorType); + }; + case (#ok(_)) {}; + }; + + let { encoded; amount } = tx_req; + + // process transaction + Utils.transfer_balance(token, tx_req); + + // burn fee + Utils.burn_balance(token, encoded.from, token._fee); + + // decrease allowance + let caller_encoded = Account.encode({ + owner = caller; + subaccount = null; + }); + let allowance_key_account = Utils.gen_account_from_two_account(encoded.from, caller_encoded); + Utils.decrease_allowance(token, allowance_key_account, amount); + + // store transaction + let index = SB.size(token.transactions) + token.archive.stored_txs; + let tx = Utils.req_to_tx(tx_req, index); + SB.add(token.transactions, tx); + + // transfer transaction to archive if necessary + await* update_canister(token); + + #Ok(tx.index); + }; + /// Approve tokens from one account to another account + public func approve(token : T.TokenData, args : T.ApproveArgs, caller : Principal) : async* T.ApproveResult { + + let from = { + owner = caller; + subaccount = args.from_subaccount; + }; + + let tx_kind = #approve; + + let tx_req = Utils.create_approve_req(args, caller, tx_kind); + + switch (Transfer.validate_approve_request(token, tx_req)) { + case (#err(errorType)) { + return #Err(errorType); + }; + case (#ok(_)) {}; + }; + + let { encoded; amount } = tx_req; + + // process transaction + switch (tx_req.kind) { + case (#approve) { + Utils.approve(token, tx_req); + + // burn fee + // attention: fee is from caller account + Utils.burn_balance(token, encoded.from, token._fee); + }; + }; + + // store transaction + let index = SB.size(token.approve_transactions) + token.archive.stored_txs; + let tx = Utils.approve_req_to_tx(tx_req, index); + SB.add(token.approve_transactions, tx); + + // transfer transaction to archive if necessary + await* update_canister(token); + + #Ok(tx.index); + }; + + /// Helper function to mint tokens with minimum args + public func mint(token : T.TokenData, args : T.Mint, caller : Principal) : async* T.TransferResult { + + if (caller != token.minting_account.owner) { + return #Err( + #GenericError { + error_code = 401; + message = "Unauthorized: Only the minting_account can mint tokens."; + } + ); + }; + + let transfer_args : T.TransferArgs = { + args with from_subaccount = token.minting_account.subaccount; + fee = null; + }; + + await* transfer(token, transfer_args, caller); + }; + + /// Helper function to burn tokens with minimum args + public func burn(token : T.TokenData, args : T.BurnArgs, caller : Principal) : async* T.TransferResult { + + let transfer_args : T.TransferArgs = { + args with to = token.minting_account; + fee = null; + }; + + await* transfer(token, transfer_args, caller); + }; + + /// Returns the total number of transactions that have been processed by the given token. + public func total_transactions(token : T.TokenData) : Nat { + let { archive; transactions } = token; + archive.stored_txs + SB.size(transactions); + }; + + /// Retrieves the transaction specified by the given `tx_index` + public func get_transaction(token : T.TokenData, tx_index : T.TxIndex) : async* ?T.Transaction { + let { archive; transactions } = token; + + let archived_txs = archive.stored_txs; + + if (tx_index < archive.stored_txs) { + await archive.canister.get_transaction(tx_index); + } else { + let local_tx_index = (tx_index - archive.stored_txs) : Nat; + SB.getOpt(token.transactions, local_tx_index); + }; + }; + + /// Retrieves the transactions specified by the given range + public func get_transactions(token : T.TokenData, req : T.GetTransactionsRequest) : T.GetTransactionsResponse { + let { archive; transactions } = token; + + var first_index = 0xFFFF_FFFF_FFFF_FFFF; // returned if no transactions are found + + let req_end = req.start + req.length; + let tx_end = archive.stored_txs + SB.size(transactions); + + var txs_in_canister : [T.Transaction] = []; + + if (req.start < tx_end and req_end >= archive.stored_txs) { + first_index := Nat.max(req.start, archive.stored_txs); + let tx_start_index = (first_index - archive.stored_txs) : Nat; + + txs_in_canister := SB.slice(transactions, tx_start_index, req.length); + }; + + let archived_range = if (req.start < archive.stored_txs) { + { + start = req.start; + end = Nat.min( + archive.stored_txs, + (req.start + req.length) : Nat, + ); + }; + } else { + { start = 0; end = 0 }; + }; + + let txs_in_archive = (archived_range.end - archived_range.start) : Nat; + + let size = Utils.div_ceil(txs_in_archive, MAX_TRANSACTIONS_PER_REQUEST); + + let archived_transactions = Array.tabulate( + size, + func(i : Nat) : T.ArchivedTransaction { + let offset = i * MAX_TRANSACTIONS_PER_REQUEST; + let start = offset + archived_range.start; + let length = Nat.min( + MAX_TRANSACTIONS_PER_REQUEST, + archived_range.end - start, + ); + + let callback = token.archive.canister.get_transactions; + + { start; length; callback }; + }, + ); + + { + log_length = txs_in_archive + txs_in_canister.size(); + first_index; + transactions = txs_in_canister; + archived_transactions; + }; + }; + + // Updates the token's data and manages the transactions + // + // **added at the end of any function that creates a new transaction** + func update_canister(token : T.TokenData) : async* () { + let txs_size = SB.size(token.transactions); + + if (txs_size >= MAX_TRANSACTIONS_IN_LEDGER) { + await* append_transactions(token); + }; + }; + + // Moves the transactions from the ICRC1 canister to the archive canister + // and returns a boolean that indicates the success of the data transfer + func append_transactions(token : T.TokenData) : async* () { + let { archive; transactions } = token; + + if (archive.stored_txs == 0) { + EC.add(200_000_000_000); + archive.canister := await Archive.Archive(); + }; + + let res = await archive.canister.append_transactions( + SB.toArray(transactions) + ); + + switch (res) { + case (#ok(_)) { + archive.stored_txs += SB.size(transactions); + SB.clear(transactions); + }; + case (#err(_)) {}; + }; + }; + +}; diff --git a/I2-code/tests/ActorTest.mo b/I2-code/tests/ActorTest.mo new file mode 100644 index 00000000..29489eb0 --- /dev/null +++ b/I2-code/tests/ActorTest.mo @@ -0,0 +1,27 @@ +import Debug "mo:base/Debug"; + +import Archive "ICRC1/Archive.ActorTest"; +import ICRC1 "ICRC1/ICRC1.ActorTest"; + +import ActorSpec "./utils/ActorSpec"; + +actor { + let { run } = ActorSpec; + + let test_modules = [ + Archive.test, + ICRC1.test, + ]; + + public func run_tests() : async () { + for (test in test_modules.vals()) { + let success = ActorSpec.run([await test()]); + + if (success == false) { + Debug.trap("\1b[46;41mTests failed\1b[0m"); + } else { + Debug.print("\1b[23;42;3m Success!\1b[0m"); + }; + }; + }; +}; diff --git a/I2-code/tests/ICRC1/Account.Test.mo b/I2-code/tests/ICRC1/Account.Test.mo new file mode 100644 index 00000000..3d736934 --- /dev/null +++ b/I2-code/tests/ICRC1/Account.Test.mo @@ -0,0 +1,223 @@ +import Array "mo:base/Array"; +import Blob "mo:base/Blob"; +import Debug "mo:base/Debug"; +import Iter "mo:base/Iter"; +import Nat8 "mo:base/Nat8"; +import Principal "mo:base/Principal"; + +import Itertools "mo:itertools/Iter"; + +import Account "../../src/ICRC1/Account"; +import ActorSpec "../utils/ActorSpec"; +import Archive "../../src/ICRC1/Canisters/Archive"; + +let { + assertTrue; + assertFalse; + assertAllTrue; + describe; + it; + skip; + pending; + run; +} = ActorSpec; + +let principal = Principal.fromText("prb4z-5pc7u-zdfqi-cgv7o-fdyqf-n6afm-xh6hz-v4bk4-kpg3y-rvgxf-iae"); + +let success = run([ + describe( + "Account", + [ + describe( + "encode / decode Account", + [ + it( + "'null' subaccount", + do { + let account = { + owner = principal; + subaccount = null; + }; + + let encoded = Account.encode(account); + let decoded = Account.decode(encoded); + assertAllTrue([ + encoded == Principal.toBlob(account.owner), + decoded == ?account, + Account.fromText("prb4z-5pc7u-zdfqi-cgv7o-fdyqf-n6afm-xh6hz-v4bk4-kpg3y-rvgxf-iae") == ?account, + Account.toText(account) == "prb4z-5pc7u-zdfqi-cgv7o-fdyqf-n6afm-xh6hz-v4bk4-kpg3y-rvgxf-iae", + Account.validate(account) + ]); + }, + ), + it( + "subaccount with only zero bytes", + do { + let account = { + owner = principal; + subaccount = ?Blob.fromArray([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + }; + + let encoded = Account.encode(account); + let decoded = Account.decode(encoded); + + assertAllTrue([ + encoded == Principal.toBlob(account.owner), + decoded == ?{ account with subaccount = null }, + Account.fromText("prb4z-5pc7u-zdfqi-cgv7o-fdyqf-n6afm-xh6hz-v4bk4-kpg3y-rvgxf-iae") == ?{ account with subaccount = null }, + Account.toText(account) == "prb4z-5pc7u-zdfqi-cgv7o-fdyqf-n6afm-xh6hz-v4bk4-kpg3y-rvgxf-iae", + Account.validate(account) + ]); + }, + ), + it( + "subaccount prefixed with zero bytes", + do { + let account = { + owner = principal; + subaccount = ?Blob.fromArray([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8]); + }; + + let encoded = Account.encode(account); + let decoded = Account.decode(encoded); + + let pricipal_iter = Principal.toBlob(account.owner).vals(); + + let valid_bytes : [Nat8] = [1, 2, 3, 4, 5, 6, 7, 8]; + let suffix_bytes : [Nat8] = [ + 8, // size of valid_bytes + 0x7f // ending tag + ]; + + let iter = Itertools.chain( + pricipal_iter, + Itertools.chain( + valid_bytes.vals(), + suffix_bytes.vals(), + ), + ); + + let expected_blob = Blob.fromArray(Iter.toArray(iter)); + + assertAllTrue([ + encoded == expected_blob, + decoded == ?account, + Account.fromText("hamcw-wpc7u-zdfqi-cgv7o-fdyqf-n6afm-xh6hz-v4bk4-kpg3y-rvgxf-iaeai-camca-kbqhb-aeh6") == ?account, + Account.toText(account) == "hamcw-wpc7u-zdfqi-cgv7o-fdyqf-n6afm-xh6hz-v4bk4-kpg3y-rvgxf-iaeai-camca-kbqhb-aeh6", + Account.validate(account) + ]); + }, + ), + it( + "subaccount with zero bytes surrounded by non zero bytes", + do { + let account = { + owner = principal; + subaccount = ?Blob.fromArray([1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8]); + }; + + let encoded = Account.encode(account); + let decoded = Account.decode(encoded); + + let pricipal_iter = Principal.toBlob(account.owner).vals(); + + let valid_bytes : [Nat8] = [1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8]; + let suffix_bytes : [Nat8] = [ + 32, // size of valid_bytes + 0x7f // ending tag + ]; + + let iter = Itertools.chain( + pricipal_iter, + Itertools.chain( + valid_bytes.vals(), + suffix_bytes.vals(), + ), + ); + + let expected_blob = Blob.fromArray(Iter.toArray(iter)); + + assertAllTrue([ + encoded == expected_blob, + decoded == ?account, + Account.fromText("ojuko-dhc7u-zdfqi-cgv7o-fdyqf-n6afm-xh6hz-v4bk4-kpg3y-rvgxf-iaeai-camca-kbqhb-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa-aacaq-daqcq-mbyie-b7q") == ?account, + Account.toText(account) == "ojuko-dhc7u-zdfqi-cgv7o-fdyqf-n6afm-xh6hz-v4bk4-kpg3y-rvgxf-iaeai-camca-kbqhb-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa-aacaq-daqcq-mbyie-b7q", + Account.validate(account) + ]); + }, + ), + it( + "subaccount with non zero bytes", + do { + let account = { + owner = principal; + subaccount = ?Blob.fromArray([123, 234, 156, 89, 92, 91, 42, 8, 15, 2, 20, 80, 60, 20, 30, 10, 78, 2, 3, 78, 89, 23, 52, 55, 1, 2, 3, 4, 5, 6, 7, 8]); + }; + + let encoded = Account.encode(account); + let decoded = Account.decode(encoded); + + let pricipal_iter = Principal.toBlob(account.owner).vals(); + + let valid_bytes : [Nat8] = [123, 234, 156, 89, 92, 91, 42, 8, 15, 2, 20, 80, 60, 20, 30, 10, 78, 2, 3, 78, 89, 23, 52, 55, 1, 2, 3, 4, 5, 6, 7, 8]; + let suffix_bytes : [Nat8] = [ + 32, // size of valid_bytes + 0x7f // ending tag + ]; + + let iter = Itertools.chain( + pricipal_iter, + Itertools.chain( + valid_bytes.vals(), + suffix_bytes.vals(), + ), + ); + + let expected_blob = Blob.fromArray(Iter.toArray(iter)); + + assertAllTrue([ + encoded == expected_blob, + decoded == ?account, + Account.fromText("tx2rl-b7c7u-zdfqi-cgv7o-fdyqf-n6afm-xh6hz-v4bk4-kpg3y-rvgxf-iae67-ktrmv-ywzkb-ahqef-cqhqk-b4cso-aibu4-wixgq-3qcaq-daqcq-mbyie-b7q") == ?account, + Account.toText(account) == "tx2rl-b7c7u-zdfqi-cgv7o-fdyqf-n6afm-xh6hz-v4bk4-kpg3y-rvgxf-iae67-ktrmv-ywzkb-ahqef-cqhqk-b4cso-aibu4-wixgq-3qcaq-daqcq-mbyie-b7q", + Account.validate(account) + ]); + }, + ), + it( + "should return false for invalid subaccount (length < 32)", + do { + + var len = 0; + var is_valid = false; + + label _loop while (len < 32){ + let account = { + owner = principal; + subaccount = ?Blob.fromArray(Array.tabulate(len, Nat8.fromNat)); + }; + + is_valid := is_valid or Account.validate(account) + or Account.validate_subaccount(account.subaccount); + + if (is_valid) { + break _loop; + }; + + len += 1; + }; + + not is_valid; + } + ) + ], + ), + ], + ), +]); + +if (success == false) { + Debug.trap("\1b[46;41mTests failed\1b[0m"); +} else { + Debug.print("\1b[23;42;3m Success!\1b[0m"); +}; diff --git a/I2-code/tests/ICRC1/Archive.ActorTest.mo b/I2-code/tests/ICRC1/Archive.ActorTest.mo new file mode 100644 index 00000000..477197a3 --- /dev/null +++ b/I2-code/tests/ICRC1/Archive.ActorTest.mo @@ -0,0 +1,135 @@ +import Debug "mo:base/Debug"; +import Array "mo:base/Array"; +import Iter "mo:base/Iter"; +import Nat "mo:base/Nat"; +import Int "mo:base/Int"; +import Float "mo:base/Float"; +import Nat64 "mo:base/Nat64"; +import Principal "mo:base/Principal"; +import EC "mo:base/ExperimentalCycles"; + +import Archive "../../src/ICRC1/Canisters/Archive"; +import T "../../src/ICRC1/Types"; + +import ActorSpec "../utils/ActorSpec"; + +module { + let { + assertTrue; + assertFalse; + assertAllTrue; + describe; + it; + skip; + pending; + run; + } = ActorSpec; + + func new_tx(i : Nat) : T.Transaction { + { + kind = ""; + mint = null; + burn = null; + transfer = null; + index = i; + timestamp = Nat64.fromNat(i); + }; + }; + + // [start, end) + func txs_range(start : Nat, end : Nat) : [T.Transaction] { + Array.tabulate( + (end - start) : Nat, + func(i : Nat) : T.Transaction { + new_tx(start + i); + }, + ); + }; + + func new_txs(length : Nat) : [T.Transaction] { + txs_range(0, length); + }; + + let TC = 1_000_000_000_000; + let CREATE_CANISTER = 100_000_000_000; + + func create_canister_and_add_cycles(n : Float) { + EC.add( + CREATE_CANISTER + Int.abs(Float.toInt(n * 1_000_000_000_000)), + ); + }; + + public func test() : async ActorSpec.Group { + describe( + "Archive Canister", + [ + it( + "append_transactions()", + do { + create_canister_and_add_cycles(0.1); + let archive = await Archive.Archive(); + + let txs = new_txs(500); + + assertAllTrue([ + (await archive.total_transactions()) == 0, + (await archive.append_transactions(txs)) == #ok(), + (await archive.total_transactions()) == 500, + ]); + }, + ), + it( + "get_transaction()", + do { + create_canister_and_add_cycles(0.1); + let archive = await Archive.Archive(); + + let txs = new_txs(3555); + + let res = await archive.append_transactions(txs); + + assertAllTrue([ + res == #ok(), + (await archive.total_transactions()) == 3555, + (await archive.get_transaction(0)) == ?new_tx(0), + (await archive.get_transaction(999)) == ?new_tx(999), + (await archive.get_transaction(1000)) == ?new_tx(1000), + (await archive.get_transaction(1234)) == ?new_tx(1234), + (await archive.get_transaction(2829)) == ?new_tx(2829), + (await archive.get_transaction(3554)) == ?new_tx(3554), + (await archive.get_transaction(3555)) == null, + (await archive.get_transaction(999999)) == null, + ]); + }, + ), + it( + "get_transactions()", + do { + + create_canister_and_add_cycles(0.1); + let archive = await Archive.Archive(); + + let txs = new_txs(5000); + + let res = await archive.append_transactions(txs); + + let tx_range = await archive.get_transactions({ + start = 3251; + length = 2000; + }); + + assertAllTrue([ + res == #ok(), + (await archive.total_transactions()) == 5000, + (await archive.get_transactions({ start = 0; length = 100 })).transactions == txs_range(0, 100), + (await archive.get_transactions({ start = 225; length = 100 })).transactions == txs_range(225, 325), + (await archive.get_transactions({ start = 225; length = 1200 })).transactions == txs_range(225, 1425), + (await archive.get_transactions({ start = 980; length = 100 })).transactions == txs_range(980, 1080), + (await archive.get_transactions({ start = 3251; length = 2000 })).transactions == txs_range(3251, 5000), + ]); + }, + ), + ], + ); + }; +}; diff --git a/I2-code/tests/ICRC1/ICRC1.ActorTest.mo b/I2-code/tests/ICRC1/ICRC1.ActorTest.mo new file mode 100644 index 00000000..e5155d6f --- /dev/null +++ b/I2-code/tests/ICRC1/ICRC1.ActorTest.mo @@ -0,0 +1,847 @@ +import Array "mo:base/Array"; +import Debug "mo:base/Debug"; +import Iter "mo:base/Iter"; +import Nat "mo:base/Nat"; +import Nat8 "mo:base/Nat8"; +import Principal "mo:base/Principal"; + +import Itertools "mo:itertools/Iter"; +import StableBuffer "mo:StableBuffer/StableBuffer"; + +import ActorSpec "../utils/ActorSpec"; + +import ICRC1 "../../src/ICRC1"; +import T "../../src/ICRC1/Types"; + +import U "../../src/ICRC1/Utils"; + +module { + public func test() : async ActorSpec.Group { + + let { + assertTrue; + assertFalse; + assertAllTrue; + describe; + it; + skip; + pending; + run; + } = ActorSpec; + + let { SB } = U; + + func add_decimals(n : Nat, decimals : Nat) : Nat { + n * (10 ** decimals); + }; + + func mock_tx(to : T.Account, index : Nat) : T.Transaction { + { + burn = null; + transfer = null; + kind = "MINT"; + timestamp = 0; + index; + mint = ?{ + to; + amount = index + 1; + memo = null; + created_at_time = null; + }; + }; + }; + + let canister : T.Account = { + owner = Principal.fromText("x4ocp-k7ot7-oiqws-rg7if-j4q2v-ewcel-2x6we-l2eqz-rfz3e-6di6e-jae"); + subaccount = null; + }; + + let user1 : T.Account = { + owner = Principal.fromText("prb4z-5pc7u-zdfqi-cgv7o-fdyqf-n6afm-xh6hz-v4bk4-kpg3y-rvgxf-iae"); + subaccount = null; + }; + + let user2 : T.Account = { + owner = Principal.fromText("ygyq4-mf2rf-qmcou-h24oc-qwqvv-gt6lp-ifvxd-zaw3i-celt7-blnoc-5ae"); + subaccount = null; + }; + + func txs_range(start : Nat, end : Nat) : [T.Transaction] { + Array.tabulate( + (end - start) : Nat, + func(i : Nat) : T.Transaction { + mock_tx(user1, start + i); + }, + ); + }; + + func is_tx_equal(t1 : T.Transaction, t2 : T.Transaction) : Bool { + { t1 with timestamp = 0 } == { t2 with timestamp = 0 }; + }; + + func is_opt_tx_equal(t1 : ?T.Transaction, t2 : ?T.Transaction) : Bool { + switch (t1, t2) { + case (?t1, ?t2) { + is_tx_equal(t1, t2); + }; + case (_, ?t2) { false }; + case (?t1, _) { false }; + case (_, _) { true }; + }; + }; + + func validate_get_transactions( + token : T.TokenData, + tx_req : T.GetTransactionsRequest, + tx_res : T.GetTransactionsResponse + ) : Bool { + let { archive } = token; + + let token_start = 0; + let token_end = ICRC1.total_transactions(token); + + let req_start = tx_req.start; + let req_end = tx_req.start + tx_req.length; + + var log_length = 0; + + if (req_start < token_end) { + log_length := (Nat.min(token_end, req_end) - Nat.max(token_start, req_start)) : Nat; + }; + + if (log_length != tx_res.log_length) { + Debug.print("Failed at log_length: " # Nat.toText(log_length) # " != " # Nat.toText(tx_res.log_length)); + return false; + }; + + var txs_size = 0; + if (req_end > archive.stored_txs and req_start <= token_end) { + txs_size := Nat.min(req_end, token_end) - archive.stored_txs; + }; + + if (txs_size != tx_res.transactions.size()) { + Debug.print("Failed at txs_size: " # Nat.toText(txs_size) # " != " # Nat.toText(tx_res.transactions.size())); + return false; + }; + + if (txs_size > 0) { + let index = tx_res.transactions[0].index; + + if (tx_res.first_index != index) { + Debug.print("Failed at first_index: " # Nat.toText(tx_res.first_index) # " != " # Nat.toText(index)); + return false; + }; + + for (i in Iter.range(0, txs_size - 1)) { + let tx = tx_res.transactions[i]; + let mocked_tx = mock_tx(user1, archive.stored_txs + i); + + if (not is_tx_equal(tx, mocked_tx)) { + + Debug.print("Failed at tx: " # debug_show (tx) # " != " # debug_show (mocked_tx)); + return false; + }; + }; + } else { + if (tx_res.first_index != 0xFFFF_FFFF_FFFF_FFFF) { + Debug.print("Failed at first_index: " # Nat.toText(tx_res.first_index) # " != " # Nat.toText(0xFFFF_FFFF_FFFF_FFFF)); + return false; + }; + }; + + true; + }; + + func validate_archived_range(request : [T.GetTransactionsRequest], response : [T.ArchivedTransaction]) : async Bool { + + if (request.size() != response.size()) { + return false; + }; + + for ((req, res) in Itertools.zip(request.vals(), response.vals())) { + if (res.start != req.start) { + Debug.print("Failed at start: " # Nat.toText(res.start) # " != " # Nat.toText(req.start)); + return false; + }; + if (res.length != req.length) { + Debug.print("Failed at length: " # Nat.toText(res.length) # " != " # Nat.toText(req.length)); + return false; + }; + + let archived_txs = (await res.callback(req)).transactions; + let expected_txs = txs_range(res.start, res.start + res.length); + + if (archived_txs.size() != expected_txs.size()) { + return false; + }; + + for ((tx1, tx2) in Itertools.zip(archived_txs.vals(), expected_txs.vals())) { + if (not is_tx_equal(tx1, tx2)) { + Debug.print("Failed at archived_txs: " # debug_show (tx1, tx2)); + return false; + }; + }; + + }; + + true; + }; + + func are_txs_equal(t1 : [T.Transaction], t2 : [T.Transaction]) : Bool { + Itertools.equal(t1.vals(), t2.vals(), is_tx_equal); + }; + + func create_mints(token : T.TokenData, minting_principal : Principal, n : Nat) : async () { + for (i in Itertools.range(0, n)) { + ignore await* ICRC1.mint( + token, + { + to = user1; + amount = i + 1; + memo = null; + created_at_time = null; + }, + minting_principal, + ); + }; + }; + + let default_token_args : T.InitArgs = { + name = "Under-Collaterised Lending Tokens"; + symbol = "UCLTs"; + decimals = 8; + fee = 5 * (10 ** 8); + max_supply = 1_000_000_000 * (10 ** 8); + minting_account = canister; + initial_balances = []; + min_burn_amount = (10 * (10 ** 8)); + advanced_settings = null; + }; + + return describe( + "ICRC1 Token Implementation Tessts", + [ + it( + "init()", + do { + let args = default_token_args; + + let token = ICRC1.init(args); + + // returns without trapping + assertAllTrue([ + token.name == args.name, + token.symbol == args.symbol, + token.decimals == args.decimals, + token._fee == args.fee, + token.max_supply == args.max_supply, + + token.minting_account == args.minting_account, + SB.toArray(token.supported_standards) == [U.default_standard, U.icrc2_standard], + SB.size(token.transactions) == 0, + ]); + }, + ), + + it( + "name()", + do { + let args = default_token_args; + + let token = ICRC1.init(args); + + assertTrue( + ICRC1.name(token) == args.name, + ); + }, + ), + + it( + "symbol()", + do { + let args = default_token_args; + + let token = ICRC1.init(args); + + assertTrue( + ICRC1.symbol(token) == args.symbol, + ); + }, + ), + + it( + "decimals()", + do { + let args = default_token_args; + + let token = ICRC1.init(args); + + assertTrue( + ICRC1.decimals(token) == args.decimals, + ); + }, + ), + it( + "fee()", + do { + let args = default_token_args; + + let token = ICRC1.init(args); + + assertTrue( + ICRC1.fee(token) == args.fee, + ); + }, + ), + it( + "minting_account()", + do { + let args = default_token_args; + + let token = ICRC1.init(args); + + assertTrue( + ICRC1.minting_account(token) == args.minting_account, + ); + }, + ), + it( + "balance_of()", + do { + let args = default_token_args; + + let token = ICRC1.init({ args + with initial_balances = [ + (user1, 100), + (user2, 200), + ]; + }); + + assertAllTrue([ + ICRC1.balance_of(token, user1) == 100, + ICRC1.balance_of(token, user2) == 200, + ]); + }, + ), + it( + "total_supply()", + do { + let args = default_token_args; + + let token = ICRC1.init({ args + with initial_balances = [ + (user1, 100), + (user2, 200), + ]; + }); + + assertTrue( + ICRC1.total_supply(token) == 300, + ); + }, + ), + + it( + "metadata()", + do { + let args = default_token_args; + + let token = ICRC1.init(args); + + assertTrue( + ICRC1.metadata(token) == [ + ("icrc1:fee", #Nat(args.fee)), + ("icrc1:name", #Text(args.name)), + ("icrc1:symbol", #Text(args.symbol)), + ("icrc1:decimals", #Nat(Nat8.toNat(args.decimals))), + ], + ); + }, + ), + + it( + "supported_standards()", + do { + let args = default_token_args; + + let token = ICRC1.init(args); + + assertTrue( + ICRC1.supported_standards(token) == [{ + name = "ICRC-1"; + url = "https://github.com/dfinity/ICRC-1"; + }, U.icrc2_standard], + ); + }, + ), + + it( + "mint()", + do { + let args = default_token_args; + + let token = ICRC1.init(args); + + let mint_args : T.Mint = { + to = user1; + amount = 200 * (10 ** Nat8.toNat(args.decimals)); + memo = null; + created_at_time = null; + }; + + let res = await* ICRC1.mint( + token, + mint_args, + args.minting_account.owner, + ); + + assertAllTrue([ + res == #Ok(0), + ICRC1.balance_of(token, user1) == mint_args.amount, + ICRC1.balance_of(token, args.minting_account) == 0, + ICRC1.total_supply(token) == mint_args.amount, + ]); + }, + ), + + describe( + "burn()", + [ + it( + "from funded account", + do { + let args = default_token_args; + + let token = ICRC1.init(args); + + let mint_args : T.Mint = { + to = user1; + amount = 200 * (10 ** Nat8.toNat(args.decimals)); + memo = null; + created_at_time = null; + }; + + ignore await* ICRC1.mint( + token, + mint_args, + args.minting_account.owner, + ); + + let burn_args : T.BurnArgs = { + from_subaccount = user1.subaccount; + amount = 50 * (10 ** Nat8.toNat(args.decimals)); + memo = null; + created_at_time = null; + }; + + let prev_balance = ICRC1.balance_of(token, user1); + let prev_total_supply = ICRC1.total_supply(token); + + let res = await* ICRC1.burn(token, burn_args, user1.owner); + + assertAllTrue([ + res == #Ok(1), + ICRC1.balance_of(token, user1) == ((prev_balance - burn_args.amount) : Nat), + ICRC1.total_supply(token) == ((prev_total_supply - burn_args.amount) : Nat), + ]); + }, + ), + it( + "from an empty account", + do { + let args = default_token_args; + + let token = ICRC1.init(args); + + let burn_args : T.BurnArgs = { + from_subaccount = user1.subaccount; + amount = 200 * (10 ** Nat8.toNat(args.decimals)); + memo = null; + created_at_time = null; + }; + + let prev_balance = ICRC1.balance_of(token, user1); + let prev_total_supply = ICRC1.total_supply(token); + let res = await* ICRC1.burn(token, burn_args, user1.owner); + + assertAllTrue([ + res == #Err( + #InsufficientFunds { + balance = 0; + }, + ), + ]); + }, + ), + it( + "burn amount less than min_burn_amount", + do { + let args = default_token_args; + + let token = ICRC1.init(args); + + let mint_args : T.Mint = { + to = user1; + amount = 200 * (10 ** Nat8.toNat(args.decimals)); + memo = null; + created_at_time = null; + }; + + ignore await* ICRC1.mint( + token, + mint_args, + args.minting_account.owner, + ); + + let burn_args : T.BurnArgs = { + from_subaccount = user1.subaccount; + amount = 5 * (10 ** Nat8.toNat(args.decimals)); + memo = null; + created_at_time = null; + }; + + let res = await* ICRC1.burn(token, burn_args, user1.owner); + + assertAllTrue([ + res == #Err( + #BadBurn { + min_burn_amount = 10 * (10 ** 8); + }, + ), + ]); + }, + ), + ], + ), + describe( + "transfer()", + [ + it( + "Transfer from funded account", + do { + let args = default_token_args; + let token = ICRC1.init(args); + + let mint_args = { + to = user1; + amount = 200 * (10 ** Nat8.toNat(token.decimals)); + memo = null; + created_at_time = null; + }; + + ignore await* ICRC1.mint( + token, + mint_args, + args.minting_account.owner, + ); + + let transfer_args : T.TransferArgs = { + from_subaccount = user1.subaccount; + to = user2; + amount = 50 * (10 ** Nat8.toNat(token.decimals)); + fee = ?token._fee; + memo = null; + created_at_time = null; + }; + + let res = await* ICRC1.transfer( + token, + transfer_args, + user1.owner, + ); + + + assertAllTrue([ + res == #Ok(1), + ICRC1.balance_of(token, user1) == ICRC1.balance_from_float(token, 145), + token._burned_tokens == ICRC1.balance_from_float(token, 5), + ICRC1.balance_of(token, user2) == ICRC1.balance_from_float(token, 50), + ICRC1.total_supply(token) == ICRC1.balance_from_float(token, 195), + ]); + }, + ), + ], + ), + describe( + "approve()", + [ + it( + "Alice approve llowance to canister account", + do { + let args = default_token_args; + let token = ICRC1.init(args); + Debug.print(debug_show("expect ap:", 1200 * (10 ** Nat8.toNat(token.decimals)))); + + let mint_args = { + to = user1; + amount = 11200 * (10 ** Nat8.toNat(token.decimals)); + memo = null; + created_at_time = null; + }; + + ignore await* ICRC1.mint( + token, + mint_args, + args.minting_account.owner, + ); + + let approve_args : T.ApproveArgs = { + from_subaccount = null; + spender = canister.owner; + amount = 1200 * (10 ** Nat8.toNat(token.decimals)); + fee = ?token._fee; + memo = null; + created_at_time = null; + expires_at = null; + expected_allowance = null; + }; + + let res = await* ICRC1.approve( + token, + approve_args, + user1.owner, + ); + + assertAllTrue([ + res == #Ok(0), + ICRC1.get_allowance_of(token, user1, canister).allowance == 1200 * (10 ** Nat8.toNat(token.decimals)), + token._burned_tokens == ICRC1.balance_from_float(token, 5), + ICRC1.balance_of(token, user1) == ICRC1.balance_from_float(token, 11195), + ICRC1.total_supply(token) == ICRC1.balance_from_float(token, 11195), + ]); + }, + ), + ], + ), + describe( + "transfer_from()", + [ + it( + "Transfer from Alice account", + do { + let args = default_token_args; + let token = ICRC1.init(args); + + let mint_args = { + to = user1; + amount = 200 * (10 ** Nat8.toNat(token.decimals)); + memo = null; + created_at_time = null; + }; + + ignore await* ICRC1.mint( + token, + mint_args, + args.minting_account.owner, + ); + + let approve_args : T.ApproveArgs = { + from_subaccount = user1.subaccount; + spender = canister.owner; + amount = 60 * (10 ** Nat8.toNat(token.decimals)); + fee = ?token._fee; + memo = null; + created_at_time = null; + expires_at = null; + expected_allowance = null; + }; + + ignore await* ICRC1.approve( + token, + approve_args, + user1.owner, + ); + + let transfer_from_args : T.TransferFromArgs = { + from_subaccount = user1; + to = user2; + amount = 50 * (10 ** Nat8.toNat(token.decimals)); + fee = ?token._fee; + memo = null; + created_at_time = null; + }; + + let res = await* ICRC1.transfer_from( + token, + transfer_from_args, + canister.owner, + ); + + assertAllTrue([ + res == #Ok(1), + ICRC1.get_allowance_of(token, user1, canister).allowance == 10 * (10 ** Nat8.toNat(token.decimals)), + ICRC1.balance_of(token, user1) == ICRC1.balance_from_float(token, 140), + token._burned_tokens == ICRC1.balance_from_float(token, 10), + ICRC1.balance_of(token, user2) == ICRC1.balance_from_float(token, 50), + ICRC1.total_supply(token) == ICRC1.balance_from_float(token, 190), + ]); + }, + ), + ], + ), + + describe( + "Internal Archive Testing", + [ + describe( + "A token canister with 4123 total txs", + do { + let args = default_token_args; + let token = ICRC1.init(args); + + await create_mints(token, canister.owner, 4123); + [ + it( + "Archive has 4000 stored txs", + do { + + assertAllTrue([ + token.archive.stored_txs == 4000, + SB.size(token.transactions) == 123, + SB.capacity(token.transactions) == ICRC1.MAX_TRANSACTIONS_IN_LEDGER, + ]); + }, + ), + it( + "get_transaction() works for txs in the archive and ledger canister", + do { + assertAllTrue([ + is_opt_tx_equal( + (await* ICRC1.get_transaction(token, 0)), + ?mock_tx(user1, 0), + ), + is_opt_tx_equal( + (await* ICRC1.get_transaction(token, 1234)), + ?mock_tx(user1, 1234), + ), + is_opt_tx_equal( + (await* ICRC1.get_transaction(token, 2000)), + ?mock_tx(user1, 2000), + ), + is_opt_tx_equal( + (await* ICRC1.get_transaction(token, 4100)), + ?mock_tx(user1, 4100), + ), + is_opt_tx_equal( + (await* ICRC1.get_transaction(token, 4122)), + ?mock_tx(user1, 4122), + ), + ]); + }, + ), + it( + "get_transactions from 0 to 2000", + do { + let req = { + start = 0; + length = 2000; + }; + + let res = ICRC1.get_transactions( + token, + req, + ); + + let archived_txs = res.archived_transactions; + + assertAllTrue([ + validate_get_transactions(token, req, res), + (await validate_archived_range([{ start = 0; length = 2000 }], archived_txs)), + ]); + }, + ), + it( + "get_transactions from 3000 to 4123", + do { + let req = { + start = 3000; + length = 1123; + }; + + let res = ICRC1.get_transactions( + token, + req, + ); + + let archived_txs = res.archived_transactions; + + assertAllTrue([ + validate_get_transactions(token, req, res), + (await validate_archived_range([{ start = 3000; length = 1000 }], archived_txs)), + ]); + }, + ), + it( + "get_transactions from 4000 to 4123", + do { + let req = { + start = 4000; + length = 123; + }; + + let res = ICRC1.get_transactions( + token, + req, + ); + + let archived_txs = res.archived_transactions; + + assertAllTrue([ + validate_get_transactions(token, req, res), + (await validate_archived_range([], archived_txs)), + ]); + }, + ), + it( + "get_transactions exceeding the txs in the ledger (0 to 5000)", + do { + let req = { + start = 0; + length = 5000; + }; + + let res = ICRC1.get_transactions( + token, + req, + ); + + let archived_txs = res.archived_transactions; + + assertAllTrue([ + validate_get_transactions(token, req, res), + (await validate_archived_range([{ start = 0; length = 4000 }], archived_txs)), + + ]); + }, + ), + it( + "get_transactions outside the txs range (5000 to 6000)", + do { + let req = { + start = 5000; + length = 1000; + }; + + let res = ICRC1.get_transactions( + token, + req, + ); + + let archived_txs = res.archived_transactions; + + assertAllTrue([ + validate_get_transactions(token, req, res), + (await validate_archived_range([], archived_txs)), + + ]); + }, + ), + ]; + }, + ), + ], + ), + ], + ); + }; +}; diff --git a/I2-code/tests/test_template.md b/I2-code/tests/test_template.md new file mode 100644 index 00000000..2f5fab75 --- /dev/null +++ b/I2-code/tests/test_template.md @@ -0,0 +1,31 @@ +Filename: `[Section]/[Function].Test.mo` + +```motoko +import Debug "mo:base/Debug"; +import Iter "mo:base/Iter"; + +import ActorSpec "../utils/ActorSpec"; +import Algo "../../src"; +// import [FnName] "../../src/[section]/[FnName]"; + +let { + assertTrue; assertFalse; assertAllTrue; + describe; it; skip; pending; run +} = ActorSpec; + +let success = run([ + describe(" (Function Name) ", [ + it("(test name)", do { + + // ... + }), + ]) +]); + +if(success == false){ + Debug.trap("\1b[46;41mTests failed\1b[0m"); +}else{ + Debug.print("\1b[23;42;3m Success!\1b[0m"); +}; + +``` \ No newline at end of file diff --git a/I2-code/tests/utils/ActorSpec.mo b/I2-code/tests/utils/ActorSpec.mo new file mode 100644 index 00000000..088348c5 --- /dev/null +++ b/I2-code/tests/utils/ActorSpec.mo @@ -0,0 +1,179 @@ +import Debug "mo:base/Debug"; +import Array "mo:base/Array"; +import Iter "mo:base/Iter"; +import Int "mo:base/Int"; +import Nat "mo:base/Nat"; +import Text "mo:base/Text"; + +module { + public type Group = { + name : Text; + groups : [Group]; + status : Status; + }; + + type Status = { + failed : Nat; + passed : Nat; + pending : Nat; + skipped : Nat; + }; + + func eqStatus(x : Status, y : Status) : Bool { + x.failed == y.failed and x.passed == y.passed and x.pending == y.pending and x.skipped == y.skipped; + }; + + let emptyStatus : Status = { + failed = 0; + passed = 0; + pending = 0; + skipped = 0; + }; + + func appendStatus(x : Status, y : Status) : Status { + { + failed = x.failed + y.failed; + passed = x.passed + y.passed; + pending = x.pending + y.pending; + skipped = x.skipped + y.skipped; + }; + }; + + func printStatus(status : Status) : Text { + "Failed: " # Int.toText(status.failed) # ", Passed: " # Int.toText(status.passed) # ", Pending: " # Int.toText(status.pending) # ", Skipped: " # Int.toText(status.skipped); + }; + + public func run(groups_ : [Group]) : Bool { + let (groups, status) = getGroups(groups_); + printGroups(groups, ""); + Debug.print("\n"); + Debug.print(printStatus(status)); + Debug.print("\n"); + status.failed == 0; + }; + + func getGroups(groups_ : [Group]) : ([Group], Status) { + let groups = Array.thaw(groups_); + var status = emptyStatus; + for (index in groups_.keys()) { + let group = groups[index]; + let (newGroups, newGroupsStatus) = getGroups(group.groups); + let newStatus = appendStatus(group.status, newGroupsStatus); + status := appendStatus(status, newStatus); + let newGroup = { + name = group.name; + groups = newGroups; + status = newStatus; + }; + groups[index] := newGroup; + }; + (Array.freeze(groups), status); + }; + + func printGroups(groups_ : [Group], indent : Text) { + for (group in groups_.vals()) { + let isDescribe = Iter.size(Array.keys(group.groups)) > 0; + let newline = if isDescribe "\n" else ""; + let status = group.status; + let statusText = if (isDescribe) { + ": " # printStatus(status); + } else { + let failed = status.failed; + let passed = status.passed; + let pending = status.pending; + let skipped = status.skipped; + switch (failed, passed, pending, skipped) { + case (0, 0, 0, 0) { "" }; + case (1, 0, 0, 0) { ": Failed" }; + case (0, 1, 0, 0) { ": Passed" }; + case (0, 0, 1, 0) { ": Pending" }; + case (0, 0, 0, 1) { ": Skipped" }; + case (_, _, _, _) { ":" # printStatus(status) }; + }; + }; + Debug.print(newline # indent # group.name # statusText # "\n"); + printGroups(group.groups, indent # " "); + }; + }; + + public func describe(name_ : Text, groups_ : [Group]) : Group { + { + name = name_; + groups = groups_; + status = emptyStatus; + }; + }; + + public func it(name_ : Text, passed_ : Bool) : Group { + { + name = name_; + groups = []; + status = { + failed = if passed_ 0 else 1; + passed = if passed_ 1 else 0; + pending = 0; + skipped = 0; + }; + }; + }; + + public func itAsync(name_ : Text, passed_ : Bool) : async Group { + { + name = name_; + groups = []; + status = { + failed = if passed_ 0 else 1; + passed = if passed_ 1 else 0; + pending = 0; + skipped = 0; + }; + }; + }; + + public let test = it; + + public func skip(name_ : Text, passed_ : Bool) : Group { + { + name = name_; + groups = []; + status = { + failed = 0; + passed = 0; + pending = 0; + skipped = 1; + }; + }; + }; + + public func pending(name_ : Text) : Group { + { + name = name_; + groups = []; + status = { + failed = 0; + passed = 0; + pending = 1; + skipped = 0; + }; + }; + }; + + public func assertTrue(x : Bool) : Bool { + x == true; + }; + + public func assertFalse(x : Bool) : Bool { + x == false; + }; + + public func assertAllTrue(xs : [Bool]) : Bool { + var allTrue = true; + for (val in xs.vals()) { + if (val == false) { + return false; + }; + allTrue := allTrue and val; + }; + allTrue; + }; +}; diff --git a/I2-code/vessel.dhall b/I2-code/vessel.dhall new file mode 100644 index 00000000..ffe130b0 --- /dev/null +++ b/I2-code/vessel.dhall @@ -0,0 +1,4 @@ +{ + dependencies = [ "base", "array", "StableTrieMap", "StableBuffer", "itertools"], + compiler = Some "0.7.0" +}