├── .github └── workflows │ └── makefile.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── dfx.json ├── docs ├── ICRC1 │ ├── Account.html │ ├── Account.md │ ├── ArchiveApi.html │ ├── ArchiveApi.md │ ├── Canisters │ │ ├── Token.html │ │ └── Token.md │ ├── Transfer.html │ ├── Transfer.md │ ├── Types.html │ ├── Types.md │ ├── Utils.html │ ├── Utils.md │ ├── lib.html │ └── lib.md ├── index.html ├── index.md └── styles.css ├── example ├── .gitignore ├── dfx.json └── icrc1 │ └── main.mo ├── icrc1-default-args.txt ├── makefile ├── mops.toml ├── package-set.dhall ├── readme.md ├── src └── ICRC1 │ ├── Account.mo │ ├── Canisters │ ├── Archive.mo │ └── Token.mo │ ├── Transfer.mo │ ├── Types.mo │ ├── Utils.mo │ └── lib.mo ├── tests ├── ActorTest.mo ├── ICRC1 │ ├── Account.Test.mo │ ├── Archive.ActorTest.mo │ └── ICRC1.ActorTest.mo ├── test_template.md └── utils │ └── ActorSpec.mo └── vessel.dhall /.github/workflows/makefile.yml: -------------------------------------------------------------------------------- 1 | name: Makefile CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - dev 8 | pull_request_target: 9 | branches: 10 | - "*" 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | name: Build and test 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: actions/setup-node@v3 20 | with: 21 | node-version: 18 22 | - uses: aviate-labs/setup-dfx@v0.2.3 23 | with: 24 | dfx-version: 0.15.1 25 | 26 | - name: Cache Node modules 27 | uses: actions/cache@v2 28 | with: 29 | path: ~/.npm 30 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 31 | restore-keys: | 32 | ${{ runner.os }}-node- 33 | 34 | - name: Install moc version manager 35 | run: | 36 | npm --yes -g i mocv 37 | mocv use 0.10.2 38 | export DFX_MOC_PATH=$(mocv bin current)/moc 39 | 40 | - name: install mops 41 | run: | 42 | npm --yes -g i ic-mops 43 | mops i 44 | 45 | # - name: Detect warnings 46 | # run: make no-warn 47 | 48 | - name: Run Tests 49 | run: | 50 | make test 51 | make actor-test 52 | 53 | icrc1-ref-test: 54 | runs-on: ubuntu-latest 55 | 56 | name: ICRC-1 reference test 57 | steps: 58 | - uses: actions/checkout@v3 59 | with: 60 | submodules: recursive 61 | - uses: actions/setup-node@v3 62 | with: 63 | node-version: 18 64 | - uses: aviate-labs/setup-dfx@v0.2.3 65 | with: 66 | dfx-version: 0.15.1 67 | 68 | - name: Install moc version manager 69 | run: | 70 | npm --yes -g i mocv 71 | mocv use 0.10.2 72 | export DFX_MOC_PATH=$(mocv bin current)/moc 73 | 74 | - name: Install stable toolchain 75 | uses: actions-rs/toolchain@v1 76 | with: 77 | profile: minimal 78 | toolchain: stable 79 | override: true 80 | 81 | - name: install mops 82 | run: | 83 | npm --yes -g i ic-mops 84 | mops i 85 | 86 | - name: Run reference tests 87 | run: | 88 | echo "${{ secrets.IDENTITY_SSH }}" > ./identity.pem 89 | dfx identity import icrc-ref-test ./identity.pem --storage-mode plaintext 90 | dfx identity use icrc-ref-test 91 | dfx identity whoami 92 | make ref-test 93 | 94 | 95 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Various IDEs and Editors 2 | .vscode/ 3 | .idea/ 4 | **/*~ 5 | 6 | # Mac OSX temporary files 7 | .DS_Store 8 | **/.DS_Store 9 | 10 | # dfx temporary files 11 | .dfx/ 12 | .vessel/ 13 | .mops/ 14 | 15 | # frontend code 16 | node_modules/ 17 | dist/ 18 | 19 | Cargo.lock -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Dfnity-ICRC1-Reference"] 2 | path = Dfnity-ICRC1-Reference 3 | url = https://github.com/dfinity/ICRC-1 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Tomi Jaga 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /dfx.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "canisters": { 4 | "icrc1": { 5 | "type": "motoko", 6 | "main": "src/ICRC1/Canisters/Token.mo" 7 | }, 8 | "test": { 9 | "type": "motoko", 10 | "main": "tests/ActorTest.mo", 11 | "args": "-v --compacting-gc" 12 | } 13 | }, 14 | "defaults": { 15 | "build": { 16 | "packtool": "mops sources", 17 | "args": "" 18 | } 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /docs/ICRC1/Account.html: -------------------------------------------------------------------------------- 1 | 2 |

ICRC1/Account

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

Checks if a subaccount is valid

3 |

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

Checks if an account is valid

4 |

public func encode() : T.EncodedAccount

Implementation of ICRC1's Textual representation of accounts Encoding Standard

5 |

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

Implementation of ICRC1's Textual representation of accounts Decoding Standard

6 |

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

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

7 |

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

Converts an ICRC-1 Account to its Textual representation

8 |

-------------------------------------------------------------------------------- /docs/ICRC1/Account.md: -------------------------------------------------------------------------------- 1 | # ICRC1/Account 2 | 3 | ## Function `validate_subaccount` 4 | ``` motoko no-repl 5 | func validate_subaccount(subaccount : ?T.Subaccount) : Bool 6 | ``` 7 | 8 | Checks if a subaccount is valid 9 | 10 | ## Function `validate` 11 | ``` motoko no-repl 12 | func validate(account : T.Account) : Bool 13 | ``` 14 | 15 | Checks if an account is valid 16 | 17 | ## Function `encode` 18 | ``` motoko no-repl 19 | func encode() : T.EncodedAccount 20 | ``` 21 | 22 | Implementation of ICRC1's Textual representation of accounts [Encoding Standard](https://github.com/dfinity/ICRC-1/tree/main/standards/ICRC-1#encoding) 23 | 24 | ## Function `decode` 25 | ``` motoko no-repl 26 | func decode(encoded : T.EncodedAccount) : ?T.Account 27 | ``` 28 | 29 | Implementation of ICRC1's Textual representation of accounts [Decoding Standard](https://github.com/dfinity/ICRC-1/tree/main/standards/ICRC-1#decoding) 30 | 31 | ## Function `fromText` 32 | ``` motoko no-repl 33 | func fromText(encoded : Text) : ?T.Account 34 | ``` 35 | 36 | Converts an ICRC-1 Account from its Textual representation to the `Account` type 37 | 38 | ## Function `toText` 39 | ``` motoko no-repl 40 | func toText(account : T.Account) : Text 41 | ``` 42 | 43 | Converts an ICRC-1 `Account` to its Textual representation 44 | -------------------------------------------------------------------------------- /docs/ICRC1/ArchiveApi.html: -------------------------------------------------------------------------------- 1 | 2 |

ICRC1/ArchiveApi

public func create_canister() : async T.ArchiveInterface

creates a new archive canister

3 |

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

Get the total number of archived transactions

4 |

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

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

6 |

-------------------------------------------------------------------------------- /docs/ICRC1/ArchiveApi.md: -------------------------------------------------------------------------------- 1 | # ICRC1/ArchiveApi 2 | 3 | ## Function `create_canister` 4 | ``` motoko no-repl 5 | func create_canister() : async T.ArchiveInterface 6 | ``` 7 | 8 | creates a new archive canister 9 | 10 | ## Function `total_txs` 11 | ``` motoko no-repl 12 | func total_txs(archives : T.StableBuffer) : Nat 13 | ``` 14 | 15 | Get the total number of archived transactions 16 | 17 | ## Function `append_transactions` 18 | ``` motoko no-repl 19 | func append_transactions(token : T.TokenData) : async () 20 | ``` 21 | 22 | Moves the transactions from the ICRC1 canister to the archive canister 23 | and returns a boolean that indicates the success of the data transfer 24 | -------------------------------------------------------------------------------- /docs/ICRC1/Canisters/Token.html: -------------------------------------------------------------------------------- 1 | 2 |

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

3 |

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 ()

-------------------------------------------------------------------------------- /docs/ICRC1/Canisters/Token.md: -------------------------------------------------------------------------------- 1 | # ICRC1/Canisters/Token 2 | 3 | ## Type `TokenInitArgs` 4 | ``` motoko no-repl 5 | 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)] } 6 | ``` 7 | 8 | 9 | ## `actor class Token` 10 | 11 | 12 | ### Function `icrc1_name` 13 | ``` motoko no-repl 14 | func icrc1_name() : async Text 15 | ``` 16 | 17 | Functions for the ICRC1 token standard 18 | 19 | 20 | ### Function `icrc1_symbol` 21 | ``` motoko no-repl 22 | func icrc1_symbol() : async Text 23 | ``` 24 | 25 | 26 | 27 | ### Function `icrc1_decimals` 28 | ``` motoko no-repl 29 | func icrc1_decimals() : async Nat8 30 | ``` 31 | 32 | 33 | 34 | ### Function `icrc1_fee` 35 | ``` motoko no-repl 36 | func icrc1_fee() : async ICRC1.Balance 37 | ``` 38 | 39 | 40 | 41 | ### Function `icrc1_metadata` 42 | ``` motoko no-repl 43 | func icrc1_metadata() : async [ICRC1.MetaDatum] 44 | ``` 45 | 46 | 47 | 48 | ### Function `icrc1_total_supply` 49 | ``` motoko no-repl 50 | func icrc1_total_supply() : async ICRC1.Balance 51 | ``` 52 | 53 | 54 | 55 | ### Function `icrc1_minting_account` 56 | ``` motoko no-repl 57 | func icrc1_minting_account() : async ?ICRC1.Account 58 | ``` 59 | 60 | 61 | 62 | ### Function `icrc1_balance_of` 63 | ``` motoko no-repl 64 | func icrc1_balance_of(args : ICRC1.Account) : async ICRC1.Balance 65 | ``` 66 | 67 | 68 | 69 | ### Function `icrc1_supported_standards` 70 | ``` motoko no-repl 71 | func icrc1_supported_standards() : async [ICRC1.SupportedStandard] 72 | ``` 73 | 74 | 75 | 76 | ### Function `icrc1_transfer` 77 | ``` motoko no-repl 78 | func icrc1_transfer(args : ICRC1.TransferArgs) : async Result.Result 79 | ``` 80 | 81 | 82 | 83 | ### Function `mint` 84 | ``` motoko no-repl 85 | func mint(args : ICRC1.Mint) : async Result.Result 86 | ``` 87 | 88 | 89 | 90 | ### Function `burn` 91 | ``` motoko no-repl 92 | func burn(args : ICRC1.BurnArgs) : async Result.Result 93 | ``` 94 | 95 | 96 | 97 | ### Function `get_transaction` 98 | ``` motoko no-repl 99 | func get_transaction(token_id : Nat) : async ?ICRC1.Transaction 100 | ``` 101 | 102 | 103 | 104 | ### Function `get_transactions` 105 | ``` motoko no-repl 106 | func get_transactions(req : ICRC1.GetTransactionsRequest) : async () 107 | ``` 108 | 109 | -------------------------------------------------------------------------------- /docs/ICRC1/Transfer.html: -------------------------------------------------------------------------------- 1 | 2 |

ICRC1/Transfer

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

Checks if a transaction memo is valid

3 |

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

4 |

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.

5 |

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.

6 |

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

7 |

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

Checks if a transfer fee is valid

8 |

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

Checks if a transfer request is valid

9 |

-------------------------------------------------------------------------------- /docs/ICRC1/Transfer.md: -------------------------------------------------------------------------------- 1 | # ICRC1/Transfer 2 | 3 | ## Function `validate_memo` 4 | ``` motoko no-repl 5 | func validate_memo(memo : ?T.Memo) : Bool 6 | ``` 7 | 8 | Checks if a transaction memo is valid 9 | 10 | ## Function `is_too_old` 11 | ``` motoko no-repl 12 | func is_too_old(token : T.TokenData, created_at_time : Nat64) : Bool 13 | ``` 14 | 15 | Checks if the `created_at_time` of a transfer request is before the accepted time range 16 | 17 | ## Function `is_in_future` 18 | ``` motoko no-repl 19 | func is_in_future(token : T.TokenData, created_at_time : Nat64) : Bool 20 | ``` 21 | 22 | Checks if the `created_at_time` of a transfer request has not been reached yet relative to the canister's time. 23 | 24 | ## Function `deduplicate` 25 | ``` motoko no-repl 26 | func deduplicate(token : T.TokenData, tx_req : T.TransactionRequest) : Result.Result<(), Nat> 27 | ``` 28 | 29 | Checks if there is a duplicate transaction that matches the transfer request in the main canister. 30 | 31 | If a duplicate is found, the function returns an error (`#err`) with the duplicate transaction's index. 32 | 33 | ## Function `validate_fee` 34 | ``` motoko no-repl 35 | func validate_fee(token : T.TokenData, opt_fee : ?T.Balance) : Bool 36 | ``` 37 | 38 | Checks if a transfer fee is valid 39 | 40 | ## Function `validate_request` 41 | ``` motoko no-repl 42 | func validate_request(token : T.TokenData, tx_req : T.TransactionRequest) : Result.Result<(), T.TransferError> 43 | ``` 44 | 45 | Checks if a transfer request is valid 46 | -------------------------------------------------------------------------------- /docs/ICRC1/Types.html: -------------------------------------------------------------------------------- 1 | 2 |

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

3 |

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

4 |

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

5 |

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

6 |

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

7 |

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

8 |

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

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

9 |

type AccountBalances = StableTrieMap<EncodedAccount, Balance>

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

The details of the archive canister

10 |

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

11 |

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

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

12 |

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

13 |

type FullInterface = TokenInterface and RosettaInterface

Interface of the ICRC token and Rosetta canister

14 |

-------------------------------------------------------------------------------- /docs/ICRC1/Types.md: -------------------------------------------------------------------------------- 1 | # ICRC1/Types 2 | 3 | ## Type `Value` 4 | ``` motoko no-repl 5 | type Value = {#Nat : Nat; #Int : Int; #Blob : Blob; #Text : Text} 6 | ``` 7 | 8 | 9 | ## Type `BlockIndex` 10 | ``` motoko no-repl 11 | type BlockIndex = Nat 12 | ``` 13 | 14 | 15 | ## Type `Subaccount` 16 | ``` motoko no-repl 17 | type Subaccount = Blob 18 | ``` 19 | 20 | 21 | ## Type `Balance` 22 | ``` motoko no-repl 23 | type Balance = Nat 24 | ``` 25 | 26 | 27 | ## Type `StableBuffer` 28 | ``` motoko no-repl 29 | type StableBuffer = StableBuffer.StableBuffer 30 | ``` 31 | 32 | 33 | ## Type `StableTrieMap` 34 | ``` motoko no-repl 35 | type StableTrieMap = STMap.StableTrieMap 36 | ``` 37 | 38 | 39 | ## Type `Account` 40 | ``` motoko no-repl 41 | type Account = { owner : Principal; subaccount : ?Subaccount } 42 | ``` 43 | 44 | 45 | ## Type `EncodedAccount` 46 | ``` motoko no-repl 47 | type EncodedAccount = Blob 48 | ``` 49 | 50 | 51 | ## Type `SupportedStandard` 52 | ``` motoko no-repl 53 | type SupportedStandard = { name : Text; url : Text } 54 | ``` 55 | 56 | 57 | ## Type `Memo` 58 | ``` motoko no-repl 59 | type Memo = Blob 60 | ``` 61 | 62 | 63 | ## Type `Timestamp` 64 | ``` motoko no-repl 65 | type Timestamp = Nat64 66 | ``` 67 | 68 | 69 | ## Type `Duration` 70 | ``` motoko no-repl 71 | type Duration = Nat64 72 | ``` 73 | 74 | 75 | ## Type `TxIndex` 76 | ``` motoko no-repl 77 | type TxIndex = Nat 78 | ``` 79 | 80 | 81 | ## Type `TxLog` 82 | ``` motoko no-repl 83 | type TxLog = StableBuffer 84 | ``` 85 | 86 | 87 | ## Type `MetaDatum` 88 | ``` motoko no-repl 89 | type MetaDatum = (Text, Value) 90 | ``` 91 | 92 | 93 | ## Type `MetaData` 94 | ``` motoko no-repl 95 | type MetaData = [MetaDatum] 96 | ``` 97 | 98 | 99 | ## Type `TxKind` 100 | ``` motoko no-repl 101 | type TxKind = {#mint; #burn; #transfer} 102 | ``` 103 | 104 | 105 | ## Type `Mint` 106 | ``` motoko no-repl 107 | type Mint = { to : Account; amount : Balance; memo : ?Blob; created_at_time : ?Nat64 } 108 | ``` 109 | 110 | 111 | ## Type `BurnArgs` 112 | ``` motoko no-repl 113 | type BurnArgs = { from_subaccount : ?Subaccount; amount : Balance; memo : ?Blob; created_at_time : ?Nat64 } 114 | ``` 115 | 116 | 117 | ## Type `Burn` 118 | ``` motoko no-repl 119 | type Burn = { from : Account; amount : Balance; memo : ?Blob; created_at_time : ?Nat64 } 120 | ``` 121 | 122 | 123 | ## Type `TransferArgs` 124 | ``` motoko no-repl 125 | type TransferArgs = { from_subaccount : ?Subaccount; to : Account; amount : Balance; fee : ?Balance; memo : ?Blob; created_at_time : ?Nat64 } 126 | ``` 127 | 128 | Arguments for a transfer operation 129 | 130 | ## Type `Transfer` 131 | ``` motoko no-repl 132 | type Transfer = { from : Account; to : Account; amount : Balance; fee : ?Balance; memo : ?Blob; created_at_time : ?Nat64 } 133 | ``` 134 | 135 | 136 | ## Type `TransactionRequest` 137 | ``` motoko no-repl 138 | type TransactionRequest = { kind : TxKind; from : Account; to : Account; amount : Balance; fee : ?Balance; memo : ?Blob; created_at_time : ?Nat64; encoded : { from : EncodedAccount; to : EncodedAccount } } 139 | ``` 140 | 141 | Internal representation of a transaction request 142 | 143 | ## Type `Transaction` 144 | ``` motoko no-repl 145 | type Transaction = { kind : Text; mint : ?Mint; burn : ?Burn; transfer : ?Transfer; index : TxIndex; timestamp : Timestamp } 146 | ``` 147 | 148 | 149 | ## Type `TimeError` 150 | ``` motoko no-repl 151 | type TimeError = {#TooOld; #CreatedInFuture : { ledger_time : Timestamp }} 152 | ``` 153 | 154 | 155 | ## Type `TransferError` 156 | ``` motoko no-repl 157 | 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 }} 158 | ``` 159 | 160 | 161 | ## Type `TransferResult` 162 | ``` motoko no-repl 163 | type TransferResult = {#Ok : TxIndex; #Err : TransferError} 164 | ``` 165 | 166 | 167 | ## Type `TokenInterface` 168 | ``` motoko no-repl 169 | 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] } 170 | ``` 171 | 172 | Interface for the ICRC token canister 173 | 174 | ## Type `TxCandidBlob` 175 | ``` motoko no-repl 176 | type TxCandidBlob = Blob 177 | ``` 178 | 179 | 180 | ## Type `ArchiveInterface` 181 | ``` motoko no-repl 182 | 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 } 183 | ``` 184 | 185 | The Interface for the Archive canister 186 | 187 | ## Type `InitArgs` 188 | ``` motoko no-repl 189 | 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 } 190 | ``` 191 | 192 | Initial arguments for the setting up the icrc1 token canister 193 | 194 | ## Type `TokenInitArgs` 195 | ``` motoko no-repl 196 | 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 } 197 | ``` 198 | 199 | [InitArgs](#type.InitArgs) with optional fields for initializing a token canister 200 | 201 | ## Type `AdvancedSettings` 202 | ``` motoko no-repl 203 | type AdvancedSettings = { burned_tokens : Balance; transaction_window : Timestamp; permitted_drift : Timestamp } 204 | ``` 205 | 206 | Additional settings for the [InitArgs](#type.InitArgs) type during initialization of an icrc1 token canister 207 | 208 | ## Type `AccountBalances` 209 | ``` motoko no-repl 210 | type AccountBalances = StableTrieMap 211 | ``` 212 | 213 | 214 | ## Type `ArchiveData` 215 | ``` motoko no-repl 216 | type ArchiveData = { var canister : ArchiveInterface; var stored_txs : Nat } 217 | ``` 218 | 219 | The details of the archive canister 220 | 221 | ## Type `TokenData` 222 | ``` motoko no-repl 223 | 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 } 224 | ``` 225 | 226 | The state of the token canister 227 | 228 | ## Type `GetTransactionsRequest` 229 | ``` motoko no-repl 230 | type GetTransactionsRequest = { start : TxIndex; length : Nat } 231 | ``` 232 | 233 | The type to request a range of transactions from the ledger canister 234 | 235 | ## Type `TransactionRange` 236 | ``` motoko no-repl 237 | type TransactionRange = { transactions : [Transaction] } 238 | ``` 239 | 240 | 241 | ## Type `QueryArchiveFn` 242 | ``` motoko no-repl 243 | type QueryArchiveFn = shared query (GetTransactionsRequest) -> async TransactionRange 244 | ``` 245 | 246 | 247 | ## Type `ArchivedTransaction` 248 | ``` motoko no-repl 249 | type ArchivedTransaction = { start : TxIndex; length : Nat; callback : QueryArchiveFn } 250 | ``` 251 | 252 | 253 | ## Type `GetTransactionsResponse` 254 | ``` motoko no-repl 255 | type GetTransactionsResponse = { log_length : Nat; first_index : TxIndex; transactions : [Transaction]; archived_transactions : [ArchivedTransaction] } 256 | ``` 257 | 258 | 259 | ## Type `RosettaInterface` 260 | ``` motoko no-repl 261 | type RosettaInterface = actor { get_transactions : shared query (GetTransactionsRequest) -> async GetTransactionsResponse } 262 | ``` 263 | 264 | Functions supported by the rosetta 265 | 266 | ## Type `FullInterface` 267 | ``` motoko no-repl 268 | type FullInterface = TokenInterface and RosettaInterface 269 | ``` 270 | 271 | Interface of the ICRC token and Rosetta canister 272 | -------------------------------------------------------------------------------- /docs/ICRC1/Utils.html: -------------------------------------------------------------------------------- 1 | 2 |

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

3 |

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

Updates the balance of an account

4 |

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 :

-------------------------------------------------------------------------------- /docs/ICRC1/Utils.md: -------------------------------------------------------------------------------- 1 | # ICRC1/Utils 2 | 3 | ## Function `init_metadata` 4 | ``` motoko no-repl 5 | func init_metadata(args : T.InitArgs) : StableBuffer.StableBuffer 6 | ``` 7 | 8 | 9 | ## Value `default_standard` 10 | ``` motoko no-repl 11 | let default_standard : T.SupportedStandard 12 | ``` 13 | 14 | 15 | ## Function `init_standards` 16 | ``` motoko no-repl 17 | func init_standards() : StableBuffer.StableBuffer 18 | ``` 19 | 20 | 21 | ## Function `default_subaccount` 22 | ``` motoko no-repl 23 | func default_subaccount() : T.Subaccount 24 | ``` 25 | 26 | 27 | ## Function `hash` 28 | ``` motoko no-repl 29 | func hash(n : Nat) : Hash.Hash 30 | ``` 31 | 32 | 33 | ## Function `create_transfer_req` 34 | ``` motoko no-repl 35 | func create_transfer_req(args : T.TransferArgs, owner : Principal, tx_kind : T.TxKind) : T.TransactionRequest 36 | ``` 37 | 38 | 39 | ## Function `kind_to_text` 40 | ``` motoko no-repl 41 | func kind_to_text(kind : T.TxKind) : Text 42 | ``` 43 | 44 | 45 | ## Function `req_to_tx` 46 | ``` motoko no-repl 47 | func req_to_tx(tx_req : T.TransactionRequest, index : Nat) : T.Transaction 48 | ``` 49 | 50 | 51 | ## Function `div_ceil` 52 | ``` motoko no-repl 53 | func div_ceil(n : Nat, d : Nat) : Nat 54 | ``` 55 | 56 | 57 | ## Function `get_balance` 58 | ``` motoko no-repl 59 | func get_balance(accounts : T.AccountBalances, encoded_account : T.EncodedAccount) : T.Balance 60 | ``` 61 | 62 | Retrieves the balance of an account 63 | 64 | ## Function `update_balance` 65 | ``` motoko no-repl 66 | func update_balance(accounts : T.AccountBalances, encoded_account : T.EncodedAccount, update : (T.Balance) -> T.Balance) 67 | ``` 68 | 69 | Updates the balance of an account 70 | 71 | ## Function `transfer_balance` 72 | ``` motoko no-repl 73 | func transfer_balance(token : T.TokenData, tx_req : T.TransactionRequest) 74 | ``` 75 | 76 | 77 | ## Function `mint_balance` 78 | ``` motoko no-repl 79 | func mint_balance(token : T.TokenData, encoded_account : T.EncodedAccount, amount : T.Balance) 80 | ``` 81 | 82 | 83 | ## Function `burn_balance` 84 | ``` motoko no-repl 85 | func burn_balance(token : T.TokenData, encoded_account : T.EncodedAccount, amount : T.Balance) 86 | ``` 87 | 88 | 89 | ## Value `SB` 90 | ``` motoko no-repl 91 | let SB 92 | ``` 93 | 94 | -------------------------------------------------------------------------------- /docs/ICRC1/lib.html: -------------------------------------------------------------------------------- 1 | 2 |

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

3 |

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

Retrieve the name of the token

4 |

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

Retrieve the symbol of the token

5 |

public func decimals() : Nat8

Retrieve the number of decimals specified for the token

6 |

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

Retrieve the fee for each transfer

7 |

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

Set the fee for each transfer

8 |

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

Retrieve all the metadata of the token

9 |

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

Returns the total supply of circulating tokens

10 |

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

Returns the total supply of minted tokens

11 |

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

Returns the total supply of burned tokens

12 |

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

Returns the maximum supply of tokens

13 |

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

Returns the account with the permission to mint tokens

14 |

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

17 |

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

Retrieve the balance of a given account

18 |

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

Returns an array of standards supported by this token

19 |

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

20 |

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)

21 |

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

Helper function to mint tokens with minimum args

22 |

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

Helper function to burn tokens with minimum args

23 |

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

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

24 |

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

Retrieves the transaction specified by the given tx_index

25 |

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

Retrieves the transactions specified by the given range

26 |

-------------------------------------------------------------------------------- /docs/ICRC1/lib.md: -------------------------------------------------------------------------------- 1 | # ICRC1/lib 2 | 3 | ## Type `Account` 4 | ``` motoko no-repl 5 | type Account = T.Account 6 | ``` 7 | 8 | 9 | ## Type `Subaccount` 10 | ``` motoko no-repl 11 | type Subaccount = T.Subaccount 12 | ``` 13 | 14 | 15 | ## Type `AccountBalances` 16 | ``` motoko no-repl 17 | type AccountBalances = T.AccountBalances 18 | ``` 19 | 20 | 21 | ## Type `Transaction` 22 | ``` motoko no-repl 23 | type Transaction = T.Transaction 24 | ``` 25 | 26 | 27 | ## Type `Balance` 28 | ``` motoko no-repl 29 | type Balance = T.Balance 30 | ``` 31 | 32 | 33 | ## Type `TransferArgs` 34 | ``` motoko no-repl 35 | type TransferArgs = T.TransferArgs 36 | ``` 37 | 38 | 39 | ## Type `Mint` 40 | ``` motoko no-repl 41 | type Mint = T.Mint 42 | ``` 43 | 44 | 45 | ## Type `BurnArgs` 46 | ``` motoko no-repl 47 | type BurnArgs = T.BurnArgs 48 | ``` 49 | 50 | 51 | ## Type `TransactionRequest` 52 | ``` motoko no-repl 53 | type TransactionRequest = T.TransactionRequest 54 | ``` 55 | 56 | 57 | ## Type `TransferError` 58 | ``` motoko no-repl 59 | type TransferError = T.TransferError 60 | ``` 61 | 62 | 63 | ## Type `SupportedStandard` 64 | ``` motoko no-repl 65 | type SupportedStandard = T.SupportedStandard 66 | ``` 67 | 68 | 69 | ## Type `InitArgs` 70 | ``` motoko no-repl 71 | type InitArgs = T.InitArgs 72 | ``` 73 | 74 | 75 | ## Type `TokenInitArgs` 76 | ``` motoko no-repl 77 | type TokenInitArgs = T.TokenInitArgs 78 | ``` 79 | 80 | 81 | ## Type `TokenData` 82 | ``` motoko no-repl 83 | type TokenData = T.TokenData 84 | ``` 85 | 86 | 87 | ## Type `MetaDatum` 88 | ``` motoko no-repl 89 | type MetaDatum = T.MetaDatum 90 | ``` 91 | 92 | 93 | ## Type `TxLog` 94 | ``` motoko no-repl 95 | type TxLog = T.TxLog 96 | ``` 97 | 98 | 99 | ## Type `TxIndex` 100 | ``` motoko no-repl 101 | type TxIndex = T.TxIndex 102 | ``` 103 | 104 | 105 | ## Type `TokenInterface` 106 | ``` motoko no-repl 107 | type TokenInterface = T.TokenInterface 108 | ``` 109 | 110 | 111 | ## Type `RosettaInterface` 112 | ``` motoko no-repl 113 | type RosettaInterface = T.RosettaInterface 114 | ``` 115 | 116 | 117 | ## Type `FullInterface` 118 | ``` motoko no-repl 119 | type FullInterface = T.FullInterface 120 | ``` 121 | 122 | 123 | ## Type `ArchiveInterface` 124 | ``` motoko no-repl 125 | type ArchiveInterface = T.ArchiveInterface 126 | ``` 127 | 128 | 129 | ## Type `GetTransactionsRequest` 130 | ``` motoko no-repl 131 | type GetTransactionsRequest = T.GetTransactionsRequest 132 | ``` 133 | 134 | 135 | ## Type `GetTransactionsResponse` 136 | ``` motoko no-repl 137 | type GetTransactionsResponse = T.GetTransactionsResponse 138 | ``` 139 | 140 | 141 | ## Type `QueryArchiveFn` 142 | ``` motoko no-repl 143 | type QueryArchiveFn = T.QueryArchiveFn 144 | ``` 145 | 146 | 147 | ## Type `TransactionRange` 148 | ``` motoko no-repl 149 | type TransactionRange = T.TransactionRange 150 | ``` 151 | 152 | 153 | ## Type `ArchivedTransaction` 154 | ``` motoko no-repl 155 | type ArchivedTransaction = T.ArchivedTransaction 156 | ``` 157 | 158 | 159 | ## Type `TransferResult` 160 | ``` motoko no-repl 161 | type TransferResult = T.TransferResult 162 | ``` 163 | 164 | 165 | ## Value `MAX_TRANSACTIONS_IN_LEDGER` 166 | ``` motoko no-repl 167 | let MAX_TRANSACTIONS_IN_LEDGER 168 | ``` 169 | 170 | 171 | ## Value `MAX_TRANSACTION_BYTES` 172 | ``` motoko no-repl 173 | let MAX_TRANSACTION_BYTES : Nat64 174 | ``` 175 | 176 | 177 | ## Value `MAX_TRANSACTIONS_PER_REQUEST` 178 | ``` motoko no-repl 179 | let MAX_TRANSACTIONS_PER_REQUEST 180 | ``` 181 | 182 | 183 | ## Function `init` 184 | ``` motoko no-repl 185 | func init(args : T.InitArgs) : T.TokenData 186 | ``` 187 | 188 | Initialize a new ICRC-1 token 189 | 190 | ## Function `name` 191 | ``` motoko no-repl 192 | func name(token : T.TokenData) : Text 193 | ``` 194 | 195 | Retrieve the name of the token 196 | 197 | ## Function `symbol` 198 | ``` motoko no-repl 199 | func symbol(token : T.TokenData) : Text 200 | ``` 201 | 202 | Retrieve the symbol of the token 203 | 204 | ## Function `decimals` 205 | ``` motoko no-repl 206 | func decimals() : Nat8 207 | ``` 208 | 209 | Retrieve the number of decimals specified for the token 210 | 211 | ## Function `fee` 212 | ``` motoko no-repl 213 | func fee(token : T.TokenData) : T.Balance 214 | ``` 215 | 216 | Retrieve the fee for each transfer 217 | 218 | ## Function `set_fee` 219 | ``` motoko no-repl 220 | func set_fee(token : T.TokenData, fee : Nat) 221 | ``` 222 | 223 | Set the fee for each transfer 224 | 225 | ## Function `metadata` 226 | ``` motoko no-repl 227 | func metadata(token : T.TokenData) : [T.MetaDatum] 228 | ``` 229 | 230 | Retrieve all the metadata of the token 231 | 232 | ## Function `total_supply` 233 | ``` motoko no-repl 234 | func total_supply(token : T.TokenData) : T.Balance 235 | ``` 236 | 237 | Returns the total supply of circulating tokens 238 | 239 | ## Function `minted_supply` 240 | ``` motoko no-repl 241 | func minted_supply(token : T.TokenData) : T.Balance 242 | ``` 243 | 244 | Returns the total supply of minted tokens 245 | 246 | ## Function `burned_supply` 247 | ``` motoko no-repl 248 | func burned_supply(token : T.TokenData) : T.Balance 249 | ``` 250 | 251 | Returns the total supply of burned tokens 252 | 253 | ## Function `max_supply` 254 | ``` motoko no-repl 255 | func max_supply(token : T.TokenData) : T.Balance 256 | ``` 257 | 258 | Returns the maximum supply of tokens 259 | 260 | ## Function `minting_account` 261 | ``` motoko no-repl 262 | func minting_account(token : T.TokenData) : T.Account 263 | ``` 264 | 265 | Returns the account with the permission to mint tokens 266 | 267 | Note: **The minting account can only participate in minting 268 | and burning transactions, so any tokens sent to it will be 269 | considered burned.** 270 | 271 | ## Function `balance_of` 272 | ``` motoko no-repl 273 | func balance_of(account : T.Account) : T.Balance 274 | ``` 275 | 276 | Retrieve the balance of a given account 277 | 278 | ## Function `supported_standards` 279 | ``` motoko no-repl 280 | func supported_standards(token : T.TokenData) : [T.SupportedStandard] 281 | ``` 282 | 283 | Returns an array of standards supported by this token 284 | 285 | ## Function `balance_from_float` 286 | ``` motoko no-repl 287 | func balance_from_float(token : T.TokenData, float : Float) : T.Balance 288 | ``` 289 | 290 | Formats a float to a nat balance and applies the correct number of decimal places 291 | 292 | ## Function `transfer` 293 | ``` motoko no-repl 294 | func transfer(token : T.TokenData, args : T.TransferArgs, caller : Principal) : async T.TransferResult 295 | ``` 296 | 297 | Transfers tokens from one account to another account (minting and burning included) 298 | 299 | ## Function `mint` 300 | ``` motoko no-repl 301 | func mint(token : T.TokenData, args : T.Mint, caller : Principal) : async T.TransferResult 302 | ``` 303 | 304 | Helper function to mint tokens with minimum args 305 | 306 | ## Function `burn` 307 | ``` motoko no-repl 308 | func burn(token : T.TokenData, args : T.BurnArgs, caller : Principal) : async T.TransferResult 309 | ``` 310 | 311 | Helper function to burn tokens with minimum args 312 | 313 | ## Function `total_transactions` 314 | ``` motoko no-repl 315 | func total_transactions(token : T.TokenData) : Nat 316 | ``` 317 | 318 | Returns the total number of transactions that have been processed by the given token. 319 | 320 | ## Function `get_transaction` 321 | ``` motoko no-repl 322 | func get_transaction(token : T.TokenData, tx_index : T.TxIndex) : async ?T.Transaction 323 | ``` 324 | 325 | Retrieves the transaction specified by the given `tx_index` 326 | 327 | ## Function `get_transactions` 328 | ``` motoko no-repl 329 | func get_transactions(token : T.TokenData, req : T.GetTransactionsRequest) : T.GetTransactionsResponse 330 | ``` 331 | 332 | Retrieves the transactions specified by the given range 333 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Index 2 | 3 | * [ICRC1/Account](ICRC1/Account.md) 4 | * [ICRC1/Transfer](ICRC1/Transfer.md) 5 | * [ICRC1/Types](ICRC1/Types.md) 6 | * [ICRC1/Utils](ICRC1/Utils.md) 7 | * [ICRC1/lib](ICRC1/lib.md) 8 | -------------------------------------------------------------------------------- /docs/styles.css: -------------------------------------------------------------------------------- 1 | 2 | * { 3 | box-sizing: border-box; 4 | } 5 | 6 | body { 7 | background: #fff; 8 | color: #222; 9 | font-family: Circular Std, sans-serif; 10 | line-height: 1.15; 11 | -webkit-font-smoothing: antialiased; 12 | margin: 0; 13 | font-size: 1.0625rem; 14 | } 15 | 16 | .keyword { 17 | color: #264059; 18 | } 19 | 20 | .type { 21 | color: #ad448e; 22 | } 23 | 24 | .parameter { 25 | color: #264059; 26 | } 27 | 28 | .classname { 29 | color: #2c8093; 30 | } 31 | 32 | .fnname { 33 | color: #9a6e31; 34 | } 35 | 36 | .sidebar { 37 | width: 200px; 38 | position: fixed; 39 | left: 0; 40 | top: 0; 41 | bottom: 0; 42 | overflow: auto; 43 | 44 | background-color: #F1F1F1; 45 | } 46 | 47 | .documentation { 48 | margin-left: 230px; 49 | max-width: 960px; 50 | } 51 | 52 | .sidebar > ul { 53 | margin: 0 10px; 54 | padding: 0; 55 | list-style: none; 56 | } 57 | 58 | .sidebar a { 59 | display: block; 60 | text-overflow: ellipsis; 61 | overflow: hidden; 62 | line-height: 15px; 63 | padding: 7px 5px; 64 | font-size: 14px; 65 | font-weight: 400; 66 | transition: border 500ms ease-out; 67 | color: #000; 68 | text-decoration: none; 69 | } 70 | 71 | .sidebar h3 { 72 | border-bottom: 1px #dddddd solid; 73 | font-weight: 500; 74 | margin: 20px 0 15px 0; 75 | padding-bottom: 6px; 76 | text-align: center; 77 | } 78 | 79 | .declaration { 80 | border-bottom: 1px solid #f0f0f0; 81 | } 82 | .declaration:last-of-type { 83 | border-bottom: none; 84 | } 85 | 86 | .declaration :last-child { 87 | border: none; 88 | } 89 | 90 | h1 { 91 | font-weight: 500; 92 | padding-bottom: 6px; 93 | border-bottom: 1px #D5D5D5 dashed; 94 | } 95 | 96 | h4.function-declaration { 97 | font-weight: 600; 98 | margin-top: 16px; 99 | } 100 | 101 | h4.value-declaration { 102 | font-weight: 600; 103 | margin-top: 16px; 104 | } 105 | 106 | h4.type-declaration { 107 | font-weight: 600; 108 | margin-top: 16px; 109 | } 110 | 111 | h4.class-declaration { 112 | font-weight: 600; 113 | margin-top: 16px; 114 | } 115 | 116 | .class-declaration ~ div { 117 | margin-left: 20px; 118 | } 119 | 120 | .index-container { 121 | display: flex; 122 | flex-direction: column; 123 | align-items: center; 124 | font-size: 1.2rem; 125 | } 126 | 127 | .index-header { 128 | font-weight: 400; 129 | } 130 | 131 | .index-listing { 132 | padding: 0; 133 | list-style: none; 134 | max-width: 960px; 135 | } 136 | 137 | .index-item { 138 | margin-bottom: 5px; 139 | } 140 | 141 | .index-item-link { 142 | text-decoration: none; 143 | font-weight: 400; 144 | } 145 | 146 | .index-item-link::after { 147 | content: " \2014\00A0"; 148 | } 149 | 150 | .index-item-comment { 151 | display: inline; 152 | } 153 | 154 | .index-item-comment > * { 155 | display: none; 156 | } 157 | 158 | .index-item-comment > *:first-child { 159 | display: inline; 160 | white-space: nowrap; 161 | } 162 | 163 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Various IDEs and Editors 2 | .vscode/ 3 | .idea/ 4 | **/*~ 5 | 6 | # Mac OSX temporary files 7 | .DS_Store 8 | **/.DS_Store 9 | 10 | # dfx temporary files 11 | .dfx/ 12 | 13 | # frontend code 14 | node_modules/ 15 | dist/ 16 | -------------------------------------------------------------------------------- /example/dfx.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dfx": "0.11.2", 4 | "canisters": { 5 | "icrc1_example": { 6 | "type": "motoko", 7 | "main": "src/icrc1/main.mo" 8 | } 9 | }, 10 | "defaults": { 11 | "build": { 12 | "packtool": "", 13 | "args": "" 14 | } 15 | }, 16 | 17 | "networks": { 18 | "local": { 19 | "bind": "127.0.0.1:8000", 20 | "type": "ephemeral" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/icrc1/main.mo: -------------------------------------------------------------------------------- 1 | import Iter "mo:base/Iter"; 2 | import Option "mo:base/Option"; 3 | import Time "mo:base/Time"; 4 | 5 | import ExperimentalCycles "mo:base/ExperimentalCycles"; 6 | 7 | import ICRC1 "../../src/ICRC1"; // replace with "mo:icrc1/ICRC1" 8 | import Array "mo:base/Array"; 9 | 10 | shared ({ caller = _owner }) actor class Token( 11 | token_args : ICRC1.TokenInitArgs, 12 | ) : async ICRC1.FullInterface { 13 | 14 | stable let token = ICRC1.init({ 15 | token_args with minting_account = Option.get( 16 | token_args.minting_account, 17 | { 18 | owner = _owner; 19 | subaccount = null; 20 | }, 21 | ); 22 | }); 23 | 24 | /// Functions for the ICRC1 token standard 25 | public shared query func icrc1_name() : async Text { 26 | ICRC1.name(token); 27 | }; 28 | 29 | public shared query func icrc1_symbol() : async Text { 30 | ICRC1.symbol(token); 31 | }; 32 | 33 | public shared query func icrc1_decimals() : async Nat8 { 34 | ICRC1.decimals(token); 35 | }; 36 | 37 | public shared query func icrc1_fee() : async ICRC1.Balance { 38 | ICRC1.fee(token); 39 | }; 40 | 41 | public shared query func icrc1_metadata() : async [ICRC1.MetaDatum] { 42 | ICRC1.metadata(token); 43 | }; 44 | 45 | public shared query func icrc1_total_supply() : async ICRC1.Balance { 46 | ICRC1.total_supply(token); 47 | }; 48 | 49 | public shared query func icrc1_minting_account() : async ?ICRC1.Account { 50 | ?ICRC1.minting_account(token); 51 | }; 52 | 53 | public shared query func icrc1_balance_of(args : ICRC1.Account) : async ICRC1.Balance { 54 | ICRC1.balance_of(token, args); 55 | }; 56 | 57 | public shared query func icrc1_supported_standards() : async [ICRC1.SupportedStandard] { 58 | ICRC1.supported_standards(token); 59 | }; 60 | 61 | public shared ({ caller }) func icrc1_transfer(args : ICRC1.TransferArgs) : async ICRC1.TransferResult { 62 | await ICRC1.transfer(token, args, caller); 63 | }; 64 | 65 | public shared ({ caller }) func mint(args : ICRC1.Mint) : async ICRC1.TransferResult { 66 | await ICRC1.mint(token, args, caller); 67 | }; 68 | 69 | public shared ({ caller }) func burn(args : ICRC1.BurnArgs) : async ICRC1.TransferResult { 70 | await ICRC1.burn(token, args, caller); 71 | }; 72 | 73 | // Functions from the rosetta icrc1 ledger 74 | public shared query func get_transactions(req : ICRC1.GetTransactionsRequest) : async ICRC1.GetTransactionsResponse { 75 | ICRC1.get_transactions(token, req); 76 | }; 77 | 78 | // Additional functions not included in the ICRC1 standard 79 | public shared func get_transaction(i : ICRC1.TxIndex) : async ?ICRC1.Transaction { 80 | await ICRC1.get_transaction(token, i); 81 | }; 82 | 83 | // Deposit cycles into this archive canister. 84 | public shared func deposit_cycles() : async () { 85 | let amount = ExperimentalCycles.available(); 86 | let accepted = ExperimentalCycles.accept(amount); 87 | assert (accepted == amount); 88 | }; 89 | }; 90 | -------------------------------------------------------------------------------- /icrc1-default-args.txt: -------------------------------------------------------------------------------- 1 | ( record { 2 | name = ""; 3 | symbol = ""; 4 | decimals = 3; 5 | fee = 1_000; 6 | max_supply = 1_000_000_000; 7 | initial_balances = vec { 8 | record { 9 | record { 10 | owner = principal "r7inp-6aaaa-aaaaa-aaabq-cai"; 11 | subaccount = null; 12 | }; 13 | 100_000_000 14 | } 15 | }; 16 | min_burn_amount = 10_000; 17 | minting_account = null; 18 | advanced_settings = null; 19 | }) -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test docs actor-test 2 | 3 | dfx-cache-install: 4 | dfx cache install 5 | 6 | test: dfx-cache-install 7 | $(shell mocv bin current)/moc -r $(shell mops sources) -wasi-system-api ./tests/**/**.Test.mo 8 | 9 | no-warn: dfx-cache-install 10 | find src -type f -name '*.mo' -print0 | xargs -0 $(shell mocv bin current)/moc -r $(shell mops sources) -Werror -wasi-system-api 11 | 12 | docs: 13 | $(shell mocv bin current)/mo-doc 14 | $(shell mocv bin current)/mo-doc --format plain 15 | 16 | actor-test: dfx-cache-install 17 | -dfx start --background 18 | dfx deploy test 19 | dfx ledger fabricate-cycles --canister test 20 | dfx canister call test run_tests 21 | 22 | ref-test: 23 | -dfx start --background --clean 24 | IDENTITY=$$(dfx identity whoami); \ 25 | echo $$IDENTITY; \ 26 | cat icrc1-default-args.txt | xargs -0 dfx deploy icrc1 --identity $$IDENTITY --no-wallet --argument ; \ 27 | CANISTER=$$(dfx canister id icrc1); \ 28 | cd Dfnity-ICRC1-Reference && cargo run --bin runner -- -u http://127.0.0.1:4943 -c $$CANISTER -s ~/.config/dfx/identity/$$IDENTITY/identity.pem -------------------------------------------------------------------------------- /mops.toml: -------------------------------------------------------------------------------- 1 | [dependencies] 2 | base = "0.10.2" 3 | array = "https://github.com/aviate-labs/array.mo#v0.2.0" 4 | StableTrieMap = "https://github.com/NatLabs/StableTrieMap#main" 5 | StableBuffer = "https://github.com/canscale/StableBuffer#v0.2.0" 6 | itertools = "0.1.2" 7 | 8 | [package] 9 | name = "icrc1" 10 | version = "0.0.1" 11 | description = "A full implementation of the ICRC-1 fungible token standard" 12 | repository = "https://github.com/NatLabs/icrc1" 13 | keywords = [ "icrc1", "fungible", "token", "standard", "dfinity" ] 14 | -------------------------------------------------------------------------------- /package-set.dhall: -------------------------------------------------------------------------------- 1 | let aviate_labs = https://github.com/aviate-labs/package-set/releases/download/v0.1.4/package-set.dhall sha256:30b7e5372284933c7394bad62ad742fec4cb09f605ce3c178d892c25a1a9722e 2 | let vessel_package_set = 3 | https://github.com/dfinity/vessel-package-set/releases/download/mo-0.6.20-20220131/package-set.dhall 4 | 5 | let Package = 6 | { name : Text, version : Text, repo : Text, dependencies : List Text } 7 | 8 | let 9 | -- This is where you can add your own packages to the package-set 10 | additions = 11 | [] : List Package 12 | 13 | let overrides = [ 14 | { 15 | name = "StableTrieMap", 16 | version = "main", 17 | repo = "https://github.com/NatLabs/StableTrieMap", 18 | dependencies = ["base"] : List Text 19 | }, 20 | { 21 | name = "StableBuffer", 22 | version = "v0.2.0", 23 | repo = "https://github.com/canscale/StableBuffer", 24 | dependencies = ["base"] : List Text 25 | }, 26 | { 27 | name = "itertools", 28 | version = "main", 29 | repo = "https://github.com/NatLabs/Itertools.mo", 30 | dependencies = ["base"] : List Text 31 | }, 32 | { 33 | name = "base", 34 | version = "moc-0.7.4", 35 | repo = "https://github.com/dfinity/motoko-base", 36 | dependencies = ["base"] : List Text 37 | }, 38 | ] : List Package 39 | 40 | in aviate_labs # vessel_package_set # overrides 41 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # ICRC-1 Implementation 2 | This repo contains the implementation of the 3 | [ICRC-1](https://github.com/dfinity/ICRC-1) token standard. 4 | 5 | ## References and other implementations 6 | - [demergent-labs/ICRC-1 (Typescript)](https://github.com/demergent-labs/ICRC-1) 7 | - [Ledger ref in Motoko](https://github.com/dfinity/ledger-ref/blob/main/src/Ledger.mo) 8 | - [ICRC1 Rosetta API](https://github.com/dfinity/ic/blob/master/rs/rosetta-api/icrc1/ledger) 9 | 10 | ## Documentation 11 | - [markdown](https://github.com/NatLabs/icrc1/blob/main/docs/ICRC1/lib.md#function-init) 12 | - [web](https://natlabs.github.io/icrc1/ICRC1/lib.html#init) 13 | 14 | ## Getting Started 15 | - Expose the ICRC-1 token functions from your canister 16 | - Import the `icrc1` lib and expose them in an `actor` class. 17 | 18 | Take a look at the [examples](./example/icrc1/main.mo) 19 | 20 | - Launch the basic token with all the standard functions for ICRC-1 21 | - Install the [mops](https://j4mwm-bqaaa-aaaam-qajbq-cai.ic0.app/#/docs/install) package manager 22 | - Replace the values enclosed in `< >` with your desired values and run in the terminal 23 | 24 | ```motoko 25 | git clone https://github.com/NatLabs/icrc1 26 | cd icrc1 27 | mops install 28 | dfx start --background --clean 29 | 30 | dfx deploy icrc1 --argument '( record { 31 | name = ""; 32 | symbol = ""; 33 | decimals = 6; 34 | fee = 1_000_000; 35 | max_supply = 1_000_000_000_000; 36 | initial_balances = vec { 37 | record { 38 | record { 39 | owner = principal ""; 40 | subaccount = null; 41 | }; 42 | 100_000_000 43 | } 44 | }; 45 | min_burn_amount = 10_000; 46 | minting_account = null; 47 | advanced_settings = null; 48 | })' 49 | ``` 50 | 51 | - Create a token dynamically from a canister 52 | ```motoko 53 | import Nat8 "mo:base/Nat8"; 54 | import Token "mo:icrc1/ICRC1/Canisters/Token"; 55 | 56 | actor{ 57 | let decimals = 8; // replace with your chosen number of decimals 58 | 59 | func add_decimals(n: Nat): Nat{ 60 | n * 10 ** decimals 61 | }; 62 | 63 | let pre_mint_account = { 64 | owner = Principal.fromText(""); 65 | subaccount = null; 66 | }; 67 | 68 | let token_canister = Token.Token({ 69 | name = ""; 70 | symbol = ""; 71 | decimals = Nat8.fromNat(decimals); 72 | fee = add_decimals(1); 73 | max_supply = add_decimals(1_000_000); 74 | 75 | // pre-mint 100,000 tokens for the account 76 | initial_balances = [(pre_mint_account, add_decimals(100_000))]; 77 | 78 | min_burn_amount = add_decimals(10); 79 | minting_account = null; // defaults to the canister id of the caller 80 | advanced_settings = null; 81 | }); 82 | } 83 | ``` 84 | 85 | > The fields for the `advanced_settings` record are documented [here](./docs/ICRC1/Types.md#type-advancedsettings) 86 | 87 | ## Tests 88 | #### Internal Tests 89 | - Download and Install [vessel](https://github.com/dfinity/vessel) 90 | - Run `make test` 91 | - Run `make actor-test` 92 | 93 | #### [Dfinity's ICRC-1 Reference Tests](https://github.com/dfinity/ICRC-1/tree/main/test) 94 | - Install Rust and Cargo via [rustup](https://rustup.rs/) 95 | 96 | ``` 97 | curl https://sh.rustup.rs -sSf | sh 98 | ``` 99 | - Then run the `ref-test` command 100 | 101 | ``` 102 | make ref-test 103 | ``` 104 | 105 | ## Funding 106 | 107 | 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. -------------------------------------------------------------------------------- /src/ICRC1/Account.mo: -------------------------------------------------------------------------------- 1 | import Array "mo:base/Array"; 2 | import Blob "mo:base/Blob"; 3 | import Char "mo:base/Char"; 4 | import Debug "mo:base/Debug"; 5 | import Int "mo:base/Int"; 6 | import Iter "mo:base/Iter"; 7 | import Nat "mo:base/Nat"; 8 | import Nat8 "mo:base/Nat8"; 9 | import Nat32 "mo:base/Nat32"; 10 | import Nat64 "mo:base/Nat64"; 11 | import Option "mo:base/Option"; 12 | import Principal "mo:base/Principal"; 13 | import Result "mo:base/Result"; 14 | import Text "mo:base/Text"; 15 | import Time "mo:base/Time"; 16 | 17 | import ArrayModule "mo:array/Array"; 18 | import Itertools "mo:itertools/Iter"; 19 | import StableBuffer "mo:StableBuffer/StableBuffer"; 20 | import STMap "mo:StableTrieMap"; 21 | 22 | import T "Types"; 23 | 24 | module { 25 | type Iter = Iter.Iter; 26 | 27 | /// Checks if a subaccount is valid 28 | public func validate_subaccount(subaccount : ?T.Subaccount) : Bool { 29 | switch (subaccount) { 30 | case (?bytes) { 31 | bytes.size() == 32; 32 | }; 33 | case (_) true; 34 | }; 35 | }; 36 | 37 | /// Checks if an account is valid 38 | public func validate(account : T.Account) : Bool { 39 | let is_anonymous = Principal.isAnonymous(account.owner); 40 | let invalid_size = Principal.toBlob(account.owner).size() > 29; 41 | 42 | if (is_anonymous or invalid_size) { 43 | false; 44 | } else { 45 | validate_subaccount(account.subaccount); 46 | }; 47 | }; 48 | 49 | func shrink_subaccount(sub : Blob) : (Iter.Iter, Nat8) { 50 | let bytes = Blob.toArray(sub); 51 | var size = Nat8.fromNat(bytes.size()); 52 | 53 | let iter = Itertools.skipWhile( 54 | bytes.vals(), 55 | func(byte : Nat8) : Bool { 56 | if (byte == 0x00) { 57 | size -= 1; 58 | return true; 59 | }; 60 | 61 | false; 62 | }, 63 | ); 64 | 65 | (iter, size); 66 | }; 67 | 68 | func encode_subaccount(sub : Blob) : Iter.Iter { 69 | 70 | let (sub_iter, size) = shrink_subaccount(sub); 71 | if (size == 0) { 72 | return Itertools.empty(); 73 | }; 74 | 75 | let suffix : [Nat8] = [size, 0x7f]; 76 | 77 | Itertools.chain( 78 | sub_iter, 79 | suffix.vals(), 80 | ); 81 | }; 82 | 83 | /// Implementation of ICRC1's Textual representation of accounts [Encoding Standard](https://github.com/dfinity/ICRC-1/tree/main/standards/ICRC-1#encoding) 84 | public func encode({ owner; subaccount } : T.Account) : T.EncodedAccount { 85 | let owner_blob = Principal.toBlob(owner); 86 | 87 | switch (subaccount) { 88 | case (?subaccount) { 89 | Blob.fromArray( 90 | Iter.toArray( 91 | Itertools.chain( 92 | owner_blob.vals(), 93 | encode_subaccount(subaccount), 94 | ), 95 | ), 96 | ); 97 | }; 98 | case (_) { 99 | owner_blob; 100 | }; 101 | }; 102 | }; 103 | 104 | /// Implementation of ICRC1's Textual representation of accounts [Decoding Standard](https://github.com/dfinity/ICRC-1/tree/main/standards/ICRC-1#decoding) 105 | public func decode(encoded : T.EncodedAccount) : ?T.Account { 106 | let bytes = Blob.toArray(encoded); 107 | var size = bytes.size(); 108 | 109 | if (bytes[size - 1] == 0x7f) { 110 | size -= 1; 111 | 112 | let subaccount_size = Nat8.toNat(bytes[size - 1]); 113 | 114 | if (subaccount_size == 0 or subaccount_size > 32) { 115 | return null; 116 | }; 117 | 118 | size -= 1; 119 | let split_index = (size - subaccount_size) : Nat; 120 | 121 | if (bytes[split_index] == 0) { 122 | return null; 123 | }; 124 | 125 | let principal = Principal.fromBlob( 126 | Blob.fromArray( 127 | ArrayModule.slice(bytes, 0, split_index), 128 | ), 129 | ); 130 | 131 | let prefix_zeroes = Itertools.take( 132 | Iter.make(0 : Nat8), 133 | (32 - subaccount_size) : Nat, 134 | ); 135 | 136 | let encoded_subaccount = Itertools.fromArraySlice(bytes, split_index, size); 137 | 138 | let subaccount = Blob.fromArray( 139 | Iter.toArray( 140 | Itertools.chain(prefix_zeroes, encoded_subaccount), 141 | ), 142 | ); 143 | 144 | ?{ owner = principal; subaccount = ?subaccount }; 145 | } else { 146 | ?{ 147 | owner = Principal.fromBlob(encoded); 148 | subaccount = null; 149 | }; 150 | }; 151 | }; 152 | 153 | /// Converts an ICRC-1 Account from its Textual representation to the `Account` type 154 | /// @deprecated - Use the account module instead - https://github.com/letmejustputthishere/account 155 | public func fromText(encoded : Text) : ?T.Account = null; 156 | 157 | /// Converts an ICRC-1 `Account` to its Textual representation 158 | /// @deprecated - Use the account module instead - https://github.com/letmejustputthishere/account 159 | public func toText(account : T.Account) : Text = ""; 160 | 161 | }; 162 | -------------------------------------------------------------------------------- /src/ICRC1/Canisters/Archive.mo: -------------------------------------------------------------------------------- 1 | import Prim "mo:prim"; 2 | 3 | import Array "mo:base/Array"; 4 | import Blob "mo:base/Blob"; 5 | import Debug "mo:base/Debug"; 6 | import Iter "mo:base/Iter"; 7 | import Nat "mo:base/Nat"; 8 | import Nat64 "mo:base/Nat64"; 9 | import Hash "mo:base/Hash"; 10 | import Result "mo:base/Result"; 11 | 12 | import ExperimentalCycles "mo:base/ExperimentalCycles"; 13 | import ExperimentalStableMemory "mo:base/ExperimentalStableMemory"; 14 | 15 | import Itertools "mo:itertools/Iter"; 16 | import StableTrieMap "mo:StableTrieMap"; 17 | import U "../Utils"; 18 | import T "../Types"; 19 | 20 | shared ({ caller = ledger_canister_id }) actor class Archive() : async T.ArchiveInterface { 21 | 22 | type Transaction = T.Transaction; 23 | type MemoryBlock = { 24 | offset : Nat64; 25 | size : Nat; 26 | }; 27 | 28 | stable let KiB = 1024; 29 | stable let GiB = KiB ** 3; 30 | stable let MEMORY_PER_PAGE : Nat64 = Nat64.fromNat(64 * KiB); 31 | stable let MIN_PAGES : Nat64 = 32; // 2MiB == 32 * 64KiB 32 | stable var PAGES_TO_GROW : Nat64 = 2048; // 64MiB 33 | stable let MAX_MEMORY = 32 * GiB; 34 | 35 | stable let BUCKET_SIZE = 1000; 36 | stable let MAX_TRANSACTIONS_PER_REQUEST = 5000; 37 | 38 | stable var memory_pages : Nat64 = ExperimentalStableMemory.size(); 39 | stable var total_memory_used : Nat64 = 0; 40 | 41 | stable var filled_buckets = 0; 42 | stable var trailing_txs = 0; 43 | 44 | stable let txStore = StableTrieMap.new(); 45 | 46 | public shared ({ caller }) func append_transactions(txs : [Transaction]) : async Result.Result<(), Text> { 47 | 48 | if (caller != ledger_canister_id) { 49 | return #err("Unauthorized Access: Only the ledger canister can access this archive canister"); 50 | }; 51 | 52 | var txs_iter = txs.vals(); 53 | 54 | if (trailing_txs > 0) { 55 | let last_bucket = StableTrieMap.get( 56 | txStore, 57 | Nat.equal, 58 | U.hash, 59 | filled_buckets, 60 | ); 61 | 62 | switch (last_bucket) { 63 | case (?last_bucket) { 64 | let new_bucket = Iter.toArray( 65 | Itertools.take( 66 | Itertools.chain( 67 | last_bucket.vals(), 68 | Iter.map(txs.vals(), store_tx), 69 | ), 70 | BUCKET_SIZE, 71 | ), 72 | ); 73 | 74 | if (new_bucket.size() == BUCKET_SIZE) { 75 | let offset = (BUCKET_SIZE - last_bucket.size()) : Nat; 76 | 77 | txs_iter := Itertools.fromArraySlice(txs, offset, txs.size()); 78 | } else { 79 | txs_iter := Itertools.empty(); 80 | }; 81 | 82 | store_bucket(new_bucket); 83 | }; 84 | case (_) {}; 85 | }; 86 | }; 87 | 88 | for (chunk in Itertools.chunks(txs_iter, BUCKET_SIZE)) { 89 | store_bucket(Array.map(chunk, store_tx)); 90 | }; 91 | 92 | #ok(); 93 | }; 94 | 95 | func total_txs() : Nat { 96 | (filled_buckets * BUCKET_SIZE) + trailing_txs; 97 | }; 98 | 99 | public shared query func total_transactions() : async Nat { 100 | total_txs(); 101 | }; 102 | 103 | public shared query func get_transaction(tx_index : T.TxIndex) : async ?Transaction { 104 | let bucket_key = tx_index / BUCKET_SIZE; 105 | 106 | let opt_bucket = StableTrieMap.get( 107 | txStore, 108 | Nat.equal, 109 | U.hash, 110 | bucket_key, 111 | ); 112 | 113 | switch (opt_bucket) { 114 | case (?bucket) { 115 | let i = tx_index % BUCKET_SIZE; 116 | if (i < bucket.size()) { 117 | ?get_tx(bucket[tx_index % BUCKET_SIZE]); 118 | } else { 119 | null; 120 | }; 121 | }; 122 | case (_) { 123 | null; 124 | }; 125 | }; 126 | }; 127 | 128 | public shared query func get_transactions(req : T.GetTransactionsRequest) : async T.TransactionRange { 129 | let { start; length } = req; 130 | var iter = Itertools.empty(); 131 | 132 | let end = start + length; 133 | let start_bucket = start / BUCKET_SIZE; 134 | let end_bucket = (Nat.min(end, total_txs()) / BUCKET_SIZE) + 1; 135 | 136 | label _loop for (i in Itertools.range(start_bucket, end_bucket)) { 137 | let opt_bucket = StableTrieMap.get( 138 | txStore, 139 | Nat.equal, 140 | U.hash, 141 | i, 142 | ); 143 | 144 | switch (opt_bucket) { 145 | case (?bucket) { 146 | if (i == start_bucket) { 147 | iter := Itertools.fromArraySlice(bucket, start % BUCKET_SIZE, Nat.min(bucket.size(), end)); 148 | } else if (i + 1 == end_bucket) { 149 | let bucket_iter = Itertools.fromArraySlice(bucket, 0, end % BUCKET_SIZE); 150 | iter := Itertools.chain(iter, bucket_iter); 151 | } else { 152 | iter := Itertools.chain(iter, bucket.vals()); 153 | }; 154 | }; 155 | case (_) { break _loop }; 156 | }; 157 | }; 158 | 159 | let transactions = Iter.toArray( 160 | Iter.map( 161 | Itertools.take(iter, MAX_TRANSACTIONS_PER_REQUEST), 162 | get_tx, 163 | ), 164 | ); 165 | 166 | { transactions }; 167 | }; 168 | 169 | public shared query func remaining_capacity() : async Nat { 170 | MAX_MEMORY - Nat64.toNat(total_memory_used); 171 | }; 172 | 173 | /// Deposit cycles into this archive canister. 174 | public shared func deposit_cycles() : async () { 175 | let amount = ExperimentalCycles.available(); 176 | let accepted = ExperimentalCycles.accept(amount); 177 | assert (accepted == amount); 178 | }; 179 | 180 | func to_blob(tx : Transaction) : Blob { 181 | to_candid (tx); 182 | }; 183 | 184 | func from_blob(tx : Blob) : Transaction { 185 | switch (from_candid (tx) : ?Transaction) { 186 | case (?tx) tx; 187 | case (_) Debug.trap("Could not decode tx blob"); 188 | }; 189 | }; 190 | 191 | func store_tx(tx : Transaction) : MemoryBlock { 192 | let blob = to_blob(tx); 193 | 194 | if ((memory_pages * MEMORY_PER_PAGE) - total_memory_used < (MIN_PAGES * MEMORY_PER_PAGE)) { 195 | ignore ExperimentalStableMemory.grow(PAGES_TO_GROW); 196 | memory_pages += PAGES_TO_GROW; 197 | }; 198 | 199 | let offset = total_memory_used; 200 | 201 | ExperimentalStableMemory.storeBlob( 202 | offset, 203 | blob, 204 | ); 205 | 206 | let mem_block = { 207 | offset; 208 | size = blob.size(); 209 | }; 210 | 211 | total_memory_used += Nat64.fromNat(blob.size()); 212 | mem_block; 213 | }; 214 | 215 | func get_tx({ offset; size } : MemoryBlock) : Transaction { 216 | let blob = ExperimentalStableMemory.loadBlob(offset, size); 217 | 218 | let tx = from_blob(blob); 219 | }; 220 | 221 | func store_bucket(bucket : [MemoryBlock]) { 222 | 223 | StableTrieMap.put( 224 | txStore, 225 | Nat.equal, 226 | U.hash, 227 | filled_buckets, 228 | bucket, 229 | ); 230 | 231 | if (bucket.size() == BUCKET_SIZE) { 232 | filled_buckets += 1; 233 | trailing_txs := 0; 234 | } else { 235 | trailing_txs := bucket.size(); 236 | }; 237 | }; 238 | }; 239 | -------------------------------------------------------------------------------- /src/ICRC1/Canisters/Token.mo: -------------------------------------------------------------------------------- 1 | import Array "mo:base/Array"; 2 | import Iter "mo:base/Iter"; 3 | import Option "mo:base/Option"; 4 | import Time "mo:base/Time"; 5 | 6 | import ExperimentalCycles "mo:base/ExperimentalCycles"; 7 | 8 | import SB "mo:StableBuffer/StableBuffer"; 9 | 10 | import ICRC1 ".."; 11 | import Archive "Archive"; 12 | 13 | shared ({ caller = _owner }) actor class Token( 14 | init_args : ICRC1.TokenInitArgs, 15 | ) : async ICRC1.FullInterface { 16 | 17 | let icrc1_args : ICRC1.InitArgs = { 18 | init_args with minting_account = Option.get( 19 | init_args.minting_account, 20 | { 21 | owner = _owner; 22 | subaccount = null; 23 | }, 24 | ); 25 | }; 26 | 27 | stable let token = ICRC1.init(icrc1_args); 28 | 29 | /// Functions for the ICRC1 token standard 30 | public shared query func icrc1_name() : async Text { 31 | ICRC1.name(token); 32 | }; 33 | 34 | public shared query func icrc1_symbol() : async Text { 35 | ICRC1.symbol(token); 36 | }; 37 | 38 | public shared query func icrc1_decimals() : async Nat8 { 39 | ICRC1.decimals(token); 40 | }; 41 | 42 | public shared query func icrc1_fee() : async ICRC1.Balance { 43 | ICRC1.fee(token); 44 | }; 45 | 46 | public shared query func icrc1_metadata() : async [ICRC1.MetaDatum] { 47 | ICRC1.metadata(token); 48 | }; 49 | 50 | public shared query func icrc1_total_supply() : async ICRC1.Balance { 51 | ICRC1.total_supply(token); 52 | }; 53 | 54 | public shared query func icrc1_minting_account() : async ?ICRC1.Account { 55 | ?ICRC1.minting_account(token); 56 | }; 57 | 58 | public shared query func icrc1_balance_of(args : ICRC1.Account) : async ICRC1.Balance { 59 | ICRC1.balance_of(token, args); 60 | }; 61 | 62 | public shared query func icrc1_supported_standards() : async [ICRC1.SupportedStandard] { 63 | ICRC1.supported_standards(token); 64 | }; 65 | 66 | public shared ({ caller }) func icrc1_transfer(args : ICRC1.TransferArgs) : async ICRC1.TransferResult { 67 | await* ICRC1.transfer(token, args, caller); 68 | }; 69 | 70 | public shared ({ caller }) func mint(args : ICRC1.Mint) : async ICRC1.TransferResult { 71 | await* ICRC1.mint(token, args, caller); 72 | }; 73 | 74 | public shared ({ caller }) func burn(args : ICRC1.BurnArgs) : async ICRC1.TransferResult { 75 | await* ICRC1.burn(token, args, caller); 76 | }; 77 | 78 | // Functions for integration with the rosetta standard 79 | public shared query func get_transactions(req : ICRC1.GetTransactionsRequest) : async ICRC1.GetTransactionsResponse { 80 | ICRC1.get_transactions(token, req); 81 | }; 82 | 83 | // Additional functions not included in the ICRC1 standard 84 | public shared func get_transaction(i : ICRC1.TxIndex) : async ?ICRC1.Transaction { 85 | await* ICRC1.get_transaction(token, i); 86 | }; 87 | 88 | // Deposit cycles into this canister. 89 | public shared func deposit_cycles() : async () { 90 | let amount = ExperimentalCycles.available(); 91 | let accepted = ExperimentalCycles.accept(amount); 92 | assert (accepted == amount); 93 | }; 94 | }; 95 | -------------------------------------------------------------------------------- /src/ICRC1/Transfer.mo: -------------------------------------------------------------------------------- 1 | import Array "mo:base/Array"; 2 | import Blob "mo:base/Blob"; 3 | import Debug "mo:base/Debug"; 4 | import Int "mo:base/Int"; 5 | import Iter "mo:base/Iter"; 6 | import Nat "mo:base/Nat"; 7 | import Nat64 "mo:base/Nat64"; 8 | import Nat8 "mo:base/Nat8"; 9 | import Option "mo:base/Option"; 10 | import Principal "mo:base/Principal"; 11 | import Result "mo:base/Result"; 12 | import Time "mo:base/Time"; 13 | 14 | import Itertools "mo:itertools/Iter"; 15 | import StableBuffer "mo:StableBuffer/StableBuffer"; 16 | import STMap "mo:StableTrieMap"; 17 | 18 | import Account "Account"; 19 | 20 | import T "Types"; 21 | import Utils "Utils"; 22 | 23 | module { 24 | let { SB } = Utils; 25 | 26 | /// Checks if a transaction memo is valid 27 | public func validate_memo(memo : ?T.Memo) : Bool { 28 | switch (memo) { 29 | case (?bytes) { 30 | bytes.size() <= 32; 31 | }; 32 | case (_) true; 33 | }; 34 | }; 35 | 36 | /// Checks if the `created_at_time` of a transfer request is before the accepted time range 37 | public func is_too_old(token : T.TokenData, created_at_time : Nat64) : Bool { 38 | let { permitted_drift; transaction_window } = token; 39 | 40 | let lower_bound = Time.now() - transaction_window - permitted_drift; 41 | Nat64.toNat(created_at_time) < lower_bound; 42 | }; 43 | 44 | /// Checks if the `created_at_time` of a transfer request has not been reached yet relative to the canister's time. 45 | public func is_in_future(token : T.TokenData, created_at_time : Nat64) : Bool { 46 | let upper_bound = Time.now() + token.permitted_drift; 47 | Nat64.toNat(created_at_time) > upper_bound; 48 | }; 49 | 50 | /// Checks if there is a duplicate transaction that matches the transfer request in the main canister. 51 | /// 52 | /// If a duplicate is found, the function returns an error (`#err`) with the duplicate transaction's index. 53 | public func deduplicate(token : T.TokenData, tx_req : T.TransactionRequest) : Result.Result<(), Nat> { 54 | // only deduplicates if created_at_time is set 55 | if (tx_req.created_at_time == null) { 56 | return #ok(); 57 | }; 58 | 59 | let { transactions = txs; archive } = token; 60 | 61 | var phantom_txs_size = 0; 62 | let phantom_txs = SB._clearedElemsToIter(txs); 63 | let current_txs = SB.vals(txs); 64 | 65 | let last_2000_txs = if (archive.stored_txs > 0) { 66 | phantom_txs_size := SB.capacity(txs) - SB.size(txs); 67 | Itertools.chain(phantom_txs, current_txs); 68 | } else { 69 | current_txs; 70 | }; 71 | 72 | label for_loop for ((i, tx) in Itertools.enumerate(last_2000_txs)) { 73 | 74 | let is_duplicate = switch (tx_req.kind) { 75 | case (#mint) { 76 | switch (tx.mint) { 77 | case (?mint) { 78 | ignore do ? { 79 | if (is_too_old(token, mint.created_at_time!)) { 80 | break for_loop; 81 | }; 82 | }; 83 | 84 | let mint_req : T.Mint = tx_req; 85 | 86 | mint_req == mint; 87 | }; 88 | case (_) false; 89 | }; 90 | }; 91 | case (#burn) { 92 | switch (tx.burn) { 93 | case (?burn) { 94 | ignore do ? { 95 | if (is_too_old(token, burn.created_at_time!)) { 96 | break for_loop; 97 | }; 98 | }; 99 | let burn_req : T.Burn = tx_req; 100 | 101 | burn_req == burn; 102 | }; 103 | case (_) false; 104 | }; 105 | }; 106 | case (#transfer) { 107 | switch (tx.transfer) { 108 | case (?transfer) { 109 | ignore do ? { 110 | if (is_too_old(token, transfer.created_at_time!)) { 111 | break for_loop; 112 | }; 113 | }; 114 | 115 | let transfer_req : T.Transfer = tx_req; 116 | 117 | transfer_req == transfer; 118 | }; 119 | case (_) false; 120 | }; 121 | }; 122 | }; 123 | 124 | if (is_duplicate) { return #err(tx.index) }; 125 | }; 126 | 127 | #ok(); 128 | }; 129 | 130 | /// Checks if a transfer fee is valid 131 | public func validate_fee( 132 | token : T.TokenData, 133 | opt_fee : ?T.Balance, 134 | ) : Bool { 135 | 136 | let ?fee = opt_fee else return true; // if fee is not set, it is assumed to be the default fee 137 | 138 | return fee == token._fee; 139 | }; 140 | 141 | /// Checks if a transfer request is valid 142 | public func validate_request( 143 | token : T.TokenData, 144 | tx_req : T.TransactionRequest, 145 | ) : Result.Result<(), T.TransferError> { 146 | 147 | if (tx_req.from == tx_req.to) { 148 | return #err( 149 | #GenericError({ 150 | error_code = 0; 151 | message = "The sender cannot have the same account as the recipient."; 152 | }), 153 | ); 154 | }; 155 | 156 | if (not Account.validate(tx_req.from)) { 157 | return #err( 158 | #GenericError({ 159 | error_code = 0; 160 | message = "Invalid account entered for sender. " # debug_show(tx_req.from); 161 | }), 162 | ); 163 | }; 164 | 165 | if (not Account.validate(tx_req.to)) { 166 | return #err( 167 | #GenericError({ 168 | error_code = 0; 169 | message = "Invalid account entered for recipient " # debug_show(tx_req.to); 170 | }), 171 | ); 172 | }; 173 | 174 | if (not validate_memo(tx_req.memo)) { 175 | return #err( 176 | #GenericError({ 177 | error_code = 0; 178 | message = "Memo must not be more than 32 bytes"; 179 | }), 180 | ); 181 | }; 182 | 183 | if (tx_req.amount == 0) { 184 | return #err( 185 | #GenericError({ 186 | error_code = 0; 187 | message = "Amount must be greater than 0"; 188 | }), 189 | ); 190 | }; 191 | 192 | switch (tx_req.kind) { 193 | case (#transfer) { 194 | if (not validate_fee(token, tx_req.fee)) { 195 | return #err( 196 | #BadFee { 197 | expected_fee = token._fee; 198 | }, 199 | ); 200 | }; 201 | 202 | let balance : T.Balance = Utils.get_balance( 203 | token.accounts, 204 | tx_req.encoded.from, 205 | ); 206 | 207 | let fee = Option.get(tx_req.fee, token._fee); 208 | 209 | if (tx_req.amount + fee > balance) { 210 | return #err(#InsufficientFunds { balance }); 211 | }; 212 | }; 213 | 214 | case (#mint) { 215 | if (token.max_supply < token._minted_tokens + tx_req.amount) { 216 | let remaining_tokens = (token.max_supply - token._minted_tokens) : Nat; 217 | 218 | return #err( 219 | #GenericError({ 220 | error_code = 0; 221 | message = "Cannot mint more than " # Nat.toText(remaining_tokens) # " tokens"; 222 | }), 223 | ); 224 | }; 225 | }; 226 | case (#burn) { 227 | if (tx_req.to == token.minting_account and tx_req.amount < token.min_burn_amount) { 228 | return #err( 229 | #BadBurn { min_burn_amount = token.min_burn_amount }, 230 | ); 231 | }; 232 | 233 | let balance : T.Balance = Utils.get_balance( 234 | token.accounts, 235 | tx_req.encoded.from, 236 | ); 237 | 238 | if (balance < tx_req.amount) { 239 | return #err(#InsufficientFunds { balance }); 240 | }; 241 | }; 242 | }; 243 | 244 | switch (tx_req.created_at_time) { 245 | case (null) {}; 246 | case (?created_at_time) { 247 | 248 | if (is_too_old(token, created_at_time)) { 249 | return #err(#TooOld); 250 | }; 251 | 252 | if (is_in_future(token, created_at_time)) { 253 | return #err( 254 | #CreatedInFuture { 255 | ledger_time = Nat64.fromNat(Int.abs(Time.now())); 256 | }, 257 | ); 258 | }; 259 | 260 | switch (deduplicate(token, tx_req)) { 261 | case (#err(tx_index)) { 262 | return #err( 263 | #Duplicate { 264 | duplicate_of = tx_index; 265 | }, 266 | ); 267 | }; 268 | case (_) {}; 269 | }; 270 | }; 271 | }; 272 | 273 | #ok(); 274 | }; 275 | 276 | }; 277 | -------------------------------------------------------------------------------- /src/ICRC1/Types.mo: -------------------------------------------------------------------------------- 1 | import Deque "mo:base/Deque"; 2 | import List "mo:base/List"; 3 | import Time "mo:base/Time"; 4 | import Result "mo:base/Result"; 5 | 6 | import STMap "mo:StableTrieMap"; 7 | import StableBuffer "mo:StableBuffer/StableBuffer"; 8 | 9 | module { 10 | 11 | public type Value = { #Nat : Nat; #Int : Int; #Blob : Blob; #Text : Text }; 12 | 13 | public type BlockIndex = Nat; 14 | public type Subaccount = Blob; 15 | public type Balance = Nat; 16 | public type StableBuffer = StableBuffer.StableBuffer; 17 | public type StableTrieMap = STMap.StableTrieMap; 18 | 19 | public type Account = { 20 | owner : Principal; 21 | subaccount : ?Subaccount; 22 | }; 23 | 24 | public type EncodedAccount = Blob; 25 | 26 | public type SupportedStandard = { 27 | name : Text; 28 | url : Text; 29 | }; 30 | 31 | public type Memo = Blob; 32 | public type Timestamp = Nat64; 33 | public type Duration = Nat64; 34 | public type TxIndex = Nat; 35 | public type TxLog = StableBuffer; 36 | 37 | public type MetaDatum = (Text, Value); 38 | public type MetaData = [MetaDatum]; 39 | 40 | public type TxKind = { 41 | #mint; 42 | #burn; 43 | #transfer; 44 | }; 45 | 46 | public type Mint = { 47 | to : Account; 48 | amount : Balance; 49 | memo : ?Blob; 50 | created_at_time : ?Nat64; 51 | }; 52 | 53 | public type BurnArgs = { 54 | from_subaccount : ?Subaccount; 55 | amount : Balance; 56 | memo : ?Blob; 57 | created_at_time : ?Nat64; 58 | }; 59 | 60 | public type Burn = { 61 | from : Account; 62 | amount : Balance; 63 | memo : ?Blob; 64 | created_at_time : ?Nat64; 65 | }; 66 | 67 | /// Arguments for a transfer operation 68 | public type TransferArgs = { 69 | from_subaccount : ?Subaccount; 70 | to : Account; 71 | amount : Balance; 72 | fee : ?Balance; 73 | memo : ?Blob; 74 | 75 | /// The time at which the transaction was created. 76 | /// If this is set, the canister will check for duplicate transactions and reject them. 77 | created_at_time : ?Nat64; 78 | }; 79 | 80 | public type Transfer = { 81 | from : Account; 82 | to : Account; 83 | amount : Balance; 84 | fee : ?Balance; 85 | memo : ?Blob; 86 | created_at_time : ?Nat64; 87 | }; 88 | 89 | /// Internal representation of a transaction request 90 | public type TransactionRequest = { 91 | kind : TxKind; 92 | from : Account; 93 | to : Account; 94 | amount : Balance; 95 | fee : ?Balance; 96 | memo : ?Blob; 97 | created_at_time : ?Nat64; 98 | encoded : { 99 | from : EncodedAccount; 100 | to : EncodedAccount; 101 | }; 102 | }; 103 | 104 | public type Transaction = { 105 | kind : Text; 106 | mint : ?Mint; 107 | burn : ?Burn; 108 | transfer : ?Transfer; 109 | index : TxIndex; 110 | timestamp : Timestamp; 111 | }; 112 | 113 | public type TimeError = { 114 | #TooOld; 115 | #CreatedInFuture : { ledger_time : Timestamp }; 116 | }; 117 | 118 | public type TransferError = TimeError or { 119 | #BadFee : { expected_fee : Balance }; 120 | #BadBurn : { min_burn_amount : Balance }; 121 | #InsufficientFunds : { balance : Balance }; 122 | #Duplicate : { duplicate_of : TxIndex }; 123 | #TemporarilyUnavailable; 124 | #GenericError : { error_code : Nat; message : Text }; 125 | }; 126 | 127 | public type TransferResult = { 128 | #Ok : TxIndex; 129 | #Err : TransferError; 130 | }; 131 | 132 | /// Interface for the ICRC token canister 133 | public type TokenInterface = actor { 134 | 135 | /// Returns the name of the token 136 | icrc1_name : shared query () -> async Text; 137 | 138 | /// Returns the symbol of the token 139 | icrc1_symbol : shared query () -> async Text; 140 | 141 | /// Returns the number of decimals the token uses 142 | icrc1_decimals : shared query () -> async Nat8; 143 | 144 | /// Returns the fee charged for each transfer 145 | icrc1_fee : shared query () -> async Balance; 146 | 147 | /// Returns the tokens metadata 148 | icrc1_metadata : shared query () -> async MetaData; 149 | 150 | /// Returns the total supply of the token 151 | icrc1_total_supply : shared query () -> async Balance; 152 | 153 | /// Returns the account that is allowed to mint new tokens 154 | icrc1_minting_account : shared query () -> async ?Account; 155 | 156 | /// Returns the balance of the given account 157 | icrc1_balance_of : shared query (Account) -> async Balance; 158 | 159 | /// Transfers the given amount of tokens from the sender to the recipient 160 | icrc1_transfer : shared (TransferArgs) -> async TransferResult; 161 | 162 | /// Returns the standards supported by this token's implementation 163 | icrc1_supported_standards : shared query () -> async [SupportedStandard]; 164 | 165 | }; 166 | 167 | public type TxCandidBlob = Blob; 168 | 169 | /// The Interface for the Archive canister 170 | public type ArchiveInterface = actor { 171 | /// Appends the given transactions to the archive. 172 | /// > Only the Ledger canister is allowed to call this method 173 | append_transactions : shared ([Transaction]) -> async Result.Result<(), Text>; 174 | 175 | /// Returns the total number of transactions stored in the archive 176 | total_transactions : shared query () -> async Nat; 177 | 178 | /// Returns the transaction at the given index 179 | get_transaction : shared query (TxIndex) -> async ?Transaction; 180 | 181 | /// Returns the transactions in the given range 182 | get_transactions : shared query (GetTransactionsRequest) -> async TransactionRange; 183 | 184 | /// Returns the number of bytes left in the archive before it is full 185 | /// > The capacity of the archive canister is 32GB 186 | remaining_capacity : shared query () -> async Nat; 187 | }; 188 | 189 | /// Initial arguments for the setting up the icrc1 token canister 190 | public type InitArgs = { 191 | name : Text; 192 | symbol : Text; 193 | decimals : Nat8; 194 | fee : Balance; 195 | minting_account : Account; 196 | max_supply : Balance; 197 | initial_balances : [(Account, Balance)]; 198 | min_burn_amount : Balance; 199 | 200 | /// optional settings for the icrc1 canister 201 | advanced_settings: ?AdvancedSettings 202 | }; 203 | 204 | /// [InitArgs](#type.InitArgs) with optional fields for initializing a token canister 205 | public type TokenInitArgs = { 206 | name : Text; 207 | symbol : Text; 208 | decimals : Nat8; 209 | fee : Balance; 210 | max_supply : Balance; 211 | initial_balances : [(Account, Balance)]; 212 | min_burn_amount : Balance; 213 | 214 | /// optional value that defaults to the caller if not provided 215 | minting_account : ?Account; 216 | 217 | advanced_settings: ?AdvancedSettings; 218 | }; 219 | 220 | /// Additional settings for the [InitArgs](#type.InitArgs) type during initialization of an icrc1 token canister 221 | public type AdvancedSettings = { 222 | /// needed if a token ever needs to be migrated to a new canister 223 | burned_tokens : Balance; 224 | transaction_window : Timestamp; 225 | permitted_drift : Timestamp; 226 | }; 227 | 228 | public type AccountBalances = StableTrieMap; 229 | 230 | /// The details of the archive canister 231 | public type ArchiveData = { 232 | /// The reference to the archive canister 233 | var canister : ArchiveInterface; 234 | 235 | /// The number of transactions stored in the archive 236 | var stored_txs : Nat; 237 | }; 238 | 239 | /// The state of the token canister 240 | public type TokenData = { 241 | /// The name of the token 242 | name : Text; 243 | 244 | /// The symbol of the token 245 | symbol : Text; 246 | 247 | /// The number of decimals the token uses 248 | decimals : Nat8; 249 | 250 | /// The fee charged for each transaction 251 | var _fee : Balance; 252 | 253 | /// The maximum supply of the token 254 | max_supply : Balance; 255 | 256 | /// The total amount of minted tokens 257 | var _minted_tokens : Balance; 258 | 259 | /// The total amount of burned tokens 260 | var _burned_tokens : Balance; 261 | 262 | /// The account that is allowed to mint new tokens 263 | /// On initialization, the maximum supply is minted to this account 264 | minting_account : Account; 265 | 266 | /// The balances of all accounts 267 | accounts : AccountBalances; 268 | 269 | /// The metadata for the token 270 | metadata : StableBuffer; 271 | 272 | /// The standards supported by this token's implementation 273 | supported_standards : StableBuffer; 274 | 275 | /// The time window in which duplicate transactions are not allowed 276 | transaction_window : Nat; 277 | 278 | /// The minimum amount of tokens that must be burned in a transaction 279 | min_burn_amount : Balance; 280 | 281 | /// The allowed difference between the ledger time and the time of the device the transaction was created on 282 | permitted_drift : Nat; 283 | 284 | /// The recent transactions that have been processed by the ledger. 285 | /// Only the last 2000 transactions are stored before being archived. 286 | transactions : StableBuffer; 287 | 288 | /// The record that stores the details to the archive canister and number of transactions stored in it 289 | archive : ArchiveData; 290 | }; 291 | 292 | // Rosetta API 293 | /// The type to request a range of transactions from the ledger canister 294 | public type GetTransactionsRequest = { 295 | start : TxIndex; 296 | length : Nat; 297 | }; 298 | 299 | public type TransactionRange = { 300 | transactions: [Transaction]; 301 | }; 302 | 303 | public type QueryArchiveFn = shared query (GetTransactionsRequest) -> async TransactionRange; 304 | 305 | public type ArchivedTransaction = { 306 | /// The index of the first transaction to be queried in the archive canister 307 | start : TxIndex; 308 | /// The number of transactions to be queried in the archive canister 309 | length : Nat; 310 | 311 | /// The callback function to query the archive canister 312 | callback: QueryArchiveFn; 313 | }; 314 | 315 | public type GetTransactionsResponse = { 316 | /// The number of valid transactions in the ledger and archived canisters that are in the given range 317 | log_length : Nat; 318 | 319 | /// the index of the first tx in the `transactions` field 320 | first_index : TxIndex; 321 | 322 | /// The transactions in the ledger canister that are in the given range 323 | transactions : [Transaction]; 324 | 325 | /// Pagination request for archived transactions in the given range 326 | archived_transactions : [ArchivedTransaction]; 327 | }; 328 | 329 | /// Functions supported by the rosetta 330 | public type RosettaInterface = actor { 331 | get_transactions : shared query (GetTransactionsRequest) -> async GetTransactionsResponse; 332 | }; 333 | 334 | /// Interface of the ICRC token and Rosetta canister 335 | public type FullInterface = TokenInterface and RosettaInterface; 336 | 337 | }; 338 | -------------------------------------------------------------------------------- /src/ICRC1/Utils.mo: -------------------------------------------------------------------------------- 1 | import Array "mo:base/Array"; 2 | import Blob "mo:base/Blob"; 3 | import Debug "mo:base/Debug"; 4 | import Hash "mo:base/Hash"; 5 | import Int "mo:base/Int"; 6 | import Iter "mo:base/Iter"; 7 | import Nat "mo:base/Nat"; 8 | import Nat8 "mo:base/Nat8"; 9 | import Nat32 "mo:base/Nat32"; 10 | import Nat64 "mo:base/Nat64"; 11 | import Option "mo:base/Option"; 12 | import Principal "mo:base/Principal"; 13 | import Result "mo:base/Result"; 14 | import Time "mo:base/Time"; 15 | 16 | import ArrayModule "mo:array/Array"; 17 | import Itertools "mo:itertools/Iter"; 18 | import STMap "mo:StableTrieMap"; 19 | import StableBuffer "mo:StableBuffer/StableBuffer"; 20 | 21 | import Account "Account"; 22 | import T "Types"; 23 | 24 | module { 25 | // Creates a Stable Buffer with the default metadata and returns it. 26 | public func init_metadata(args : T.InitArgs) : StableBuffer.StableBuffer { 27 | let metadata = SB.initPresized(4); 28 | SB.add(metadata, ("icrc1:fee", #Nat(args.fee))); 29 | SB.add(metadata, ("icrc1:name", #Text(args.name))); 30 | SB.add(metadata, ("icrc1:symbol", #Text(args.symbol))); 31 | SB.add(metadata, ("icrc1:decimals", #Nat(Nat8.toNat(args.decimals)))); 32 | 33 | metadata; 34 | }; 35 | 36 | public let default_standard : T.SupportedStandard = { 37 | name = "ICRC-1"; 38 | url = "https://github.com/dfinity/ICRC-1"; 39 | }; 40 | 41 | // Creates a Stable Buffer with the default supported standards and returns it. 42 | public func init_standards() : StableBuffer.StableBuffer { 43 | let standards = SB.initPresized(4); 44 | SB.add(standards, default_standard); 45 | 46 | standards; 47 | }; 48 | 49 | // Returns the default subaccount for cases where a user does 50 | // not specify it. 51 | public func default_subaccount() : T.Subaccount { 52 | Blob.fromArray( 53 | Array.tabulate(32, func(_ : Nat) : Nat8 { 0 }), 54 | ); 55 | }; 56 | 57 | // this is a local copy of deprecated Hash.hashNat8 (redefined to suppress the warning) 58 | func hashNat8(key : [Nat32]) : Hash.Hash { 59 | var hash : Nat32 = 0; 60 | for (natOfKey in key.vals()) { 61 | hash := hash +% natOfKey; 62 | hash := hash +% hash << 10; 63 | hash := hash ^ (hash >> 6); 64 | }; 65 | hash := hash +% hash << 3; 66 | hash := hash ^ (hash >> 11); 67 | hash := hash +% hash << 15; 68 | return hash; 69 | }; 70 | 71 | // Computes a hash from the least significant 32-bits of `n`, ignoring other bits. 72 | public func hash(n : Nat) : Hash.Hash { 73 | let j = Nat32.fromNat(n); 74 | hashNat8([ 75 | j & (255 << 0), 76 | j & (255 << 8), 77 | j & (255 << 16), 78 | j & (255 << 24), 79 | ]); 80 | }; 81 | 82 | // Formats the different operation arguements into 83 | // a `TransactionRequest`, an internal type to access fields easier. 84 | public func create_transfer_req( 85 | args : T.TransferArgs, 86 | owner : Principal, 87 | tx_kind: T.TxKind, 88 | ) : T.TransactionRequest { 89 | 90 | let from = { 91 | owner; 92 | subaccount = args.from_subaccount; 93 | }; 94 | 95 | let encoded = { 96 | from = Account.encode(from); 97 | to = Account.encode(args.to); 98 | }; 99 | 100 | switch (tx_kind) { 101 | case (#mint) { 102 | { 103 | args with kind = #mint; 104 | fee = null; 105 | from; 106 | encoded; 107 | }; 108 | }; 109 | case (#burn) { 110 | { 111 | args with kind = #burn; 112 | fee = null; 113 | from; 114 | encoded; 115 | }; 116 | }; 117 | case (#transfer) { 118 | { 119 | args with kind = #transfer; 120 | from; 121 | encoded; 122 | }; 123 | }; 124 | }; 125 | }; 126 | 127 | // Transforms the transaction kind from `variant` to `Text` 128 | public func kind_to_text(kind : T.TxKind) : Text { 129 | switch (kind) { 130 | case (#mint) "MINT"; 131 | case (#burn) "BURN"; 132 | case (#transfer) "TRANSFER"; 133 | }; 134 | }; 135 | 136 | // Formats the tx request into a finalised transaction 137 | public func req_to_tx(tx_req : T.TransactionRequest, index: Nat) : T.Transaction { 138 | 139 | { 140 | kind = kind_to_text(tx_req.kind); 141 | mint = switch (tx_req.kind) { 142 | case (#mint) { ?tx_req }; 143 | case (_) null; 144 | }; 145 | 146 | burn = switch (tx_req.kind) { 147 | case (#burn) { ?tx_req }; 148 | case (_) null; 149 | }; 150 | 151 | transfer = switch (tx_req.kind) { 152 | case (#transfer) { ?tx_req }; 153 | case (_) null; 154 | }; 155 | 156 | index; 157 | timestamp = Nat64.fromNat(Int.abs(Time.now())); 158 | }; 159 | }; 160 | 161 | public func div_ceil(n : Nat, d : Nat) : Nat { 162 | (n + d - 1) / d; 163 | }; 164 | 165 | /// Retrieves the balance of an account 166 | public func get_balance(accounts : T.AccountBalances, encoded_account : T.EncodedAccount) : T.Balance { 167 | let res = STMap.get( 168 | accounts, 169 | Blob.equal, 170 | Blob.hash, 171 | encoded_account, 172 | ); 173 | 174 | switch (res) { 175 | case (?balance) { 176 | balance; 177 | }; 178 | case (_) 0; 179 | }; 180 | }; 181 | 182 | /// Updates the balance of an account 183 | public func update_balance( 184 | accounts : T.AccountBalances, 185 | encoded_account : T.EncodedAccount, 186 | update : (T.Balance) -> T.Balance, 187 | ) { 188 | let prev_balance = get_balance(accounts, encoded_account); 189 | let updated_balance = update(prev_balance); 190 | 191 | if (updated_balance != prev_balance) { 192 | STMap.put( 193 | accounts, 194 | Blob.equal, 195 | Blob.hash, 196 | encoded_account, 197 | updated_balance, 198 | ); 199 | }; 200 | }; 201 | 202 | // Transfers tokens from the sender to the 203 | // recipient in the tx request 204 | public func transfer_balance( 205 | token : T.TokenData, 206 | tx_req : T.TransactionRequest, 207 | ) { 208 | let { encoded; amount } = tx_req; 209 | 210 | update_balance( 211 | token.accounts, 212 | encoded.from, 213 | func(balance) { 214 | balance - amount; 215 | }, 216 | ); 217 | 218 | update_balance( 219 | token.accounts, 220 | encoded.to, 221 | func(balance) { 222 | balance + amount; 223 | }, 224 | ); 225 | }; 226 | 227 | public func mint_balance( 228 | token : T.TokenData, 229 | encoded_account : T.EncodedAccount, 230 | amount : T.Balance, 231 | ) { 232 | update_balance( 233 | token.accounts, 234 | encoded_account, 235 | func(balance) { 236 | balance + amount; 237 | }, 238 | ); 239 | 240 | token._minted_tokens += amount; 241 | }; 242 | 243 | public func burn_balance( 244 | token : T.TokenData, 245 | encoded_account : T.EncodedAccount, 246 | amount : T.Balance, 247 | ) { 248 | update_balance( 249 | token.accounts, 250 | encoded_account, 251 | func(balance) { 252 | balance - amount; 253 | }, 254 | ); 255 | 256 | token._burned_tokens += amount; 257 | }; 258 | 259 | // Stable Buffer Module with some additional functions 260 | public let SB = { 261 | StableBuffer with slice = func(buffer : T.StableBuffer, start : Nat, end : Nat) : [A] { 262 | let size = SB.size(buffer); 263 | if (start >= size) { 264 | return []; 265 | }; 266 | 267 | let slice_len = (Nat.min(end, size) - start) : Nat; 268 | 269 | Array.tabulate( 270 | slice_len, 271 | func(i : Nat) : A { 272 | SB.get(buffer, i + start); 273 | }, 274 | ); 275 | }; 276 | 277 | toIterFromSlice = func(buffer : T.StableBuffer, start : Nat, end : Nat) : Iter.Iter { 278 | if (start >= SB.size(buffer)) { 279 | return Itertools.empty(); 280 | }; 281 | 282 | Iter.map( 283 | Itertools.range(start, Nat.min(SB.size(buffer), end)), 284 | func(i : Nat) : A { 285 | SB.get(buffer, i); 286 | }, 287 | ); 288 | }; 289 | 290 | appendArray = func(buffer : T.StableBuffer, array : [A]) { 291 | for (elem in array.vals()) { 292 | SB.add(buffer, elem); 293 | }; 294 | }; 295 | 296 | getLast = func(buffer : T.StableBuffer) : ?A { 297 | let size = SB.size(buffer); 298 | 299 | if (size > 0) { 300 | SB.getOpt(buffer, (size - 1) : Nat); 301 | } else { 302 | null; 303 | }; 304 | }; 305 | 306 | capacity = func(buffer : T.StableBuffer) : Nat { 307 | buffer.elems.size(); 308 | }; 309 | 310 | _clearedElemsToIter = func(buffer : T.StableBuffer) : Iter.Iter { 311 | Iter.map( 312 | Itertools.range(buffer.count, buffer.elems.size()), 313 | func(i : Nat) : A { 314 | buffer.elems[i]; 315 | }, 316 | ); 317 | }; 318 | }; 319 | }; 320 | -------------------------------------------------------------------------------- /src/ICRC1/lib.mo: -------------------------------------------------------------------------------- 1 | import Array "mo:base/Array"; 2 | import Blob "mo:base/Blob"; 3 | import Debug "mo:base/Debug"; 4 | import Float "mo:base/Float"; 5 | import Int "mo:base/Int"; 6 | import Iter "mo:base/Iter"; 7 | import Nat "mo:base/Nat"; 8 | import Nat64 "mo:base/Nat64"; 9 | import Nat8 "mo:base/Nat8"; 10 | import Option "mo:base/Option"; 11 | import Principal "mo:base/Principal"; 12 | import EC "mo:base/ExperimentalCycles"; 13 | 14 | import Itertools "mo:itertools/Iter"; 15 | import StableTrieMap "mo:StableTrieMap"; 16 | 17 | import Account "Account"; 18 | import T "Types"; 19 | import Utils "Utils"; 20 | import Transfer "Transfer"; 21 | import Archive "Canisters/Archive"; 22 | 23 | /// The ICRC1 class with all the functions for creating an 24 | /// ICRC1 token on the Internet Computer 25 | module { 26 | let { SB } = Utils; 27 | 28 | public type Account = T.Account; 29 | public type Subaccount = T.Subaccount; 30 | public type AccountBalances = T.AccountBalances; 31 | 32 | public type Transaction = T.Transaction; 33 | public type Balance = T.Balance; 34 | public type TransferArgs = T.TransferArgs; 35 | public type Mint = T.Mint; 36 | public type BurnArgs = T.BurnArgs; 37 | public type TransactionRequest = T.TransactionRequest; 38 | public type TransferError = T.TransferError; 39 | 40 | public type SupportedStandard = T.SupportedStandard; 41 | 42 | public type InitArgs = T.InitArgs; 43 | public type TokenInitArgs = T.TokenInitArgs; 44 | public type TokenData = T.TokenData; 45 | public type MetaDatum = T.MetaDatum; 46 | public type TxLog = T.TxLog; 47 | public type TxIndex = T.TxIndex; 48 | 49 | public type TokenInterface = T.TokenInterface; 50 | public type RosettaInterface = T.RosettaInterface; 51 | public type FullInterface = T.FullInterface; 52 | 53 | public type ArchiveInterface = T.ArchiveInterface; 54 | 55 | public type GetTransactionsRequest = T.GetTransactionsRequest; 56 | public type GetTransactionsResponse = T.GetTransactionsResponse; 57 | public type QueryArchiveFn = T.QueryArchiveFn; 58 | public type TransactionRange = T.TransactionRange; 59 | public type ArchivedTransaction = T.ArchivedTransaction; 60 | 61 | public type TransferResult = T.TransferResult; 62 | 63 | public let MAX_TRANSACTIONS_IN_LEDGER = 2000; 64 | public let MAX_TRANSACTION_BYTES : Nat64 = 196; 65 | public let MAX_TRANSACTIONS_PER_REQUEST = 5000; 66 | 67 | /// Initialize a new ICRC-1 token 68 | public func init(args : T.InitArgs) : T.TokenData { 69 | let { 70 | name; 71 | symbol; 72 | decimals; 73 | fee; 74 | minting_account; 75 | max_supply; 76 | initial_balances; 77 | min_burn_amount; 78 | advanced_settings; 79 | } = args; 80 | 81 | var _burned_tokens = 0; 82 | var permitted_drift = 60_000_000_000; 83 | var transaction_window = 86_400_000_000_000; 84 | 85 | switch(advanced_settings){ 86 | case(?options) { 87 | _burned_tokens := options.burned_tokens; 88 | permitted_drift := Nat64.toNat(options.permitted_drift); 89 | transaction_window := Nat64.toNat(options.transaction_window); 90 | }; 91 | case(null) { }; 92 | }; 93 | 94 | if (not Account.validate(minting_account)) { 95 | Debug.trap("minting_account is invalid"); 96 | }; 97 | 98 | let accounts : T.AccountBalances = StableTrieMap.new(); 99 | 100 | var _minted_tokens = _burned_tokens; 101 | 102 | for ((i, (account, balance)) in Itertools.enumerate(initial_balances.vals())) { 103 | 104 | if (not Account.validate(account)) { 105 | Debug.trap( 106 | "Invalid Account: Account at index " # debug_show i # " is invalid in 'initial_balances'", 107 | ); 108 | }; 109 | 110 | let encoded_account = Account.encode(account); 111 | 112 | StableTrieMap.put( 113 | accounts, 114 | Blob.equal, 115 | Blob.hash, 116 | encoded_account, 117 | balance, 118 | ); 119 | 120 | _minted_tokens += balance; 121 | }; 122 | 123 | { 124 | name = name; 125 | symbol = symbol; 126 | decimals; 127 | var _fee = fee; 128 | max_supply; 129 | var _minted_tokens = _minted_tokens; 130 | var _burned_tokens = _burned_tokens; 131 | min_burn_amount; 132 | minting_account; 133 | accounts; 134 | metadata = Utils.init_metadata(args); 135 | supported_standards = Utils.init_standards(); 136 | transactions = SB.initPresized(MAX_TRANSACTIONS_IN_LEDGER); 137 | permitted_drift; 138 | transaction_window; 139 | archive = { 140 | var canister = actor ("aaaaa-aa"); 141 | var stored_txs = 0; 142 | }; 143 | }; 144 | }; 145 | 146 | /// Retrieve the name of the token 147 | public func name(token : T.TokenData) : Text { 148 | token.name; 149 | }; 150 | 151 | /// Retrieve the symbol of the token 152 | public func symbol(token : T.TokenData) : Text { 153 | token.symbol; 154 | }; 155 | 156 | /// Retrieve the number of decimals specified for the token 157 | public func decimals({ decimals } : T.TokenData) : Nat8 { 158 | decimals; 159 | }; 160 | 161 | /// Retrieve the fee for each transfer 162 | public func fee(token : T.TokenData) : T.Balance { 163 | token._fee; 164 | }; 165 | 166 | /// Set the fee for each transfer 167 | public func set_fee(token : T.TokenData, fee : Nat) { 168 | token._fee := fee; 169 | }; 170 | 171 | /// Retrieve all the metadata of the token 172 | public func metadata(token : T.TokenData) : [T.MetaDatum] { 173 | SB.toArray(token.metadata); 174 | }; 175 | 176 | /// Returns the total supply of circulating tokens 177 | public func total_supply(token : T.TokenData) : T.Balance { 178 | token._minted_tokens - token._burned_tokens; 179 | }; 180 | 181 | /// Returns the total supply of minted tokens 182 | public func minted_supply(token : T.TokenData) : T.Balance { 183 | token._minted_tokens; 184 | }; 185 | 186 | /// Returns the total supply of burned tokens 187 | public func burned_supply(token : T.TokenData) : T.Balance { 188 | token._burned_tokens; 189 | }; 190 | 191 | /// Returns the maximum supply of tokens 192 | public func max_supply(token : T.TokenData) : T.Balance { 193 | token.max_supply; 194 | }; 195 | 196 | /// Returns the account with the permission to mint tokens 197 | /// 198 | /// Note: **The minting account can only participate in minting 199 | /// and burning transactions, so any tokens sent to it will be 200 | /// considered burned.** 201 | 202 | public func minting_account(token : T.TokenData) : T.Account { 203 | token.minting_account; 204 | }; 205 | 206 | /// Retrieve the balance of a given account 207 | public func balance_of({ accounts } : T.TokenData, account : T.Account) : T.Balance { 208 | let encoded_account = Account.encode(account); 209 | Utils.get_balance(accounts, encoded_account); 210 | }; 211 | 212 | /// Returns an array of standards supported by this token 213 | public func supported_standards(token : T.TokenData) : [T.SupportedStandard] { 214 | SB.toArray(token.supported_standards); 215 | }; 216 | 217 | /// Formats a float to a nat balance and applies the correct number of decimal places 218 | public func balance_from_float(token : T.TokenData, float : Float) : T.Balance { 219 | if (float <= 0) { 220 | return 0; 221 | }; 222 | 223 | let float_with_decimals = float * (10 ** Float.fromInt(Nat8.toNat(token.decimals))); 224 | 225 | Int.abs(Float.toInt(float_with_decimals)); 226 | }; 227 | 228 | /// Transfers tokens from one account to another account (minting and burning included) 229 | public func transfer( 230 | token : T.TokenData, 231 | args : T.TransferArgs, 232 | caller : Principal, 233 | ) : async* T.TransferResult { 234 | 235 | let from = { 236 | owner = caller; 237 | subaccount = args.from_subaccount; 238 | }; 239 | 240 | let tx_kind = if (from == token.minting_account) { 241 | #mint 242 | } else if (args.to == token.minting_account) { 243 | #burn 244 | } else { 245 | #transfer 246 | }; 247 | 248 | let tx_req = Utils.create_transfer_req(args, caller, tx_kind); 249 | 250 | switch (Transfer.validate_request(token, tx_req)) { 251 | case (#err(errorType)) { 252 | return #Err(errorType); 253 | }; 254 | case (#ok(_)) {}; 255 | }; 256 | 257 | let { encoded; amount } = tx_req; 258 | 259 | // process transaction 260 | switch(tx_req.kind){ 261 | case(#mint){ 262 | Utils.mint_balance(token, encoded.to, amount); 263 | }; 264 | case(#burn){ 265 | Utils.burn_balance(token, encoded.from, amount); 266 | }; 267 | case(#transfer){ 268 | Utils.transfer_balance(token, tx_req); 269 | 270 | // burn fee 271 | Utils.burn_balance(token, encoded.from, token._fee); 272 | }; 273 | }; 274 | 275 | // store transaction 276 | let index = SB.size(token.transactions) + token.archive.stored_txs; 277 | let tx = Utils.req_to_tx(tx_req, index); 278 | SB.add(token.transactions, tx); 279 | 280 | // transfer transaction to archive if necessary 281 | await* update_canister(token); 282 | 283 | #Ok(tx.index); 284 | }; 285 | 286 | /// Helper function to mint tokens with minimum args 287 | public func mint(token : T.TokenData, args : T.Mint, caller : Principal) : async* T.TransferResult { 288 | 289 | if (caller != token.minting_account.owner) { 290 | return #Err( 291 | #GenericError { 292 | error_code = 401; 293 | message = "Unauthorized: Only the minting_account can mint tokens."; 294 | }, 295 | ); 296 | }; 297 | 298 | let transfer_args : T.TransferArgs = { 299 | args with from_subaccount = token.minting_account.subaccount; 300 | fee = null; 301 | }; 302 | 303 | await* transfer(token, transfer_args, caller); 304 | }; 305 | 306 | /// Helper function to burn tokens with minimum args 307 | public func burn(token : T.TokenData, args : T.BurnArgs, caller : Principal) : async* T.TransferResult { 308 | 309 | let transfer_args : T.TransferArgs = { 310 | args with to = token.minting_account; 311 | fee = null; 312 | }; 313 | 314 | await* transfer(token, transfer_args, caller); 315 | }; 316 | 317 | /// Returns the total number of transactions that have been processed by the given token. 318 | public func total_transactions(token : T.TokenData) : Nat { 319 | let { archive; transactions } = token; 320 | archive.stored_txs + SB.size(transactions); 321 | }; 322 | 323 | /// Retrieves the transaction specified by the given `tx_index` 324 | public func get_transaction(token : T.TokenData, tx_index : T.TxIndex) : async* ?T.Transaction { 325 | let { archive; transactions } = token; 326 | 327 | let archived_txs = archive.stored_txs; 328 | 329 | if (tx_index < archive.stored_txs) { 330 | await archive.canister.get_transaction(tx_index); 331 | } else { 332 | let local_tx_index = (tx_index - archive.stored_txs) : Nat; 333 | SB.getOpt(token.transactions, local_tx_index); 334 | }; 335 | }; 336 | 337 | /// Retrieves the transactions specified by the given range 338 | public func get_transactions(token : T.TokenData, req : T.GetTransactionsRequest) : T.GetTransactionsResponse { 339 | let { archive; transactions } = token; 340 | 341 | var first_index = 0xFFFF_FFFF_FFFF_FFFF; // returned if no transactions are found 342 | 343 | let req_end = req.start + req.length; 344 | let tx_end = archive.stored_txs + SB.size(transactions); 345 | 346 | var txs_in_canister: [T.Transaction] = []; 347 | 348 | if (req.start < tx_end and req_end >= archive.stored_txs) { 349 | first_index := Nat.max(req.start, archive.stored_txs); 350 | let tx_start_index = (first_index - archive.stored_txs) : Nat; 351 | 352 | txs_in_canister:= SB.slice(transactions, tx_start_index, tx_start_index + req.length); 353 | }; 354 | 355 | let archived_range = if (req.start < archive.stored_txs) { 356 | { 357 | start = req.start; 358 | end = Nat.min( 359 | archive.stored_txs, 360 | (req.start + req.length) : Nat, 361 | ); 362 | }; 363 | } else { 364 | { start = 0; end = 0 }; 365 | }; 366 | 367 | let txs_in_archive = (archived_range.end - archived_range.start) : Nat; 368 | 369 | let size = Utils.div_ceil(txs_in_archive, MAX_TRANSACTIONS_PER_REQUEST); 370 | 371 | let archived_transactions = Array.tabulate( 372 | size, 373 | func(i : Nat) : T.ArchivedTransaction { 374 | let offset = i * MAX_TRANSACTIONS_PER_REQUEST; 375 | let start = offset + archived_range.start; 376 | let length = Nat.min( 377 | MAX_TRANSACTIONS_PER_REQUEST, 378 | archived_range.end - start, 379 | ); 380 | 381 | let callback = token.archive.canister.get_transactions; 382 | 383 | { start; length; callback }; 384 | }, 385 | ); 386 | 387 | { 388 | log_length = txs_in_archive + txs_in_canister.size(); 389 | first_index; 390 | transactions = txs_in_canister; 391 | archived_transactions; 392 | }; 393 | }; 394 | 395 | // Updates the token's data and manages the transactions 396 | // 397 | // **added at the end of any function that creates a new transaction** 398 | func update_canister(token : T.TokenData) : async* () { 399 | let txs_size = SB.size(token.transactions); 400 | 401 | if (txs_size >= MAX_TRANSACTIONS_IN_LEDGER) { 402 | await* append_transactions(token); 403 | }; 404 | }; 405 | 406 | // Moves the transactions from the ICRC1 canister to the archive canister 407 | // and returns a boolean that indicates the success of the data transfer 408 | func append_transactions(token : T.TokenData) : async* () { 409 | let { archive; transactions } = token; 410 | 411 | if (archive.stored_txs == 0) { 412 | EC.add(200_000_000_000); 413 | archive.canister := await Archive.Archive(); 414 | }; 415 | 416 | let res = await archive.canister.append_transactions( 417 | SB.toArray(transactions), 418 | ); 419 | 420 | switch (res) { 421 | case (#ok(_)) { 422 | archive.stored_txs += SB.size(transactions); 423 | SB.clear(transactions); 424 | }; 425 | case (#err(_)) {}; 426 | }; 427 | }; 428 | 429 | }; 430 | -------------------------------------------------------------------------------- /tests/ActorTest.mo: -------------------------------------------------------------------------------- 1 | import Debug "mo:base/Debug"; 2 | 3 | import Archive "ICRC1/Archive.ActorTest"; 4 | import ICRC1 "ICRC1/ICRC1.ActorTest"; 5 | 6 | import ActorSpec "./utils/ActorSpec"; 7 | 8 | actor { 9 | let { run } = ActorSpec; 10 | 11 | let test_modules = [ 12 | Archive.test, 13 | ICRC1.test, 14 | ]; 15 | 16 | public func run_tests() : async () { 17 | for (test in test_modules.vals()) { 18 | let success = ActorSpec.run([await test()]); 19 | 20 | if (success == false) { 21 | Debug.trap("\1b[46;41mTests failed\1b[0m"); 22 | } else { 23 | Debug.print("\1b[23;42;3m Success!\1b[0m"); 24 | }; 25 | }; 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /tests/ICRC1/Account.Test.mo: -------------------------------------------------------------------------------- 1 | import Array "mo:base/Array"; 2 | import Blob "mo:base/Blob"; 3 | import Debug "mo:base/Debug"; 4 | import Iter "mo:base/Iter"; 5 | import Nat8 "mo:base/Nat8"; 6 | import Principal "mo:base/Principal"; 7 | 8 | import Itertools "mo:itertools/Iter"; 9 | 10 | import Account "../../src/ICRC1/Account"; 11 | import ActorSpec "../utils/ActorSpec"; 12 | import Archive "../../src/ICRC1/Canisters/Archive"; 13 | 14 | let { 15 | assertTrue; 16 | assertFalse; 17 | assertAllTrue; 18 | describe; 19 | it; 20 | skip; 21 | pending; 22 | run; 23 | } = ActorSpec; 24 | 25 | let principal = Principal.fromText("prb4z-5pc7u-zdfqi-cgv7o-fdyqf-n6afm-xh6hz-v4bk4-kpg3y-rvgxf-iae"); 26 | 27 | let success = run([ 28 | describe( 29 | "Account", 30 | [ 31 | describe( 32 | "encode / decode Account", 33 | [ 34 | it( 35 | "'null' subaccount", 36 | do { 37 | let account = { 38 | owner = principal; 39 | subaccount = null; 40 | }; 41 | 42 | let encoded = Account.encode(account); 43 | let decoded = Account.decode(encoded); 44 | assertAllTrue([ 45 | encoded == Principal.toBlob(account.owner), 46 | decoded == ?account, 47 | Account.validate(account) 48 | ]); 49 | }, 50 | ), 51 | it( 52 | "subaccount with only zero bytes", 53 | do { 54 | let account = { 55 | owner = principal; 56 | 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]); 57 | }; 58 | 59 | let encoded = Account.encode(account); 60 | let decoded = Account.decode(encoded); 61 | 62 | assertAllTrue([ 63 | encoded == Principal.toBlob(account.owner), 64 | decoded == ?{ account with subaccount = null }, 65 | Account.validate(account) 66 | ]); 67 | }, 68 | ), 69 | it( 70 | "subaccount prefixed with zero bytes", 71 | do { 72 | let account = { 73 | owner = principal; 74 | 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]); 75 | }; 76 | 77 | let encoded = Account.encode(account); 78 | let decoded = Account.decode(encoded); 79 | 80 | let pricipal_iter = Principal.toBlob(account.owner).vals(); 81 | 82 | let valid_bytes : [Nat8] = [1, 2, 3, 4, 5, 6, 7, 8]; 83 | let suffix_bytes : [Nat8] = [ 84 | 8, // size of valid_bytes 85 | 0x7f // ending tag 86 | ]; 87 | 88 | let iter = Itertools.chain( 89 | pricipal_iter, 90 | Itertools.chain( 91 | valid_bytes.vals(), 92 | suffix_bytes.vals(), 93 | ), 94 | ); 95 | 96 | let expected_blob = Blob.fromArray(Iter.toArray(iter)); 97 | 98 | assertAllTrue([ 99 | encoded == expected_blob, 100 | decoded == ?account, 101 | Account.validate(account) 102 | ]); 103 | }, 104 | ), 105 | it( 106 | "subaccount with zero bytes surrounded by non zero bytes", 107 | do { 108 | let account = { 109 | owner = principal; 110 | 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]); 111 | }; 112 | 113 | let encoded = Account.encode(account); 114 | let decoded = Account.decode(encoded); 115 | 116 | let pricipal_iter = Principal.toBlob(account.owner).vals(); 117 | 118 | 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]; 119 | let suffix_bytes : [Nat8] = [ 120 | 32, // size of valid_bytes 121 | 0x7f // ending tag 122 | ]; 123 | 124 | let iter = Itertools.chain( 125 | pricipal_iter, 126 | Itertools.chain( 127 | valid_bytes.vals(), 128 | suffix_bytes.vals(), 129 | ), 130 | ); 131 | 132 | let expected_blob = Blob.fromArray(Iter.toArray(iter)); 133 | 134 | assertAllTrue([ 135 | encoded == expected_blob, 136 | decoded == ?account, 137 | Account.validate(account) 138 | ]); 139 | }, 140 | ), 141 | it( 142 | "subaccount with non zero bytes", 143 | do { 144 | let account = { 145 | owner = principal; 146 | 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]); 147 | }; 148 | 149 | let encoded = Account.encode(account); 150 | let decoded = Account.decode(encoded); 151 | 152 | let pricipal_iter = Principal.toBlob(account.owner).vals(); 153 | 154 | 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]; 155 | let suffix_bytes : [Nat8] = [ 156 | 32, // size of valid_bytes 157 | 0x7f // ending tag 158 | ]; 159 | 160 | let iter = Itertools.chain( 161 | pricipal_iter, 162 | Itertools.chain( 163 | valid_bytes.vals(), 164 | suffix_bytes.vals(), 165 | ), 166 | ); 167 | 168 | let expected_blob = Blob.fromArray(Iter.toArray(iter)); 169 | 170 | assertAllTrue([ 171 | encoded == expected_blob, 172 | decoded == ?account, 173 | Account.validate(account) 174 | ]); 175 | }, 176 | ), 177 | it( 178 | "should return false for invalid subaccount (length < 32)", 179 | do { 180 | 181 | var len = 0; 182 | var is_valid = false; 183 | 184 | label _loop while (len < 32){ 185 | let account = { 186 | owner = principal; 187 | subaccount = ?Blob.fromArray(Array.tabulate(len, Nat8.fromNat)); 188 | }; 189 | 190 | is_valid := is_valid or Account.validate(account) 191 | or Account.validate_subaccount(account.subaccount); 192 | 193 | if (is_valid) { 194 | break _loop; 195 | }; 196 | 197 | len += 1; 198 | }; 199 | 200 | not is_valid; 201 | } 202 | ) 203 | ], 204 | ), 205 | ], 206 | ), 207 | ]); 208 | 209 | if (success == false) { 210 | Debug.trap("\1b[46;41mTests failed\1b[0m"); 211 | } else { 212 | Debug.print("\1b[23;42;3m Success!\1b[0m"); 213 | }; 214 | -------------------------------------------------------------------------------- /tests/ICRC1/Archive.ActorTest.mo: -------------------------------------------------------------------------------- 1 | import Debug "mo:base/Debug"; 2 | import Array "mo:base/Array"; 3 | import Iter "mo:base/Iter"; 4 | import Nat "mo:base/Nat"; 5 | import Int "mo:base/Int"; 6 | import Float "mo:base/Float"; 7 | import Nat64 "mo:base/Nat64"; 8 | import Principal "mo:base/Principal"; 9 | import EC "mo:base/ExperimentalCycles"; 10 | 11 | import Archive "../../src/ICRC1/Canisters/Archive"; 12 | import T "../../src/ICRC1/Types"; 13 | 14 | import ActorSpec "../utils/ActorSpec"; 15 | 16 | module { 17 | let { 18 | assertTrue; 19 | assertFalse; 20 | assertAllTrue; 21 | describe; 22 | it; 23 | skip; 24 | pending; 25 | run; 26 | } = ActorSpec; 27 | 28 | func new_tx(i : Nat) : T.Transaction { 29 | { 30 | kind = ""; 31 | mint = null; 32 | burn = null; 33 | transfer = null; 34 | index = i; 35 | timestamp = Nat64.fromNat(i); 36 | }; 37 | }; 38 | 39 | // [start, end) 40 | func txs_range(start : Nat, end : Nat) : [T.Transaction] { 41 | Array.tabulate( 42 | (end - start) : Nat, 43 | func(i : Nat) : T.Transaction { 44 | new_tx(start + i); 45 | }, 46 | ); 47 | }; 48 | 49 | func new_txs(length : Nat) : [T.Transaction] { 50 | txs_range(0, length); 51 | }; 52 | 53 | let TC = 1_000_000_000_000; 54 | let CREATE_CANISTER = 100_000_000_000; 55 | 56 | func create_canister_and_add_cycles(n : Float) { 57 | EC.add( 58 | CREATE_CANISTER + Int.abs(Float.toInt(n * 1_000_000_000_000)), 59 | ); 60 | }; 61 | 62 | public func test() : async ActorSpec.Group { 63 | describe( 64 | "Archive Canister", 65 | [ 66 | it( 67 | "append_transactions()", 68 | do { 69 | create_canister_and_add_cycles(0.1); 70 | let archive = await Archive.Archive(); 71 | 72 | let txs = new_txs(500); 73 | 74 | assertAllTrue([ 75 | (await archive.total_transactions()) == 0, 76 | (await archive.append_transactions(txs)) == #ok(), 77 | (await archive.total_transactions()) == 500, 78 | ]); 79 | }, 80 | ), 81 | it( 82 | "get_transaction()", 83 | do { 84 | create_canister_and_add_cycles(0.1); 85 | let archive = await Archive.Archive(); 86 | 87 | let txs = new_txs(3555); 88 | 89 | let res = await archive.append_transactions(txs); 90 | 91 | assertAllTrue([ 92 | res == #ok(), 93 | (await archive.total_transactions()) == 3555, 94 | (await archive.get_transaction(0)) == ?new_tx(0), 95 | (await archive.get_transaction(999)) == ?new_tx(999), 96 | (await archive.get_transaction(1000)) == ?new_tx(1000), 97 | (await archive.get_transaction(1234)) == ?new_tx(1234), 98 | (await archive.get_transaction(2829)) == ?new_tx(2829), 99 | (await archive.get_transaction(3554)) == ?new_tx(3554), 100 | (await archive.get_transaction(3555)) == null, 101 | (await archive.get_transaction(999999)) == null, 102 | ]); 103 | }, 104 | ), 105 | it( 106 | "get_transactions()", 107 | do { 108 | 109 | create_canister_and_add_cycles(0.1); 110 | let archive = await Archive.Archive(); 111 | 112 | let txs = new_txs(5000); 113 | 114 | let res = await archive.append_transactions(txs); 115 | 116 | let tx_range = await archive.get_transactions({ 117 | start = 3251; 118 | length = 2000; 119 | }); 120 | 121 | assertAllTrue([ 122 | res == #ok(), 123 | (await archive.total_transactions()) == 5000, 124 | (await archive.get_transactions({ start = 0; length = 100 })).transactions == txs_range(0, 100), 125 | (await archive.get_transactions({ start = 225; length = 100 })).transactions == txs_range(225, 325), 126 | (await archive.get_transactions({ start = 225; length = 1200 })).transactions == txs_range(225, 1425), 127 | (await archive.get_transactions({ start = 980; length = 100 })).transactions == txs_range(980, 1080), 128 | (await archive.get_transactions({ start = 3251; length = 2000 })).transactions == txs_range(3251, 5000), 129 | ]); 130 | }, 131 | ), 132 | ], 133 | ); 134 | }; 135 | }; 136 | -------------------------------------------------------------------------------- /tests/test_template.md: -------------------------------------------------------------------------------- 1 | Filename: `[Section]/[Function].Test.mo` 2 | 3 | ```motoko 4 | import Debug "mo:base/Debug"; 5 | import Iter "mo:base/Iter"; 6 | 7 | import ActorSpec "../utils/ActorSpec"; 8 | import Algo "../../src"; 9 | // import [FnName] "../../src/[section]/[FnName]"; 10 | 11 | let { 12 | assertTrue; assertFalse; assertAllTrue; 13 | describe; it; skip; pending; run 14 | } = ActorSpec; 15 | 16 | let success = run([ 17 | describe(" (Function Name) ", [ 18 | it("(test name)", do { 19 | 20 | // ... 21 | }), 22 | ]) 23 | ]); 24 | 25 | if(success == false){ 26 | Debug.trap("\1b[46;41mTests failed\1b[0m"); 27 | }else{ 28 | Debug.print("\1b[23;42;3m Success!\1b[0m"); 29 | }; 30 | 31 | ``` -------------------------------------------------------------------------------- /tests/utils/ActorSpec.mo: -------------------------------------------------------------------------------- 1 | import Debug "mo:base/Debug"; 2 | import Array "mo:base/Array"; 3 | import Iter "mo:base/Iter"; 4 | import Int "mo:base/Int"; 5 | import Nat "mo:base/Nat"; 6 | import Text "mo:base/Text"; 7 | 8 | module { 9 | public type Group = { 10 | name : Text; 11 | groups : [Group]; 12 | status : Status; 13 | }; 14 | 15 | type Status = { 16 | failed : Nat; 17 | passed : Nat; 18 | pending : Nat; 19 | skipped : Nat; 20 | }; 21 | 22 | func eqStatus(x : Status, y : Status) : Bool { 23 | x.failed == y.failed and x.passed == y.passed and x.pending == y.pending and x.skipped == y.skipped; 24 | }; 25 | 26 | let emptyStatus : Status = { 27 | failed = 0; 28 | passed = 0; 29 | pending = 0; 30 | skipped = 0; 31 | }; 32 | 33 | func appendStatus(x : Status, y : Status) : Status { 34 | { 35 | failed = x.failed + y.failed; 36 | passed = x.passed + y.passed; 37 | pending = x.pending + y.pending; 38 | skipped = x.skipped + y.skipped; 39 | }; 40 | }; 41 | 42 | func printStatus(status : Status) : Text { 43 | "Failed: " # Int.toText(status.failed) # ", Passed: " # Int.toText(status.passed) # ", Pending: " # Int.toText(status.pending) # ", Skipped: " # Int.toText(status.skipped); 44 | }; 45 | 46 | public func run(groups_ : [Group]) : Bool { 47 | let (groups, status) = getGroups(groups_); 48 | printGroups(groups, ""); 49 | Debug.print("\n"); 50 | Debug.print(printStatus(status)); 51 | Debug.print("\n"); 52 | status.failed == 0; 53 | }; 54 | 55 | func getGroups(groups_ : [Group]) : ([Group], Status) { 56 | let groups = Array.thaw(groups_); 57 | var status = emptyStatus; 58 | for (index in groups_.keys()) { 59 | let group = groups[index]; 60 | let (newGroups, newGroupsStatus) = getGroups(group.groups); 61 | let newStatus = appendStatus(group.status, newGroupsStatus); 62 | status := appendStatus(status, newStatus); 63 | let newGroup = { 64 | name = group.name; 65 | groups = newGroups; 66 | status = newStatus; 67 | }; 68 | groups[index] := newGroup; 69 | }; 70 | (Array.freeze(groups), status); 71 | }; 72 | 73 | func printGroups(groups_ : [Group], indent : Text) { 74 | for (group in groups_.vals()) { 75 | let isDescribe = Iter.size(Array.keys(group.groups)) > 0; 76 | let newline = if isDescribe "\n" else ""; 77 | let status = group.status; 78 | let statusText = if (isDescribe) { 79 | ": " # printStatus(status); 80 | } else { 81 | let failed = status.failed; 82 | let passed = status.passed; 83 | let pending = status.pending; 84 | let skipped = status.skipped; 85 | switch (failed, passed, pending, skipped) { 86 | case (0, 0, 0, 0) { "" }; 87 | case (1, 0, 0, 0) { ": Failed" }; 88 | case (0, 1, 0, 0) { ": Passed" }; 89 | case (0, 0, 1, 0) { ": Pending" }; 90 | case (0, 0, 0, 1) { ": Skipped" }; 91 | case (_, _, _, _) { ":" # printStatus(status) }; 92 | }; 93 | }; 94 | Debug.print(newline # indent # group.name # statusText # "\n"); 95 | printGroups(group.groups, indent # " "); 96 | }; 97 | }; 98 | 99 | public func describe(name_ : Text, groups_ : [Group]) : Group { 100 | { 101 | name = name_; 102 | groups = groups_; 103 | status = emptyStatus; 104 | }; 105 | }; 106 | 107 | public func it(name_ : Text, passed_ : Bool) : Group { 108 | { 109 | name = name_; 110 | groups = []; 111 | status = { 112 | failed = if passed_ 0 else 1; 113 | passed = if passed_ 1 else 0; 114 | pending = 0; 115 | skipped = 0; 116 | }; 117 | }; 118 | }; 119 | 120 | public func itAsync(name_ : Text, passed_ : Bool) : async Group { 121 | { 122 | name = name_; 123 | groups = []; 124 | status = { 125 | failed = if passed_ 0 else 1; 126 | passed = if passed_ 1 else 0; 127 | pending = 0; 128 | skipped = 0; 129 | }; 130 | }; 131 | }; 132 | 133 | public let test = it; 134 | 135 | public func skip(name_ : Text, passed_ : Bool) : Group { 136 | { 137 | name = name_; 138 | groups = []; 139 | status = { 140 | failed = 0; 141 | passed = 0; 142 | pending = 0; 143 | skipped = 1; 144 | }; 145 | }; 146 | }; 147 | 148 | public func pending(name_ : Text) : Group { 149 | { 150 | name = name_; 151 | groups = []; 152 | status = { 153 | failed = 0; 154 | passed = 0; 155 | pending = 1; 156 | skipped = 0; 157 | }; 158 | }; 159 | }; 160 | 161 | public func assertTrue(x : Bool) : Bool { 162 | x == true; 163 | }; 164 | 165 | public func assertFalse(x : Bool) : Bool { 166 | x == false; 167 | }; 168 | 169 | public func assertAllTrue(xs : [Bool]) : Bool { 170 | var allTrue = true; 171 | for (val in xs.vals()) { 172 | if (val == false) { 173 | return false; 174 | }; 175 | allTrue := allTrue and val; 176 | }; 177 | allTrue; 178 | }; 179 | }; 180 | -------------------------------------------------------------------------------- /vessel.dhall: -------------------------------------------------------------------------------- 1 | { 2 | dependencies = [ "base", "array", "StableTrieMap", "StableBuffer", "itertools"], 3 | compiler = Some "0.7.0" 4 | } 5 | --------------------------------------------------------------------------------